<?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) 2015-2021 Open Assessment Technologies S.A.
 */

declare(strict_types=1);

use oat\generis\model\WidgetRdf;
use oat\generis\model\GenerisRdf;
use oat\oatbox\event\EventManager;
use oat\tao\model\dto\OldProperty;
use oat\generis\model\OntologyRdfs;
use oat\oatbox\log\LoggerAwareTrait;
use oat\generis\model\OntologyAwareTrait;
use oat\tao\model\search\tasks\IndexTrait;
use oat\tao\model\search\index\OntologyIndex;
use oat\tao\model\event\ClassFormUpdatedEvent;
use oat\tao\helpers\form\ValidationRuleRegistry;
use oat\tao\model\featureFlag\FeatureFlagChecker;
use oat\tao\model\event\ClassPropertiesChangedEvent;
use oat\tao\model\search\index\OntologyIndexService;
use oat\tao\model\validator\PropertyChangedValidator;
use oat\tao\model\AdvancedSearch\AdvancedSearchChecker;
use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
use oat\generis\model\resource\DependsOnPropertyCollection;
use oat\tao\model\ClassProperty\RemoveClassPropertyService;
use oat\tao\model\ClassProperty\AddClassPropertyFormFactory;
use oat\tao\model\Lists\Business\Service\RemoteSourcedListOntology;
use oat\tao\model\Lists\Business\Service\DependsOnPropertySynchronizer;
use oat\tao\model\Lists\DataAccess\Repository\DependsOnPropertyRepository;
use oat\tao\model\Lists\Business\Domain\DependsOnPropertySynchronizerContext;
use oat\tao\model\Lists\Business\Contract\DependsOnPropertyRepositoryInterface;
use oat\tao\model\Lists\Business\Contract\DependsOnPropertySynchronizerInterface;
use oat\tao\model\Lists\DataAccess\Repository\ParentPropertyListCachedRepository;

/**
 * Regrouping all actions related to authoring
 * of properties
 */
class tao_actions_PropertiesAuthoring extends tao_actions_CommonModule
{
    use OntologyAwareTrait;
    use LoggerAwareTrait;
    use IndexTrait;

    /**
     * @return EventManager
     */
    protected function getEventManager(): EventManager
    {
        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
    }

    /**
     * @requiresRight id READ
     */
    public function index(): void
    {
        $this->defaultData();
        $class = $this->getClass($this->getRequestParameter('id'));

        $myForm = $this->getClassForm($class);
        if ($myForm->isSubmited()) {
            if ($myForm->isValid()) {
                if ($class instanceof core_kernel_classes_Resource) {
                    $this->setData("selectNode", tao_helpers_Uri::encode($class->getUri()));
                    $properties = $this->hasRequestParameter('properties') ? $this->getRequestParameter('properties') : [];
                    $this->getEventManager()->trigger(new ClassFormUpdatedEvent($class, $properties));
                }
                $this->setData('message', __('%s Class saved', $class->getLabel()));
                $this->setData('reload', false);
            }
        }
        $this->setData('formTitle', __('Manage class schema'));
        $this->setData('myForm', $myForm->render());
        $this->setView('form.tpl', 'tao');
    }

    /**
     * Render the add property sub form.
     *
     * @requiresRight id WRITE
     */
    public function addClassProperty(AddClassPropertyFormFactory $addClassPropertyFormFactory): void
    {
        if (!$this->isXmlHttpRequest()) {
            throw new common_exception_BadRequest('wrong request mode');
        }

        $myForm = $addClassPropertyFormFactory->add(
            $this->getPsrRequest(),
            $this->hasWriteAccessToAction(__FUNCTION__)
        );

        $this->setData('data', $myForm->renderElements());
        $this->setView('blank.tpl', 'tao');
    }

