<?php

/**
 * Lifestyle Webconsulting GmbH
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is private by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2017 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 */

namespace Sso\Webservices\ObjectBundle\Events\Services;

use Sso\Webservices\ObjectBundle\Entity\Object as EntityObject;
use Sso\Webservices\ObjectBundle\Database\Manager as DbManager;
use Sso\Webservices\ObjectBundle\Exception\InvalidDataModelException;
use Psr\Log\LoggerInterface;

/**
 * Class GenerateTreeIds
 *
 * @copyright  2017 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 * @package Sso\Webservices\ObjectBundle\Events\Services
 */
class GenerateTreeIds
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var DbManager
     */
    protected $objectDbM;

    /**
     * @var EntityObject[]
     */
    protected $objectModels;

    /**
     * Cache of generated tree ID numbers
     * (used during ID number generation)
     *
     * @var array
     */
    private $treeIdCache;

    /**
     * Working cache of all objects
     * (used during ID number generation)
     *
     * @var EntityObject[]
     */
    private $objectCache;

    /**
     * @var integer
     */
    private $treeRootCount;

    /**
     * @var array
     */
    private $treeRoots;

    /**
     * GenerateTreeIds constructor.
     * @param LoggerInterface $logger
     * @param DbManager $objectDbM
     */
    public function __construct(LoggerInterface $logger, DbManager $objectDbM)
    {
        $this->logger = $logger;
        $this->objectDbM = $objectDbM;
    }

    public function refreshTreeIdNumbers()
    {
        if ($this->objectDbM->data()->treeObject()->generationRequired()) {
            $this->clearCache();
            $this->objectModels = $this->objectDbM->data()->treeObject()->findAllForIdGen();
            $this->generateTreeIndexNumbers();
            $this->clearCache();
        }
    }

    /**
     * Generate tree ID numbers for all objects in objectModels
     */
    private function generateTreeIndexNumbers()
    {
        // get all objects into a structure / array from where we can directly access them by their GUID
        foreach ($this->objectModels as $object) {
            $this->objectCache[(string)$object['guid']] = $object;
        }

        // loop through all objects and get the tree id number for each
        // this is a for loop rather than a foreach as we need to access and manipulate the objects in the models store,
        // i.e. need to access the object in the array, not copies in a loop variable
        for ($i = 0; $i < count($this->objectModels); $i++) {
            try {
                $treeId = $this->getTreeNumberForObject((string)$this->objectModels[$i]['guid']);
            } catch (InvalidDataModelException $exception) {
                $this->logger->warning($exception->getMessage());
                $treeId = EntityObject::INVALID_TREE_ID;
            }
            $this->objectModels[$i]['treeId'] = $treeId;
            $this->objectDbM->data()->treeObject()->updateTreeId((string)$this->objectModels[$i]['guid'], $treeId);
        }

        $this->objectDbM->entityManager()->flush();
        $this->objectDbM->entityManager()->clear();
    }

    private function clearCache()
    {
        $this->objectModels = null;
        $this->treeIdCache = [];
        $this->objectCache = [];
        $this->treeRoots = [];
        $this->treeRootCount = 0;
    }

    /**
     * Get the tree ID number for the object with the given GUID
     *
     * @param string $guid
     * @param array $processedGuids
     * @return string
     * @throws InvalidDataModelException
     */
    private function getTreeNumberForObject($guid, array $processedGuids = [])
    {
        // Detect infinite loops
        if (in_array($guid, $processedGuids)) {
            $processedGuids[] = $guid;
            throw new InvalidDataModelException(
                'Detected infinite loop while generating tree structure. Object-GUIDs: ' .
                implode(' -> ', $processedGuids)
            );
        }
        $processedGuids[] = $guid;

        if ((null === $this->objectCache[$guid]['parentGuid']) || (strlen($this->objectCache[$guid]['parentGuid']) == 0)) {
            // this object has no parent => it's the root node
            if (!isset($this->treeRoots[$guid])) {
                $this->treeRootCount++;
                $this->treeRoots[$guid] = (string)$this->treeRootCount;
            }
            return $this->treeRoots[$guid];
        }

        if (isset($this->treeIdCache[$guid])) {
            return $this->treeIdCache[$guid]['treeId'];
        }

        $parentTreeIdNumber = $this->getParentTreeNumber($guid, $processedGuids);
        $parentGuid = (string)$this->objectCache[$guid]['parentGuid'];

        // the parent object of this one is about to get another child, increase the counter
        $this->treeIdCache[$parentGuid]['childs']++;

        // the number of the parent appended with the how n'th child this one is, is the tree ID number for this object
        $childs = (string)$this->treeIdCache[$parentGuid]['childs'];

        // make sure it's two digits (to aid sorting later)
        $thisTreeId = $parentTreeIdNumber . '.' . sprintf('%02s', $childs);

        // remember the tree ID number of this object so we don't have to generate it again when we need it
        $this->treeIdCache[$guid] = ['treeId' => $thisTreeId, 'childs' => 0];

        return $thisTreeId;
    }

    /**
     * Fetches the tree ID number of the parent object of the object with the given GUID
     *
     * @param string $guid
     * @param array $processedGuids
     * @return mixed
     */
    private function getParentTreeNumber($guid, array $processedGuids)
    {
        $parentGuid = (string)$this->objectCache[$guid]['parentGuid'];
        if (!isset($this->treeIdCache[$parentGuid])) {
            // we haven't gotten a tree ID number for this object yet, generate one
            $this->treeIdCache[$parentGuid] = [
                'treeId' => $this->getTreeNumberForObject($parentGuid, $processedGuids),
                'childs' => 0
            ];
            // and yes, there's a neat recursion between this and getTreeNumberForObject() ;-)
        }

        return $this->treeIdCache[$parentGuid]['treeId'];
    }
}
