<?php

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Mtc\Shop\Category;
use Mtc\Shop\Item as ShopItem;
use Illuminate\Support\Facades\Event;
use Mtc\Shop\Events\FilterUrlToSelections;
use Mtc\Shop\Events\FilterGenerateUrl;
use MtcPharmacy\Multisite\Classes\MultisiteManager;
use MtcPharmacy\Multisite\Classes\MultisiteConstants;

/**
 * ProductFilter
 *
 * @author       mtc.
 * @author       Alan <alan.reid@mtcmeida.co.uk>
 * @contributors Lukas <lukas.giegerich@mtcmeida.co.uk>
 * @copyright    2013 mtc. http://www.mtcmedia.co.uk/
 * @version      2014-02-24
 * @access       public
 */
class ProductFilter
{
    public $base_url = '/pharmacy'; //No trailing slash
    //Product Search
    public $selections = []; // Search criteria by type
    public $selections_all = [];  //Search criteria by order
    //Product Filter
    public $results = [];
    //Items Returned
    public $items = [];
    //Order of items
    public $max_per_page_default = MAX_PER_PAGE;
    public $max_per_page = MAX_PER_PAGE;
    public $page = 1;
    public $price_min;
    public $price_max;
    public $currency_ratio = 1;
    public $multi_categories = false;
    public $nofollow = false;
    public $item_count = 0;
    //Debug Information
    protected $sortby = '';
    protected $sortbies = [];
    private $sortby_default = 'default';
    private $startnumber = 0;
    public $last_selection = '';
    private $ajax;
    public $category_display_level = 0;

    private bool $only_categories_selection = false;

    /**
     * ProductFilter constructor.
     *
     * @param bool $ajax
     */
    public function __construct($ajax = false)
    {
        $this->ajax = $ajax;

        $this->selections['brands'] = [];
        $this->selections['categories'] = [];
        $this->selections['categories_toplevel'] = [];
        $this->selections['categories_all'] = [];

        $this->selections['sale'] = [];

        $this->selections['searches'] = [];

        $this->results['cats'] = [];

        $this->sortbies = $this->supportedSortings();
    }

    /**
     * Initialize filter
     */
    public function start()
    {
        $this->getResults('sale');
        $this->getResults('cat');
        $this->getResults('brand');

        if (!empty($this->selections['searches'])) {
            $this->getResults('search');
            $this->setSortby('relevance');
        }

        /*
         * Filter out display categories.
         * $this->category_display_level stores the value of category whose children to show
         * We simply load all children of this category and unset irrelevant categories from results
         */
        $child_categories = array_map(function ($category) {
            return $category['id'];
        }, categoryChild($this->category_display_level));


        foreach ($this->results['cats'] as $cat_id => $result_count) {
            if (!in_array($cat_id, $child_categories)) {
                unset($this->results['cats'][$cat_id]);
            }
        }

        $this->startnumber = ($this->page * $this->max_per_page) - $this->max_per_page;

        //Get Item Data
        $result = $this->getItems();

        $this->setNofollow();

        return $result;
    }

