/**
* This file is licensed under the GPLv3 license (c) Aleksi 'Allexit' Talarmo
* See COPYING-GPL and README.md for more information.
*/
namespace JavaScriptX {
   export class JSX implements JSTerminal.Program {

      terminal: JSTerminal.Terminal;
      stdio: Std.IO;

      filesystem: FileSystem;
      currentFolder: Folder;

      environmentVariables: { [variable: string]: string } = {
         "system": "~/.system/"
      };

      create(terminal: JSTerminal.Terminal, stdio: Std.IO, args?: string[]) {
         this.terminal = terminal;
         this.stdio = stdio;

         this.filesystem = new FileSystem();
         this.currentFolder = this.filesystem.rootFolder;

         let system = new Folder(".system", true);
         this.filesystem.rootFolder.addFile(system);

         system.addFile(new BinaryFile("echo", new JSXCommands.Echo()));
         system.addFile(new BinaryFile("ls", new JSXCommands.List(this)));
         system.addFile(new BinaryFile("cd", new JSXCommands.ChangeDirectory(this)));
         system.addFile(new BinaryFile("mkdir", new JSXCommands.MakeDirectory(this)));
         system.addFile(new BinaryFile("woman", new JSXCommands.Woman(this)));
         system.addFile(new BinaryFile("cat", new JSXCommands.Caternate(this)));
         system.addFile(new BinaryFile("touch", new JSXCommands.Touch(this)));
         system.addFile(new BinaryFile("jedi", new JSXCommands.JEdi(this)));

         this.filesystem.rootFolder.addFile(new TextFile("textfile.txt", "Hello, world!"));

         stdio.println("Welcome to ");
         stdio.print("JSX", "red");
         stdio.print(" version ");
         stdio.print("0.4.0 beta", "green");
         stdio.print(" (K) All rites reversed");
         stdio.println("\nTo get started: call a ");
         stdio.print("woman", "blue");
         stdio.print(" for help.");
         stdio.refresh();

         this.takeCommand();
      }

      /**
      * Inherited from Program-interface. Not to be manually called.
      */
      enable() {
         this.takeCommand();
      }

      /**
      * Inherited from Program-interface. Not to be manually called.
      */
      disable() {
      }

      /**
      * Inherited from Program-interface. Not to be manually called.
      */
      onClose() {
         return true;
      }

      /**
      * Utility function. Writes a line such as 'Anonymous@localhost: ~/' and then reads line.
      * The result of the readline is used to call commands and such.
      */
      takeCommand() {
         let name = window.location.hostname || "localhost";
         this.stdio.println(`\nAnonymous@${name}`, "yellow");
         this.stdio.print(":");
         let path = this.currentFolder.getPath();
         this.stdio.print(path, "green");
         this.stdio.readline({
            callback: (result: string) => {
               let split = result.split(" ");
               let command = split[0];
               let args = split.slice(1, split.length);

               for (let v in this.environmentVariables) {
                  if (command == v) {
                     command = this.environmentVariables[v];
                     break;
                  }

                  let file = this.findPath(this.environmentVariables[v]);
                  if (file instanceof Folder) {
                     for (let i = 0; i < file.files.length; i++) {
                        let f = file.files[i];

                        if (command == f.name) {
                           command = f.getPath();
                           break;
                        }
                     }
                  }
               }

               for (let i = 0; i < args.length; i++) {
                  let regexp = /\${[\w]*}/g;
                  let arg = args[i];
                  let result: string[];
                  while ((result = regexp.exec(arg)) != null) {
                     let match = result[0];
                     let v = match.substr(2, match.length - 3);
                     let toReplace = null;

                     for (let env in this.environmentVariables) {
                        if (env == v) {
                           toReplace = this.environmentVariables[env];
                           break;
                        }
                     }
                     if (toReplace != null) {
                        args[i] = arg.replace(new RegExp("\\" + match, "g"), toReplace);
                     }
                  }
               }

               let file = this.findPath(command);
               if (file && (
                  command.startsWith("./")
                  || command.startsWith("~/")
                  || command.startsWith("../"))) {

                  if (file instanceof BinaryFile) {
                     file.run(this.terminal, args);
                     return;
                  } else {
                     this.stdio.println(`${file.name} is not executable`);
                  }
               } else {
                  if (command.startsWith(".")) {
                     this.stdio.println(`File not found`);
                  } else {
                     this.stdio.println(`Command not found`);
                  }
               }

               this.takeCommand();
            },
            prefix: " > ",
            printAfterDone: true,
            style: "red"
         })
         this.stdio.refresh();
      }

