
import { DocumentSymbol, SymbolKind, Range, WorkspaceSymbol, Position, Location, FoldingRange } from 'vscode-languageserver';
import { SyntaxNode } from 'web-tree-sitter';
import { isFunctionDefinitionName, isVariableDefinitionName, refinedFindParentVariableDefinitionKeyword } from './utils/node-types';
//import { findVariableDefinitionOptions } from './utils/options';
import { DocumentSymbolDetail } from './utils/symbol-documentation-builder';
import { getNodeAtRange, getRange, isPositionAfter, pointToPosition } from './utils/tree-sitter';
import { ScopeTag, DefinitionScope, getScope } from './utils/definition-scope';
import { GenericTree } from './utils/generic-tree';
import { LspDocument } from './document';

// add some form of tags to the symbol so that we can extend the symbol with more information
// current implementation is WIP inside file : ./utils/options.ts
export interface FishDocumentSymbol extends DocumentSymbol {
  name: string;
  uri: string;
  text: string;
  detail: string;
  kind: SymbolKind;
  range: Range;
  selectionRange: Range;
  scope: DefinitionScope;
  children: FishDocumentSymbol[];
}

export namespace FishDocumentSymbol {
  /**
     * Creates a new symbol information literal.
     *
     * @param name The name of the symbol.
     * @param uri The documentUri of the symbol.
     * @param text The text in the symbol scope.
     * @param detail The detail of the symbol. (Markdown included inside 'range')
     * @param kind The kind of the symbol.
     * @param range The enclosing range of the symbol.
     * @param selectionRange The selectionRange of the symbol.
     * @param children Children of the symbol.
     */
  export function create(name: string, uri: string, text: string, detail: string, kind: SymbolKind, range: Range, selectionRange: Range, scope: DefinitionScope, children: FishDocumentSymbol[]): FishDocumentSymbol {
    return {
      name,
      uri,
      text,
      detail,
      kind,
      range,
      selectionRange,
      scope,
      children,
    } as FishDocumentSymbol;
  }

  export function copy(symbol: FishDocumentSymbol, newChildren: FishDocumentSymbol[] = []): FishDocumentSymbol {
    return create(
      symbol.name,
      symbol.uri,
      symbol.text,
      symbol.detail,
      symbol.kind,
      symbol.range,
      symbol.selectionRange,
      symbol.scope,
      newChildren,
    );
  }

  export function equal(a: FishDocumentSymbol, b: FishDocumentSymbol): boolean {
    return (
      a.name === b.name &&
            a.uri === b.uri &&
            a.range.start.character === b.range.start.character &&
            a.range.start.line === b.range.start.line &&
            a.range.end.character === b.range.end.character &&
            a.range.end.line === b.range.end.line &&
            a.selectionRange.start.character === b.selectionRange.start.character &&
            a.selectionRange.start.line === b.selectionRange.start.line &&
            a.selectionRange.end.line === b.selectionRange.end.line &&
            a.selectionRange.end.character === b.selectionRange.end.character
    );
  }

  export function toWorkspaceSymbol(symbol: FishDocumentSymbol): WorkspaceSymbol {
    return WorkspaceSymbol.create(
      symbol.name,
      symbol.kind,
      symbol.uri,
      symbol.range,
    );
  }

  export function toLocation(symbol: FishDocumentSymbol): Location {
    return Location.create(
      symbol.uri,
      symbol.selectionRange,
    );
  }

  export function logString(symbol: FishDocumentSymbol): string {
    const symbolIcon = symbol.kind === SymbolKind.Function ? '  ' : '  ';
    return `${symbolIcon}${symbol.name}   ::::  ${symbol.scope.scopeTag}`;
  }

  export function flattenArray(symbols: FishDocumentSymbol[]) : FishDocumentSymbol[] {
    function* flattenGenerator(symbols: FishDocumentSymbol[]): Generator<FishDocumentSymbol> {
      for (const symbol of symbols) {
        yield symbol;
        yield* flattenGenerator(symbol.children);
      }
    }
    return [...flattenGenerator(symbols)];
  }