    /**
     * Performs counts of items available for the current filter type
     *
     * @param string $filter_type
     */
    public function getResults(string $filter_type): void
    {

        if (count($this->selections['categories'])) {
            //check whether we should apply the admin-set order
            $this->only_categories_selection = self::checkIfCategorySelection();
        }
        $results = ShopItem::query()
            ->select([
                'items.id',
                'items_categories.cat_id',
                DB::raw('count(items.id) as total'),
            ])
            ->join('items_categories', 'items_categories.item_id', '=', 'items.id')
            ->when($filter_type === 'brand', function (Builder $query) {
                $query->join('items_brands', 'items_brands.item_id', '=', 'items.id')
                    ->join('brands', 'items_brands.brand_id', '=', 'brands.id')
                    ->orderBy('brands.name')
                    ->groupBy('items_brands.brand_id')
                    ->addSelect('brands.id as brand_id');
            })
            ->when($filter_type === 'cat', function (Builder $query) {
                $query->join('categories', 'items_categories.cat_id', '=', 'categories.id')
                    ->orderBy('categories.name')
                    ->groupBy('items_categories.cat_id');
            })
            ->when($filter_type === 'sale', function (Builder $query) {
                $query->where('items.sale_price', '>', 0);
            })
            ->when($filter_type === 'price', function (Builder $query) {
                $query->select([
                    'MIN(items.sort_price) as price_min',
                    'MAX(items.sort_price) as price_max']
                );
            })
            ->when(!empty($this->price_min), function (Builder $query) {})
            ->where('items.hidden', 0)
            ->whereNotIn('items.product_type', [
                ShopItem::TYPE_PRIVATE_PRESCRIPTION,
                ShopItem::TYPE_CONSULTATION,
                ShopItem::TYPE_PRESCRIPTION,
            ])
            ->where('items.deleted', 0)
            ->whereHas('categories', function (Builder $query) {
                $query->where('categories.hide', 0)
                    ->where('categories.is_online_doctor', 0);
            })
            ->when(!empty($this->selections['brands']), function (Builder $query) {
                $query->whereHas('brands', function (Builder $query) {
                    $query->whereIn('brands.id', $this->selections['brands'])
                        ->where('brands.hide', 0);
                });
            })
            ->when(!empty($this->selections['categories']), function (Builder $query) {
                $query->whereHas('categories', function (Builder $query) {
                    $query->whereIn('categories.id', $this->getSearchCategories());
                });
            })
            ->when(count($this->selections['searches']) > 0, function (Builder $query) {
                $query->where(1);
                foreach ($this->selections['searches'] as $search) {
                    $query->orWhereRaw("(MATCH(`items`.`name`) AGAINST('*?*' IN BOOLEAN MODE))", $search)
                        ->orWhereRaw("(MATCH(`categories`.`name`) AGAINST('*?*' IN BOOLEAN MODE))", $search)
                        ->orWhereRaw("(MATCH(`brands`.`name`) AGAINST('*?*' IN BOOLEAN MODE))", $search);
                }
            })
            ->get();

        foreach ($results as $result) {
            $data = $result->toArray();

            if ($filter_type === 'price') {
                if (empty($this->results['price']['min'])
                    || (
                        $this->results['price']['min'] > $data['price_min']
                        && $data['price_min'] != 0
                    )
                ) {
                    $this->results['price']['min'] = $data['price_min'];
                }
                if (empty($this->results['price']['max'])
                    || ($this->results['price']['max'] < $data['price_max'])
                ) {
                    $this->results['price']['max'] = $data['price_max'];
                }
            }

            if ($filter_type === 'cat') {
                if ($data['cat_id']) {
                    $this->results['cats'][$data['cat_id']] = $data['total'];
                }
            }

            if ($filter_type === 'brand') {
                if ($data['brand_id']) {
                    $this->results['brands'][$data['brand_id']] = $data['total'];
                }
            }

            if ($filter_type === 'search') {
                $this->results['searches'][] = $data['total'];
            }

            if ($filter_type === 'sale') {
                $this->results['sale'][] = $data['total'];
            }
        }
    }

