<?php

/**
 * Questo file è parte di Pongho.
 *
 * @author    Daniele Termini
 * @copyright Web Agency Meta Line S.r.l.
 * @package   Application\Admin
 */

namespace Application\Admin\Utilities;

use ActiveRecord\Base;
use Application\Admin\Model\Filter;
use Application\Core\I18n\Translation\Translator;
use Application\Core\Utilities\Pagination;
use Pongho\Form\Form;
use Pongho\Http\Request;
use Pongho\Menu\Item;
use Pongho\Menu\Menu;

class ArchiveManager implements \IteratorAggregate, ArchiveInterface
{
    /**
     * Configurazione.
     *
     * @var array
     */
    protected $config;

    /**
     * Opzioni iniziali per la query dell’archivio.
     *
     * @var array
     */
    protected $model_options_backup = [];

    /**
     * Opzioni per la query dell’archivio con i filtri e gli ordinamenti aggiunti.
     *
     * @var array
     */
    protected $model_options;

    /**
     * Localizzazione.
     *
     * @var \Application\Core\I18n\Translation\Translator
     */
    protected $translator;

    /**
     * Numero della pagina corrente.
     *
     * @var int
     */
    protected $page;

    /**
     * Indirizzo della pagina corrente.
     *
     * @var string
     */
    protected $url;

    /**
     * Elenco dei records.
     *
     * @var \ActiveRecord\Base[]
     */
    protected $rows = [];

    /**
     * Numero di record per pagina.
     *
     * @var int
     */
    protected $records_per_page = 50;

    /**
     * Richiesta HTTP.
     *
     * @var \Pongho\Http\Request
     */
    protected $request;

    /**
     * Indica se i filtri sono abilitati.
     *
     * @var bool
     */
    protected $filters_enabled = true;

    /**
     * Indica se i filtri rapidi sono abilitati.
     *
     * @var bool
     */
    protected $fast_filters_enabled = true;

    /**
     * Opzioni per la query dei filtri rapidi.
     *
     * @var array
     */
    protected $fast_filters_query_options;

    /**
     * Indica se la ricerca è abilitata.
     *
     * @var bool
     */
    protected $search_enabled = true;

    /**
     * @var int
     */
    protected $total_records;

    /**
     * @param                       $url
     * @param int                   $page
     */
    public function __construct(Request $request, Translator $translator, array $config, $url, $page = 1)
    {
        $this->request = $request;
        $this->translator = $translator;

        $this->parseConfig($config);

        $this->url = $url;
        $this->page = $page;
    }

