<?php declare(strict_types=1);
/*
 * This file is part of "irstea/ng-model-generator-bundle".
 *
 * "irstea/ng-model-generator-bundle" generates Typescript interfaces for Angular using api-platform metadata.
 * Copyright (C) 2018-2019 IRSTEA
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 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 and the GNU
 * Lesser General Public License along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

namespace Irstea\NgModelGeneratorBundle\Models;

use Irstea\NgModelGeneratorBundle\Exceptions\DomainException;
use Irstea\NgModelGeneratorBundle\Metadata\PropertyMetadata;

/**
 * Class ClassInfo.
 */
final class ClassInfo implements ClassName
{
    public const UNDEFINED = 'UNDEFINED';
    public const IRI = 'IRI';
    public const UNION = 'UNION';
    public const INTERFACE = 'INTERFACE';

    /** @var ClassName */
    private $class;

    /** @var self|false|null */
    private $parent = false;

    /** @var PropertyMetadata[] */
    private $virtualProperties = [];

    /** @var PropertyMetadata[] */
    private $concreteProperties = [];

    /** @var self[] */
    private $children = [];

    /** @var string */
    private $type = self::UNDEFINED;

    /** @var bool */
    private $abstract;

    /** @var bool */
    private $resource;

    /**
     * ClassInfo constructor.
     *
     * @param ClassName          $class
     * @param PropertyMetadata[] $properties
     * @param bool               $abstract
     * @param bool               $resource
     */
    public function __construct(ClassName $class, array $properties = [], bool $abstract = false, bool $resource = false)
    {
        $this->class = $class;
        $this->abstract = $abstract;

        foreach ($properties as $property) {
            $this->virtualProperties[$property->getName()] = $property;
        }
        $this->concreteProperties = $abstract ? [] : $this->virtualProperties;
        $this->resource = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function getNamespace(): string
    {
        return $this->class->getNamespace();
    }

    /**
     * {@inheritdoc}
     */
    public function getBaseName(): string
    {
        return $this->class->getBaseName();
    }

    /**
     * {@inheritdoc}
     */
    public function getFullName(): string
    {
        return $this->class->getFullName();
    }

    /**
     * Get properties.
     *
     * @return PropertyMetadata[]
     */
    public function getVirtualProperties(): array
    {
        return $this->virtualProperties;
    }

    /**
     * Get properties.
     *
     * @return PropertyMetadata[]
     */
    public function getConcreteProperties(): array
    {
        return $this->concreteProperties;
    }

    /**
     * Get abstract.
     *
     * @return bool
     */
    public function isAbstract(): bool
    {
        return $this->abstract;
    }

    /**
     * Get resource.
     *
     * @return bool
     */
    public function isResource(): bool
    {
        return $this->resource;
    }

    /**
     * Get parent.
     *
     * @return ClassInfo|null
     */
    public function getParent(): ?ClassInfo
    {
        return $this->parent ?: null;
    }

    /**
     * Set parent.
     *
     * @param ClassInfo|null $parent
     */
    public function setParent(?ClassInfo $parent): void
    {
        if ($parent === $this->parent) {
            return;
        }
        if ($parent === $this) {
            throw new DomainException('A class cannot be its own parent');
        }
        if ($this->parent !== false) {
            throw new DomainException('Can only set parent once');
        }
        $this->parent = $parent;
        if ($parent) {
            $parent->addChild($this);
        }
    }

    /**
     * Get children.
     *
     * @return ClassInfo[]
     */
    public function getChildren(): array
    {
        return $this->children;
    }

    /**
     * @param ClassInfo $child
     *
     * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
     */
    private function addChild(ClassInfo $child): void
    {
        if (\in_array($child, $this->children, true)) {
            return;
        }
        $this->children[] = $child;
        $child->setParent($this);
    }

    /**
     * Test type.
     *
     * @param string $type
     *
     * @return bool
     */
    public function isType(string $type): bool
    {
        return $this->type === $type;
    }

    /**
     * @return bool
     */
    public function isInterface(): bool
    {
        return $this->isType(self::INTERFACE);
    }

    /**
     * @return bool
     */
    public function isUnion(): bool
    {
        return $this->isType(self::UNION);
    }

    /**
     * @return bool
     */
    public function isUndefined(): bool
    {
        return $this->isType(self::UNDEFINED);
    }

    /**
     * @return bool
     */
    public function isIRI(): bool
    {
        return $this->isType(self::IRI);
    }

    /**
     * {@inheritdoc}
     */
    public function jsonSerialize()
    {
        return [
            'class'      => $this->class->getFullName(),
            'type'       => $this->type,
            'abstract'   => $this->abstract,
            'parent'     => $this->parent ? $this->parent->getFullName() : null,
            'children'   => array_map(
                function (ClassInfo $ci) {
                    return $ci->getFullName();
                },
                $this->children
            ),
            'virtualProperties'  => $this->virtualProperties,
            'concreteProperties' => $this->concreteProperties,
        ];
    }

    /**
     * @return self[]|\Generator
     */
    public function iterateConcreteDescendants(): \Generator
    {
        if (!$this->abstract) {
            yield $this;
        }
        foreach ($this->children as $child) {
            yield from $child->iterateConcreteDescendants();
        }
    }

    /**
     * @return self[]|\Generator
     */
    public function iterateInterfaceDescendants(): \Generator
    {
        if ($this->isInterface()) {
            yield $this;
        }
        foreach ($this->children as $child) {
            yield from $child->iterateInterfaceDescendants();
        }
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->getFullName();
    }

    /**
     * Fait remonter les propriétés communes des sous-classes.
     */
    public function rearrangeHiearchy(): void
    {
        if ($this->parent) {
            return;
        }
        $this->bubbleUpProperties();
        $this->sinkDownProperties();
    }

    private function bubbleUpProperties(): void
    {
        if (!$this->children) {
            return;
        }

        $classProperties = [];
        if (!$this->abstract) {
            $classProperties[] = $this->virtualProperties;
        }
        foreach ($this->children as $child) {
            $child->bubbleUpProperties();
            $classProperties[] = $child->virtualProperties;
        }

        switch (\count($classProperties)) {
            case 0:
                return;
            case 1:
                $commonProperties = array_shift($classProperties);
                break;
            default:
                $commonProperties = array_intersect_key(...$classProperties);
        }

        if (!$commonProperties) {
            return;
        }

        $this->virtualProperties = array_replace($this->virtualProperties, $commonProperties);
    }

    /**
     * @return bool
     */
    private function sinkDownProperties(): bool
    {
        if ($this->parent) {
            $this->virtualProperties = array_replace($this->parent->virtualProperties, $this->virtualProperties);

            if (!$this->abstract) {
                $this->concreteProperties = array_diff_key($this->virtualProperties, $this->parent->getAllConcreteProperties());
            }
        } elseif (!$this->abstract) {
            $this->concreteProperties = $this->virtualProperties;
        }

        $hasInterfaceChildren = false;
        foreach ($this->children as $child) {
            if ($child->sinkDownProperties()) {
                $hasInterfaceChildren = true;
            }
        }

        if ($this->concreteProperties) {
            $this->type = self::INTERFACE;
            $hasInterfaceChildren = true;
        } elseif ($hasInterfaceChildren) {
            $this->type = self::UNION;
        } else {
            $this->type = self::IRI;
        }

        return $hasInterfaceChildren;
    }

    /**
     * Get properties.
     *
     * @return HasName[]
     * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
     */
    private function getAllConcreteProperties(): array
    {
        if ($this->parent) {
            return array_replace($this->parent->getAllConcreteProperties(), $this->concreteProperties);
        }

        return $this->concreteProperties;
    }
}