<?php declare(strict_types=1);

namespace Aviat\Kilo;

use Aviat\Kilo\Enum\Highlight;
use Aviat\Kilo\Enum\KeyCode;

/**
 * @property-read int $size
 * @property-read int $rsize
 */
class Row {
	use Traits\MagicProperties;

	private string $chars = '';
	public string $render = '';

	public array $hl = [];

	public int $idx;

	// This feels dirty...
	private Editor $parent;
	private bool $hlOpenComment = FALSE;

	private const T_RAW = -1;

	public static function new(Editor $parent, string $chars, int $idx): self
	{
		$self = new self();
		$self->chars = $chars;
		$self->parent = $parent;
		$self->idx = $idx;

		return $self;
	}

	private function __construct() {}

	public function __get(string $name): mixed
	{
		return match ($name)
		{
			'size' => strlen($this->chars),
			'rsize' => strlen($this->render),
			'chars' => $this->chars,
			default => NULL,
		};
	}

	public function __set(string $name, mixed $value): void
	{
		if ($name === 'chars')
		{
			$this->chars = $value;
			$this->update();
		}
	}

	public function __toString(): string
	{
		return $this->chars . "\n";
	}

	public function __debugInfo(): array
	{
		return [
			'size' => $this->size,
			'rsize' => $this->rsize,
			'chars' => $this->chars,
			'render' => $this->render,
			'hl' => $this->hl,
			'hlOpenComment' => $this->hlOpenComment,
		];
	}

	public function insertChar(int $at, string $c): void
	{
		if ($at < 0 || $at > $this->size)
		{
			$this->appendString($c);
			return;
		}

		// Safely insert into arbitrary position in the existing string
		$this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
		$this->update();

		$this->parent->dirty++;
	}

	public function appendString(string $s): void
	{
		$this->chars .= $s;
		$this->update();

		$this->parent->dirty++;
	}

	public function deleteChar(int $at): void
	{
		if ($at < 0 || $at >= $this->size)
		{
			return;
		}

		$this->chars = substr_replace($this->chars, '', $at, 1);
		$this->update();

		$this->parent->dirty++;
	}

	public function update(): void
	{
		$this->render = tabs_to_spaces($this->chars);

		$this->updateSyntax();
	}

	// ------------------------------------------------------------------------
	// ! Syntax Highlighting
	// ------------------------------------------------------------------------

	public function updateSyntax(): void
	{
		$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);

		if ($this->parent->syntax === NULL)
		{
			return;
		}

		if ($this->parent->syntax->filetype === 'PHP')
		{
			$this->updateSyntaxPHP();
			return;
		}

		$keywords1 = $this->parent->syntax->keywords1;
		$keywords2 = $this->parent->syntax->keywords2;

		$scs = $this->parent->syntax->singleLineCommentStart;
		$mcs = $this->parent->syntax->multiLineCommentStart;
		$mce = $this->parent->syntax->multiLineCommentEnd;

		$scsLen = strlen($scs);
		$mcsLen = strlen($mcs);
		$mceLen = strlen($mce);

