<?php
namespace Migrations\Util;

use Cake\Collection\Collection;
use Cake\Utility\Hash;
use ReflectionClass;

/**
 * Utility class used to parse arguments passed to a ``bake migration`` class
 */
class ColumnParser
{

    /**
     * Regex used to parse the column definition passed through the shell
     *
     * @var string
     */
    protected $regexpParseColumn = '/
        ^
        (\w+)
        (?::(\w+\??
            (?:\[
                (?:[0-9]|[1-9][0-9]+)
                (?:,(?:[0-9]|[1-9][0-9]+))?
            \])?
        ))?
        (?::(\w+))?
        (?::(\w+))?
        $
        /x';

    /**
     * Regex used to parse the field type and length
     *
     * @var string
     */
    protected $regexpParseField = '/(\w+\??)\[([0-9,]+)\]/';

    /**
     * Parses a list of arguments into an array of fields
     *
     * @param array $arguments A list of arguments being parsed
     * @return array
     */
    public function parseFields($arguments)
    {
        $fields = [];
        $arguments = $this->validArguments($arguments);
        foreach ($arguments as $field) {
            preg_match($this->regexpParseColumn, $field, $matches);
            $field = $matches[1];
            $type = Hash::get($matches, 2);
            $indexType = Hash::get($matches, 3);

            $typeIsPk = in_array($type, ['primary', 'primary_key']);
            $isPrimaryKey = false;
            if ($typeIsPk || in_array($indexType, ['primary', 'primary_key'])) {
                $isPrimaryKey = true;

                if ($typeIsPk) {
                    $type = 'primary';
                }
            }
            $nullable = (bool)strpos($type, '?');
            $type = $nullable ? str_replace('?', '', $type) : $type;

            list($type, $length) = $this->getTypeAndLength($field, $type);
            $fields[$field] = [
                'columnType' => $type,
                'options' => [
                    'null' => $nullable,
                    'default' => null,
                ],
            ];

            if ($length !== null) {
                if (is_array($length)) {
                    list($fields[$field]['options']['precision'], $fields[$field]['options']['scale']) = $length;
                } else {
                    $fields[$field]['options']['limit'] = $length;
                }
            }

            if ($isPrimaryKey === true && $type === 'integer') {
                $fields[$field]['options']['autoIncrement'] = true;
            }
        }

        return $fields;
    }

    /**
     * Parses a list of arguments into an array of indexes
     *
     * @param array $arguments A list of arguments being parsed
     * @return array
     */
    public function parseIndexes($arguments)
    {
        $indexes = [];
        $arguments = $this->validArguments($arguments);
        foreach ($arguments as $field) {
            preg_match($this->regexpParseColumn, $field, $matches);
            $field = $matches[1];
            $type = Hash::get($matches, 2);
            $indexType = Hash::get($matches, 3);
            $indexName = Hash::get($matches, 4);

            if (
                in_array($type, ['primary', 'primary_key']) ||
                in_array($indexType, ['primary', 'primary_key']) ||
                $indexType === null
            ) {
                continue;
            }

            $indexUnique = false;
            if ($indexType === 'unique') {
                $indexUnique = true;
            }

            $indexName = $this->getIndexName($field, $indexType, $indexName, $indexUnique);

            if (empty($indexes[$indexName])) {
                $indexes[$indexName] = [
                    'columns' => [],
                    'options' => [
                        'unique' => $indexUnique,
                        'name' => $indexName,
                    ],
                ];
            }

            $indexes[$indexName]['columns'][] = $field;
        }

        return $indexes;
    }

    /**
     * Parses a list of arguments into an array of fields composing the primary key
     * of the table
     *
     * @param array $arguments A list of arguments being parsed
     * @return array
     */
    public function parsePrimaryKey($arguments)
    {
        $primaryKey = [];
        $arguments = $this->validArguments($arguments);
        foreach ($arguments as $field) {
            preg_match($this->regexpParseColumn, $field, $matches);
            $field = $matches[1];
            $type = Hash::get($matches, 2);
            $indexType = Hash::get($matches, 3);

            if (in_array($type, ['primary', 'primary_key']) || in_array($indexType, ['primary', 'primary_key'])) {
                $primaryKey[] = $field;
            }
        }

        return $primaryKey;
    }

    /**
     * Returns a list of only valid arguments
     *
     * @param array $arguments A list of arguments
     * @return array
     */
    public function validArguments($arguments)
    {
        $collection = new Collection($arguments);

        return $collection->filter(function ($value, $field) {
            return preg_match($this->regexpParseColumn, $field);
        })->toArray();
    }

    /**
     * Get the type and length of a field based on the field and the type passed
     *
     * @param string $field Name of field
     * @param string $type User-specified type
     * @return array First value is the field type, second value is the field length. If no length
     * can be extracted, null is returned for the second value
     */
    public function getTypeAndLength($field, $type)
    {
        if (preg_match($this->regexpParseField, $type, $matches)) {
            if (strpos($matches[2], ',') !== false) {
                $matches[2] = explode(',', $matches[2]);
            }

            return [$matches[1], $matches[2]];
        }

        $fieldType = $this->getType($field, $type);
        $length = $this->getLength($fieldType);

        return [$fieldType, $length];
    }

    /**
     * Retrieves a type that should be used for a specific field
     *
     * @param string $field Name of field
     * @param string $type User-specified type
     * @return string
     */
    public function getType($field, $type)
    {
        $reflector = new ReflectionClass('Phinx\Db\Adapter\AdapterInterface');
        $collection = new Collection($reflector->getConstants());

        $validTypes = $collection->filter(function ($value, $constant) {
            return substr($constant, 0, strlen('PHINX_TYPE_')) === 'PHINX_TYPE_';
        })->toArray();
        $fieldType = $type;
        if ($type === null || !in_array($type, $validTypes)) {
            if ($type === 'primary') {
                $fieldType = 'integer';
            } elseif ($field === 'id') {
                $fieldType = 'integer';
            } elseif (in_array($field, ['created', 'modified', 'updated']) || substr($field, -3) === '_at') {
                $fieldType = 'datetime';
            } elseif (in_array($field, ['latitude', 'longitude'])) {
                $fieldType = 'decimal';
            } else {
                $fieldType = 'string';
            }
        }

        return $fieldType;
    }

    /**
     * Returns the default length to be used for a given fie
     *
     * @param string $type User-specified type
     * @return int
     */
    public function getLength($type)
    {
        $length = null;
        if ($type === 'string') {
            $length = 255;
        } elseif ($type === 'integer') {
            $length = 11;
        } elseif ($type === 'biginteger') {
            $length = 20;
        } elseif ($type === 'decimal') {
            $length = [10, 6];
        }

        return $length;
    }

    /**
     * Returns the default length to be used for a given fie
     *
     * @param string $field Name of field
     * @param string $indexType Type of index
     * @param string $indexName Name of index
     * @param bool $indexUnique Whether this is a unique index or not
     * @return string
     */
    public function getIndexName($field, $indexType, $indexName, $indexUnique)
    {
        if (empty($indexName)) {
            $indexName = strtoupper('BY_' . $field);
            if ($indexType === 'primary') {
                $indexName = 'PRIMARY';
            } elseif ($indexUnique) {
                $indexName = strtoupper('UNIQUE_' . $field);
            }
        }

        return $indexName;
    }
}
