<?php

use Illuminate\Support\Facades\DB;
use Mtc\Core\Seo\Canonical as SeoCanonical;
use Mtc\Core\Seo\Defaults;
use Mtc\Shop\Brand;
use Mtc\Shop\Category;

/**
 * Seo class
 *
 * @author: Lukas Giegerich | mtc.
 * @version 2014-04-22
 * @deprecated
 */
class Seo
{

    /**
     * array holding seo meta information
     * @var array
     */
    public $data = array();

    /**
     * String holding details of what criteria was used to match seo data (script_name, fallback, etc)
     * @var string
     */
    public $matched_by;

    /**
     * path to current page from site root
     * @var string
     */
    public $path;

    /**
     * @var string
     */
    protected $match_path;

    /**
     * @var \Page
     */
    protected $p;

    function __construct()
    {
        $this->data = [
            'title' => '',
            'description' => '',
            'heading' => '',
        ];

        $this->setPath();
    }

    /**
     * Sets CMS Page.
     *
     * @param Page $p
     */
    public function setCmsPage(Page $p)
    {
        $this->p = $p;
    }

    /**
     * Get seo data for a specific object
     *
     * @param $object
     * @return array
     */
    public function getDataForClass($object)
    {
        return $this->getSeoData($object);
    }

    /**
     * Works out the SEO data for the requested URI
     * @return array
     */
    public function getSeoData($object = null)
    {
        $matched_by = '';

        //do we have an exact uri match?
        $matched = $this->tryMatchApproximateURI();
        if ($matched) {
            $matched_by = 'approximate_uri';
        }

        $matched_exact = $this->tryMatchExactUri();
        if (!$matched) {
            $matched = $matched_exact;
        }

        //do we have a script name match?
        if (!$matched) {
            $matched = $this->tryMatchRequestUri();
        } elseif (empty($matched_by)) {
            $matched_by = 'exact_uri';
        }

        //do we have a module set seo data match?
        if (!$matched) {
            $matched = $this->tryMatchModuleSetSeoData();
        } elseif (empty($matched_by)) {
            $matched_by = 'script_name';
        }
        //do we have applicable defaults?
        if (!$matched) {
            $class_name = $object ? get_class($object) : '';
            $matched = $this->tryMatchDefaults($class_name);
        } elseif (empty($matched_by)) {
            $matched_by = 'module_set_seo_data';
        }

        //if still no match, use fallback default
        if (!$matched || (empty($this->data['title']) && empty($this->data['description']))) {
            $matched = $this->tryMatchFallback();
        } elseif (empty($matched_by) && $matched) {
            $matched_by = 'default';
        }

        //this code block is the safetey net if someone were to delete the fallback from the db
        if (!$matched) {
            if (!$this->data['title']) {
                $this->data['title'] = SITE_NAME;
            }

            if (!$this->data['description']) {
                $this->data['description'] = SITE_NAME;
            }

            $matched_by = 'safety';
        } elseif (empty($matched_by)) {
            $matched_by = 'fallback';
        }

        $this->matched_by = $matched_by;

        $this->tryFindHeading();
        //let's process the given {keywords} based on the context of the current path
        $this->processSeoKeywords($object);

        $this->tryFindCanonical();

        return $this->data;
    }

    /**
     * Check if we have a specific canonical
     * in SeoAdmin for this URL
     *
     * @return boolean
     */
    private function tryFindCanonical()
    {
        $canonical_path = SeoCanonical::where('path', parse_url($this->path)['path'])
            ->pluck('canonical_path');

        if ($canonical_path !== null) {
            if ($this->path == $canonical_path) {
                return false;
            }

            $this->data['canonical_path'] = $canonical_path;
            return true;
        } else {
            return false;
        }
    }

    /**
     * Check if we have a specific heading
     * in SeoAdmin for this URL
     *
     * @return boolean
     */
    private function tryFindHeading()
    {
        $heading = \Mtc\Core\Seo\Heading::query()
            ->where('path', $this->path)
            ->first();

        if ($heading) {
            $this->data['heading'] = $heading->text;
        }

        return !empty($heading);
    }

    /**
     * Matches path to exact uri page rule in the db
     * The by product of this method is that if no exact match has been made a
     * list of all appoximate uri matches (page rules that fit into the current
     * uri) will be available.
     *
     * @return boolean
     */
    private function tryMatchExactUri()
    {
        $page = \Mtc\Core\Seo\Page::query()
            ->where('path', $this->path)
            ->first();

        return $page ? $this->setSeoData($page->toArray()) : false;
    }

