import {
  Location,
  TokenKind,
  GraphQLOutputType,
  GraphQLNamedType,
  isNonNullType,
  isListType,
  Token,
  Source,
  Lexer,
} from 'graphql';
import type { Comment, SourceLocation } from 'estree';
import type { AST } from 'eslint';
import { valueFromASTUntyped } from 'graphql/utilities/valueFromASTUntyped';

export const valueFromNode = (...args: Parameters<typeof valueFromASTUntyped>): any => {
  return valueFromASTUntyped(...args);
};

export function getBaseType(type: GraphQLOutputType): GraphQLNamedType {
  if (isNonNullType(type) || isListType(type)) {
    return getBaseType(type.ofType);
  }
  return type;
}

// Hardcoded type because tests fails on graphql 15
type TokenKindValue =
  | '<SOF>'
  // | '<EOF>'
  | '!'
  | '$'
  | '&'
  | '('
  | ')'
  | '...'
  | ':'
  | '='
  | '@'
  | '['
  | ']'
  | '{'
  | '|'
  | '}'
  | 'Name'
  | 'Int'
  | 'Float'
  | 'String'
  | 'BlockString'
  | 'Comment';

export function convertToken<T extends 'Line' | 'Block' | TokenKindValue>(
  token: Token,
  type: T
): Omit<AST.Token, 'type'> & { type: T } {
  const { line, column, end, start, value } = token;
  return {
    type,
    value,
    /*
     * ESLint has 0-based column number
     * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
     */
    loc: {
      start: {
        line,
        column: column - 1,
      },
      end: {
        line,
        column: column - 1 + (end - start),
      },
    },
    range: [start, end],
  };
}

function getLexer(source: Source): Lexer {
  // GraphQL v14
  const gqlLanguage = require('graphql/language');
  if (gqlLanguage && gqlLanguage.createLexer) {
    return gqlLanguage.createLexer(source, {});
  }

  // GraphQL v15
  const { Lexer: LexerCls } = require('graphql');
  if (LexerCls && typeof LexerCls === 'function') {
    return new LexerCls(source);
  }

  throw new Error('Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!');
}

export function extractTokens(filePath: string, code: string): AST.Token[] {
  const source = new Source(code, filePath);
  const lexer = getLexer(source);
  const tokens: AST.Token[] = [];
  let token = lexer.advance();

  while (token && token.kind !== TokenKind.EOF) {
    const result = convertToken(token, token.kind) as AST.Token;
    tokens.push(result);
    token = lexer.advance();
  }

  return tokens;
}

export function extractComments(loc: Location): Comment[] {
  if (!loc) {
    return [];
  }
  const comments: Comment[] = [];
  let token = loc.startToken;

  while (token) {
    if (token.kind === TokenKind.COMMENT) {
      const comment = convertToken(
        token,
        // `eslint-disable` directive works only with `Block` type comment
        token.value.trimStart().startsWith('eslint') ? 'Block' : 'Line'
      );
      comments.push(comment);
    }
    token = token.next;
  }
  return comments;
}

export function convertLocation(location: Location): SourceLocation {
  const { startToken, endToken, source, start, end } = location;
  /*
   * ESLint has 0-based column number
   * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
   */
  const loc = {
    start: {
      /*
       * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
       */
      line: startToken.line === 0 ? 1 : startToken.line,
      column: startToken.column === 0 ? 0 : startToken.column - 1,
    },
    end: {
      line: endToken.line,
      column: endToken.column - 1,
    },
    source: source.body,
  };
  if (loc.start.column === loc.end.column) {
    loc.end.column += end - start;
  }
  return loc;
}