text: /*
   * Licensed to Elasticsearch B.V. under one or more contributor
   * license agreements. See the NOTICE file distributed with
   * this work for additional information regarding copyright
   * ownership. Elasticsearch B.V. licenses this file to you under
   * the Apache License, Version 2.0 (the "License"); you may
   * not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  
  import _ from 'lodash';
  
  export const SUGGESTION_TYPE = {
    ARGUMENTS: 'arguments',
    ARGUMENT_VALUE: 'argument_value',
    FUNCTIONS: 'functions'
  };
  
  export class Suggestions {
    constructor() {
      this.reset();
    }
  
    reset() {
      this.index = -1;
      this.list = [];
      this.type = null;
      this.isVisible = false;
    }
  
    setList(list, type) {
      this.list = list.sort((a, b) => {
        if (a.name < b.name) {
          return -1;
        }
        if (a.name > b.name) {
          return 1;
        }
        // names must be equal
        return 0;
      });
      this.type = type;
  
      // Only try to position index inside of list range, when it was already focused
      // beforehand (i.e. not -1)
      if (this.index > -1) {
        // We may get a shorter list than the one we have now, so we need to make sure our index doesn't
        // fall outside of the new list's range.
        this.index = Math.max(0, Math.min(this.index, this.list.length - 1));
      }
    }
  
    getCount() {
      return this.list.length;
    }
  
    isEmpty() {
      return this.list.length === 0;
    }
  
    show() {
      this.isVisible = true;
    }
  
    hide() {
      this.isVisible = false;
    }
  
    stepForward() {
      if (this.index > 0) {
        this.index -= 1;
      }
    }
  
    stepBackward() {
      if (this.index < this.list.length - 1) {
        this.index += 1;
      }
    }
  }
  
  function inLocation(cursorPosition, location) {
    return cursorPosition >= location.min && cursorPosition <= location.max;
  }
  
  function getArgumentsHelp(functionHelp, functionArgs = []) {
    if (!functionHelp) {
      return [];
    }
  
    // Do not provide 'inputSeries' as argument suggestion for chainable functions
    const argsHelp = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0);
  
    // ignore arguments that are already provided in function declaration
    const functionArgNames = functionArgs.map((arg) => {
      return arg.name;
    });
    return argsHelp.filter(arg => {
      return !functionArgNames.includes(arg.name);
    });
  }
  
  async function extractSuggestionsFromParsedResult(result, cursorPosition, functionList, argValueSuggestions) {
    const activeFunc = result.functions.find((func) => {
      return cursorPosition >= func.location.min && cursorPosition < func.location.max;
    });
  
    if (!activeFunc) {
      return;
    }
  
    const functionHelp = functionList.find((func) => {
      return func.name === activeFunc.function;
    });
  
    // return function suggestion when cursor is outside of parentheses
    // location range includes '.', function name, and '('.
    const openParen = activeFunc.location.min + activeFunc.function.length + 2;
    if (cursorPosition < openParen) {
      return { list: [functionHelp], location: activeFunc.location, type: SUGGESTION_TYPE.FUNCTIONS };
    }
  
    // return argument value suggestions when cursor is inside agrument value
    const activeArg = activeFunc.arguments.find((argument) => {
      return inLocation(cursorPosition, argument.location);
    });
    if (activeArg && activeArg.type === 'namedArg' && inLocation(cursorPosition, activeArg.value.location)) {
      const {
        function: functionName,
        arguments: functionArgs,
      } = activeFunc;
  
      const {
        name: argName,
        value: { text: partialInput },
      } = activeArg;
  
      let valueSuggestions;
      if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
        valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(functionName, argName, functionArgs, partialInput);
      } else {
        const {
          suggestions: staticSuggestions,
        } = functionHelp.args.find((arg) => {
          return arg.name === activeArg.name;
        });
        valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput(partialInput, staticSuggestions);
      }
      return { list: valueSuggestions, location: activeArg.value.location, type: SUGGESTION_TYPE.ARGUMENT_VALUE };
    }
  
    // return argument suggestions
    const argsHelp = getArgumentsHelp(functionHelp, activeFunc.arguments);
    const argumentSuggestions = argsHelp.filter(arg => {
      if (_.get(activeArg, 'type') === 'namedArg') {
        return _.startsWith(arg.name, activeArg.name);
      } else if (activeArg) {
        return _.startsWith(arg.name, activeArg.text);
      }
      return true;
    });
    const location = activeArg ? activeArg.location : { min: cursorPosition, max: cursorPosition };
    return { list: argumentSuggestions, location: location, type: SUGGESTION_TYPE.ARGUMENTS };
  }
  
  export async function suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions) {
    try {
      const result = await Parser.parse(expression);
      return await extractSuggestionsFromParsedResult(result, cursorPosition, functionList, argValueSuggestions);
    } catch (e) {
  
      let message;
      try {
        // The grammar will throw an error containing a message if the expression is formatted
        // correctly and is prepared to accept suggestions. If the expression is not formmated
        // correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
        // attempt will throw an error.
        message = JSON.parse(e.message);
      } catch (e) {
        // The expression isn't correctly formatted, so JSON.parse threw an error.
        return;
      }
  
      switch (message.type) {
        case 'incompleteFunction': {
          let list;
          if (message.function) {
            // The user has start typing a function name, so we'll filter the list down to only
            // possible matches.
            list = functionList.filter(func => _.startsWith(func.name, message.function));
          } else {
            // The user hasn't typed anything yet, so we'll just return the entire list.
            list = functionList;
          }
          return { list, location: message.location, type: SUGGESTION_TYPE.FUNCTIONS };
        }
        case 'incompleteArgument': {
          const {
            currentFunction: functionName,
            currentArgs: functionArgs,
          } = message;
          const functionHelp = functionList.find(func => func.name === functionName);
          return {
            list: getArgumentsHelp(functionHelp, functionArgs),
            location: message.location,
            type: SUGGESTION_TYPE.ARGUMENTS
          };
        }
        case 'incompleteArgumentValue': {
          const {
            name: argName,
            currentFunction: functionName,
            currentArgs: functionArgs,
          } = message;
          let valueSuggestions = [];
          if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
            valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(functionName, argName, functionArgs);
          } else {
            const functionHelp = functionList.find(func => func.name === functionName);
            if (functionHelp) {
              const argHelp = functionHelp.args.find(arg => arg.name === argName);
              if (argHelp && argHelp.suggestions) {
                valueSuggestions = argHelp.suggestions;
              }
            }
          }
          return {
            list: valueSuggestions,
            location: { min: cursorPosition, max: cursorPosition },
            type: SUGGESTION_TYPE.ARGUMENT_VALUE
          };
        }
      }
    }
  }
  
  export function insertAtLocation(valueToInsert, destination, replacementRangeStart, replacementRangeEnd) {
    // Insert the value at a location caret within the destination.
    const prefix = destination.slice(0, replacementRangeStart);
    const suffix =  destination.slice(replacementRangeEnd, destination.length);
    const result = `${prefix}${valueToInsert}${suffix}`;
    return result;
  }
  