import * as Is from './is';
import { TclError } from './tclerror';
import { Interpreter } from './interpreter';
import { CommandToken } from './parser';
import { isArray } from 'util';

/**
 * The basic structure for any variable in this interpreter
 *
 * @export
 * @class TclVariable
 */
export class TclVariable {
  protected value: any = '';
  protected name: string | undefined = undefined;

  /**
   * Construct a parent TclVariable, you should never be directly constructing this object.
   *
   * @param {*} value
   * @param {string} [name]
   * @memberof TclVariable
   */
  public constructor(value: any, name?: string) {
    this.value = value;
    if (name) this.name = name;
  }

  /**
   * Get the value of any tcl type, this will always be a string
   *
   * @returns {string}
   * @memberof TclVariable
   */
  public getValue(): string {
    return this.value;
  }

  // Not needed
  /**
   * Set the value of any tcl type, this can be anything
   *
   * @param  {any} value
   * @returns {any}
   * @memberof TclVariable
   */
  /*
  public setValue(value: any): any {
    this.value = value;
    return value;
  }*/

  // Not needed
  /**
   * This function should not be used but is here in case you need it for testing
   * It return the raw interal data storage of the variable
   *
   * @returns {*}
   * @memberof TclVariable
   */
  /*
  public getRawValue(): any {
    return this.value;
  }*/

  /**
   * This function returns the name of the variable, as long as it has one
   *
   * @returns {(string | undefined)}
   * @memberof TclVariable
   */
  public getName(): string | undefined {
    return this.name;
  }

  /**
   * This function sets the name of the variable
   *
   * @param {string} name
   * @memberof TclVariable
   */
  public setName(name: string) {
    this.name = name;
  }
}

/**
 * A variable for holding a list, this is created from a raw string most of the time
 *
 * @export
 * @class TclList
 * @extends {TclVariable}
 */
export class TclList extends TclVariable {
  /**
   * Creates an instance of TclList.
   *
   * @param {(string | Array<TclVariable>)} - The string to parse into the variable
   * @param {string} [name] - An optional name for the variable
   * @memberof TclList
   */
  public constructor(value: string | Array<TclVariable>, name?: string) {
    super([], name);
    if (typeof value === 'string') this.destruct(value);
    else this.value = value;
  }

  /**
   * Called to parse a string into the internal value array
   *
   * @private
   * @param {string} input - The list input you want to parse
   * @memberof TclList
   */
  private destruct(input: string): void {
    // Initialize a counter for keeping track of the posisition in the string
    let idx = 0;
    // Initialize a char with the first character of the string
    let char = input.charAt(idx);

    /**
     * This function is used for progressing to the next char in the string
     *
     * @returns {string} - This is the previous char
     */
    function read(): string {
      let old = char;
      idx += 1;
      char = input.charAt(idx);
      return old;
    }

    /**
     * This function is used to parse the braces in lists
     *
     * This is because words enclosed in bracets should be seen as one list item
     * E.g. 'hi {hello there}' => ['hi', 'hello there']
     * and 'hi {this is a {nested list}} wow' => ['hi', 'this is a {nested list}', 'wow']
     *
     * @returns {string}
     */
    function parseBrace(): string {
      // Initialize a string to keep the return value
      let returnVar = '';

      // Intialize a variable to keep track of how many braces deep we are
      let depth = 0;

      // Keep reading the string as long as we have input
      while (idx < input.length) {
        // Increase or decrease the depth depending on the brackets
        if (char === '{') {
          depth++;
          // Ignore the outer brace
          if (depth === 1) {
            read();
            continue;
          }
        }
        if (char === '}') {
          depth--;
          // Ignore the outer brace
          if (depth === 0) {
            read();
            break;
          }
        }

        // Add the next character to the output
        returnVar += read();
      }

      // This is true when there are more closing brackets than opening brackets
      if (depth !== 0) throw new TclError('incorrect brackets in list');

      // Check if the character following the } is a whitespace
      if (!Is.WordSeparator(char) && char !== '')
        throw new TclError(
          'list element in braces followed by character instead of space',
        );

      return returnVar;
    }

    // Initialize a counter for keeping track of the current list index
    let i = 0;

    // Keep reading until there is no more string to read
    while (idx < input.length) {
      // Initialize an empty string to contain the next list item
      let tempWord = '';

      // Skip all whitespace
      while (Is.WordSeparator(char) && idx < input.length) {
        read();
      }

      // Parse the braces if a brace is found
      if (char === '{') {
        tempWord += parseBrace();
      }
      // Just add the characters to the output if not
      else {
        while (!Is.WordSeparator(char) && idx < input.length) {
          tempWord += read();
        }
      }

      // Check if there was actually data left to read
      if (tempWord === '') break;

      // Set the value correctly
      this.value[i] = new TclSimple(tempWord);

      // Increment the item index
      i++;

      // Move to the next char and word, regardless if the index has incremented
      read();
    }
  }

