<?php

namespace SilverStripe\Forms\GridField;

use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataList;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
use LogicException;
use SilverStripe\ORM\Filters\SearchFilter;

/**
 * This class is is responsible for adding objects to another object's has_many
 * and many_many relation, as defined by the {@link RelationList} passed to the
 * {@link GridField} constructor.
 *
 * Objects can be searched through an input field (partially matching one or
 * more fields).
 *
 * Selecting from the results will add the object to the relation.
 *
 * Often used alongside {@link GridFieldDeleteAction} for detaching existing
 * records from a relationship.
 *
 * For easier setup, have a look at a sample configuration in
 * {@link GridFieldConfig_RelationEditor}.
 */
class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler
{

    /**
     * The HTML fragment to write this component into
     */
    protected $targetFragment;

    /**
     * @var SS_List
     */
    protected $searchList;

    /**
     * Define column names which should be included in the search.
     * By default, they're searched with a {@link StartsWithFilter}.
     * To define custom filters, use the same notation as {@link DataList->filter()},
     * e.g. "Name:EndsWith".
     *
     * If multiple fields are provided, the filtering is performed non-exclusive.
     * If no fields are provided, tries to auto-detect fields from
     * {@link DataObject->searchableFields()}.
     *
     * The fields support "dot-notation" for relationships, e.g.
     * a entry called "Team.Name" will search through the names of
     * a "Team" relationship.
     *
     * @example
     *  array(
     *      'Name',
     *      'Email:StartsWith',
     *      'Team.Name'
     *  )
     *
     * @var array
     */
    protected $searchFields = [];

    /**
     * @var string SSViewer template to render the results presentation
     */
    protected $resultsFormat = '$Title';

    /**
     * @var string Text shown on the search field, instructing what to search for.
     */
    protected $placeholderText;

    /**
     * @var int
     */
    protected $resultsLimit = 20;

    /**
     *
     * @param string $targetFragment
     * @param array $searchFields Which fields on the object in the list should be searched
     */
    public function __construct($targetFragment = 'before', $searchFields = null)
    {
        $this->targetFragment = $targetFragment;
        $this->searchFields = (array)$searchFields;
    }

    /**
     *
     * @param GridField $gridField
     * @return string[] - HTML
     */
    public function getHTMLFragments($gridField)
    {
        $dataClass = $gridField->getModelClass();

        $forTemplate = new ArrayData([]);
        $forTemplate->Fields = new FieldList();

        $searchField = new TextField('gridfield_relationsearch', _t('SilverStripe\\Forms\\GridField\\GridField.RelationSearch', "Relation search"));

        $searchField->setAttribute('data-search-url', Controller::join_links($gridField->Link('search')));
        $searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
        $searchField->addExtraClass('relation-search no-change-track action_gridfield_relationsearch');

        $findAction = new GridField_FormAction(
            $gridField,
            'gridfield_relationfind',
            _t('SilverStripe\\Forms\\GridField\\GridField.Find', "Find"),
            'find',
            'find'
        );
        $findAction->setAttribute('data-icon', 'relationfind');
        $findAction->addExtraClass('action_gridfield_relationfind');

        $addAction = new GridField_FormAction(
            $gridField,
            'gridfield_relationadd',
            _t('SilverStripe\\Forms\\GridField\\GridField.LinkExisting', "Link Existing"),
            'addto',
            'addto'
        );
        $addAction->setAttribute('data-icon', 'chain--plus');
        $addAction->addExtraClass('btn btn-outline-secondary font-icon-link action_gridfield_relationadd');

        // If an object is not found, disable the action
        if (!is_int($gridField->State->GridFieldAddRelation(null))) {
            $addAction->setDisabled(true);
        }

        $forTemplate->Fields->push($searchField);
        $forTemplate->Fields->push($findAction);
        $forTemplate->Fields->push($addAction);
        if ($form = $gridField->getForm()) {
            $forTemplate->Fields->setForm($form);
        }

        $template = SSViewer::get_templates_by_class($this, '', __CLASS__);
        return [
            $this->targetFragment => $forTemplate->renderWith($template)
        ];
    }

    /**
     *
     * @param GridField $gridField
     * @return array
     */
    public function getActions($gridField)
    {
        return ['addto', 'find'];
    }

    /**
     * Manipulate the state to add a new relation
     *
     * @param GridField $gridField
     * @param string $actionName Action identifier, see {@link getActions()}.
     * @param array $arguments Arguments relevant for this
     * @param array $data All form data
     */
    public function handleAction(GridField $gridField, $actionName, $arguments, $data)
    {
        switch ($actionName) {
            case 'addto':
                if (isset($data['relationID']) && $data['relationID']) {
                    $gridField->State->GridFieldAddRelation = $data['relationID'];
                }
                break;
        }
    }

    /**
     * If an object ID is set, add the object to the list
     *
     * @param GridField $gridField
     * @param SS_List $dataList
     * @return SS_List
     */
    public function getManipulatedData(GridField $gridField, SS_List $dataList)
    {
        $objectID = $gridField->State->GridFieldAddRelation(null);
        if (empty($objectID)) {
            return $dataList;
        }
        $object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
        if ($object) {
            $dataList->add($object);
        }
        $gridField->State->GridFieldAddRelation = null;
        return $dataList;
    }

    /**
     *
     * @param GridField $gridField
     * @return array
     */
    public function getURLHandlers($gridField)
    {
        return [
            'search' => 'doSearch',
        ];
    }