    /**
     * Restituisce gli operatori.
     *
     * @return array
     * @deprecated
     */
    public function getOperators()
    {
        return [
            'foreign'        => [
                'EQ'      => $this->translator->trans('equal to'),
                'NOT'     => $this->translator->trans('different from'),
                'IN'      => $this->translator->trans('in'),
                'NOTIN'   => $this->translator->trans('not in'),
                'NULL'    => $this->translator->trans('not compiled'),
                'NOTNULL' => $this->translator->trans('compiled'),
            ],
            'foreign-enum'   => [
                'EQ'      => $this->translator->trans('equal to'),
                'NOT'     => $this->translator->trans('different from'),
                'IN'      => $this->translator->trans('in'),
                'NOTIN'   => $this->translator->trans('not in'),
                'NULL'    => $this->translator->trans('not compiled'),
                'NOTNULL' => $this->translator->trans('compiled'),
            ],
            'foreign-string' => [
                'EQ'    => $this->translator->trans('equal to'),
                'NOT'   => $this->translator->trans('different from'),
                'LIKE'  => $this->translator->trans('contains'),
                'START' => $this->translator->trans('begins with'),
            ],
            'string'         => [
                'EQ'    => $this->translator->trans('equal to'),
                'NOT'   => $this->translator->trans('different from'),
                'LIKE'  => $this->translator->trans('contains'),
                'START' => $this->translator->trans('begins with'),
//				'END'		=> $this->translator->trans('ends with'),
//				'IN'		=> $this->translator->trans('in'),
//				'NOTIN'		=> $this->translator->trans('not in'),
//				'NULL'		=> $this->translator->trans('not compiled'),
//				'NOTNULL'	=> $this->translator->trans('compiled'),
            ],
            'numeric'        => [
                'EQ'    => $this->translator->trans('equal to'),
                'NOT'   => $this->translator->trans('different from'),
                'IN'    => $this->translator->trans('in'),
                'NOTIN' => $this->translator->trans('not in'),
                'LT'    => $this->translator->trans('lower than'),
                'LTE'   => $this->translator->trans('lower than or equal to'),
                'GT'    => $this->translator->trans('greater than'),
                'GTE'   => $this->translator->trans('greater than or equal to'),
//				'NULL'		=> $this->translator->trans('not compiled'),
//				'NOTNULL'	=> $this->translator->trans('compiled'),
            ],
            'date'           => [
                'DATEEQ'      => $this->translator->trans('equal to'),
                'DATEBETWEEN' => $this->translator->trans('from / to'),
//				'NOT'		=> $this->translator->trans('different from'),
                'LT'          => $this->translator->trans('before'),
//				'LTE'		=> $this->translator->trans('before or equal'),
                'GT'          => $this->translator->trans('after'),
//				'GTE'		=> $this->translator->trans('after or equal'),
                'NULL'        => $this->translator->trans('not compiled'),
                'NOTNULL'     => $this->translator->trans('compiled'),
            ],
            'datetime'       => [
                'DATEEQ'      => $this->translator->trans('equal to'),
                'DATEBETWEEN' => $this->translator->trans('from / to'),
//				'NOT'		=> $this->translator->trans('different from'),
                'LT'          => $this->translator->trans('before'),
//				'LTE'		=> $this->translator->trans('before or equal'),
                'GT'          => $this->translator->trans('after'),
//				'GTE'		=> $this->translator->trans('after or equal'),
                'NULL'        => $this->translator->trans('not compiled'),
                'NOTNULL'     => $this->translator->trans('compiled'),
            ],
            'time'           => [
                'DATEEQ'      => $this->translator->trans('equal to'),
                'DATEBETWEEN' => $this->translator->trans('from hour / to hour'),
//				'NOT'		=> $this->translator->trans('different from'),
                'LT'          => $this->translator->trans('before hour'),
//				'LTE'		=> $this->translator->trans('before or equal'),
                'GT'          => $this->translator->trans('after hour'),
//				'GTE'		=> $this->translator->trans('after or equal'),
                'NULL'        => $this->translator->trans('not compiled'),
                'NOTNULL'     => $this->translator->trans('compiled'),
            ],
            'enum'           => [
                'EQ' => $this->translator->trans('equal to'),
//				'NOT'		=> $this->translator->trans('different from'),
//				'NULL'		=> $this->translator->trans('not compiled'),
//				'NOTNULL'	=> $this->translator->trans('compiled'),
            ],
        ];
    }

    /**
     * Elabora l’array della configurazione.
     * @todo sarebbe bello che $config fosse un value object o una classe apposita per gestire la configurazione
     * @throws \LogicException
     */
    protected function parseConfig(array $config)
    {
        if (!array_key_exists('search_fields', $config)) {
            $config['search_fields'] = [];
        }

        if (empty($config['search_fields'])) {
            $this->search_enabled = false;
        }

        if (empty($config['fast_filters'])) {
            $this->fast_filters_enabled = false;
        }

        if (empty($config['model_fields']) || (!array_key_exists('model_class', $config) && !array_key_exists('model_manager_all', $config))) {
            $this->filters_enabled = false;
        }

        if (array_key_exists('model_manager_query', $config) || array_key_exists('model_manager_all', $config)) {
            if (!array_key_exists('model_manager_count', $config)) {
                throw new \LogicException('The "model_manager_count" callback is not defined in the ArchiveManager config!');
            }

            // In questo caso non mi servono le opzioni perché le gestisce il manager del modello, gli devo passare solo le condizioni
            $this->model_options_backup = [];
        } else {
            if (!array_key_exists('model_class', $config)) {
                throw new \LogicException('The "model_class" is not defined in the ArchiveManager config!');
            }

            if (!array_key_exists('model_options', $config)) {
                throw new \LogicException('The "model_options" is not defined in the ArchiveManager config!');
            }

            // Copia di backup delle opzioni per il modello
            $this->model_options_backup = $config['model_options'];
        }

        $this->config = $config;
    }