  // Not yet needed
  /**
   * Function to set an item in the list to a value
   *
   * @param {number} index - The index in the list you want to set to the value
   * @param {TclSimple} [value] - The value you want to add, if this empty the item will be removed
   * @returns {(TclSimple | undefined)} - The value you sent via the value argument
   * @memberof TclList
   */
  /*
  public set(index: number, value?: TclSimple): TclSimple | undefined {
    // If the value is nonexitant we want to delete the item
    if (!value) {
      // We cannot delete an item that does not exist
      if (!this.value[index])
        throw new TclError('cannot delete list item, item does not exist');

      // Remove the item from the array
      this.value.splice(index, 1);
    } else {
      // If we dont want to delete we just set the value
      this.value[index] = value;
    }
    return value;
  }*/

  // Not yet needed
  /**
   * Delete a value from the list
   *
   * @param {number} index
   * @memberof TclList
   */
  /*
  public unset(index: number): void {
    // Use the set function to delete
    this.set(index);
  }*/

  /**
   * Get the string value of the list, this is made by joining the values with a space
   *
   * @returns {string} - The joined array
   * @memberof TclList
   */
  public getValue(): string {
    let toReturn = this.value.map((val: TclSimple) => val.getValue());
    toReturn = toReturn.map((val: string) =>
      val.indexOf(' ') > -1 ? `{${val}}` : val,
    );
    return toReturn.join(' ');
  }

  /**
   * This function is used to recursively retrieve items from a list, every argument is one list deeper
   *
   * @param {...Array<number>} args - The indexes of the lists
   * @returns {TclSimple} - The eventually retrieved value
   * @memberof TclList
   */
  public getSubValue(...args: Array<number>): TclSimple {
    // There are no arguments, so just return a TclSimple with the current value
    if (args.length === 0)
      return new TclSimple(this.getValue(), this.getName());

    // There is only one argument, so if available return the value at that index
    if (args.length === 1) {
      if (this.value[args[0]]) return this.value[args[0]];
      // If not return an empty TclSimple, according to the tcl wiki
      else return new TclSimple('');
    }

    // Code reaches here when there are more than 1 arguments

    // Create a variable for keeping the current list
    let tempList: TclList = this;

    // Create a variable for keeping the eventual return value
    let out: TclSimple = new TclSimple('');

    // Loop over the received arguments
    for (let arg of args) {
      // Throw an error when there is no list available
      // if (!tempList) throw new TclError('item is no list');

      // Retrieve the value from the list at the current index=arg and assign this to the output
      out = tempList.getSubValue(arg);

      // we create a list out of it and assign that to the tempList
      tempList = out.getList();

      /* TODO: Check if this can be commented
      // If the output is a TclSimple we create a list out of it and assign that to the tempList
      if (out instanceof TclSimple) tempList = out.getList();
      // If not tempList will be undefined
      else tempList = undefined;*/
    }

    // TODO: Check if this can be commented
    // Throw an error when there is still no return value
    // if (!out) throw new TclError('no such element in array');
    return out;
  }