  export function equalScopes(a: FishDocumentSymbol, b: FishDocumentSymbol): boolean {
    if (a.scope.scopeNode && b.scope.scopeNode) {
      if ([a.scope.scopeTag, b.scope.scopeTag].includes('inherit')) {
        return a.scope.scopeNode.equals(b.scope.scopeNode);
      } else if (
        ['global', 'universal'].includes(a.scope.scopeTag) &&
                ['global', 'universal'].includes(b.scope.scopeTag)
      ) {
        return true;
      }
      return a.scope.scopeTag === b.scope.scopeTag &&
                a.scope.scopeNode.equals(b.scope.scopeNode);
    }
    return false;
  }

  /*
     * the first symbol is before the second symbol
     */
  export function isBefore(first: FishDocumentSymbol, second: FishDocumentSymbol): boolean {
    return first.range.start.line < second.range.start.line;
  }

  /*
     * the first symbol is after the second symbol
     */
  export function isAfter(first: FishDocumentSymbol, second: FishDocumentSymbol): boolean {
    return first.range.start.line > second.range.start.line;
  }

  export function getSyntaxNode(root: SyntaxNode, symbol: FishDocumentSymbol): SyntaxNode | null {
    return getNodeAtRange(root, symbol.range);
  }

  export function toTree(symbols: FishDocumentSymbol[]) {
    return new GenericTree<FishDocumentSymbol>(symbols);
  }

  export function debug(symbol: FishDocumentSymbol) {
    const positionString = (pos: Position) => `(line: ${pos.line}, char: ${pos.character})`;
    const rangeString = (n: SyntaxNode) => {
      const range = getRange(n);
      return `${positionString(range.start)} --- ${positionString(range.end)}`;
    };

    const scopeNodeLines = symbol.scope.scopeNode.text.split('\n');
    return {
      name: symbol.name,
      range: positionString(symbol.range.start) + ' --- ' + positionString(symbol.range.end),
      selectionRange: positionString(symbol.selectionRange.start) + ' --- ' + positionString(symbol.selectionRange.end),
      text: symbol.text.split('\n').length > 1
        ? symbol.text + '...'
        : symbol.text,
      scope: {
        scopeTag: symbol.scope.scopeTag,
        scopeNode: {
          text: scopeNodeLines[0] + '...',
          type: symbol.scope.scopeNode.type,
          range: rangeString(symbol.scope.scopeNode),
        },
      },
      type: symbol.kind === SymbolKind.Function ? 'function' : 'variable',
      uri: symbol.uri,
    };
  }

  export function toFoldingRange(symbol: FishDocumentSymbol): FoldingRange {
    return {
      startLine: symbol.range.start.line,
      endLine: symbol.range.end.line,
      collapsedText: symbol.name,
    };
    //return FoldingRange.create(
    //    symbol.range.start.line,
    //    symbol.range.end.line,
    //    symbol.range.start.character,
    //    symbol.range.end.character,
    //    FoldingRangeKind.Region,
    //    symbol.name
    //)
  }

  //export function toGlobalCompletion(symbol: FishDocumentSymbol, data: FishCompletionData): FishCompletionItem {
  //    const kind = symbol.kind === SymbolKind.Function ? FishCompletionItemKind.GLOBAL_FUNCTION : FishCompletionItemKind.GLOBAL_VARIABLE;
  //    const detail: MarkupContent = {kind: 'markdown', value: symbol.detail}
  //    return createCompletionItem(symbol.name, kind, detail, data)
  //}
  //
  //export function toLocalCompletion(symbol: FishDocumentSymbol, data: FishCompletionData): FishCompletionItem {
  //    const kind = symbol.kind === SymbolKind.Function
  //        ? isGlobalSymbol(symbol) ? FishCompletionItemKind.USER_FUNCTION : FishCompletionItemKind.LOCAL_FUNCTION
  //        : isGlobalSymbol(symbol) ? FishCompletionItemKind.GLOBAL_VARIABLE : FishCompletionItemKind.LOCAL_VARIABLE;
  //    const detail: MarkupContent = {kind: 'markdown', value: symbol.detail}
  //    return createCompletionItem(symbol.name, kind, detail, data)
  //}

