<?php

namespace App\Helpers;

use App\Helpers\Filter;
use App\Helpers\Filters\Attribute;
use Route;

/**
 * Filters!
 * (c) Draga Sergiu
 *  v 3
 * 22.01.2018
 * 14.01.2019
 * 02.02.2020
 *
 *
 * @todo: improve performance
 *
 *
 * v3. 02/2021
 * - send also the id of the attribute key
 *    (because if we only match by attribute value we may get invalid results,
 *     since multiple keys can share the same values)
 * ONLY FOR DYNAMIC FILTERS
 * - updated app/Model/Filterable.php
 *
 * v2. 30/05/2019
 * - database filters
 *
 * v1.
 * - Config based filters (not database auto discovery!)
 * - Read filters from URI
 * - Add filters
 * - Filters are sorted by the way they appear in the config.
 * - Prepare query based on filters
 * - excluded filters  when building queries.
 *
 */
class Filters
{

    const SEPARATOR_ATTRIBUTE = ';';
    const SEPARATOR_VALUE = ',';
    const SEPARATOR_SLUG = '-';
    const ROUTER_PARAM = 'filters';

    public static $instance;
    private $options;

    /** $attributes Attribute * */
    private $attributes = [];

    /** $attributes Attribute * */
    private $attributes_uri = null;

    public function __construct()
    {
        $this->options = config('filters');
    }

    /**
     * Creates new filters
     *
     * Input:
     *  ['key' => 'filtername',
     * 'options' => [
     * 'type' => 'in',
     * 'use_slug' => true,
     * 'column' => 'attribute_value_id',
     * 'table' => 'attributes',
     * 'is_external' => true,
     * ],
     * ]
     *
     * @param $filters
     * @return $this
     */
    public function createDynamicFilters($filters)
    {
        foreach ($filters as $filter) {
            $this->options[$filter['key']] = $filter['options'];
        }
        return $this;
    }

    /** @return $this */
    public static function instance()
    {
        if (self::$instance) {
            return self::$instance;
        }

        return self::$instance = new self();
    }

    public static function factory()
    {
        return new self();
    }

    public function hasAttribute($key)
    {

        return !empty($this->attributes[$key]);
    }

    public function getAttribute($key)
    {
        if (!$this->hasAttribute($key)) {
            $this->attributes[$key] = new Attribute($this, $key, $this->options[$key]);
        }
        return $this->attributes[$key];
    }

    public function deleteAttribute($attribute)
    {
        unset($this->attributes[is_object($attribute) ? $attribute->key : $attribute]);
        return $this;
    }

    private function sort()
    {
        // sort by filters defined in our config.
        $aux = [];
        foreach ($this->options as $key => $value) {
            foreach ($this->attributes as $attribute) {
                if ($key === $attribute->key) {
                    $aux[$attribute->key] = $attribute;
                }
            }
        }
        $this->attributes = $aux;
    }

    private function cleanup()
    {
        /** $attribute Attribute * */
        foreach ($this->attributes as $attribute) {
            // do a sort first.
            $attribute->sort();
            // see if we have linked attributes. if yes, we may delete those (otherwise we may get a confilct)
            // eg: marime_intre cannot coexist with marime.
            if ($attribute->linked_with && $this->hasAttribute($attribute->linked_with)) {
                if (!$attribute->shouldAllowOnlyOneValue()) {
                    // frst we save the range
                    $this->add($attribute->key, $this->getAttribute($attribute->linked_with)->getPossibleValues());
                    // sort again after adding extra values
                    $attribute->sort();
                }

                $this->deleteAttribute($attribute->linked_with);
            }
            // now go and see if we got a range of items, and covert it to the linked attribue.
            if ($attribute->linked_with && $attribute->hasRange() && !$attribute->shouldAllowOnlyOneValue()) {
                $this->add($attribute->linked_with, [$attribute->getFirst(), $attribute->getLast()]);
                // don't forget to delete initial filters
                $attribute->delete();
            }
            // delete if no more values in the attribute!
            if (count($attribute->values) === 0) {
                $attribute->delete();
            }
        }


        $this->sort();
    }

    public function has($key, $value)
    {

        // first check in attribute
        if ($this->hasAttribute($key) && $this->getAttribute($key)->has($value)) {
            return true;
        }

        // check in linked attribute.
        if (!empty($this->options[$key]['linked_with']) && $this->hasAttribute($this->options[$key]['linked_with']) && !$this->getAttribute($key)->shouldAllowOnlyOneValue()) {
            return $this->getAttribute($this->options[$key]['linked_with'])->has($value);
        }

        return false;
    }

    public function get($key)
    {
        return $this->hasAttribute($key) ? $this->getAttribute($key)->getValues() : [];
    }

