<?php

namespace App\Classes;

use Illuminate\Support\Arr;
use Illuminate\Support\Collection;

class Font
{
	private const DEBUG = FALSE;

	protected const MSG_WIDTH = 79;

	private string $text = '';
	private int $width = 0;
	private int $height = 0;

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

			case 'width':
				return $this->width;

			default:
				throw new \Exception(sprintf('Unknown key %s',$key));
		}
	}

	/**
	 * Message text, goes after header, and if a logo, to the right of it
	 *
	 * @param string $text
	 */
	public function addText(string $text)
	{
		$this->text = $text;
		$this->dimensions();
	}

	/**
	 * Characters used in the font
	 *
	 * @return Collection
	 */
	private function chars(): Collection
	{
		static $chars = [];

		if ($this->text && empty($chars[$this->text])) {
			// Trim any leading/trailing spaces
			$text = trim(strtolower($this->text));
			$chars[$this->text] = collect();

			// Work out the characters we need
			foreach (array_unique(str_split($text)) as $c) {
				if (! $x=Arr::get(static::FONT,$c))
					continue;

				$chars[$this->text]->put($c,$x);
			}
		}

		return $chars[$this->text] ?: collect();
	}

	/**
	 * Full width of the rendered text
	 *
	 * @return void
	 */
	private function dimensions(): void
	{
		$chars = $this->chars();
		$escape = FALSE;

		foreach (str_split(strtolower($this->text)) as $c) {
			if ($c == "\x1b") {
				$escape = TRUE;
				continue;

			} elseif ($escape) {
				$escape = FALSE;
				continue;
			}

			$this->width += ($x=Arr::get($chars->get($c),0)) ? count($x) : 1;

			if ($x)
				$this->height = (($y=count($chars->get($c))) > $this->height) ? $y : $this->height;
		}

		// If the last character is a space, we'll reduce the width
		$space = TRUE;
		foreach ($chars->get($c) as $x)
			if (array_pop($x) != 32) {
				$space = FALSE;
				break;
			}

		if ($space)
			$this->width--;
	}

	public function render_line(int $line): string
	{
		$chars = $this->chars();
		$result = '';
		$escape = FALSE;
		$ansi = FALSE;

		foreach (str_split(strtolower($this->text)) as $c) {
			if (ord($c) == 0x1b) {
				$escape = TRUE;

			} elseif ($escape && $c) {
				$result .= ANSI::ansi_color(ord($c));
				$escape = FALSE;
				$ansi = TRUE;

			} elseif (($c == ' ') || (! $font_chars=$chars->get($c))) {
				$result .= $c;

			} else {
				foreach (Arr::get($font_chars,$line) as $char)
					$result .= chr($char);
			}
		}

		return $result.($ansi ? ANSI::ansi_color(0x0e) : '');
	}

	/**
	 * This function will format text to static::MSG_WIDTH, as well as adding the logo.
	 * It is up to the text to be spaced appropriately to wrap around the icon.
	 *
	 * @param string $text
	 * @param ANSI|null $logo
	 * @param bool $right
	 * @param int $step
	 * @return string
	 */
	public static function format_msg(string $text,ANSI $logo=NULL,int $step=1,bool $right=FALSE): string
	{
		$result = '';
		$result_height = 0;
		$current_pos = 0;

		while ($current_pos < strlen($text)) {
			$result_line = '';			// Line being created
			$lc = 0;					// Line length count (without ANSI)

			$buffer = $step ? $logo->width->skip(intdiv($result_height,$step)*$step)->take($step)->max() : 1;

			// Add our logo
			if ($result_height <= $logo->height-1) {
				$line = ANSI::bin_to_ansi([$logo->line($result_height)],FALSE);
				$lc = $logo->line_width($logo->line_raw($result_height),FALSE);
				$result_line = str_repeat(' ',ANSI::LOGO_OFFSET_WIDTH)
					.$line
					.str_repeat(' ',ANSI::LOGO_BUFFER_WIDTH+($right ? 0 : $buffer-$lc));
			}

			// Look for a return
			$return_pos = strpos($text,"\r",$current_pos);

			// We have a return
			if ($return_pos !== FALSE) {
				$subtext = substr($text,$current_pos,$return_pos-$current_pos);

			// If the reset of the string will fit on the current line
			} elseif (strlen($text)-$current_pos < static::MSG_WIDTH-$lc) {
				$subtext = substr($text,$current_pos);

			// Get the next lines worth of chars
			} else {
				$subtext = substr($text,$current_pos,static::MSG_WIDTH-$buffer);

				// Include the text up to the last space
				if (substr($text,$current_pos+strlen($subtext),1) !== ' ')
					$subtext = substr($text,$current_pos,strrpos($subtext,' '));
			}

			$result .= $result_line.
				str_repeat(' ',($right ? static::MSG_WIDTH-$lc-strlen($subtext)+((! $lc) ? (ANSI::LOGO_OFFSET_WIDTH+ANSI::LOGO_BUFFER_WIDTH) : 0) : 0))
				.$subtext."\r\n";
			$current_pos += strlen($subtext)+1;
			$result_height++;
		}

		// In case our text is shorter than the logo
		for (;$result_height<$logo->height;$result_height++) {
			$result .= str_repeat(' ',ANSI::LOGO_OFFSET_WIDTH)
				.ANSI::bin_to_ansi([$logo->line($result_height)],FALSE)
				.str_repeat(' ',ANSI::LOGO_BUFFER_WIDTH)
				."\r\n";
		}

		return $result;
	}

	/**
	 * The height of this font (based on the 1st char)
	 *
	 * @return int
	 */
	public static function height(): int
	{
		return count(Arr::get(static::FONT,'a'));
	}

	/**
	 * Convert text into a graphical font
	 *
	 * @param string $text
	 * @param Collection $width
	 * @param int $height
	 * @param int $step
	 * @return string
	 */
	public static function fontText(string $text,Collection $width,int $height,int $step): string
	{
		return self::text_to_font($text,$width,$height,$step);
	}

	/**
	 * Convert text to this font
	 * This function will pad the text to fit around the icon, so that the icon+font fils to self::MSG_WIDTH
	 *
	 * @param string $text
	 * @param Collection $icon_width Width to make the font
	 * @param int $icon_height Minimal width for this height, then full width (self::MSG_WIDTH)
	 * @param int $step The grouping of lines (normally font height) around the icon
	 * @return string
	 */
	protected static function text_to_font(string $text,Collection $icon_width,int $icon_height,int $step): string
	{
		$chars = collect();							// Characters needed for this $text
		$font_height = 0;							// Max height of text using font

		// Trim any leading/trailing spaces
		$text = trim(strtolower($text));

		// Work out the characters we need
		foreach (array_unique(str_split($text)) as $c) {
			if (($c == ' ') || (! $x=Arr::get(static::FONT,$c))) {
				continue;
			}

			$chars->put($c,$x);
			$font_height = (($y=count($x)) > $font_height) ? $y : $font_height;
		}

		if (self::DEBUG) dump(['uniquechars'=>$chars->count(),'font_height'=>$font_height]);
		if (self::DEBUG) dump(['drawing'=>$text,'textlen'=>strlen($text),'logo_width'=>$icon_width,'logo_height'=>$icon_height]);

		$result = '';								// Our result
		$current_pos = 0;							// Our current position through $text
		$result_height = 0;							// Our current line height
		$line_pos = 0;								// Our current character position for this line of the font

		while ($current_pos < strlen($text)) {
			if (self::DEBUG) dump(sprintf('current position %d of %d',$current_pos,strlen($text)));

			for ($line=0;$line<$font_height;$line++) {
				if ($line == 0) {
					$line_icon_width = $icon_width
						->skip(intdiv($result_height,$step)*$step)
						->take($step)
						->max();

					if ($line_icon_width)
						$line_icon_width += ANSI::LOGO_OFFSET_WIDTH+ANSI::LOGO_BUFFER_WIDTH;

					$line_width = self::MSG_WIDTH-$line_icon_width;	// Width we are working towards, initially $icon_width until height then its self::MSG_WIDTH
				}

				$line_result = '';					// Our current line of font
				if (self::DEBUG) dump(sprintf('- current line %d of %d',$line+1,$font_height));

				// If we are mid way through rendering a font, and have already finished with the height offset, we'll need to fill with blanks
				if (($line_width !== self::MSG_WIDTH) && ($result_height > $icon_height-1))
					$line_result .= str_repeat(' ',$line_icon_width);

				$line_pos = $current_pos;
				$next_space_pos = $current_pos;
				$next_next_space_width = 0;			// What our width will be after the next next space
				$next_next_space_pos = 0;			// The position of the next space after the next one
				$next_next_space_chars = 0;			// The number of chars between the next space and the next next space
				$current_line_width = 0;			// Our current width of the line

				while ($current_line_width < $line_width) {
					if (self::DEBUG) dump(sprintf('  - current width %d of %d, and we are working on char %d',$current_line_width,$line_width,$line_pos));
					$find_space_pos = $line_pos;

					// Find our next char
					if (self::DEBUG) dump(sprintf('  - find our next space from %d after %d',$find_space_pos,$next_space_pos));

					$next_space_chars = 0;
					if ($next_space_pos <= $line_pos) {
						if (! $next_next_space_pos) {
							while (($find_space_pos < strlen($text)) && (($c=substr($text,$find_space_pos++,1)) !== ' ')) {
								$x = count(Arr::get($chars->get($c),$line));
								if (self::DEBUG) dump(sprintf('  + char is [%s] (%x) and will take %d chars',$c,ord($c),$x));
								$next_space_chars += $x;
							}

							$next_space_pos = $find_space_pos;
							$next_next_space_pos = $find_space_pos;
							$next_next_space_width = $current_line_width+$next_space_chars;

						} else {
							$next_space_pos = $next_next_space_pos;
							$next_space_chars = $next_next_space_chars;
						}

						// Find our next next space, which we'll use to decide whether we need to include a space when we find one
						$next_next_space_chars = 0;
						while (($next_next_space_pos < strlen($text)) && (($c=substr($text,$next_next_space_pos++,1)) !== ' ')) {
							$next_next_space_chars += count(Arr::get($chars->get($c),$line,[]));
							if (self::DEBUG) dump(sprintf('  + char is [%s] (%x) and will take us to %d',$c,ord($c),$next_next_space_chars));
						}

						$next_next_space_width = $current_line_width+$next_space_chars+$next_next_space_chars;
					};

					if (self::DEBUG) dump(sprintf('  - our next space is: [%s] (%x) at %d in %d chars, taking %d chars (taking our width to %d)',$c,ord($c),$find_space_pos,$find_space_pos-$line_pos,$next_space_chars,$current_line_width+$next_space_chars));

					// We are only spaces, so we can return to the next line
					if ($current_line_width+$next_space_chars > $line_width) {
						if (self::DEBUG) dump(' = next char should go onto new line');

						// Only go to a new line if we already have chars
						if ($current_line_width)
							break;
					}

					$c = substr($text,$line_pos,1);
					if (($c == ' ') || (! $font_chars=$chars->get($c))) {
						// Ignore this space if we are at the beginning of the line
						if ($current_line_width && ($next_next_space_width < $line_width)) {
							$line_result .= $c;
							$current_line_width++;
						}

					} else {
						if (self::DEBUG) dump(sprintf('adding char [%s] which is [%s]',$c,join('|',Arr::get($font_chars,$line))));

						foreach ($x=Arr::get($font_chars,$line) as $char)
							$line_result .= chr($char);

						$current_line_width += count($x);
					}

					$line_pos++;
					if (self::DEBUG) dump(sprintf(' = line width [%d of %d] and we are on char [%d] our space is [%d]',$current_line_width,$line_width,$line_pos,$find_space_pos));

					if ($line_pos == strlen($text)) {
						if (self::DEBUG) dump(sprintf(' = we are finished, as we are on char %d on line %d',$line_pos,$line));
						break;
					}
				}

				$result_height++;
				$result .= $line_result."\r";
			}

			$current_pos = $line_pos;
			if (self::DEBUG) dump(sprintf('= new line starting with char [%d] - our width is [%d] and we are on line [%d]',$current_pos,$line_width,$result_height));
		}

		if (self::DEBUG)
			dd(['result'=>$result]);

		return $result;
	}
}