  export type MockSymbol = {
    name: string;
    scope: ScopeTag;
    range: Range;
  };

  export function toMock(symbol: FishDocumentSymbol): MockSymbol {
    const { name, scope, range } = symbol;
    return {
      name,
      scope: scope.scopeTag,
      range,
    };
  }

  export function createMock(name: string, scope: ScopeTag, range: Range): MockSymbol {
    return {
      name,
      scope,
      range,
    };
  }
}

/**
 * Checks if a FishDocumentSymbol's state, should NOT be changeable.
 * Renaming a FishDocumentSymbol across the entire workspace, shouldn't
 * be possible for internal symbols (seen in '/usr/share/fish/**.fish').
 */
export function symbolIsImmutable(symbol: FishDocumentSymbol): boolean {
  const { uri, scope } = symbol;
  return uri.startsWith('/usr/share/fish/') || scope.scopeTag === 'universal';
}

export function isGlobalSymbol(symbol: FishDocumentSymbol): boolean {
  return symbol.scope.scopeTag === 'global';
}

export function isUniversalSymbol(symbol: FishDocumentSymbol): boolean {
  return symbol.scope.scopeTag === 'universal';
}

export function filterGlobalSymbols(symbols: FishDocumentSymbol[]): FishDocumentSymbol[] {
  return FishDocumentSymbol
    .toTree(symbols)
    .toFlatArray()
    .filter((symbol) => symbol.scope.scopeTag === 'global');
}

export function filterLocalSymbols(symbols: FishDocumentSymbol[]): FishDocumentSymbol[] {
  return FishDocumentSymbol
    .toTree(symbols)
    .toFlatArray()
    .filter((symbol) => symbol.scope.scopeTag !== 'global' && symbol.scope.scopeTag !== 'universal');
}

export function filterLastPerScopeSymbol(symbolArray: FishDocumentSymbol[]) {
  const symbolTree: GenericTree<FishDocumentSymbol> = new GenericTree(symbolArray);
  const flatArray: FishDocumentSymbol[] = symbolTree.toFlatArray();
  return symbolTree
    .filterToTree((symbol: FishDocumentSymbol) => !flatArray.some((s) => {
      return (
        s.name === symbol.name &&
                !FishDocumentSymbol.equal(symbol, s) &&
                FishDocumentSymbol.equalScopes(symbol, s) &&
                FishDocumentSymbol.isBefore(symbol, s)
      );
    }))
    .toArray();
}

const compareSymbolToPosition = (symbol: FishDocumentSymbol, position: Position) => {
  const compareHelper = (symbol: FishDocumentSymbol, position: Position) => {
    const { scope } = symbol;
    if (['global', 'universal'].includes(scope.scopeTag)) {
      return true;
    }
    return scope.containsPosition(position);
  };

  return symbol.kind === SymbolKind.Function
    ? compareHelper(symbol, position)
    : symbol.scope.containsPosition(position)
           && isPositionAfter(symbol.selectionRange.end, position);
};

export function findSymbolsForCompletion(symbols: FishDocumentSymbol[], position: Position): FishDocumentSymbol[] {
  const symbolTree = new GenericTree<FishDocumentSymbol>(symbols);
  const possibleDuplicates = symbolTree
    .filterToTree((symbol: FishDocumentSymbol) => compareSymbolToPosition(symbol, position))
    .toFlatArray()
    .reverse();
  const uniqueSymbolsArray: FishDocumentSymbol[] = [];
  for (const symbol of possibleDuplicates) {
    if (uniqueSymbolsArray.some((s) => s.name === symbol.name)) {
      continue;
    }
    uniqueSymbolsArray.push(symbol);
  }
  return uniqueSymbolsArray;
}

