<?php


namespace PhpMyAdmin\SqlParser\Statements;

use PhpMyAdmin\SqlParser\Components\ArrayObj;
use PhpMyAdmin\SqlParser\Components\CreateDefinition;
use PhpMyAdmin\SqlParser\Components\DataType;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\ParameterDefinition;
use PhpMyAdmin\SqlParser\Components\PartitionDefinition;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;

/**
 * `CREATE` statement.
 *
 * @category   Statements
 *
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
 */
class CreateStatement extends Statement
{
    public static $OPTIONS = array(
        'TEMPORARY' => 1,

        'OR REPLACE' => 2,
        'ALGORITHM' => array(
            3,
            'var=',
        ),
        'DEFINER' => array(
            4,
            'expr=',
        ),
        'SQL SECURITY' => array(
            5,
            'var',
        ),

        'DATABASE' => 6,
        'EVENT' => 6,
        'FUNCTION' => 6,
        'INDEX' => 6,
        'UNIQUE INDEX' => 6,
        'FULLTEXT INDEX' => 6,
        'SPATIAL INDEX' => 6,
        'PROCEDURE' => 6,
        'SERVER' => 6,
        'TABLE' => 6,
        'TABLESPACE' => 6,
        'TRIGGER' => 6,
        'USER' => 6,
        'VIEW' => 6,
        'SCHEMA' => 6,

        'IF NOT EXISTS' => 7
    );

    public static $DB_OPTIONS = array(
        'CHARACTER SET' => array(
            1,
            'var=',
        ),
        'CHARSET' => array(
            1,
            'var=',
        ),
        'DEFAULT CHARACTER SET' => array(
            1,
            'var=',
        ),
        'DEFAULT CHARSET' => array(
            1,
            'var=',
        ),
        'DEFAULT COLLATE' => array(
            2,
            'var=',
        ),
        'COLLATE' => array(
            2,
            'var=',
        )
    );

    public static $TABLE_OPTIONS = array(
        'ENGINE' => array(
            1,
            'var=',
        ),
        'AUTO_INCREMENT' => array(
            2,
            'var=',
        ),
        'AVG_ROW_LENGTH' => array(
            3,
            'var',
        ),
        'CHARACTER SET' => array(
            4,
            'var=',
        ),
        'CHARSET' => array(
            4,
            'var=',
        ),
        'DEFAULT CHARACTER SET' => array(
            4,
            'var=',
        ),
        'DEFAULT CHARSET' => array(
            4,
            'var=',
        ),
        'CHECKSUM' => array(
            5,
            'var',
        ),
        'DEFAULT COLLATE' => array(
            6,
            'var=',
        ),
        'COLLATE' => array(
            6,
            'var=',
        ),
        'COMMENT' => array(
            7,
            'var=',
        ),
        'CONNECTION' => array(
            8,
            'var',
        ),
        'DATA DIRECTORY' => array(
            9,
            'var',
        ),
        'DELAY_KEY_WRITE' => array(
            10,
            'var',
        ),
        'INDEX DIRECTORY' => array(
            11,
            'var',
        ),
        'INSERT_METHOD' => array(
            12,
            'var',
        ),
        'KEY_BLOCK_SIZE' => array(
            13,
            'var',
        ),
        'MAX_ROWS' => array(
            14,
            'var',
        ),
        'MIN_ROWS' => array(
            15,
            'var',
        ),
        'PACK_KEYS' => array(
            16,
            'var',
        ),
        'PASSWORD' => array(
            17,
            'var',
        ),
        'ROW_FORMAT' => array(
            18,
            'var',
        ),
        'TABLESPACE' => array(
            19,
            'var',
        ),
        'STORAGE' => array(
            20,
            'var',
        ),
        'UNION' => array(
            21,
            'var',
        )
    );

    public static $FUNC_OPTIONS = array(
        'NOT' =>  array(
            2,
            'var',
        ),
        'FUNCTION' =>  array(
            3,
            'var=',
        ),
        'PROCEDURE' =>  array(
            3,
            'var=',
        ),
        'CONTAINS' =>  array(
            4,
            'expr',
        ),
        'NO' =>  array(
            4,
            'var',
        ),
        'READS' =>  array(
            4,
            'var',
        ),
        'MODIFIES' =>  array(
            4,
            'expr',
        ),
        'SQL SECURITY' =>  array(
            6,
            'var',
        ),
        'LANGUAGE' =>  array(
            7,
            'var',
        ),
        'COMMENT' =>  array(
            8,
            'var',
        ),

        'CREATE' => 1,
        'DETERMINISTIC' => 2,
        'DATA' =>  5,
    );

    public static $TRIGGER_OPTIONS = array(
        'BEFORE' => 1,
        'AFTER' => 1,
        'INSERT' => 2,
        'UPDATE' => 2,
        'DELETE' => 2
    );

    public $name;

    public $entityOptions;

    public $fields;

    public $select;

    public $like;

    public $partitionBy;

    public $partitionsNum;

    public $subpartitionBy;

    public $subpartitionsNum;

    public $partitions;

    public $table;

    public $return;

    public $parameters;

