<?php

namespace Jackalope\NodeType;

use DOMElement;
use ArrayObject;
use InvalidArgumentException;
use PHPCR\NodeType\NodeTypeDefinitionInterface;
use Jackalope\FactoryInterface;
use PHPCR\Util\ValueConverter;

/**
 * {@inheritDoc}
 *
 * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
 * @license http://opensource.org/licenses/MIT MIT License
 *
 * @api
 */
class NodeTypeDefinition implements NodeTypeDefinitionInterface
{
    const NAME_NT_BASE = 'nt:base';

    /**
     * The factory to instantiate objects
     * @var FactoryInterface
     */
    protected $factory;

    /**
     * @var NodeTypeManager
     */
    protected $nodeTypeManager;

    /**
     * @var ValueConverter
     */
    protected $valueConverter;

    /**
     * The name of this node type definition.
     * @var string
     */
    protected $name = null;

    /**
     * @var boolean
     */
    protected $isAbstract = false;

    /**
     * Whether this is a mixin node type (otherwise it's a primary node type).
     * @var boolean
     */
    protected $isMixin = false;

    /**
     * @var boolean
     */
    protected $isQueryable = true;

    /**
     * @var boolean
     */
    protected $hasOrderableChildNodes = false;

    /**
     * Name of the primary item of this node type.
     * @var string
     */
    protected $primaryItemName = null;

    /**
     * @var array
     */
    protected $declaredSuperTypeNames = null;

    /**
     * @var ArrayObject
     */
    protected $declaredPropertyDefinitions = null;

    /**
     * @var ArrayObject
     */
    protected $declaredNodeDefinitions = null;

    /**
     * Create a new node type definition.
     *
     * Optionally initializes the data from XML, an array or another
     * NodeTypeDefinition.
     *
     * @param FactoryInterface $factory         the object factory
     * @param NodeTypeManager  $nodeTypeManager
     * @param DOMElement|NodeTypeDefinitionInterface|null
     *      $nodetype Either by XML or by NodeTypeDefinition or null for an
     *      empty definition
     *
     * @throws InvalidArgumentException If it is not possible to read data
     *      from $nodetype
     */
    public function __construct(FactoryInterface $factory, NodeTypeManager $nodeTypeManager, $nodetype = null)
    {
        $this->factory = $factory;
        $this->valueConverter = $this->factory->get(ValueConverter::class);
        $this->nodeTypeManager = $nodeTypeManager;

        if ($nodetype instanceof DOMElement) {
            $this->fromXml($nodetype);
        } elseif (is_array($nodetype)) {
            $this->fromArray($nodetype);
        } elseif ($nodetype instanceof NodeTypeDefinitionInterface) {
            $this->fromNodeTypeDefinition($nodetype); // copy constructor
        } elseif (!is_null($nodetype)) {
            throw new InvalidArgumentException('Implementation Error -- unknown nodetype class: '.get_class($nodetype));
        }
    }

    /**
     * Read the node type definition from another NodeTypeDefinition
     *
     * @param NodeTypeDefinitionInterface $ntd The node type
     *      definition to copy information from
     */
    protected function fromNodeTypeDefinition(NodeTypeDefinitionInterface $ntd)
    {
        $this->name = $ntd->getName();
        $this->isAbstract = $ntd->isAbstract();
        $this->isMixin = $ntd->isMixin();
        $this->isQueryable = $ntd->isQueryable();
        $this->hasOrderableChildNodes = $ntd->hasOrderableChildNodes();
        $this->primaryItemName = $ntd->getPrimaryItemName();
        $this->declaredSuperTypeNames = $ntd->getDeclaredSupertypeNames();
        $this->declaredPropertyDefinitions = new ArrayObject($ntd->getDeclaredPropertyDefinitions() ?: []);
        $this->declaredNodeDefinitions = new ArrayObject($ntd->getDeclaredChildNodeDefinitions() ?: []);
    }

    /**
     * Reads the node type definition from an array
     *
     * @param array $data an array with key-value information
     */
    protected function fromArray(array $data)
    {
        $this->name = $data['name'];
        $this->isAbstract = $data['isAbstract'];
        $this->isMixin = $data['isMixin'];
        $this->isQueryable = $data['isQueryable'];
        $this->hasOrderableChildNodes = $data['hasOrderableChildNodes'];
        $this->primaryItemName = $data['primaryItemName'] ?: null;
        $this->declaredSuperTypeNames = (isset($data['declaredSuperTypeNames']) && count($data['declaredSuperTypeNames'])) ? $data['declaredSuperTypeNames'] : [];
        $this->declaredPropertyDefinitions = new ArrayObject();

        foreach ($data['declaredPropertyDefinitions'] as $propertyDef) {
            $this->declaredPropertyDefinitions[] = $this->factory->get(
                PropertyDefinition::class,
                [$propertyDef, $this->nodeTypeManager]
            );
        }
        $this->declaredNodeDefinitions = new ArrayObject();
        foreach ($data['declaredNodeDefinitions'] as $nodeDef) {
            $this->declaredNodeDefinitions[] = $this->factory->get(
                NodeDefinition::class,
                [$nodeDef, $this->nodeTypeManager]
            );
        }
    }

    /**
     * Reads the node type definition from an xml element
     *
     * @param DOMElement $node The dom element to read information from
     */
    protected function fromXml(DOMElement $node)
    {
        $nodeTypeXmlConverter = new NodeTypeXmlConverter($this->factory);
        $this->fromArray($nodeTypeXmlConverter->getNodeTypeDefinitionFromXml($node));
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function getDeclaredSupertypeNames()
    {
        if (null === $this->declaredSuperTypeNames) {
            return [self::NAME_NT_BASE];
        }

        return $this->declaredSuperTypeNames;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function isAbstract()
    {
        return $this->isAbstract;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function isMixin()
    {
        return $this->isMixin;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function hasOrderableChildNodes()
    {
        return $this->hasOrderableChildNodes;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function isQueryable()
    {
        return $this->isQueryable;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function getPrimaryItemName()
    {
        return $this->primaryItemName;
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function getDeclaredPropertyDefinitions()
    {
        return null === $this->declaredPropertyDefinitions
            ? null : $this->declaredPropertyDefinitions->getArrayCopy();
    }

    /**
     * {@inheritDoc}
     *
     * @api
     */
    public function getDeclaredChildNodeDefinitions()
    {
        return null === $this->declaredNodeDefinitions
            ? null : $this->declaredNodeDefinitions->getArrayCopy();
    }
}
