/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

const {format} = require('util');

export type FlowResult = {
  passed: boolean,
  errors: Array<FlowError>,
  flowVersion: string,
  timing?: FlowTiming,
  memory?: FlowMemory,
};
export type FlowError = {
  kind: string,
  error_codes : Array<string>,
  level: string,
  message: Array<FlowMessage>,
  trace: ?Array<FlowMessage>,
  operation?: FlowMessage,
  extra?: FlowExtra,
  error_codes: Array<string>,
};
export type FlowMessage = {
  descr: string,
  type: "Blame" | "Comment",
  context?: ?string,
  loc?: ?FlowLoc,
  indent?: number,
};
type FlowExtra = Array<{
  message: Array<FlowMessage>,
  children: FlowExtra,
}>
export type FlowLoc = {
  source: ?string,
  type: ?("LibFile" | "SourceFile" | "JsonFile" | "Builtin"),
  start: FlowPos,
  end: FlowPos,
}
type FlowPos = {
  line: number,
  column: number,
  offset: number,
}
type FlowTimer = {
  start_age: number,
  duration: number,
}
type FlowTiming = {
  results: {
    [key: string]: {
      // Legacy fields
      start_wall_age: number,
      wall_duration: number,

      // New hotness
      wall: FlowTimer;
      user: FlowTimer;
      system: FlowTimer;
      worker_user: FlowTimer;
      worker_system: FlowTimer;
    }
  }
}
type FlowMemoryStat = {| start: number; delta: number; hwm_delta: number |}
type FlowMemoryGroup = {| [stat: string]: FlowMemoryStat |}
type FlowMemory = {|
  sub_results: { [key: string]: FlowMemory };
  [group: string]: number | FlowMemoryGroup;
|}

const noErrors: FlowResult = {
  passed: true,
  errors: [],
  flowVersion: "No version",
};

// Returns a result that is a - b
function difference(a: FlowResult, b: FlowResult): FlowResult {
  const oldHashes = {};
  const errors = [];
  for (let error of b.errors) {
    const hash = JSON.stringify(error.message);
    oldHashes[hash] = error;
  }
  for (let error of a.errors) {
    const hash = JSON.stringify(error.message);
    if (oldHashes[hash] !== undefined) {
      continue;
    }
    errors.push(JSON.parse(JSON.stringify(error)));
  }
  return {
    passed: errors.length === 0,
    errors,
    flowVersion: a.flowVersion,
  };
}

function prettyPrintWithHeader(result: FlowResult): string {
  if (result.passed) {
    return "No errors";
  }

  return format(
    "%d error%s\n%s",
    result.errors.length,
    result.errors.length === 1 ? "" : "s",
    prettyPrint(result),
  );
}

function prettyPrint(result: FlowResult): string {
  // Copy the result so we can mess with it
  result = JSON.parse(JSON.stringify(result));
  return result.errors.map(prettyPrintError).join("\n\n");
}

function mainLocOfError(error: FlowError): ?FlowLoc {
  const { operation, message } = error;
  return operation && operation.loc || message[0].loc;
}

function mergedMessagesOfError(error: FlowError): Array<FlowMessage> {
  const { level, kind, message, operation, trace, extra } = error;
  let mainLoc = mainLocOfError(error);
  let messages = [].concat(
    getHeader(mainLoc, kind, level),
    getOpReason(operation),
    message,
    getExtraMessages(extra),
    getTraceReasons(trace),
  );
  const mainFile = mainLoc && mainLoc.source || "[No file]";
  // Merge comments into blames
  return messages.reduce((acc, message) => {
    const {descr, loc, type} = message;
    if (loc != null || acc.length == 0 || type == "Blame") {
      acc.push(message);
    } else if (descr != "Error:") {
      const prev = acc[acc.length - 1];
      prev.descr = prev.descr == "" ? descr : format("%s. %s", prev.descr, descr);
    }
    return acc;
  }, []);
}

