<?php

namespace Kirby\Cms;

use Kirby\Content\Content;
use Kirby\Content\Field;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Str;
use Throwable;

/**
 * Represents a single block
 * which can be inspected further or
 * converted to HTML
 * @since 3.5.0
 *
 * @package   Kirby Cms
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 */
class Block extends Item
{
	use HasMethods;

	public const ITEMS_CLASS = Blocks::class;

	/**
	 * Registry with all block models
	 */
	public static array $models = [];

	protected Content $content;
	protected bool $isHidden;
	protected string $type;

	/**
	 * Proxy for content fields
	 */
	public function __call(string $method, array $args = []): mixed
	{
		// block methods
		if ($this->hasMethod($method)) {
			return $this->callMethod($method, $args);
		}

		return $this->content()->get($method);
	}

	/**
	 * Creates a new block object
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException
	 */
	public function __construct(array $params)
	{
		parent::__construct($params);

		// @deprecated import old builder format
		// @todo block.converter remove eventually
		// @codeCoverageIgnoreStart
		$params = BlockConverter::builderBlock($params);
		$params = BlockConverter::editorBlock($params);
		// @codeCoverageIgnoreEnd

		if (isset($params['type']) === false) {
			throw new InvalidArgumentException('The block type is missing');
		}

		// make sure the content is always defined as array to keep
		// at least a bit of backward compatibility with older fields
		if (is_array($params['content'] ?? null) === false) {
			$params['content'] = [];
		}

		$this->isHidden = $params['isHidden'] ?? false;
		$this->type     = $params['type'];

		// create the content object
		$this->content = new Content($params['content'], $this->parent);
	}

	/**
	 * Converts the object to a string
	 */
	public function __toString(): string
	{
		return $this->toHtml();
	}

	/**
	 * Returns the content object
	 */
	public function content(): Content
	{
		return $this->content;
	}

	/**
	 * Controller for the block snippet
	 */
	public function controller(): array
	{
		return [
			'block'   => $this,
			'content' => $this->content(),
			// deprecated block data
			'data'    => $this,
			'id'      => $this->id(),
			'prev'    => $this->prev(),
			'next'    => $this->next()
		];
	}

	/**
	 * Converts the block to HTML and then
	 * uses the Str::excerpt method to create
	 * a non-formatted, shortened excerpt from it
	 */
	public function excerpt(mixed ...$args): string
	{
		return Str::excerpt($this->toHtml(), ...$args);
	}

	/**
	 * Constructs a block object with registering blocks models
	 * @internal
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException
	 */
	public static function factory(array $params): static
	{
		$type = $params['type'] ?? null;

		if (
			empty($type) === false &&
			$class = (static::$models[$type] ?? null)
		) {
			$object = new $class($params);

			if ($object instanceof self) {
				return $object;
			}
		}

		// default model for blocks
		if ($class = (static::$models['default'] ?? null)) {
			$object = new $class($params);

			if ($object instanceof self) {
				return $object;
			}
		}

		return new static($params);
	}

	/**
	 * Checks if the block is empty
	 */
	public function isEmpty(): bool
	{
		return empty($this->content()->toArray());
	}

	/**
	 * Checks if the block is hidden
	 * from being rendered in the frontend
	 */
	public function isHidden(): bool
	{
		return $this->isHidden;
	}

	/**
	 * Checks if the block is not empty
	 */
	public function isNotEmpty(): bool
	{
		return $this->isEmpty() === false;
	}

	/**
	 * Returns the sibling collection that filtered by block status
	 */
	protected function siblingsCollection(): Blocks
	{
		return $this->siblings->filter('isHidden', $this->isHidden());
	}

	/**
	 * Returns the block type
	 */
	public function type(): string
	{
		return $this->type;
	}

	/**
	 * The result is being sent to the editor
	 * via the API in the panel
	 */
	public function toArray(): array
	{
		return [
			'content'  => $this->content()->toArray(),
			'id'       => $this->id(),
			'isHidden' => $this->isHidden(),
			'type'     => $this->type(),
		];
	}

	/**
	 * Converts the block to html first
	 * and then places that inside a field
	 * object. This can be used further
	 * with all available field methods
	 */
	public function toField(): Field
	{
		return new Field($this->parent(), $this->id(), $this->toHtml());
	}

	/**
	 * Converts the block to HTML
	 */
	public function toHtml(): string
	{
		try {
			$kirby = $this->parent()->kirby();
			return (string)$kirby->snippet(
				'blocks/' . $this->type(),
				$this->controller(),
				true
			);
		} catch (Throwable $e) {
			if ($kirby->option('debug') === true) {
				return '<p>Block error: "' . $e->getMessage() . '" in block type: "' . $this->type() . '"</p>';
			}

			return '';
		}
	}
}