      /**
      * Returns the file at the specified path.
      * Example usage: findPath("~/somefolder/hello/");
      */
      findPath(path: string): File {
         let originalPath = path.slice();
         let pathPieces = path.split("/");

         let relativeFolder = this.currentFolder;
         let fileFound = null;
         for (let i = 0; i < pathPieces.length; i++) {
            let p = pathPieces[i];

            if (p == "") {
               if (relativeFolder == null) {
                  fileFound = null;
                  break;
               }
               if (i == pathPieces.length - 1 && relativeFolder instanceof Folder) {
                  fileFound = relativeFolder;
                  break;
               }
               fileFound = null;
               break;
            } else if (p == "..") {
               if (i == 0) {
                  if (this.currentFolder.parentFolder == null) {
                     fileFound = null;
                     break;
                  }
                  relativeFolder = this.currentFolder.parentFolder;
               }
               else if (relativeFolder == null) {
                  fileFound = null;
                  break;
               } else if (relativeFolder.parentFolder == null) {
                  fileFound = null;
                  break;
               } else {
                  relativeFolder = relativeFolder.parentFolder;
               }
               if (i == pathPieces.length - 1) {
                  fileFound = relativeFolder;
                  break;
               }
            } else if (p == ".") {
               if (i == 0) {
                  relativeFolder = this.currentFolder;
               } if (i == pathPieces.length - 1) {
                  fileFound = relativeFolder;
                  break;
               }
               continue;
            } else if (p == "~" && i == 0) {
               relativeFolder = this.filesystem.rootFolder;
               if (i == pathPieces.length - 1) {
                  fileFound = relativeFolder;
                  break;
               }
               continue;
            } else if (i == pathPieces.length - 1) {
               if (relativeFolder == null) {
                  fileFound = null;
                  break;
               }
               let idx = relativeFolder.files.map((f) => { return f.name })
                  .indexOf(p);
               let found = relativeFolder.files[idx];
               if (idx == -1) {
                  break;
               } else {
                  fileFound = found;
               }
            } else {
               if (relativeFolder == null) {
                  fileFound = null;
                  break;
               }
               let idx = relativeFolder.files.map((f) => { return f.name })
                  .indexOf(p);
               let found = relativeFolder.files[idx];
               if (idx == -1) {
                  break;
               } else if (found instanceof Folder) {
                  relativeFolder = found;
               } else {
                  break;
               }
            }
         }

         return fileFound;
      }

   }

   class FileSystem {

      rootFolder: Folder;

      constructor() {
         this.rootFolder = new Folder("~");
      }
   }

   abstract class File {

      name: string;
      parentFolder: Folder;
      hidden: boolean;
      created: number = new Date().getTime();

      constructor(name: string, hidden: boolean = false) {
         this.name = name;
         this.hidden = hidden;
      }

      /**
      * Returns the string path of the file.
      * Example: If the file is in root folder (~) and inside a 'somefolder'-folder, it would return "~/somefolder/<filename>"
      */
      getPath(): string {
         let path = "";
         let curr: File = this;
         while (curr != null) {
            if (curr instanceof Folder || path != "") {
               path = "/" + path;
            }
            path = curr.name + path;
            curr = curr.parentFolder;
         }
         return path;
      }

   }

   /**
   * File that contains other files.
   */
   export class Folder extends File {

      files: File[];

      constructor(name: string, hidden: boolean = false) {
         super(name, hidden);
         this.files = [];
      }

      /**
      * Add file to this folder.
      */
      addFile(file: File) {
         this.files.push(file);
         file.parentFolder = this;
      }

   }

   /**
   * File that contains JavaScript program which can be run
   */
   export class BinaryFile extends File {

      program: JSTerminal.Program;

      constructor(name: string, program: JSTerminal.Program, hidden: boolean = false) {
         super(name, hidden);
         this.program = program;
      }

      /**
      * Launch the program specified in this file.
      */
      run(terminal: JSTerminal.Terminal, args?: string[]) {
         terminal.launchProgram(this.program, args);
      }
   }

   /**
   * File that contains text
   */
   export class TextFile extends File {

      content: string;

      constructor(name: string, content: string = "", hidden: boolean = false) {
         super(name, hidden);
         this.content = content;
      }

      readContent() {
         return this.content.slice();
      }

      setContent(content: string) {
         this.content = content;
      }
   }

   /**
   * Used as an interface, to give programs documentation that can be read with the 'woman' command.
   * Implement along with Program.
   */
   export interface Manual {
      getManual(): string;
   }

   /**
   * Formats the given string to a certain length and alignment.
   * Example: formatToLength("hello", 10, "right") -> ".....hello" (where dots represent spaces)
   */
   export function formatToLength(text: string, length: number, alignment: "left" | "right" | "center" = "left") {
      if (alignment == "left") {
         text += " ".repeat(length);
         return text.substr(0, text.length - (text.length - length));
      } else if (alignment == "right") {
         text = " ".repeat(length) + text;
         return text.substr(-length);
      } else if (alignment == "center") {
         let left = Math.max(0, Math.floor(length / 2 - text.length / 2));
         let right = Math.max(0, Math.ceil(length / 2 - text.length / 2));
         return " ".repeat(left) + text + " ".repeat(right);
      }
      return null;
   }
}