<?php

namespace App\Classes;

use Illuminate\Support\Facades\Log;

/**
 * Handles all aspects of frame
 *
 * Frame are constructed:
 * + First line is the header, displaying TITLE/CUG TITLE|PAGE #|COST
 * + Up to $frame_length for content
 * + Input/Status Line
 *
 * NOTES:
 * + Frames are stored in binary. ESC codes are stored as a single char < 32.
 * + Header is on line 1.
 * + Input field is on Line 24.
 * + 'i' Frames are info frames, no looking for fields. (Lines 2-23)
 * + 'a' Frames have active frames with responses.
 * + 't' Frames terminate the session
 *
 * + Frame types:
 * + 'ip' Frames are Information Provider frames - no header added. (Lines 1-23)
 *
 * To Consider
 * + 'x' External frames - living in another viewdata server
 *
 * @package App\Classes
 */
class Frame
{
	private $frame = NULL;
	private $output = NULL;
	private $frame_length = 22;
	private $frame_width = 40;

	private $header_length = 20;    // 20
	private $pagenum_length = 9;    // 11 (prefixed with a color, suffixed with frame)
	private $cost_length = 7;       // 9 (prefixed with a color, suffixed with unit)
	private $cost_unit = 'u';

	public $fields = NULL;         // The fields in this frame.

	// Magic Fields that are pre-filled
	private $fieldmap = [
		'a'=>'address#',
		'd'=>'%date',
	];

	// Fields that are editable
	private $fieldoptions = [
		'a'=>['edit'=>TRUE],    // Address
		'p'=>['edit'=>TRUE],    // Password
		'u'=>['edit'=>TRUE],    // User
	];

	// @todo Move this to the database
	private $header = RED.'T'.BLUE.'E'.GREEN.'S'.YELLOW.'T'.MAGENTA.'!';

	public function __construct(\App\Models\Frame $o,string $msg=NULL)
	{
		$this->frame = $o;

		$this->output = $this->hasFlag('clear') ? CLS : HOME;

		// If we have a message to display on the bottom line.
		if ($msg)
			$this->output .= UP.$msg.HOME;

		$startline = 0;

		if (! $this->hasFlag('ip')) {
			// Set the page header: CUG/Site Name | Page # | Cost
			$this->output .= $this->render_header($this->header).
				$this->render_page($this->frame->frame,$this->frame->index).
				$this->render_cost($this->frame->cost);

			$startline = 1;
		}

		// Calculate fields and render output.
		$this->fields($startline);
	}

	/**
	 * Render the frame
	 *
	 * @return null|string
	 */
	public function __toString()
	{
		return $this->output;
	}

	/**
	 * Return a list of alternative versions of this frame.
	 */
	public function alts()
	{
		return \App\Models\Frame::where('frame',$this->frame())
			->where('index',$this->index())
			->where('id','<>',$this->frame->id)
			->limit(9);
	}

	/**
	 * Frame Created Date
	 */
	public function created()
	{
		return $this->frame->created_at;
	}