function prettyPrintError(error: FlowError): string {
  let mainLoc = mainLocOfError(error);
  const mainFile = mainLoc && mainLoc.source || "[No file]";
  const messages = mergedMessagesOfError(error);
  return messages.map(prettyPrintMessage.bind(null, mainFile)).join("\n");
}

function prettyPrintMessageOfError(error: FlowError, message: FlowMessage): string {
  let mainLoc = mainLocOfError(error);
  const mainFile = mainLoc && mainLoc.source || "[No file]";
  return prettyPrintMessage(mainFile, message);
}

function mkComment(descr: string): FlowMessage {
  return { descr, type: "Comment" };
}

function getHeader(
  mainLoc: ?FlowLoc,
  kind: string,
  level: string,
): Array<FlowMessage> {
  let line = -1;
  let filename = "[No file]";
  if (mainLoc != null) {
    const {source, start} = mainLoc;
    line = start.line;
    if (source != null) {
      filename = source;
    }
  }

  let prefix = "";
  if (kind == "internal" && level == "error") {
    prefix = "Internal error (see logs):\n";
  } else if (mainLoc != null && mainLoc.type == "LibFile") {
    if (kind == "parse" && level == "error") {
      prefix = "Library parse error:\n";
    } else if (kind == "infer") {
      prefix = "Library type error:\n";
    }
  }

  return [mkComment(format("%s%s:%d", prefix, filename, line))];
}

function getOpReason(op: ?FlowMessage): Array<FlowMessage> {
  if (op) {
    return [
      op,
      mkComment("Error:"),
    ];
  }
  return [];
}

function getExtraMessages(extra: ?FlowExtra): Array<FlowMessage> {
  if (extra) {
    const messages = extra.reduce((acc, current) => {
      const childrenMessages = current.children == null ?
        [] :
        getExtraMessages(current.children);
      const messages = acc.concat(current.message, childrenMessages);
      return messages;
    }, []);
    messages.forEach(message => message.indent = (message.indent || 0)+2);
    return messages;
  }
  return [];
}

function getTraceReasons(trace: ?Array<FlowMessage>): Array<FlowMessage> {
  if (trace != null && trace.length > 0) {
    return [{ descr: "Trace:", type: "Blame" }].concat(trace);
  }
  return [];
}

function prettyPrintMessage(
  mainFile: string,
  {context, descr, loc, indent}: FlowMessage,
): string {
  const indentation = Array((indent || 0)+1).join(" ");
  if (loc != null) {
    let startCol = loc.start.column - 1;
    let contextStr = indentation;
    if (context != null) {
      // On Windows this might have \r
      context = context.trimRight();
      // Replace tabs with spaces
      context = context.replace(/\t/g, " ");
      let lineStr = String(loc.start.line);
      if (lineStr.length < 3) {
        lineStr = ("   "+lineStr).slice(-3);
      }
      lineStr += ": ";
      let padding = Array(lineStr.length+1).join(" ");
      if (context.length > startCol) {
        padding += context.substr(0, startCol).replace(/[^\t ]/g, " ");
      }
      const underline_size = loc.start.line == loc.end.line ?
        Math.max(1, loc.end.column - startCol) :
        1;
      const underline = Array(underline_size+1).join("^");

      contextStr = format(
        "%s%s%s\n%s%s%s ",
        indentation,
        lineStr,
        context,
        indentation,
        padding,
        underline,
      );
    }
    let see_another_file = loc.source == mainFile ?
      "" :
      format(
        ". See%s: %s:%d",
        loc.type === "LibFile" ? " lib" : "",
        loc.source,
        loc.start.line,
      );
    return format("%s%s%s", contextStr, descr, see_another_file);
  }
  return indentation+descr;
}

module.exports = {
  noErrors,
  difference,
  prettyPrintWithHeader,
  prettyPrint,
  mainLocOfError,
  mergedMessagesOfError,
  prettyPrintError,
  prettyPrintMessageOfError,
};
