<?php

namespace Swaggest\JsonSchema\Structure;

use Swaggest\JsonSchema\Constraint\Properties;
use Swaggest\JsonSchema\Context;
use Swaggest\JsonSchema\Schema;
use Swaggest\JsonSchema\Wrapper;
use Swaggest\JsonSchema\NameMirror;

trait ClassStructureTrait
{
    use ObjectItemTrait;

    /**
     * @return Wrapper
     */
    public static function schema()
    {
        static $schemas = array();
        $className = get_called_class();
        $schemaWrapper = &$schemas[$className];

        if (null === $schemaWrapper) {
            $schema = new Schema();
            $properties = new Properties();
            $schema->properties = $properties;
            $schema->objectItemClass = $className;
            $schemaWrapper = new Wrapper($schema);
            static::setUpProperties($properties, $schema);
            if (null === $schema->getFromRefs()) {
                $schema->setFromRef('#/definitions/' . $className);
            }
            if ($properties->isEmpty()) {
                $schema->properties = null;
            }
            $properties->lock();
        }

        return $schemaWrapper;
    }

    /**
     * @return Properties|static|null
     */
    public static function properties()
    {
        return static::schema()->getProperties();
    }

    /**
     * @param mixed $data
     * @param Context $options
     * @return static|mixed
     * @throws \Swaggest\JsonSchema\Exception
     * @throws \Swaggest\JsonSchema\InvalidValue
     */
    public static function import($data, ?Context $options = null)
    {
        return static::schema()->in($data, $options);
    }

    /**
     * @param mixed $data
     * @param Context $options
     * @return mixed
     * @throws \Swaggest\JsonSchema\InvalidValue
     * @throws \Exception
     */
    public static function export($data, ?Context $options = null)
    {
        return static::schema()->out($data, $options);
    }

    /**
     * @param ObjectItemContract $objectItem
     * @return static
     */
    public static function pick(ObjectItemContract $objectItem)
    {
        $className = get_called_class();
        return $objectItem->getNestedObject($className);
    }

    /**
     * @return static
     */
    public static function create()
    {
        return new static;
    }

    protected $__validateOnSet = true; // todo skip validation during import

    /**
     * @return \stdClass
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        $result = new \stdClass();
        $schema = static::schema();
        $properties = $schema->getProperties();
        $processed = array();
        if (null !== $properties) {
            foreach ($properties->getDataKeyMap() as $propertyName => $dataName) {
                // Get uninitialized properties as null; direct access will throw error on typed properties
                $value = isset($this->$propertyName) ? $this->$propertyName : null;
                
                $value = $this->$propertyName;

                // Value is exported if exists.
                if (null !== $value || array_key_exists($propertyName, $this->__arrayOfData)) {
                    $result->$dataName = $value;
                    $processed[$propertyName] = true;
                    continue;
                }

                // Non-existent value is only exported if belongs to nullable property (having 'null' in type array).
                $property = $schema->getProperty($propertyName);
                if ($property instanceof Schema) {
                    $types = $property->type;
                    if ($types === Schema::NULL || (is_array($types) && in_array(Schema::NULL, $types))) {
                        $result->$dataName = $value;
                    }
                }
            }
        }
        foreach ($schema->getNestedPropertyNames() as $name) {
            /** @var ObjectItem $nested */
            $nested = $this->$name;
            if (null !== $nested) {
                foreach ((array)$nested->jsonSerialize() as $key => $value) {
                    $result->$key = $value;
                }
            }
        }

        if (!empty($this->__arrayOfData)) {
            foreach ($this->__arrayOfData as $name => $value) {
                if (!isset($processed[$name])) {
                    $result->$name = $this->{$name};
                }
            }
        }

        return $result;
    }

    /**
     * @return static|NameMirror
     */
    public static function names(?Properties $properties = null, $mapping = Schema::DEFAULT_MAPPING)
    {
        if ($properties !== null) {
            return new NameMirror($properties->getDataKeyMap($mapping));
        }

        static $nameflector = null;
        if (null === $nameflector) {
            $nameflector = new NameMirror();
        }
        return $nameflector;
    }

    public function __set($name, $column) // todo nested schemas
    {
        if ($this->__validateOnSet) {
            if ($property = static::schema()->getProperty($name)) {
                $property->out($column);
            }
        }
        $this->__arrayOfData[$name] = $column;
        return $this;
    }

    public static function className()
    {
        return get_called_class();
    }

    /**
     * @throws \Exception
     * @throws \Swaggest\JsonSchema\InvalidValue
     */
    public function validate()
    {
        static::schema()->out($this);
    }
}
