<?php

/**
 * Base Custom Database Table Column Class.
 *
 * @package     Database
 * @subpackage  Column
 * @copyright   Copyright (c) 2021
 * @license     https://opensource.org/licenses/MIT MIT
 * @since       1.0.0
 */
namespace WP_Ultimo\Dependencies\BerlinDB\Database;

// Exit if accessed directly
\defined('ABSPATH') || exit;
/**
 * Base class used for each column for a custom table.
 *
 * @since 1.0.0
 *
 * @see Column::__construct() for accepted arguments.
 */
class Column extends Base
{
    /** Table Attributes ******************************************************/
    /**
     * Name for the database column.
     *
     * Required. Must contain lowercase alphabetical characters only. Use of any
     * other character (number, ascii, unicode, emoji, etc...) will result in
     * fatal application errors.
     *
     * @since 1.0.0
     * @var   string
     */
    public $name = '';
    /**
     * Type of database column.
     *
     * See: https://dev.mysql.com/doc/en/data-types.html
     *
     * @since 1.0.0
     * @var   string
     */
    public $type = '';
    /**
     * Length of database column.
     *
     * See: https://dev.mysql.com/doc/en/storage-requirements.html
     *
     * @since 1.0.0
     * @var   string
     */
    public $length = \false;
    /**
     * Is integer unsigned?
     *
     * See: https://dev.mysql.com/doc/en/numeric-type-overview.html
     *
     * @since 1.0.0
     * @var   bool
     */
    public $unsigned = \true;
    /**
     * Is integer filled with zeroes?
     *
     * See: https://dev.mysql.com/doc/en/numeric-type-overview.html
     *
     * @since 1.0.0
     * @var   bool
     */
    public $zerofill = \false;
    /**
     * Is data in a binary format?
     *
     * See: https://dev.mysql.com/doc/en/binary-varbinary.html
     *
     * @since 1.0.0
     * @var   bool
     */
    public $binary = \false;
    /**
     * Is null an allowed value?
     *
     * See: https://dev.mysql.com/doc/en/data-type-defaults.html
     *
     * @since 1.0.0
     * @var   bool
     */
    public $allow_null = \false;
    /**
     * Typically empty/null, or date value.
     *
     * See: https://dev.mysql.com/doc/en/data-type-defaults.html
     *
     * @since 1.0.0
     * @var   string
     */
    public $default = '';
    /**
     * auto_increment, etc...
     *
     * See: https://dev.mysql.com/doc/en/data-type-defaults.html
     *
     * @since 1.0.0
     * @var   string
     */
    public $extra = '';
    /**
     * Typically inherited from the database interface (wpdb).
     *
     * By default, this will use the globally available database encoding. You
     * most likely do not want to change this; if you do, you already know what
     * to do.
     *
     * See: https://dev.mysql.com/doc/mysql/en/charset-column.html
     *
     * @since 1.0.0
     * @var   string
     */
    public $encoding = '';
    /**
     * Typically inherited from the database interface (wpdb).
     *
     * By default, this will use the globally available database collation. You
     * most likely do not want to change this; if you do, you already know what
     * to do.
     *
     * See: https://dev.mysql.com/doc/mysql/en/charset-column.html
     *
     * @since 1.0.0
     * @var   string
     */
    public $collation = '';
    /**
     * Typically empty; probably ignore.
     *
     * By default, columns do not have comments. This is unused by any other
     * relative code, but you can include less than 1024 characters here.
     *
     * @since 1.0.0
     * @var   string
     */
    public $comment = '';
    /** Special Attributes ****************************************************/
    /**
     * Is this the primary column?
     *
     * By default, columns are not the primary column. This is used by the Query
     * class for several critical functions, including (but not limited to) the
     * cache key, meta-key relationships, auto-incrementing, etc...
     *
     * @since 1.0.0
     * @var   bool
     */
    public $primary = \false;
    /**
     * Is this the column used as a created date?
     *
     * By default, columns do not represent the date a value was first entered.
     * This is used by the Query class to set its value automatically to the
     * current datetime value immediately before insert.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $created = \false;
    /**
     * Is this the column used as a modified date?
     *
     * By default, columns do not represent the date a value was last changed.
     * This is used by the Query class to update its value automatically to the
     * current datetime value immediately before insert|update.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $modified = \false;
    /**
     * Is this the column used as a unique universal identifier?
     *
     * By default, columns are not UUIDs. This is used by the Query class to
     * generate a unique string that can be used to identify a row in a database
     * table, typically in such a way that is unrelated to the row data itself.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $uuid = \false;
    /** Query Attributes ******************************************************/
    /**
     * What is the string-replace pattern?
     *
     * By default, column patterns will be guessed based on their type. Set this
     * manually to `%s|%d|%f` only if you are doing something weird, or are
     * explicitly storing numeric values in text-based column types.
     *
     * @since 1.0.0
     * @var   string
     */
    public $pattern = '';
    /**
     * Is this column searchable?
     *
     * By default, columns are not searchable. When `true`, the Query class will
     * add this column to the results of search queries.
     *
     * Avoid setting to `true` on large blobs of text, unless you've optimized
     * your database server to accommodate these kinds of queries.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $searchable = \false;
    /**
     * Is this column a date?
     *
     * By default, columns do not support date queries. When `true`, the Query
     * class will accept complex statements to help narrow results down to
     * specific periods of time for values in this column.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $date_query = \false;
    /**
     * Is this column used in orderby?
     *
     * By default, columns are not sortable. This ensures that the database
     * table does not perform costly operations on unindexed columns or columns
     * of an inefficient type.
     *
     * You can safely turn this on for most numeric columns, indexed columns,
     * and text columns with intentionally limited lengths.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $sortable = \false;
    /**
     * Is __in supported?
     *
     * By default, columns support being queried using an `IN` statement. This
     * allows the Query class to retrieve rows that match your array of values.
     *
     * Consider setting this to `false` for longer text columns.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $in = \true;
    /**
     * Is __not_in supported?
     *
     * By default, columns support being queried using a `NOT IN` statement.
     * This allows the Query class to retrieve rows that do not match your array
     * of values.
     *
     * Consider setting this to `false` for longer text columns.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $not_in = \true;
    /** Cache Attributes ******************************************************/
    /**
     * Does this column have its own cache key?
     *
     * By default, only primary columns are used as cache keys. If this column
     * is unique, or is frequently used to get database results, you may want to
     * consider setting this to true.
     *
     * Use in conjunction with a database index for speedy queries.
     *
     * @since 1.0.0
     * @var   string
     */
    public $cache_key = \false;
    /** Action Attributes *****************************************************/
    /**
     * Does this column fire a transition action when it's value changes?
     *
     * By default, columns do not fire transition actions. In some cases, it may
     * be desirable to know when a database value changes, and what the old and
     * new values are when that happens.
     *
     * The Query class is responsible for triggering the event action.
     *
     * @since 1.0.0
     * @var   bool
     */
    public $transition = \false;
    /** Callback Attributes ***************************************************/
    /**
     * Maybe validate this data before it is written to the database.
     *
     * By default, column data is validated based on the type of column that it
     * is. You can set this to a callback function of your choice to override
     * the default validation behavior.
     *
     * @since 1.0.0
     * @var   string
     */
    public $validate = '';
    /**
     * Array of capabilities used to interface with this column.
     *
     * These are used by the Query class to allow and disallow CRUD access to
     * column data, typically based on roles or capabilities.
     *
     * @since 1.0.0
     * @var   array
     */
    public $caps = array();
    /**
     * Array of possible aliases this column can be referred to as.
     *
     * These are used by the Query class to allow for columns to be renamed
     * without requiring complex architectural backwards compatibility support.
     *
     * @since 1.0.0
     * @var   array
     */
    public $aliases = array();
    /**
     * Array of possible relationships this column has with columns in other
     * database tables.
     *
     * These are typically unenforced foreign keys, and are used by the Query
     * class to help prime related items.
     *
     * @since 1.0.0
     * @var   array
     */
    public $relationships = array();
    /** Methods ***************************************************************/
    /**
     * Sets up the order query, based on the query vars passed.
     *
     * @since 1.0.0
     *
     * @param string|array $args {
     *     Optional. Array or query string of order query parameters. Default empty.
     *
     *     @type string   $name           Name of database column
     *     @type string   $type           Type of database column
     *     @type int      $length         Length of database column
     *     @type bool     $unsigned       Is integer unsigned?
     *     @type bool     $zerofill       Is integer filled with zeroes?
     *     @type bool     $binary         Is data in a binary format?
     *     @type bool     $allow_null     Is null an allowed value?
     *     @type mixed    $default        Typically empty/null, or date value
     *     @type string   $extra          auto_increment, etc...
     *     @type string   $encoding       Typically inherited from wpdb
     *     @type string   $collation      Typically inherited from wpdb
     *     @type string   $comment        Typically empty
     *     @type bool     $pattern        What is the string-replace pattern?
     *     @type bool     $primary        Is this the primary column?
     *     @type bool     $created        Is this the column used as a created date?
     *     @type bool     $modified       Is this the column used as a modified date?
     *     @type bool     $uuid           Is this the column used as a universally unique identifier?
     *     @type bool     $searchable     Is this column searchable?
     *     @type bool     $sortable       Is this column used in orderby?
     *     @type bool     $date_query     Is this column a datetime?
     *     @type bool     $in             Is __in supported?
     *     @type bool     $not_in         Is __not_in supported?
     *     @type bool     $cache_key      Is this column queried independently?
     *     @type bool     $transition     Does this column transition between changes?
     *     @type string   $validate       A callback function used to validate on save.
     *     @type array    $caps           Array of capabilities to check.
     *     @type array    $aliases        Array of possible column name aliases.
     *     @type array    $relationships  Array of columns in other tables this column relates to.
     * }
     */
    public function __construct($args = array())
    {
        // Parse arguments
        $r = $this->parse_args($args);
        // Maybe set variables from arguments
        if (!empty($r)) {
            $this->set_vars($r);
        }
    }
    /** Argument Handlers *****************************************************/
    /**
     * Parse column arguments
     *
     * @since 1.0.0
     * @param array $args Default empty array.
     * @return array
     */
    private function parse_args($args = array())
    {
        // Parse arguments
        $r = wp_parse_args($args, array(
            // Table
            'name' => '',
            'type' => '',
            'length' => '',
            'unsigned' => \false,
            'zerofill' => \false,
            'binary' => \false,
            'allow_null' => \false,
            'default' => '',
            'extra' => '',
            'encoding' => $this->get_db()->charset,
            'collation' => $this->get_db()->collate,
            'comment' => '',
            // Query
            'pattern' => \false,
            'searchable' => \false,
            'sortable' => \false,
            'date_query' => \false,
            'transition' => \false,
            'in' => \true,
            'not_in' => \true,
            // Special
            'primary' => \false,
            'created' => \false,
            'modified' => \false,
            'uuid' => \false,
            // Cache
            'cache_key' => \false,
            // Validation
            'validate' => '',
            // Capabilities
            'caps' => array(),
            // Backwards Compatibility
            'aliases' => array(),
            // Column Relationships
            'relationships' => array(),
        ));
        // Force some arguments for special column types
        $r = $this->special_args($r);
        // Set the args before they are sanitized
        $this->set_vars($r);
        // Return array
        return $this->validate_args($r);
    }
    /**
     * Validate arguments after they are parsed.
     *
     * @since 1.0.0
     * @param array $args Default empty array.
     * @return array
     */
    private function validate_args($args = array())
    {
        // Sanitization callbacks
        $callbacks = array('name' => 'sanitize_key', 'type' => 'strtoupper', 'length' => 'intval', 'unsigned' => 'wp_validate_boolean', 'zerofill' => 'wp_validate_boolean', 'binary' => 'wp_validate_boolean', 'allow_null' => 'wp_validate_boolean', 'default' => array($this, 'sanitize_default'), 'extra' => 'wu_kses_data', 'encoding' => 'wu_kses_data', 'collation' => 'wu_kses_data', 'comment' => 'wu_kses_data', 'primary' => 'wp_validate_boolean', 'created' => 'wp_validate_boolean', 'modified' => 'wp_validate_boolean', 'uuid' => 'wp_validate_boolean', 'searchable' => 'wp_validate_boolean', 'sortable' => 'wp_validate_boolean', 'date_query' => 'wp_validate_boolean', 'transition' => 'wp_validate_boolean', 'in' => 'wp_validate_boolean', 'not_in' => 'wp_validate_boolean', 'cache_key' => 'wp_validate_boolean', 'pattern' => array($this, 'sanitize_pattern'), 'validate' => array($this, 'sanitize_validation'), 'caps' => array($this, 'sanitize_capabilities'), 'aliases' => array($this, 'sanitize_aliases'), 'relationships' => array($this, 'sanitize_relationships'));
        // Default args array
        $r = array();
        // Loop through and try to execute callbacks
        foreach ($args as $key => $value) {
            // Callback is callable
            if (isset($callbacks[$key]) && \is_callable($callbacks[$key])) {
                $r[$key] = \call_user_func($callbacks[$key], $value ?? "");
                // Callback is malformed so just let it through to avoid breakage
            } else {
                $r[$key] = $value;
            }
        }
        // Return sanitized arguments
        return $r;
    }
    /**
     * Force column arguments for special column types
     *
     * @since 1.0.0
     * @param array $args Default empty array.
     * @return array
     */
    private function special_args($args = array())
    {
        // Primary key columns are always used as cache keys
        if (!empty($args['primary'])) {
            $args['cache_key'] = \true;
            // All UUID columns need to follow a very specific pattern
        } elseif (!empty($args['uuid'])) {
            $args['name'] = 'uuid';
            $args['type'] = 'varchar';
            $args['length'] = '100';
            $args['in'] = \false;
            $args['not_in'] = \false;
            $args['searchable'] = \false;
            $args['sortable'] = \false;
        }
        // Return args
        return (array) $args;
    }
    /** Public Helpers ********************************************************/
    /**
     * Return if a column type is numeric or not.
     *
     * @since 1.0.0
     * @return bool
     */
    public function is_numeric()
    {
        return $this->is_type(array('tinyint', 'int', 'mediumint', 'bigint'));
    }
    /** Private Helpers *******************************************************/
    /**
     * Return if this column is of a certain type.
     *
     * @since 1.0.0
     * @param mixed $type Default empty string. The type to check. Also accepts an array.
     * @return bool True if of type, False if not
     */
    private function is_type($type = '')
    {
        // If string, cast to array
        if (\is_string($type)) {
            $type = (array) $type;
        }
        // Make them lowercase
        $types = \array_map('strtolower', $type);
        // Return if match or not
        return (bool) \in_array(\strtolower($this->type), $types, \true);
    }
    /** Private Sanitizers ****************************************************/
    /**
     * Sanitize capabilities array
     *
     * @since 1.0.0
     * @param array $caps Default empty array.
     * @return array
     */
    private function sanitize_capabilities($caps = array())
    {
        return wp_parse_args($caps, array('select' => 'exist', 'insert' => 'exist', 'update' => 'exist', 'delete' => 'exist'));
    }
    /**
     * Sanitize aliases array using `sanitize_key()`
     *
     * @since 1.0.0
     * @param array $aliases Default empty array.
     * @return array
     */
    private function sanitize_aliases($aliases = array())
    {
        return \array_map('sanitize_key', $aliases);
    }
    /**
     * Sanitize relationships array
     *
     * @todo
     * @since 1.0.0
     * @param array $relationships Default empty array.
     * @return array
     */
    private function sanitize_relationships($relationships = array())
    {
        return \array_filter($relationships);
    }
    /**
     * Sanitize the default value
     *
     * @since 1.0.0
     * @param string $default
     * @return string|null
     */
    private function sanitize_default($default = '')
    {
        // Null
        if (\true === $this->allow_null && \is_null($default)) {
            return null;
            // String
        } elseif (\is_string($default)) {
            return wu_kses_data($default);
            // Integer
        } elseif ($this->is_numeric()) {
            return (int) $default;
        }
        // @todo datetime, decimal, and other column types
        // Unknown, so return the default's default
        return '';
    }
    /**
     * Sanitize the pattern
     *
     * @since 1.0.0
     * @param string $pattern
     * @return string
     */
    private function sanitize_pattern($pattern = '%s')
    {
        // Allowed patterns
        $allowed_patterns = array('%s', '%d', '%f');
        // Return pattern if allowed
        if (\in_array($pattern, $allowed_patterns, \true)) {
            return $pattern;
        }
        // Fallback to digit or string
        return $this->is_numeric() ? '%d' : '%s';
    }
    /**
     * Sanitize the validation callback
     *
     * @since 1.0.0
     * @param string $callback Default empty string. A callable PHP function name or method
     * @return string The most appropriate callback function for the value
     */
    private function sanitize_validation($callback = '')
    {
        // Return callback if it's callable
        if (\is_callable($callback)) {
            return $callback;
        }
        // UUID special column
        if (\true === $this->uuid) {
            $callback = array($this, 'validate_uuid');
            // Datetime fallback
        } elseif ($this->is_type('datetime')) {
            $callback = array($this, 'validate_datetime');
            // Decimal fallback
        } elseif ($this->is_type('decimal')) {
            $callback = array($this, 'validate_decimal');
            // Intval fallback
        } elseif ($this->is_numeric()) {
            $callback = 'intval';
        }
        // Return the callback
        return $callback;
    }
    /** Public Validators *****************************************************/
    /**
     * Fallback to validate a datetime value if no other is set.
     *
     * This assumes NO_ZERO_DATES is off or overridden.
     *
     * If MySQL drops support for zero dates, this method will need to be
     * updated to support different default values based on the environment.
     *
     * @since 1.0.0
     * @param string $value Default ''. A datetime value that needs validating
     * @return string A valid datetime value
     */
    public function validate_datetime($value = '')
    {
        // Handle "empty" values
        if (empty($value) || '0000-00-00 00:00:00' === $value) {
            $value = !empty($this->default) ? $this->default : '';
            // Convert to MySQL datetime format via gmdate() && strtotime
        } elseif (\function_exists('gmdate')) {
            $value = \gmdate('Y-m-d H:i:s', \strtotime($value));
        }
        // Return the validated value
        return $value;
    }
    /**
     * Validate a decimal
     *
     * (Recommended decimal column length is '18,9'.)
     *
     * This is used to validate a mixed value before it is saved into a decimal
     * column in a database table.
     *
     * Uses number_format() which does rounding to the last decimal if your
     * value is longer than specified.
     *
     * @since 1.0.0
     * @param mixed $value    Default empty string. The decimal value to validate
     * @param int   $decimals Default 9. The number of decimal points to accept
     * @return float
     */
    public function validate_decimal($value = 0, $decimals = 9)
    {
        // Protect against non-numeric values
        if (!\is_numeric($value)) {
            $value = 0;
        }
        // Protect against non-numeric decimals
        if (!\is_numeric($decimals)) {
            $decimals = 9;
        }
        // Is the value negative?
        $negative_exponent = $value < 0 ? -1 : 1;
        // Only numbers and period
        $value = \preg_replace('/[^0-9\\.]/', '', (string) $value);
        // Format to number of decimals, and cast as float
        $formatted = \number_format($value, $decimals, '.', '');
        // Adjust for negative values
        $retval = $formatted * $negative_exponent;
        // Return
        return $retval;
    }
    /**
     * Validate a UUID.
     *
     * This uses the v4 algorithm to generate a UUID that is used to uniquely
     * and universally identify a given database row without any direct
     * connection or correlation to the data in that row.
     *
     * From http://php.net/manual/en/function.uniqid.php#94959
     *
     * @since 1.0.0
     * @param string $value The UUID value (empty on insert, string on update)
     * @return string Generated UUID.
     */
    public function validate_uuid($value = '')
    {
        // Default URN UUID prefix
        $prefix = 'urn:uuid:';
        // Bail if not empty and correctly prefixed
        // (UUIDs should _never_ change once they are set)
        if (!empty($value) && 0 === \strpos($value, $prefix)) {
            return $value;
        }
        // Put the pieces together
        $value = \sprintf(
            "{$prefix}%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
            // 32 bits for "time_low"
            \mt_rand(0, 0xffff),
            \mt_rand(0, 0xffff),
            // 16 bits for "time_mid"
            \mt_rand(0, 0xffff),
            // 16 bits for "time_hi_and_version",
            // four most significant bits holds version number 4
            \mt_rand(0, 0xfff) | 0x4000,
            // 16 bits, 8 bits for "clk_seq_hi_res",
            // 8 bits for "clk_seq_low",
            // two most significant bits holds zero and one for variant DCE1.1
            \mt_rand(0, 0x3fff) | 0x8000,
            // 48 bits for "node"
            \mt_rand(0, 0xffff),
            \mt_rand(0, 0xffff),
            \mt_rand(0, 0xffff)
        );
        // Return the new UUID
        return $value;
    }
    /** Table Helpers *********************************************************/
    /**
     * Return a string representation of what this column's properties look like
     * in a MySQL.
     *
     * @todo
     * @since 1.0.0
     * @return string
     */
    public function get_create_string()
    {
        // Default return val
        $retval = '';
        // Bail if no name
        if (!empty($this->name)) {
            $retval .= $this->name;
        }
        // Type
        if (!empty($this->type)) {
            $retval .= " {$this->type}";
        }
        // Length
        if (!empty($this->length)) {
            $retval .= '(' . $this->length . ')';
        }
        // Unsigned
        if (!empty($this->unsigned)) {
            $retval .= " unsigned";
        }
        // Zerofill
        if (!empty($this->zerofill)) {
            // TBD
        }
        // Binary
        if (!empty($this->binary)) {
            // TBD
        }
        // Allow null
        if (!empty($this->allow_null)) {
            $retval .= " NOT NULL ";
        }
        // Default
        if (!empty($this->default)) {
            $retval .= " default '{$this->default}'";
            // A literal false means no default value
        } elseif (\false !== $this->default) {
            // Numeric
            if ($this->is_numeric()) {
                $retval .= " default '0'";
            } elseif ($this->is_type('datetime')) {
                $retval .= " default '0000-00-00 00:00:00'";
            } else {
                $retval .= " default ''";
            }
        }
        // Extra
        if (!empty($this->extra)) {
            $retval .= " {$this->extra}";
        }
        // Encoding
        if (!empty($this->encoding)) {
        } else {
        }
        // Collation
        if (!empty($this->collation)) {
        } else {
        }
        // Return the create string
        return $retval;
    }
}
