<?php

namespace Webapp\FileManagement\Service;

use Doctrine\ORM\EntityManagerInterface;
use Mobile\Device\Entity\Block;
use Mobile\Device\Entity\Device;
use Mobile\Measure\Entity\Variable\StateCode;
use Mobile\Project\Entity\DataEntryProject;
use Mobile\Project\Entity\DesktopUser;
use Mobile\Project\Entity\Platform;
use Mobile\Shared\Util\PlatformCrawler;
use ReflectionException;
use Shared\Authentication\Entity\User;
use Shared\FileManagement\Entity\UserLinkedJob;
use Shared\TransferSync\Entity\StatusDataEntry;
use SimpleXMLElement;
use Vich\UploaderBundle\Storage\StorageInterface;
use Webapp\FileManagement\Entity\ParsingJob;
use Webapp\FileManagement\Entity\RequestFile;
use Webapp\FileManagement\Exception\ParsingException;

/**
 * Class FileReaderWorker.
 */
class FileReaderService extends AbstractReaderService
{
    private MobileReaderHelper $parsingHelper;

    /**
     * FileReaderWorker constructor.
     *
     * @param EntityManagerInterface $entityManager
     * @param DirectoryNamer $directoryNameManager
     * @param StorageInterface $storage
     * @param string $tmpDir
     */
    public function __construct(EntityManagerInterface $entityManager,
                                DirectoryNamer         $directoryNameManager,
                                StorageInterface       $storage,
                                string                 $tmpDir)
    {
        parent::__construct($entityManager, $directoryNameManager, $storage, $tmpDir);
    }

    /**
     * @param int $projectFileId
     * @throws ParsingException
     * @throws ReflectionException
     */
    public function parseProjectFile(int $projectFileId): void
    {
        /** @var $requestFile RequestFile */
        $requestFile = $this->entityManager->getRepository(RequestFile::class)->find($projectFileId);
        $this->parsingHelper = new MobileReaderHelper($this->entityManager->getRepository(User::class), $this->getTmpDir($requestFile));
        $this->currentParsingStateError = -1;
        $requestFile->setStatus(RequestFile::STATUS_RUNNING);
        $this->entityManager->flush();

        $this->copyFileInTmpDir($requestFile);

        $xml = $this->getSimpleXMLElement($requestFile);
        $xmlBaseUriMap = [];
        foreach ($xml->xpath('//xmi:XMI/*') as $item) {
            $xmlBaseUriMap[] = $item;
        }
        list($dataEntryProjectNS, $variableNS, $workpathNS, $platformNS, $desktopUserNS, $graphicNS, $stateCodeNS, $dataEntryNS) = $this->handleNamespaces($xml);

        $hashMap = [];
        $stateCodes = $this->readStateCodeCollection($dataEntryProjectNS, $stateCodeNS, $xmlBaseUriMap, $hashMap); // Sets parsing helper stateCode array too.
        $creatorLogin = $this->readProjectCreatorLogin($dataEntryProjectNS, $xml);
        $desktopUsers = $this->readDesktopUserCollection($desktopUserNS, $xmlBaseUriMap, $hashMap);
        $naturesZHE = $this->readNatureZheCollection($platformNS, $xmlBaseUriMap, $hashMap);
        list($platformXML, $platform) = $this->readPlatform($platformNS, $xmlBaseUriMap, $hashMap);
        $connectedVariables = $this->readConnectedVariableCollection($variableNS, $xmlBaseUriMap, $hashMap);
        foreach ($dataEntryProjectNS->ProjetDeSaisie as $dataEntryProjectXml) {
            $dataEntryProject = $this->readProject($dataEntryProjectXml, $xmlBaseUriMap, $hashMap);
        }
        // Proceed to data association.
        $dataEntryProject->setOriginFile($requestFile);
        $this->projectStateCodeCollection($stateCodes, $dataEntryProject);
        $this->projectDesktopUserCollection($desktopUsers, $dataEntryProject, $creatorLogin);
        $this->projectNatureZheCollection($naturesZHE, $dataEntryProject);
        $dataEntryProject->setPlatform($platform);
        $this->projectConnectedVariableCollection($dataEntryProject, $connectedVariables);
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_GRAPHICAL_STRUCTURE;
        $dataEntryProject->setGraphicalStructure($this->parsingHelper->parseGraphicalStructure($graphicNS, $platformXML));
        // Create status project to allow synchronization.
        $this->projectWorkpathCollection($workpathNS, $dataEntryProjectNS, $hashMap, $dataEntryProject);
        $project = new StatusDataEntry($requestFile->getUser(), StatusDataEntry::STATUS_NEW);
        $project->setSyncable(false);
        $project->setRequest($dataEntryProject);


        $this->verifyProjectIntegrity($project);

        $this->cleanTmpDir($requestFile);

        $this->currentParsingStateError = RequestFile::PARSING_STATE_DATABASE_SAVE;
        $this->entityManager->persist($project);
        $requestFile->setStatus(UserLinkedJob::STATUS_SUCCESS);
        $this->entityManager->flush();
    }

