<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\DI\Config\Adapters;

use Nette;
use Nette\DI\Config\Helpers;
use Nette\DI\Definitions\Reference;
use Nette\DI\Definitions\Statement;
use Nette\Neon;


/**
 * Reading and generating NEON files.
 */
final class NeonAdapter implements Nette\DI\Config\Adapter
{
	use Nette\SmartObject;

	private const PreventMergingSuffix = '!';

	/** @var string */
	private $file;


	/**
	 * Reads configuration from NEON file.
	 */
	public function load(string $file): array
	{
		$input = Nette\Utils\FileSystem::read($file);
		if (substr($input, 0, 3) === "\u{FEFF}") { // BOM
			$input = substr($input, 3);
		}

		$this->file = $file;
		$decoder = new Neon\Decoder;
		$node = $decoder->parseToNode($input);
		$traverser = new Neon\Traverser;
		$node = $traverser->traverse($node, [$this, 'removeUnderscoreVisitor']);
		$node = $traverser->traverse($node, [$this, 'convertAtSignVisitor']);
		return $this->process((array) $node->toValue());
	}


	/** @throws Nette\InvalidStateException */
	public function process(array $arr): array
	{
		$res = [];
		foreach ($arr as $key => $val) {
			if (is_string($key) && substr($key, -1) === self::PreventMergingSuffix) {
				if (!is_array($val) && $val !== null) {
					throw new Nette\DI\InvalidConfigurationException(sprintf(
						"Replacing operator is available only for arrays, item '%s' is not array (used in '%s')",
						$key,
						$this->file
					));
				}

				$key = substr($key, 0, -1);
				$val[Helpers::PREVENT_MERGING] = true;
			}

			if (is_array($val)) {
				$val = $this->process($val);

			} elseif ($val instanceof Neon\Entity) {
				if ($val->value === Neon\Neon::CHAIN) {
					$tmp = null;
					foreach ($this->process($val->attributes) as $st) {
						$tmp = new Statement(
							$tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')],
							$st->arguments
						);
					}

					$val = $tmp;
				} else {
					$tmp = $this->process([$val->value]);
					if (is_string($tmp[0]) && strpos($tmp[0], '?') !== false) {
						throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')");
					}

					$val = new Statement($tmp[0], $this->process($val->attributes));
				}
			}

			$res[$key] = $val;
		}

		return $res;
	}


	/**
	 * Generates configuration in NEON format.
	 */
	public function dump(array $data): string
	{
		array_walk_recursive(
			$data,
			function (&$val): void {
				if ($val instanceof Statement) {
					$val = self::statementToEntity($val);
				}
			}
		);
		return "# generated by Nette\n\n" . Neon\Neon::encode($data, Neon\Neon::BLOCK);
	}


	private static function statementToEntity(Statement $val): Neon\Entity
	{
		array_walk_recursive(
			$val->arguments,
			function (&$val): void {
				if ($val instanceof Statement) {
					$val = self::statementToEntity($val);
				} elseif ($val instanceof Reference) {
					$val = '@' . $val->getValue();
				}
			}
		);

		$entity = $val->getEntity();
		if ($entity instanceof Reference) {
			$entity = '@' . $entity->getValue();
		} elseif (is_array($entity)) {
			if ($entity[0] instanceof Statement) {
				return new Neon\Entity(
					Neon\Neon::CHAIN,
					[
						self::statementToEntity($entity[0]),
						new Neon\Entity('::' . $entity[1], $val->arguments),
					]
				);
			} elseif ($entity[0] instanceof Reference) {
				$entity = '@' . $entity[0]->getValue() . '::' . $entity[1];
			} elseif (is_string($entity[0])) {
				$entity = $entity[0] . '::' . $entity[1];
			}
		}

		return new Neon\Entity($entity, $val->arguments);
	}


	/** @internal */
	public function removeUnderscoreVisitor(Neon\Node $node)
	{
		if (!$node instanceof Neon\Node\EntityNode) {
			return;
		}

		$index = false;
		foreach ($node->attributes as $i => $attr) {
			if ($index) {
				$attr->key = $attr->key ?? new Neon\Node\LiteralNode((string) $i);
			}

			if ($attr->value instanceof Neon\Node\LiteralNode && $attr->value->value === '_') {
				unset($node->attributes[$i]);
				$index = true;

			} elseif ($attr->value instanceof Neon\Node\LiteralNode && $attr->value->value === '...') {
				trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED);
				unset($node->attributes[$i]);
				$index = true;
			}
		}
	}


	/** @internal */
	public function convertAtSignVisitor(Neon\Node $node)
	{
		if ($node instanceof Neon\Node\StringNode) {
			if (substr($node->value, 0, 2) === '@@') {
				trigger_error("There is no need to escape @ anymore, replace @@ with @ in: '$node->value' (used in $this->file)", E_USER_DEPRECATED);
			} else {
				$node->value = preg_replace('#^@#', '$0$0', $node->value); // escape
			}

		} elseif (
			$node instanceof Neon\Node\LiteralNode
			&& is_string($node->value)
			&& substr($node->value, 0, 2) === '@@'
		) {
			trigger_error("There is no need to escape @ anymore, replace @@ with @ and put string in quotes: '$node->value' (used in $this->file)", E_USER_DEPRECATED);
		}
	}
}
