<?php
/* ============================================================================
 * Copyright 2020 Zindex Software
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ============================================================================ */

namespace CommonsBooking\Opis\JsonSchema\Keywords;

use CommonsBooking\Opis\JsonSchema\Errors\ValidationError;
use CommonsBooking\Opis\JsonSchema\Exceptions\UnresolvedReferenceException;
use CommonsBooking\Opis\JsonSchema\{Schema, ValidationContext, Variables, Uri};

class RecursiveRefKeyword extends AbstractRefKeyword
{
    protected Uri $uri;
    /** @var bool|null|Schema */
    protected $resolved = false;
    protected string $anchor;
    protected $anchorValue;

    public function __construct(
        Uri $uri,
        ?Variables $mapper,
        ?Variables $globals,
        ?array $slots = null,
        string $keyword = '$recursiveRef',
        string $anchor = '$recursiveAnchor',
        $anchorValue = true
    ) {
        parent::__construct($mapper, $globals, $slots, $keyword);
        $this->uri = $uri;
        $this->anchor = $anchor;
        $this->anchorValue = $anchorValue;
    }

    /**
     * @inheritDoc
     */
    public function doValidate(ValidationContext $context, Schema $schema): ?ValidationError
    {
        if ($this->resolved === false) {
            $this->resolved = $context->loader()->loadSchemaById($this->uri);
        }

        if ($this->resolved === null) {
            throw new UnresolvedReferenceException((string)$this->uri, $schema, $context);
        }

        $new_context = $this->createContext($context, $schema);

        if (!$this->hasRecursiveAnchor($this->resolved)) {
            $this->setLastRefSchema($this->resolved);
            return $this->resolved->validate($new_context);
        }

        $ok_sender = $this->resolveSchema($context);

        if (!$ok_sender) {
            $this->setLastRefSchema($this->resolved);
            return $this->resolved->validate($new_context);
        }

        $this->setLastRefSchema($ok_sender);

        return $ok_sender->validate($new_context);
    }

    protected function resolveSchema(ValidationContext $context): ?Schema
    {
        $ok = null;
        $loader = $context->loader();

        while ($context) {
            $sender = $context->sender();

            if (!$sender) {
                break;
            }

            if (!$this->hasRecursiveAnchor($sender)) {
                if ($sender->info()->id()) {
                    // id without recursiveAnchor
                    break;
                }

                $sender = $loader->loadSchemaById($sender->info()->root());
                if (!$sender || !$this->hasRecursiveAnchor($sender)) {
                    // root without recursiveAnchor
                    break;
                }
            }

            if ($sender->info()->id()) {
                // id with recursiveAnchor
                $ok = $sender;
            } else {
                // root with recursiveAnchor
                $ok = $loader->loadSchemaById($sender->info()->root());
            }

            $context = $context->parent();
        }

        return $ok;
    }

    protected function hasRecursiveAnchor(?Schema $schema): bool
    {
        if (!$schema) {
            return false;
        }

        $info = $schema->info();

        if (!$info->isObject()) {
            return false;
        }

        $data = $info->data();

        if (!property_exists($data, $this->anchor)) {
            return false;
        }

        return $data->{$this->anchor} === $this->anchorValue;
    }
}