<?php

/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2020-2021 (original work) Open Assessment Technologies SA;
 */

declare(strict_types=1);

namespace oat\tao\helpers\form;

use common_Logger;
use core_kernel_classes_Class;
use core_kernel_classes_Literal;
use core_kernel_classes_Property;
use core_kernel_classes_Resource;
use oat\generis\model\OntologyRdfs;
use oat\oatbox\service\ConfigurableService;
use oat\tao\helpers\form\elements\TreeAware;
use oat\tao\model\Lists\Business\Domain\ValueCollection;
use oat\tao\model\Lists\Business\Domain\ValueCollectionSearchRequest;
use oat\tao\model\Lists\Business\Input\ValueCollectionSearchInput;
use oat\tao\model\Lists\Business\Service\ValueCollectionService;
use oat\tao\model\TaoOntology;
use tao_helpers_Context;
use tao_helpers_form_elements_AsyncFile as AsyncFile;
use tao_helpers_form_elements_Authoring as Authoring;
use tao_helpers_form_elements_GenerisAsyncFile as GenerisAsyncFile;
use tao_helpers_form_FormElement;
use tao_helpers_form_FormFactory;
use tao_helpers_Uri;

class ElementMapFactory extends ConfigurableService
{
    public const SERVICE_ID = 'tao/ElementMapFactory';

    /** @var core_kernel_classes_Resource */
    private $instance;

    public function withInstance(core_kernel_classes_Resource $instance): self
    {
        $this->instance = $instance;

        return $this;
    }

    public function create(core_kernel_classes_Property $property): ?tao_helpers_form_FormElement
    {
        //create the element from the right widget
        $property->feed();

        $widgetResource = $property->getWidget();
        if (null === $widgetResource) {
            return null;
        }

        $widgetUri   = $widgetResource->getUri();
        $propertyUri = $property->getUri();

        //authoring widget is not used in standalone mode
        if (
            $widgetUri === Authoring::WIDGET_ID
            && tao_helpers_Context::check('STANDALONE_MODE')
        ) {
            return null;
        }

        // horrible hack to fix file widget
        if ($widgetUri === AsyncFile::WIDGET_ID) {
            $widgetResource = new core_kernel_classes_Resource(GenerisAsyncFile::WIDGET_ID);
        }

        $element = tao_helpers_form_FormFactory::getElementByWidget(
            tao_helpers_Uri::encode($propertyUri),
            $widgetResource
        );

        if (null === $element) {
            return null;
        }

        $parentProperty = $this->getParentProperty($property);

        if ($parentProperty) {
            $element->addAttribute('data-depends-on-property', tao_helpers_Uri::encode($parentProperty->getUri()));
        }

        if ($element->getWidget() !== $widgetUri) {
            common_Logger::w(sprintf(
                'Widget definition differs from implementation: %s != %s',
                $element->getWidget(),
                $widgetUri
            ));

            return null;
        }

        //use the property label as element description
        $propDesc = (trim($property->getLabel()) !== '')
            ? $property->getLabel()
            : str_replace(LOCAL_NAMESPACE, '', $propertyUri);

        $element->setDescription($propDesc);

        //multi elements use the property range as options
        if (method_exists($element, 'setOptions')) {
            $range = $property->getRange();

            if ($range !== null) {
                $options = [];

                if ($element instanceof TreeAware) {
                    $sortedOptions = $element->rangeToTree(
                        $propertyUri === OntologyRdfs::RDFS_RANGE
                            ? new core_kernel_classes_Class(OntologyRdfs::RDFS_RESOURCE)
                            : $range
                    );
                } else {
                    if ($this->isList($range)) {
                        $values = $this->getListValues($property, $range, $parentProperty);

                        foreach ($values as $value) {
                            $encodedUri = tao_helpers_Uri::encode($value->getUri());
                            $options[$encodedUri] = [$encodedUri, $value->getLabel()];
                        }
                    } else {
                        foreach ($range->getInstances(true) as $rangeInstance) {
                            $level = $rangeInstance->getOnePropertyValue(
                                new core_kernel_classes_Property(TaoOntology::PROPERTY_LIST_LEVEL)
                            );
                            if (null === $level) {
                                $encodedUri = tao_helpers_Uri::encode($rangeInstance->getUri());
                                $options[$encodedUri] = [$encodedUri, $rangeInstance->getLabel()];
                            } else {
                                $level = ($level instanceof core_kernel_classes_Resource)
                                    ? $level->getUri()
                                    : (string)$level;
                                $options[$level] = [
                                    tao_helpers_Uri::encode($rangeInstance->getUri()),
                                    $rangeInstance->getLabel()
                                ];
                            }
                        }
                    }
                    ksort($options);
                    $sortedOptions = [];
                    foreach ($options as $id => $values) {
                        $sortedOptions[$values[0]] = $values[1];
                    }
                    //set the default value to an empty space
                    if (method_exists($element, 'setEmptyOption')) {
                        $element->setEmptyOption(' ');
                    }
                }

                //complete the options listing
                $element->setOptions($sortedOptions);
            }
        }

        foreach (ValidationRuleRegistry::getRegistry()->getValidators($property) as $validator) {
            $element->addValidator($validator);
        }

        return $element;
    }

    private function isList($range): bool
    {
        if (!$range->isClass()) {
            return false;
        }

        return $range->isSubClassOf(
            new core_kernel_classes_Class(TaoOntology::CLASS_URI_LIST)
        );
    }

    private function getValueCollectionService(): ValueCollectionService
    {
        /** @noinspection PhpIncompatibleReturnTypeInspection */
        return $this->getServiceLocator()->get(ValueCollectionService::class);
    }

    private function getParentProperty(core_kernel_classes_Property $property): ?core_kernel_classes_Property
    {
        $collection = $property->getDependsOnPropertyCollection();

        return $collection->offsetExists(0)
            ? $collection->offsetGet(0)
            : null;
    }

    private function getListValues(
        core_kernel_classes_Property $property,
        core_kernel_classes_Resource $range,
        core_kernel_classes_Property $parentProperty = null
    ): ValueCollection {
        $searchRequest = (new ValueCollectionSearchRequest())->setValueCollectionUri($range->getUri());

        if ($this->instance instanceof core_kernel_classes_Resource) {
            $selectedValue = $this->instance->getOnePropertyValue($property);

            if ($selectedValue instanceof core_kernel_classes_Literal && !empty($selectedValue->literal)) {
                $searchRequest->setSelectedValues($selectedValue->literal);
            }

            if ($parentProperty) {
                $parentPropertyValues = [];

                foreach ($this->instance->getPropertyValuesCollection($parentProperty) as $parentPropertyValue) {
                    $parentPropertyValues[] = (string)$parentPropertyValue;
                }

                $searchRequest->setPropertyUri($property->getUri());
                $searchRequest->setParentListValues(...$parentPropertyValues);
            }
        }

        return $this->getValueCollectionService()
            ->findAll(new ValueCollectionSearchInput($searchRequest));
    }
}