    /**
     * Similar to ProductFilter::getResults() but returns ids of items to show,
     * not counts of items available
     */
    public function getItems()
    {
        $query = ShopItem::query()
            ->select('items.id')
            ->groupBy('items.id')
            ->where('items.hidden', 0)
            ->whereNotIn('items.product_type', [
                ShopItem::TYPE_PRIVATE_PRESCRIPTION,
                ShopItem::TYPE_CONSULTATION,
                ShopItem::TYPE_PRESCRIPTION,
            ])
            ->join('items_categories', 'items_categories.item_id', '=', 'items.id')
            ->where('items.deleted', 0)
            ->whereDoesntHave('categories', function (Builder $query) {
                $query->where('categories.hide', 1);
            })
            ->when(!empty($this->selections['brands']), function (Builder $query) {
                $query->whereHas('brands', function (Builder $query) {
                    $query->whereIn('brands.id', $this->selections['brands']);
                });
            })
            ->when(!empty($this->selections['categories']), function (Builder $query) {
                $query->whereHas('categories', function (Builder $query) {
                    $query->whereIn('categories.id', $this->getSearchCategories());
                });
            })
            ->when(!empty($this->selections['sale']), function (Builder $query) {
                $query->where('items.sale_price', '>', 0);
            })
            ->when(!empty($this->price_min), function (Builder $query) {
                $priceMin = $this->currency_ratio == '1' ?
                    $this->price_min :
                    ($this->price_min / $this->currency_ratio - 0.01);
                $query->where('items.sort_price', '>=', $priceMin);
            })
            ->when(!empty($this->price_max), function (Builder $query) {
                $priceMax = $this->currency_ratio == '1' ?
                    $this->price_max :
                    ($this->price_max / $this->currency_ratio + 0.01);
                $query->where('items.sort_price', '<=', $priceMax);
            })
            ->when(count($this->selections['searches']) > 0, function (Builder $query) {
                $query->where(1);
                foreach ($this->selections['searches'] as $search) {
                    $query->orWhereRaw("(MATCH(`items`.`name`) AGAINST('*?*' IN BOOLEAN MODE))", $search)
                        ->orWhereRaw("(MATCH(`categories`.`name`) AGAINST('*?*' IN BOOLEAN MODE))", $search)
                        ->orWhereRaw("(MATCH(`brands`.`name`) AGAINST('*?*' IN BOOLEAN MODE))", $search);
                }
            })
            ->when(empty($this->sortby), function (Builder $query) {
                $query->orderByRaw("CASE WHEN `items_categories`.`order` IS NULL THEN 2 ELSE 1 END, `items_categories`.`order`, `items`.`id` DESC");
            })
            ->when(!empty($this->sortby), function (Builder $query) {
                $query->orderByRaw($this->sortbies[$this->sortby]($this->selections));
            });

        $this->item_count = $query->get()->count();

        $this->items = $query->skip($this->startnumber)
            ->limit($this->max_per_page)
            ->get()
            ->pluck('id')
            ->toArray();

        if (count($this->items) == 0
            && $this->item_count > 0
        ) {
            if (!$this->ajax) {
                header('Location: ' . $this->generateURL(array('page' => 1)));
                exit();
            } else {
                return $this->generateURL(array('page' => 1));
            }
        }
        return true;
    }