  /**
   * Get the length of the list
   *
   * @returns {number}
   * @memberof TclList
   */
  public getLength(): number {
    return this.value.length;
  }

  /**
   * Function to create a list from an array of arrays
   *
   * @static
   * @param {Array<any>} input - The nested arrays
   * @returns {TclSimple} - The generated list
   * @memberof TclList
   */
  public static createList(input: Array<any>): TclSimple {
    let processable = [...input];
    for (let i = 0; i < processable.length; i++) {
      if (isArray(processable[i])) {
        processable[i] = TclList.createList(processable[i]);
      }
    }

    let simpleResults = processable.map((r) =>
      r instanceof TclVariable ? r : new TclSimple(r),
    );
    let listResult = new TclList(simpleResults).getSubValue();
    return listResult;
  }
}

/**
 * A tcl variable for holding simple values like strings and numbers.
 * Although numbers will still be stored as a string instead of a number, and converted on request.
 *
 * @export
 * @class TclSimple
 * @extends {TclVariable}
 */
export class TclSimple extends TclVariable {
  /**
   *Creates an instance of TclSimple.
   * @param {(string | boolean | number)} value - The initial value
   * @param {string} [name] - Variable name
   * @memberof TclSimple
   */
  constructor(value: string | boolean | number, name?: string) {
    super(value.toString(), name);
  }

  /**
   * This function will generate a new TclList object from the current value it holds
   *
   * @returns {TclList} - The created list
   * @memberof TclSimple
   */
  public getList(): TclList {
    let list = new TclList(this.value, this.getName());
    return list;
  }

  /**
   * Function to convert the TclSimple to a js number if the value allows this
   *
   * @param {boolean} [isInt=false] - Tell the function to return an int or a float
   * @returns {number} - The returned number, 0 if the variable is not a number
   * @memberof TclSimple
   */
  public getNumber(isInt: boolean = false): number {
    if (this.isNumber())
      return isInt ? parseInt(this.value, 10) : parseFloat(this.value);
    else if (this.isBoolean()) return this.getBoolean() ? 1 : 0;
    else return 0;
  }

  /**
   * Check if this TclSimple can be converted to a number
   *
   * @returns {boolean}
   * @memberof TclSimple
   */
  public isNumber(): boolean {
    return Is.Number(this.value);
  }

  /**
   * Convert this TclSimple to a boolean
   *
   * @returns {boolean}
   * @memberof TclSimple
   */
  public getBoolean(): boolean {
    if (
      this.value === 'true' ||
      this.value === 'on' ||
      this.value === 'yes' ||
      this.value === '1'
    )
      return true;
    else if (
      this.value === 'false' ||
      this.value === 'off' ||
      this.value === 'no' ||
      this.value === '0'
    )
      return false;
    else if (this.value) return true;
    else return false;
  }

  /**
   * Check if this TclSimple can be converted to a boolean
   *
   * @returns {boolean}
   * @memberof TclSimple
   */
  public isBoolean(): boolean {
    return Is.Boolean(this.value);
  }
}

/**
 * This is a variable for holding objects in tcl, these could be compared to the objects in normal JS
 *
 * @export
 * @class TclObject
 * @extends {TclVariable}
 */
export class TclObject extends TclVariable {
  /**
   * Creates an instance of TclObject.
   *
   * @param {TclVariableHolder} [value] - Will set the internal value to empty if none is parsed
   * @param {string} [name]
   * @memberof TclObject
   */
  constructor(value?: TclVariableHolder, name?: string) {
    super(value, name);
    if (!this.value) this.value = {};
  }

