import _ from 'lodash';
import { push } from 'connected-react-router';
import { getLocators, APP_MODE } from '../components/Inspector/shared';
import {
  showError
} from './Session';
import {xmlToJSON} from '../util';
import { v4 as UUID } from 'uuid';
import frameworks from '../lib/client-frameworks';
import {getSetting, setSetting, SAVED_FRAMEWORK, SAVE_GLOBAL_VARS} from '../../shared/settings';
import i18n from '../../configs/i18next.config.renderer';
import AppiumClient, { NATIVE_APP } from '../lib/appium-client';
import { notification } from 'antd';
import {ipcRenderer} from 'electron';
import JsWdIoFramework from '../lib/client-frameworks/js-wdio';

export const SET_SESSION_DETAILS = 'SET_SESSION_DETAILS';
export const SET_SOURCE_AND_SCREENSHOT = 'SET_SOURCE_AND_SCREENSHOT';
export const SESSION_DONE = 'SESSION_DONE';
export const SELECT_ELEMENT = 'SELECT_ELEMENT';
export const UNSELECT_ELEMENT = 'UNSELECT_ELEMENT';
export const SET_SELECTED_ELEMENT_ID = 'SET_SELECTED_ELEMENT_ID';
export const SET_INTERACTIONS_NOT_AVAILABLE = 'SET_INTERACTIONS_NOT_AVAILABLE';
export const METHOD_CALL_REQUESTED = 'METHOD_CALL_REQUESTED';
export const METHOD_CALL_DONE = 'METHOD_CALL_DONE';
export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export const SET_EXPANDED_PATHS = 'SET_EXPANDED_PATHS';
export const SELECT_HOVERED_ELEMENT = 'SELECT_HOVERED_ELEMENT';
export const UNSELECT_HOVERED_ELEMENT = 'UNSELECT_HOVERED_ELEMENT';
export const SELECT_HOVERED_CENTROID = 'SELECT_HOVERED_CENTROID';
export const UNSELECT_HOVERED_CENTROID = 'UNSELECT_HOVERED_CENTROID';
export const SELECT_CENTROID = 'SELECT_CENTROID';
export const UNSELECT_CENTROID = 'UNSELECT_CENTROID';
export const SET_SHOW_CENTROIDS = 'SET_SHOW_CENTROIDS';
export const SHOW_SEND_KEYS_MODAL = 'SHOW_SEND_KEYS_MODAL';
export const HIDE_SEND_KEYS_MODAL = 'HIDE_SEND_KEYS_MODAL';
export const SHOW_REGEX_VALUE_MODAL = 'SHOW_REGEX_VALUE_MODAL';
export const HIDE_REGEX_VALUE_MODAL = 'HIDE_REGEX_VALUE_MODAL';
export const SHOW_ASSERT_VALUE_MODAL = 'SHOW_ASSERT_VALUE_MODAL';
export const HIDE_ASSERT_VALUE_MODAL = 'HIDE_ASSERT_VALUE_MODAL';
export const QUIT_SESSION_REQUESTED = 'QUIT_SESSION_REQUESTED';
export const QUIT_SESSION_DONE = 'QUIT_SESSION_DONE';
export const SET_SESSION_TIME = 'SET_SESSION_TIME';

export const START_RECORDING = 'START_RECORDING';
export const PAUSE_RECORDING = 'PAUSE_RECORDING';
export const CLEAR_RECORDING = 'CLEAR_RECORDING';
export const INSERT_RECORD_CONTENT_WITH_ACTION = 'INSERT_RECORD_CONTENT_WITH_ACTION';
export const CLOSE_RECORDER = 'CLOSE_RECORDER';
export const SET_ACTION_FRAMEWORK = 'SET_ACTION_FRAMEWORK';
export const RECORD_ACTION = 'RECORD_ACTION';
export const SET_RECORD_CONTENT = 'SET_RECORD_CONTENT';
export const SET_RECORD_CURSOR = 'SET_RECORD_CURSOR';
export const SET_SHOW_BOILERPLATE = 'SET_SHOW_BOILERPLATE';

export const SHOW_LOCATOR_TEST_MODAL = 'SHOW_LOCATOR_TEST_MODAL';
export const HIDE_LOCATOR_TEST_MODAL = 'HIDE_LOCATOR_TEST_MODAL';
export const SET_LOCATOR_TEST_STRATEGY = 'SET_LOCATOR_TEST_STRATEGY';
export const SET_LOCATOR_TEST_VALUE = 'SET_LOCATOR_TEST_VALUE';
export const SEARCHING_FOR_ELEMENTS = 'SEARCHING_FOR_ELEMENTS';
export const SEARCHING_FOR_ELEMENTS_COMPLETED = 'SEARCHING_FOR_ELEMENTS_COMPLETED';
export const GET_FIND_ELEMENTS_TIMES = 'GET_FIND_ELEMENTS_TIMES';
export const GET_FIND_ELEMENTS_TIMES_COMPLETED = 'GET_FIND_ELEMENTS_TIMES_COMPLETED';
export const SET_LOCATOR_TEST_ELEMENT = 'SET_LOCATOR_TEST_ELEMENT';
export const CLEAR_SEARCH_RESULTS = 'CLEAR_SEARCH_RESULTS';
export const ADD_ASSIGNED_VAR_CACHE = 'ADD_ASSIGNED_VAR_CACHE';
export const CLEAR_ASSIGNED_VAR_CACHE = 'CLEAR_ASSIGNED_VAR_CACHE';
export const SET_SCREENSHOT_INTERACTION_MODE = 'SET_SCREENSHOT_INTERACTION_MODE';
export const SET_APP_MODE = 'SET_APP_MODE';
export const SET_SEARCHED_FOR_ELEMENT_BOUNDS = 'SET_SEARCHED_FOR_ELEMENT_BOUNDS';
export const CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS = 'CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS';

