<?php

namespace ProcessWire;

/**
 * InputfieldPWCommerceNotes for ProcessWire.
 *
 * This Inputfield is a GUI for FieldtypePWCommerceNotes.
 *
 * @author Francis Otieno (Kongondo) <kongondo@gmail.com> kongondo.com
 *
 *
 *
 * InputfieldPWCommerceNotes for PWCommerce
 * Copyright (C) 2025 by Francis Otieno
 * MIT License
 *
 */

class InputfieldPWCommerceNotes extends Inputfield
{

	public static function getModuleInfo() {
		return array(
			'title' => 'PWCommerce Notes: Inputfield',
			'author' => 'Francis Otieno (Kongondo)',
			'version' => "100",
			'href' => 'https://kongondo.com',
			'summary' => 'GUI for the field that stores PWCommerce notes.',
			'requires' => 'FieldtypePWCommerceNotes',
		);
	}

	protected $page;
	protected $field;

	private $datetimeFormat;
	private $isNoNotes; // TODO: EXPERIMENTAL!
	private $notes;

	public function init() {
		parent::init();
		// if we want this modules css and js classes to be autoloaded
		// Any modules that extend: Inputfield, Process or ModuleJS will auto-load their CSS/JS files if they have the same name as the module and appear in the same directory. However, in order for that to work, their init() method has to be called. So if your module extends one of those, and has an init() method, then make sure to call the parent init() method:


		// get helper for rendering inputfields

		// GET GENERAL SETTINGS: FOR DATE AND TIME FORMAT


		$this->datetimeFormat = $this->pwcommerce->getDateTimeFormat();
	}

	public function setPage(Page $page) {

		$this->page = $page;
	}

	public function setField(Field $field) {

		$this->field = $field;
	}

	// for classes calling this class externally
	// e.g. PWCommerceProcessRenderOrders::getSingleViewOrderNotes
	public function setNotes(WireArray $notes) {

		$this->notes = $notes;
	}

	// TODO: WE NEED A RENDERVALUE() FOR VIEW NOTES, E.G. IN ORDER VIEW AS OPPOSED TO EDIT

	/**
	 * Render the entire input area for notes
	 *
	 */
	public function ___render() {

		// IF ADD NEW BLANK NOTE ITEM/RECORD - AJAX
		$input = $this->wire('input');
		$ajax = $this->wire('config')->ajax;

		if ($ajax && $input->get('field') == $this->attr('name')) {
			$newItem = $this->getNewBlankNote();
			// send back just the new row
			$out = $this->buildRow($newItem)->render();
			echo $out;
			die();
		}

		// NORMAL NON-AJAX/SAVED OUTPUT FOR SAVED VALUES
		/** @var WireArray $value */
		// $value = $this->attr('value');
		$this->notes = $this->attr('value');
		$out =
			"<div id='pwcommerce_notes_main_wrapper'>" .
			//-------------
			$this->renderNotesColumnHeaders() .
			// -----------
			"<div id='pwcommerce_notes_wrapper'>" .
			// $this->buildForm($value) .
			$this->buildForm() .
			"</div>" .
			//----------
			// end: div#pwcommerce_notes_main_wrapper
			"</div>" .
			"<div id='pwcommerce_note_add_new_wrapper'>" . $this->renderFooter() . "</div>";

		// ---------
		return $out;
	}

