<?php

namespace App\Classes;

use Illuminate\Support\Collection;

class ANSI
{
	/* 8 BIT COLORS
	 * Foreground 0-3, Background 4-7
	 * Color 0-2, 4-6, High 3, 7
	 */
	private const COLOR_8BIT		= 0x1B;
	private const COLOR_HIGH		= 1; // Bit 1.
	private const COLOR_BLACK		= 0; // F30 B40
	private const COLOR_RED			= 1; // F31 B41
	private const COLOR_GREEN		= 2; // F32 B42
	private const COLOR_YELLOW		= 3; // F33 B43
	private const COLOR_BLUE		= 4; // F34 B44
	private const COLOR_MAGENTA		= 5; // F35 B45
	private const COLOR_CYAN		= 6; // F36 B46
	private const COLOR_WHITE		= 7; // F37 B47

	private const DEFAULT_FORE		= 37;
	private const DEFAULT_BACK		= 40;

	/* 256 BIT COLORS */
	/* 0x26 0xAA 0xBB */

	public const LOGO_BUFFER_WIDTH = 1;
	public const LOGO_BUFFER_HEIGHT = 0;	// Not implemented
	public const LOGO_OFFSET_WIDTH = 1;
	public const LOGO_OFFSET_HEIGHT = 0;	// Not implemented

	private Collection $width;
	private Collection $ansi;
	private const BUFREAD = 2048;

	/* MAGIC METHODS */

	public function __construct(string $file='')
	{
		$this->width = collect();
		$this->ansi = collect();

		if ($file) {
			$f = fopen($file,'r');
			while (! feof($f)) {
				$line = stream_get_line($f,self::BUFREAD,"\r");

				// If the last line is blank, we'll ignore it
				if ((! feof($f)) || $line) {
					$this->width->push(self::line_width($line,FALSE));
					$this->ansi->push(array_map(function($item) { return ord($item); },str_split($line,1)));
				}
			}
			fclose($f);
		}

		return $this->ansi;
	}

	public function __get($key)
	{
		switch ($key) {
			case 'width':
				return $this->width;

			case 'max_width':
				return $this->width->max()+self::LOGO_BUFFER_WIDTH+self::LOGO_OFFSET_WIDTH;

			case 'height':
				return $this->ansi->count()+self::LOGO_BUFFER_HEIGHT+self::LOGO_OFFSET_HEIGHT;
		}
	}

	/* STATIC METHODS */

	/**
	 * Convert a binary ANSI file back to its ANSI version
	 *
	 * @param string $file
	 * @return string
	 */
	public static function ansi(string $file)
	{
		return static::bin_to_ansi((new self($file))->ansi->toArray());
	}

	/**
	 * Convert an array of ANSI codes into a binary equivalent
	 *
	 * @param array $code
	 * @return string
	 */
	public static function ansi_code(array $code): string
	{
		if (!$code)
			return '';

		return "\x1b".chr(self::code($code));
	}

	/**
	 * Render an ANSI binary code into an ANSI string
	 *
	 * @param int $code
	 * @return string
	 */
	public static function ansi_color(int $code): string
	{
		static $current = [];

		return "\x1b[".self::color($code,$current);
	}

	/**
	 * Convert a binary ANS file to ANSI text
	 *
	 * @param array $ansi
	 * @param bool $return
	 * @return string
	 */
	public static function bin_to_ansi(array $ansi,bool $return=TRUE): string
	{
		$output = '';
		$escape = FALSE;
		$current = [];		// Default Screen

		foreach ($ansi as $line) {
			foreach ($line as $char) {
				if ($char === 0x1b) {
					$escape = TRUE;
					continue;
				}

				if ($escape) {
					if ($x=static::color($char,$current))
						$output .= "\x1b[".$x;

					$escape = FALSE;

				} else {
					$output .= chr($char);
				}
			}

			if ($return)
				$output .= "\r\n";
		}

		return $output;
	}

