/**
 * Result of rewriting a magic line.
 */
export type Rewrite = {
  text?: string;
  annotations?: MagicAnnotation[];
};

/**
 * An annotation to hold metadata about what a magic is doing.
 */
export type MagicAnnotation = {
  key: string;
  value: string;
};

/**
 * Position of a text match for magics.
 */
export type MatchPosition = [
  { line: number; col: number },
  { line: number; col: number }
];

/**
 * Interface for command-specific magic rewrites.
 */
export interface LineMagicRewriter {
  /**
   * Name of the magic command this will apply to.
   */
  commandName: string;

  /**
   * Rewrite the line magic.
   * @param matchedText the original matched text from the program
   * @param magicStmt the line magic text with newlines and continuations removed
   * @param postion ((start_line, start_col),(end_line, end_col)) of `matchedText` within the cell
   * @return rewrite operation. Leave text empty if you want to use default rewrites.
   */
  rewrite(
    matchedText: string,
    magicStmt: string,
    position: MatchPosition
  ): Rewrite;
}

/**
 * Utility to rewrite IPython code to remove magics.
 * Should be applied at to cells, not the entire program, to properly handle cell magics.
 * One of the most important aspects of the rewriter is that it shouldn't change the line number
 * of any of the statements in the program. If it does, this will make it impossible to
 * map back from the results of code analysis to the relevant code in the editor.
 */
export class MagicsRewriter {
  /**
   * Construct a magics rewriter.
   */
  constructor(lineMagicRewriters?: LineMagicRewriter[]) {
    this._lineMagicRewriters =
      lineMagicRewriters || this._defaultLineMagicRewriters;
  }

  /**
   * Rewrite code so that it doesn't contain magics.
   */
  rewrite(text: string, lineMagicRewriters?: LineMagicRewriter[]) {
    text = this.rewriteCellMagic(text);
    text = this.rewriteLineMagic(text, this._lineMagicRewriters);
    return text;
  }

  /**
   * Default rewrite rule for cell magics.
   */
  rewriteCellMagic(text: string): string {
    // 
    if (String(text).match(/^[^#\s]*\s*%%/gm)) {
      return text
        .split('\n')
        .map(l => '##' + l) // #%% is used for VS Code Python cell markers, so avoid that combo
        .join('\n');
    }
    return text;
  }

  /**
   * Default rewrite rule for line magics.
   */
  rewriteLineMagic(
    text: string,
    lineMagicRewriters?: LineMagicRewriter[]
  ): string {
    // Create a mapping from character offsets to line starts.
    let lines = String(text).split('\n');
    let lastLineStart = 0;
    let lineStarts: number[] = lines.map((line, i) => {
      if (i == 0) {
        return 0;
      }
      let lineStart = lastLineStart + lines[i - 1].length + 1;
      lastLineStart = lineStart;
      return lineStart;
    });

    // Map magic to comment and location.
    return String(text).replace(/^\s*(%(?:\\\s*\n|[^\n])+)/gm, (match, magicStmt) => {
      // Find the start and end lines where the character appeared.
      let startLine = -1,
        startCol = -1;
      let endLine = -1,
        endCol = -1;
      let offset = match.length - magicStmt.length;
      for (let i = 0; i < lineStarts.length; i++) {
        if (offset >= lineStarts[i]) {
          startLine = i;
          startCol = offset - lineStarts[i];
        }
        if (offset + magicStmt.length >= lineStarts[i]) {
          endLine = i;
          endCol = offset + magicStmt.length - lineStarts[i];
        }
      }

      let position: MatchPosition = [
        { line: startLine, col: startCol },
        { line: endLine, col: endCol },
      ];

      let magicStmtCleaned = magicStmt.replace(/\\\s*\n/g, '');
      let commandMatch = magicStmtCleaned.match(/^%(\w+).*/);
      let rewriteText;
      let annotations: MagicAnnotation[] = [];

      // Look for command-specific rewrite rules.
      if (commandMatch && commandMatch.length >= 2) {
        let command = commandMatch[1];
        if (lineMagicRewriters) {
          for (let lineMagicRewriter of lineMagicRewriters) {
            if (lineMagicRewriter.commandName == command) {
              let rewrite = lineMagicRewriter.rewrite(
                match,
                magicStmtCleaned,
                position
              );
              if (rewrite.text) {
                rewriteText = rewrite.text;
              }
              if (rewrite.annotations) {
                annotations = annotations.concat(rewrite.annotations);
              }
              break;
            }
          }
        }
      }

      // Default rewrite: comment out all lines.
      if (!rewriteText) {
        rewriteText = match
          .split('\n')
          .map(s => '#' + s)
          .join('\n');
      }

      // Add annotations to the beginning of the magic.
      for (let annotation of annotations) {
        rewriteText =
          "'''" +
          annotation.key +
          ': ' +
          annotation.value +
          "'''" +
          ' ' +
          rewriteText;
      }
      return rewriteText;
    });
  }

  private _lineMagicRewriters: LineMagicRewriter[];
  private _defaultLineMagicRewriters = [
    new TimeLineMagicRewriter(),
    new PylabLineMagicRewriter(),
  ];
}

/**
 * Line magic rewriter for the "time" magic.
 */
export class TimeLineMagicRewriter implements LineMagicRewriter {
  commandName: string = 'time';
  rewrite(
    matchedText: string,
    magicStmt: string,
    position: MatchPosition
  ): Rewrite {
    return {
      text: matchedText.replace(/^\s*%time/, match => {
        return '"' + ' '.repeat(match.length - 2) + '"';
      }),
    };
  }
}

/**
 * Line magic rewriter for the "pylab" magic.
 */
export class PylabLineMagicRewriter implements LineMagicRewriter {
  commandName: string = 'pylab';
  rewrite(
    matchedText: string,
    magicStmt: string,
    position: MatchPosition
  ): Rewrite {
    let defData = [
      'numpy',
      'matplotlib',
      'pylab',
      'mlab',
      'pyplot',
      'np',
      'plt',
      'display',
      'figsize',
      'getfigs',
    ].map(symbolName => {
      return {
        name: symbolName,
        pos: [
          [position[0].line, position[0].col],
          [position[1].line, position[1].col],
        ],
      };
    });
    return {
      annotations: [{ key: 'defs', value: JSON.stringify(defData) }],
    };
  }
}