    /**
     * Add a filter
     * $filter->add('marime', 33)
     * OR
     * $filter->add('marime', [33, 34, 12])
     * OR
     * $filter->add('marime', $sizeModel)
     *
     * @param type $key
     * @param type $value
     * @param type $slug
     * @return $this
     */
    public function add($key, $value, $slug = null)
    {
        // @quikfix
        // sometimes we need to pass and extract the attribute database corresponding key
        $key_parts = explode(self::SEPARATOR_SLUG, $key);
        $key_database_id = null;
        if (count($key_parts) >= 2 && is_numeric($key_parts[0])) {
            $key_database_id = array_shift($key_parts);
            $key = implode(self::SEPARATOR_SLUG, $key_parts);
        }

        if (!empty($this->options[$key])) {
            $attribute = $this->getAttribute($key);
            $attribute->add($value, $slug);
        } else {
            throw new \Exception("There is no attribute named $key");
        }
        return $this;
    }

    public function toggle($key, $value, $slug = null)
    {
        if ($this->has(...func_get_args())) {
            $this->delete(...func_get_args());
        } else {
            $this->add(...func_get_args());
        }
        return $this;
    }

    public function delete($key, $value)
    {
        if ($this->hasAttribute($key)) {
            $this->getAttribute($key)->delete($value);
        }
        // check also in linked attribute.
        if (!empty($this->options[$key]['linked_with']) && $this->hasAttribute($this->options[$key]['linked_with'])) {
            // ups, we have do delete the whole attribute this way... but first get its values.
            $this->getAttribute($key)->add($this->getAttribute($this->options[$key]['linked_with'])->getDiffValues($value));
            $this->deleteAttribute($this->options[$key]['linked_with']);
        }
        return $this;
    }

    public function all()
    {
        $this->cleanup();
        return $this->attributes;
    }

    public function count()
    {
        return count($this->attributes);
    }

    public function count_real()
    {
        // don't count dummy filters..
        return count(array_filter($this->attributes, function ($item) {
            return $item->type !== 'dummy';
        }));
    }

    /**
     * Extract filters from current Uri string.
     * @todo optimize this, don't repeat operation if not needed!
     *
     * @param type $uri
     * @return $this
     */
    public function fromUri($uri = null)
    {
        // keep the original attributes from uri
//        if ($this->attributes_uri) {
//            foreach ($this->attributes_uri as $attribute) {
//                $this->attributes[$attribute->key] = clone $attribute;
//            }
//            return $this;
//        }

        $this->attributes = [];
        //$uri = 'filter-brand,1-apple,3-koo,2-samsung;marime_intre,1,10;culoare,1-rosu;marime,4;brand,31-kukla;culoare,2-negru';
        // try to read from route if nothing passed.
        if (!$uri) {
            $uri = Route::input(self::ROUTER_PARAM) ?? request('filters');
        }

        if (!$uri) {
            return $this;
        }

        // the filter uri is in the following format:
        // brand,1-apple,3-koo,2-samsung;marime_intre,1,10;culoare,1-rosu;marime,4;brand,31-kukla;culoare,2-negru
        $attributes = explode(self::SEPARATOR_ATTRIBUTE, $uri);
        foreach ($attributes as $attribute) {
            $attribute_values = explode(self::SEPARATOR_VALUE, $attribute);
            $key = array_shift($attribute_values);
            // if we got a slug, we must break it then..
            if (strpos($attribute, self::SEPARATOR_SLUG)) {
                foreach ($attribute_values as $attribute_value) {
                    $parts = explode(self::SEPARATOR_SLUG, $attribute_value);
                    $value = array_shift($parts);
                    $this->add($key, $value, implode(self::SEPARATOR_SLUG, $parts));
                }
            } else {
                $this->add($key, $attribute_values);
            }
        }

        // go cleanup
        $this->cleanup();

        // keep the uri attributes!
        $this->attributes_uri = $this->attributes;

        return $this;
    }

    /**
     * Get the Uri string from current filters.
     *
     * @param type $resetToOriginalFilters
     * @return type
     */
    public function getUri($x = false)
    {
        $this->cleanup();

        // return all filters into a string
        $uri = collect($this->attributes)->map(function ($attribute) {
            return implode(self::SEPARATOR_VALUE, $attribute->toArray());
        })->implode(self::SEPARATOR_ATTRIBUTE);

        return $uri;
    }

    /**
     * Eloquent model filtering based on current filters and config.
     *
     * @param type $baseModel
     * @return type
     */
    public function buildQuery($baseModel = 0, $except = [])
    {
        $this->cleanup();

        foreach ($this->attributes as $attribute) {
            if (in_array($attribute->key, $except)) {
                continue;
            }

            $baseModel = $attribute->buildQuery($baseModel);
        }

        return $baseModel;
    }

    public function getAttributes()
    {
        return $this->attributes;
    }

}
