<?php

namespace WapplerSystems\Meilisearch\Domain\Search\Query;

/***************************************************************
 *  Copyright notice
 *
 *  (c) 2017 <timo.hund@dkd.de>
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\BigramPhraseFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Elevation;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Faceting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\FieldCollapsing;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Grouping;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Highlighting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Operator;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\PhraseFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\QueryFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\ReturnFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Slops;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Sorting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Sortings;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Spellchecking;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\TrigramPhraseFields;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

/**
 * The AbstractQueryBuilder contains all logic to initialize solr queries independent from TYPO3.
 */
abstract class AbstractQueryBuilder {

    /**
     * @var Query
     */
    protected $queryToBuild = null;

    /**
     * @param Query $query
     * @return $this
     */
    public function startFrom(Query $query)
    {
        $this->queryToBuild = $query;
        return $this;
    }

    /**
     * @return Query
     */
    public function getQuery(): Query
    {
        return $this->queryToBuild;
    }

    /**
     * @param bool $omitHeader
     * @return $this
     */
    public function useOmitHeader($omitHeader = true)
    {
        $this->queryToBuild->setOmitHeader($omitHeader);

        return $this;
    }

    /**
     * Uses an array of filters and applies them to the query.
     *
     * @param array $filterArray
     * @return $this
     */
    public function useFilterArray(array $filterArray)
    {
        foreach ($filterArray as $key => $additionalFilter) {
            $this->useFilter($additionalFilter, $key);
        }

        return $this;
    }

    /**
     * Applies the queryString that is used to search
     *
     * @param string $queryString
     * @return $this
     */
    public function useQueryString($queryString)
    {
        $this->queryToBuild->setQuery($queryString);
        return $this;
    }

    /**
     * Applies the passed queryType to the query.
     *
     * @param string $queryType
     * @return $this
     */
    public function useQueryType(string $queryType)
    {
        $this->queryToBuild->addParam('qt', $queryType);
        return $this;
    }

    /**
     * Remove the queryType (qt) from the query.
     *
     * @return $this
     */
    public function removeQueryType()
    {
        $this->queryToBuild->addParam('qt', null);
        return $this;
    }

    /**
     * Can be used to remove all sortings from the query.
     *
     * @return $this
     */
    public function removeAllSortings()
    {
        $this->queryToBuild->clearSorts();
        return $this;
    }

    /**
     * Applies the passed sorting to the query.
     *
     * @param Sorting $sorting
     * @return $this
     */
    public function useSorting(Sorting $sorting)
    {
        if (strpos($sorting->getFieldName(), 'relevance') !== false) {
            $this->removeAllSortings();
            return $this;
        }

        $this->queryToBuild->addSort($sorting->getFieldName(), $sorting->getDirection());
        return $this;
    }

    /**
     * Applies the passed sorting to the query.
     *
     * @param Sortings $sortings
     * @return $this
     */
    public function useSortings(Sortings $sortings)
    {
        foreach($sortings->getSortings() as $sorting) {
            $this->useSorting($sorting);
        }

        return $this;
    }

    /**
     * @param int $resultsPerPage
     * @return $this
     */
    public function useResultsPerPage($resultsPerPage)
    {
        $this->queryToBuild->setRows($resultsPerPage);
        return $this;
    }

    /**
     * @param int $page
     * @return $this
     */
    public function usePage($page)
    {
        $this->queryToBuild->setStart($page);
        return $this;
    }

    /**
     * @param Operator $operator
     * @return $this
     */
    public function useOperator(Operator $operator)
    {
        $this->queryToBuild->setQueryDefaultOperator( $operator->getOperator());
        return $this;
    }

    /**
     * Remove the default query operator.
     *
     * @return $this
     */
    public function removeOperator()
    {
        $this->queryToBuild->setQueryDefaultOperator('');
        return $this;
    }

    /**
     * @param Slops $slops
     * @return $this
     */
    public function useSlops(Slops $slops)
    {
        return $slops->build($this);
    }