    /**
     * Render the add property sub form.
     *
     * @requiresRight classUri WRITE
     * @throws common_Exception
     */
    public function removeClassProperty(RemoveClassPropertyService $removeClassPropertyService): void
    {
        if (!$this->isXmlHttpRequest()) {
            throw new common_exception_BadRequest('wrong request mode');
        }

        $success = $removeClassPropertyService->remove($this->getPsrRequest());

        if ($success) {
            $this->returnJson(['success' => true]);
        } else {
            $this->returnError(__('Unable to remove the property.'));
        }
    }

    /**
     * remove the index of the property.
     * @throws Exception
     * @throws common_exception_BadRequest
     * @return void
     */
    public function removePropertyIndex(): void
    {
        if (!$this->isXmlHttpRequest()) {
            throw new common_exception_BadRequest('wrong request mode');
        }
        if (!$this->hasRequestParameter('uri')) {
            throw new common_exception_MissingParameter("Uri parameter is missing");
        }

        if (!$this->hasRequestParameter('indexProperty')) {
            throw new common_exception_MissingParameter("indexProperty parameter is missing");
        }

        $indexPropertyUri = tao_helpers_Uri::decode($this->getRequestParameter('indexProperty'));

        //remove use of index property in property
        $property = $this->getProperty(tao_helpers_Uri::decode($this->getRequestParameter('uri')));
        $property->removePropertyValue($this->getProperty(OntologyIndex::PROPERTY_INDEX), $indexPropertyUri);

        //remove index property
        $indexProperty = new OntologyIndex($indexPropertyUri);
        $indexProperty->delete();

        $this->returnJson(['id' => $this->getRequestParameter('indexProperty')]);
    }

    /**
     * Render the add index sub form.
     * @throws Exception
     * @throws common_exception_BadRequest
     * @return void
     */
    public function addPropertyIndex(): void
    {
        if (!$this->isXmlHttpRequest()) {
            throw new common_exception_BadRequest('wrong request mode');
        }
        if (!$this->hasRequestParameter('uri')) {
            throw new Exception("wrong request Parameter");
        }
        $uri = $this->getRequestParameter('uri');

        $index = 1;
        if ($this->hasRequestParameter('index')) {
            $index = $this->getRequestParameter('index');
        }

        $propertyIndex = 1;
        if ($this->hasRequestParameter('propertyIndex')) {
            $propertyIndex = $this->getRequestParameter('propertyIndex');
        }

        //create and attach the new index property to the property
        $property = $this->getProperty(tao_helpers_Uri::decode($uri));
        $class = $this->getClass("http://www.tao.lu/Ontologies/TAO.rdf#Index");

        //get property range to select a default tokenizer
        /** @var core_kernel_classes_Class $range */
        $range = $property->getRange();
        //range is empty select item content
        $tokenizer = null;
        if (is_null($range)) {
            $tokenizer = $this->getResource('http://www.tao.lu/Ontologies/TAO.rdf#RawValueTokenizer');
        } else {
            $tokenizer = $range->getUri() === OntologyRdfs::RDFS_LITERAL
                ? $this->getResource('http://www.tao.lu/Ontologies/TAO.rdf#RawValueTokenizer')
                : $this->getResource('http://www.tao.lu/Ontologies/TAO.rdf#LabelTokenizer');
        }

        $indexClass = $this->getClass('http://www.tao.lu/Ontologies/TAO.rdf#Index');
        $i = 0;
        $indexIdentifierBackup = preg_replace('/[^a-z_0-9]/', '_', strtolower($property->getLabel()));
        $indexIdentifierBackup = ltrim(trim($indexIdentifierBackup, '_'), '0..9');
        $indexIdentifier = $indexIdentifierBackup;
        do {
            if ($i !== 0) {
                $indexIdentifier = $indexIdentifierBackup . '_' . $i;
            }
            $resources = $indexClass->searchInstances([OntologyIndex::PROPERTY_INDEX_IDENTIFIER => $indexIdentifier], ['like' => false]);
            $count = count($resources);
            $i++;
        } while ($count !== 0);

        $indexProperty = $class->createInstanceWithProperties([
                OntologyRdfs::RDFS_LABEL => preg_replace('/_/', ' ', ucfirst($indexIdentifier)),
                OntologyIndex::PROPERTY_INDEX_IDENTIFIER => $indexIdentifier,
                OntologyIndex::PROPERTY_INDEX_TOKENIZER => $tokenizer,
                OntologyIndex::PROPERTY_INDEX_FUZZY_MATCHING => GenerisRdf::GENERIS_TRUE,
                OntologyIndex::PROPERTY_DEFAULT_SEARCH  => GenerisRdf::GENERIS_FALSE,
            ]);

        $property->setPropertyValue($this->getProperty(OntologyIndex::PROPERTY_INDEX), $indexProperty);

        //generate form
        $indexFormContainer = new tao_actions_form_IndexProperty(new OntologyIndex($indexProperty), $propertyIndex . $index);
        $myForm = $indexFormContainer->getForm();
        $form = trim(preg_replace('/\s+/', ' ', $myForm->renderElements()));
        $this->returnJson(['form' => $form]);
    }