    /**
     * @return array
     */
    protected function getModelOptions()
    {
        if ($this->model_options === null) {
            // Tratto il filtro proveniente dalla query string
            if (($orders = $this->request->query->get('o', '')) !== '') {
                $this->orderBy($orders);
            }

            if (($query = $this->request->query->get('q', '')) !== '') {
                $this->search($query);
            }

            $this->model_options = $this->config['model_options'] ?? [];
        }

        return $this->model_options;
    }

    /**
     * @return int
     */
    protected function getRecordsPerPage()
    {
        if (($show = $this->request->query->get('s', '')) !== '') {
            $this->records_per_page = intval($show);
        }

        return $this->records_per_page;
    }

    /**
     * Implementazione di `IteratorAggregate`.
     *
     * @return \ArrayIterator
     */
    public function getIterator(): \Traversable
    {
        return new \ArrayIterator($this->rows);
    }

    /**
     * Restituisce i record del modello filtrati.
     *
     * @return \ActiveRecord\Base[]
     */
    public function getRecords($page = null)
    {
        // Gestisco le opzioni per i filtri rapidi
        //$this->config['model_options'] = $this->getStateConditions($this->request->get->get('f[status]', null, true));

        if ($page === null) {
            $page = $this->page;
        }

        /*
         * Nuovo funzionamento:
         *
         * se è presente l'opzione "model_class" effettua una query tramite il modello diretto
         * se è presente l'opzione "model_manager_all" ma non "model_manager_query", le query vengono gestite dal "findAll" del manager
         * se è presente l'opzione "model_manager_query" ma non "model_manager_all", la funzione del manager esegue già un filtro e non saranno disponibili i filtri
         * se sono presenti entrambe le opzioni del manager, verrà usata _query per la vista normale e _all per i filtri
         */

        if (isset($this->config['model_class'])) {
            $rows = $this->doQueryWithModel($page);
        } else {
            $order = '';
            if (($orders = $this->request->query->get('o', '')) !== '') {
                $order = $this->orderBy($orders);
            }

            $limit = $this->getRecordsPerPage();
            $offset = ($page - 1) * $limit;

            if ($this->request->query->has('f')) {
                if (!isset($this->config['model_manager_all'])) {
                    throw new \LogicException('Non è stata definita la configurazione "model_manager_all" per gestire la query dei filtri');
                }

                $rows = $this->doQueryWithManagerAndFilters($order, $limit, $offset);
            } elseif (isset($this->config['model_manager_query'])) {
                $rows = $this->doQueryWithManager($order, $limit, $offset);
            } else {
                $rows = $this->doQueryWithManagerAndFilters($order, $limit, $offset);
            }
        }

        // Aggiungo i campi estesi
        if (isset($this->config['parse_row']) && is_callable($this->config['parse_row'])) {
            array_map($this->config['parse_row'], $rows);
        }

        $this->rows = $rows;

        return $this->rows;
    }

    /**
     * @param $page
     * @return \ActiveRecord\Base[]|\ActiveRecord\Collection
     */
    protected function doQueryWithModel($page)
    {
        /** @var $model_class \ActiveRecord\Base */
        $model_class = $this->config['model_class'];

        return $model_class::page(
            $page,
            $this->getRecordsPerPage(),
            $this->getModelOptions()
        );
    }

    /**
     * @param $order
     * @param $limit
     * @param $offset
     * @return \ActiveRecord\Base[]|\ActiveRecord\Collection
     */
    protected function doQueryWithManager($order, $limit, $offset)
    {
        $function = array_shift($this->config['model_manager_query']);
        $parameters = $this->config['model_manager_query'];

        $parameters[] = $order;
        $parameters[] = $limit;
        $parameters[] = $offset;

        return call_user_func_array($function, $parameters);
    }

    /**
     * @param $order
     * @param $limit
     * @param $offset
     * @return \ActiveRecord\Base[]|\ActiveRecord\Collection
     */
    protected function doQueryWithManagerAndFilters($order, $limit, $offset)
    {
        $function = array_shift($this->config['model_manager_all']);
        $parameters = $this->config['model_manager_all'];

        $model_options = $this->getModelOptions();

        $conditions = [];
        if (isset($model_options['conditions'])) {
            $conditions = $model_options['conditions'];
        }

        $parameters[] = $conditions;
        $parameters[] = $order;
        $parameters[] = $limit;
        $parameters[] = $offset;

        return call_user_func_array($function, $parameters);
    }