    public $body = array();

    public function build()
    {
        $fields = '';
        if (! empty($this->fields)) {
            if (is_array($this->fields)) {
                $fields = CreateDefinition::build($this->fields) . ' ';
            } elseif ($this->fields instanceof ArrayObj) {
                $fields = ArrayObj::build($this->fields);
            }
        }
        if ($this->options->has('DATABASE') || $this->options->has('SCHEMA')) {
            return 'CREATE '
                . OptionsArray::build($this->options) . ' '
                . Expression::build($this->name) . ' '
                . OptionsArray::build($this->entityOptions);
        } elseif ($this->options->has('TABLE')) {
            if (! is_null($this->select)) {
                return 'CREATE '
                    . OptionsArray::build($this->options) . ' '
                    . Expression::build($this->name) . ' '
                    . $this->select->build();
            } elseif (! is_null($this->like)) {
                return 'CREATE '
                    . OptionsArray::build($this->options) . ' '
                    . Expression::build($this->name) . ' LIKE '
                    . Expression::build($this->like);
            } else {
                $partition = '';

                if (! empty($this->partitionBy)) {
                    $partition .= "\nPARTITION BY " . $this->partitionBy;
                }
                if (! empty($this->partitionsNum)) {
                    $partition .= "\nPARTITIONS " . $this->partitionsNum;
                }
                if (! empty($this->subpartitionBy)) {
                    $partition .= "\nSUBPARTITION BY " . $this->subpartitionBy;
                }
                if (! empty($this->subpartitionsNum)) {
                    $partition .= "\nSUBPARTITIONS " . $this->subpartitionsNum;
                }
                if (! empty($this->partitions)) {
                    $partition .= "\n" . PartitionDefinition::build($this->partitions);
                }

                return 'CREATE '
                    . OptionsArray::build($this->options) . ' '
                    . Expression::build($this->name) . ' '
                    . $fields
                    . OptionsArray::build($this->entityOptions)
                    . $partition;
            }
        } elseif ($this->options->has('VIEW')) {
            return 'CREATE '
                . OptionsArray::build($this->options) . ' '
                . Expression::build($this->name) . ' '
                . $fields . ' AS ' . ($this->select ? $this->select->build() : '') . (! empty($this->body) ? TokensList::build($this->body) : '') . ' '
                . OptionsArray::build($this->entityOptions);
        } elseif ($this->options->has('TRIGGER')) {
            return 'CREATE '
                . OptionsArray::build($this->options) . ' '
                . Expression::build($this->name) . ' '
                . OptionsArray::build($this->entityOptions) . ' '
                . 'ON ' . Expression::build($this->table) . ' '
                . 'FOR EACH ROW ' . TokensList::build($this->body);
        } elseif ($this->options->has('PROCEDURE')
            || $this->options->has('FUNCTION')
        ) {
            $tmp = '';
            if ($this->options->has('FUNCTION')) {
                $tmp = 'RETURNS ' . DataType::build($this->return);
            }

            return 'CREATE '
                . OptionsArray::build($this->options) . ' '
                . Expression::build($this->name) . ' '
                . ParameterDefinition::build($this->parameters) . ' '
                . $tmp . ' ' .  OptionsArray::build($this->entityOptions) . ' '
                . TokensList::build($this->body);
        }

        return 'CREATE '
            . OptionsArray::build($this->options) . ' '
            . Expression::build($this->name) . ' '
            . TokensList::build($this->body);
    }