    /**
     * @param int $id
     * @throws ParsingException
     */
    public function parseReturnFile(int $id): void
    {
        $parsingJob = $this->entityManager->getRepository(ParsingJob::class)->find($id);
        $this->parsingHelper = new MobileReaderHelper($this->entityManager->getRepository(User::class), $this->getTmpDir($parsingJob));

        $parsingJob->setStatus(UserLinkedJob::STATUS_RUNNING);
        $this->entityManager->flush();

        $this->copyFileInTmpDir($parsingJob);
        $xml = $this->getSimpleXMLElement($parsingJob);
        $xmlBaseUriMap = [];
        foreach ($xml->xpath('//xmi:XMI/*') as $item) {
            $xmlBaseUriMap[] = $item;
        }
        list($dataEntryProjectNS, $variableNS, $workpathNS, $platformNS, $desktopUserNS, $graphicNS, $stateCodeNS, $dataEntryNS) = $this->handleNamespaces($xml);
        $hashMap = [];
        $stateCodes = $this->readStateCodeCollection($dataEntryProjectNS, $stateCodeNS, $xmlBaseUriMap, $hashMap); // Sets parsing helper stateCode array too.
        $creatorLogin = $this->readProjectCreatorLogin($dataEntryProjectNS, $xml);
        $desktopUsers = $this->readDesktopUserCollection($desktopUserNS, $xmlBaseUriMap, $hashMap);
        $naturesZHE = $this->readNatureZheCollection($platformNS, $xmlBaseUriMap, $hashMap);
        /** @var Platform $platform */
        list($platformXML, $platform) = $this->readPlatform($platformNS, $xmlBaseUriMap, $hashMap);
        if ($platform->getName() !== $parsingJob->getStatusDataEntry()->getRequest()->getPlatform()->getName()) {
            throw new ParsingException("", RequestFile::ERROR_WRONG_PLATFORM_NAME);
        }

        $connectedVariables = $this->readConnectedVariableCollection($variableNS, $xmlBaseUriMap, $hashMap, true);

        foreach ($dataEntryProjectNS->ProjetDeSaisie as $item) {
            $dataEntryProject = (new DataEntryProject())
                ->setImprovised(false)
                ->setCreationDate(new \DateTime())
                ->setName($parsingJob->getStatusDataEntry()->getRequest()->getName());
            $hashMap["/" . array_search($item, $xmlBaseUriMap)] = $dataEntryProject;
        }

        $this->projectStateCodeCollection($stateCodes, $dataEntryProject);
        $this->projectDesktopUserCollection($desktopUsers, $dataEntryProject, $creatorLogin);
        $this->projectNatureZheCollection($naturesZHE, $dataEntryProject);
        $dataEntryProject->setPlatform($platform);
        $this->projectConnectedVariableCollection($dataEntryProject, $connectedVariables);
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_GRAPHICAL_STRUCTURE;

        $this->projectWorkpathCollection($workpathNS, $dataEntryProjectNS, $hashMap, $dataEntryProject);

        foreach ($dataEntryNS->Saisie as $item) {
            $dataEntryUri = "/" . array_search($item, $xmlBaseUriMap);
            $dataEntry = $this->parsingHelper->parseDataEntry($item, $hashMap, $dataEntryUri, $dataEntryProject);
            $hashMap[$dataEntryUri] = $dataEntry;
            $dataEntryProject->setDataEntry($dataEntry);
            $this->entityManager->persist($dataEntry);
            $dataEntry->setProject($hashMap[$item->attributes()['projetDeSaisie']->__toString()]);
        }

        $parsingJob->getStatusDataEntry()
            ->setResponse($dataEntryProject)
            ->setStatus(StatusDataEntry::STATUS_WRITE_PENDING);
        //Needed to delete the status
        $parsingJob->setStatusDataEntry(null);

        $this->cleanTmpDir($parsingJob);

        $this->currentParsingStateError = RequestFile::PARSING_STATE_DATABASE_SAVE;
        $parsingJob->setStatus(UserLinkedJob::STATUS_SUCCESS);
        $this->entityManager->flush();

    }