    /**
     * Matches if a single brand has been set (browse only)
     *
     * @param ProductFilter $filter
     * @return boolean|mixed
     * @return boolean|mixed
     */
    private function tryMatchBrand(ProductFilter $filter)
    {
        if (count($filter->selections['brands']) != 1) {
        return false;
    }

        return Brand::query()
            ->where('id', $filter->selections['brands'][0])
            ->where(function ($query) {
                return $query->where('seo_title', '!=', '')
                    ->orWhere('seo_description', '!=', '');
            })
            ->select([
                'seo_title as title',
                'seo_description as description'
            ])
            ->first();
    }

    /**
     * Matches if a category selection makes sense (part of path on category tree selected)
     *
     * @param ProductFilter $filter
     * @return boolean
     */
    private function tryMatchCategory(ProductFilter $filter)
    {
        $total_count = count($filter->selections['categories_all']);
        if ($total_count < MAX_CATEGORY_DEPTH && $total_count > 0) { //maximum of six levels assumed
            //figure out if the selected cats are a path
            $selected_cats = Category::query()
                ->whereIn('id', $filter->selections['categories_all'])
                ->pluck('id')
                ->toArray();

            $bottom_cat_id = array_diff($filter->selections['categories_all'], $selected_cats);
            if (count($bottom_cat_id) != 1) {
                return false;
            }

            $bottom_cat_id = end($bottom_cat_id);

            $category_data = Category::query()
                ->select([
                    'seo_title AS title',
                    'seo_description AS description'
                ])
                ->where('id', $bottom_cat_id)
                ->where(function ($query) {
                    return $query->where('seo_title', '!=', '')
                        ->orWhere('seo_description', '!=', '');
                })
                ->first();

            if ($category_data) {
                return $category_data->toArray();
            }
        }
        return false;
    }

    /**
     * Matches path to closest page rule
     *
     * @return boolean
     */
    private function tryMatchApproximateURI()
    {
        $uri_matches = \Mtc\Core\Seo\Page::query()
            ->whereRaw(DB::raw("'{$this->path}' LIKE CONCAT(`path`, \"%\")"))
            ->get()
            ->toArray();

        if (empty($uri_matches)) {
            return false;
        }

        if (count($uri_matches) > 1) {
            //sort matches with best on top
            usort($uri_matches, ['self', 'sortByLength']);
        }
        //return best match
        return $this->setSeoData($uri_matches[0]);
    }

    /**
     * Matches path against script name defined in page rules
     *
     * @return boolean
     */
    private function tryMatchRequestUri()
    {
        try{
            $path_info = request()->getPathInfo();
        } catch(\Exception $exception) {
            $path_info = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
        }

        $seo_page = \Mtc\Core\Seo\Page::query()
            ->where('path', $path_info)
            ->first();

        if (!$seo_page) {
            return false;
        }

        return $this->setSeoData($seo_page);
    }

    /**
     * Matches path against default rules
     *
     * @param string $class_name
     * @return boolean
     */
    private function tryMatchDefaults($class_name)
    {
        if (self::isBrowse()) {
            $path = '/browse/index.php';
        } elseif (!empty($this->p)) {
            if ($this->p->listing_item) {
                $path = 'cms_listing_item';
            } elseif ($this->p->listing_container) {
                $path = 'cms_listing_container';
            } else {
                $path = 'cms_page';
            }
        } else {
            $path = parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH);
        }

        $default = Defaults::query()
            ->where('path', $path)
            ->orWhere('path', $class_name)
            ->first();