	/**
	 * Convert the frame from Binary to Output
	 * Look for fields within the frame.
	 *
	 * @param int $startline
	 */
	public function fields($startline=0,$fieldchar='.')
	{
		$infield = FALSE;           // In a field
		$fieldtype = NULL;          // Type of field
		$fieldlength = 0;           // Length of field
		$this->fields = collect();  // Fields in this frame.

		if ($startline)
			$this->output .= str_repeat(DOWN,$startline);

		// $fieldadrline = 1;

		// Scan the frame for a field start
		for ($y=$startline;$y<=$this->frame_length;$y++)
		{
			// Fields can only be on a single line
			$fieldx = $fieldy = FALSE;

			for ($x=0;$x<$this->frame_width;$x++)
			{
				$posn = $y*40+$x;

				// If the frame is not big enough, fill it with spaces.
				$byte = ord(isset($this->frame->content{$posn}) ? $this->frame->content{$posn} : ' ')%128;

				// Check for start-of-field
				if ($byte == ord(ESC)) {     // Esc designates start of field (Esc-K is end of edit)
					$infield = TRUE;
					$fieldlength = 1;
					$fieldtype = ord(substr($this->frame->content,$posn+1,1))%128;
					$this->output .= $fieldchar;

				} else {
					if ($infield) {
						if ($byte == $fieldtype) {
							$fieldlength++;
							$byte = ord($fieldchar);	    // Replace field with $fieldchar.

							if ($fieldx === FALSE) {
								$fieldx = $x;
								$fieldy = $y;
							}

							// Is this a magic field?
							// @todo For page redisplay *00, we should show entered contents - for refresh *09 we should show updated contents
							if (array_get($this->fieldmap,chr($fieldtype)) ) {
								$field = $this->fieldmap[chr($fieldtype)];
								//dump(['infield','byte'=>$byte,'fieldtype'=>$fieldtype,'field'=>$field,'strpos'=>strpos($field,'#')]);

								/*
								// address field has many lines. increment when hit on first character.
								if ($fieldlength == 1 && strpos($field,'#') !== false) {
									$field = str_replace('#',$fieldadrline,$field);
									dump(['field'=>$field,'fieldadrline'=>$fieldadrline,'fieldadrline'=>$fieldadrline]);
									$fieldadrline++;
								}
								*/

								// Replace field with Date
								if ($field == '%date') {
									// Drop the last dot and replace it.
									if ($fieldlength == 2) {
										$datetime = date('D d M H:ia');
										$this->output = rtrim($this->output,$fieldchar);
										$this->output .= $datetime{0};
									}

									if ($fieldlength > 1 AND $fieldlength <= strlen($datetime))
										$byte = ord($datetime{$fieldlength-1});
								}

								// @todo user data
								/* else if (isset($user[$field])) {
									if ($fieldlength <= strlen($user[$field])) {
										$byte = ord($user[$field]{$fieldlength-1});
									}
								} /*else 	// pre-load field contents. PAM or *00 ?
								if (isset($fields[what]['value'])) {

								*/
							}

						} else {
							$this->fields->push(new FrameFields([
								'type'=>chr($fieldtype),
								'length'=>$fieldlength,
								'x'=>$fieldx-1,             // Adjust for the ESC char
								'y'=>$fieldy,
							]));

							Log::debug(sprintf('Frame: %s, Field found at [%s,%s], Type: %s, Length: %s',$this->page(),$fieldx-1,$fieldy,$fieldtype,$fieldlength));

							$infield = FALSE;
							$fieldx = $fieldy = FALSE;
						}
					}
				}

				// truncate end of lines @todo havent validated this code or used it?
				if (isset($pageflags['tru']) && substr($this->frame->content,$posn,40-$x) === str_repeat(' ',40-$x)) {
					$this->output .= CR . LF;
					break;
				}

				if (! $infield OR $fieldlength > 1)
					$this->output .= ($byte < 32) ? ESC.chr($byte+64) : chr($byte);
			}
		}
	}

	/**
	 * Returns the current frame.
	 */
	public function frame()
	{
		return $this->frame->frame;
	}

	/**
	 * Return the current field configuration
	 */
	public function getField(int $id)
	{
		return $this->fields->get($id);
	}

	/**
	 * Get a specific key of the field options that passes a filter test
	 *
	 * @param string $type
	 * @param int $after
	 * @return mixed
	 */
	public function getFieldId($type='edit',$after=0)
	{
		return $this->fields
			->search(function($item,$key) use ($type,$after) {
				return $key >= $after AND $this->isFieldEditable($item->type);
			});
	}

	/**
	 * Return the flag for this page
	 *
	 * CLEAR: Clear Screen before rendering.
	 *
	 * @param $flag
	 * @return bool
	 */
	public function hasFlag($flag)
	{
		return $this->frame->hasFlag($flag);
	}

	/**
	 * Return the frame DB id
	 *
	 * @return mixed
	 */
	public function id()
	{
		return $this->frame->id;
	}