export const SET_SWIPE_START = 'SET_SWIPE_START';
export const SET_SWIPE_END = 'SET_SWIPE_END';
export const CLEAR_SWIPE_ACTION = 'CLEAR_SWIPE_ACTION';
export const PROMPT_KEEP_ALIVE = 'PROMPT_KEEP_ALIVE';
export const HIDE_PROMPT_KEEP_ALIVE = 'HIDE_PROMPT_KEEP_ALIVE';

export const SELECT_INTERACTION_MODE = 'SELECT_INTERACTION_MODE';

export const SELECT_ACTION_GROUP = 'SELECT_ACTION_GROUP';
export const SELECT_SUB_ACTION_GROUP = 'SELECT_SUB_ACTION_GROUP';

export const ENTERING_ACTION_ARGS = 'ENTERING_ACTION_ARGS';
export const REMOVE_ACTION = 'REMOVE_ACTION';
export const SET_ACTION_ARG = 'SET_ACTION_ARG';

export const SET_CONTEXT = 'SET_CONTEXT';

export const SET_APP_ID = 'SET_APP_ID';
export const SET_SERVER_STATUS = 'SET_SERVER_STATUS';

export const SET_KEEP_ALIVE_INTERVAL = 'SET_KEEP_ALIVE_INTERVAL';
export const SET_USER_WAIT_TIMEOUT = 'SET_USER_WAIT_TIMEOUT';
export const SET_LAST_ACTIVE_MOMENT = 'SET_LAST_ACTIVE_MOMENT';

export const SET_VISIBLE_COMMAND_RESULT = 'SET_VISIBLE_COMMAND_RESULT';

export const SET_AWAITING_MJPEG_STREAM = 'SET_AWAITING_MJPEG_STREAM';

export const SHOW_GESTURE_EDITOR = 'SHOW_GESTURE_EDITOR';
export const HIDE_GESTURE_EDITOR = 'HIDE_GESTURE_EDITOR';
export const SET_SAVED_GESTURES = 'SET_SAVED_GESTURES';
export const GET_SAVED_GESTURES_REQUESTED = 'GET_SAVED_GESTURES_REQUESTED';
export const GET_SAVED_GESTURES_DONE = 'GET_SAVED_GESTURES_DONE';
export const DELETE_SAVED_GESTURES_REQUESTED = 'DELETE_SAVED_GESTURES_REQUESTED';
export const DELETE_SAVED_GESTURES_DONE = 'DELETE_SAVED_GESTURES_DONE';
export const SET_LOADED_GESTURE = 'SET_LOADED_GESTURE';
export const REMOVE_LOADED_GESTURE = 'REMOVE_LOADED_GESTURE';
export const SHOW_GESTURE_ACTION = 'SHOW_GESTURE_ACTION';
export const HIDE_GESTURE_ACTION = 'HIDE_GESTURE_ACTION';
export const SELECT_TICK_ELEMENT = 'SELECT_TICK_ELEMENT';
export const UNSELECT_TICK_ELEMENT = 'UNSELECT_TICK_ELEMENT';
export const SET_GESTURE_TAP_COORDS_MODE = 'SET_GESTURE_TAP_COORDS_MODE';
export const CLEAR_TAP_COORDINATES = 'CLEAR_TAP_COORDINATES';

export const TOGGLE_SHOW_ATTRIBUTES = 'TOGGLE_SHOW_ATTRIBUTES';

export const SAVE_EXECUTE_SCRIPT_APPEAR = 'SAVE_EXECUTE_SCRIPT_APPEAR';

export const SAVE_EXECUTE_SCRIPT = 'SAVE_EXECUTE_SCRIPT';

export const SAVED_TEXT = 'SAVED_TEXT';

export const SAVE_APP_DATA = 'SAVE_APP_DATA';

export const SHOW_TESTSUITE_GROUP = 'SHOW_TESTSUITE_GROUP';

export const SELECT_TESTSUIT_ID = 'SELECT_TESTSUIT_ID';

export const SELECT_ENV_ID = 'SELECT_ENV_ID';

export const SET_TEST_CAPABILITY_PARAM = 'SET_TEST_CAPABILITY_PARAM';

export const ADD_TEST_CAPABILITY = 'ADD_TEST_CAPABILITY';

export const REMOVE_TEST_CAPABILITY = 'REMOVE_TEST_CAPABILITY';

export const INSERT_TEST_CASE_APPEAR = 'INSERT_TEST_CASE_APPEAR';

export const SAVE_REQUITE_VARS = 'SAVE_REQUITE_VARS';

export const SHOW_GLOBAL_VARS_MODAL = 'SHOW_GLOBAL_VARS_MODAL';


const KEEP_ALIVE_PING_INTERVAL = 5 * 1000;
const NO_NEW_COMMAND_LIMIT = 24 * 60 * 60 * 1000; // Set timeout to 24 hours
const WAIT_FOR_USER_KEEP_ALIVE = 60 * 60 * 1000; // Give user 1 hour to reply

