<?php

declare(strict_types=1);

/*
 * This file is part of the ActivityPhp package.
 *
 * Copyright (c) landrok at github.com/landrok
 *
 * For the full copyright and license information, please see
 * <https://github.com/landrok/activitypub/blob/master/LICENSE>.
 */

namespace ActivityPhp;

use ActivityPhp\Type\AbstractObject;
use ActivityPhp\Type\Dialect;
use ActivityPhp\Type\TypeResolver;
use ActivityPhp\Type\Validator;
use Exception;

/**
 * \ActivityPhp\Type is a Factory for ActivityStreams 2.0 types.
 *
 * It provides shortcuts methods for type instanciation and more.
 *
 * @see https://www.w3.org/TR/activitystreams-vocabulary/#types
 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
 * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
 * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
 */
abstract class Type
{
    /**
     * Factory method to create type instance and set attributes values
     *
     * To see which default types are defined and their attributes:
     *
     * @see https://www.w3.org/TR/activitystreams-vocabulary/#types
     * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
     * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
     * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
     *
     * @param  string|array<string,mixed> $type
     * @param  array<string,mixed>  $attributes
     */
    public static function create($type, array $attributes = []): AbstractObject
    {
        if (! is_string($type) && ! is_array($type)) {
            throw new Exception(
                'Type parameter must be a string or an array. Given='
                . gettype($type)
            );
        }

        if (is_array($type)) {
            if (! isset($type['type'])) {
                throw new Exception(
                    "Type parameter must have a 'type' key"
                );
            }

            $attributes = $type;
        }

        try {
            $class = is_array($type)
                ? TypeResolver::getClass($type['type'])
                : TypeResolver::getClass($type);
        } catch (Exception $exception) {
            $message = json_encode($attributes, JSON_PRETTY_PRINT);
            throw new Exception(
                $exception->getMessage() . "\n{$message}"
            );
        }

        if (is_string($class)) {
            $class = new $class();
        }

        self::extend($class);

        foreach ($attributes as $name => $value) {
            $class->set($name, $value);
        }

        return $class;
    }

    /**
     * Create an activitystream type from a JSON string
     */
    public static function fromJson(string $json): AbstractObject
    {
        $data = json_decode($json, true);

        if (json_last_error() === JSON_ERROR_NONE
            && is_array($data)
        ) {
            return self::create($data);
        }

        throw new Exception(
            sprintf(
                "An error occurred during the JSON decoding.\n '%s'",
                $json
            )
        );
    }

    /**
     * Add a custom type definition
     * It overrides defined types
     *
     * @param string $name A short name.
     * @param string $class Fully qualified class name
     */
    public static function add(string $name, string $class): void
    {
        TypeResolver::addCustomType($name, $class);
    }

    /**
     * Add a custom validator for an attribute.
     * It checks that it implements Validator\Interface
     *
     * @param string $name An attribute name to validate.
     * @param string $class A validator class name
     */
    public static function addValidator(string $name, string $class): void
    {
        Validator::add($name, $class);
    }

    /**
     * ActivityPub real world applications not only implements the basic
     * vocabulary.
     * They extends basic protocol with custom properties.
     * These extensions are called dialects.
     *
     * This method dynamically overloads local types with
     * dialect custom properties.
     */
    private static function extend(AbstractObject $type): void
    {
        // @todo should call Dialect stack to see if there are any
        // properties to overloads $type with
        Dialect::extend($type);
    }
}
