"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.flushContextAfterIndex = exports.flushContext = exports.fetchRenderableWithContext = exports.fetchRenderable = exports.fetchContext = exports.fetchAllRenderables = exports.elementLayer = exports.deleteArgumentAtIndex = exports.addElement = exports.addArgumentValue = void 0;
exports.getSiblingContext = getSiblingContext;
exports.setMultiplePositions = exports.setFilter = exports.setExpression = exports.setAstAtIndex = exports.setArgument = exports.removeElements = exports.insertNodes = void 0;
var _reduxActions = require("redux-actions");
var _objectPathImmutable = _interopRequireDefault(require("object-path-immutable"));
var _lodash = require("lodash");
var _interpreter = require("@kbn/interpreter");
var _create_thunk = require("../../lib/create_thunk");
var _workpad = require("../../lib/workpad");
var _workpad2 = require("../selectors/workpad");
var _resolved_args = require("../selectors/resolved_args");
var _defaults = require("../defaults");
var _i18n = require("../../../i18n");
var _functional = require("../../lib/aeroelastic/functional");
var _services = require("../../services");
var _transient = require("./transient");
var args = _interopRequireWildcard(require("./resolved_args"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */

const {
  actionsElements: strings
} = _i18n.ErrorStrings;
const {
  set,
  del
} = _objectPathImmutable.default;
function getSiblingContext(state, elementId, checkIndex, path = ['ast.chain']) {
  const prevContextPath = [elementId, 'expressionContext', ...path, checkIndex];
  const prevContextValue = (0, _resolved_args.getValue)(state, prevContextPath);

  // if a value is found, return it, along with the index it was found at
  if (prevContextValue != null) {
    return {
      index: checkIndex,
      context: prevContextValue
    };
  }

  // check previous index while we're still above 0
  const prevContextIndex = checkIndex - 1;
  if (prevContextIndex < 0) {
    return {};
  }

  // walk back up to find the closest cached context available
  return getSiblingContext(state, elementId, prevContextIndex, path);
}
function getBareElement(el, includeId = false) {
  const props = ['position', 'expression', 'filter'];
  if (includeId) {
    return (0, _lodash.pick)(el, props.concat('id'));
  }
  return (0, _lodash.cloneDeep)((0, _lodash.pick)(el, props));
}
const elementLayer = (0, _reduxActions.createAction)('elementLayer');
exports.elementLayer = elementLayer;
const setMultiplePositions = (0, _reduxActions.createAction)('setMultiplePosition', repositionedElements => ({
  repositionedElements
}));
exports.setMultiplePositions = setMultiplePositions;
const flushContext = (0, _reduxActions.createAction)('flushContext');
exports.flushContext = flushContext;
const flushContextAfterIndex = (0, _reduxActions.createAction)('flushContextAfterIndex');
exports.flushContextAfterIndex = flushContextAfterIndex;
const fetchContextFn = ({
  dispatch,
  getState
}, index, element, fullRefresh = false, path) => {
  const pathToTarget = [...path.split('.'), 'chain'];
  const chain = (0, _lodash.get)(element, pathToTarget);
  const invalidIndex = chain ? index >= chain.length : true;
  if (!element || !chain || invalidIndex) {
    throw new Error(strings.getInvalidArgIndexErrorMessage(index));
  }

  // cache context as the previous index
  const contextIndex = index - 1;
  const contextPath = [element.id, 'expressionContext', path, contextIndex];

  // set context state to loading
  dispatch(args.setLoading({
    path: contextPath
  }));

  // function to walk back up to find the closest context available
  const getContext = () => getSiblingContext(getState(), element.id, contextIndex - 1, [path]);
  const {
    index: prevContextIndex,
    context: prevContextValue
  } = fullRefresh !== true ? getContext() : {};

  // modify the ast chain passed to the interpreter
  const astChain = chain.filter((exp, i) => {
    if (prevContextValue != null) {
      return i > prevContextIndex && i < index;
    }
    return i < index;
  });
  const variables = (0, _workpad2.getWorkpadVariablesAsObject)(getState());
  const {
    expressions
  } = _services.pluginServices.getServices();
  const elementWithNewAst = set(element, pathToTarget, astChain);

  // get context data from a partial AST
  return expressions.interpretAst(elementWithNewAst.ast, variables, prevContextValue).then(value => {
    dispatch(args.setValue({
      path: contextPath,
      value
    }));
  });
};
const fetchContext = (0, _create_thunk.createThunk)('fetchContext', fetchContextFn);
exports.fetchContext = fetchContext;
const fetchRenderableWithContextFn = ({
  dispatch,
  getState
}, element, ast, context) => {
  const argumentPath = [element.id, 'expressionRenderable'];
  dispatch(args.setLoading({
    path: argumentPath
  }));
  const getAction = renderable => args.setValue({
    path: argumentPath,
    value: renderable
  });
  const variables = (0, _workpad2.getWorkpadVariablesAsObject)(getState());
  const {
    expressions,
    notify
  } = _services.pluginServices.getServices();
  return expressions.runInterpreter(ast, context, variables, {
    castToRender: true
  }).then(renderable => {
    dispatch(getAction(renderable));
  }).catch(err => {
    notify.error(err);
    dispatch(getAction(err));
  });
};
const fetchRenderableWithContext = (0, _create_thunk.createThunk)('fetchRenderableWithContext', fetchRenderableWithContextFn);
exports.fetchRenderableWithContext = fetchRenderableWithContext;
const fetchRenderable = (0, _create_thunk.createThunk)('fetchRenderable', ({
  dispatch
}, element) => {
  const ast = element.ast || (0, _interpreter.safeElementFromExpression)(element.expression);
  dispatch(fetchRenderableWithContext(element, ast, null));
});
exports.fetchRenderable = fetchRenderable;
const fetchAllRenderables = (0, _create_thunk.createThunk)('fetchAllRenderables', ({
  dispatch,
  getState
}, {
  onlyActivePage = false
} = {}) => {
  const workpadPages = (0, _workpad2.getPages)(getState());
  const currentPageIndex = (0, _workpad2.getSelectedPageIndex)(getState());
  const currentPage = workpadPages[currentPageIndex];
  const otherPages = (0, _lodash.without)(workpadPages, currentPage);
  dispatch(args.inFlightActive());
  function fetchElementsOnPages(pages) {
    const elements = [];
    pages.forEach(page => {
      page.elements.forEach(element => {
        elements.push(element);
      });
    });
    const renderablePromises = elements.map(element => {
      const ast = element.ast || (0, _interpreter.safeElementFromExpression)(element.expression);
      const argumentPath = [element.id, 'expressionRenderable'];
      const variables = (0, _workpad2.getWorkpadVariablesAsObject)(getState());
      const {
        expressions,
        notify
      } = _services.pluginServices.getServices();
      return expressions.runInterpreter(ast, null, variables, {
        castToRender: true
      }).then(renderable => ({
        path: argumentPath,
        value: renderable
      })).catch(err => {
        notify.error(err);
        return {
          path: argumentPath,
          value: err
        };
      });
    });
    return Promise.all(renderablePromises).then(renderables => {
      dispatch(args.setValues(renderables));
    });
  }
  if (onlyActivePage) {
    fetchElementsOnPages([currentPage]).then(() => dispatch(args.inFlightComplete()));
  } else {
    fetchElementsOnPages([currentPage]).then(() => {
      return otherPages.reduce((chain, page) => {
        return chain.then(() => fetchElementsOnPages([page]));
      }, Promise.resolve());
    }).then(() => dispatch(args.inFlightComplete()));
  }
});
exports.fetchAllRenderables = fetchAllRenderables;
const insertNodes = (0, _create_thunk.createThunk)('insertNodes', ({
  dispatch,
  type
}, elements, pageId) => {
  const _insertNodes = (0, _reduxActions.createAction)(type);
  const newElements = elements.map(_lodash.cloneDeep);
  // move the root element so users can see that it was added
  newElements.forEach(newElement => {
    newElement.position.top = newElement.position.top + 10;
    newElement.position.left = newElement.position.left + 10;
  });
  dispatch(_insertNodes({
    pageId,
    elements: newElements
  }));

  // refresh all elements just once per `insertNodes call` if there's a filter on any, otherwise just render the new element
  if (elements.some(element => element.filter)) {
    dispatch(fetchAllRenderables());
  } else {
    newElements.forEach(newElement => dispatch(fetchRenderable(newElement)));
  }
});
exports.insertNodes = insertNodes;
const removeElements = (0, _create_thunk.createThunk)('removeElements', ({
  dispatch,
  getState
}, rootElementIds, pageId) => {
  const state = getState();

  // todo consider doing the group membership collation in aeroelastic, or the Redux reducer, when adding templates
  const allElements = (0, _workpad2.getNodes)(state, pageId);
  const allRoots = rootElementIds.map(id => allElements.find(e => id === e.id)).filter(d => d);
  const elementIds = (0, _functional.subMultitree)(e => e.id, e => e.position.parent, allElements, allRoots).map(e => e.id);
  const shouldRefresh = elementIds.some(elementId => {
    const element = (0, _workpad2.getNodeById)(state, elementId, pageId);
    const filterIsApplied = element.filter && element.filter.length > 0;
    return filterIsApplied;
  });
  const _removeElements = (0, _reduxActions.createAction)('removeElements', (elementIds, pageId) => ({
    elementIds,
    pageId
  }));
  dispatch(_removeElements(elementIds, pageId));
  if (shouldRefresh) {
    dispatch(fetchAllRenderables());
  }
});
exports.removeElements = removeElements;
const setFilter = (0, _create_thunk.createThunk)('setFilter', ({
  dispatch
}, filter, elementId, doRender = true) => {
  const _setFilter = (0, _reduxActions.createAction)('setFilter');
  dispatch(_setFilter({
    filter,
    elementId
  }));
  if (doRender === true) {
    dispatch(fetchAllRenderables());
  }
});
exports.setFilter = setFilter;
const setExpression = (0, _create_thunk.createThunk)('setExpression', setExpressionFn);
exports.setExpression = setExpression;
function setExpressionFn({
  dispatch,
  getState
}, expression, elementId, pageId, doRender = true) {
  // dispatch action to update the element in state
  const _setExpression = (0, _reduxActions.createAction)('setExpression');
  dispatch(_setExpression({
    expression,
    elementId,
    pageId
  }));

  // read updated element from state and fetch renderable
  const updatedElement = (0, _workpad2.getNodeById)(getState(), elementId, pageId);

  // reset element.filter if element is no longer a filter
  // TODO: find a way to extract a list of filter renderers from the functions registry
  if (updatedElement.filter && !['dropdownControl', 'timefilterControl', 'exactly'].some(filter => updatedElement.expression.includes(filter))) {
    const filter = '';
    dispatch(setFilter(filter, elementId, pageId, doRender));
    // setFilter will trigger a re-render so we can skip the fetch here
  } else if (doRender === true) {
    dispatch(fetchRenderable(updatedElement));
  }
}
const setAst = (0, _create_thunk.createThunk)('setAst', ({
  dispatch
}, ast, element, pageId, doRender = true) => {
  try {
    const expression = (0, _interpreter.toExpression)(ast);
    dispatch(setExpression(expression, element.id, pageId, doRender));
  } catch (err) {
    const notifyService = _services.pluginServices.getServices().notify;
    notifyService.error(err);

    // TODO: remove this, may have been added just to cause a re-render, but why?
    dispatch(setExpression(element.expression, element.id, pageId, doRender));
  }
});

// index here is the top-level argument in the expression. for example in the expression
// demodata().pointseries().plot(), demodata is 0, pointseries is 1, and plot is 2
const setAstAtIndex = (0, _create_thunk.createThunk)('setAstAtIndex', ({
  dispatch,
  getState
}, index, ast, element, pageId) => {
  // invalidate cached context for elements after this index
  dispatch(flushContextAfterIndex({
    elementId: element.id,
    index
  }));
  const newElement = set(element, `ast.chain.${index}`, ast);
  const newAst = (0, _lodash.get)(newElement, 'ast');

  // fetch renderable using existing context, if available (value is null if not cached)
  const {
    index: contextIndex,
    context: contextValue
  } = getSiblingContext(getState(), element.id, index - 1);

  // if we have a cached context, update the expression, but use cache when updating the renderable
  if (contextValue) {
    // set the expression, but skip the fetchRenderable step
    dispatch(setAst(newAst, element, pageId, false));

    // use context when updating the expression, it will be passed to the intepreter
    const partialAst = {
      ...newAst,
      chain: newAst.chain.filter((exp, i) => {
        if (contextValue) {
          return i > contextIndex;
        }
        return i >= index;
      })
    };
    return dispatch(fetchRenderableWithContext(newElement, partialAst, contextValue));
  }

  // if no cached context, update the ast like normal
  dispatch(setAst(newAst, element, pageId));
});

/**
 * Updating the value of the given argument of the element's expression.
 * @param {string} args.path - the path to the argument at the AST. Example: "ast.chain.0.arguments.some_arg.chain.1.arguments".
 * @param {string} args.argName - the argument name at the AST.
 * @param {number} args.valueIndex - the index of the value in the array of argument's values.
 * @param {any} args.value - the value to be set to the AST.
 * @param {any} args.element - the element, which contains the expression.
 * @param {any} args.pageId - the workpad's page, where element is located.
 */
exports.setAstAtIndex = setAstAtIndex;
const setArgument = (0, _create_thunk.createThunk)('setArgument', ({
  dispatch,
  getState
}, args) => {
  const {
    argName,
    value,
    valueIndex,
    elementId,
    pageId,
    path
  } = args;
  let selector = `${path}.${argName}`;
  if (valueIndex != null) {
    selector += '.' + valueIndex;
  }
  const element = (0, _workpad2.getElementById)(getState(), elementId);
  const newElement = set(element, selector, value);
  const pathTerms = path.split('.');
  const argumentChainPath = pathTerms.slice(0, 3);
  const argumnentChainIndex = (0, _lodash.last)(argumentChainPath);
  const newAst = (0, _lodash.get)(newElement, argumentChainPath);
  dispatch(setAstAtIndex(argumnentChainIndex, newAst, element, pageId));
});

/**
 * Adding the value to the given argument of the element's expression.
 * @param {string} args.path - the path to the argument at the AST. Example: "ast.chain.0.arguments.some_arg.chain.1.arguments".
 * @param {string} args.argName - the argument name at the given path of the AST.
 * @param {any} args.value - the value to be added to the array of argument's values at the AST.
 * @param {any} args.element - the element, which contains the expression.
 * @param {any} args.pageId - the workpad's page, where element is located.
 */
exports.setArgument = setArgument;
const addArgumentValue = (0, _create_thunk.createThunk)('addArgumentValue', ({
  dispatch,
  getState
}, args) => {
  const {
    argName,
    value,
    elementId,
    path
  } = args;
  const element = (0, _workpad2.getElementById)(getState(), elementId);
  const values = (0, _lodash.get)(element, [...path.split('.'), argName], []);
  const newValue = values.concat(value);
  dispatch(setArgument({
    ...args,
    elementId,
    value: newValue
  }));
});
exports.addArgumentValue = addArgumentValue;
const deleteArgumentAtIndex = (0, _create_thunk.createThunk)('deleteArgumentAtIndex', ({
  dispatch
}, args) => {
  const {
    element,
    pageId,
    argName,
    argIndex,
    path
  } = args;
  const pathTerms = path.split('.');
  const argumentChainPath = pathTerms.slice(0, 3);
  const argumnentChainIndex = (0, _lodash.last)(argumentChainPath);
  const curVal = (0, _lodash.get)(element, [...pathTerms, argName]);
  let newElement = argIndex != null && curVal.length > 1 ?
  // if more than one val, remove the specified val
  del(element, `${path}.${argName}.${argIndex}`) :
  // otherwise, remove the entire key
  del(element, argName ? `${path}.${argName}` : path);
  const parentPath = pathTerms.slice(0, pathTerms.length - 1);
  const updatedArgument = (0, _lodash.get)(newElement, parentPath);
  if (Array.isArray(updatedArgument) && !updatedArgument.length) {
    newElement = del(element, parentPath);
  }
  dispatch(setAstAtIndex(argumnentChainIndex, (0, _lodash.get)(newElement, argumentChainPath), element, pageId));
});

/*
  payload: element defaults. Eg {expression: 'foo'}
*/
exports.deleteArgumentAtIndex = deleteArgumentAtIndex;
const addElement = (0, _create_thunk.createThunk)('addElement', ({
  dispatch
}, pageId, element) => {
  const newElement = {
    ...(0, _defaults.getDefaultElement)(),
    ...getBareElement(element, true)
  };
  if (element.width) {
    newElement.position.width = element.width;
  }
  if (element.height) {
    newElement.position.height = element.height;
  }
  const _addElement = (0, _reduxActions.createAction)('addElement');
  dispatch(_addElement({
    pageId,
    element: newElement
  }));

  // refresh all elements if there's a filter, otherwise just render the new element
  if (element.filter) {
    dispatch(fetchAllRenderables());
    // element, which represents the group, should not be rendered. Its elements are rendered separately.
  } else if (!(0, _workpad.isGroupId)(newElement.id)) {
    dispatch(fetchRenderable(newElement));
  }

  // select the new element
  dispatch((0, _transient.selectToplevelNodes)([newElement.id]));
});
exports.addElement = addElement;