// A debounced function that calls findElement and gets info about the element
const findElement = _.debounce(async function (strategyMap, dispatch, getState, path) {
  for (let [strategy, selector] of strategyMap) {
    // Get the information about the element
    const action = callClientMethod({
      strategy,
      selector,
    });
    let {elementId, variableName, variableType} = await action(dispatch, getState);

    // Set the elementId, variableName and variableType for the selected element
    // (check first that the selectedElementPath didn't change, to avoid race conditions)
    if (elementId && getState().inspector.selectedElementPath === path) {
      return dispatch({type: SET_SELECTED_ELEMENT_ID, elementId, variableName, variableType});
    }
  }

  return dispatch({type: SET_INTERACTIONS_NOT_AVAILABLE});
}, 1000);


function insertRecordContent (dispatch, getState) {
  function getLinesBeforeLength (str, lineCount) {
    // 使用正则表达式将字符串按换行符分割成行数组
    const lines = str.split(/\r\n|\r|\n/);
    // 使用 slice 方法截取指定行数之前的部分
    const linesBefore = lines.slice(0, lineCount);
    // 使用 join 方法将截取的行数组重新连接成一个字符串
    const result = linesBefore.join('\n');
    // 返回截取字符串的长度
    return result.length;
  }
  function getInsertIndex (source, data) {
    if (data == null) {
      return 0;
    }
    return getLinesBeforeLength(source, data.line + 1);
  }
  function getLineCount (str) {
    // 使用正则表达式将字符串按换行符分割成行数组
    const lines = str.split(/\r\n|\r|\n/);
    // 返回行数组的长度，即为字符串的行数
    return lines.length;
  }
  function rawCode (recordedActions) {
    let framework = new JsWdIoFramework();
    let code = framework.getActionCodeString(recordedActions);
    return code;
  }
  function insertStr (source, data, newStr) {
    if (data == null) {
      // 没有坐标数据默认在尾部插入
      return source + newStr;
    } else {
      const start = getInsertIndex(source, data);
      return source.slice(0, start) + newStr + source.slice(start);
    }
  }
  // 获取除it外的新坐标
  function getNormalCursorData (code, lastCursorData) {
    if (lastCursorData == null) {
      return { line: 0, ch: 0 };
    }
    const codeLine = getLineCount(code);
    return {line: lastCursorData.line + codeLine - 1, ch: 0};
  }
  const state = getState().inspector;
  const recordedActions = state.recordedActions;
  const code = rawCode(recordedActions);
  if (recordedActions.map((element) => element.action).indexOf('insertIt') === -1) {
    const cursorData = getNormalCursorData(code, state.recordCursorData);
    console.log(cursorData);
    const totalCode = insertStr(state.recordContent, state.recordCursorData, code);
    dispatch({type: INSERT_RECORD_CONTENT_WITH_ACTION, totalCode});
    setTimeout(() => {
      dispatch({type: SET_RECORD_CURSOR, data: cursorData});
    }, 300);
  } else {
    const cursorData = {line: getLineCount(state.recordContent) + 1, ch: 0};
    console.log(cursorData);
    const totalCode = insertStr(state.recordContent, null, code);
    dispatch({type: INSERT_RECORD_CONTENT_WITH_ACTION, totalCode});
    setTimeout(() => {
      dispatch({type: SET_RECORD_CURSOR, data: cursorData});
    }, 300);
  }
}
// function resetCursor (recordedActions) {
//   if (recordedActions.map((element) => element.action).indexOf('insertIt') > -1) {
//
//   }
//   return {line: 0, ch: 0};
// }

export function selectElement (path) {
  return async (dispatch, getState) => {
    // Set the selected element in the source tree
    dispatch({type: SELECT_ELEMENT, path});
    const state = getState().inspector;
    const {attributes: selectedElementAttributes, xpath: selectedElementXPath} = state.selectedElement;
    const {sourceXML} = state;

    // Expand all of this element's ancestors so that it's visible in the source tree
    let {expandedPaths} = getState().inspector;
    let pathArr = path.split('.').slice(0, path.length - 1);
    while (pathArr.length > 1) {
      pathArr.splice(pathArr.length - 1);
      let path = pathArr.join('.');
      if (expandedPaths.indexOf(path) < 0) {
        expandedPaths.push(path);
      }
    }
    dispatch({type: SET_EXPANDED_PATHS, paths: expandedPaths});


    // Find the optimal selection strategy. If none found, fall back to XPath.
    const strategyMap = _.toPairs(getLocators(selectedElementAttributes, sourceXML));
    strategyMap.push(['xpath', selectedElementXPath]);

    // Debounce find element so that if another element is selected shortly after, cancel the previous search
    await findElement(strategyMap, dispatch, getState, path);
  };
}

export function unselectElement () {
  return (dispatch) => {
    dispatch({type: UNSELECT_ELEMENT});
  };
}


export function selectCentroid (path) {
  return (dispatch) => {
    dispatch({type: SELECT_CENTROID, path});
  };
}

export function unselectCentroid () {
  return (dispatch) => {
    dispatch({type: UNSELECT_CENTROID});
  };
}

export function selectHoveredCentroid (path) {
  return (dispatch) => {
    dispatch({type: SELECT_HOVERED_CENTROID, path});
  };
}

export function unselectHoveredCentroid () {
  return (dispatch) => {
    dispatch({type: UNSELECT_HOVERED_CENTROID});
  };
}

export function selectHoveredElement (path) {
  return (dispatch) => {
    dispatch({type: SELECT_HOVERED_ELEMENT, path});
  };
}

export function unselectHoveredElement (path) {
  return (dispatch) => {
    dispatch({type: UNSELECT_HOVERED_ELEMENT, path});
  };
}

/**
 * Requests a method call on appium
 */