    /**
     * Restituisce il numero totale di record disponibili.
     *
     * @return int
     */
    public function getTotalRecords()
    {
        if ($this->total_records === null) {
            // TODO
            if (array_key_exists('model_manager_count', $this->config)) {
                $function = array_shift($this->config['model_manager_count']);
                $this->total_records = call_user_func_array($function, $this->config['model_manager_count']);
            } else {
                /** @var $model_class \ActiveRecord\Base */
                $model_class = $this->config['model_class'];
                $this->total_records = $model_class::count($this->getModelOptions());
            }
        }

        return $this->total_records;
    }

    /**
     * Restituisce la paginazione.
     *
     * @return \Application\Core\Utilities\Pagination
     */
    public function getPagination()
    {
        $query = $this->request->query->all();

        return new Pagination(
            $this->url,
            $this->getTotalRecords(),
            $this->page,
            $this->getRecordsPerPage(),
            $query
        );
    }

    /**
     * @throws \LogicException
     * @return string
     */
    public function getTable()
    {
        static $table;

        if (!isset($this->config['table'])) {
            throw new \LogicException('Please define the table configuration in the controller');
        }

        if ($table === null) {
            $table = new TablesManager($this->config['table'], $this->request, $this->getRecords());
        }

        return $table->render();
    }

    public function setFilters(array $filters)
    {
        $this->config['model_options'] = $this->filterBy($this->model_options_backup, $filters);
    }

    /**
     * Imposta un termine di ricerca.
     *
     * @param string $value
     */
    public function search($value)
    {
        if (!$this->isSearchEnabled()) {
            return;
        }

        $conditions = [];

        foreach ($this->config['search_fields'] as $field) {
            $conditions[] = 'pongho_like(' . $field . ', :query)';
        }

        /** @var $model_class \ActiveRecord\Base */
        $model_class = $this->config['model_class'];
        $this->config['model_options'] = $model_class::addCondition(
            $this->config['model_options'],
            [
                '(' . implode(' OR ', $conditions) . ')',
                'query' => '%' . $value . '%',
            ],
        );
    }

    /**
     * Imposta l’ordinamento.
     *
     * @internal Viene usata solo internamente, dovrebbe essere protetta
     */
    public function orderBy(array $order_by)
    {
        $orders = [];
        foreach ($order_by as $order => $direction) {
            $orders[] = $this->quote($order) . ' ' . $direction;
        }

        $this->config['model_options']['order'] = implode(', ', $orders);

        return $this->config['model_options']['order'];
    }

    /**
     * Imposta un filtro.
     *
     * @return array
     * @throws \RuntimeException
     */
    protected function filterBy(array $model_options, array $filters)
    {
        if (!$this->isFiltersEnabled()) {
            return $model_options;
        }

        // todo: in futuro posso passargli i dati delle colonne dal modello per facilitare l'implementazione dei filtri
        $converter = new FilterConverter();

        foreach ($filters as $column => $value) {
            $field = $this->quote($column);
            if (array_key_exists($column, $this->config['model_fields'])) {
                $field_attrs = $this->config['model_fields'][$column];

                if (array_key_exists('column', $field_attrs)) {
                    $field = $field_attrs['column'];
                }

                if (array_key_exists('join', $field_attrs)) {
                    if (!isset($model_options['select'])) {
                        $model_options['select'] = '`from`.*';
                    }

                    if (!isset($model_options['joins'])) {
                        $model_options['joins'] = $field_attrs['join'];
                    } else {
                        $model_options['joins'] .= ' ' . $field_attrs['join'];
                    }

                    if (!isset($model_options['group'])) {
                        $model_options['group'] = '`from`.id';
                    }
                }
            }

            $condition = $converter->toCondition($column, $value, $field);

            $model_options = Base::addCondition($model_options, $condition);
        }

        return $model_options;
    }

    /**
     * Imposta i filtri ricavandoli dalla form.
     */
    protected function parseFilter(Form $form)
    {
        $filters = $form->getField('filter');

        $filter = [];
        /** @var $row \Pongho\Form\Repeater\RepeaterRow */
        foreach ($filters->getFields() as $row) {
            if ($row->getKey() === 'new' || $row->getValue('field') === '') {
                continue;
            }

            $filter[$row->getValue('field')][$row->getValue('operator')] = $row->getValue($row->getValue('valuefield'));
        }

        $this->config['model_options'] = $this->filterBy($this->model_options_backup, $filter);
    }