		$prevSep = TRUE;
		$inString = '';
		$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);

		$i = 0;

		while ($i < $this->rsize)
		{
			$char = $this->render[$i];
			$prevHl = ($i > 0) ? $this->hl[$i - 1] : Highlight::NORMAL;

			// Single-line comments
			if ($scsLen > 0 && $inString === '' && $inComment === FALSE
				&& substr($this->render, $i, $scsLen) === $scs)
			{
				array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
				break;
			}

			// Multi-line comments
			if ($mcsLen > 0 && $mceLen > 0 && $inString === '')
			{
				if ($inComment)
				{
					$this->hl[$i] = Highlight::ML_COMMENT;
					if (substr($this->render, $i, $mceLen) === $mce)
					{
						array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT);
						$i += $mceLen;
						$inComment = FALSE;
						$prevSep = TRUE;
						continue;
					}

					$i++;
					continue;
				}

				if (substr($this->render, $i, $mcsLen) === $mcs)
				{
					array_replace_range($this->hl, $i, $mcsLen, Highlight::ML_COMMENT);
					$i += $mcsLen;
					$inComment = TRUE;
					continue;
				}
			}

			// String/Char literals
			if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_STRINGS)
			{
				if ($inString !== '')
				{
					$this->hl[$i] = Highlight::STRING;

					// Check for escaped character
					if ($char === '\\' && $i+1 < $this->rsize)
					{
						$this->hl[$i + 1] = Highlight::STRING;
						$i += 2;
						continue;
					}

					if ($char === $inString)
					{
						$inString = '';
					}
					$i++;
					$prevSep = 1;
					continue;
				}

				if ( $char === '"' || $char === '\'')
				{
					$inString = $char;
					$this->hl[$i] = Highlight::STRING;
					$i++;
					continue;
				}
			}

			// Numbers, including decimal points
			if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_NUMBERS)
			{
				if (
					($char === '.' && $prevHl === Highlight::NUMBER) ||
					(($prevSep || $prevHl === Highlight::NUMBER) && is_digit($char))
				)
				{
					$this->hl[$i] = Highlight::NUMBER;
					$i++;
					$prevSep = FALSE;
					continue;
				}
			}

			// Keywords
			if ($prevSep)
			{
				$findKeywords = function (array $keywords, int $syntaxType) use (&$i): void
				{
					foreach ($keywords as $k)
					{
						$klen = strlen($k);
						$nextCharOffset = $i + $klen;
						$isEndOfLine = $nextCharOffset >= $this->rsize;
						$nextChar = ($isEndOfLine) ? KeyCode::NULL : $this->render[$nextCharOffset];

						if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
						{
							array_replace_range($this->hl, $i, $klen, $syntaxType);
							$i += $klen - 1;
							break;
						}
					}
				};

				$findKeywords($keywords1, Highlight::KEYWORD1);
				$findKeywords($keywords2, Highlight::KEYWORD2);
			}

			$prevSep = is_separator($char);
			$i++;
		}

		$changed = $this->hlOpenComment !== $inComment;
		$this->hlOpenComment = $inComment;
		if ($changed && $this->idx + 1 < $this->parent->numRows)
		{
			// @codeCoverageIgnoreStart
			$this->parent->rows[$this->idx + 1]->updateSyntax();
			// @codeCoverageIgnoreEnd
		}
	}

	protected function updateSyntaxPHP():void
	{
		$rowNum = $this->idx + 1;

		$hasRowTokens = array_key_exists($rowNum, $this->parent->tokens);

		if ( ! (
			$hasRowTokens &&
			$this->idx < $this->parent->numRows
		))
		{
			// @codeCoverageIgnoreStart
			return;
			// @codeCoverageIgnoreEnd
		}

		$tokens = $this->parent->tokens[$rowNum];

		$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);

		// Keep track of where you are in the line, so that
		// multiples of the same tokens can be effectively matched
		$offset = 0;

		foreach ($tokens as $token)
		{
			if ($offset >= $this->rsize)
			{
				// @codeCoverageIgnoreStart
				break;
				// @codeCoverageIgnoreEnd
			}

			// A multi-line comment can end in the middle of a line...
			if ($inComment)
			{
				// Try looking for the end of the comment first
				$commentEnd = strpos($this->render, '*/');
				if ($commentEnd !== FALSE)
				{
					$inComment = FALSE;
					array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::ML_COMMENT);
					$offset = $commentEnd;
					continue;
				}

				// Otherwise, just set the whole row
				$this->hl = array_fill(0, $this->rsize, Highlight::ML_COMMENT);
				$this->hl[$offset] = Highlight::ML_COMMENT;
				break;
			}

			$char = $token['char']; // ?? '';
			$charLen = strlen($char);
			if ($charLen === 0 || $offset >= $this->rsize)
			{
				// @codeCoverageIgnoreStart
				continue;
				// @codeCoverageIgnoreEnd
			}
			$charStart = strpos($this->render, $char, $offset);
			if ($charStart === FALSE)
			{
				continue;
			}
			$charEnd = $charStart + $charLen;

			// Start of multiline comment/single line comment
			if (in_array($token['type'], [T_DOC_COMMENT, T_COMMENT], TRUE))
			{
				// Single line comments
				if (str_contains($char, '//') || str_contains($char, '#'))
				{
					array_replace_range($this->hl, $charStart, $charLen, Highlight::COMMENT);
					break;
				}

				// Start of multi-line comment
				$start = strpos($this->render, '/*', $offset);
				$end = strpos($this->render, '*/', $offset);
				$hasStart = $start !== FALSE;
				$hasEnd = $end !== FALSE;

				if ($hasStart)
				{
					if ($hasEnd)
					{
						$len = $end - $start + 2;
						array_replace_range($this->hl, $start, $len, Highlight::ML_COMMENT);
						$inComment = FALSE;
					}
					else
					{
						$inComment = TRUE;
						array_replace_range($this->hl, $start, $charLen - $offset, Highlight::ML_COMMENT);
						$offset = $start + $charLen - $offset;
					}
				}

				if ($inComment)
				{
					break;
				}
			}

			$tokenHighlight = php_token_to_highlight($token['type']);
			$charHighlight = php_char_to_highlight(trim($token['char']));

			$hl = match(true) {
				// Matches a predefined PHP token
				$token['type'] !== self::T_RAW && $tokenHighlight !== Highlight::NORMAL
					=> $tokenHighlight,

				// Matches a specific syntax character
				$charHighlight !== Highlight::NORMAL => $charHighlight,

				// Types/identifiers/keywords that don't have their own token, but are
				// defined as keywords
				in_array($token['char'], $this->parent->syntax->keywords2 ?? [], TRUE)
					=> Highlight::KEYWORD2,

				default => Highlight::NORMAL,
			};

			if ($hl !== Highlight::NORMAL)
			{
				array_replace_range($this->hl, $charStart, $charLen, $hl);
				$offset = $charEnd;
			}
		}

		$changed = $this->hlOpenComment !== $inComment;
		$this->hlOpenComment = $inComment;
		if ($changed && ($this->idx + 1) < $this->parent->numRows)
		{
			// @codeCoverageIgnoreStart
			$this->parent->rows[$this->idx + 1]->updateSyntax();
			// @codeCoverageIgnoreEnd
		}
	}
}