text: 'use strict';
  
  import Dialogue from './binaryReaders/dialogue';
  import * as FileIO from './fileIO';
  import Logic from './binaryReaders/logic';
  import * as VM from './vm';
  
  import * as Messages from './messages';
  
  const privates = new WeakMap();
  
  function checkReady() {
    const priv = privates.get(this);
    priv.errors = [];
    if (priv.dialogue == null || priv.logic == null) {
      priv.ready = false;
      return;
    }
    
    if (priv.dialogue.version !== priv.logic.version) {
      const error = `Incompatible file versions - logic ${priv.logic.version}, dialogue: ${priv.dialogue.version}`;
      priv.errors.push(error);
      console.error(error);
      priv.ready = false;
      return;
    } else {
      priv.IP = VM.createIP();
      priv.state = {
        variables: {},
        visited: [],
        waitForReturn: false,
      }
      priv.ready = true;
    }
  }
  
  function addShowText(command) {
    const priv = privates.get(this);
    const textLine = { text: command.display }
    if (command.characterIndex !== -1) {
      textLine.characterIndex = command.characterIndex;
      textLine.character = priv.logic.characters[command.characterIndex];
      textLine.localizedCharacter = priv.dialogue.characters[command.characterIndex];
    }
  
    if (priv.showText == null) priv.showText = [];
    priv.showText.push(textLine);
  }
  
  function handleFunc(command) {
    const { IP, state } = privates.get(this);
    if (command.function.name !== 'visited') {
      state.waitForReturn = command.function.return;
      return Messages.Function.handleCommand(command);
    }
    let nodeName = null;
    if (command.function.args.length > 0) nodeName = command.function.args[0];
    const visited = state.visited.indexOf(nodeName) !== -1;
    IP.args.unshift(visited);
    command.function.return = false;
    command.function.returnValue = visited;
    state.waitForReturn = false;
    return Messages.Function.handleCommand(command);
  }
  
  function generateMessage(command, priv) {
    if (command.enterNode != null) {
      const nodeName = priv.logic.nodeNames[command.index]
      priv.state.visited.push(nodeName);
      return new Messages.NodeChange(nodeName);
    } else if (command.var != null) {
      if (command.var.type === 'get') {
        return new Messages.Variable.Load(command.var.index, command.var.name, command.var.value);
      } else if (command.var.type === 'set') {
        return new Messages.Variable.Save(command.var.index, command.var.name, command.var.value);
      }
    } else if (command.function != null) {
      return handleFunc.call(this, command);
    } else if (command.options != null) {
      return Messages.Options.handleCommand(command);
    } else if (command.external === "runCommand") {
      return Messages.Command.handleCommand(command);
    } else if (command.external === "text") {
      return Messages.Text.handleCommand(command);
    } else if (command.endOfFile != null) {
      return new Messages.EndOfFile();
    } else if (command.dialogueBlock === true) {
      const messages = command.datas.map((value) => generateMessage(value, priv));
      return Messages.DialogueSegment.handleCommand(command.id, command.characterIndex, messages);
    }
  }
  
  /** This class represents a Jacquard bytecode runtime
  */
  export class Runtime {
    constructor() {
      const priv = {
        logic: null,
        dialogue: null,
        sourceMap: null,
        errors: [],
        ready: false,
      }
  
      privates.set(this, priv);
    }
  
    /** Load a file referenced by 'handle' 
     * @param {FileIO.handle} handle 
     */
    loadFile(handle) {
      const priv = privates.get(this);
      switch(FileIO.Type(handle)) {
        case FileIO.Types.Logic:
          priv.logic = new Logic(handle);
          break;
        case FileIO.Types.Dialogue:
          priv.dialogue = new Dialogue(handle);
          break;
        case FileIO.Types.SourceMap:
          priv.sourceMap = {logic: handle.logic, dialogue: handle.dialogue};
          break;
        default:
          return;
      }
  
      checkReady.call(this);
    }
  
    /**
     * Unload the file of specified type.
     * Note: will reset the runtime state.
     * @param {FileIO.Types} type 
     */
    removeFile(type) {
      const priv = privates.get(this);
      switch(FileIO.Type(handle)) {
        case FileIO.Types.Logic:
          priv.logic = null;
          break;
        case FileIO.Types.Dialogue:
          priv.dialogue = null;
          break;
        case FileIO.Types.SourceMap:
          priv.sourceMap = null;
          break;
        default:
          return;
      }
  
      checkReady.call(this);
    }
  
    /**
     * Run the main state bytecode. Be sure to act upon messages received.
     * If a function call message that requires a return value is received, 
     * you won't be able to run again until the return value has been given 
     * to the runtime. (not currently implemented)
     * @param {*} singleInstruction - run only one instruction at a time
     * @return {Message} a message that was generated by the runtime
     */
    run(singleInstruction) {
      const priv = privates.get(this);
      while(!singleInstruction && !priv.IP.halted && !priv.state.waitForReturn) {
        const result = VM.execute(priv.state, priv.IP, priv.logic, priv.dialogue);
        if (result != null) return generateMessage(result, priv);
      }
  
      if (priv.IP.halted) return new Messages.Halt();
  
      return null;
    }  
  
    /**
     * Return the value for the currently outstanding function call
     * @param {*} value the value to feed into the runtime 
     */
    functionReturnValue(value) {
      const { state, IP }  = privates.get(this);
      if (state == null) return;
      if (!state.waitForReturn) return;
      state.waitForReturn = false;
      IP.args.unshift(value);
    }
  
    /** Change the current instruction pointer to another one as specified.
     * @param {InstructionPointer} newIP 
     */
    moveInstructionPointerTo(newIP) {
      const priv = privates.get(this);
      priv.IP = newIP;
    }
  
    /** Change the instruction pointer to the start of the specified nodename 
     * @param {string} nodeName 
     */
    moveInstructionPointerToNode(nodeName) {
      const logic = privates.get(this).logic;
      if (logic == null) return false;
      const newOffset = logic.getOffsetForNode(nodeName);
      if (newOffset < 0) return false
      const IP = privates.get(this).IP;
      IP.logicOffset = newOffset;
      IP.dialogueOffset = -1;
      return true;
    }
  
    /** Reset the runtime to a base state */
    reset() {
      const priv = privates.get(this);
      priv.logic = null;
      priv.dialogue = null;
      priv.sourceMap = null;
      priv.errors = [];
      priv.ready = false;
    }
  
    /** Get the current instruction pointer
     * @returns {InstructionPointer}
     */
    get currentInstructionPointer() { return privates.get(this).IP; } 
  
    /**
     * Returns if the runtime is ready to start
     * @returns {boolean} ready to start
     */
    get ready() { return privates.get(this).ready; }
  
    /**
     * Returns if the runtime has encountered any errors
     * @returns {string[]} ready to start
     */
    get errors() { return privates.get(this).errors; }
  
    /**
     * Returns if a logic file has been loaded
     * @returns {boolean} loaded
     */
    get logicLoaded() { return privates.get(this).logic != null; }
  
    /**
     * Returns if a dialogue file has been loaded
     * @returns {boolean} loaded
     */
    get dialogueLoaded() { return privates.get(this).dialogue != null; }
  
    /**
     * Returns if a sourceMap file has been loaded
     * @returns {boolean} loaded
     */
    get sourceMapLoaded() { return privates.get(this).sourceMap != null; }
  
    /**
     * returns the variable list if the logic file is loaded
     * @returns {string[]} variable list
     */
    get variableList() {
      const logic = privates.get(this).logic;
      if (logic == null) return [];
      return logic.variables;
    }
  
    /** Gets the current variable state
     * @returns {Object{string, value}} a dictionary of variable names and their values
     */
    get variables() {
      const state = privates.get(this).state;
      if (state == null) return {};
      return Object.assign({}, state.variables);
    }
  
    /**
     * returns the function list if the logic file is loaded
     * @returns {string[]} function list
     */
    get functionList() {
      const logic = privates.get(this).logic;
      if (logic == null) return [];
      return logic.functions;
    }
  
    /**
     * returns the character list (in the engine language) if the logic file is loaded
     * @returns {string[]} character list
     */
    get characters() {
      const logic = privates.get(this).logic;
      if (logic == null) return [];
      return logic.characters;
    }
  
    /**
     * returns the character list (in the dialogue language) if the dialogue file is loaded
     * @returns {string[]} character list
     */  
    get localizedCharacters() {
      const dialogue = privates.get(this).dialogue;
      if (dialogue == null) return [];
      return dialogue.characters;    
    }
  
    /**
     * Get the entire list of node names
     * @returns {string[]} node names
     */
    get nodeNames() {
      const logic = privates.get(this).logic;
      if (logic == null) return [];
      return logic.nodeNames;
    }
  
    /**
     * Get the list of previously visted nodes (in order)
     * @returns {string[]} node names
     */
    get nodeHistory() {
      const state = privates.get(this).state;
      if (state == null) return [];
      return state.visited.map((item) => {return item;});
    }
  
  }
  
  const ExportedFileIO = {Open: FileIO.Open, Types: FileIO.Types, Type: FileIO.Type}
  export { ExportedFileIO as FileIO };
  
  const ExternalMessages = Messages.ExternalMessages;
  export { ExternalMessages as Messages } 
  