<?php
declare( strict_types = 1 );

namespace Wikimedia\Parsoid\NodeData;

use stdClass;
use Wikimedia\JsonCodec\Hint;
use Wikimedia\JsonCodec\JsonCodec;
use Wikimedia\JsonCodec\JsonCodecable;
use Wikimedia\JsonCodec\JsonCodecableTrait;
use Wikimedia\Parsoid\Tokens\SourceRange;

/**
 * Editing data for a DOM node.  Managed by DOMDataUtils::get/setDataMw().
 *
 * To reduce memory usage, most of the properties need to be dynamic, but
 * we use the property declarations below to allow type checking.
 *
 * @property list<string|TemplateInfo> $parts
 * @property string $name
 * @property string $extPrefix
 * @property string $extSuffix
 * @property list<DataMwAttrib> $attribs Extended attributes of an HTML tag
 * @property string $src
 * @property string $caption
 * @property string $thumb
 * @property bool $autoGenerated
 * @property list<DataMwError> $errors
 * @property stdClass $body
 * @property mixed $html
 * @property float $scale
 * @property string $starttime
 * @property string $endtime
 * @property string $thumbtime
 * @property string $page
 * == Annotations ==
 * @property string $rangeId
 * @property SourceRange $wtOffsets
 * @property bool $extendedRange
 * @property stdClass $attrs Attributes for an extension tag or annotation (T367616 should be renamed)
 */
#[\AllowDynamicProperties]
class DataMw implements JsonCodecable {
	use JsonCodecableTrait;

	public function __construct( array $initialVals = [] ) {
		foreach ( $initialVals as $k => $v ) {
			// @phan-suppress-next-line PhanNoopSwitchCases
			switch ( $k ) {
				// Add cases here for components which should be instantiated
				// as proper classes.
				default:
					$this->$k = $v;
					break;
			}
		}
	}

	/** Returns true iff there are no dynamic properties of this object. */
	public function isEmpty(): bool {
		return ( (array)$this ) === [];
	}

	/**
	 * Deeply clone this object
	 *
	 * @return DataMw
	 */
	public function clone(): self {
		$codec = new JsonCodec;
		return $codec->newFromJsonArray( $codec->toJsonArray( $this ) );
	}

	/** @inheritDoc */
	public static function jsonClassHintFor( string $keyname ) {
		static $hints = null;
		if ( $hints === null ) {
			$hints = [
				'attribs' => Hint::build( DataMwAttrib::class, Hint::USE_SQUARE, Hint::LIST ),
				// T367616: 'attrs' should be renamed to 'extAttrs'
				'attrs' => Hint::build( stdClass::class, Hint::ALLOW_OBJECT ),
				'body' => Hint::build( stdClass::class, Hint::ALLOW_OBJECT ),
				'wtOffsets' => Hint::build( SourceRange::class, Hint::USE_SQUARE ),
				'parts' => Hint::build( TemplateInfo::class, Hint::STDCLASS, Hint::LIST ),
				'errors' => Hint::build( DataMwError::class, Hint::LIST ),
			];
		}
		return $hints[$keyname] ?? null;
	}

	/** @inheritDoc */
	public function toJsonArray(): array {
		$result = (array)$this;
		// T367141: Third party clients (eg Cite) create arrays instead of
		// error objects.  We should convert them to proper DataMwError
		// objects once those exist.
		if ( isset( $result['errors'] ) ) {
			$result['errors'] = array_map(
				fn ( $e ) => is_array( $e ) ? DataMwError::newFromJsonArray( $e ) :
					( $e instanceof DataMwError ? $e : DataMwError::newFromJsonArray( (array)$e ) ),
				$result['errors']
			);
		}
		// Legacy encoding of parts.
		if ( isset( $result['parts'] ) ) {
			$result['parts'] = array_map( static function ( $p ) {
				if ( $p instanceof TemplateInfo ) {
					$type = $p->type ?? 'template';
					if ( $type === 'parserfunction' ) {
						$type = 'template';
					}
					$pp = (object)[];
					$pp->$type = $p;
					return $pp;
				}
				return $p;
			}, $result['parts'] );
		}
		return $result;
	}

	/** @inheritDoc */
	public static function newFromJsonArray( array $json ): DataMw {
		// Decode legacy encoding of parts.
		if ( isset( $json['parts'] ) ) {
			$json['parts'] = array_map( static function ( $p ) {
				if ( is_object( $p ) ) {
					$type = 'template';
					if ( isset( $p->templatearg ) ) {
						$type = 'templatearg';
					}
					$p = $p->$type;
					if ( isset( $p->func ) ) {
						$type = 'parserfunction';
					}
					$p->type = $type;
				}
				return $p;
			}, $json['parts'] );
		}
		return new DataMw( $json );
	}
}