    public function parse(Parser $parser, TokensList $list)
    {
        ++$list->idx;

        $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
        ++$list->idx;

        $isDatabase = $this->options->has('DATABASE') || $this->options->has('SCHEMA');
        $fieldName = $isDatabase ? 'database' : 'table';

        $this->name = Expression::parse(
            $parser,
            $list,
            array(
                'parseField' => $fieldName,
                'breakOnAlias' => true
            )
        );

        if (! isset($this->name) || ($this->name === '')) {
            $parser->error(
                'The name of the entity was expected.',
                $list->tokens[$list->idx]
            );
        } else {
            ++$list->idx;
        }

        $token = $list->tokens[$list->idx];
        $nextidx = $list->idx + 1;
        while ($nextidx < $list->count && $list->tokens[$nextidx]->type === Token::TYPE_WHITESPACE) {
            ++$nextidx;
        }

        if ($isDatabase) {
            $this->entityOptions = OptionsArray::parse(
                $parser,
                $list,
                static::$DB_OPTIONS
            );
        } elseif ($this->options->has('TABLE')) {
            if (($token->type === Token::TYPE_KEYWORD)
             && ($token->keyword === 'SELECT')) {
                $this->select = new SelectStatement($parser, $list);
            } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'AS')
                && ($list->tokens[$nextidx]->type === Token::TYPE_KEYWORD)
                && ($list->tokens[$nextidx]->value === 'SELECT')) {
                $list->idx = $nextidx;
                $this->select = new SelectStatement($parser, $list);
            } elseif ($token->type === Token::TYPE_KEYWORD
                && $token->keyword === 'LIKE') {
                $list->idx = $nextidx;
                $this->like = Expression::parse(
                    $parser,
                    $list,
                    array(
                        'parseField' => 'table',
                        'breakOnAlias' => true
                    )
                );
                if (is_null($this->like)) {
                    $parser->error(
                        'A table name was expected.',
                        $list->tokens[$list->idx]
                    );
                }
            } else {
                $this->fields = CreateDefinition::parse($parser, $list);
                if (empty($this->fields)) {
                    $parser->error(
                        'At least one column definition was expected.',
                        $list->tokens[$list->idx]
                    );
                }
                ++$list->idx;

                $this->entityOptions = OptionsArray::parse(
                    $parser,
                    $list,
                    static::$TABLE_OPTIONS
                );

                $field = null;

                $brackets = false;

                for (; $list->idx < $list->count; ++$list->idx) {
                    $token = $list->tokens[$list->idx];

                    if ($token->type === Token::TYPE_DELIMITER) {
                        break;
                    }

                    if ($token->type === Token::TYPE_COMMENT) {
                        continue;
                    }

                    if (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITION BY')) {
                        $field = 'partitionBy';
                        $brackets = false;
                    } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'SUBPARTITION BY')) {
                        $field = 'subpartitionBy';
                        $brackets = false;
                    } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITIONS')) {
                        $token = $list->getNextOfType(Token::TYPE_NUMBER);
                        --$list->idx;
                        $this->partitionsNum = $token->value;
                    } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'SUBPARTITIONS')) {
                        $token = $list->getNextOfType(Token::TYPE_NUMBER);
                        --$list->idx;
                        $this->subpartitionsNum = $token->value;
                    } elseif (! empty($field)) {

                        if ($token->type === Token::TYPE_OPERATOR) {
                            if ($token->value === '(') {
                                $brackets = $brackets + 1;
                            } elseif ($token->value === ')') {
                                --$brackets;
                            }
                        }

                        $this->$field .= ($token->type === Token::TYPE_WHITESPACE) ? ' ' : $token->token;

                        if ($brackets === 0) {
                            $this->$field = trim($this->$field);
                            $field = null;
                        }
                    } elseif (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
                        if (! empty($this->partitionBy)) {
                            $this->partitions = ArrayObj::parse(
                                $parser,
                                $list,
                                array(
                                    'type' => 'PhpMyAdmin\\SqlParser\\Components\\PartitionDefinition'
                                )
                            );
                        }
                        break;
                    }
                }
            }
        } elseif ($this->options->has('PROCEDURE')
            || $this->options->has('FUNCTION')
        ) {
            $this->parameters = ParameterDefinition::parse($parser, $list);
            if ($this->options->has('FUNCTION')) {
                $prev_token = $token;
                $token = $list->getNextOfType(Token::TYPE_KEYWORD);
                if (is_null($token) || $token->keyword !== 'RETURNS') {
                    $parser->error(
                        'A "RETURNS" keyword was expected.',
                        is_null($token) ? $prev_token : $token
                    );
                } else {
                    ++$list->idx;
                    $this->return = DataType::parse(
                        $parser,
                        $list
                    );
                }
            }
            ++$list->idx;

            $this->entityOptions = OptionsArray::parse(
                $parser,
                $list,
                static::$FUNC_OPTIONS
            );
            ++$list->idx;

            for (; $list->idx < $list->count; ++$list->idx) {
                $token = $list->tokens[$list->idx];
                $this->body[] = $token;
            }
        } elseif ($this->options->has('VIEW')) {
            $token = $list->getNext();

            if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
                --$list->idx;
                $this->fields = ArrayObj::parse($parser, $list);
                ++$list->idx;
                $list->getNext();
            }

            if (
                $token->type === Token::TYPE_KEYWORD
                && $token->keyword === 'AS'
                && $list->tokens[$nextidx]->type === Token::TYPE_KEYWORD
                && $list->tokens[$nextidx]->value === 'SELECT'
            ) {
                $list->idx = $nextidx;
                $this->select = new SelectStatement($parser, $list);
            }
            for (; $list->idx < $list->count; ++$list->idx) {
                $token = $list->tokens[$list->idx];
                if ($token->type === Token::TYPE_DELIMITER) {
                    break;
                }
                $this->body[] = $token;
            }
        } elseif ($this->options->has('TRIGGER')) {
            $this->entityOptions = OptionsArray::parse(
                $parser,
                $list,
                static::$TRIGGER_OPTIONS
            );
            ++$list->idx;

            $list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ON');
            ++$list->idx;

            $this->table = Expression::parse(
                $parser,
                $list,
                array(
                    'parseField' => 'table',
                    'breakOnAlias' => true
                )
            );
            ++$list->idx;

            $list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'FOR EACH ROW');
            ++$list->idx;

            for (; $list->idx < $list->count; ++$list->idx) {
                $token = $list->tokens[$list->idx];
                $this->body[] = $token;
            }
        } else {
            for (; $list->idx < $list->count; ++$list->idx) {
                $token = $list->tokens[$list->idx];
                if ($token->type === Token::TYPE_DELIMITER) {
                    break;
                }
                $this->body[] = $token;
            }
        }
    }
}
