<?php

/**
 * OpenAPI Petstore
 * PHP version 7.4
 *
 * @package OpenAPIServer
 * @author  OpenAPI Generator team
 * @link    https://github.com/openapitools/openapi-generator
 */

/**
 * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
 * The version of the OpenAPI document: 1.0.0
 * Generated by: https://github.com/openapitools/openapi-generator.git
 */

/**
 * NOTE: This class is auto generated by the openapi generator program.
 * https://github.com/openapitools/openapi-generator
 * Do not edit the class manually.
 */

declare(strict_types=1);

namespace OpenAPIServer;

use OpenAPIServer\Mock\OpenApiModelInterface;
use OpenAPIServer\Mock\OpenApiDataMockerInterface as IMocker;
use InvalidArgumentException;
use StdClass;

/**
 * BaseModel.
 */
class BaseModel implements OpenApiModelInterface
{
    // phpcs:disable Generic.Commenting.DocComment

    /**
     * @var string Constant with OAS schema of current class.
     * Should be overwritten by inherited class.
     */
    protected const MODEL_SCHEMA =
    <<<'SCHEMA'
    {
        "type" : "object",
        "properties": {}
    }
SCHEMA;

    /** @var string[] Valid OAS data types */
    protected const VALID_OAS_DATA_TYPES = [
        IMocker::DATA_TYPE_INTEGER,
        IMocker::DATA_TYPE_NUMBER,
        IMocker::DATA_TYPE_STRING,
        IMocker::DATA_TYPE_BOOLEAN,
        IMocker::DATA_TYPE_ARRAY,
        IMocker::DATA_TYPE_OBJECT,
    ];

    /**
     * @var string Models namespace.
     * Can be required for data deserialization when model contains referenced schemas.
     */
    protected const MODELS_NAMESPACE = '\OpenAPIServer\Model';

    /**
     * @var mixed Data container.
     * PHP has restrictions on variable names, while OAS is much more permissive.
     * This container helps to store unusual properties like '123_prop' without renaming.
     */
    protected $dataContainer;
    // phpcs:enable

    /**
     * Model constructor.
     */
    public function __construct()
    {
        $schema = static::getOpenApiSchema();
        $modelType = (array_key_exists('type', $schema)) ? $schema['type'] : null;
        $this->validateModelType($modelType, true);

        // set initial data
        switch ($modelType) {
            case IMocker::DATA_TYPE_OBJECT:
            case IMocker::DATA_TYPE_ARRAY:
                $this->dataContainer = [];
                break;
            case IMocker::DATA_TYPE_INTEGER:
            case IMocker::DATA_TYPE_NUMBER:
            case IMocker::DATA_TYPE_STRING:
            case IMocker::DATA_TYPE_BOOLEAN:
            default:
                // scalar type
                $this->dataContainer = null;
        }
    }

    /**
     * Gets OAS 3.0 schema mapped to current class.
     *
     * @return array
     */
    public static function getOpenApiSchema(): array
    {
        return json_decode(static::MODEL_SCHEMA, true);
    }

    /**
     * Creates new instance from provided data.
     *
     * @param mixed $data Data with values for new instance.
     *
     * @return OpenApiModelInterface
     */
    public static function createFromData($data): OpenApiModelInterface
    {
        $instance = new static();
        $instance->setData($data);
        return $instance;
    }

    /**
     * Sets instance data.
     *
     * @param mixed $data Data with values for new instance.
     *
     * @throws \InvalidArgumentException When value for array type is invalid.
     *
     * @return void
     */
    public function setData($data): void
    {
        $schema = (array) static::getOpenApiSchema();
        $modelType = (array_key_exists('type', $schema)) ? $schema['type'] : null;
        switch ($modelType) {
            case IMocker::DATA_TYPE_ARRAY:
                // data for array OAS type should be straight indexed array
                if (is_array($data)) {
                    $arr = [];
                    for ($i = 0; $i < count($data); $i++) {
                        if (isset($data[$i])) {
                            $arr[$i] = $data[$i];
                        }
                    }

                    if (count($arr) === count($data)) {
                        $this->dataContainer = $arr;
                        return;
                    }
                }

                throw new InvalidArgumentException(
                    sprintf('Invalid data for %s model because it accepts straight indexed arrays only', static::class)
                );
                break;
            case IMocker::DATA_TYPE_OBJECT:
                foreach ($data as $key => $value) {
                    // this action handles __set method
                    $this->{$key} = $value;
                }
                break;
            case IMocker::DATA_TYPE_INTEGER:
            case IMocker::DATA_TYPE_NUMBER:
            case IMocker::DATA_TYPE_STRING:
            case IMocker::DATA_TYPE_BOOLEAN:
            default:
                $this->dataContainer = $data;
                break;
        }
    }

