<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonPickwareMobile\Components\PickingOrderFilter\OrderFilterConditionQueryComponent;

use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\FieldDescriptorQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\PlainSqlQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\ScalarValueQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryComponentArrayCoding\ComparisonQueryComponentArrayCoder;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryComponentArrayCoding\FieldDescriptorQueryComponentArrayCoder;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryComponentArrayCoding\QueryComponentArrayCodingService;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryComponentArrayCoding\QueryComponentArrayDecodingPreprocessor;

class GroupingConstraintQueryComponentArrayDecodingPreprocessor implements QueryComponentArrayDecodingPreprocessor
{
    const GROUPING_CONSTRAINT_AT_LEAST_ONE = 'atLeastOne';
    const GROUPING_CONSTRAINT_ALL = 'all';
    const GROUPING_CONSTRAINT_NONE = 'none';

    /**
     * @var string[] The kinds of grouping constraints, that is conditions on the result of rule that is grouped by the
     *      order ID, that are supported by this preprocessor.
     */
    const GROUPING_CONSTRAINTS = [
        self::GROUPING_CONSTRAINT_AT_LEAST_ONE,
        self::GROUPING_CONSTRAINT_ALL,
        self::GROUPING_CONSTRAINT_NONE,
    ];

    /**
     * @var QueryComponentArrayCodingService
     */
    protected $codingService;

    /**
     * @var array[]
     */
    protected $processedGroupingConstraints = [];

    /**
     * @param QueryComponentArrayCodingService $codingService
     */
    public function __construct(QueryComponentArrayCodingService $codingService)
    {
        $this->codingService = $codingService;
    }

    /**
     * {@inheritdoc}
     *
     * Matches any field comparison components that contain a grouping constraint and and changes the component data to
     * select a value from the computed order item conditions table instead and compare it based on the
     * grouping constraint.
     */
    public function process(array $arrayData)
    {
        $componentDataMatches = (
            isset($arrayData['groupingConstraint'])
            && in_array($arrayData['groupingConstraint'], self::GROUPING_CONSTRAINTS)
            && isset($arrayData['type'])
            && $arrayData['type'] === ComparisonQueryComponentArrayCoder::CODABLE_TYPE
            && isset($arrayData['leftOperand']['type'])
            && $arrayData['leftOperand']['type'] === FieldDescriptorQueryComponentArrayCoder::CODABLE_TYPE
        );
        if (!$componentDataMatches) {
            return $arrayData;
        }

        // Save the original encoded data of the grouping constrained condition
        $this->processedGroupingConstraints[] = $arrayData;

        // Adjust the left operand to point to the custom grouping constraint result
        $arrayData['leftOperand']['fieldName'] = self::createUniqueSelectAlias($arrayData);
        $arrayData['leftOperand']['tableName'] = OrderFilterConditionQueryComponent::ORDER_ITEM_CONDITIONS_TABLE_ALIAS;

        // Adjust the operator and right operand depending on the grouping constraint
        switch ($arrayData['groupingConstraint']) {
            case self::GROUPING_CONSTRAINT_AT_LEAST_ONE:
                // >= 1
                $newOperator = '>=';
                $rightOperand = new ScalarValueQueryComponent(1);
                break;
            case self::GROUPING_CONSTRAINT_ALL:
                // == item_count
                $newOperator = '=';
                $rightOperand = new FieldDescriptorQueryComponent(
                    OrderFilterConditionQueryComponent::ORDER_ITEM_CONDITIONS_TABLE_ALIAS,
                    'item_count'
                );
                break;
            case self::GROUPING_CONSTRAINT_NONE:
                // == 0
                $newOperator = '=';
                $rightOperand = new ScalarValueQueryComponent(0);
                break;
        }
        $arrayData['operator'] = $newOperator;
        $arrayData['rightOperand'] = $this->codingService->encode($rightOperand);

        return $arrayData;
    }

    /**
     * Creates the query components that need to be part of the computed order item conditions table in order to be able
     * evaluate the grouping constrained comparison components that have been processed by the called instance.
     *
     * @param QueryComponentArrayDecodingPreprocessor[] $decodingPreprocessors
     * @return array
     */
    public function createConditionQueryComponents(array $decodingPreprocessors)
    {
        $queryComponents = array_combine(
            // Prefix the keys with their original table name to make them *unique*
            array_map(
                [
                    self::class,
                    'createUniqueSelectAlias',
                ],
                $this->processedGroupingConstraints
            ),
            // Decode the original grouping constraint component and use it to construct a query component that sums all
            // rows which fulfill the original condition
            array_map(
                function (array $encodedQueryComponent) use ($decodingPreprocessors) {
                    $queryComponent = $this->codingService->decode($encodedQueryComponent, $decodingPreprocessors);

                    return new PlainSqlQueryComponent(
                        sprintf('SUM(%s)', $queryComponent->createQueryString()),
                        $queryComponent->getRequiredTables()
                    );
                },
                $this->processedGroupingConstraints
            )
        );

        if (count($queryComponents) > 0) {
            // Add the `item_count` component to be able to compate the row count against the total item count of
            // an order
            $queryComponents['item_count'] = new PlainSqlQueryComponent(
                'COUNT(`s_order_details`.`id`)',
                ['s_order_details']
            );
        }

        return $queryComponents;
    }

    /**
     * @param array $encodedQueryComponent
     * @return string An alias of the form <table_name>__<field_name>__<operator_right_operand_hash_substring>
     */
    protected static function createUniqueSelectAlias(array $encodedQueryComponent)
    {
        return (
            $encodedQueryComponent['leftOperand']['tableName']
            . '__'
            . $encodedQueryComponent['leftOperand']['fieldName']
            . '__'
            . mb_substr(
                sha1(sprintf(
                    '%s%s',
                    json_encode($encodedQueryComponent['operator']),
                    json_encode($encodedQueryComponent['rightOperand'])
                )),
                0,
                8
            )
        );
    }
}