    /**
     * Uses the passed boostQuer(y|ies) for the query.
     *
     * @param string|array $boostQueries
     * @return $this
     */
    public function useBoostQueries($boostQueries)
    {
        $boostQueryArray = [];
        if(is_array($boostQueries)) {
            foreach($boostQueries as $boostQuery) {
                $boostQueryArray[] = ['key' => md5($boostQuery), 'query' => $boostQuery];
            }
        } else {
            $boostQueryArray[] = ['key' => md5($boostQueries), 'query' => $boostQueries];
        }

        $this->queryToBuild->getEDisMax()->setBoostQueries($boostQueryArray);
        return $this;
    }

    /**
     * Removes all boost queries from the query.
     *
     * @return $this
     */
    public function removeAllBoostQueries()
    {
        $this->queryToBuild->getEDisMax()->clearBoostQueries();
        return $this;
    }

    /**
     * Uses the passed boostFunction for the query.
     *
     * @param string $boostFunction
     * @return $this
     */
    public function useBoostFunction(string $boostFunction)
    {
        $this->queryToBuild->getEDisMax()->setBoostFunctions($boostFunction);
        return $this;
    }

    /**
     * Removes all previously configured boost functions.
     *
     * @return $this
     */
    public function removeAllBoostFunctions()
    {
        $this->queryToBuild->getEDisMax()->setBoostFunctions('');
        return $this;
    }


    /**
     * Uses the passed minimumMatch(mm) for the query.
     *
     * @param string $minimumMatch
     * @return $this
     */
    public function useMinimumMatch(string $minimumMatch)
    {
        $this->queryToBuild->getEDisMax()->setMinimumMatch($minimumMatch);
        return $this;
    }

    /**
     * Remove any previous passed minimumMatch parameter.
     *
     * @return $this
     */
    public function removeMinimumMatch()
    {
        $this->queryToBuild->getEDisMax()->setMinimumMatch('');
        return $this;
    }


    /**
     * Applies the tie parameter to the query.
     *
     * @param mixed $tie
     * @return $this
     */
    public function useTieParameter($tie)
    {
        $this->queryToBuild->getEDisMax()->setTie($tie);
        return $this;
    }

    /**
     * Applies custom QueryFields to the query.
     *
     * @param QueryFields $queryFields
     * @return $this
     */
    public function useQueryFields(QueryFields $queryFields)
    {
        return $queryFields->build($this);
    }

    /**
     * Applies custom ReturnFields to the query.
     *
     * @param ReturnFields $returnFields
     * @return $this
     */
    public function useReturnFields(ReturnFields $returnFields)
    {
        return $returnFields->build($this);
    }

    /**
     * Can be used to use a specific filter string in the solr query.
     *
     * @param string $filterString
     * @param string $filterName
     * @return $this
     */
    public function useFilter($filterString, $filterName = '')
    {
        $filterName = $filterName === '' ? $filterString : $filterName;

        $nameWasPassedAndFilterIsAllreadySet = $filterName !== '' && $this->queryToBuild->getFilterQuery($filterName) !== null;
        if($nameWasPassedAndFilterIsAllreadySet) {
            return $this;
        }
        $this->queryToBuild->addFilterQuery(['key' => $filterName, 'query' => $filterString]);
        return $this;
    }

    /**
     * Removes a filter by the fieldName.
     *
     * @param string $fieldName
     * @return $this
     */
    public function removeFilterByFieldName($fieldName)
    {
        return $this->removeFilterByFunction(
            function($key, $query) use ($fieldName) {
                $queryString = $query->getQuery();
                $storedFieldName = substr($queryString,0, strpos($queryString, ":"));
                return $storedFieldName == $fieldName;
            }
        );
    }