export function applyClientMethod (params) {
  return async (dispatch, getState) => {
    const isRecording = params.methodName !== 'quit' &&
                      params.methodName !== 'getPageSource' &&
                      params.methodName !== 'gesture' &&
                      getState().inspector.isRecording;
    try {
      dispatch({type: METHOD_CALL_REQUESTED});
      const callAction = callClientMethod(params);
      let {contexts, contextsError, commandRes, currentContext, currentContextError,
           source, screenshot, windowSize, sourceError,
           screenshotError, windowSizeError, variableName,
           variableIndex, strategy, selector} = await callAction(dispatch, getState);
      if (params.selector !== undefined) {
        selector = params.selector;
      }
      // TODO: Implement recorder code for gestures
      if (isRecording) {
        // Add 'findAndAssign' line of code. Don't do it for arrays though. Arrays already have 'find' expression
        if (strategy && selector && !variableIndex && variableIndex !== 0) {
          const findAction = findAndAssign(strategy, selector, variableName, false);
          findAction(dispatch, getState);
        }

        // now record the actual action
        let args = [variableName, variableIndex];
        args = args.concat(params.args || []);
        dispatch({type: RECORD_ACTION, action: params.methodName, params: args });
        insertRecordContent(dispatch, getState);
      }
      dispatch({type: METHOD_CALL_DONE});

      if (source) {
        dispatch({
          type: SET_SOURCE_AND_SCREENSHOT,
          contexts,
          currentContext,
          source: source && xmlToJSON(source),
          sourceXML: source,
          screenshot,
          windowSize,
          contextsError,
          currentContextError,
          sourceError,
          screenshotError,
          windowSizeError,
        });
      }
      window.dispatchEvent(new Event('resize'));
      return commandRes;
    } catch (error) {
      console.log(error); // eslint-disable-line no-console
      let methodName = params.methodName === 'click' ? 'tap' : params.methodName;
      showError(error, methodName, 10);
      dispatch({type: METHOD_CALL_DONE});
    }
  };
}

export function addAssert (params) {
  return (dispatch, getState) => {
    const localVar = `assertEl${getState().inspector.assertVarCount}`;
    const findAction = findAndAssign(params.accessibilityId ? 'accessibility id' : 'xpath', params.accessibilityId ? params.accessibilityId : params.xpath, localVar, false);
    findAction(dispatch, getState);
    let args = [localVar, params.expectedTextType, params.expectedText];
    dispatch({type: RECORD_ACTION, action: 'addAssert', params: args});
    insertRecordContent(dispatch, getState);
  };
}

export function addAssignedVarCache (varName) {
  return (dispatch) => {
    dispatch({type: ADD_ASSIGNED_VAR_CACHE, varName});
  };
}

export function showSendKeysModal () {
  return (dispatch) => {
    dispatch({type: SHOW_SEND_KEYS_MODAL});
  };
}

export function hideSendKeysModal () {
  return (dispatch) => {
    dispatch({type: HIDE_SEND_KEYS_MODAL});
  };
}

export function showRegexValueModal () {
  return (dispatch) => {
    dispatch({type: SHOW_REGEX_VALUE_MODAL});
  };
}

export function hideRegexValueModal () {
  return (dispatch) => {
    dispatch({type: HIDE_REGEX_VALUE_MODAL});
  };
}

export function showAssertValueModal () {
  return (dispatch) => {
    dispatch({type: SHOW_ASSERT_VALUE_MODAL});
  };
}

export function hideAssertValueModal () {
  return (dispatch) => {
    dispatch({type: HIDE_ASSERT_VALUE_MODAL});
  };
}


/**
 * Set a value of an arbitrarily named field
 */
export function setFieldValue (name, value) {
  return (dispatch) => {
    dispatch({type: SET_FIELD_VALUE, name, value});
  };
}

export function setExpandedPaths (paths) {
  return (dispatch) => {
    dispatch({type: SET_EXPANDED_PATHS, paths});
  };
}

/**
 * Quit the session and go back to the new session window
 */
export function quitSession (reason, killedByUser = true) {
  return async (dispatch, getState) => {
    const killAction = killKeepAliveLoop();
    killAction(dispatch, getState);
    const applyAction = applyClientMethod({methodName: 'quit'});
    await applyAction(dispatch, getState);
    dispatch({type: QUIT_SESSION_DONE});
    dispatch(push('/session'));
    if (!killedByUser) {
      notification.error({
        message: 'Error',
        description: reason || i18n.t('Session has been terminated'),
        duration: 0
      });
    }
  };
}

export function startRecording () {
  return (dispatch) => {
    dispatch({type: START_RECORDING});
  };
}

export function pauseRecording () {
  return (dispatch) => {
    dispatch({type: PAUSE_RECORDING});
  };
}

export function clearRecording () {
  return (dispatch) => {
    dispatch({type: CLEAR_RECORDING});
    dispatch({type: CLEAR_ASSIGNED_VAR_CACHE}); // Get rid of the variable cache
  };
}

export function getSavedActionFramework () {
  return async (dispatch) => {
    let framework = await getSetting(SAVED_FRAMEWORK);
    dispatch({type: SET_ACTION_FRAMEWORK, framework});
  };
}

export function setActionFramework (framework) {
  return async (dispatch) => {
    if (!frameworks[framework]) {
      throw new Error(i18n.t('frameworkNotSupported', {framework}));
    }
    await setSetting(SAVED_FRAMEWORK, framework);
    dispatch({type: SET_ACTION_FRAMEWORK, framework});
  };
}

export function recordAction (action, params) {
  return (dispatch) => {
    dispatch({type: RECORD_ACTION, action, params});
  };
}