	/**
	 * Convert an ANSI file into a binary form
	 *
	 * @param string $file
	 * @return Collection
	 */
	public static function binary(string $file): Collection
	{
		$f = fopen($file,'r');
		$escape = FALSE;
		$ansi = FALSE;
		$buffer = '';
		$line = '';
		$result = collect();

		$current = self::reset();

		while (! feof($f)) {
			$c = fread($f,1);

			switch (ord($c)) {
				// Ignore \n (0x0a)
				case 0x0a:
					continue 2;

				// New line \r (0x0d)
				case 0x0d:
					$result->push($line);
					$line = '';
					continue 2;

				// We got our ESC
				case 0x1b:
					$escape = TRUE;
					continue 2;

				case ord('['):
					if ($escape) {
						$ansi = TRUE;
						continue 2;
					}

					break;
			}

			if ($ansi) {
				switch ($c) {
					case ';':
					case 'm':
						if ((int)$buffer === 0) {
							$current = self::reset();

						} elseif ((int)$buffer === 1) {
							$current['h'] = 1;

						} elseif (((int)$buffer >= 30) && (int)$buffer <= 37) {
							$current['f'] = (int)$buffer;

						} elseif (((int)$buffer >= 40) && (int)$buffer <= 47) {
							$current['b'] = (int)$buffer;
						}

						if ($c === 'm') {
							$ansi = FALSE;
							$escape = FALSE;
							$line .= chr(0x1b).chr(self::code($current));
						}

						$buffer = '';
						break;

					default:
						$buffer .= $c;
				}

			} else {
				// If escape is still set, but we didnt get an ANSI starter, then we need to record the ESC.
				if ($escape) {
					$line .= chr(0x1b);
					$escape = FALSE;
				}

				$line .= $c;
			}
		}

		// In case our line didnt end \r and we still have data
		if ($line)
			$result->push($line);

		fclose($f);

		return $result;
	}

	/**
	 * Convert an array of 8 bit color codes to a binary form
	 *
	 * @param array $code
	 * @return int
	 */
	private static function code(array $code): int
	{
		$result = 0;
		foreach ($code as $item) {
			switch ($item) {
				// Color Reset
				case 0:
				// Low Intensity
				case 2: $result = 0; break;

				// High Intensity
				case 1: $result |= self::COLOR_HIGH; break;

				// Foreground
				case 30: $result |= (self::COLOR_BLACK<<1); break;
				case 31: $result |= (self::COLOR_RED<<1); break;
				case 32: $result |= (self::COLOR_GREEN<<1); break;
				case 33: $result |= (self::COLOR_YELLOW<<1); break;
				case 34: $result |= (self::COLOR_BLUE<<1); break;
				case 35: $result |= (self::COLOR_MAGENTA<<1); break;
				case 36: $result |= (self::COLOR_CYAN<<1); break;
				case 37: $result |= (self::COLOR_WHITE<<1); break;

				// Background
				case 40: $result |= (self::COLOR_BLACK<<5); break;
				case 41: $result |= (self::COLOR_RED<<5); break;
				case 42: $result |= (self::COLOR_GREEN<<5); break;
				case 43: $result |= (self::COLOR_YELLOW<<5); break;
				case 44: $result |= (self::COLOR_BLUE<<5); break;
				case 45: $result |= (self::COLOR_MAGENTA<<5); break;
				case 46: $result |= (self::COLOR_CYAN<<5); break;
				case 47: $result |= (self::COLOR_WHITE<<5); break;

				default:
					dd('unhandled code:'.$item);
			}
		}

		return $result;
	}

