<?php
namespace Envms\FluentPDO\Queries;

use Envms\FluentPDO\{Query,Literal};

/** INSERT query builder
 */
class Insert extends Base
{

    /** @var array */
    private $columns = array();

    /** @var array */
    private $firstValue = array();

    /** @var bool */
    private $ignore = false;
    /** @var bool */
    private $delayed = false;

    /**
     * InsertQuery constructor.
     *
     * @param Query     $fluent
     * @param string    $table
     * @param           $values
     */
    public function __construct(Query $fluent, $table, $values) {
        $clauses = array(
            'INSERT INTO'             => array($this, 'getClauseInsertInto'),
            'VALUES'                  => array($this, 'getClauseValues'),
            'ON DUPLICATE KEY UPDATE' => array($this, 'getClauseOnDuplicateKeyUpdate'),
        );
        parent::__construct($fluent, $clauses);

        $this->statements['INSERT INTO'] = $table;
        $this->values($values);
    }

    /**
     * Execute insert query
     * 
     * @param mixed $sequence
     *
     * @return integer last inserted id or false
     */
    public function execute($sequence = null) {
        $result = parent::execute();
        if ($result) {
            return $this->getPDO()->lastInsertId($sequence);
        }

        return false;
    }

    /**
     * Add ON DUPLICATE KEY UPDATE
     *
     * @param array $values
     *
     * @return Insert
     */
    public function onDuplicateKeyUpdate($values) {
        $this->statements['ON DUPLICATE KEY UPDATE'] = array_merge(
            $this->statements['ON DUPLICATE KEY UPDATE'], $values
        );

        return $this;
    }

    /**
     * Add VALUES
     *
     * @param $values
     *
     * @return Insert
     * @throws \Exception
     */
    public function values($values) {
        if (!is_array($values)) {
            throw new \Exception('Param VALUES for INSERT query must be array');
        }
        $first = current($values);
        if (is_string(key($values))) {
            // is one row array
            $this->addOneValue($values);
        } elseif (is_array($first) && is_string(key($first))) {
            // this is multi values
            foreach ($values as $oneValue) {
                $this->addOneValue($oneValue);
            }
        }

        return $this;
    }

    /**
     * Force insert operation to fail silently
     *
     * @return Insert
     */
    public function ignore() {
        $this->ignore = true;

        return $this;
    }

    /** Force insert operation delay support
     *
     * @return Insert
     */
    public function delayed() {
        $this->delayed = true;

        return $this;
    }

    /**
     * @return string
     */
    protected function getClauseInsertInto() {
        return 'INSERT' . ($this->ignore ? " IGNORE" : '') . ($this->delayed ? " DELAYED" : '') . ' INTO ' . $this->statements['INSERT INTO'];
    }

    /**
     * @param $param
     *
     * @return string
     */
    protected function parameterGetValue($param) {
        return $param instanceof Literal ? (string)$param : '?';
    }

    /**
     * @return string
     */
    protected function getClauseValues() {
        $valuesArray = array();
        foreach ($this->statements['VALUES'] as $rows) {
            // literals should not be parametrized.
            // They are commonly used to call engine functions or literals.
            // Eg: NOW(), CURRENT_TIMESTAMP etc
            $placeholders  = array_map(array($this, 'parameterGetValue'), $rows);
            $valuesArray[] = '(' . implode(', ', $placeholders) . ')';
        }

        $columns = implode(', ', $this->columns);
        $values  = implode(', ', $valuesArray);

        return " ($columns) VALUES $values";
    }

    /**
     * Removes all Literal instances from the argument
     * since they are not to be used as PDO parameters but rather injected directly into the query
     *
     * @param $statements
     *
     * @return array
     */
    protected function filterLiterals($statements) {
        $f = function ($item) {
            return !$item instanceof Literal;
        };

        return array_map(function ($item) use ($f) {
            if (is_array($item)) {
                return array_filter($item, $f);
            }

            return $item;
        }, array_filter($statements, $f));
    }

    /**
     * @return array
     */
    protected function buildParameters() {
        $this->parameters = array_merge(
            $this->filterLiterals($this->statements['VALUES']),
            $this->filterLiterals($this->statements['ON DUPLICATE KEY UPDATE'])
        );

        return parent::buildParameters();
    }

    /**
     * @return string
     */
    protected function getClauseOnDuplicateKeyUpdate() {
        $result = array();
        foreach ($this->statements['ON DUPLICATE KEY UPDATE'] as $key => $value) {
            $result[] = "$key = " . $this->parameterGetValue($value);
        }

        return ' ON DUPLICATE KEY UPDATE ' . implode(', ', $result);
    }

    /**
     * @param array $oneValue
     *
     * @throws \Exception
     */
    private function addOneValue($oneValue) {
        // check if all $keys are strings
        foreach ($oneValue as $key => $value) {
            if (!is_string($key)) {
                throw new \Exception('INSERT query: All keys of value array have to be strings.');
            }
        }
        if (!$this->firstValue) {
            $this->firstValue = $oneValue;
        }
        if (!$this->columns) {
            $this->columns = array_keys($oneValue);
        }
        if ($this->columns != array_keys($oneValue)) {
            throw new \Exception('INSERT query: All VALUES have to same keys (columns).');
        }
        $this->statements['VALUES'][] = $oneValue;
    }

}