    protected function getCurrentClass(): core_kernel_classes_Class
    {
        $classUri = tao_helpers_Uri::decode($this->getRequestParameter('classUri'));
        if (is_null($classUri) || empty($classUri)) {
            $class = null;
            $resource = $this->getCurrentInstance();
            foreach ($resource->getTypes() as $type) {
                $class = $type;
                break;
            }
            if (is_null($class)) {
                throw new Exception("No valid class uri found");
            }
            $returnValue = $class;
        } else {
            $returnValue = $this->getClass($classUri);
        }

        return $returnValue;
    }

    protected function getCurrentInstance(): core_kernel_classes_Resource
    {
        $uri = tao_helpers_Uri::decode($this->getRequestParameter('uri'));
        if (is_null($uri) || empty($uri)) {
            throw new tao_models_classes_MissingRequestParameterException("uri");
        }
        return $this->getResource($uri);
    }

    /**
     * @param core_kernel_classes_Class $clazz
     * @param array $classData
     * @param array $propertyData
     * @return tao_helpers_form_Form
     */
    private function getForm(core_kernel_classes_Class $clazz, array $classData, array $propertyData)
    {
        $formContainer = new tao_actions_form_Clazz($clazz, $classData, $propertyData);
        return $formContainer->getForm();
    }

    /**
     * Create an edit form for a class and its property
     * and handle the submitted data on save
     *
     * @param core_kernel_classes_Class $class
     * @return tao_helpers_form_Form the generated form
     * @throws Exception
     */
    public function getClassForm(core_kernel_classes_Class $class): tao_helpers_form_Form
    {
        $data = $this->getRequestParameters();
        $classData = $this->extractClassData($data);
        $propertyData = $this->extractPropertyData($data);
        $formContainer = new tao_actions_form_Clazz($class, $classData, $propertyData, $this->isElasticSearchEnabled());
        $myForm = $formContainer->getForm();

        if ($myForm->isSubmited()) {
            if ($myForm->isValid()) {
                //get the data from parameters

                // get class data and save them
                if (isset($data['class'])) {
                    $classValues = [];
                    foreach ($data['class'] as $key => $value) {
                        $classKey =  tao_helpers_Uri::decode($key);
                        $classValues[$classKey] =  tao_helpers_Uri::decode($value);
                    }

                    $this->bindProperties($class, $classValues);
                }

                //save all properties values
                if (isset($data['properties'])) {
                    $this->saveProperties($data);
                    $this->populateSubmittedProperties($myForm, $data);
                }
            }
        }
        return $myForm;
    }

