/*
 This file is part of TALER
 (C) 2019 GNUnet e.V.

 TALER is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 TALER is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * Check if we are running under nodejs.
 */

const isNode =
  typeof process !== "undefined" &&
  typeof process.release !== "undefined" &&
  process.release.name === "node";

export enum LogLevel {
  Trace = "trace",
  Message = "message",
  Info = "info",
  Warn = "warn",
  Error = "error",
  None = "none",
}

let globalLogLevel = LogLevel.Info;
const byTagLogLevel: Record<string, LogLevel> = {};

let nativeLogging: boolean = false;

// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString
Error.prototype.toString = function () {
  if (
    this === null ||
    (typeof this !== "object" && typeof this !== "function")
  ) {
    throw new TypeError();
  }
  let name = this.name;
  name = name === undefined ? "Error" : `${name}`;
  let msg = this.message;
  msg = msg === undefined ? "" : `${msg}`;

  let cause = "";
  if ("cause" in this) {
    cause = `\n Caused by: ${this.cause}`;
  }
  return `${name}: ${msg}${cause}`;
};

export function getGlobalLogLevel(): string {
  return globalLogLevel;
}

export function setGlobalLogLevelFromString(logLevelStr: string): void {
  globalLogLevel = getLevelForString(logLevelStr);
}

export function setLogLevelFromString(tag: string, logLevelStr: string): void {
  byTagLogLevel[tag] = getLevelForString(logLevelStr);
}

export function enableNativeLogging() {
  nativeLogging = true;
}

function getLevelForString(logLevelStr: string): LogLevel {
  switch (logLevelStr.toLowerCase()) {
    case "trace":
      return LogLevel.Trace;
    case "info":
      return LogLevel.Info;
    case "warn":
    case "warning":
      return LogLevel.Warn;
    case "error":
      return LogLevel.Error;
    case "none":
      return LogLevel.None;
    default:
      if (isNode) {
        process.stderr.write(`Invalid log level, defaulting to WARNING\n`);
      } else {
        console.warn(`Invalid log level, defaulting to WARNING`);
      }
      return LogLevel.Warn;
  }
}

function writeNativeLog(
  message: any,
  tag: string,
  level: number,
  args: any[],
): void {
  const logFn = (globalThis as any).__nativeLog;
  if (logFn) {
    let m: string;
    if (args.length == 0) {
      m = message;
    } else {
      m = message + " " + args.toString();
    }
    logFn(level, tag, message);
  }
}

function writeNodeLog(
  message: any,
  tag: string,
  level: string,
  args: any[],
): void {
  try {
    let msg = `${new Date().toISOString()} ${tag} ${level} ${message}`;
    if (args.length != 0) {
      msg += ` ${JSON.stringify(args, undefined, 2)}\n`;
    } else {
      msg += `\n`;
    }
    process.stderr.write(msg);
  } catch (e) {
    // This can happen when we're trying to log something that doesn't want to be
    // converted to a string.
    let msg = `${new Date().toISOString()} (logger) FATAL `;
    if (e instanceof Error) {
      msg += `failed to write log: ${e.message}\n`;
    } else {
      msg += "failed to write log\n";
    }
    process.stderr.write(msg);
  }
}

/**
 * Logger that writes to stderr when running under node,
 * and uses the corresponding console.* method to log in the browser.
 */
export class Logger {
  constructor(private tag: string) {}

  getGlobalLogLevel(): string {
    return globalLogLevel;
  }

  shouldLogTrace(): boolean {
    const level = byTagLogLevel[this.tag] ?? globalLogLevel;
    switch (level) {
      case LogLevel.Trace:
        return true;
      case LogLevel.Message:
      case LogLevel.Info:
      case LogLevel.Warn:
      case LogLevel.Error:
      case LogLevel.None:
        return false;
    }
  }

  shouldLogInfo(): boolean {
    const level = byTagLogLevel[this.tag] ?? globalLogLevel;
    switch (level) {
      case LogLevel.Trace:
      case LogLevel.Message:
      case LogLevel.Info:
        return true;
      case LogLevel.Warn:
      case LogLevel.Error:
      case LogLevel.None:
        return false;
    }
  }

  shouldLogWarn(): boolean {
    const level = byTagLogLevel[this.tag] ?? globalLogLevel;
    switch (level) {
      case LogLevel.Trace:
      case LogLevel.Message:
      case LogLevel.Info:
      case LogLevel.Warn:
        return true;
      case LogLevel.Error:
      case LogLevel.None:
        return false;
    }
  }

  shouldLogError(): boolean {
    const level = byTagLogLevel[this.tag] ?? globalLogLevel;
    switch (level) {
      case LogLevel.Trace:
      case LogLevel.Message:
      case LogLevel.Info:
      case LogLevel.Warn:
      case LogLevel.Error:
        return true;
      case LogLevel.None:
        return false;
    }
  }

  info(message: string, ...args: any[]): void {
    if (!this.shouldLogInfo()) {
      return;
    }
    if (nativeLogging) {
      writeNativeLog(message, this.tag, 2, args);
      return;
    }
    if (isNode) {
      writeNodeLog(message, this.tag, "INFO", args);
    } else {
      console.info(
        `${new Date().toISOString()} ${this.tag} INFO ` + message,
        ...args,
      );
    }
  }

  warn(message: string, ...args: any[]): void {
    if (!this.shouldLogWarn()) {
      return;
    }
    if (nativeLogging) {
      writeNativeLog(message, this.tag, 3, args);
      return;
    }
    if (isNode) {
      writeNodeLog(message, this.tag, "WARN", args);
    } else {
      console.warn(
        `${new Date().toISOString()} ${this.tag} INFO ` + message,
        ...args,
      );
    }
  }

  error(message: string, ...args: any[]): void {
    if (!this.shouldLogError()) {
      return;
    }
    if (nativeLogging) {
      writeNativeLog(message, this.tag, 4, args);
      return;
    }
    if (isNode) {
      writeNodeLog(message, this.tag, "ERROR", args);
    } else {
      console.info(
        `${new Date().toISOString()} ${this.tag} ERROR ` + message,
        ...args,
      );
    }
  }

  trace(message: string, ...args: any[]): void {
    if (!this.shouldLogTrace()) {
      return;
    }
    if (nativeLogging) {
      writeNativeLog(message, this.tag, 1, args);
      return;
    }
    if (isNode) {
      writeNodeLog(message, this.tag, "TRACE", args);
    } else {
      console.info(
        `${new Date().toISOString()} ${this.tag} TRACE ` + message,
        ...args,
      );
    }
  }

  reportBreak(): void {
    if (!this.shouldLogError()) {
      return;
    }
    const location = new Error("programming error");
    this.error(`assertion failed: ${location.stack}`);
  }
}