        return $default ? $this->setSeoData($default->toArray()) : false;
    }

    /**
     * Attempts to get seo data from a module(shop/cms/..) seo area/fields
     * Currently only does this for CMS as shop doesn't have a seo area/fields
     * @return boolean
     * @author Rihards Silins <rihards.silins@mtcmedia.co.uk>
     */
    private function tryMatchModuleSetSeoData()
    {
        $seo_data = false;

        // SHOP (browse)
        if (self::isBrowse()) {
            $filter = self::getFilter();
            // brands
            $seo_data = $this->tryMatchBrand($filter);
            if ($seo_data !== false) {
                return $this->setSeoData($seo_data);
            }

            // categories
            $seo_data = $this->tryMatchCategory($filter);
            if ($seo_data !== false) {
                return $this->setSeoData($seo_data);
            }
        }

        // CMS
        if (!empty($this->p)) {
            if (!empty($this->p->seo_title)) {
                $seo_data['title'] = $this->p->seo_title;
            }
            if (!empty($this->p->seo_description)) {
                $seo_data['description'] = $this->p->seo_description;
            }
            if (empty($seo_data)) {
                return false;
            }
            return $this->setSeoData($seo_data);
        }

        return false;
    }

    /**
     * Matches all against fallback
     *
     * @return boolean
     */
    private function tryMatchFallback()
    {
        $fallback = \Mtc\Core\Seo\Defaults::query()
            ->where('path', '')
            ->first();

        if (!$fallback) {
            return false;
        }

        foreach ($this->data as $key => $value) {
            if (empty($this->data[$key])) {
                $this->data[$key] = $fallback[$key];
            }
        }

        return true;
    }

    /**
     * replaces keywords with defined values in seo data and otherwise removes
     * keywords
     */
    private function processSeoKeywords($object)
    {

        //process {SITE_NAME}
        $this->data = str_replace('{SITE_NAME}', SITE_NAME, $this->data);

        //process {FILE_NAME}
        if (strpos(implode($this->data), '{FILE_NAME}') !== false && $_SERVER['SCRIPT_NAME']) {

            //get the name of the file or folder if index for the current page
            $pathinfo = (pathinfo($_SERVER['SCRIPT_NAME']));
            $sanitized = filter_var($pathinfo, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
            if ($sanitized['filename'] != 'index') {
                $file_name = $sanitized['filename'];
            } else {
                $file_name = preg_replace('#.*/([^/]+)$#', '$1', $sanitized['dirname']);
            }

            $file_name = preg_replace('/[._-]/', ' ', $file_name); //replace special chars with spaces
            $file_name = preg_replace('/([A-Z](?=[a-z ]))/', ' $1', $file_name); //insert spaces before upper case letters
            $file_name = preg_replace('/((?<=[a-z])[A-Z])/', ' $1', $file_name); //insert spaces before upper case letters
            $file_name = ucwords($file_name); //make all words uppercase

            $this->data = str_replace('{FILE_NAME}', $file_name, $this->data);
        }

        if ($this->match_path === \Item::class) {
            $this->processItemPageSeoKeywords($object);
        } elseif (self::isBrowse()) {
            $this->processBrowsePageSeoKeywords();
        } elseif ($this->p) {
            $this->processCmsSeoKeywords();
        }

        //remove unmatched keywords and resulting whitespace surplus
        $this->data = preg_replace('/\{[^}]+\}?/', '', $this->data);
        $this->data = preg_replace('/(\s+)/', ' ', $this->data);
        $this->data = preg_replace('/ (- )+/', ' - ', $this->data);

        //make the ends nice and neat
        foreach ($this->data as $key => $value) {
            while ($this->data[$key] != trim($value, " \t\n\r\0\x0B|-")) {
                $this->data[$key] = trim($value, " \t\n\r\0\x0B|-");
            }
        }
    }

    /**
     * Processes keywords that bear special meaning in the context of the item
     * page
     */
    private function processItemPageSeoKeywords(Item $item)
    {
        //construct search string
        $search_string = implode('', $this->data);

        if (strpos($search_string, '{ITEM}') !== false) {
            $this->data = str_replace('{ITEM}', $item->name, $this->data);
        }

        if (strpos($search_string, '{BRAND}') !== false) {
            $this->data = str_replace('{BRAND}', $item->brand_name, $this->data);
        }

        if (strpos($search_string, '{CATEGORY}') !== false) {
            $category = collect($item->categories)
                ->pluck('name')
                ->first();

            $this->data = str_replace('{CATEGORY}', $category, $this->data);
        }
    }

    /**
     * Processes keywords that bear special meaning in the context of the browse
     * page
     */
    private function processBrowsePageSeoKeywords()
    {
        //construct search string
        $search_string = implode('', $this->data);

        $filter = self::getFilter();

        if (strpos($search_string, '{BRAND}') !== false) {
            $replacement = '';
            if (count($filter->selections['brands']) == 1) {
                $replacement = Brand::query()->find($filter->selections['brands'][0])->name ?? '';
            }
            $this->data = str_replace('{BRAND}', $replacement, $this->data);
        }

        if (strpos($search_string, '{PAGE}') !== false) {
            $this->data = str_replace('{PAGE}', $filter->page, $this->data);
        }

        if (strpos($search_string, '{CATEGORIES}') !== false || strpos($search_string, '{CATEGORY}') !== false) {
            $cat_replacement = '';
            $cats_replacement = '';
            $total_count = count($filter->selections['categories_all']);
            if ($total_count <= MAX_CATEGORY_DEPTH && $total_count > 0) { //maximum of six levels assumed
                $parents_pairs = Category::query()
                    ->whereIn('id', $filter->selections['categories_all'])
                    ->pluck('sub_id', 'id')
                    ->toArray();

                $bottom_level_categories = array_diff($filter->selections['categories_all'], $parents_pairs);
                if (count($bottom_level_categories) != 1) {
                    return false;
                }

                $seek_category = $lowest_level_category = Category::query()->find(end($bottom_level_categories));
                $category_names = collect([
                    $lowest_level_category->name,
                ]);

                do {
                    $seek_category = $seek_category->parent;
                    if ($seek_category) {
                        $category_names->push($seek_category->name);
                    }
                } while (!empty($seek_category));

                $cats_replacement = $category_names->implode(' | ');
                $cat_replacement = $lowest_level_category->name ?? '';
            }
            $this->data = str_replace('{CATEGORIES}', $cats_replacement, $this->data);
            $this->data = str_replace('{CATEGORY}', $cat_replacement, $this->data);
        }
    }

    /**
     * Sets SEO data - returns true as this is used in the context of a
     * successfull rule match
     *
     * @param array $seo_data
     * @return boolean
     */
    private function setSeoData($seo_data)
    {
        $this->match_path = $seo_data['path'];
        $this->data = [
            'title' => $seo_data['title'],
            'description' => $seo_data['description'],
        ];
        return true;
    }

    /**
     * sets the path to be looked at by working out the clients request uri
     */
    public function setPath()
    {
        $path = '';
        if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] !== '') {
            $path = $_SERVER['REQUEST_URI'];
        } else {
            $path = $_SERVER['PHP_SELF'];
            if (!empty($_SERVER['argv'])) {
                $path .= '?' . $_SERVER['argv'][0];
            }
        }

        // if the URI is just a forward slash it should be interpreted as
        // index.php for seo purposes
        if (($path == '/') || (substr($path, 0, 2) === '/?')) {
            $path = '/index.php';
        }

        $this->path = $path;
    }

    /**
     * Replaces two keywords {PAGE_TITLE} by page title and {PAGE_CONTENT}{<CUTOFF CHARS>}
     * by Content shortened to a maximum of <CUTOFF CHARS> while not cutting in the middle
     * of the last word and appending … at the end/replacing ?/!/. if it's last char.
     * If Content is not available, falls back to <page title> | SITE_NAME.
     *
     * @author Jindřich Prokop <jindrich.prokop@mtcmedia.co.uk>
     */
    protected function processCmsSeoKeywords()
    {
        $this->data = str_replace('{PAGE_TITLE}', $this->p->title, $this->data);

        if (empty($this->p->pagedata['Content'][0]['value'])) {
            $replacement = "{$this->p->title} | " . SITE_NAME;
        } else {
            $replacement = strip_tags(preg_replace('/\s+/', ' ', $this->p->pagedata['Content'][0]['value']));
        }

        foreach ($this->data as &$datum) {
            if (preg_match_all('/\{PAGE_CONTENT\}\{([0-9]*)\}/', $datum, $matches)) {
                foreach ($matches[1] as $cutoff) {
                    $cut_position = strpos(wordwrap($replacement, $cutoff - 2), "\n");
                    $cut_position = $cut_position ?: $cutoff;
                    $replacement = rtrim(substr($replacement, 0, $cut_position), '?.!') . '…';
                    $datum = str_replace("{PAGE_CONTENT}{{$cutoff}}", $replacement, $datum);
                }
            }
        }
    }

    /**
     * Helper used with usort to sort 2d array returned from db by length of
     * path field
     *
     * @param array $a
     * @param array $b
     * @return array
     */
    public static function sortByLength($a, $b)
    {
        return strlen($b['path']) - strlen($a['path']);
    }

    /**
     * Checks if the current uri is in the /browse/ module or not
     *
     * @return boolean
     */
    public static function isBrowse()
    {
        return defined('IS_BROWSE');
    }

    /**
     * Gets product filter in the correct state
     *
     * @return \ProductFilter
     */
    public static function getFilter()
    {
        $filter = new ProductFilter(request()->wantsJson());
        $filter->buildFromRequest(request());

        return $filter;
    }

}
