<?php

namespace Kirby\Content;

use Closure;
use Kirby\Cms\ModelWithContent;
use Stringable;

/**
 * Every field in a Kirby content text file
 * is being converted into such a Field object.
 *
 * Field methods can be registered for those Field
 * objects, which can then be used to transform or
 * convert the field value. This enables our
 * daisy-chaining API for templates and other components
 *
 * ```php
 * // Page field example with lowercase conversion
 * $page->myField()->lower();
 * ```
 *
 * @package   Kirby Content
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 */
class Field implements Stringable
{
	/**
	 * Field method aliases
	 */
	public static array $aliases = [];

	/**
	 * Registered field methods
	 */
	public static array $methods = [];

	/**
	 * Creates a new field object
	 *
	 * @param \Kirby\Cms\ModelWithContent|null $parent Parent object if available. This will be the page, site, user or file to which the content belongs
	 * @param string $key The field name
	 */
	public function __construct(
		protected ModelWithContent|null $parent,
		protected string $key,
		public mixed $value
	) {
	}

	/**
	 * Magic caller for field methods
	 */
	public function __call(string $method, array $arguments = []): mixed
	{
		$method = strtolower($method);

		if (isset(static::$methods[$method]) === true) {
			return (static::$methods[$method])(clone $this, ...$arguments);
		}

		if (isset(static::$aliases[$method]) === true) {
			$method = strtolower(static::$aliases[$method]);

			if (isset(static::$methods[$method]) === true) {
				return (static::$methods[$method])(clone $this, ...$arguments);
			}
		}

		return $this;
	}

	/**
	 * Simplifies the var_dump result
	 * @codeCoverageIgnore
	 *
	 * @see self::toArray()
	 */
	public function __debugInfo(): array
	{
		return $this->toArray();
	}

	/**
	 * Makes it possible to simply echo
	 * or stringify the entire object
	 *
	 * @see self::toString()
	 */
	public function __toString(): string
	{
		return $this->toString();
	}

	/**
	 * Checks if the field exists in the content data array
	 */
	public function exists(): bool
	{
		return $this->parent->content()->has($this->key);
	}

	/**
	 * Checks if the field content is empty
	 */
	public function isEmpty(): bool
	{
		$value = $this->value;

		if (is_string($value) === true) {
			$value = trim($value);
		}

		return
			$value === null ||
			$value === '' ||
			$value === [] ||
			$value === '[]';
	}

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

	/**
	 * Returns the name of the field
	 */
	public function key(): string
	{
		return $this->key;
	}

	/**
	 * @see self::parent()
	 */
	public function model(): ModelWithContent|null
	{
		return $this->parent;
	}

	/**
	 * Provides a fallback if the field value is empty
	 *
	 * @return $this|static
	 */
	public function or(mixed $fallback = null): static
	{
		if ($this->isNotEmpty() === true) {
			return $this;
		}

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

		$field = clone $this;
		$field->value = $fallback;
		return $field;
	}

	/**
	 * Returns the parent object of the field
	 */
	public function parent(): ModelWithContent|null
	{
		return $this->parent;
	}

	/**
	 * Converts the Field object to an array
	 */
	public function toArray(): array
	{
		return [$this->key => $this->value];
	}

	/**
	 * Returns the field value as string
	 */
	public function toString(): string
	{
		return (string)$this->value;
	}

	/**
	 * Returns the field content. If a new value is passed,
	 * the modified field will be returned. Otherwise it
	 * will return the field value.
	 */
	public function value(string|Closure|null $value = null): mixed
	{
		if ($value === null) {
			return $this->value;
		}

		if ($value instanceof Closure) {
			$value = $value->call($this, $this->value);
		}

		$clone = clone $this;
		$clone->value = (string)$value;

		return $clone;
	}
}