  /**
   * Function to set a value at a object key
   *
   * @param {string} name - The key to put the value at
   * @param {TclVariable} [value] - If this is undefined, the object key will be deleted
   * @returns {(TclVariable | undefined)} - The value parsed
   * @memberof TclObject
   */
  public set(name: string, value?: TclVariable): TclVariable | undefined {
    // If value is empty delete value from the internal object
    if (!value) {
      if(Object.keys(this.value).indexOf(name) < 0) throw new TclError('cannot delete object item, item does not exist');
      delete this.value[name];
    }
    // If there is data append it to the correct key
    else this.value[name] = value;
    return value;
  }

  /**
   * Remove a key from the object
   *
   * @param {string} name - The key you want to remove
   * @memberof TclObject
   */
  public unset(name: string): void {
    // Use the set function to do the job
    this.set(name);
  }

  /**
   * You are not meant to directly get the value of an object, so throw an error
   *
   * @returns {string}
   * @throws {TclError}
   * @memberof TclObject
   */
  public getValue(): string {
    throw new TclError(`can't read "${this.getName()}": variable is object`);
  }

  /**
   * Get a value from a specified key in the object
   *
   * @param {string} name - The key you want the value from
   * @returns {TclVariable} - The value
   * @memberof TclObject
   */
  public getSubValue(name: string): TclVariable {
    // Return this value when no name is specified
    if (name === '') return new TclSimple(this.getValue(), this.getName());

    // Throw error when key does not exist
    if (!this.value[name])
      throw new TclError(`no value found at given key: ${name}`);
    return this.value[name];
  }

  /**
   * Get all object keys that are present
   *
   * @returns {string[]}
   * @memberof TclObject
   */
  public getKeys(): string[] {
    return Object.keys(this.value);
  }

  /**
   * Get the size of the object
   *
   * @returns {number} - Size
   * @memberof TclObject
   */
  public getSize(): number {
    return Object.keys(this.value).length;
  }
}

/**
 * A variable for holding a tcl array, this is comparable to the standard JS array
 *
 * @export
 * @class TclArray
 * @extends {TclVariable}
 */
export class TclArray extends TclVariable {
  /**
   * Creates an instance of TclArray.
   *
   * @param {Array<TclVariable>} [value] - Will set the value to an empty array if nothing is parsed
   * @param {string} [name]
   * @memberof TclArray
   */
  public constructor(value?: Array<TclVariable>, name?: string) {
    super(value, name);
    if (!this.value) this.value = [];
  }

  /**
   * Set a value to a specified index in the array and return the value
   *
   * @param {number} index - The index you want the value to be set at
   * @param {TclVariable} [value] - The value you want to set, leave empty to remove the index
   * @returns {(TclVariable | undefined)} - The value specified
   * @memberof TclArray
   */
  public set(index: number, value?: TclVariable): TclVariable | undefined {
    // If the value is nonexitant we want to delete the item
    if (!value) {
      // We cannot delete an item that does not exist
      if (!this.value[index])
        throw new TclError('cannot delete array item, item does not exist');

      // Remove the item from the array
      delete this.value[index];
    } else {
      // If we dont want to delete we just set the value
      this.value[index] = value;
    }
    return value;
  }

  /**
   * Remove an index from the array
   *
   * @param {number} index
   * @memberof TclArray
   */
  public unset(index: number): void {
    // Use the set function to remove the index
    this.set(index);
  }

  /**
   * You are not meant to directly get the value of an array, so throw an error
   *
   * @returns {string}
   * @memberof TclArray
   */
  public getValue(): string {
    throw new TclError(`can't read "${this.getName()}": variable is array`);
  }

  /**
   * Get the value at a specified index
   *
   * @param {number} index - The index you want the value from
   * @param {boolean} [force] - If true, wont throw error on nonexistant index (please avoid using this)
   * @returns {TclVariable} - The found value
   * @memberof TclArray
   */
  public getSubValue(index: number, force?: boolean): TclVariable {
    // If index is not correct return the value of this variable
    if (index === undefined || index === null)
      return new TclSimple(this.getValue(), this.getName());

    // Throw error if index does not exist
    if (!this.value[index]) {
      if (force) return new TclVariable(undefined);
      else throw new TclError(`no value found at given index: ${index}`);
    }
    return this.value[index];
  }

