<?php

namespace App\Classes;

use App\Models\Mode;
use Illuminate\Support\Facades\Log;

use App\User;
use App\Models\CUG;

/**
 * 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
 */
abstract class Frame
{
	protected $frame = NULL;
	protected $output = NULL;

	// All this vars should be overridden in the child class
	/*
	protected $frame_length = 22;
	protected $frame_width = 40;

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

	const FRAMETYPE_INFO = 'i';
	const FRAMETYPE_ACTION = 'a';
	const FRAMETYPE_LOGIN = 'l';
	const FRAMETYPE_TERMINATE = 't';

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

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

	// Fields that are editable
	private $fieldoptions = [
		'p'=>['edit'=>TRUE,'mask'=>'*'],    // Password
		'u'=>['edit'=>TRUE],                // User
		't'=>['edit'=>TRUE],                // Text
	];

	// @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') AND (! $this->isCUG(0) OR $this->type() !== self::FRAMETYPE_LOGIN)) {
			// 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 = collect();  // Fields in this frame.
		$this->fields($startline);
	}

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

	/**
	 * Return a list of alternative versions of this frame.
	 *
	 * @todo: Need to adjust to not include access=0 frames unless owner
	 */
	public function alts(Mode $o)
	{
		return \App\Models\Frame::where('frame',$this->frame())
			->where('index',$this->index())
			->where('id','<>',$this->frame->id)
			->where('mode_id',$o->id)
			->where('access',1)
			->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
	 */
	abstract public function fields($startline=0);

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

	/**
	 * Get the CUG for a frame
	 *
	 * Frame CUG are derived from their frame number.
	 * EG: Frame 642 is a member of 642, or 64, or 6, or 0, whichever matches first.
	 *
	 * @return CUG
	 */
	public function getCUG()
	{
		$co = NULL;
		$frame = $this->frame->frame;

		while (! $co)
		{
			$co = CUG::find($frame);

			if (! $co) {
				$frame = substr($frame,0,strlen($frame)-1);

				if (! $frame)
					$frame = 0;
			}
		}

		return $co;
	}

	/**
	 * 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);
			});
	}

	public function getFieldOptions(int $id)
	{
		return array_get($this->fieldoptions,$this->getField($id)->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);
	}

	public function isAccessible():bool
	{
		return $this->frame->access ? TRUE : FALSE;
	}

	/**
	 * Determine if the frame is a particular CUG
	 * @param int $cug
	 * @return bool
	 */
	public function isCUG(int $cug): bool
	{
		return ($this->getCUG()->id == $cug);
	}

	/**
	 * 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);
	}

	public function isFieldMasked(string $field)
	{
		return array_get(array_get($this->fieldoptions,$field),'mask',FALSE);
	}

	/**
	 * Is this frame Public
	 *
	 * @return bool
	 */
	public function isFramePublic(): bool
	{
		return $this->frame->closed ? FALSE : TRUE;
	}
	// @todo To implement

	public function isOwner(User $o):bool
	{
		return 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.'% '.static::$cost_length.'.0f%s',$cost,static::$cost_unit);
	}

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

		return substr($header.$filler,0,static::$header_length+strlen($header)-$this->strlenv($header));
	}

	/**
	 * 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 WHITE.$num.$frame.(str_repeat(' ',static::$pagenum_length-strlen($num)));
	}

	/**
	 * Get the route for the key press
	 *
	 * @param string $read
	 */
	public function route(string $read)
	{
		if (! preg_match('/^[0-9]$/',$read))
			throw new \Exception('Routes are single digit');

		// If we dont have a route record...
		if (! $this->frame->route)
			return '*';

		$key = 'r'.$read;
		return $this->frame->route->{$key};
	}

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

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

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

		// Header
		$sid = R_RED.'T'.R_BLUE.'E'.R_GREEN.'S'.R_YELLOW.'T';
		$o->content .= substr($sid.'-'.str_repeat('12345678901234567890',4),0,static::$header_length+(strlen($sid)-$so->strlenv($sid))).
			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();
	}
}