<?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\AbstractJoinQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\ComparisonQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\FieldDescriptorQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\GroupByQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\PlainSqlQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\QueryBuilder;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\ScalarValueQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\SelectQueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\SubQueryJoinQueryComponent;
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 OrderItemCategoryConditionQueryComponentArrayDecodingPreprocessor implements QueryComponentArrayDecodingPreprocessor
{
    /**
     * @var string
     */
    const ORDER_ITEM_CATEGORIES_TABLE_ALIAS = 'order_item_categories';

    /**
     * @var string
     */
    const CATEGORY_ID_FIELD_NAME = '__category_id';

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

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

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

    /**
     * {@inheritdoc}
     *
     * Matches any field descriptor components that select the special category ID field from the `s_articles_details`
     * table and changes the component data to select a value from the computed order item categories table instead.
     */
    public function process(array $arrayData)
    {
        $componentDataMatches = (
            isset($arrayData['type'])
            && $arrayData['type'] === ComparisonQueryComponentArrayCoder::CODABLE_TYPE
            && isset($arrayData['leftOperand']['type'])
            && $arrayData['leftOperand']['type'] === FieldDescriptorQueryComponentArrayCoder::CODABLE_TYPE
            && $arrayData['leftOperand']['tableName'] === 's_articles_details'
            && $arrayData['leftOperand']['fieldName'] === self::CATEGORY_ID_FIELD_NAME
        );
        if (!$componentDataMatches) {
            return $arrayData;
        }

        // Save the original encoded data of the category condition
        $this->processedConditions[] = $arrayData;

        // Adjust the left operand to evaluate whether the respective condition in the categories table is fulfilled
        $arrayData['leftOperand']['tableName'] = self::ORDER_ITEM_CATEGORIES_TABLE_ALIAS;
        $arrayData['leftOperand']['fieldName'] = self::createUniqueSelectAlias($arrayData);
        $arrayData['operator'] = '=';
        $arrayData['rightOperand'] = $this->codingService->encode(new ScalarValueQueryComponent(1));

        return $arrayData;
    }

    /**
     * Creates and returns a new {@link SubQueryJoinQueryComponent} that joins the evaluated order item category
     * conditions that have been processed by the called instance. The selected values are aliased to the same names
     * as they were injected when processing the encoded query components.
     *
     * @return SubQueryJoinQueryComponent|null
     */
    public function createCategorySubqueryJoinQueryComponent()
    {
        if (count($this->processedConditions) === 0) {
            return null;
        }

        // Create the main query that evaluates whether the processed category conditions are fulfilled for each article
        $queryBuilder = new QueryBuilder();
        $queryBuilder
            ->select(
                new SelectQueryComponent(
                    new FieldDescriptorQueryComponent('s_articles_categories_ro', 'articleID'),
                    'articleId'
                )
            )
            ->from('s_articles_categories_ro')
            ->groupBy(
                new GroupByQueryComponent(new FieldDescriptorQueryComponent('s_articles_categories_ro', 'articleID')),
                new GroupByQueryComponent(new FieldDescriptorQueryComponent('s_articles_categories_ro', 'categoryID'))
            );

        // Deduplicate the query components that need to be selected. This ensures that the same condition (same alias
        // i.e. same operator and right operand) is not selected twice.
        $selectableRightOperands = array_combine(
            array_map(
                [
                    self::class,
                    'createUniqueSelectAlias',
                ],
                $this->processedConditions
            ),
            array_map(
                function (array $encodedQueryComponent) {
                    return $this->codingService->decode($encodedQueryComponent)->getRightOperand();
                },
                $this->processedConditions
            )
        );

        // Add all category conditions as selected values, using the same aliases as were used when processing the
        // encoded query components
        $queryBuilder->addSelect(...array_map(
            function ($selectAlias, ScalarValueQueryComponent $rightOperand) {
                return new SelectQueryComponent(
                    new ComparisonQueryComponent(
                        new FieldDescriptorQueryComponent('s_articles_categories_ro', 'categoryID'),
                        'IN',
                        $rightOperand
                    ),
                    $selectAlias
                );
            },
            array_keys($selectableRightOperands),
            $selectableRightOperands
        ));

        // Create a SELECT query that wraps the main query result to reduce it to one row per article. This is necessary
        // since we must not fan out any queries which the returned subquery is joined to. We reduce the "dimensions" by
        // applying `MAX()` to all selected condition results of the main query. The result of the new query consists of
        // one row per article, each consisting of the article ID and the result (1 or 0) of each category condition.
        $categoryConditionSelectQueryStrings = array_map(
            function ($selectAlias) {
                $selectQueryComponent = new SelectQueryComponent(
                    new PlainSqlQueryComponent(sprintf('MAX(`category_tmp`.`%s`)', $selectAlias), []),
                    $selectAlias
                );

                return $selectQueryComponent->createQueryString();
            },
            array_keys($selectableRightOperands)
        );
        $sql = sprintf(
            'SELECT `category_tmp`.`articleId` AS `articleId`, %1$s
            FROM (%2$s) AS `category_tmp`
            GROUP BY `category_tmp`.`articleId`',
            implode(', ', $categoryConditionSelectQueryStrings),
            $queryBuilder->getSql()
        );

        return new SubQueryJoinQueryComponent(
            AbstractJoinQueryComponent::JOIN_OPERATOR_LEFT_JOIN,
            $sql,
            self::ORDER_ITEM_CATEGORIES_TABLE_ALIAS,
            new PlainSqlQueryComponent(
                sprintf('`%s`.`articleId` = `s_articles_details`.`articleID`', self::ORDER_ITEM_CATEGORIES_TABLE_ALIAS),
                ['s_articles_details']
            )
        );
    }

    /**
     * @param array $encodedQueryComponent
     * @return string An alias of the form category_group__<operator_right_operand_hash_substring>
     */
    protected static function createUniqueSelectAlias(array $encodedQueryComponent)
    {
        $comparisonHash = sha1(
            json_encode($encodedQueryComponent['operator']) . json_encode($encodedQueryComponent['rightOperand'])
        );

        return 'category_group__' . mb_substr($comparisonHash, 0, 8);
    }
}