export function setRecordContent (content) {
  return (dispatch) => {
    dispatch({type: SET_RECORD_CONTENT, content});
  };
}

export function setRecordCursor (data) {
  return (dispatch) => {
    dispatch({type: SET_RECORD_CURSOR, data});
  };
}

export function closeRecorder () {
  return (dispatch) => {
    dispatch({type: CLOSE_RECORDER});
  };
}

export function toggleShowBoilerplate () {
  return (dispatch, getState) => {
    const show = !getState().inspector.showBoilerplate;
    dispatch({type: SET_SHOW_BOILERPLATE, show});
  };
}

export function setSessionDetails ({driver, sessionDetails, mode, mjpegScreenshotUrl, globalVars}) {
  return (dispatch) => {
    dispatch({type: SET_SESSION_DETAILS, driver, sessionDetails, mode, mjpegScreenshotUrl, globalVars});
  };
}

export function showLocatorTestModal () {
  return (dispatch) => {
    dispatch({type: SHOW_LOCATOR_TEST_MODAL});
  };
}

export function hideLocatorTestModal () {
  return (dispatch) => {
    dispatch({type: HIDE_LOCATOR_TEST_MODAL});
    dispatch({type: CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS});
  };
}

export function setLocatorTestValue (locatorTestValue) {
  return (dispatch) => {
    dispatch({type: SET_LOCATOR_TEST_VALUE, locatorTestValue});
  };
}

export function setLocatorTestStrategy (locatorTestStrategy) {
  return (dispatch) => {
    dispatch({type: SET_LOCATOR_TEST_STRATEGY, locatorTestStrategy});
  };
}

export function setContext (context) {
  return (dispatch) => {
    dispatch({type: SET_CONTEXT, context});
  };
}

export function searchForElement (strategy, selector) {
  return async (dispatch, getState) => {
    dispatch({type: SEARCHING_FOR_ELEMENTS});
    try {
      const callAction = callClientMethod({strategy, selector, fetchArray: true});
      let {elements, variableName} = await callAction(dispatch, getState);
      const findAction = findAndAssign(strategy, selector, variableName, true);
      findAction(dispatch, getState);
      elements = elements.map((el) => el.id);
      dispatch({type: SEARCHING_FOR_ELEMENTS_COMPLETED, elements});
    } catch (error) {
      dispatch({type: SEARCHING_FOR_ELEMENTS_COMPLETED});
      showError(error, 10);
    }
  };
}

/**
 * Get all the find element times based on the find data source
 */
export function getFindElementsTimes (findDataSource) {
  return async (dispatch, getState) => {
    dispatch({type: GET_FIND_ELEMENTS_TIMES});
    try {
      const findElementsExecutionTimes = [];
      for (const element of findDataSource) {
        const {find, key, selector} = element;
        const action = callClientMethod({strategy: key, selector});
        const {executionTime} = await action(dispatch, getState);
        findElementsExecutionTimes.push({find, key, selector, time: executionTime});
      }

      dispatch({
        type: GET_FIND_ELEMENTS_TIMES_COMPLETED,
        findElementsExecutionTimes: _.sortBy(findElementsExecutionTimes, ['time']),
      });
    } catch (error) {
      dispatch({type: GET_FIND_ELEMENTS_TIMES_COMPLETED});
      showError(error, 10);
    }
  };
}

export function findAndAssign (strategy, selector, variableName, isArray) {
  return (dispatch, getState) => {
    const {assignedVarCache} = getState().inspector;

    // If this call to 'findAndAssign' for this variable wasn't done already, do it now
    if (!assignedVarCache[variableName]) {
      dispatch({type: RECORD_ACTION, action: 'findAndAssign', params: [strategy, selector, variableName, isArray]});
      dispatch({type: ADD_ASSIGNED_VAR_CACHE, varName: variableName});
    }
  };
}

export function setLocatorTestElement (elementId) {
  return async (dispatch, getState) => {
    dispatch({type: SET_LOCATOR_TEST_ELEMENT, elementId});
    dispatch({type: CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS});
    if (elementId) {
      try {
        const action = callClientMethod({
          elementId,
          methodName: 'getRect',
          skipRefresh: true,
          skipRecord: true,
          ignoreResult: true
        });
        const { commandRes } = await action(dispatch, getState);
        dispatch({
          type: SET_SEARCHED_FOR_ELEMENT_BOUNDS,
          location: {x: commandRes.x, y: commandRes.y},
          size: {width: commandRes.width, height: commandRes.height},
        });
      } catch (ign) { }
    }
  };
}

export function clearSearchResults () {
  return (dispatch) => {
    dispatch({type: CLEAR_SEARCH_RESULTS});
  };
}

export function selectScreenshotInteractionMode (screenshotInteractionMode) {
  return (dispatch) => {
    dispatch({type: SET_SCREENSHOT_INTERACTION_MODE, screenshotInteractionMode });
  };
}

export function selectAppMode (mode) {
  return async (dispatch, getState) => {
    const {appMode} = getState().inspector;
    dispatch({type: SET_APP_MODE, mode});
    // if we're transitioning to hybrid mode, do a pre-emptive search for contexts
    if (appMode !== mode && mode === APP_MODE.WEB_HYBRID) {
      const action = applyClientMethod({methodName: 'getPageSource'});
      await action(dispatch, getState);
    }
    if (appMode !== mode && mode === APP_MODE.NATIVE) {
      const action = applyClientMethod({ methodName: 'switchContext', args: [NATIVE_APP] });
      await action(dispatch, getState);
    }
  };
}

