<?php

namespace App\Classes;

use Illuminate\Support\Arr;

/**
 * Page layout is as follows
 *
 * |----------|--------------|
 * | LOGO     | HEADER       |
 * | LEFT BOX | TEXT         |
 * |----------|--------------|
 */
class Page
{
	// If false, no debug, if null, show padding, if true, show debug
	private ?bool $DEBUG = NULL;

	/** @var int Max width of message */
	public const MSG_WIDTH = 78;
	/** @var int Chars to add between logo and header/text */
	private const LOGO_OFFSET_WIDTH = 1;

	/** @var bool Do we add an "\n" when rendering */
	private bool $crlf;
	/** @var Font Our header text */
	private Font $header;
	/** @var string Text to go below the header */
	private string $header_foot = '';
	/** @var bool If the header will be right aligned */
	private bool $header_right;
	/** @var int Character to show after the header + header_footer */
	private int $header_underline = 0;

	/** @var Font Left box content, that goes below the logo */
	private Font $left_box;
	/** @var bool If there is no logo, should the left box start below the header */
	private bool $left_box_below_header = FALSE;

	/** @var ANSI ANSI logo to display */
	private ANSI $logo;
	/** @var int Buffer of chars between logo/left box and text, 0 means retain max width */
	private int $step;

	/** @var int The current cursor position when rendering */
	private int $x;
	/** @var int The current line when rendering */
	private int $y;

	private const MAX_LEFTBOX_WIDTH = 28;

	private string $text = '';
	private bool $text_right = FALSE;

	public function __construct(bool $crlf=FALSE,?bool $debug=FALSE)
	{
		$this->header = new Font;
		$this->logo = new ANSI;
		$this->left_box = new Font;

		$this->crlf = $crlf;
		$this->DEBUG = $debug;
	}

