import { isAstNode, isYulAstNode, AstWalker } from './astWalker'
import { AstNode, LineColPosition, LineColRange, Location } from './types'
import { util } from '@remix-project/remix-lib'

export declare interface SourceMappings {
  // eslint-disable-next-line @typescript-eslint/no-misused-new
  new(): SourceMappings;
}

/**
 * Turn an character offset into a "LineColPosition".
 *
 * @param offset  The character offset to convert.
 */
export function lineColPositionFromOffset (offset: number, lineBreaks: Array<number>): LineColPosition {
  let line: number = util.findLowerBound(offset, lineBreaks)
  if (lineBreaks[line] !== offset) {
    line += 1
  }
  const beginColumn = line === 0 ? 0 : (lineBreaks[line - 1] + 1)
  return <LineColPosition>{
    line: line + 1,
    character: (offset - beginColumn) + 1
  }
}

/**
 * Turn a solc AST's "src" attribute string (s:l:f)
 * into a Location
 *
 * @param astNode  The object to convert.
 */
export function sourceLocationFromAstNode (astNode: AstNode): Location | null {
  if (isAstNode(astNode) && isYulAstNode(astNode) && astNode.src) {
    return sourceLocationFromSrc(astNode.src)
  }
  return null
}

/**
 * Break out fields of solc AST's "src" attribute string (s:l:f)
 * into its "start", "length", and "file index" components
 * and return that as a Location
 *
 * @param src  A solc "src" field.
 * @returns {Location}
 */
export function sourceLocationFromSrc (src: string): Location {
  const split = src.split(':')
  return <Location>{
    start: parseInt(split[0], 10),
    length: parseInt(split[1], 10),
    file: parseInt(split[2], 10)
  }
}

/**
 * Routines for retrieving solc AST object(s) using some criteria, usually
 * includng "src' information.
 */
// eslint-disable-next-line no-redeclare
export class SourceMappings {
  readonly source: string;
  readonly lineBreaks: Array<number>;

  constructor (source: string) {
    this.source = source

    // Create a list of line offsets which will be used to map between
    // character offset and line/column positions.
    const lineBreaks: Array<number> = []
    for (let pos = source.indexOf('\n'); pos >= 0; pos = source.indexOf('\n', pos + 1)) {
      lineBreaks.push(pos)
    }
    this.lineBreaks = lineBreaks
  };

  /**
   * Get a list of nodes that are at the given "position".
   *
   * @param astNodeType  Type of node to return or null.
   * @param position     Character offset where AST node should be located.
   */
  nodesAtPosition (astNodeType: string | null, position: Location, ast: AstNode): Array<AstNode> {
    const astWalker = new AstWalker()
    const found: Array<AstNode> = []

    const callback = function (node: AstNode): boolean {
      const nodeLocation = sourceLocationFromAstNode(node)
      if (nodeLocation &&
        nodeLocation.start === position.start &&
        nodeLocation.length === position.length) {
        if (!astNodeType || astNodeType === node.nodeType) {
          found.push(node)
        }
      }
      return true
    }
    astWalker.walkFull(ast, callback)
    return found
  }

  /**
   * Retrieve the first "astNodeType" that includes the source map at arg instIndex, or "null" if none found.
   *
   * @param astNodeType   nodeType that a found ASTNode must be. Use "null" if any ASTNode can match.
   * @param sourceLocation "src" location that the AST node must match.
   */
  findNodeAtSourceLocation (astNodeType: string | undefined, sourceLocation: Location, ast: AstNode | null): AstNode | null {
    const astWalker = new AstWalker()
    let found = null
    /* FIXME: Looking at AST walker code,
       I don't understand a need to return a boolean. */
    const callback = function (node: AstNode) {
      const nodeLocation = sourceLocationFromAstNode(node)
      if (nodeLocation &&
        nodeLocation.start === sourceLocation.start &&
        nodeLocation.length === sourceLocation.length) {
        if (astNodeType === undefined || astNodeType === node.nodeType) {
          found = node
        }
      }
      return true
    }

    astWalker.walkFull(ast, callback)
    return found
  }

  /**
   * Retrieve the line/column range position for the given source-mapping string.
   *
   * @param src  Solc "src" object containing attributes {source} and {length}.
   */
  srcToLineColumnRange (src: string): LineColRange {
    const sourceLocation = sourceLocationFromSrc(src)
    if (sourceLocation.start >= 0 && sourceLocation.length >= 0) {
      return <LineColRange>{
        start: lineColPositionFromOffset(sourceLocation.start, this.lineBreaks),
        end: lineColPositionFromOffset(sourceLocation.start + sourceLocation.length, this.lineBreaks)
      }
    } else {
      return <LineColRange>{
        start: null,
        end: null
      }
    }
  }
}