export function toggleShowCentroids () {
  return (dispatch, getState) => {
    const {showCentroids} = getState().inspector;
    const show = !showCentroids;
    dispatch({type: SET_SHOW_CENTROIDS, show});
  };
}

export function getActiveAppId (isIOS, isAndroid) {
  return async (dispatch, getState) => {
    try {
      if (isIOS) {
        const action = applyClientMethod({methodName: 'executeScript', args: ['mobile:activeAppInfo', []]});
        const { bundleId } = await action(dispatch, getState);
        dispatch({type: SET_APP_ID, appId: bundleId});
      }
      if (isAndroid) {
        const action = applyClientMethod({methodName: 'getCurrentPackage'});
        const appPackage = await action(dispatch, getState);
        dispatch({type: SET_APP_ID, appId: appPackage});
      }
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error(`Could not Retrieve Active App ID: ${err}`);
    }
  };
}

export function getServerStatus () {
  return async (dispatch, getState) => {
    const status = applyClientMethod({methodName: 'status'});
    const { build } = await status(dispatch, getState);
    dispatch({type: SET_SERVER_STATUS, status: build});
  };
}

// Start the session timer once session starts
export function setSessionTime (time) {
  return (dispatch) => {
    dispatch({type: SET_SESSION_TIME, sessionStartTime: time});
  };
}

export function setSwipeStart (swipeStartX, swipeStartY) {
  return (dispatch) => {
    dispatch({type: SET_SWIPE_START, swipeStartX, swipeStartY});
  };
}

export function setSwipeEnd (swipeEndX, swipeEndY) {
  return (dispatch) => {
    dispatch({type: SET_SWIPE_END, swipeEndX, swipeEndY});
  };
}

export function clearSwipeAction () {
  return (dispatch) => {
    dispatch({type: CLEAR_SWIPE_ACTION});
  };
}

export function promptKeepAlive () {
  return (dispatch) => {
    dispatch({type: PROMPT_KEEP_ALIVE});
  };
}

export function hideKeepAlivePrompt () {
  return (dispatch) => {
    dispatch({type: HIDE_PROMPT_KEEP_ALIVE});
  };
}

export function selectActionGroup (group) {
  return (dispatch) => {
    dispatch({type: SELECT_ACTION_GROUP, group});
  };
}

export function selectSubActionGroup (group) {
  return (dispatch) => {
    dispatch({type: SELECT_SUB_ACTION_GROUP, group});
  };
}

export function selectInteractionMode (interaction) {
  return (dispatch) => {
    dispatch({type: SELECT_INTERACTION_MODE, interaction});
  };
}

export function startEnteringActionArgs (actionName, action) {
  return (dispatch) => {
    dispatch({type: ENTERING_ACTION_ARGS, actionName, action});
  };
}

export function cancelPendingAction () {
  return (dispatch) => {
    dispatch({type: REMOVE_ACTION});
  };
}

export function setActionArg (index, value) {
  return (dispatch) => {
    dispatch({type: SET_ACTION_ARG, index, value});
  };
}

/**
 * Ping server every 30 seconds to prevent `newCommandTimeout` from killing session
 */
export function runKeepAliveLoop () {
  return (dispatch, getState) => {
    dispatch({type: SET_LAST_ACTIVE_MOMENT, lastActiveMoment: Date.now()});
    const {driver} = getState().inspector;

    const keepAliveInterval = setInterval(async () => {
      const {lastActiveMoment} = getState().inspector;
      console.log('Pinging Appium server to keep session active'); // eslint-disable-line no-console
      try {
        await driver.getTimeouts(); // Pings the Appium server to keep it alive
      } catch (ign) {}
      const now = Date.now();

      // If the new command limit has been surpassed, prompt user if they want to keep session going
      // Give them WAIT_FOR_USER_KEEP_ALIVE ms to respond
      if (now - lastActiveMoment > NO_NEW_COMMAND_LIMIT) {
        const action = promptKeepAlive();
        action(dispatch);

        // After the time limit kill the session (this timeout will be killed if they keep it alive)
        const userWaitTimeout = setTimeout(() => {
          const action = quitSession('Session closed due to inactivity', false);
          action(dispatch, getState);
        }, WAIT_FOR_USER_KEEP_ALIVE);
        dispatch({type: SET_USER_WAIT_TIMEOUT, userWaitTimeout});
      }
    }, KEEP_ALIVE_PING_INTERVAL);
    dispatch({type: SET_KEEP_ALIVE_INTERVAL, keepAliveInterval});
  };
}

/**
 * Get rid of the intervals to keep the session alive
 */
export function killKeepAliveLoop () {
  return (dispatch, getState) => {
    const {keepAliveInterval, userWaitTimeout} = getState().inspector;
    clearInterval(keepAliveInterval);
    if (userWaitTimeout) {
      clearTimeout(userWaitTimeout);
    }
    dispatch({type: SET_KEEP_ALIVE_INTERVAL, keepAliveInterval: null});
    dispatch({type: SET_USER_WAIT_TIMEOUT, userWaitTimeout: null});
  };
}

/**
 * Reset the new command clock and kill the wait for user timeout
 */
export function keepSessionAlive () {
  return (dispatch, getState) => {
    const {userWaitTimeout} = getState().inspector;
    const action = hideKeepAlivePrompt();
    action(dispatch);
    dispatch({type: SET_LAST_ACTIVE_MOMENT, lastActiveMoment: +(new Date())});
    if (userWaitTimeout) {
      clearTimeout(userWaitTimeout);
      dispatch({type: SET_USER_WAIT_TIMEOUT, userWaitTimeout: null});
    }
  };
}