    /**
     * Returns instance data.
     *
     * @return mixed
     */
    public function getData()
    {
        $data = null;
        $schema = (array) static::getOpenApiSchema();
        $modelType = (array_key_exists('type', $schema)) ? $schema['type'] : null;
        switch ($modelType) {
            case IMocker::DATA_TYPE_OBJECT:
                // need to convert data container to object
                $data = new StdClass();
                $definedProps = (array_key_exists('properties', $schema)) ? $schema['properties'] : null;
                if (is_array($definedProps) || is_object($definedProps)) {
                    foreach ($definedProps as $propName => $propSchema) {
                        if (array_key_exists($propName, $this->dataContainer)) {
                            $data->{$propName} = $this->dataContainer[$propName];
                        } elseif (array_key_exists('required', $schema) && in_array($propName, $schema['required'])) {
                            // property is required but not set
                            $data->{$propName} = null;
                        }
                    }
                }
                break;
            case IMocker::DATA_TYPE_INTEGER:
            case IMocker::DATA_TYPE_NUMBER:
            case IMocker::DATA_TYPE_STRING:
            case IMocker::DATA_TYPE_BOOLEAN:
            case IMocker::DATA_TYPE_ARRAY:
            default:
                $data = $this->dataContainer;
        }
        return $data;
    }

    /**
     * Writes data to inaccessible (protected or private) or non-existing properties.
     * Ref @link https://www.php.net/manual/en/language.oop5.overloading.php#object.set
     *
     * @param string $param Property name.
     * @param mixed  $value Property value.
     *
     * @throws \InvalidArgumentException When property doesn't exist in related OAS schema.
     *
     * @return void
     */
    public function __set(string $param, $value): void
    {
        $schema = (array) static::getOpenApiSchema();
        $modelType = (array_key_exists('type', $schema)) ? $schema['type'] : null;
        switch ($modelType) {
            case IMocker::DATA_TYPE_OBJECT:
                $definedProps = (array_key_exists('properties', $schema)) ? (array) $schema['properties'] : null;
                if (is_array($definedProps) && !in_array($param, array_keys($definedProps))) {
                    throw new InvalidArgumentException(
                        sprintf('Cannot set %s property of %s model because it doesn\'t exist in related OAS schema', $param, static::class)
                    );
                }
                $this->dataContainer[$param] = $value;
                break;
            case IMocker::DATA_TYPE_INTEGER:
            case IMocker::DATA_TYPE_NUMBER:
            case IMocker::DATA_TYPE_STRING:
            case IMocker::DATA_TYPE_BOOLEAN:
            case IMocker::DATA_TYPE_ARRAY:
            default:
                // scalar type and array cannot use property assignation
                throw new InvalidArgumentException(
                    sprintf('Cannot set %s property of %s model because it\'s %s type. Use setData method instead', $param, static::class, $modelType)
                );
        }
    }

    /**
     * Reads data from inaccessible (protected or private) or non-existing properties.
     * Ref @link https://www.php.net/manual/en/language.oop5.overloading.php#object.get
     *
     * @param string $param Property name.
     *
     * @throws \InvalidArgumentException When property doesn't exist in related OAS schema.
     *
     * @return mixed Property value
     */
    public function __get(string $param)
    {
        $schema = (array) static::getOpenApiSchema();
        $modelType = (array_key_exists('type', $schema)) ? $schema['type'] : null;
        $definedProps = (array_key_exists('properties', $schema)) ? (array) $schema['properties'] : null;
        if (!in_array($modelType, [null, IMocker::DATA_TYPE_OBJECT])) {
            // scalar type
            throw new InvalidArgumentException(
                sprintf('Cannot get %s property of %s model because getter is for object OAS type only', $param, static::class)
            );
        }

        if (
            is_array($definedProps)
            && in_array($param, array_keys($definedProps))
        ) {
            return $this->dataContainer[$param];
        } elseif ($definedProps === null) {
            // props are undefined
            return (isset($this->dataContainer[$param])) ? $this->dataContainer[$param] : null;
        }

        throw new InvalidArgumentException(
            sprintf('Cannot get %s property of %s model because it doesn\'t exist in related OAS schema', $param, static::class)
        );
    }

    /**
     * Serializes the object to a value that can be serialized natively by json_encode().
     * Ref @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
     *
     * @return mixed Returns data which can be serialized by json_encode(), which is a value of any type other than a resource.
     */
    public function jsonSerialize()
    {
        return $this->getData();
    }

    /**
     * Checks if type is valid OAS data type.
     *
     * @param string|null $type           Model type.
     * @param bool        $throwException Throws InvalidArgumentException when set to true and processed type is invalid.
     *
     * @throws \InvalidArgumentException When $throwException set to TRUE.
     *
     * @return bool
     */
    protected function validateModelType(?string $type = null, bool $throwException = true): bool
    {
        $isValid = in_array($type, static::VALID_OAS_DATA_TYPES);
        if ($type !== null && $isValid === false && $throwException) {
            throw new InvalidArgumentException(
                sprintf(
                    'Invalid OAS schema of %s model, "type" must be one of %s',
                    static::class,
                    implode(', ', static::VALID_OAS_DATA_TYPES)
                )
            );
        }
        return $isValid;
    }

    /**
     * Returns models namespace.
     *
     * @return string
     */
    public static function getModelsNamespace(): string
    {
        return static::MODELS_NAMESPACE . stripslashes('\\');
    }
}