    private function populateSubmittedProperties($myForm, $data): void
    {
        if (empty($data['properties'])) {
            return;
        }
        $elementRangeArray = [];
        $groups = $myForm->getGroups();

        foreach ($data['properties'] as $prop) {
            if (empty($prop['range']) || empty($prop['uri']) || empty($prop['depends-on-property'])) {
                continue;
            }

            $elementUri = $groups['property_' . $prop['uri']]['elements'][0] ?? null;

            if (isset($elementUri)) {
                $index = strstr($elementUri, '_', true);
                $elementRangeArray[$index . '_range_list'] = $prop['range'];
                if ($prop['depends-on-property']) {
                    $elementRangeArray[$index . '_depends-on-property'] = $prop['depends-on-property'];
                    $elementRangeArray[$index . '_uri'] = $prop['uri'];
                }
            }
        }

        $elements = [];
        $dependsOnPropertyRepository = $this->getDependsOnPropertyRepository();

        foreach ($myForm->getElements() as $element) {
            if (
                $element instanceof tao_helpers_form_elements_xhtml_Combobox
                && array_key_exists($element->getName(), $elementRangeArray)
            ) {
                if (strpos($element->getName(), 'depends-on-property') !== false) {
                    $options = $this->getDependsOnPropertyOptions($element, $elementRangeArray, $dependsOnPropertyRepository);
                    $element->setOptions($options);
                }

                $element->setValue($elementRangeArray[$element->getName()]);
            }
            $elements[] = $element;
        }

        $myForm->setElements($elements);
    }

    private function getDependsOnPropertyOptions(
        tao_helpers_form_FormElement $element,
        array $elementRangeArray,
        DependsOnPropertyRepositoryInterface $dependsOnPropertyRepository
    ): array {
        $index = substr($element->getName(), 0, strpos($element->getName(), '_'));
        $options = $dependsOnPropertyRepository->findAll(
            [
                'property' => $this->getProperty(tao_helpers_Uri::decode($elementRangeArray[$index . '_uri'])),
                'listUri' => tao_helpers_Uri::decode($elementRangeArray[$index . '_range_list']),
            ]
        )->getOptionsList();
        return $options;
    }

    /**
     * Default property handling
     *
     * @param array $propertyValues
     * @param core_kernel_classes_Resource $property
     * @throws Exception
     */
    protected function saveSimpleProperty(array $propertyValues, core_kernel_classes_Resource $property): void
    {
        $propertyMap = tao_helpers_form_GenerisFormFactory::getPropertyMap();

        $type = $propertyValues['type'];
        $range = $this->getDecodedPropertyValue($propertyValues, 'range');
        $dependsOnPropertyUri = $this->getDecodedPropertyValue($propertyValues, 'depends-on-property');

        unset(
            $propertyValues['uri'],
            $propertyValues['type'],
            $propertyValues['range'],
            $propertyValues['depends-on-property']
        );

        $rangeNotEmpty = false;
        $values = [
            ValidationRuleRegistry::PROPERTY_VALIDATION_RULE => [],
        ];

        if (isset($propertyMap[$type])) {
            $values[WidgetRdf::PROPERTY_WIDGET] = $propertyMap[$type]['widget'];
            $rangeNotEmpty = $propertyMap[$type]['range'] === OntologyRdfs::RDFS_RESOURCE;
        }

        foreach ($propertyValues as $key => $value) {
            if (is_string($value)) {
                $values[tao_helpers_Uri::decode($key)] = tao_helpers_Uri::decode($value);
            } elseif (is_array($value)) {
                $values[tao_helpers_Uri::decode($key)] = $value;
            } else {
                $this->logWarning('Unsuported value type ' . gettype($value));
            }
        }

        $rangeValidator = new tao_helpers_form_validators_NotEmpty(['message' => __('Range field is required')]);
        if ($rangeNotEmpty && !$rangeValidator->evaluate($range)) {
            throw new Exception($rangeValidator->getMessage());
        }

        $this->bindProperties($property, $values);

        // set the range
        $property->removePropertyValues($this->getProperty(OntologyRdfs::RDFS_RANGE));
        if (!empty($range)) {
            $property->setRange($this->getClass($range));
        } elseif (isset($propertyMap[$type]) && !empty($propertyMap[$type]['range'])) {
            $property->setRange($this->getClass($propertyMap[$type]['range']));
        }

        // set cardinality
        if (isset($propertyMap[$type]['multiple'])) {
            $property->setMultiple($propertyMap[$type]['multiple'] == GenerisRdf::GENERIS_TRUE);
        }

        $this->setDependsOnProperty($property, $dependsOnPropertyUri);
    }