	/**
	 * Called before render() or renderValue() method by InputfieldWrapper, before Inputfield-specific CSS/JS files added
	 *
	 * @param Inputfield|InputfieldWrapper|null The parent Inputfield/wrapper that is rendering it or null if no parent.
	 * @param bool $renderValueMode Whether renderValueMode will be used.
	 * @return bool
	 *
	 */
	public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
		return parent::renderReady($parent, $renderValueMode);
	}

	private function renderNotesColumnHeaders() {
		// BUILD ROW HEADERS
		$out = "";
		// show notes 'headers' if we have notes TODO: @update: we need this for new notes!
		//if (!empty($value->count())) {

		$out .= "<div class='grid grid-cols-12 gap-4'>" .
			//-------------------------
			// COLUMN HEADER: note date
			"<div class='col-span-full md:col-span-3'>" .
			"<h4 class='hidden md:block'>" . $this->_('Date') . "</h4>" .
			"</div>" .
			//-------------------------
			// COLUMN HEADER: note text
			"<div class='col-span-full md:col-span-6'>" .
			"<h4 class='hidden md:block'>" . $this->_('Note') . "</h4>" .
			"</div>" .
			//-------------------------
			// COLUMN HEADER: note author
			"<div class='col-span-full md:col-span-3'>" .
			"<h4 class='hidden md:block'>" . $this->_('Author') . "</h4>" .
			"</div>" .
			//-------------------------
			"</div>";
		return $out;
	}

	private function renderAddNewLink() {
		// @note: in case being used in InputfieldPWCommerceRuntimeMarkup
		// we strip the '_repeater' off
		$name = $this->attr('name');
		$name = preg_replace('/_repeater\d+$/', '', $name);
		$adminEditURL = $this->wire('config')->urls->admin . "page/edit/";
		$adminEdit = "{$adminEditURL}?id={$this->page->id}&field={$name}";
		$out =
			"<div>" .
			// @note: the class 'pwcommerce_run_request_indicators_operations' tells InputfieldRuntimeMarkup.js to show/hide spinner before send and after swap.
			// @note the htmx swap value: afterbegin prepends the content before the first child inside the target
			// @note: the hx-swap modifier 'show:window:top' will scrop to the top of the window and show the added 'note'
			"<a id='pwcommerce_note_add_new' class='pwcommerce_run_request_indicators_operations' href='#' hx-get='{$adminEdit}' hx-target='#pwcommerce_notes_wrapper' hx-swap='afterbegin show:window:top' hx-indicator='#pwcommerce_add_new_note_spinner_indicator'>" .
			"<i id='pwcommerce_add_new_note_spinner_indicator' class='pwcommerce_add_new_note_spinner_indicator pwcommerce_spinner_indicator fa fa-fw fa-plus-circle'></i>" .
			$this->_("Add new note") .
			"</a>" .
			"</div>";
		return $out;
	}

	private function renderFooter() {
		//------------------- add new note (InputfieldMarkup)
		// @note: SINGLE ADD NEW IN FOOTER OF WRAPPER - CAN ONLY HAVE ONE!
		$options = [
			'skipLabel' => Inputfield::skipLabelHeader,
			'collapsed' => Inputfield::collapsedNever,
			'wrapClass' => true,
			'classes' => 'pwcommerce_note_add_new',
			'wrapper_classes' => 'pwcommerce_no_outline',
			'value' => $this->renderAddNewLink(),
		];
		$field = $this->pwcommerce->getInputfieldMarkup($options);
		$wrapper = $this->pwcommerce->getInputfieldWrapper();
		$wrapper->add($field);
		return $wrapper->render();
	}

	// TODO: OK PASS VARIABLE LIKE THIS? THIS IS INHERITED FROM INPUTFIELD!
	// TODO: THIS IS THROWING ERROR IN PHP 8 ENVIRONMENTS DUE TO THE OVERLOADING OF renderValue() with $notes!
	// public function ___renderValue($notes) {
	public function ___renderValue() {
		// return $this->buildForm($notes, true);
		return $this->buildForm(true);
	}

	// ~~~~~~~~~~~~~~~

	// private function buildForm($notes, $isRenderValueOnly = false) {
	private function buildForm($isRenderValueOnly = false) {
		// TODO: SHOULD WE SHOW SUBSCRIPT ON NOTE, AS ADMIN, SYSTEM, CUSTOMER ETC? maybe under author?

		$notes = $this->notes;



		// GET MAIN WRAPPER
		$wrapper = $this->pwcommerce->getInputfieldWrapper();

		$out = "";
		// no notes found notice
		if (empty($notes->count())) {
			// TODO: HIDE IN JS IF ADD NEW! after htmx settle
			$out .= $this->getNoNotesFoundNoticeMarkup();
		}

		// options for InputfieldMarkup
		$options = [
			'skipLabel' => Inputfield::skipLabelHeader,
			'collapsed' => Inputfield::collapsedNever,
			'wrapClass' => true,
			// TODO delete if not in use
			'wrapper_classes' => 'pwcommerce_no_outline',
			'value' => $out,
		];
		$field = $this->pwcommerce->getInputfieldMarkup($options);
		$wrapper->add($field);


		// BUILD NOTE ROWS

		foreach ($notes as $note) {
			/** @var InputfieldMarkup $row */
			$row = $this->buildRow($note, $isRenderValueOnly);
			$wrapper->add($row);
		}
		// FINAL WRAPPER MARKUP
		return $wrapper->render();
	}

	/**
	 * Build a row of inputs representing a single note.
	 *
	 * @access private
	 * @return InputfieldMarkup
	 */
	private function buildRow(WireData $note, $isRenderValueOnly = false) {

		// GET WRAPPER
		// $wrapper = $this->pwcommerce->getInputfieldWrapper();

		$out = "";

		// ######### ONLY IF IN EDIT MODE #########
		if (!$isRenderValueOnly) {
			//------------------- note id (getInputfieldHidden)
			$options = [
				'id' => "pwcommerce_note_id{$note->id}",
				'name' => 'pwcommerce_note_id[]',
				'value' => $note->id, // @note: runtime ID!
			];

			$field = $this->pwcommerce->getInputfieldHidden($options);
			$out .= $field->render();

			//------------------- note type (getInputfieldHidden)
			$options = [
				'id' => "pwcommerce_note_type{$note->id}",
				'name' => 'pwcommerce_note_type[]',
				'value' => $note->type,
			];

			$field = $this->pwcommerce->getInputfieldHidden($options);
			$out .= $field->render();

			//------------------- note is_new (getInputfieldHidden)
			$options = [
				'id' => "pwcommerce_note_is_new{$note->id}",
				'name' => 'pwcommerce_note_is_new[]',
				'value' => (int) $note->isNew,
			];

			$field = $this->pwcommerce->getInputfieldHidden($options);
			$out .= $field->render();
		}

		// ============

		//------------------- note text/content/value (getInputfieldTextarea/getInputfieldMarkup)
		$field = $this->getNoteTextareaField($note, $isRenderValueOnly);
		$noteText = $field->render();
		//-------
		$noteDate = $this->getNoteCreatedAndModifiedDatesString($note);
		$noteAuthor = $this->getNoteAuthor($note->createdUsersID);

		## -------------

		$out .= "<div class='grid grid-cols-12 gap-4 mb-10'>" .
			//-------------------------
			//  note date
			$this->getNoteDateMarkup($noteDate) .
			//-------------------------
			//  note text
			$this->getNoteTextMarkup($noteText) .
			//-------------------------
			//  note author
			$this->getNoteTextAuthor($noteAuthor) .
			//-------------------------
			"</div>" .
			"<hr>";

		// ---------------------
		/** @var InputfieldMarkup $field */
		$field = $this->buildNoteFinalMarkup($out);
		return $field;
		// TODO: DELETE BELOW WHEN DONE: HAVING ISSUES WITH MARKUP IN renderValue()
		//-------------------
		// $wrapper = $this->pwcommerce->getInputfieldWrapper();
		// $wrapper->add($field);

		// //--------------
		// return $wrapper;
	}

	private function getNoNotesFoundNoticeMarkup() {
		$out = "<p id='pwcommerce_notes_no_notes_found'>" . $this->_('Order has no saved notes.') . "</p>";
		return $out;
	}

	private function getNoteTextareaField($note, $isRenderValueOnly = false) {
		//------------------- note text/content/value (getInputfieldTextarea/getInputfieldMarkup)
		if ($note->type !== 'system' && empty($isRenderValueOnly)) {
			// non-system note is editable || if not in renderValue() mode
			$options = [
				'id' => "pwcommerce_note_text{$note->id}",
				'name' => "pwcommerce_note_text[]",
				'label' => $this->_('Note'),
				'required' => true,
				'collapsed' => Inputfield::collapsedNever,
				'rows' => 2,
				'classes' => 'pwcommerce_note_text',
				'wrapClass' => true,
				// TODO delete if not in use
				'wrapper_classes' => 'pwcommerce_no_outline',
				'value' => $note->text,
			];
			$field = $this->pwcommerce->getInputfieldTextarea($options);
		} else {
			// system note is not editable
			$sytemNoteText = "<p>" . $note->text . "</p>";
			$options = [
				'skipLabel' => Inputfield::skipLabelHeader,
				'collapsed' => Inputfield::collapsedNever,
				'wrapClass' => true,
				// TODO delete if not in use
				'wrapper_classes' => 'pwcommerce_no_outline',
				'value' => $sytemNoteText,
			];

			$field = $this->pwcommerce->getInputfieldMarkup($options);
		}
		return $field;
	}

	private function getNoteDateMarkup($noteDate) {
		//  note date
		$out = "<div class='col-span-full md:col-span-3'>" .
			"<span class='md:hidden mr-1 italic'>" . $this->_('Date') . ":</span><span>" . $noteDate .
			"</span>" .
			"</div>";
		return $out;
	}

	private function getNoteTextMarkup($noteText) {
		//  note text
		$out = "<div class='col-span-full md:col-span-6 md:mr-3'>" .
			"<span class='md:hidden mr-1 italic'>" . $this->_('Note') . "</span><span>" . $noteText . "</span>" .
			"</div>";
		return $out;
	}

	private function getNoteTextAuthor($noteAuthor) {
		//  note author
		$out = "<div class='col-span-full md:col-span-3'>" .
			"<span class='md:hidden mr-1 italic'>" . $this->_('Author') . ":</span><span>" . $noteAuthor . "</span>" .
			"</div>";
		return $out;
	}

	private function buildNoteFinalMarkup($noteMarkup) {

		$options = [
			'skipLabel' => Inputfield::skipLabelHeader,
			'collapsed' => Inputfield::collapsedNever,
			'wrapClass' => true,
			'classes' => 'pwcommerce_note_wrapper',
			'wrapper_classes' => 'pwcommerce_no_outline',
			'value' => $noteMarkup,
		];
		$field = $this->pwcommerce->getInputfieldMarkup($options);
		return $field;
	}

	/**
	 * Process input for the values sent from the notes for this page
	 *
	 */
	public function ___processInput(WireInputData $input) {

		// nothing to process
		if (empty($input->pwcommerce_note_id)) {
			return;
		}

		//------------------
		$sanitizer = $this->wire('sanitizer');
		$notes = $this->field->type->getBlankValue($this->page, $this->field);
		$isError = false;
		$allowedNoteTypeValues = ['system', 'admin'];
		$value = $this->attr('value');
		$notesModified = false;

		// TODO: HOW TO HANDLE EMPTY NOTES? WE MAKE THIS A REQUIRED FIELD; SO IF EMPTY PROCESSWIRE WILL THROUGH FIELD ERROR; ELSE, WE HANDLE ERROR HERE OURSELF, ABORTING WHOLESAEL!

		// loop through and process values
		foreach ($input->pwcommerce_note_id as $n => $noteID) {
			$noteID = (int) $noteID;
			// @note: we handle system vs admin, etc, notes differently

			// @note: system notes are not editable. admins can view but not edit them.
			//--------------
			// CHECK IF SYSTEM NOTE
			$noteType = $sanitizer->option($input->pwcommerce_note_type[$n], $allowedNoteTypeValues);
			if ($noteType === 'system') {
				$note = $value->get("id={$noteID}");
				// nothing else to do for system note
			}
			// EITHER EXISTING 'ADMIN' OR NEW 'ADMIN' NOTE
			else {
				$noteText = $sanitizer->text($input->pwcommerce_note_text[$n]);
				// HANDLE NEW NOTES FIRST
				// @note: bool int (1/0)
				if (!empty($input->pwcommerce_note_is_new[$n])) {
					// skip empty new notes
					if (empty($noteText)) {
						continue;
					}

					//--------------
					// GOOD TO GO: CREATE NEW NOTE
					// create note and add to notes
					// new non-system note
					$time = time();
					$note = $this->field->type->getBlankRecord();
					$note->text = $noteText;
					$note->created = $time;
					$note->modified = $time;
					$userID = (int) $this->wire('user')->id;
					$note->createdUsersID = $userID;
					$note->modifiedUsersID = $userID;
					// @note: cannot create any other type of note here; must be 'admin'
					$note->type = 'admin';
					// @note: at least one note has been modified, i.e. a new note created
					// we use this to track if there are changes
					$notesModified = true;
					//--------
					// tempoary to track empty existing notes
					$note->isNew = true;
				} else {
					// ERROR: note text is required IF NOT NEW NOTE
					// @note TODO? for now, we stop wholesale
					if (empty($noteText)) {
						$isError = true;
						break;
					}
					//------------
					// GOOD TO GO: EDIT EXISING NOTE
					// @note: $note->created and $note->modified will be automatically filled by mysql as per the field's schema
					//--------
					// existing non-system note
					// just get existing note and update it if needed
					$note = $value->get("id={$noteID}");
					// if not has changed, update its text, modified time and modified users id
					if ($this->isNoteTextChanged($noteText, $note->text)) {
						$time = time();
						$note->text = $noteText;
						$note->modified = $time;
						$note->modifiedUsersID = (int) $this->wire('user')->id;
						// @note: at least one note has been modified
						// we use this to track if there are changes
						// this is because note here is the original note in the WireArray (value) and the above $noteText assignment has altered the original! hence, comparison will be meaningless
						$notesModified = true;
					}
				}
			}
			//------------
			$notes->add($note);
		} // end loop

		// ---------------
		// if we have an error, throw error
		if ($isError) {
			// @note: THROW ERROR HERE
			$this->error($this->_('Notes cannot be empty!'));
			// @note - return the same form!
			return;
		}

		# ************* TODO DELETE WHEN DONE IF NOT IN USE
		// if the string values of the processed notes are different from the previous,
		// or if any notes have been deleted, then flag this Inputfield as changed
		// so that it will be automatically saved with the page
		// @note: we compare using an in-house toString() private method as we don't implement toString() in the field.
		# *************
		// @update: we now check differently!
		// if at least one note has changed, we get this via $notesModified
		// if ($this->toStringInhouse($notes) !== $this->toStringInhouse($this->value)) {
		if (!empty($notesModified)) {
			$this->attr('value', $notes);
			$this->trackChange('value');
		}
	}

	//  TODO DELETE WHEN DONE IF NOT IN USE
	/**
	 * Make a string value to represent these values that can be used for comparison purposes.
	 *
	 * @note: this is only for internal use since we don't have a __toString() method.
	 * @return string
	 *
	 */
	private function toStringInhouse($items) {
		$a = [];
		foreach ($items as $item) {
			// @note: only these two properties can change client-side on edit, so we use them for comparison purposes
			$a[] = "$item->text: $item->modifiedUsersID";
		}
		return implode("\n", $a);
	}

	/**
	 * Compare if input versus saved admin note text are identical.
	 *
	 * If not identical, it means the admin note has been edited.
	 *
	 * @param string $newNoteText Note text from $input.
	 * @param string $existingNoteText Existing note text from note object.
	 * @return boolean Whether the note texts are identical.
	 */
	private function isNoteTextChanged($newNoteText, $existingNoteText) {
		return (string) $newNoteText !== (string) $existingNoteText;
	}

	/**
	 * Build the string for the creaeted and last modified date of this note.
	 *
	 * @credits: Amended from ProcessPageEdit::buildFormInfo().
	 * @param WireData $note The note whose created and modified date we are building.
	 * @return String $date The created and last modified date string.
	 */
	private function getNoteCreatedAndModifiedDatesString(WireData $note) {
		$unknown = '[?]';
		$datetimeFormat = $this->datetimeFormat;
		$lowestDate = strtotime('1974-10-10');
		// $createdDate = $note->created > $lowestDate ? date($datetimeFormat, $note->created) . " " .
		//   "<span class='detail'>(" . wireRelativeTimeStr($note->created) . ")</span>" : $unknown;
		$date = $unknown;
		if ($note->created > $lowestDate) {
			$date = date($datetimeFormat, $note->created) . " " .
				"<span class='detail'>(" . wireRelativeTimeStr($note->created) . ")</span>";
			// add modified date if greater than created date
			if ($note->modified && $note->modified > $note->created) {
				$date .= "<small class='block italic'>(" . $this->_('modified') . " " . date($datetimeFormat, $note->modified) . ")</small>";
			}
		}
		//--------------
		return $date;
	}

	private function getNoteAuthor($createdUsersID) {
		$out = $this->_('System');
		$author = $this->wire('users')->get($createdUsersID);
		// if (!empty($author->id)) {
		if (!$author instanceof NullPage) {
			// $out = $author->name; // TODO? email?
			$out = $author->email; // TODO? name?
		}
		return $out;
	}

	private function getNewBlankNote() {
		$newItem = $this->field->type->getBlankRecord();
		$newItem->id = (int) microtime(true);
		$newItem->created = time();
		$newItem->type = 'admin';
		$newItem->isNew = 1; // bool
		return $newItem;
	}
}