    /**
     * Check if the only selection in the filter is the category or size
     * so that appropriate order can be set
     *
     * @author Justyna Cala <justyna.cala@mtcmedia.co.uk>
     * @return boolean
     */
    public function checkIfCategorySelection(): bool
    {
        //check only if there is no sorting applied
        if (!empty($this->sortby) && $this->sortby !== 'default') {
            return false;
        }

        if (is_array($this->selections)) {
            foreach ($this->selections as $type => $selections) {
                //dont check for categories selections
                if (!str_contains($type, 'categories')) {
                    if (count($selections) > 0) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Builds a URL to the current state of the filter and returns it
     *
     * @param  array <string> $params
     * @param Illuminate\Support\Collection $name_collection collection of known names for params
     * @return string
     */
    public function generateURL($params = [], $name_collection = false): string
    {

        $url = $this->base_url . '/';

        //We don't want to change the current selections, so we create a copy
        $selections = $this->selections;
        $selections_all = $this->selections_all;

        if (!empty($params['sale_add'])) {
            $selections['sale'][] = $params['sale_add'];
            $selections_all[] = ['type' => 'sale', 'value' => $params['sale_add']];
        }

        if (!empty($params['sale_remove'])) {
            if (($key = array_search($params['sale_remove'], $selections['sale'])) !== false) {
                unset($selections['sale'][$key]);
            }

            $i = 0;
            foreach ($selections_all as $selection) {
                if ($selection['type'] == 'sale'
                    && $selection['value'] == $params['sale_remove']
                ) {
                    unset($selections_all[$i]);
                }

                $i++;
            }
        }

        if (isset($params['brand_add'])
            && $params['brand_add'] > 0
        ) {
            $selections['brands'][] = $params['brand_add'];

            $selections_all[] = ['type' => 'brand', 'value' => $params['brand_add']];
        }

        if (isset($params['brand_remove'])
            && $params['brand_remove'] > 0
        ) {
            if (($key = array_search($params['brand_remove'], $selections['brands'])) !== false) {
                unset($selections['brands'][$key]);
            }

            $i = 0;
            foreach ($selections_all as $selection) {
                if ($selection['type'] == 'brand'
                    && $selection['value'] == $params['brand_remove']
                ) {
                    unset($selections_all[$i]);
                }

                $i++;
            }
        }


         //Search
        if (!empty($params['search_add'])) {
            $search_terms = $params['search_add'];
            if (!is_array($search_terms)) {
                $search_terms[] = $search_terms;
            }
            foreach ($search_terms as $search_term) {
                $selections['searches'][] = $search_term;
                $selections_all[] = ['type' => 'search', 'value' => $search_term];
            }
        }

        if (!empty($params['search_remove'])) {
            if (($key = array_search($params['search_remove'], $selections['searches'])) !== false) {
                unset($selections['searches'][$key]);
            }

            $i = 0;
            foreach ($selections_all as $selection) {
                if ($selection['type'] == 'search' && $selection['value'] == $params['search_remove']) {
                    unset($selections_all[$i]);
                }
                $i++;
            }
        }

        if (isset($params['category_add']) && $params['category_add'] > 0) {
            $category = Category::query()->find($params['category_add']);
            if ($category) {
                if ($category->is_online_doctor) {
                    return $category->getDoctorLink();
                }
            }
            $parents = Mtc\Shop\Category::allParents($params['category_add']);

            //We need to remove all categories that arent part of this tree if this category has sub children
            if (Mtc\Shop\Category::hasChildren($params['category_add'])) {
                //Get Tree from current category
                foreach ($selections_all as $key => $selection) {
                    if ($selection['type'] === 'category'
                        && !in_array($selection['value'], $parents)
                    ) {
                        unset($selections_all[$key]);
                    }
                }
            }

             //Add in any missing parents
            foreach ($parents as $parent_id) {
                if (!in_array($parent_id, $selections['categories_all'])) {
                    $selections['categories'][] = $parent_id;
                    $selections_all[] = ['type' => 'category', 'value' => $parent_id];
                }
            }

            $selections['categories'][] = $params['category_add'];
            $selections_all[] = ['type' => 'category', 'value' => $params['category_add']];
        }

        if (isset($params['category_remove'])
            && $params['category_remove'] > 0
        ) {
            if (($key = array_search($params['category_remove'], $selections['categories'])) !== false) {
                unset($selections['categories'][$key]);
            }

            $i = 0;
            foreach ($selections_all as $selection) {
                if ($selection['type'] == 'category'
                    && $selection['value'] == $params['category_remove']
                ) {
                    unset($selections_all[$i]);
                }

                $i++;
            }

            //Remove all child level categories if its a top level cateogry thats been removed
            $cat_data = category_data($params['category_remove']);
            $allchildren = categoryAllChildren($cat_data['id']);

            foreach ($selections_all as $key => $selection) {

                if ($selection['type'] == 'category') {

                    if (in_array($selection['value'], $allchildren)) {
                        unset($selections_all[$key]);
                    }
                }
            }
        }

        foreach ($selections_all as $selection) {
            switch ($selection['type']) {
                case 'sale':
                    $url .= 'sale/';
                    break;
                case "brand":
                    if (!empty($name_collection['brands'][$selection['value']])) {
                        $brand_name = $name_collection['brands'][$selection['value']];
                    } else {
                        $brand_name = brand_name($selection['value']);
                    }
                    $url .= $selection['value'] . '-' . self::cleanStringForURL($brand_name) . '-brand/';
                    break;
                case "category":
                    if (!empty($name_collection['categories'][$selection['value']])) {
                        $category_name = $name_collection['categories'][$selection['value']];
                    } else {
                        $category_name = category_name($selection['value']);
                    }
                    $url .= $selection['value'] . '-' . self::cleanStringForURL($category_name) .'-category/';
                    break;
                case "search":
                    $url .= 'q-' . urlencode(str_replace("/", "--", $selection['value'])) . '/';
                    break;
            }
        }

        $q = "?";

        if (empty($params['ignore_page'])) {
            if (isset($params['page'])
                && $params['page'] > 0
            ) {
                $url .= $q . "page=" . $params['page'];
                $q = '&';
            } elseif ($this->page > 1) {
                $url .= $q . "page=" . $this->page;
                $q = '&';
            }
        }

        if (isset($params['price_min'])
            && $params['price_min'] > 0
        ) {
            $url .= $q . "price_min=" . $params['price_min'];
            $q = '&';
        } elseif ($this->price_min > 0) {
            $url .= $q . "price_min=" . $this->price_min;
            $q = '&';
        }

        if (isset($params['price_max'])
            && $params['price_max'] > 0
        ) {
            $url .= $q . "price_max=" . $params['price_max'];
            $q = '&';
        } elseif ($this->price_min > 0) {
            $url .= $q . "price_max=" . $this->price_max;
            $q = '&';
        }

        if (!empty($params['sortby'])
            && $this->isValidSorting($params['sortby'])
        ) {
            $url .= $q . 'sortby=' . $params['sortby'];
            $q = '&';
        } elseif ($this->sortby != ''
            && $this->sortby != $this->sortby_default
        ) {
            $url .= $q . 'sortby=' . $this->sortby;
            $q = '&';
        }

        if (!empty($params['max_per_page'])) {
            $url .= $q . 'ps=' . $params['max_per_page'];
            $q = '&';
        } elseif ($this->max_per_page != ''
            && $this->max_per_page != $this->max_per_page_default
        ) {
            $url .= $q . 'ps=' . $this->max_per_page;
            $q = '&';
        }
        $url = preg_replace("/-{2,}/", '-', $url);

        Event::dispatch(new FilterGenerateUrl($selections_all, $url));

        $url = rtrim($url, '/');

        return $url;
    }

    /**
     * Takes a given filter URL and turns it into filter selection parameters
     *
     * @param string $url
     */
    public function urlToSelections($url)
    {
        $path = parse_url($url, PHP_URL_PATH);
        if ($path) {
            $url = $path;
        }

        $url = urldecode($url);
        $parts = explode("/", $url);

        foreach ($parts as $part) {
            if (!empty($part)) {

                $selection = explode("-", $part);
                $last_part = $selection[count($selection) - 1];

                $was_found = false;
                switch ($last_part) {
                    case 'category':

                        $value = (int)$selection[0];

                        // Mark the selected category for use by front-end
                        CategoryHelper::$active_category = $value;

                        $cat_data = category_data($value);

                        if (Category::hasChildren($value)) {
                            $this->category_display_level = $cat_data['id'];
                        }

                        $this->selections_all[] = ['type' => 'category', 'value' => $value];
                        $this->selections['categories'][] = $value;

                        $this->selections['categories_all'][] = $value;
                        $this->last_selection = 'category';
                        $was_found = true;
                        break;
                    case 'brand':
                        $this->selections_all[] = ['type' => 'brand', 'value' => (int) $selection[0]];
                        $this->selections['brands'][] = (int) $selection[0];
                        $this->last_selection = 'brand';
                        $was_found = true;
                        break;
                }

                if ($was_found) {
                    continue;
                }

                switch ($selection[0]) {
                    case 'sale':
                        $this->selections_all[] = ['type' => 'sale', 'value' => 'sale'];
                        $this->selections['sale'][] = 'sale';
                        $this->last_selection = 'sale';
                        break;
                    case "b":
                        $this->selections_all[] = ['type' => 'brand', 'value' => (int) $selection[count($selection) - 1]];
                        $this->selections['brands'][] = (int) $selection[count($selection) - 1];
                        $this->last_selection = 'brand';
                        break;
                    case "c":
                        $value = (int) $selection[count($selection) - 1];

                        // Mark the selected category for use by front-end
                        CategoryHelper::$active_category = $value;

                        $cat_data = category_data($value);

                        if (Category::hasChildren($value)) {
                            $this->category_display_level = $cat_data['id'];
                        }

                        $this->selections_all[] = ['type' => 'category', 'value' => $value];
                        $this->selections['categories'][] = $value;

                        $this->selections['categories_all'][] = $value;
                        $this->last_selection = 'category';
                        break;

                    case "q":
                        $search = preg_replace('/^q-/', '', $part);
                        $search = urldecode($search);
                        $this->selections_all[] = ['type' => 'search', 'value' => $search];
                        $this->selections['searches'][] = $search;
                        $this->last_selection = 'search';
                        break;
                }
            }
        }

        Event::dispatch(new FilterUrlToSelections($this, $url));
    }

    /**
     * Static wrapper for ProductFilter::generateUrl() to allow doing URL
     * declarations in a one liner. Incurs a tiny bit of overhead but no extra
     * db calls or anything so unless you are hugely concerned about efficiency
     * using this should be fine
     *
     * @param  array $params
     * @return string
     */
    public static function url($params = [])
    {
        $urlFilter = new ProductFilter();
        $url = $urlFilter->generateURL($params);
        unset($urlFilter);
        return $url;
    }

    /**
     * Checks if google should be following this particular filter state. If any
     * filter selection type > 1 then no, with the exception of categories,
     * which may have more than 1 selection IF they all belong to the same
     * branch of the category tree.
     */
    public function setNofollow(): void
    {
        if (empty(PRODUCT_FILTER)) {
            return;
        }

        if (!empty($this->selections['searches'])) {
            $this->nofollow = true;
            return;
        }

        $nofollow = false;
        $non_category_based_nofollow = false;

        if (count($this->selections['brands']) > 0 && count($this->selections['categories']) > 0) {
            $this->nofollow = true;
            return;
        }

        $selection_counter = [];
        foreach ($this->selections_all as $selection) {
            if (!isset($selection_counter[$selection['type']])) {
                $selection_counter[$selection['type']] = 0;
            }

            ++$selection_counter[$selection['type']];
            if ($selection_counter[$selection['type']] > 1) {
                $nofollow = true;
                if ($selection['type'] != 'category') {
                    $non_category_based_nofollow = true;
                }
            }
        }

        //enable indexing if the current selection only consists of a category and its parents
        if ($nofollow //only check if there is an active nofollow rule
            && !$non_category_based_nofollow //only look at the category nofollow rule if there aren't any others from other selection types
            && $selection_counter['category'] > 1 //only check categroies if there's an active nofollow for them
            && count(categoryAllParents(end($this->selections['categories_all']))) == (count($this->selections['categories_all']) - 1) //checks if selected categories are of the same ancestry
        ) {
            $nofollow = false;
        }

        $this->nofollow = $nofollow;
    }

    /**
     * Clean string for use in url
     *
     * @param  string $string
     * @return string
     */
    private static function cleanStringForURL($string)
    {
        $string = str_replace(' ', '-', $string); //remove spaces
        $string = clean_value($string); //remove invalid chars
        $string = preg_replace('/-+/', '-', $string); //replace two or more - with a single -
        $string = trim($string, '-'); //remove trailing -

        return $string;
    }


    /**
     * Static helper method for generating filter URLs. Based on old
     * browse_url function and used for converting calls to old function
     *
     * @param  array $args
     * @return string
     */
    public static function browseUrl(Array $args)
    {
        $url = '';
        $params = [];

        if ($args['catid'] > 0) {
            $params['category_add'] = (int)$args['catid'];
        }

        if ($args['brandid'] > 0) {
            $params['brand_add'] = (int)$args['brandid'];
        }

        if ($args['page'] > 0) {
            $params['brand_add'] = (int)$args['page'];
        }

        if ($args['order'] != '') {
            $params['order'] = $args['order'];
        }

        if (count($params) > 0) {
            $tmpFilter = new ProductFilter();
            $url = $tmpFilter->generateURL($params);
            unset($tmpFilter);
        }

        return $url;
    }

    /**
     * Returns the selected "Sort By" option
     *
     * @return string
     */
    public function getSortby()
    {
        return $this->sortby;
    }

    /**
     * Safely sets the "Sort By" option.
     *
     * Returns FALSE if the attempted option is not available; TRUE on success.
     *
     * @param string $sortby
     *
     * @return bool
     */
    public function setSortby($sortby)
    {
        if ($this->isValidSorting($sortby)) {
            $this->sortby = $sortby;

            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks that the "Sort By" option supplied in $value is available.
     *
     * @param string $value
     *
     * @return bool
     */
    protected function isValidSorting($value)
    {
        return in_array($value, array_keys($this->sortbies));
    }

    /**
     * The supported "Sort By" options and their logic implemented through closures.
     *
     * You can override this in a child class or add more options directly to $this->sortbies array.
     *
     * This method is only called in constructor to populate $this->sortbies.
     *
     * @return array
     */
    protected function supportedSortings(): array
    {
        return [
            'default' =>
                function () {
                    if ($this->only_categories_selection) {
                        return "CASE WHEN `items_categories`.`order` IS NULL THEN 2 ELSE 1 END,
                            `items_categories`.`order`, `items`.`num_sold` DESC";
                    } else {
                        return '`items`.`num_sold` DESC, `items`.`id` DESC';
                    }
                },
            'best-selling' =>
                function () {
                    return '`items`.`num_sold` DESC, `items`.`id` DESC ';
                },
            'price-high-low' =>
                function () {
                    return '`items`.`sort_price` DESC, `items`.`sale_price` DESC, `items`.`price` DESC, `items`.`id` DESC ';
                },
            'price-low-high' =>
                function () {
                    return '`items`.`sort_price` ASC, `items`.`sale_price` ASC, `items`.`price` ASC, `items`.`id` DESC ';
                },
        ];
    }

    /**
     * Retrieve the list of categories that are viable to be searched
     *
     * @return array $search_categories list of categories to search
     * @author mtc.
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    private function getSearchCategories(): Collection
    {
        $search_categories = collect();
        foreach ($this->selections['categories'] as $category_id) {
            // Include searched category
            $search_categories->push($category_id);

            // Include all children
            $search_categories = $search_categories->merge(Category::getCompleteFlatTree($category_id)->pluck('id'));

            // Because we're doing a more refined search, remove all parents
            $allParents = Category::allParents($category_id);
            $search_categories = $search_categories->diff($allParents);
        }
        return $search_categories;
    }

    /**
     * Get the collection of names for fetched results
     *
     * This function works as helper to reduce the duplicate queries like category_name() and brand_name()
     * Not only this reduces the duplicate function calls in ajax.include.php and productFilter class
     * but it also combines all looped queries into a collection therefore reducing the number of queries made
     *
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     * @return \Illuminate\Support\Collection
     */
    public function getNameCollection()
    {
        $name_collection = [];

        if (CATEGORIES_MAX > 0 && is_array($this->results['cats'])) {
            $name_collection['categories'] = Category::whereIn('id', array_keys($this->results['cats']))
                ->select('id', 'name')
                ->get()
                ->keyBy('id')
                ->map(function ($category) {
                    $mm = new MultisiteManager($category);
                    $obj_to_display = $mm->getDisplayEntity();
                    return $obj_to_display->name;
                });
        }

        if (BRANDS_ENABLED === true && is_array($this->results['brands'] ?? null)) {
            $name_collection['brands'] = \Mtc\Shop\Brand::whereIn('id', array_keys($this->results['brands']))
                ->select('id', 'name')
                ->get()
                ->keyBy('id')
                ->map(function ($brand) {
                    $mm = new MultisiteManager($brand);
                    $obj_to_display = $mm->getDisplayEntity();
                    return $obj_to_display->name;
                });
        }

        return collect($name_collection);
    }
}