    /**
     * @param SimpleXMLElement $xml
     * @return array
     * @throws ParsingException
     */
    private function handleNamespaces(SimpleXMLElement $xml): array
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_FIND_NAMESPACES;
        $namespaces = $xml->getNamespaces(true);
        if (!isset($namespaces['adonis.modeleMetier.projetDeSaisie'])) {
            throw new ParsingException("", RequestFile::ERROR_INCORRECT_FILE_TYPE);
        }
        $dataEntryProjectNS = $xml->children($namespaces['adonis.modeleMetier.projetDeSaisie']);
        $variableNS = null;
        if (isset($namespaces['adonis.modeleMetier.projetDeSaisie.variables'])) {
            $variableNS = $xml->children($namespaces['adonis.modeleMetier.projetDeSaisie.variables']);
        }
        $workpathNS = null;
        if (isset($namespaces['adonis.modeleMetier.projetDeSaisie.cheminement'])) {
            $workpathNS = $xml->children($namespaces['adonis.modeleMetier.projetDeSaisie.cheminement']);
        }
        $platformNS = $xml->children($namespaces['adonis.modeleMetier.plateforme']);
        $desktopUserNS = $xml->children($namespaces['adonis.modeleMetier.utilisateur']);
        $graphicNS = array_key_exists('adonis.modeleMetier.graphique', $namespaces)
            ? $xml->children($namespaces['adonis.modeleMetier.graphique'])
            : null;
        $stateCodeNS = array_key_exists('adonis.modeleMetier.conceptsDeBase', $namespaces)
            ? $xml->children($namespaces['adonis.modeleMetier.conceptsDeBase'])
            : null;
        $dataEntryNS = array_key_exists('adonis.modeleMetier.saisieTerrain', $namespaces)
            ? $xml->children($namespaces['adonis.modeleMetier.saisieTerrain'])
            : null;
        return [$dataEntryProjectNS, $variableNS, $workpathNS, $platformNS, $desktopUserNS, $graphicNS, $stateCodeNS, $dataEntryNS];
    }

    /**
     * @param $dataEntryProjectNS
     * @param SimpleXMLElement $xml
     * @return string
     */
    private function readProjectCreatorLogin($dataEntryProjectNS, SimpleXMLElement $xml): string
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_PROJECT_CREATOR;
        $creatorXmlLocation = intval(str_replace('/', '', $dataEntryProjectNS->attributes()['createur']));
        return (string)$xml->xpath('//xmi:XMI/*')[$creatorXmlLocation]->attributes()['login'];
    }

    /**
     * @param $dataEntryProjectNS
     * @param $stateCodeNS
     * @param array $XMLBaseURIMap
     * @param array $hashMap
     * @return StateCode[]
     * @throws ParsingException
     */
    private function readStateCodeCollection($dataEntryProjectNS, $stateCodeNS, array $XMLBaseURIMap, array &$hashMap): array
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_STATE_CODE;
        $stateCodes = [];
        foreach ($dataEntryProjectNS->ProjetDeSaisie as $item) {
            $stateCodeCount = 0;
            foreach ($item->children()->codesEtats as $stateCodeXml) {
                $stateCode = $this->parsingHelper->parseStateCode($stateCodeXml);
                $stateCodes[$stateCode->getCode()] = $stateCode;
                $hashMap["/" . array_search($item, $XMLBaseURIMap) . "/@codesEtats." . $stateCodeCount++] = $stateCode;
            }
        }
        // Need to loop over codeEtat namespace to get all previous values state code.
        if (isset($stateCodeNS)) {
            foreach ($stateCodeNS as $stateCodeXml) {
                $stateCode = $this->parsingHelper->parseStateCode($stateCodeXml);
                $hashMap["/" . array_search($stateCodeXml, $XMLBaseURIMap)] = $stateCode;
                if (!array_key_exists($stateCode->getCode(), $stateCodes)) {
                    $stateCodes[$stateCode->getCode()] = $stateCode;
                }
            }
        }
        $this->parsingHelper->setStateCodes($stateCodes);
        return $stateCodes;
    }

    /**
     * @param $desktopUserNS
     * @param array $XMLBaseURIMap
     * @param array $hashMap
     * @return DesktopUser[]
     */
    private function readDesktopUserCollection($desktopUserNS, array $XMLBaseURIMap, array &$hashMap): array
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_DESKTOP_USER_COLLECTION;
        $desktopUsers = [];
        foreach ($desktopUserNS as $userXml) {
            $user = new DesktopUser();
            $user->setName($userXml->attributes()['nom']);
            $user->setFirstname($userXml->attributes()['prenom']);
            $user->setLogin($userXml->attributes()['login']);
            $user->setEmail($userXml->attributes()['email']);
            $user->setPassword($userXml->attributes()['motDePasse']);
            $desktopUsers[] = $user;
            $hashMap["/" . array_search($userXml, $XMLBaseURIMap)] = $user;
        }
        return $desktopUsers;
    }

    /**
     * @param $platformNS
     * @param array $XMLBaseURIMap
     * @param array $hashMap
     * @return array
     * @throws ParsingException
     */
    private function readNatureZheCollection($platformNS, array $XMLBaseURIMap, array &$hashMap): array
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_NATURE_ZHE;
        $naturesZHE = [];
        foreach ($platformNS as $key => $item) {
            if ($key === "NatureZhe") {
                if (array_search($item, $XMLBaseURIMap) === false) {
                    throw new ParsingException("", RequestFile::ERROR_URI_NOT_FOUND);
                }
                $natureZHEUri = "/" . array_search($item, $XMLBaseURIMap);
                $natureZHE = $this->parsingHelper->readNatureZheItem($item);
                if (isset($hashMap[$natureZHEUri])) {
                    throw new ParsingException("", RequestFile::ERROR_DUPLICATED_URI);
                }
                $hashMap[$natureZHEUri] = $natureZHE;
                $naturesZHE[] = $natureZHE;
            }
        }
        return $naturesZHE;
    }

    /**
     * @param $variableNS
     * @param array $xmlBaseUriMap
     * @param array $hashMap
     * @return array
     * @throws ParsingException
     */
    private function readConnectedVariableCollection($variableNS, array $xmlBaseUriMap, array &$hashMap): array
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_CONNECTED_VARIABLE_COLLECTION;
        $connectedVariables = [];
        if (!is_null($variableNS)) {
            foreach ($variableNS as $variable) {
                if (array_search($variable, $xmlBaseUriMap) === false) {
                    throw new ParsingException("", RequestFile::ERROR_URI_NOT_FOUND);
                }
                $variableUri = "/" . array_search($variable, $xmlBaseUriMap);
                $variable = $this->parsingHelper->parseVariable($variable, $hashMap, $variableUri);
                if (isset($hashMap[$variableUri])) {
                    throw new ParsingException("", RequestFile::ERROR_DUPLICATED_URI);
                }
                $hashMap[$variableUri] = $variable;
                $connectedVariables[] = $variable;
            }

        }
        return $connectedVariables;
    }

    /**
     * @param $platformNS
     * @param array $hashMap
     * @return array
     * @throws ParsingException
     */
    private function readPlatform($platformNS, array $XMLBaseURIMap, array &$hashMap): array
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_PLATFORM;
        $platformXML = null;
        $platform = null;
        foreach ($platformNS as $key => $item) {
            if ($key === "Plateforme") {
                $platformUri = "/" . array_search($item, $XMLBaseURIMap);
                $platformXML = $item;
                $platform = $this->parsingHelper->parsePlatform($item, $hashMap, $platformUri);
                $hashMap[$platformUri] = $platform;
                $this->entityManager->persist($platform); // To persist all elements here and give them an id.
            }
        }
        if (is_null($platformXML) || is_null($platform)) {
            throw new ParsingException("", RequestFile::ERROR_NO_PLATFORM);
        }
        return array($platformXML, $platform);
    }

    /**
     * @param $dataEntryProjectXml
     * @param array $hashMap
     * @return DataEntryProject
     * @throws ParsingException
     */
    private function readProject($dataEntryProjectXml, array $XMLBaseURIMap, array &$hashMap): DataEntryProject
    {
        $this->currentParsingStateError = RequestFile::PARSING_STATE_READ_PROJECT;
        $projectUri = "/" . array_search($dataEntryProjectXml, $XMLBaseURIMap);

        $dataEntryProject = $this->parsingHelper->parseDataEntryProject($dataEntryProjectXml, $hashMap, $projectUri);

        $hashMap[$projectUri] = $dataEntryProject;
        return $dataEntryProject;
    }

    /**
     * @param array $stateCodes
     * @param DataEntryProject $dataEntryProject
     */
    private function projectStateCodeCollection(array $stateCodes, DataEntryProject $dataEntryProject): void
    {
        foreach ($stateCodes as $stateCode) {
            $dataEntryProject->addStateCode($stateCode);
        }
    }

    /**
     * @param array $desktopUsers
     * @param DataEntryProject $dataEntryProject
     * @param string $creatorLogin
     */
    private function projectDesktopUserCollection(array $desktopUsers, DataEntryProject $dataEntryProject, string $creatorLogin): void
    {
        foreach ($desktopUsers as $user) {
            $dataEntryProject->addDesktopUser($user);
            if ($creatorLogin === $user->getLogin() && !$dataEntryProject->getCreator()) {
                $dataEntryProject->setCreator($user);
            }
        }
    }

    /**
     * @param array $naturesZHE
     * @param DataEntryProject $dataEntryProject
     */
    private function projectNatureZheCollection(array $naturesZHE, DataEntryProject $dataEntryProject): void
    {
        foreach ($naturesZHE as $natureZHE) {
            $dataEntryProject->addNatureZHE($natureZHE);
        }
    }

    /**
     * @param DataEntryProject $dataEntryProject
     * @param array $connectedVariables
     */
    private function projectConnectedVariableCollection(DataEntryProject $dataEntryProject, array $connectedVariables): void
    {
        foreach ($connectedVariables as $connectedVariable) {
            $dataEntryProject->addConnectedVariable($connectedVariable);
        }
    }

    /**
     * @param $workpathNS
     * @param $dataEntryProjectNS
     * @param array $hashMap
     * @param DataEntryProject $dataEntryProject
     * @throws ParsingException
     * @throws ReflectionException
     */
    private function projectWorkpathCollection($workpathNS, $dataEntryProjectNS, array $hashMap, DataEntryProject $dataEntryProject): void
    {
        if (!is_null($workpathNS)) {
            $paths = $this->parsingHelper->parsePaths(
                $workpathNS,
                isset($dataEntryProjectNS->children()->sectionCheminements) && isset($dataEntryProjectNS->children()->sectionCheminements->children()->cheminementCalcules) ? $dataEntryProjectNS->children()->sectionCheminements->children()->cheminementCalcules : null,
                $hashMap);
            foreach ($paths as $path) {
                $dataEntryProject->addWorkpath($path);
            }
        }
    }

    /**
     * @param StatusDataEntry $project
     */
    private function verifyProjectIntegrity(StatusDataEntry $project)
    {
        $crawler = new PlatformCrawler();
        $blockNumbers = [];
        //The crawler use a depth-first algorithm by calling the function when entering a node.
        //So we assume that the block callback is called in his parent device context setup (here initialised bu an empty array)
        $crawler->setDeviceCallable(function (Device $d) use (&$blockNumbers) {
            $blockNumbers = [];
        });
        $crawler->setBlockCallable(function (Block $b) use (&$blockNumbers) {
            //If the number is already used in the device, we stop the crawler and throw an error
            if (isset($blockNumbers[$b->getName()])) {
                throw new ParsingException("", RequestFile::SEMANTIC_DUPLICATE_BLOCK_NUMBER);
            }
            $blockNumbers[$b->getName()] = true;
        });
        $crawler->crawl($project->getRequest()->getPlatform());
    }
}
