import {isString} from "@github/actions-workflow-parser";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types";
import {Position} from "vscode-languageserver-textdocument";

export function findInnerToken(pos: Position, root?: TemplateToken) {
  const {token} = findToken(pos, root);
  return token;
}

export type TokenResult = {
  parent: TemplateToken | null;
  keyToken: TemplateToken | null;
  token: TemplateToken | null;

  path: TemplateToken[];
};

/**
 * Find a token at the given position in the document.
 *
 * If the position is within
 * - the key of a mapping, parent will be the mapping, keyToken will be null, and token will be the key.
 * - the value of a mapping, parent will be the mapping, keyToken will be the key for the value, and token will be the value
 * - a sequence item, parent will be the sequence, keyToken will be null, and token will be the item
 *
 * @param pos Position within the document for which to find a token
 * @param root Root node
 * @returns Token result
 */
export function findToken(pos: Position, root?: TemplateToken): TokenResult {
  if (!root) {
    return {
      token: null,
      keyToken: null,
      parent: null,
      path: []
    };
  }

  let lastMatching: TokenResult | null = null;

  const s: TokenResult[] = [
    {
      token: root,
      keyToken: null,
      parent: null,
      path: []
    }
  ];

  while (s.length > 0) {
    const result = s.shift()!;
    const {parent, token, keyToken, path} = result;
    if (!token) {
      break;
    }

    if (!posInToken(pos, token)) {
      continue;
    }

    // Pos is in token, remember this token
    lastMatching = result;

    // Position is in token, enqueue children if there are any
    switch (token.templateTokenType) {
      case TokenType.Mapping:
        const mappingToken = token as MappingToken;
        for (const {key, value} of mappingToken) {
          // If the position is within the key, immediately return it as the token.
          if (posInToken(pos, key)) {
            return {
              parent: mappingToken,
              keyToken: null,
              token: key,
              path: [...path, mappingToken]
            };
          }

          // If pos, key, and value are on the same line, and value is an empty node (null, empty string) return early
          // we cannot reliably check the position in that empty node
          if (onSameLine(pos, key, value) && emptyNode(value)) {
            return {
              parent: mappingToken,
              keyToken: key,
              token: value,
              path: [...path, mappingToken]
            };
          }

          s.push({
            parent: mappingToken,
            keyToken: key,
            token: value,
            path: [...path, mappingToken, key]
          });
        }
        continue;

      case TokenType.Sequence:
        const sequenceToken = token as SequenceToken;
        for (const token of sequenceToken) {
          s.push({
            parent: sequenceToken,
            keyToken: null,
            token: token,
            path: [...path, sequenceToken]
          });
        }
        continue;
    }

    return {
      token,
      keyToken,
      parent,
      path
    };
  }

  // Did not find a matching token, return the last matching token as parent
  return {
    token: null,
    parent: lastMatching?.token ?? null,
    keyToken: null,
    path: lastMatching?.token ? [...lastMatching.path, lastMatching.token] : []
  };
}

function posInToken(pos: Position, token: TemplateToken): boolean {
  if (!token.range) {
    return false;
  }
  const r = token.range;

  // TokenRange is one-based, Position is zero-based
  const tokenLine = pos.line + 1;
  const tokenChar = pos.character + 1;

  // Check lines
  if (r.start.line > tokenLine || tokenLine > r.end.line) {
    return false;
  }

  // Position is within the token lines. Check character/column if pos line matches
  // start or end
  if (
    (r.start.line === tokenLine && tokenChar < r.start.column) ||
    (r.end.line === tokenLine && tokenChar > r.end.column)
  ) {
    return false;
  }

  return true;
}

function onSameLine(pos: Position, key: TemplateToken, value: TemplateToken): boolean {
  if (!value.range) {
    return false;
  }

  if (!key.range) {
    return false;
  }

  if (value.range.start.line !== value.range.end.line) {
    // Token occupies multiple lines, can't be an empty node
    return false;
  }

  // TokenRange is one-based, Position is zero-based
  const posLine = pos.line + 1;
  if (posLine != value.range.start.line) {
    return false;
  }

  return true;
}

function emptyNode(token: TemplateToken | null): boolean {
  if (!token) {
    return false;
  }

  if (token.templateTokenType === TokenType.Null) {
    return true;
  }

  if (isString(token)) {
    return token.value === "";
  }

  return false;
}