	public static function color_array(int $code): array
	{
		$h = ($code&0x01);

		switch ($x=(($code>>1)&0x07)) {
			case self::COLOR_BLACK:		$f = '30'; break;
			case self::COLOR_RED:		$f = '31'; break;
			case self::COLOR_GREEN:		$f = '32'; break;
			case self::COLOR_YELLOW:	$f = '33'; break;
			case self::COLOR_BLUE:		$f = '34'; break;
			case self::COLOR_MAGENTA:	$f = '35'; break;
			case self::COLOR_CYAN:		$f = '36'; break;
			case self::COLOR_WHITE:		$f = '37'; break;
			default:
				dump(['unknown color'=>$x]);
		}

		switch ($x=(($code>>5)&0x07)) {
			case self::COLOR_BLACK:		$b = '40'; break;
			case self::COLOR_RED:		$b = '41'; break;
			case self::COLOR_GREEN:		$b = '42'; break;
			case self::COLOR_YELLOW:	$b = '43'; break;
			case self::COLOR_BLUE:		$b = '44'; break;
			case self::COLOR_MAGENTA:	$b = '45'; break;
			case self::COLOR_CYAN:		$b = '46'; break;
			case self::COLOR_WHITE:		$b = '47'; break;
			default:
				dump(['unknown color'=>$x]);
		}

		return ['h'=>$h,'f'=>$f,'b'=>$b];
	}

	/**
	 * Convert binary code to ANSI escape code
	 *
	 * @param int $code
	 * @param array $current
	 * @return string
	 */
	private static function color(int $code,array &$current): string
	{
		if (! $current)
			$current = static::reset();

		$return = '';
		$color = self::color_array($code);
		$highlight_changed = FALSE;

		if ($color['h'] !== $current['h']) {
			$return .= $color['h'] ?: ($code != 0x0e ? 2 : 0);
			$current['h'] = $color['h'];
			$highlight_changed = TRUE;
		}

		if ($color['f'] !== $current['f']) {
			if (! $highlight_changed || $color['h'] || (($color['f'] != self::DEFAULT_FORE) || ($color['b'] != self::DEFAULT_BACK)))
				$return .= (strlen($return) ? ';' : '').$color['f'];

			$x = $color['f'];
			$current['f'] = $color['f'];
		}

		if ($color['b'] !== $current['b']) {
			if (! $highlight_changed || $color['h'] || (($x != self::DEFAULT_FORE) || ($color['b'] != self::DEFAULT_BACK)))
				$return .= (strlen($return) ? ';' : '').$color['b'];

			$current['b'] = $color['b'];
		}

		return ($return !== '') ? $return.'m' : '';
	}

	/**
	 * Calculate the width of a line
	 *
	 * @param string $line
	 * @param bool $buffer
	 * @return int
	 */
	public static function line_width(string $line,bool $buffer=TRUE): int
	{
		return strlen(preg_replace('/\x1b./','',$line))+($buffer ? self::LOGO_OFFSET_WIDTH+self::LOGO_BUFFER_WIDTH : 0);
	}

	/**
	 * The ANSI was reset (normally CSI [ 0m)
	 *
	 * @return int[]
	 */
	private static function reset(): array
	{
		return [
			'h'=>0,
			'f'=>self::DEFAULT_FORE,
			'b'=>self::DEFAULT_BACK
		];
	}

	/**
	 * Show a line with embedded color codes in ANSI
	 *
	 * @param string $text
	 * @return string
	 */
	public static function text_to_ansi(string $text): string
	{
		$ansi = preg_match('/\x1b./',$text);

		return self::bin_to_ansi([array_map(function($item) {return ord($item); },str_split($text))],FALSE).((strlen($text) && $ansi) ? "\x1b[0m" : '');
	}

	/* METHODS */

	/**
	 * Return a specific line
	 *
	 * @param int $line
	 * @return array
	 */
	public function line(int $line): array
	{
		return $this->ansi->get($line);
	}

	/**
	 * Return the binary line
	 *
	 * @param int $line
	 * @return string
	 */
	public function line_raw(int $line): string
	{
		return join('',array_map(function($item) { return chr($item); },$this->line($line)));
	}
}