export function callClientMethod (params) {
  return async (dispatch, getState) => {
    const {driver, appMode, mjpegScreenshotUrl} = getState().inspector;
    const {methodName, ignoreResult = true} = params;
    params.appMode = appMode;

    // don't retrieve screenshot if we're already using the mjpeg stream
    if (mjpegScreenshotUrl) {
      params.skipScreenshot = true;
    }

    console.log(`Calling client method with params:`); // eslint-disable-line no-console
    console.log(params); // eslint-disable-line no-console
    const action = keepSessionAlive();
    action(dispatch, getState);
    const client = AppiumClient.instance(driver);
    const res = await client.run(params);
    let {commandRes} = res;

    // Ignore empty objects
    if (_.isObject(res) && _.isEmpty(res)) {
      commandRes = null;
    }

    if (!ignoreResult) {
      // if the user is running actions manually, we want to show the full response with the
      // ability to scroll etc...
      const result = JSON.stringify(commandRes, null, '  ');
      const truncatedResult = _.truncate(result, {length: 2000});
      console.log(`Result of client command was:`); // eslint-disable-line no-console
      console.log(truncatedResult); // eslint-disable-line no-console
      setVisibleCommandResult(result, methodName)(dispatch);
    }
    res.elementId = res.id;
    return res;
  };
}

export function setVisibleCommandResult (result, methodName) {
  return (dispatch) => {
    dispatch({type: SET_VISIBLE_COMMAND_RESULT, result, methodName});
  };
}

export function setAwaitingMjpegStream (isAwaiting) {
  return (dispatch) => {
    dispatch({type: SET_AWAITING_MJPEG_STREAM, isAwaiting});
  };
}

export function saveGesture (params) {
  return async (dispatch, getState) => {
    const savedGestures = getState().inspector.savedGestures;
    if (!params.id) {
      params.id = UUID();
      params.date = Date.now();
      savedGestures.push(params);
    } else {
      for (const gesture of savedGestures) {
        if (gesture.id === params.id) {
          gesture.name = params.name;
          gesture.description = params.description;
          gesture.actions = params.actions;
        }
      }
    }
    dispatch({type: SET_SAVED_GESTURES, savedGestures});
    await setSetting(SET_SAVED_GESTURES, savedGestures);
    const action = getSavedGestures();
    await action(dispatch);
  };
}

export function getSavedGestures () {
  return async (dispatch) => {
    dispatch({type: GET_SAVED_GESTURES_REQUESTED});
    const savedGestures = await getSetting(SET_SAVED_GESTURES);
    dispatch({type: GET_SAVED_GESTURES_DONE, savedGestures});
  };
}

export function deleteSavedGesture (id) {
  return async (dispatch) => {
    dispatch({type: DELETE_SAVED_GESTURES_REQUESTED, deleteGesture: id});
    const gestures = await getSetting(SET_SAVED_GESTURES);
    const newGestures = gestures.filter((gesture) => gesture.id !== id);
    await setSetting(SET_SAVED_GESTURES, newGestures);
    dispatch({type: DELETE_SAVED_GESTURES_DONE});
    dispatch({type: GET_SAVED_GESTURES_DONE, savedGestures: newGestures});
  };
}

export function showGestureEditor () {
  return (dispatch) => {
    dispatch({type: SHOW_GESTURE_EDITOR});
    dispatch({type: SET_SCREENSHOT_INTERACTION_MODE, screenshotInteractionMode: 'gesture' });
  };
}

export function hideGestureEditor () {
  return (dispatch) => {
    dispatch({type: HIDE_GESTURE_EDITOR});
  };
}

export function setLoadedGesture (loadedGesture) {
  return (dispatch) => {
    dispatch({type: SET_LOADED_GESTURE, loadedGesture});
  };
}

export function removeLoadedGesture () {
  return (dispatch) => {
    dispatch({type: REMOVE_LOADED_GESTURE});
  };
}

export function displayGesture (showGesture) {
  return (dispatch) => {
    dispatch({type: SHOW_GESTURE_ACTION, showGesture});
  };
}

export function removeGestureDisplay () {
  return (dispatch) => {
    dispatch({type: HIDE_GESTURE_ACTION});
  };
}

export function selectTick (tick) {
  return (dispatch, getState) => {
    const {tickCoordinates} = getState().inspector;

    if (tickCoordinates) {
      dispatch({type: SET_GESTURE_TAP_COORDS_MODE, x: undefined, y: undefined});
    }

    dispatch({type: SELECT_TICK_ELEMENT, selectedTick: tick});
  };
}

export function unselectTick () {
  return (dispatch) => {
    dispatch({type: CLEAR_TAP_COORDINATES});
    dispatch({type: UNSELECT_TICK_ELEMENT});
  };
}

export function tapTickCoordinates (x, y) {
  return (dispatch) => {
    dispatch({type: SET_GESTURE_TAP_COORDS_MODE, x, y});
  };
}

export function toggleShowAttributes () {
  return (dispatch) => {
    dispatch({type: TOGGLE_SHOW_ATTRIBUTES});
  };
}

export function saveExecuteScriptAppear (isSaveExecuteScript) {
  return (dispatch) => {
    dispatch({type: SAVE_EXECUTE_SCRIPT_APPEAR, isSaveExecuteScript});
  };
}

export function savedText (savedScriptText) {
  return (dispatch) => {
    dispatch({type: SAVED_TEXT, savedScriptText});
  };
}

export function saveExecuteScript (scriptName, actions) {
  return (dispatch) => {
    dispatch({type: SAVE_EXECUTE_SCRIPT_APPEAR, isSaveExecuteScript: false});

    ipcRenderer.send('save-script', {scriptName, actions});
  };
}