/**
 * finds all symbols (variables and function that have been defined)
 */
export function findSymbolReferences(symbols: FishDocumentSymbol[], matchSymbol: FishDocumentSymbol): FishDocumentSymbol[] {
  return new GenericTree<FishDocumentSymbol>(symbols)
    .filterToTree((symbol: FishDocumentSymbol) => {
      //if (symbol.scope.scopeTag === 'global' ) return true;
      return matchSymbol.name === symbol.name
                && FishDocumentSymbol.equalScopes(matchSymbol, symbol);
    })
    .toFlatArray();
}

export function findLastDefinition(symbols: FishDocumentSymbol[], matchNode: SyntaxNode) {
  const symbolTree = new GenericTree<FishDocumentSymbol>(symbols);
  const symbolFunctionCompare = (symbol: FishDocumentSymbol, matchNode: SyntaxNode) => {
    const matchPosition = pointToPosition(matchNode.startPosition);
    const { name, kind: _kind, scope: _scope } = symbol;
    return name === matchNode.text
                && compareSymbolToPosition(symbol, matchPosition);
  };
  return symbolTree
    .filterToTree((symbol: FishDocumentSymbol) => symbolFunctionCompare(symbol, matchNode))
    .toFlatArray()
    .pop();
}

/**
 * TreeSitter definition nodes in fish shell rely on commands, and thus create trees that
 * need specific traversals per command. Creates a standard object of properties to be
 * deconstructed into a FishDocumentSymbol. Where parent is the root most node of the
 * entire command to create a symbol. Child is the identifier of the symbol.
 *
 * See fish below:
 * ---------------------------------------------------------------------------------------
 * set -gx FOO BAR; # FOO is a variable we globally define and export
 * ---------------------------------------------------------------------------------------
 * Child is just the identifier `$FOO`
 * Parent is the entire string `set -gx FOO BAR;` for the command
 */
export function definitionSymbolHandler(node: SyntaxNode): {
  shouldCreate: boolean;
  kind: SymbolKind;
  child: SyntaxNode;
  parent: SyntaxNode;
} {
  let shouldCreate = false;
  let [child, parent] = [node, node.parent || node];
  let kind: SymbolKind = SymbolKind.Null;
  if (isVariableDefinitionName(node)) {
    parent = refinedFindParentVariableDefinitionKeyword(node)!.parent!;
    kind = SymbolKind.Variable;
    shouldCreate = true;
    if (node.text.startsWith('$')) {
      shouldCreate = false;
    }
  } else if (node.firstNamedChild && isFunctionDefinitionName(node.firstNamedChild)) {
    parent = node;
    child = node.firstNamedChild!;
    kind = SymbolKind.Function;
    shouldCreate = true;
  }
  return {
    shouldCreate,
    kind,
    child,
    parent,
  };
}

/**
 * Creates all FishDocumentSymbols in a file
 * @param {string} uri - path to the file
 * @param {SyntaxNode[]} currentNodes - root node(s) to traverse for definitions
 * @returns {FishDocumentSymbol[]} - all defined FishDocumentSymbol's in file
 */
export function getFishDocumentSymbols(document: LspDocument, ...currentNodes: SyntaxNode[]): FishDocumentSymbol[] {
  const symbols: FishDocumentSymbol[] = [];
  for (const node of currentNodes) {
    const childrenSymbols = getFishDocumentSymbols(document, ...node.children);
    const { shouldCreate, kind, child, parent } = definitionSymbolHandler(node);
    if (shouldCreate) {
      symbols.push(
        FishDocumentSymbol.create(
          child.text,
          document.uri,
          parent.text,
          DocumentSymbolDetail.create(child.text, document.uri, kind, child),
          kind,
          getRange(parent),
          getRange(child),
          getScope(document, child),
          childrenSymbols,
        ),
      );
      continue;
    }
    symbols.push(...childrenSymbols);
  }
  return symbols;
}