    /**
     * Alias di getFastFilters().
     *
     * @return string
     * @deprecated
     */
    public function getFilters()
    {
        return $this->getFastFilters();
    }

    /**
     * Restituisce i filtri rapidi.
     *
     * @return \Pongho\Menu\Menu
     * @todo   al momento funziona correttamente solo con query dirette sul campo "status" (f[status]=value) e in un caso particolare con "IN" (f[status][IN][]=value1&f[status][IN][]=value2)
     */
    public function getFastFilters()
    {
        if (!$this->isFastFiltersEnabled()) {
            return '';
        }

        /** @var \ActiveRecord\Base $model_class */
        $model_class = $this->config['model_class'];
        $label_tpl = '%s <span class="count">(%d)</span>';
        $filter_status = new Menu('filter-status');

        $filters = $this->request->query->get('f', []);
        $filter = $filters['status'] ?? null;

        // Ricalcolo le opzioni per ottenere i totali togliendo il precendente filtro rapido
        unset($filters['status']);
        $base_options = $this->filterBy($this->getFastFiltersQuery(), $filters);

        // Per ogni filtro rapido definito...
        foreach ($this->config['fast_filters'] as $state => $data) {
            // Compongo le condizioni per il contatore delle righe
            $conditions = $base_options;
            if (isset($data['conditions'])) {
                $conditions = $model_class::addCondition($conditions, $data['conditions']);
            }

            // Calcolo della label
            $total = $model_class::count($conditions);
            $title = $this->translator->get('filter_status_' . $state);
            $label = sprintf($label_tpl, $title, $total);
            $item = new Item($label, $data['url'], ['title' => $title]);

            // Stato attivo
            if (
                $filter === $state
                || ($filter === null && $state === 'all')
                || (is_array($filter) && in_array($state, $filter['IN']))
            ) {
                $item->setActive();
            }

            // Inserisco l'elemento nel menu
            if ($state === 'all') {
                $filter_status->add($item);
            } elseif ($total) {
                // Solo se contiene delle righe
                $filter_status->add($item);
            }
        }

        return $filter_status;
    }

    /**
     * Consente di impostare il node_type per i filtri del modello se mi trovo in un nodo
     *
     * @param $node_type
     *
     * @deprecated
     */
    public function setNodeType($node_type)
    {
    }

    /**
     * Imposta le opzioni per la query dei filtri rapidi, nel caso serva personalizzarla rispetto a quella del modello di default
     *
     * @param $options
     */
    public function setFastFilterQuery($options)
    {
        $this->fast_filters_query_options = $options;
    }

    /**
     * Restituisce la query per i filtri rapidi
     *
     * @return array
     */
    protected function getFastFiltersQuery()
    {
        if ($this->fast_filters_query_options !== null) {
            return $this->fast_filters_query_options;
        }

        return $this->model_options_backup;
    }

    /**
     * Restituisce la form in base al filtro salvato passato in argomento.
     *
     * @return \Pongho\Form\Form
     * @deprecated
     */
    public function getFiltersForm(Filter $filter)
    {
        /** @var $form \Pongho\Form\Form */
        $form = null;

        if (isset($this->config['form']) && is_callable($this->config['form'])) {
            $form = call_user_func($this->config['form'], $filter);
        }

        return $form;
    }

    /**
     * Indica se i filtri sono abilitati.
     *
     * @return bool
     */
    public function isFiltersEnabled()
    {
        return $this->filters_enabled;
    }

    /**
     * Indica se i filtri rapidi sono abilitati.
     *
     * @return bool
     */
    public function isFastFiltersEnabled()
    {
        return $this->fast_filters_enabled;
    }

    /**
     * Indica se la ricerca è abilitata.
     *
     * @return bool
     */
    public function isSearchEnabled()
    {
        return $this->search_enabled;
    }

    /**
     * Restituisce il nome della colonna o identificatore con le virgolette in modo da non incappare in casi
     * in cui il nome corrisponde ad un identificatore riservato (from, key, left...)
     *
     * @param $name
     * @return string
     */
    protected function quote($name)
    {
        return "`{$name}`";
    }
}
