<?php declare(strict_types=1);

namespace Newland\Toubiz\Poi\Neos\Filter\Items;

use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Newland\NeosFiltering\Contract\Expression;
use Newland\NeosFiltering\Items\CheckboxList;
use Newland\Toubiz\Sync\Neos\Domain\Model\Attribute;

/**
 * Checkbox list for article attributes.
 * Please use `entity.Persistence_Object_Identifier` as the database column.
 *
 * The data sources used must provide values in the format `{attributeName}:{attributeValue}`.
 * If you wan't nicer looking strings in your URL you can use the `queryString` feature that
 * is documented on the regular checkbox list.
 *
 * If you need to select all attributes with a given `attributeName` you can provide values in the
 * filter configuration with the format `{attributeName}:*` which will return all instances of
 * attributes with that name.
 *
 * By default the attribute type is assumed to be `string`. If you want to use a different
 * attribute type then you can use the `attributeType` option in your data source.
 */
class AttributeCheckboxList extends CheckboxList
{
    use AttributeCommon;

    public function queryExpression(Expr $expr): Expression
    {
        if (\count((array) $this->state) === 0) {
            return Expression::empty();
        }

        return Expression::where($expr->in($this->databaseColumn, $this->buildSubQuery()->getDQL()));
    }


    private function buildSubQuery(): QueryBuilder
    {
        $alias = uniqid('attribute', false);
        $query = $this->initializeSubQuery($alias);

        $parts = $this->getPartsForSubquery($query->expr(), $alias);
        $query->andWhere($query->expr()->orX(...$parts));

        if ($this->combine === static::$AND && \count($this->state) > 1) {
            $query->groupBy(sprintf('%s.article', $alias));
            $query->having($query->expr()->eq($query->expr()->countDistinct($alias), \count($this->state)));
        }

        return $query;
    }

    private function getPartsForSubquery(Expr $expr, string $alias): array
    {
        $parts = [];

        foreach ($this->getValuesForQueryFromCurrentState() as $attributeName => $typesAndValues) {
            foreach ($typesAndValues as $attributeType => $values) {
                if (in_array('*', $values, true)) {
                    $parts[] = $expr->andX(
                        $expr->eq(sprintf('%s.name', $alias), $expr->literal($attributeName)),
                        $expr->eq(sprintf('%s.type', $alias), $expr->literal($attributeType))
                    );
                } else {
                    $parts[] = $expr->andX(
                        $expr->eq(sprintf('%s.name', $alias), $expr->literal($attributeName)),
                        $expr->eq(sprintf('%s.type', $alias), $expr->literal($attributeType)),
                        $expr->in(sprintf('%s.data', $alias), $values)
                    );
                }
            }
        }
        return $parts;
    }

    /**
     * Returns a multi-dimensional array of the following format:
     * - First layer uses attribute names as keys
     * - Second layer uses attribute types as keys
     * - Leafs are arrays of attribute values.
     *
     * @return array
     */
    private function getValuesForQueryFromCurrentState(): array
    {
        $values = [];
        foreach ($this->state as $state) {
            if (strpos($state['value'], ':') === false) {
                continue;
            }

            [ $attributeName, $attributeValue ] = explode(':', $state['value'], 2);
            $attributeType = $state['attributeType'] ?? Attribute::TYPE_STRING;
            if (!array_key_exists($attributeName, $values)) {
                $values[$attributeName] = [];
            }
            if (!array_key_exists($attributeType, $values[$attributeName])) {
                $values[$attributeName][$attributeType] = [];
            }
            $values[$attributeName][$attributeType][] = $attributeValue;
        }
        return $values;
    }
}
