<?php declare(strict_types=1);

namespace Newland\NeosFiltering\Items;

use Doctrine\ORM\Query\Expr;
use Newland\Contracts\Neos\Filter\Expression;
use Newland\Contracts\Neos\Filter\FilterItem;
use Newland\Contracts\Neos\Filter\ProvidesHiddenFieldsIfObstructed;
use Newland\Contracts\Neos\Filter\QueryBoundFilterItem;
use Newland\Contracts\Neos\Filter\RangedFilterItem;
use Newland\Contracts\Neos\Filter\RangeSource;
use Newland\Contracts\Neos\Filter\StatusIndicatingFilterItem;
use Newland\Contracts\Neos\Filter\TitledFilterItem;
use Newland\NeosFiltering\StatusIndicator\StatusIndicator;
use Newland\NeosFiltering\Traits\FilterItemCommon;
use Newland\NeosFiltering\Traits\HasQueryString;
use Newland\NeosFiltering\Traits\HasTitle;
use Newland\NeosFiltering\Traits\HasVisibleState;
use Newland\NeosFiltering\Traits\NeedsDatabaseColumn;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;

/**
 * Range item: Allows the user to specify a range in which they want to search (from a given
 * minimum to a given maximum).
 */
class Range implements
    FilterItem,
    QueryBoundFilterItem,
    RangedFilterItem,
    TitledFilterItem,
    StatusIndicatingFilterItem,
    ProvidesHiddenFieldsIfObstructed
{
    use FilterItemCommon,
        NeedsDatabaseColumn,
        HasTitle,
        HasQueryString,
        HasVisibleState;

    /** @var RangeSource */
    protected $rangeSource;

    public function setRangeSource(RangeSource $rangeSource): void
    {
        $this->rangeSource = $rangeSource;
    }

    public function setState($state): void
    {
        $min = $state['min'] ?? null;
        $max = $state['max'] ?? null;
        $min = ($min !== null && $min !== '') ? (float) $min : null;
        $max = ($max !== null && $max !== '') ? (float) $max : null;

        $range = $this->range();
        if ($min > $max || $min < $range['min']) {
            $min = $range['min'];
        }
        if ($max < $min || $max > $range['max']) {
            $max = $range['max'];
        }

        $this->state = [ 'min' => $min, 'max' => $max ];
    }

    public function queryExpression(Expr $expr): Expression
    {
        $this->throwIfNoDatabaseColumnDeclared();
        $this->throwIfNoQueryString();
        $parts = [];
        $min = $this->state['min'] ?? null;
        $max = $this->state['max'] ?? null;

        if (\is_numeric($min)) {
            $parts[] = $expr->gte($this->databaseColumn, $min);
        }
        if (\is_numeric($max)) {
            $parts[] = $expr->lte($this->databaseColumn, $max);
        }

        switch (\count($parts)) {
            case 0:
                return Expression::empty();
            case 1:
                return Expression::where(array_values($parts)[0]);
            case 2:
            default:
                return Expression::where($expr->andX(...$parts));
        }
    }

    public function render(RenderingContextInterface $renderingContext)
    {
        $view = $this->initializeView();
        $view->assignMultiple($this->range());
        $view->assignMultiple(
            [
                'factor' => $this->configuration['factor'] ?? 1,
                'unit' => $this->configuration['unit'] ?? '',
                'step' => $this->configuration['steps'] ?? 1,
            ]
        );
        return $view->render('Range');
    }

    protected function range(): array
    {
        $factor = $this->configuration['factor'] ?? 1;

        if ($this->rangeSource === null) {
            // TODO throw more specific exception
            throw new \Exception('No range source set.');
        }

        $min = $this->rangeSource->min();
        $max = $this->rangeSource->max();

        return [
            'min' => $min,
            'max' => $max,
            'humanizedMin' => floor($min * $factor),
            'humanizedMax' => round($max * $factor),
        ];
    }

    /** @return StatusIndicator[] */
    public function getStatusIndicators(): array
    {
        $factor = $this->configuration['factor'] ?? 1;
        $range = $this->range();

        $min = $this->state['min'] ?? $range['min'];
        $max = $this->state['max'] ?? $range['max'];

        if ($min === $range['min'] && $max === $range['max']) {
            return [];
        }

        $id = $this->getId();
        $indicator = new StatusIndicator();
        $indicator->setId($id);
        $indicator->setResetEvent('#range-' . $id, 'range-slider.reset');

        $unit = $this->configuration['unit'] ?? null;
        $title = sprintf('%d - %d', floor($min * $factor), round($max * $factor));
        if ($unit) {
            $title .= ' ' . $unit;
        }
        $indicator->setTitle($title);

        return [ $indicator ];
    }

    public function getHiddenFieldsIfObstructed(): array
    {
        if (!\is_array($this->state)) {
            return [];
        }

        $fields = [];
        if (array_key_exists('min', $this->state)) {
            $name = sprintf('%s[min]', $this->queryString);
            $fields[$name] = $this->state['min'];
        }
        if (array_key_exists('max', $this->state)) {
            $name = sprintf('%s[max]', $this->queryString);
            $fields[$name] = $this->state['max'];
        }

        return $fields;
    }
}
