'use strict';

(({ App, Backbone, _ }) => {
  const { Inspector } = App;

  const LOOP_VARIABLES = [
    'CYCLE_INDEX',
    'FOREACH_DATA',
  ];

  const ERROR_VARIABLES = [
    'ERROR_ID',
    'WAS_ERROR',
    'LAST_ERROR',
  ];

  const IGNORED_ACTION_NAMES = [
    'custom',
    'browserscript',
    'onloadjavascript',
    'browserjavascript',
    'EmbeddedLanguagesNodeJs',
    'user_notification_web_interface_eval',
  ];

  Inspector.History = class History {
    #items = [];

    #limit = 90;

    #nextId = 0;

    #disable = false;

    #fnVariable = "";

    #historyItem = "";

    #prevCallstack = [];

    reset() {
      this.#items = [];
    }

    setLimit(value) {
      this.#limit = value;
    }

    setNextId(value) {
      this.#nextId = value;
    }

    findItemById(id = 0) {
      return this.#items.find((item) => {
        return item.id === id;
      });
    }

    removeItemById(id = 0) {
      const index = this.#items.findIndex((item) => {
        return item.id === id;
      });

      if (index >= 0) {
        this.#items.splice(index, 1);
      }
    }

    disableHistory() {
      this.#disable = true;
    }

    enableHistory() {
      this.#disable = false;
    }

    addOrUpdateItem(id, entries = {}, was_error = false) {
      const changedIds = new Set();
      const item = this.findItemById(id);

      if (item) {
        const oldSerialized = JSON.stringify(item.entries);
        const oldWasError = item.was_error;

        item.was_error = was_error;
        Object.assign(item.entries, entries);
        if (oldSerialized != JSON.stringify(item.entries) || oldWasError != was_error) {
          changedIds.add(item.id);
        }
      } else {
        this.#items.push({ id, entries, wrap: false, was_error });
        changedIds.add(id);
        if (this.#items.length > this.#limit) {
          changedIds.add(this.#items[0].id);
          this.#items.shift();
        }
      }

      return changedIds;
    }

    setCurrentHistoryItem(item) {
      this.#historyItem = item;
    }    

    generateContextMenu() {
      const self = this;
      return [
        {text: tr('history.context.toggle'), action: function(e){
          e.preventDefault();
          
          const el = document.getElementById(self.#historyItem);
          const root = el.closest('.tool-margin');
          const id = parseInt(root.getAttribute('task-id'));
          const item = _Inspector.history.findItemById(id);

          item.wrap = !el.classList.contains('wrap');
          el.classList.toggle('wrap');
          $(el).updateVirtualDOM();
          _VisualizeLabels.Visualize();
        }},
        {text: tr('history.context.removethis'), action: function(e){
          e.preventDefault();

          const el = document.getElementById(self.#historyItem);
          const root = el.closest('.tool-margin');
          const id = parseInt(root.getAttribute('task-id'));
          self.removeItemById(id);

          _Renderer.RenderVariablesHistoryContainer(id)

          _VisualizeLabels.Visualize();

        }},
        {text: tr('history.context.removeall'), action: function(e){
          e.preventDefault();

          const changedIds = new Set(self.#items.map((item) => item.id));

          self.reset();

          for(let id of changedIds) {
            _Renderer.RenderVariablesHistoryContainer(id)
          }

          _VisualizeLabels.Visualize();
        }},
        {text: tr('history.context.disable'), action: function(e){
          e.preventDefault();

          const changedIds = new Set(self.#items.map((item) => item.id));

          self.disableHistory();

          self.reset();

          for(let id of changedIds) {
            _Renderer.RenderVariablesHistoryContainer(id)
          }

          _VisualizeLabels.Visualize();
        }},
      ]
    }

    /*
     * The `internal` trace collected only for the last actions inside the single block.
     * In order to do this, the `_trace_variable_internal` method is called inside the `_next_or_section` call.
     * 
     * This is necessary to correctly handle situations where the last action in a block modifies a variable associated with
     * that block - for example `WAS_ERROR` for ignore errors or `CYCLE_INDEX` for any cycle.
     * 
     * The `_trace_variable_internal` method internally remembers the state of such variables at the moment of exiting the loop,
     * so that it can be sent later along with the regular trace - without this, we will not be able to get the actual values.
     * 
     * On the interface side, we handle each situation separately:
     * 
     * 1. For the "Ignore Errors" block we should check each special variable - if its
     * value taken from the internal trace differs from the current ones, and the action code strictly
     * contains this variable, then we must add an entry to a separate list and delete the original key.
     * 
     * 2. A similar approach is used for cycles, but there we follow the conditions of the old logic - we perform
     * the necessary operations only if there were changes, while using a default variables list for checking.
     * 
     * At the moment, this approach has become the most obvious and simple.
    */
    update({ diff, internal, callstack, variables, was_fail } = {}) {
      const { getVariablesList } = Inspector.utils;

      const prevCallstack = this.#prevCallstack;
      this.#prevCallstack = callstack;
      const changedIds = new Set();

      const id = this.#nextId;
      this.#nextId = 0;

      if (id && diff && variables && App.settings.trace && !this.#disable) {
        if (was_fail) {
          return this.addOrUpdateItem(id, {}, was_fail);
        }

        const strictVariablesList = getVariablesList(id, true);
        const variablesList = [...strictVariablesList, ERROR_VARIABLES];
        if (!variablesList.length) return changedIds;

        const externalItems = [];
        const task = FindTaskById(id);
        const dat = task ? task.dat() : null;
        const callstackIds = callstack.map((item) => item.id);
        // Add all variables that the current action outputs:
        const keys = new Set(
          variablesList.filter((key) => {
            // Drop all special variables that have not changed:
            if (ERROR_VARIABLES.includes(key)) {
              return diff.variables.some(({ type, path }) => {
                return (
                  path[0] === key && (type === 'CHANGE' || (type === 'CREATE' && typeof variables[key] !== 'undefined'))
                );
              });
            } else if (typeof variables[key] !== 'undefined') {
              return true;
            }

            return false;
          })
        );

        if (dat && dat.s) {
          if (dat.s === 'executefunction') {
            // Preserve the variable returned by the function:
            const item = dat.d.find((item) => item.type === 'var');
            if (item) this.#fnVariable = item.data;
          } else if (dat.s === 'return') {
            // Process the function results:
            if (this.#fnVariable && callstack.length < prevCallstack.length) {
              const cycle = prevCallstack.find(
                ({ id, name, type }) =>
                  name !== 'Main' &&
                  type === 'function' &&
                  !callstackIds.includes(id) &&
                  getVariablesList(id).includes(this.#fnVariable)
              );

              // If the change was caused by returning the function result, then
              // we forcefully update the action that called this function:
              if (cycle) {
                externalItems.push({ id: cycle.id, name: this.#fnVariable });
                this.#fnVariable = '';
              }
            }
          } else if (dat.s === 'if') {
            // Process the "If" statements:
            externalItems.push({ id, name: '', value: callstackIds.includes(id) });
          }
        }

        // Process the internal trace for the "Ignore Errors" block:
        if (internal && internal.id === id) {
          for (const key of ERROR_VARIABLES) {
            if (
              key in internal.variables &&
              strictVariablesList.includes(key) &&
              variables[key] !== internal.variables[key]
            ) {
              externalItems.push({ id, name: key, value: internal.variables[key] });
            }
          }
        }

        for (const { path, type } of diff.variables) {
          // Process the "CYCLE_INDEX" and "FOREACH_DATA" variables.
          // If the "CYCLE_INDEX" or "FOREACH_DATA" has been actually changed and if
          // this action didn't do this, search for any ancestor loop action that did this:
          if (
            LOOP_VARIABLES.includes(path[0]) &&
            (type === 'CHANGE' || (type === 'CREATE' && typeof variables[path[0]] !== 'undefined'))
          ) {
            // Process internal trace for any cycle block:
            if (variablesList.includes(path[0])) {
              if (
                internal &&
                internal.id === id &&
                path[0] in internal.variables &&
                variables[path[0]] !== internal.variables[path[0]]
              ) {
                externalItems.push({ id, name: path[0], value: internal.variables[path[0]] });
              } else {
                continue;
              }
            }

            // Search for any ancestor cycle of the action that did this:
            let currentId = id;
            while (true) {
              const task = FindTaskById(currentId);
              if (!task) break;

              const parentId = parseInt(task.get('parentid'));
              if (parentId === 0) break;

              // We should update the variable for the parent, but only if the
              // loop is still on the stack, meaning the block has not yet exited:
              if (callstackIds.includes(parentId) && getVariablesList(parentId).includes(path[0])) {
                externalItems.push({ id: parentId, name: path[0] });
                break;
              }

              currentId = parentId;
            }
          } else if (callstack.length < prevCallstack.length) {
            // Process the exit from the function (or from any other block):
            const cycle = prevCallstack.find(
              ({ id, name, type }) =>
                type === 'action' &&
                name === 'Ignore Errors' &&
                !callstackIds.includes(id) &&
                ERROR_VARIABLES.includes(path[0])
            );

            // If the change was caused by exiting a try/catch block, then we
            // should move all changes to the corresponding "Ignore Errors" action:
            if (cycle) {
              externalItems.push({ id: cycle.id, name: path[0] });
            }
          }
        }

        for (const externalItem of externalItems) {
          const { id, name, value } = externalItem;
          // External keys should be removed
          // from the original keys set:
          keys.delete(name);

          const entries = { [name]: 'value' in externalItem ? value : variables[name] };
          for (const changedId of this.addOrUpdateItem(id, entries, was_fail)) {
            changedIds.add(changedId);
          }
        }

        const entries = Object.fromEntries(Array.from(keys, (key) => [key, variables[key]]));
        for (const changedId of this.addOrUpdateItem(id, entries, was_fail)) {
          changedIds.add(changedId);
        }
      }

      return changedIds;
    }
  };

  Inspector.View = Backbone.View.extend({
   history: new Inspector.History(),

   async showPreview($ref, $top, title, title_raw, forceShowTrivial) {
      const { name } = $ref.dataset;
      const $tool = $ref.closest('.tool');
      if (!$tool) return;
      const id = parseInt($tool.getAttribute('task-id'));

      const task = FindTaskById(id);
      if (!task) return;

      const dat = task.dat();
      if (!dat) return;

      const propData = BrowserAutomationStudio_GetPropData(dat, name);
      if (!propData || IGNORED_ACTION_NAMES.includes(dat.s) || (propData.id === 'FunctionName' &&!forceShowTrivial)) return;

      //Block preview for trivial values
      if (!forceShowTrivial && ['check', 'select'].includes(propData.type)) {
        return
      }



      let { type, data: value } = propData;
      const { utils, showPreview } = Inspector;

      if (['text', 'check', 'select'].includes(type)) {
        showPreview($ref, $top, { title, value, type: 'json', calculateNoTruncate: null });
      } else if (name === 'PATH' && propData.id === 'ImageBase64') {
        showPreview($ref, $top, { title: tr('Selector (image)'), value, type: 'image', calculateNoTruncate: null });
      } else {
        
        let expression = "";

        //If there are several vars, evaluate as array
        if (
          type === 'var' &&
          value.includes(",")
        ) {
          expression = "[ " + value.split(",").map(x => "[[" + x.trim() + "]]").join(",") + " ]";
        } else if (type === 'filter') {
          var logicEll = BrowserAutomationStudio_GetPropData(dat, `filter-logic-select-${name}`);
          var logicMode = logicEll ? logicEll.data : "and";
          expression = generate_filter_template(value, logicMode);
          propData.class = 'string'
        } else
        {
          expression = (type === 'var' && propData.id != "ResourceName") ? `[[${dat.s === 'globalset' ? 'GLOBAL:' : ''}${value}]]` : value
        }

        //Treat template action text param as string
        try{
          if (
            dat.s === 'template' &&
            propData.id == "Template"
          ) {
            propData.class = 'string'
          }
        }catch(e) {

        }

        try{
          if (
            propData.id == "ResourceName"
          ) {
            propData.class = 'string'
          }
        }catch(e) {

        }

        const params = { timeout: 200, isVariable: type === 'var', isString: propData.class === 'string', noTruncate: false }

        const { result, error, wasTruncated } = await utils.executeExpression(
          expression,
          params
        );

        //Block preview for trivial values
        try{
          if (
            !forceShowTrivial && 
            ['string', 'check', 'select', 'int'].includes(propData.class) &&
            !title.includes(`class="param-omission"`) &&
            (result).toString() === value
          ) {
            return
          }
        }catch(e) {

        }

        if (
          !forceShowTrivial && 
          (propData.id == 'use-waiter' || propData.id == 'use-timeout' || propData.id == 'checkbox-wait-element' || propData.id == 'checkbox-fail-action')
        ) {
          return
        }

        let calculateNoTruncate = null;
        if(wasTruncated) {
          calculateNoTruncate = async () => {
            params.noTruncate = true
            params.timeout = 10000
            return await utils.executeExpression(
              expression,
              params
            )   
          }
        }

        showPreview($ref, $top, {
          title,
          value:
            (error === 'INVALID_EXPRESSION'
              ? tr('Preview is not available for code that can change variable values')
              : error) || result,
          type: error ? 'error' : 'json',
          calculateNoTruncate
        });
      }
    },

    initialize() {
      let attached = false;

      window.addEventListener('message', ({ data: { type, payload } }) => {
        // prettier-ignore
        switch (type) {
          case 'focusAction': return BrowserAutomationStudio_FocusAction(payload.id);
          case 'confirm': return confirm(payload);
          case 'prompt': return prompt(payload);
          case 'edit': return edit(payload);
          case 'hide': return this.hide();
          case 'show': return this.show();
          case 'get': return get(payload);
          default: {
            if (type === 'destroyed') {
              attached = false;
            }
            if (type === 'mounted') {
              attached = true;
            }
            this.trigger(type, payload);
          }
        }
      });

      const confirm = ({ message }) => {
        bootbox.confirm(message, (result) => {
          this.send({ type: 'confirm', payload: { result } });
        });
      };

      const prompt = ({ message }) => {
        bootbox.prompt(message, (result) => {
          this.send({ type: 'prompt', payload: { result } });
        });
      };

      const get = ({ path }) => {
        request(path).then((value) => {
          this.send({ type: 'get', payload: { value } });
        });
      };

      this.send = async (message) => {
        if (!attached) {
          await new Promise((resolve) => this.once('mounted', resolve));
        }

        const changedIds = new Set();

        if (message.type === 'update') {
          if(typeof(message.payload) === 'object' && typeof(message.payload.trace_data) === "object" && message.payload.trace_data !== null) {
            for(const [key, value] of message.payload.trace_data) {
              try {
                const task = FindTaskById(key);
                // We should skip updating the history for disabled tasks:
                if (task && task.get('donotexecuteduringrecord')) continue;

                this.history.setNextId(parseInt(key))
                for (const changedId of this.history.update({ ...value, internal: message.payload.trace_internal })) {
                  changedIds.add(changedId);
                }
              } catch (e) {
                console.error('Unable to update history:', e);
              }  
            }
          }

          for (const key of ['trace_data', 'trace_internal']) {
            try {
              delete message.payload[key];
            } catch (e) {
              console.error('Unable to delete record:', e);
            }
          }
        }

        if(changedIds.size > 0) {
          for(let id of changedIds) {
            _Renderer.RenderVariablesHistoryContainer(id)
          }

          _VisualizeLabels.Visualize();
        }

        this.$('iframe')[0].contentWindow.postMessage(message, '*');
      };
    },

    render() {
      if (!this.el.isConnected) {
        const { body } = document;

        this.setElement('#inspector').$el.resizable({
          onDragStart(e, ...args) {
            body.style.cursor = e.type === 'mouseup' ? '' : 'ns-resize';
            BrowserAutomationStudio_PreserveInterfaceState(true);
          },
          onDragEnd(e, ...args) {
            body.style.cursor = e.type === 'mouseup' ? '' : 'ns-resize';
            BrowserAutomationStudio_PreserveInterfaceState(true);
          },
          onDrag(e, ...args) {
            const height = Math.min(args[2], window.innerHeight - 300);
            return args[0].css('height', Math.max(110, height)), false;
          },
          handleSelector: '.handle',
        });

        new ResizeObserver(() => this.calc()).observe(this.el);
        BrowserAutomationStudio_AskForVariablesUpdate();
      }
      return this;
    },

    toggle() {
      _FunctionManager.Hide();
      if (this.$el.is(':visible')) {
        this.hide();
      } else {
        this.show();
      }
      return this;
    },

    state() {
      return {
        visible: this.$el.is(':visible'),
        height: this.$el.css('height'),
      };
    },

    wait() {
      clearTimeout(this.timeout);

      this.timeout = setTimeout(() => {
        if (arguments[0]) {
          this.$('.loader').show();
        } else {
          this.$('.loader').hide();
        }
      }, 150);
    },

    show() {
      this.$el.show();
      BrowserAutomationStudio_PreserveInterfaceState(true);
      return this;
    },

    hide() {
      this.$el.hide();
      BrowserAutomationStudio_PreserveInterfaceState(true);
      return this;
    },

    calc() {
      const height = this.$el.is(':visible') && this.$el.height();
      $('.main').css('padding-bottom', `${height + 50 + 0}px`);
    },
  });

  function edit(options) {
    if (document.querySelector('.modal.in')) return;

    Promise.resolve(options.mode === 'variables' ? request(options.path) : options.value).then((value) => {
      const callback = (accept, { changed, value, type }) => {
        if (accept && changed) {
          let [root, ...path] = options.path;
          path = path.map((k) => `[${JSON.stringify(k)}]`).join('');

          _.attempt(() => {
            if (type === 'date') {
              value = `_parse_date("${value}", "auto")`;
            } else if (type === 'script') {
              value = JSON.stringify(eval(`(${value})`));
            } else if (type === 'string') {
              value = JSON.stringify(value);
            }

            BrowserAutomationStudio_ExecuteSyncCode(`
              (function (root, value) {
                try {
                  if (root.indexOf("GLOBAL:") === 0) {
                    root = root.slice(7);
                    var obj = JSON.parse(P("basglobal", root) || "{}");
                    obj${path} = value;
                    PSet("basglobal", root, JSON.stringify(obj));
                  } else {
                    GLOBAL["VAR_" + root]${path} = value;
                  }
                } catch (e) {}
              })(${JSON.stringify(root)}, ${value});
              _execute_sync_result("-BAS-EXECUTESYNC-ID-", "")
            `).then(() =>{
              BrowserAutomationStudio_AskForVariablesUpdate();
            });
          });
        }
      };

      return new Inspector.Modal({ ...options, callback, value }).render();
    });
  }

  function request(path) {
    return new Promise((resolve) => {
      const pointer = JSON.stringify(path);
      const code = `
          try {
            _execute_sync_result("-BAS-EXECUTESYNC-ID-", request_variables(${JSON.stringify(pointer)}))
          }catch (e) {
            _execute_sync_result("-BAS-EXECUTESYNC-ID-", {})
          }
        `

      BrowserAutomationStudio_ExecuteSyncCode(code).then(({ result }) =>{
        resolve(App.utils.parseJSON(result)[pointer])
      } )

    });
  }
})(window);