  /**
   * Get the length of the array
   *
   * @returns {number}
   * @memberof TclArray
   */
  public getLength(): number {
    return this.value.length;
  }
}

/**
 * A simple interface for an object that stores TclVariables with a string index
 *
 * @export
 * @interface TclVariableHolder
 */
export interface TclVariableHolder {
  [index: string]: TclVariable;
}

/**
 * A simple interface for an object that holds tcl procs by a string index
 *
 * @export
 * @interface TclProcHolder
 */
export interface TclProcHolder {
  [index: string]: TclProc;
}

export type ProcArgs = TclVariable[] | string[];

// Types of functions a proc can have
export type TclProcFunction =
  | ((
      interpreter: Interpreter,
      args: ProcArgs,
      command: CommandToken,
      helpers: TclProcHelpers,
    ) => TclVariable)
  | ((
      interpreter: Interpreter,
      args: ProcArgs,
      command: CommandToken,
      helpers: TclProcHelpers,
    ) => Promise<TclVariable>);

/**
 * The given options for a proc
 *
 * @interface TclProcOptions
 */
interface TclProcOptions {
  helpMessages: {
    [index: string]: string;
  };
  arguments: {
    pattern: string;
    textOnly: boolean;
    simpleOnly: boolean;
    amount:
      | number
      | {
          start: number;
          end: number;
        };
  };
}

/**
 * This is the same as TclProcOptions, except you can leave options empty
 *
 * @export
 * @interface TclProcOptionsEmpty
 */
export interface TclProcOptionsEmpty {
  helpMessages?: {
    [index: string]: string;
  };
  arguments?: {
    pattern?: string;
    textOnly?: boolean;
    simpleOnly?: boolean;
    amount?:
      | number
      | {
          start: number;
          end: number;
        };
  };
}

/**
 * The helper functions while running a proc
 *
 * @export
 * @interface TclProcHelpers
 */
export interface TclProcHelpers {
  sendHelp: (helpType: string) => never;
  solveExpression: (expression: string) => Promise<number>;
}

/**
 * This is the standard holder for a tcl procedure
 *
 * @export
 * @class TclProc
 */
export class TclProc {
  name: string;
  callback: TclProcFunction;
  options: TclProcOptions = {
    helpMessages: {
      wargs: `wrong # args`,
      wtype: `wrong type`,
      wexpression: `expression resolved to unusable value`,
      undefifop: `undefined if operation`,
    },
    arguments: {
      amount: -1,
      pattern: `blank`,
      textOnly: false,
      simpleOnly: false,
    },
  };

  /**
   * Creates an instance of TclProc
   * Will assign the name and callback
   *
   * @param {string} name
   * @param {TclProcFunction} callback
   * @param {TclProcOptionsEmpty} [options]
   * @memberof TclProc
   */
  constructor(
    name: string,
    callback: TclProcFunction,
    options?: TclProcOptionsEmpty,
  ) {
    this.name = name;
    this.callback = callback;

    // Set the options

    if (options) {
      if (options.helpMessages)
        this.options.helpMessages = {
          ...this.options.helpMessages,
          ...options.helpMessages,
        };

      if (options.arguments) {
        if (options.arguments.amount)
          this.options.arguments.amount = options.arguments.amount;
        if (options.arguments.pattern)
          this.options.arguments.pattern = options.arguments.pattern;
        if (options.arguments.textOnly)
          this.options.arguments.textOnly = options.arguments.textOnly;
        if (options.arguments.textOnly || options.arguments.simpleOnly)
          this.options.arguments.simpleOnly = true;
      }
    }
  }
}