export function insertTestCaseAppear (isInsertTestCaseAppear) {
  return (dispatch) => {
    dispatch({type: INSERT_TEST_CASE_APPEAR, isInsertTestCaseAppear});
  };
}

export function insertTestCase (insertTestCaseName) {
  return (dispatch, getState) => {
    dispatch({type: RECORD_ACTION, action: 'insertIt', params: [insertTestCaseName]});
    insertRecordContent(dispatch, getState);
  };
}

export function requestAppdata (apiPath, responseName) {
  return (dispatch, getState) => {
    let inspector = getState().inspector;
    let caps = inspector.sessionDetails.desiredCapabilities;
    const params = {caps, apiPath, responseName};
    const jsonParams = JSON.stringify(params);
    ipcRenderer.send('get-appdata', jsonParams);
  };

}

export function saveAppdata (dataName, response) {
  return (dispatch, getState) => {
    dispatch({type: SAVE_APP_DATA, dataName, response });
    let args = [dataName, response.data.request_api];
    dispatch({type: RECORD_ACTION, action: 'getAppData', params: args});
    insertRecordContent(dispatch, getState);
  };
}

export function aipAutoCodeGenerate () {
  // apiAutoSuit (index, requestVars, suiteVars, testId, testEnvId)
  return (dispatch, getState) => {
    let inspector = getState().inspector;
    const testId = inspector.suitId;
    const suiteVars = inspector.selectSuitCaps;
    const requreVars = inspector.requireVars;
    let args = [inspector.apiAutoIndex, requreVars, suiteVars, testId];
    dispatch({type: RECORD_ACTION, action: 'apiAutoSuit', params: args});
    insertRecordContent(dispatch, getState);
  };
}

export function saveRequireVars () {
  return (dispatch, getState) => {
    let inspector = getState().inspector;
    const suitId = inspector.suitId;
    const suiteName = inspector.suiteName;
    let globalVars = [];
    const selectSuitCaps = inspector.selectSuitCaps;
    for (let i = 0; i < selectSuitCaps.length; i++) {
      const element = selectSuitCaps[i];
      let value = element.value;
      if (element.type === 'variable') {
        const responseName = value.split('.')[0];
        if (responseName === 'globalVars') {
          globalVars.push(element);
        }
      } else {
        globalVars.push(element);
      }
    }
    if (globalVars.length > 0) {
      const requireVars = {'type': 'api', 'key': suitId, 'name': suiteName, 'value': globalVars};
      dispatch({type: SAVE_REQUITE_VARS, requireVars});
    }

  };
}

export function showSuiteGroupList (groupListData) {
  return (dispatch) => {
    dispatch({type: SHOW_TESTSUITE_GROUP, groupListData});
  };
}

export function requestTestSuiteList ({groupId, skip}) {
  return (dispatch, getState) => {
    if (groupId === undefined) {
      groupId = getState().groupId;
    } else {
      getState().groupId = groupId;
    }
    const params = {requestName: 'testSuiteList', groupId, skip};
    ipcRenderer.send('make-request', JSON.stringify(params));
  };
}

export function selectTestSuitId (suitId, suiteName) {
  return (dispatch) => {
    dispatch({type: SELECT_TESTSUIT_ID, suitId, suiteName});
  };
}

export function requestApiAuto () {
  //REQUEST_API_AUTO
  return async (dispatch, getState) => {
    let inspector = getState().inspector;
    const { suiteVars, testId, envId } = await getApiAutoParams(inspector);
    const params = {requestName: 'startUIAutomation', suiteVars, testId, envId};
    ipcRenderer.send('make-request', JSON.stringify(params));
  };
}

async function getApiAutoParams (inspector) {
  const testId = inspector.suitId;
  const envId = inspector.envId;
  const appdata = inspector.appdata;
  const globalVars = await getSetting(SAVE_GLOBAL_VARS);
  let suiteVars = {};
  const selectSuitCaps = inspector.selectSuitCaps;
  for (let i = 0; i < selectSuitCaps.length; i++) {
    const element = selectSuitCaps[i];
    let value = element.value;
    if (element.type === 'variable') {
      const responseName = value.split('.')[0];
      if (responseName === 'globalVars') {
        const paramName = value.split('.')[1];
        const globalVarElement = globalVars.find((element) => paramName === element.name);
        value = globalVarElement.value;
      } else {
        for (let key in appdata) {
          if (key === responseName) {
            const shiftArr = value.split('.');
            shiftArr.shift();
            value = shiftArr.reduce((obj, key) => obj[key], appdata[key]);
            break;
          }
        }
      }
    }
    suiteVars[element.name] = value;
  }
  return {suiteVars, testId, envId};
}

export function selectTestEnvId (envId, envName) {
  return (dispatch) => {
    dispatch({type: SELECT_ENV_ID, envId, envName});
  };
}

export function setTestSuitCapabilityParam (index, name, value) {
  return (dispatch) => {
    dispatch({type: SET_TEST_CAPABILITY_PARAM, index, name, value});
  };
}

export function addTestSuitCapability () {
  return (dispatch) => {
    dispatch({type: ADD_TEST_CAPABILITY});
  };
}

export function removeTestCapability (index) {
  return (dispatch) => {
    dispatch({type: REMOVE_TEST_CAPABILITY, index});
  };
}

export function showGlobalVarsModal (isGlobalVarShow) {

  return (dispatch) => {
    dispatch({type: SHOW_GLOBAL_VARS_MODAL, isGlobalVarShow});
  };
}