    protected function savePropertyIndex(array $indexValues): void
    {
        $values = [];
        foreach ($indexValues as $key => $value) {
            $values[tao_helpers_Uri::decode($key)] = tao_helpers_Uri::decode($value);
        }

        $validator = new tao_helpers_form_validators_IndexIdentifier();

        // if the identifier is valid
        $values[OntologyIndex::PROPERTY_INDEX_IDENTIFIER] = strtolower($values[OntologyIndex::PROPERTY_INDEX_IDENTIFIER]);
        if (!$validator->evaluate($values[OntologyIndex::PROPERTY_INDEX_IDENTIFIER])) {
            throw new Exception($validator->getMessage());
        }

        //if the property exists edit it, else create one
        $existingIndex = OntologyIndexService::getIndexById($values[OntologyIndex::PROPERTY_INDEX_IDENTIFIER]);
        $indexProperty = $this->getProperty($values['uri']);
        if (!is_null($existingIndex) && !$existingIndex->equals($indexProperty)) {
            throw new Exception("The index identifier should be unique");
        }
        unset($values['uri']);
        $this->bindProperties($indexProperty, $values);
    }

    /**
     * Helper to save class and properties
     *
     * @param core_kernel_classes_Resource $resource
     * @param array $values
     */
    protected function bindProperties(core_kernel_classes_Resource $resource, array $values): void
    {
        $binder = new tao_models_classes_dataBinding_GenerisInstanceDataBinder($resource);
        $binder->bind($values);
    }

    /**
     * Extracts the data assoicuated with the class from the request
     *
     * @param array $data
     * @return array
     */
    protected function extractClassData(array $data): array
    {
        $classData = [];
        if (isset($data['class'])) {
            foreach ($data['class'] as $key => $value) {
                $classData['class_' . $key] = $value;
            }
        }
        return $classData;
    }

    /**
     * Extracts the properties data from the request data, and formats
     * it as an array with the keys being the property URI and the values
     * being the associated data
     *
     * @param array $data
     * @return array
     */
    protected function extractPropertyData(array $data): array
    {
        $propertyData = [];
        if (isset($data['properties'])) {
            foreach ($data['properties'] as $key => $value) {
                $propertyData[tao_helpers_Uri::decode($value['uri'])] = $value;
            }
        }
        return $propertyData;
    }

    /**
     * @param array $properties
     *
     * @throws core_kernel_persistence_Exception
     */
    private function saveProperties(array $properties): void
    {
        $changedProperties = [];

        foreach ($properties['properties'] as $i => $propertyValues) {
            //get index values
            $indexes = null;
            if (isset($propertyValues['indexes'])) {
                $indexes = $propertyValues['indexes'];
                unset($propertyValues['indexes']);
            }

            $property = $this->getProperty(tao_helpers_Uri::decode($propertyValues['uri']));
            $oldProperty = new OldProperty(
                $property->getLabel(),
                $property->getOnePropertyValue($this->getProperty(WidgetRdf::PROPERTY_WIDGET)),
                $property->getRange() ? $property->getRange()->getUri() : null,
                $property->getPropertyValues(
                    $property->getProperty(ValidationRuleRegistry::PROPERTY_VALIDATION_RULE)
                ),
                $property->getDependsOnPropertyCollection()
            );

            $this->saveSimpleProperty($propertyValues, $property);

            $currentProperty = $this->getProperty(tao_helpers_Uri::decode($propertyValues['uri']));
            $validator = $this->getPropertyChangedValidator();

            if ($validator->isPropertyChanged($currentProperty, $oldProperty)) {
                $this->invalidatePropertyCache($validator, $currentProperty, $oldProperty);

                $changedProperties[] = [
                    'class' => $this->getCurrentClass(),
                    'property' => $currentProperty,
                    'oldProperty' => $oldProperty,
                ];
            }

            //save index
            if (!is_null($indexes)) {
                foreach ($indexes as $indexValues) {
                    $this->savePropertyIndex($indexValues);
                }
            }
        }

        if (!empty($changedProperties)) {
            $this->getEventManager()->trigger(new ClassPropertiesChangedEvent($changedProperties));

            $this->getDependsOnPropertySynchronizer()->sync(
                new DependsOnPropertySynchronizerContext([
                    DependsOnPropertySynchronizerContext::PARAM_PROPERTIES => array_column(
                        $changedProperties,
                        'property'
                    ),
                ])
            );
        }
    }