    /**
     * Removes a filter by the name of the filter (also known as key).
     *
     * @param string $name
     * @return $this
     */
    public function removeFilterByName($name)
    {
        return $this->removeFilterByFunction(
            function($key, $query) use ($name) {
                $key = $query->getKey();
                return $key == $name;
            }
        );
    }

    /**
     * Removes a filter by the filter value.
     *
     * @param string $value
     * @return $this
     */
    public function removeFilterByValue($value)
    {
        return $this->removeFilterByFunction(
            function($key, $query) use ($value) {
                $query = $query->getQuery();
                return $query == $value;
            }
        );
    }

    /**
     * @param \Closure $filterFunction
     * @return $this
     */
    public function removeFilterByFunction($filterFunction)
    {
        $queries = $this->queryToBuild->getFilterQueries();
        foreach($queries as $key =>  $query) {
            $canBeRemoved = $filterFunction($key, $query);
            if($canBeRemoved) {
                unset($queries[$key]);
            }
        }

        $this->queryToBuild->setFilterQueries($queries);
        return $this;
    }

    /**
     * Passes the alternative query to the Query
     * @param string $query
     * @return $this
     */
    public function useAlternativeQuery(string $query)
    {
        $this->queryToBuild->getEDisMax()->setQueryAlternative($query);
        return $this;
    }

    /**
     * Remove the alternative query from the Query.
     *
     * @return $this
     */
    public function removeAlternativeQuery()
    {
        $this->queryToBuild->getEDisMax()->setQueryAlternative(null);
        return $this;
    }

    /**
     * Applies a custom Faceting configuration to the query.
     *
     * @param Faceting $faceting
     * @return $this
     */
    public function useFaceting(Faceting $faceting)
    {
        return $faceting->build($this);
    }

    /**
     * @param FieldCollapsing $fieldCollapsing
     * @return $this
     */
    public function useFieldCollapsing(FieldCollapsing $fieldCollapsing)
    {
        return $fieldCollapsing->build($this);
    }

    /**
     * Applies a custom initialized grouping to the query.
     *
     * @param Grouping $grouping
     * @return $this
     */
    public function useGrouping(Grouping $grouping)
    {
        return $grouping->build($this);
    }

    /**
     * @param Highlighting $highlighting
     * @return $this
     */
    public function useHighlighting(Highlighting $highlighting)
    {
        return $highlighting->build($this);
    }

    /**
     * @param boolean $debugMode
     * @return $this
     */
    public function useDebug($debugMode)
    {
        if (!$debugMode) {
            $this->queryToBuild->addParam('debugQuery', null);
            $this->queryToBuild->addParam('echoParams', null);
            return $this;
        }

        $this->queryToBuild->addParam('debugQuery', 'true');
        $this->queryToBuild->addParam('echoParams', 'all');

        return $this;
    }

    /**
     * @param Elevation $elevation
     * @return QueryBuilder
     */
    public function useElevation(Elevation $elevation)
    {
        return $elevation->build($this);
    }

    /**
     * @param Spellchecking $spellchecking
     * @return $this
     */
    public function useSpellchecking(Spellchecking $spellchecking)
    {
        return $spellchecking->build($this);
    }

    /**
     * Applies a custom configured PhraseFields to the query.
     *
     * @param PhraseFields $phraseFields
     * @return $this
     */
    public function usePhraseFields(PhraseFields $phraseFields)
    {
        return $phraseFields->build($this);
    }

    /**
     * Applies a custom configured BigramPhraseFields to the query.
     *
     * @param BigramPhraseFields $bigramPhraseFields
     * @return $this
     */
    public function useBigramPhraseFields(BigramPhraseFields $bigramPhraseFields)
    {
        return $bigramPhraseFields->build($this);
    }

    /**
     * Applies a custom configured TrigramPhraseFields to the query.
     *
     * @param TrigramPhraseFields $trigramPhraseFields
     * @return $this
     */
    public function useTrigramPhraseFields(TrigramPhraseFields $trigramPhraseFields)
    {
        return $trigramPhraseFields->build($this);
    }
}