	/**
	 * Current frame index
	 *
	 * @return mixed
	 */
	public function index()
	{
		return $this->frame->index;
	}

	/**
	 * Return the next index
	 */
	public function index_next()
	{
		return chr(ord($this->frame->index)+1);
	}

	/**
	 * Determine if a field is editable
	 *
	 * @param string $field
	 * @return mixed
	 */
	public function isFieldEditable(string $field)
	{
		return array_get(array_get($this->fieldoptions,$field),'edit',FALSE);
	}

	/**
	 * Return the Page Number
	 */
	public function page(bool $as_array=FALSE)
	{
		return $as_array ? ['frame'=>$this->frame->frame,'index'=>$this->frame->index] : $this->frame->page;
	}

	/**
	 * Return the next page number.
	 *
	 * @param bool $as_array
	 * @return mixed
	 */
	public function pagenext(bool $as_array=FALSE)
	{
		return $as_array ? ['frame'=>$this->frame->frame,'index'=>$this->index_next()] : $this->frame->frame.$this->index_next();
	}

	/**
	 * Render the cost of the frame
	 *
	 * @param int $cost
	 * @return string
	 * @throws \Exception
	 */
	private function render_cost(int $cost)
	{
		if ($cost > 999)
			throw new \Exception('Price too high');

		if ($cost > 100)
			$color = RED;
		elseif ($cost > 0)
			$color = YELLOW;
		else
			$color = GREEN;

		return sprintf($color.'% '.$this->cost_length.'.0f%s',$cost,$this->cost_unit);
	}

	/**
	 * Render the Site Header
	 *
	 * @param string $header
	 * @return bool|string
	 */
	private function render_header(string $header)
	{
		$filler = ($this->strlenv($header) < $this->header_length) ? str_repeat(' ',$this->header_length-$this->strlenv($header)) : '';

		return substr($header.$filler,0,$this->header_length+substr_count($this->header,ESC));
	}

	/**
	 * Render the Frame Number
	 *
	 * @param int $num
	 * @param string $frame
	 * @return string
	 * @throws \Exception
	 */
	private function render_page(int $num,string $frame)
	{
		if ($num > 999999999)
			throw new \Exception('Page Number too big',500);

		if (strlen($frame) !== 1)
			throw new \Exception('Frame invalid',500);

		return sprintf(WHITE.'% '.$this->pagenum_length.'.0f%s',$num,$frame);
	}

	/**
	 * Get the route for the key press
	 *
	 * @param string $read
	 */
	public function route(string $read)
	{
		// @todo
		return FALSE;
	}

	/**
	 * Calculate the length of text
	 *
	 * ESC characters are two chars, and need to be counted as one.
	 *
	 * @param $text
	 * @return int
	 */
	function strlenv($text):int {
		return strlen($text)-substr_count($text,ESC);
	}

	public static function testFrame()
	{
		// Simulate a DB load
		$o = new \App\Models\Frame;

		$o->content = '';
		$o->flags = ['ip'];
		$o->type = 'a';
		$o->frame = 999;
		$o->index = 'a';

		// Header
		$o->content .= substr(R_RED.'T'.R_BLUE.'E'.R_GREEN.'S'.R_YELLOW.'T-12345678901234567890',0,20).
			R_WHITE.'999999999a'.R_RED.sprintf('%07.0f',999).'u';

		$o->content .= str_repeat('+-',18).' '.R_RED.'01';
		$o->content .= 'Name: '.ESC.str_repeat('u',5).str_repeat('+-',14);
		$o->content .= 'Date: '.ESC.str_repeat('d',25).str_repeat('+-',4);
		$o->content .= 'Address: '.ESC.str_repeat('a',19).' '.str_repeat('+-',5);
		$o->content .= '       : '.ESC.str_repeat('a',19).' '.str_repeat('+-',5);

		return $o;
	}

	/**
	 * Return the Frame Type
	 */
	public function type()
	{
		return $this->frame->type();
	}
}