// 2020 Bjoern Annighoefer

var ACTIONS = ACTIONS || {};

String.prototype.insertArray = function (findDict, startCharacter = "", endCharacter = "") {
  let replaceString = this;
  for (let key in findDict) {
    replaceString = replaceString.replace(
      startCharacter + key + endCharacter,
      findDict[key] + " " + startCharacter + key + endCharacter,
    );
  }
  return replaceString;
};

Object.assign(
  ACTIONS,
  (function () {
    /* BA: Fetch actions might be removed later if loading becomes part of a common domain*/
    function FetchActions(domain) {
      return new Promise(function (resolve, reject) {
        let cmd = new eoq2.Gaa();
        domain.Do(cmd).then(function (val) {
          try {
            let actions = [];
            let nActions = val.length;
            for (let i = 0; i < nActions; i++) {
              let actionInfo = val[i];
              let actionSegments = actionInfo[0]
                .replaceAll("\\", "*")
                .replaceAll("/", "*")
                .split("*");
              let nSegments = actionSegments.length;
              let actionName = actionSegments[nSegments - 1];
              let categories = actionSegments.slice(0, nSegments - 1);
              actions.push({
                id: actionInfo[0],
                name: actionName,
                categories: categories,
                description: actionInfo[3],
                parameters: [],
                results: [],
                tags: actionInfo[4],
              });
              let argsInfos = actionInfo[1];
              let nParameters = argsInfos.length;
              for (let j = 0; j < nParameters; j++) {
                let argsInfo = argsInfos[j];
                actions[i].parameters.push({
                  name: argsInfo[0],
                  type: argsInfo[1],
                  upperBound: argsInfo[2],
                  lowerBound: argsInfo[3],
                  default: argsInfo[4],
                  description: argsInfo[5],
                  choices: [],
                });
                let choicesInfos = argsInfo[6];
                let nChoices = choicesInfos.length;
                for (let k = 0; k < nChoices; k++) {
                  let choicesInfo = choicesInfos[k];
                  actions[i].parameters[j].choices.push({
                    value: choicesInfo[0],
                    description: choicesInfo[1],
                  });
                }
              }
              let resultInfos = actionInfo[1];
              let nResultParameters = resultInfos.length;
              for (let j = 0; j < nResultParameters; j++) {
                let resultInfo = resultInfos[j];
                actions[i].results.push({
                  name: resultInfo[0],
                  type: resultInfo[1],
                  upperBound: resultInfo[2],
                  lowerBound: resultInfo[3],
                  description: resultInfo[4],
                  default: resultInfo[5],
                });
              }
              resolve(actions);
            }
          } catch (e) {
            reject(new Error("Failed to retrieve action list:" + e.toString()));
          }
        });
      });
    }

    function ActionRunner(params) {
      this.domain = null;
      this.actionName = "";
      this.args = []; //no arguments by default
      this.refreshRate = 100; //ms
      this.data = null; //to be used in callbacks
      this.onFailedCallback = function (info) {};
      this.onStatusUpdateCallback = function (statusInfo) {};
      this.onRunCallback = function (info) {};
      this.onFinishedCallback = function (info) {};
      this.onAbortedCallback = function (info) {};
      this.onResultCallback = function (res) {};

      //initialized later
      this.callEventCallback = null;
      this.statusInfo = null;
      this.processStatus = {
        currentTask: 0,
        numTasks: 0,
        taskPercentage: 0,
        totalPercentage: 0,
        taskStack: [],
      };
      this.streamBuffer = "";

      //Copy params
      jsa.CopyParams(this, params);

      //internals
      this.callId = -1;
      this.pullTimout = null;

      return this;
    }

    ActionRunner.prototype.Run = function () {
      //starts the action execution
      //initialize the status
      this.statusInfo = {
        status: eoq2.action.CallStatus.INI, // TODO: is being overridden later on!
        output: "",
        progress: 0,
        //            status : ""
      };
      this.processStatus.numTasks = 0;

      //register for any changes on actions befor running it
      let self = this;
      this.callEventCallback = function (evts, src) {
        self.OnCallEvent(evts, src);
      };

      this.domain.Observe(this.callEventCallback, [
        eoq2.event.EvtTypes.OUP,
        eoq2.event.EvtTypes.CST,
        eoq2.event.EvtTypes.CVA,
      ]);

      let cmd = new CMD.Cmp().Asc(this.actionName, this.args, ["autoobserve", true]); //args are already an array

      this.domain
        .Do(cmd)
        .then(function (val) {
          self.callId = val[0];
        })
        .catch(function (e) {
          self.onFailedCallback(e);
        });
    };

    ActionRunner.prototype.UnobserveCall = function () {
      this.domain.Unobserve(this.callEventCallback);
    };

    ActionRunner.prototype.__ParseActionOutput = function (newData) {
      let data = "";
      this.streamBuffer += newData;
      if (!this.streamBuffer.includes("\n")) return "";
      else {
        data = this.streamBuffer.substring(0, this.streamBuffer.indexOf("\n"));
        this.streamBuffer = this.streamBuffer.substring(this.streamBuffer.indexOf("\n") + 1);
      }
      if (data.includes("Started Task")) {
        this.processStatus.currentTask++;
        this.processStatus.taskPercentage = 0;
        if (this.processStatus.numTasks < this.processStatus.currentTask) {
          this.processStatus.numTasks = this.processStatus.currentTask;
        }
        const regex = /\[Started Task]\s*(.*)/gm;
        let m;
        while ((m = regex.exec(data)) !== null) {
          if (m.index === regex.lastIndex) {
            regex.lastIndex++;
          }
          m.forEach((match, groupIndex) => {
            if (groupIndex === 1) {
              this.processStatus.currentTaskName = match;
              this.statusInfo.callStatusStr = match;
              this.processStatus.taskStack.append(match);
            }
          });
        }
      } else if (data.includes("Ended Task")) {
        this.processStatus.taskPercentage = 100;
        this.processStatus.taskStack.pop();
        this.statusInfo.callStatusStr = "Completed.";
      } else if (data.includes("Announced Tasks")) {
        const regex = /\[Announced Tasks]\s*(\d+)/gm;
        let m;
        while ((m = regex.exec(data)) !== null) {
          if (m.index === regex.lastIndex) {
            regex.lastIndex++;
          }
          m.forEach((match, groupIndex) => {
            if (groupIndex === 1) {
              this.processStatus.numTasks += Number.parseInt(match);
              console.log("added task number " + this.processStatus.numTasks);
            }
          });
        }
      }
      if (!this.__FindProgressFromText(data)) {
        return this.__StyleActionOutput(data);
      } else {
        return "";
      }
    };
    ActionRunner.prototype.__CalculateProgress = function () {
      if (0 === this.processStatus.numTasks) {
        // no tasks announced, assuming legacy action
        return this.processStatus.taskPercentage;
      } else {
        return Math.ceil(
          ((this.processStatus.currentTask - 1) / this.processStatus.numTasks) * 100 +
            this.processStatus.taskPercentage / this.processStatus.numTasks,
        );
      }
    };

    ActionRunner.prototype.__StyleActionOutput = function (data) {
      let classes = "";
      if (data.includes("WARNING")) {
        classes = "action-warning";
      } else if (data.includes("FAILED")) {
        classes = "action-error";
      } else if (data.includes("SUCCESS")) {
        classes = "action-success";
      } else if (data.includes("INFO")) {
        classes = "action-info";
      } else if (data.includes("Started Task")) {
        classes = "action-info";
      }
      data = '<div class="' + classes + '">' + data + "</div>";
      let replacements = {
        SAVED: '<i class="fas fa-save" title="SAVED"></i>',
        DELETED: '<i class="fas fa-trash" title="DELETED"></i>',
        CHANGED: '<i class="fas fa-edit" title="CHANGED"></i>',
        CREATED: '<i class="fas fa-plus-square" title="CREATED"></i>',
        SUCCESS: '<i class="fas fa-check-circle" title="SUCCESS"></i>',
        FAILED: '<i class="fas fa-times-octagon" title="FAILED"></i>',
        WARNING: '<i class="fas fa-exclamation-triangle" title="WARNING"></i>',
        INFO: '<i class="fas fa-info-circle" title="INFO"></i>',
        FOUND: '<i class="fas fa-search" title="FOUND"></i>',
      };
      return data.insertArray(replacements, "[", "]");
    };

    ActionRunner.prototype.__HandleCallOutput = function (evt) {
      let channel = evt.a[1];
      let data = evt.a[2];

      if (channel === "STDOUT") {
        // TODO: check that this if statement compares against STDOUT

        this.statusInfo.output += this.__ParseActionOutput(data);
        this.statusInfo.progress = this.__CalculateProgress();
      }
      this.onStatusUpdateCallback(this.statusInfo);
    };

    ActionRunner.prototype.OnCallEvent = function (evts, src) {
      for (let i = 0; i < evts.length; i++) {
        let evt = evts[i];
        let callId = evt.a[0]; //all registered event types have the call id in the first position
        if (callId == this.callId) {
          //only process events for the own call
          switch (evt.evt) {
            case eoq2.event.EvtTypes.OUP:
              this.__HandleCallOutput(evt);
              break;
            case eoq2.event.EvtTypes.CST:
              {
                let status = evt.a[1];
                let info = evt.a[2];

                this.statusInfo.status = status;
                this.onStatusUpdateCallback(this.statusInfo);

                switch (status) {
                  case eoq2.action.CallStatus.RUN:
                    this.onRunCallback(info);
                    break;
                  case eoq2.action.CallStatus.FIN:
                    //stop listening to events
                    this.UnobserveCall();
                    this.onFinishedCallback(info);
                    break;
                  case eoq2.action.CallStatus.ABO:
                    this.UnobserveCall();
                    this.onAbortedCallback(info);
                    break;
                  case eoq2.action.CallStatus.ERR:
                    this.UnobserveCall();
                    this.onFailedCallback(info);
                    break;
                  default:
                    break; //nothing
                }
              }
              break;
            case eoq2.event.EvtTypes.CVA:
              {
                let res = evt.a[1];
                this.onResultCallback(res);
              }
              break;
          }
        }
      }
    };

    ActionRunner.prototype.__FindProgressFromText = function (text) {
      let progressStrs = text.match(/\s\d+\%/g);
      if (progressStrs) {
        let progressStr = progressStrs[progressStrs.length - 1]; //last element
        let progressNumberStr = progressStr.substring(1, progressStr.length - 1); //ommit the trailing %
        this.processStatus.taskPercentage = Number.parseInt(progressNumberStr);
        return true;
      }
      return false;
    };

    ActionRunner.prototype.Abort = function () {
      //var cmd = jseoq.CommandParser.StringToCommand("ABORTCALL "+this.callId);
      let cmd = new eoq2.Abc(this.callId);
      let self = this;
      this.domain
        .RawDo(cmd)
        .then(function (res) {
          let val = eoq2.ResGetValue(res); //val is not used, but ResGetVal triggeres exception if cmd failed
          //nothing more to do the progress task will to the rest.
        })
        .catch(function (e) {
          self.onFailedCallback(e);
          throw e; //abort failed
        });
      return this;
    };

    return {
      FetchActions: FetchActions,
      ActionRunner: ActionRunner,
    };
  })(),
);