    /**
     * Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
     *
     * @param GridField $gridField
     * @param HTTPRequest $request
     * @return string
     */
    public function doSearch($gridField, $request)
    {
        $searchStr = $request->getVar('gridfield_relationsearch');
        $dataClass = $gridField->getModelClass();

        $searchFields = ($this->getSearchFields())
            ? $this->getSearchFields()
            : $this->scaffoldSearchFields($dataClass);
        if (!$searchFields) {
            throw new LogicException(
                sprintf(
                    'GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"',
                    $dataClass
                )
            );
        }

        $params = [];
        foreach ($searchFields as $searchField) {
            $name = (strpos($searchField, ':') !== false) ? $searchField : "$searchField:StartsWith";
            $params[$name] = $searchStr;
        }

        $results = null;
        if ($this->searchList) {
            // Assume custom sorting, don't apply default sorting
            $results = $this->searchList;
        } else {
            $results = DataList::create($dataClass)
                ->sort(strtok($searchFields[0], ':'), 'ASC');
        }

        // Apply baseline filtering and limits which should hold regardless of any customisations
        $results = $results
            ->subtract($gridField->getList())
            ->filterAny($params)
            ->limit($this->getResultsLimit());

        $json = [];
        Config::nest();
        SSViewer::config()->update('source_file_comments', false);
        $viewer = SSViewer::fromString($this->resultsFormat);
        foreach ($results as $result) {
            $title = Convert::html2raw($viewer->process($result));
            $json[] = [
                'label' => $title,
                'value' => $title,
                'id' => $result->ID,
            ];
        }
        Config::unnest();
        $response = new HTTPResponse(json_encode($json));
        $response->addHeader('Content-Type', 'application/json');
        return $response;
    }

    /**
     * @param string $format
     *
     * @return $this
     */
    public function setResultsFormat($format)
    {
        $this->resultsFormat = $format;
        return $this;
    }

    /**
     * @return string
     */
    public function getResultsFormat()
    {
        return $this->resultsFormat;
    }

    /**
     * Sets the base list instance which will be used for the autocomplete
     * search.
     *
     * @param SS_List $list
     */
    public function setSearchList(SS_List $list)
    {
        $this->searchList = $list;
        return $this;
    }

    /**
     * @param array $fields
     * @return $this
     */
    public function setSearchFields($fields)
    {
        $this->searchFields = $fields;
        return $this;
    }

    /**
     * @return array
     */
    public function getSearchFields()
    {
        return $this->searchFields;
    }

    /**
     * Detect searchable fields and searchable relations.
     * Falls back to {@link DataObject->summaryFields()} if
     * no custom search fields are defined.
     *
     * @param string $dataClass The class name
     * @return array|null names of the searchable fields
     */
    public function scaffoldSearchFields($dataClass)
    {
        $obj = DataObject::singleton($dataClass);
        $fields = null;
        if ($fieldSpecs = $obj->searchableFields()) {
            $customSearchableFields = $obj->config()->get('searchable_fields');
            foreach ($fieldSpecs as $name => $spec) {
                if (is_array($spec) && array_key_exists('filter', $spec)) {
                    // The searchableFields() spec defaults to PartialMatch,
                    // so we need to check the original setting.
                    // If the field is defined $searchable_fields = array('MyField'),
                    // then default to StartsWith filter, which makes more sense in this context.
                    if (!$customSearchableFields || array_search($name, $customSearchableFields)) {
                        $filter = 'StartsWith';
                    } else {
                        $filterName = $spec['filter'];
                        // It can be an instance
                        if ($filterName instanceof SearchFilter) {
                            $filterName = get_class($filterName);
                        }
                        // It can be a fully qualified class name
                        if (strpos($filterName, '\\') !== false) {
                            $filterNameParts = explode("\\", $filterName);
                            // We expect an alias matching the class name without namespace, see #coresearchaliases
                            $filterName = array_pop($filterNameParts);
                        }
                        $filter = preg_replace('/Filter$/', '', $filterName);
                    }
                    $fields[] = "{$name}:{$filter}";
                } else {
                    $fields[] = $name;
                }
            }
        }
        if (is_null($fields)) {
            if ($obj->hasDatabaseField('Title')) {
                $fields = ['Title'];
            } elseif ($obj->hasDatabaseField('Name')) {
                $fields = ['Name'];
            }
        }

        return $fields;
    }

    /**
     * @param string $dataClass The class of the object being searched for
     *
     * @return string
     */
    public function getPlaceholderText($dataClass)
    {
        $searchFields = ($this->getSearchFields())
            ? $this->getSearchFields()
            : $this->scaffoldSearchFields($dataClass);

        if ($this->placeholderText) {
            return $this->placeholderText;
        } else {
            $labels = [];
            if ($searchFields) {
                foreach ($searchFields as $searchField) {
                    $searchField = explode(':', $searchField);
                    $label = singleton($dataClass)->fieldLabel($searchField[0]);
                    if ($label) {
                        $labels[] = $label;
                    }
                }
            }
            if ($labels) {
                return _t(
                    'SilverStripe\\Forms\\GridField\\GridField.PlaceHolderWithLabels',
                    'Find {type} by {name}',
                    ['type' => singleton($dataClass)->i18n_plural_name(), 'name' => implode(', ', $labels)]
                );
            } else {
                return _t(
                    'SilverStripe\\Forms\\GridField\\GridField.PlaceHolder',
                    'Find {type}',
                    ['type' => singleton($dataClass)->i18n_plural_name()]
                );
            }
        }
    }

    /**
     * @param string $text
     *
     * @return $this
     */
    public function setPlaceholderText($text)
    {
        $this->placeholderText = $text;
        return $this;
    }

    /**
     * Gets the maximum number of autocomplete results to display.
     *
     * @return int
     */
    public function getResultsLimit()
    {
        return $this->resultsLimit;
    }

    /**
     * @param int $limit
     *
     * @return $this
     */
    public function setResultsLimit($limit)
    {
        $this->resultsLimit = $limit;
        return $this;
    }
}
