/**
* Copyright (c) 2016 Aleksi 'Allexit' Talarmo
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
namespace JSTerminal {

   class TerminalProgram implements Program {
      create(terminal: Terminal, stdio: Std.IO) {}
      enable() {}
      disable() {}
      onClose() { return false; }
   }

   export class Terminal {
      selector: string;
      width: number;
      height: number;
      lineElems: JQuery[];
      characters: Character[][];

      programStack: Program[];

      stdio: Std.IO;

      constructor(selector: string, width: number, height: number) {
         this.selector = selector;
         this.width = width;
         this.height = height;

         this.lineElems = [];
         this.characters = [];
         for (let i = 0; i < this.height; i++) {
            let characterLine: Character[] = [];
            for (let i2 = 0; i2 < this.width; i2 ++) {
               characterLine.push(new Character(" ", ""));
            }
            this.characters.push(characterLine);

            let line = " ".repeat(this.width);
            let element = $(document.createElement("p"));
            element.text(line);
            this.lineElems.push(element);
            $(selector).append(element);
         }

         this.stdio = new Std.IO(this);

         this.programStack = [];
         this.programStack.push(new TerminalProgram());
      }

      /**
      * Used to simply clear the terminal screen.
      * Useful for when you don't use Jash/JSX.
      */
      clearScreen(styles: string = "") {
         for (let y = 0; y < this.height; y++) {
            for (let x = 0; x < this.width; x ++) {
               this.characters[y][x] = new Character(" ", styles);
            }
         }
      }

      /**
      * Used to write text at a certain coordinate on screen.
      * Useful for when you want to make your own rendering systems
      * and don't want to use regular text-based systems.
      */
      writeText(text: string, x: number, y: number, styles: string = "") {
         for (let i = 0; i < text.length; i++) {
            this.putChar(text[i], x + i, y, styles);
         }
      }

      /**
      * Utility function for writeText. Can also be used but probably not as useful.
      * Prints only one char (first from given string).
      */
      putChar(char: string, x: number, y: number, styles: string = "") {
         this.putTrueChar(new Character(char, styles), x, y);
      }

      /**
      * Utility function when you might want to print out a true
      * Character-class. (works similarily to putChar)
      */
      putTrueChar(char: Character, x: number, y: number) {
         if (x >= this.width || y >= this.height) { return; }
         this.characters[y][x] = char;
      }

      /**
      * Empties the screen and re-draws it. Required to call every time
      * you want to change the screen.
      */
      render() {
         for (let y = 0; y < this.characters.length; y++) {
            let line = this.characters[y];
            let lineElem = this.lineElems[y];

            lineElem.empty();
            let currSpan = null;
            let currStyle = null;
            let currText = "";
            for (let x = 0; x < line.length + 1; x++) {
               let currChar = line[x];
               if (x == line.length || currStyle != currChar.styles) {
                  if (currSpan != null) {
                     currSpan.text(currText);
                     currSpan.addClass(currStyle);
                     lineElem.append(currSpan);
                  }
                  if (x >= line.length) {
                     break;
                  }

                  currText = "";
                  currSpan = $(document.createElement("span"));
                  currStyle = currChar.styles;
               }
               currText += currChar.char;
            }
         }
      }

      /**
      * Used to launch programs like Jash.
      */
      launchProgram(program: Program, args?: string[]) {
         this.programStack.slice(-1).pop().disable();
         this.programStack.push(program);
         program.create(this, this.stdio, args);
      }

      /**
      * Used to close programs like Jash.
      */
      closeProgram(program: Program) {
         let idx = this.programStack.indexOf(program);
         if (idx == -1) {
            return;
         }
         if (this.programStack[idx].onClose()) {
            if (idx == this.programStack.length - 1) {
               this.programStack[this.programStack.length - 2].enable();
            }
            this.programStack.splice(idx, 1);
         }
      }
   }

   /**
   * Utility function for creating the terminal.
   * Initializes the class and sets up some classes you might want to use.
   */
   export function createTerminal(selector: string, width: number, height: number) {
      console.info(`Creating a terminal with ${width}x${height}`);

      $(selector).addClass("js-terminal-style");
      $(selector).css('width', width + "ch");
      $(selector).css('height', height + "em");
      $(selector).empty();

      return new Terminal(selector, width, height);
   }

   export class Character {
      char: string;
      styles: string;

      constructor(char: string, styles: string) {
         this.char = char[0];
         this.styles = styles;
      }
   }

   /**
   * The Program-interface can be used as a program in the terminal.
   * Programs are launched, enabled, disabled and closed. Only one program may run at a time (the previous one will be disabled).
   *
   * When the program is asked to close (onClose is called) it returns a boolean weather it should close or not.
   */
   export interface Program {
      create(terminal: JSTerminal.Terminal, stdio: Std.IO, args?: string[]);
      enable();
      disable();
      onClose(): boolean;
   }
}
