import {DescriptionPair} from "./completion/descriptionDictionary";
import {Dictionary, isDictionary} from "./data/dictionary";
import {ExpressionData} from "./data/expressiondata";
import {Evaluator} from "./evaluator";
import {wellKnownFunctions} from "./funcs";
import {FunctionDefinition, FunctionInfo} from "./funcs/info";
import {Lexer, Token, TokenType} from "./lexer";
import {Parser} from "./parser";

export type CompletionItem = {
  label: string;
  description?: string;
  function: boolean;
};

/**
 * Complete returns a list of completion items for the given expression.
 * The main functionality is auto-completing functions and context access:
 * We can only provide assistance if the input is in one of the following forms (with | denoting the cursor position):
 * - context.path.inp| or context.path['inp| -- auto-complete context access
 * - context.path.| or context.path['| -- auto-complete context access
 * - toJS| -- auto-complete function call or top-level
 * - | -- auto-complete function call or top-level context access
 *
 * @param input Input expression
 * @param context Context available for the expression
 * @param extensionFunctions List of functions available
 * @param functions Optional map of functions to use during evaluation
 * @returns Array of completion items
 */
export function complete(
  input: string,
  context: Dictionary,
  extensionFunctions: FunctionInfo[],
  functions?: Map<string, FunctionDefinition>
): CompletionItem[] {
  // Lex
  const lexer = new Lexer(input);
  const lexResult = lexer.lex();

  // Find interesting part of the tokenVector. For example, for an expression like `github.actor == env.actor.log|`, we are
  // only interested in the `env.actor.log` part for auto-completion
  const tokenInputVector = trimTokenVector(lexResult.tokens);

  // Start by skipping the EOF token
  let tokenIdx = tokenInputVector.length - 2;

  if (tokenIdx >= 0) {
    switch (tokenInputVector[tokenIdx].type) {
      // If there is a (partial) identifier under the cursor, ignore that
      case TokenType.IDENTIFIER:
        tokenIdx--;
        break;

      case TokenType.STRING:
        // TODO: Support string for `context.name['test|`
        return [];
    }
  }

  if (tokenIdx < 0) {
    // Vector only contains the EOF token. Suggest functions and root context access
    const result = contextKeys(context);

    // Merge with functions
    result.push(...functionItems(extensionFunctions));

    return result;
  }

  // Determine path that led to the last token
  // Use parser & evaluator to determine context to complete.
  const pathTokenVector = tokenInputVector.slice(0, tokenIdx);

  // Include the original EOF token to make the parser happy
  pathTokenVector.push(tokenInputVector[tokenInputVector.length - 1]);

  const p = new Parser(
    pathTokenVector,
    context.pairs().map(x => x.key),
    extensionFunctions
  );
  const expr = p.parse();

  const ev = new Evaluator(expr, context, functions);
  const result = ev.evaluate();

  return contextKeys(result);
}

function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
  const result: CompletionItem[] = [];

  for (const fdef of [...Object.values(wellKnownFunctions), ...extensionFunctions]) {
    result.push({
      label: fdef.name,
      description: fdef.description,
      function: true
    });
  }

  // Sort functions
  result.sort((a, b) => a.label.localeCompare(b.label));

  return result;
}

function contextKeys(context: ExpressionData): CompletionItem[] {
  if (isDictionary(context)) {
    return (
      context
        .pairs()
        .map(x => completionItemFromContext(x))
        // Sort contexts
        .sort((a, b) => a.label.localeCompare(b.label))
    );
  }

  return [];
}

function completionItemFromContext(pair: DescriptionPair): CompletionItem {
  const context = pair.key.toString();
  const parenIndex = context.indexOf("(");
  const isFunc = parenIndex >= 0 && context.indexOf(")") >= 0;

  return {
    label: isFunc ? context.substring(0, parenIndex) : context,
    description: pair.description,
    function: isFunc
  };
}

export function trimTokenVector(tokenVector: Token[]): Token[] {
  let tokenIdx = tokenVector.length;

  let openParen = 0;

  while (tokenIdx > 0) {
    const token = tokenVector[tokenIdx - 1];
    switch (token.type) {
      case TokenType.LEFT_PAREN:
        if (openParen == 0) {
          // Encountered an open parenthesis without a closing first, stop here
          break;
        }
        openParen--;
        tokenIdx--;
        continue;

      case TokenType.RIGHT_PAREN:
        openParen++;
        tokenIdx--;
        continue;

      case TokenType.IDENTIFIER:
      case TokenType.DOT:
      case TokenType.EOF:
      case TokenType.LEFT_BRACKET:
      case TokenType.RIGHT_BRACKET:
      case TokenType.STRING:
        tokenIdx--;
        continue;
    }

    break;
  }

  // Only keep the part of the token vector we're interested in
  return tokenVector.slice(tokenIdx);
}