	public function __get($key)
	{
		switch ($key) {
			// Total height of the header, including footer+underline
			case 'drawing_header':
				return $this->y < $this->header_height;

			case 'drawing_headcontent':
				return $this->y < $this->header->height;

			case 'drawing_headfoot':
				//return ($this->y < $this->header_height) && $this->header_foot;
				return ($this->y === $this->header->height) && $this->header_foot;

			case 'drawing_headunderline':
				return ($this->y === $this->header->height+($this->header_foot ? 1:0)) && $this->header_underline;

			case 'drawing_leftbox':
				return ($this->y >= $this->left_box_start) && ($this->y < $this->left_box_start+$this->left_box->height) && (! $this->drawing_logo);

			case 'drawing_logo':
				return $this->y < $this->logo->height;

			// Height of the header
			case 'header_height':
				return max($this->left_box_below_header ? 0 : $this->left_box->height+self::LOGO_OFFSET_WIDTH,$this->header->height+($this->header_foot ? 1 : 0)+($this->header_underline ? 1 : 0));

			// Our header width
			case 'header_width':
				return max($this->header->width,strlen($this->header_foot));

			// The width of the left column
			case 'left_width':
				return max($this->logo->width->max(),$this->left_box->width) + self::LOGO_OFFSET_WIDTH*2;

			// The height of the left column
			case 'left_height':
				if ($this->left_box->height && $this->left_box_below_header) {
					// If the logo is smaller than the header, then its the height header+left box
					// Otherwise its the height of the logo+spacer+left box height+space
					if ($this->logo->height < $this->header_height) {
						return $this->header_height+$this->left_box->height + self::LOGO_OFFSET_WIDTH;
					} else {
						return $this->logo->height + self::LOGO_OFFSET_WIDTH + $this->left_box->height + self::LOGO_OFFSET_WIDTH;
					}

				} else
					return ($this->logo->height ? $this->logo->height+self::LOGO_OFFSET_WIDTH : 0) + ($this->left_box->height ? $this->left_box->height+self::LOGO_OFFSET_WIDTH : 0);

			case 'left_box_start':
				if ($this->left_box_below_header) {
					return max($this->logo->height+self::LOGO_OFFSET_WIDTH,$this->header_height);

				} else {
					return max(($this->logo->height ? $this->logo->height+self::LOGO_OFFSET_WIDTH : 0),0);
				}

			// The right width
			case 'right_width':
				return self::MSG_WIDTH-$this->left_width;

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

	/**
	 * Message header - goes at top, right of logo
	 *
	 * @param Font $text
	 * @param string $foot
	 * @param bool $right
	 * @param int $underline
	 * @throws \Exception
	 */
	public function addHeader(Font $text,string $foot='',bool $right=FALSE,int $underline=0): void
	{
		if (($text->width > $this->right_width) || (strlen($foot) > $this->right_width))
			throw new \Exception(sprintf('Header or Header Footer greater than available width'));

		$this->header = $text;
		$this->header_foot = $foot;
		$this->header_right = $right;
		$this->header_underline = $underline;
	}

	/**
	 * Content that can go below logo, to the left of the text, if text $logo_left_border is TRUE
	 *
	 * @param Font $text
	 * @param bool $below_header
	 * @throws \Exception
	 */
	public function addLeftBoxContent(Font $text,bool $below_header=TRUE): void
	{
		if ($text->width > self::MAX_LEFTBOX_WIDTH)
			throw new \Exception(sprintf('Leftbox content greater than %d',self::MAX_LEFTBOX_WIDTH));

		$this->left_box = $text;
		$this->left_box_below_header = $below_header;
	}

	/**
	 * Message logo - goes at top left
	 *
	 * @param ANSI $ansi
	 */
	public function addLogo(ANSI $ansi): void
	{
		$this->logo = $ansi;
	}

	/**
	 * Message text, goes after header, and if a logo, to the right of it
	 *
	 * @param string $text Main Body Text
	 * @param bool $right Right Aligned
	 */
	public function addText(string $text,bool $right=FALSE)
	{
		$this->text .= $text;

		$this->text_right = $right;
	}

	/**
	 * Render the page.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function render(): string
	{
		$this->x = 0;
		$this->y = 0;
		$result = '';							// The output

		$current_pos = 0;						// Current position of the text being rendered
		$text_length = strlen($this->text);		// Length of text we need to place in the message
		$this->step = 0;						// If step is 0, our left_width is fixed until the end of the left_height,
												// otherwise, its a buffer of chars between the left item and the text
		$text_current_color = NULL;				// Current text color

		while (TRUE) {
			$result_line = '';					// Line being created

			/*
			// The buffer represents how many spaces need to pad between the left_width and whatever is drawn on the left
			if ($this->step) {
				// If we have a logo + left text box, then the min width is the max width of the left text box, until after we pass the max width

				// If we only have a logo and a header, we step after the header

				// If we only have a text box and a header above, we step after the header

				// If we only have a text box, and header below, we step after passing max width

				$buffer = $this->step;

			} else {
				$buffer = 0;
			}
			*/
			$buffer = 0;

			if ($this->DEBUG)
			dump([
				'line'=>$this->y,
				'drawing_logo'=>$this->drawing_logo,
				'drawing_header'=>$this->drawing_header,
				'drawing_headcontent'=>$this->drawing_headcontent,
				'drawing_headfoot'=>$this->drawing_headfoot,
				'drawing_headunderline'=>$this->drawing_headunderline,
				'header_height'=>$this->header_height,
				'drawing_leftbox'=>$this->drawing_leftbox,
				'left_height'=>$this->left_height,
				'left_box_height'=>$this->left_box->height,
				'logo_height'=>$this->logo->height,
				'left_width'=>$this->left_width,
				'left_box_start'=>$this->left_box_start,
				]);

			$size = 0;
			// We are drawing our logo and/or header
			if ($this->drawing_logo) {
				$line = ANSI::bin_to_ansi([$this->logo->line($this->y)],FALSE);
				$size = $this->logo->line_width($this->logo->line_raw($this->y),FALSE);

				$result_line = str_repeat((is_null($this->DEBUG) ? 'l' : ' '),self::LOGO_OFFSET_WIDTH)
					.$line
					.str_repeat((is_null($this->DEBUG) ? 'L' : ' '),self::LOGO_OFFSET_WIDTH);

				$this->x += $size+self::LOGO_OFFSET_WIDTH*2;

			} elseif ($this->drawing_leftbox) {
				$line = $this->left_box->render_line($this->y-$this->left_box_start);
				$size = $this->left_box->width;

				$result_line = str_repeat((is_null($this->DEBUG) ? 'b' : ' '),self::LOGO_OFFSET_WIDTH)
					.$line
					.str_repeat((is_null($this->DEBUG) ? 'B' : ' '),self::LOGO_OFFSET_WIDTH);

				$this->x += $size+self::LOGO_OFFSET_WIDTH*2;

			// For when there is a line between the logo and left box
			} elseif (($this->y < $this->left_height) && ((! $this->left_box_below_header) || $this->logo->height)) {
				$size = $this->left_box->width;

				$result_line .= str_repeat((is_null($this->DEBUG) ? 'X' : ' '),self::LOGO_OFFSET_WIDTH*2+$this->left_box->width);

				$this->x += $this->left_box->width+self::LOGO_OFFSET_WIDTH*2;
			}

			if (($this->y < $this->left_height) && ((! $this->left_box_below_header) || $this->logo->height))
				$buffer = $this->left_width-$size-self::LOGO_OFFSET_WIDTH*2;

			// Our step buffer
			$result_line .= str_repeat((is_null($this->DEBUG) ? '+' : ' '),$buffer);
			$this->x += $buffer;

			if ($this->drawing_header) {
				if ($this->drawing_headcontent) {
					// If the header is on the right, we may need to pad it
					if ($this->header_right) {
						// Pad the header with any right justification
						$result_line .= str_repeat((is_null($this->DEBUG) ? 'H' : ' '),self::MSG_WIDTH-$this->x-$this->header->width)
							.$this->header->render_line($this->y);

					} else {
						$result_line .= $this->header->render_line($this->y);
					}

				} elseif ($this->drawing_headfoot) {
					if ($x=ANSI::line_width($this->header_foot,FALSE)) {
						$result_line .=
							str_repeat((is_null($this->DEBUG) ? 'f' : ' '),$this->header_right ? (self::MSG_WIDTH-$this->x-$x) : ($this->left_box_below_header ? 0 : $this->left_width+$buffer-$this->x))
							.ANSI::text_to_ansi($this->header_foot);
					}

				} elseif ($this->drawing_headunderline) {
					// Add our header underline
					$result_line .= str_repeat(chr($this->header_underline),self::MSG_WIDTH-$this->x);
				}

			// We are drawing the text
			} else {
				$subtext = '';
				$subtext_ansi = '';
				$subtext_length = 0;

				// If we have some text to render
				if ($current_pos < $text_length) {
					// Look for a return
					$return_pos = strpos($this->text,"\r",$current_pos);

					// We have a return
					if ($return_pos !== FALSE) {
						// If the remaining text is within our width, we'll use it all.
						if ($return_pos-$current_pos < self::MSG_WIDTH-$this->x-$buffer) {
							$subtext = substr($this->text,$current_pos,$return_pos-$current_pos);

						// Look for the space.
						} else {
							$space_pos = strrpos(substr($this->text,$current_pos,static::MSG_WIDTH-$this->x-$buffer),' ');
							$subtext = substr($this->text,$current_pos,$space_pos);
						}

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

					// Get the next lines worth of chars, breaking on a space
					} else {
						$subtext = $this->text_substr(substr($this->text,$current_pos),static::MSG_WIDTH-$buffer);

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

					$current_pos += strlen($subtext)+1;

					if ($subtext) {
						$subtext_length = ANSI::line_width($subtext,FALSE);
						$subtext_ansi = ANSI::text_to_ansi(($text_current_color ? "\x1b".$text_current_color : '').$subtext);

						// Get our last color used, for the next line.
						$m = [];
						preg_match('/^.*(\x1b(.))+(.*?)$/s',$subtext,$m);
						if (Arr::get($m,2))
							$text_current_color = $m[2];
					}
				}

				if ($result_line || $subtext) {
					$result_line .=
						($subtext_ansi
							? str_repeat(($this->DEBUG ? 'F' : ' '),
							($this->text_right && ($this->y > $this->header_height-1) ? static::MSG_WIDTH-$subtext_length-$buffer : 0)).$subtext_ansi
							: '');

				}
			}

			$result_line .= $this->crlf ? "\n\r" : "\r";

			$this->y++;
			$this->x = 0;

			//$result_line .= ".........+.........+.........+.........+.........+.........+.........+.........+\r\n";
			$result .= $result_line;

			if ($this->DEBUG)
				echo $result_line;

			// If we are processed our logo, left box and text, we are done
			if (($this->y >= $this->left_height) && ($current_pos >= $text_length))
				break;
		}

		return $result;
	}

	private function text_substr(string $text,int $goal): string
	{
		$chars = $goal;

		while (($x=ANSI::line_width($subtext=substr($text,0,$chars),FALSE)) < $goal) {
			$chars += ($chars-$x);
		}

		// If the last char is an escape, we need to more chars until the last char is no longer an escape.
		while (preg_match('/\x1b$/',$subtext) && (strlen($subtext) < strlen($text))) {
			$subtext .= substr($text,strlen($subtext),2);
		}

		return $subtext;
	}
}