<?php declare(strict_types=1);

namespace Kilo;

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

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

	public array $hl = [];

	public int $idx;

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

	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)
	{
		switch ($name)
		{
			case 'size':
				return strlen($this->chars);

			case 'rsize':
				return strlen($this->render);

			default:
				return NULL;
		}
	}

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

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

		// 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
	{
		$idx = 0;

		for ($i = 0; $i < $this->size; $i++)
		{
			if ($this->chars[$i] === "\t")
			{
				$this->render[$idx++] = ' ';
				while ($idx % KILO_TAB_STOP !== 0)
				{
					$this->render[$idx++] = ' ';
				}
			}
			else
			{
				$this->render[$idx++] = $this->chars[$i];
			}
		}

		$this->updateSyntax();
	}

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

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

		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;

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

			// 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)
			{
				foreach ($keywords1 as $k)
				{
					$klen = strlen($k);
					$nextCharOffset = $i + $klen;
					$isEndOfLine = $nextCharOffset >= $this->rsize;
					$nextChar = ($isEndOfLine) ? "\0" : $this->render[$nextCharOffset];

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

				foreach ($keywords2 as $k)
				{
					$klen = strlen($k);
					$nextCharOffset = $i + $klen;
					$isEndOfLine = $nextCharOffset >= $this->rsize;
					$nextChar = ($isEndOfLine) ? "\0" : $this->render[$nextCharOffset];

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

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

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

	protected function updateSyntaxPHP():void
	{
		$tokens = $this->parent->syntax->tokens[$this->idx + 1];
		$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);

		// The line is probably just empty
		if ($tokens === NULL)
		{
			return;
		}

		// 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)
		{
			$char = $token['char'];
			$charLen = strlen($char);
			$charStart = strpos($this->render, $char, $offset);
			$charEnd = $charStart + $charLen;

			if ($token['typeName'] === 'RAW')
			{
				switch($token['char'])
				{
					case '^':
					case '%':
					case '+':
					case '-':
					case '*':
					case '/':
					case '.':
					case '|':
					case '~':
					case '>':
					case '<':
					case '=':
					case '!':
						array_replace_range($this->hl, $charStart, $charLen, Highlight::OPERATOR);
						$offset = $charEnd;
						continue 2;
				}
			}

			switch ($token['type'])
			{
				// Number literals
				case T_LNUMBER:
					array_replace_range($this->hl, $charStart, $charLen, Highlight::NUMBER);
					$offset = $charEnd;
					continue 2;

				// Simple string literals
				case T_CONSTANT_ENCAPSED_STRING:
					array_replace_range($this->hl, $charStart, $charLen, Highlight::STRING);
					$offset = $charEnd;
					continue 2;

				// Operators
				case T_AND_EQUAL:
				case T_BOOLEAN_AND:
				case T_BOOLEAN_OR:
				case T_COALESCE:
				case T_CONCAT_EQUAL:
				case T_DEC:
				case T_DIV_EQUAL:
				case T_DOUBLE_ARROW:
				case T_DOUBLE_COLON:
				case T_ELLIPSIS:
				case T_INC:
				case T_IS_EQUAL:
				case T_IS_GREATER_OR_EQUAL:
				case T_IS_IDENTICAL:
				case T_IS_NOT_EQUAL:
				case T_IS_NOT_IDENTICAL:
				case T_IS_SMALLER_OR_EQUAL:
				case T_SPACESHIP:
				case T_LOGICAL_AND:
				case T_LOGICAL_OR:
				case T_LOGICAL_XOR:
				case T_MINUS_EQUAL:
				case T_MOD_EQUAL:
				case T_MUL_EQUAL:
				case T_NS_SEPARATOR:
				case T_OBJECT_OPERATOR:
				case T_OR_EQUAL:
				case T_PAAMAYIM_NEKUDOTAYIM:
				case T_PLUS_EQUAL:
				case T_POW:
				case T_POW_EQUAL:
				case T_SL:
				case T_SL_EQUAL:
				case T_SR:
				case T_SR_EQUAL:
				case T_XOR_EQUAL:
					array_replace_range($this->hl, $charStart, $charLen, Highlight::OPERATOR);
					$offset = $charEnd;
					continue 2;

				// Simple variables
				case T_VARIABLE:
					array_replace_range($this->hl, $charStart, $charLen, Highlight::VARIABLE);
					$offset = $charEnd;
					continue 2;

				case T_COMMENT:
				case T_DOC_COMMENT:
					// TODO
				break;

				// Not string literals, but identifiers, keywords, etc.
				case T_STRING:
					if (in_array($char, $this->parent->syntax->keywords2, TRUE))
					{
						array_replace_range($this->hl, $charStart, $charLen, Highlight::KEYWORD2);
						$offset = $charEnd;
						continue 2;
					}
				break;

				// Keywords1
				case T_ABSTRACT:
				case T_AS:
				case T_BREAK:
				case T_CASE:
				case T_CATCH:
				case T_CLASS:
				case T_DO:
					array_replace_range($this->hl, $charStart, $charLen, Highlight::KEYWORD1);
					// $keyword = $this->getKeywordFromToken($token['type']);
					$offset = $charEnd;
					continue 2;
				break;

				// Keywords 2
			}
		}
	}
}