    private function getDecodedPropertyValue(array $propertyValues, string $propertyName): ?string
    {
        if (!isset($propertyValues[$propertyName])) {
            return null;
        }

        $propertyValue = trim($propertyValues[$propertyName]);

        if (empty($propertyValue)) {
            return null;
        }

        return tao_helpers_Uri::decode($propertyValue);
    }

    private function setDependsOnProperty(core_kernel_classes_Resource $property, ?string $dependsOnPropertyUri): void
    {
        $isListsDependencyEnabled = $this->getFeatureFlagChecker()->isEnabled(
            FeatureFlagChecker::FEATURE_FLAG_LISTS_DEPENDENCY_ENABLED
        );

        if (!$isListsDependencyEnabled) {
            return;
        }

        $property->removePropertyValues(
            $this->getProperty(RemoteSourcedListOntology::PROPERTY_DEPENDS_ON_PROPERTY)
        );

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

        $dependsOnPropertyCollection = new DependsOnPropertyCollection();
        $dependsOnPropertyCollection->append($this->getProperty($dependsOnPropertyUri));

        $property->setDependsOnPropertyCollection($dependsOnPropertyCollection);
    }

    private function invalidatePropertyCache(
        PropertyChangedValidator $validator,
        core_kernel_classes_Property $currentProperty,
        OldProperty $oldProperty
    ): void {
        if (
            $oldProperty->getRangeUri()
            && ($validator->isRangeChanged($currentProperty, $oldProperty)
            || $validator->isPropertyTypeChanged($currentProperty, $oldProperty))
        ) {
            $listUri = $oldProperty->getRangeUri();
        }

        if (empty($listUri) && $currentProperty->getRange() === null) {
            return;
        }

        $this->getParentPropertyListCachedRepository()->deleteCache(
            [
                'listUri' => $listUri ?? $currentProperty->getRange()->getUri()
            ]
        );
    }

    private function isElasticSearchEnabled(): bool
    {
        /** @var AdvancedSearchChecker $advancedSearchChecker */
        $advancedSearchChecker = $this->getServiceLocator()->get(AdvancedSearchChecker::class);

        return $advancedSearchChecker->isEnabled();
    }

    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
    {
        return $this->getServiceLocator()->get(FeatureFlagChecker::class);
    }

    private function getParentPropertyListCachedRepository(): ParentPropertyListCachedRepository
    {
        return $this->getServiceLocator()->get(ParentPropertyListCachedRepository::class);
    }

    private function getPropertyChangedValidator(): PropertyChangedValidator
    {
        return $this->getServiceLocator()->get(PropertyChangedValidator::class);
    }

    private function getDependsOnPropertyRepository(): DependsOnPropertyRepositoryInterface
    {
        return $this->getServiceLocator()->get(DependsOnPropertyRepository::class);
    }

    private function getDependsOnPropertySynchronizer(): DependsOnPropertySynchronizerInterface
    {
        return $this->getServiceLocator()->get(DependsOnPropertySynchronizer::class);
    }
}