import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/number/number_utils.dart';
import 'package:guru_utils/random/random_utils.dart';
import 'package:logger/logger.dart';
import 'package:persistent/log/persistent_log.dart';

import 'dart:developer' as developer;

/// Created by Haoyi on 4/13/21
///
///

class LogLevel {
  static const verbose = 0;
  static const debug = 1;
  static const info = 2;
  static const warning = 3;
  static const error = 4;
  static const wtf = 5;
  static const nothing = -1;

  static const List<Level> levels = [
    Level.trace,
    Level.debug,
    Level.info,
    Level.warning,
    Level.error,
    Level.fatal,
  ];
}

abstract class _TerminalColor {
  String call(String msg) => msg;
}

class CmdColor extends _TerminalColor {
  final String cmd;

  CmdColor(this.cmd);

  @override
  String call(String msg) {
    return "$cmd$msg${AnsiColor.ansiDefault}";
  }
}

/// This class handles colorizing of terminal output.
class AnsiColor extends _TerminalColor {
  /// ANSI Control Sequence Introducer, signals the terminal for new settings.
  static const ansiEsc = '\x1B[';

  /// Reset all colors and options for current SGRs to terminal defaults.
  static const ansiDefault = '${ansiEsc}0m';

  final int? fg;
  final int? bg;
  final bool color;

  AnsiColor.none()
      : fg = null,
        bg = null,
        color = false;

  AnsiColor.fg(this.fg)
      : bg = null,
        color = true;

  AnsiColor.bg(this.bg)
      : fg = null,
        color = true;

  @override
  String toString() {
    if (fg != null) {
      return '${ansiEsc}38;5;${fg}m';
    } else if (bg != null) {
      return '${ansiEsc}48;5;${bg}m';
    } else {
      return '';
    }
  }

  @override
  String call(String msg) {
    if (!kIsWeb && color && Platform.isAndroid) {
      return '${this}$msg$ansiDefault';
    } else {
      return msg;
    }
  }

  AnsiColor toFg() => AnsiColor.fg(bg);

  AnsiColor toBg() => AnsiColor.bg(fg);

  /// Defaults the terminal's foreground color without altering the background.
  String get resetForeground => color ? '${ansiEsc}39m' : '';

  /// Defaults the terminal's background color without altering the foreground.
  String get resetBackground => color ? '${ansiEsc}49m' : '';

  static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round();
}

class LogRecord {
  final int sequence;
  final DateTime time;
  final Level level;
  final String? tag;
  final String msg;

  const LogRecord(this.sequence, this.time, this.level, this.tag, this.msg);
}

class Log {
  static final errorFgColor = AnsiColor.fg(1);

  static String _appName = "GAME";

  static final levelFgColors = [
    AnsiColor.none(),
    AnsiColor.fg(33),
    AnsiColor.fg(40),
    AnsiColor.fg(208),
    AnsiColor.fg(1),
    AnsiColor.fg(129),
  ];

  static final tagCandidateColor = [
    AnsiColor.fg(1),
    AnsiColor.fg(2),
    AnsiColor.fg(3),
    AnsiColor.fg(4),
    AnsiColor.fg(5),
    AnsiColor.fg(6),
    AnsiColor.fg(9),
    AnsiColor.fg(10),
    AnsiColor.fg(11),
    AnsiColor.fg(12),
    AnsiColor.fg(13),
    AnsiColor.fg(14),
    AnsiColor.fg(28),
    AnsiColor.fg(29),
    AnsiColor.fg(30),
    AnsiColor.fg(31),
    AnsiColor.fg(32),
    AnsiColor.fg(33),
    AnsiColor.fg(34),
    AnsiColor.fg(35),
    AnsiColor.fg(36),
    AnsiColor.fg(37),
    AnsiColor.fg(38),
    AnsiColor.fg(39),
    AnsiColor.fg(40),
    AnsiColor.fg(41),
    AnsiColor.fg(42),
    AnsiColor.fg(43),
    AnsiColor.fg(44),
    AnsiColor.fg(45),
    AnsiColor.fg(46),
    AnsiColor.fg(47),
    AnsiColor.fg(48),
    AnsiColor.fg(49),
    AnsiColor.fg(50),
    AnsiColor.fg(51),
    AnsiColor.fg(68),
    AnsiColor.fg(69),
    AnsiColor.fg(70),
    AnsiColor.fg(71),
    AnsiColor.fg(72),
    AnsiColor.fg(73),
    AnsiColor.fg(74),
    AnsiColor.fg(75),
    AnsiColor.fg(76),
    AnsiColor.fg(77),
    AnsiColor.fg(78),
    AnsiColor.fg(79),
    AnsiColor.fg(80),
    AnsiColor.fg(81),
    AnsiColor.fg(82),
    AnsiColor.fg(83),
    AnsiColor.fg(84),
    AnsiColor.fg(85),
    AnsiColor.fg(86),
    AnsiColor.fg(87),
    AnsiColor.fg(88),
    AnsiColor.fg(89),
    AnsiColor.fg(90),
    AnsiColor.fg(91),
    AnsiColor.fg(92),
    AnsiColor.fg(93),
    AnsiColor.fg(94),
    AnsiColor.fg(95),
    AnsiColor.fg(96),
    AnsiColor.fg(97),
    AnsiColor.fg(98),
    AnsiColor.fg(99),
    AnsiColor.fg(100),
    AnsiColor.fg(101),
    AnsiColor.fg(102),
    AnsiColor.fg(103),
    AnsiColor.fg(104),
    AnsiColor.fg(105),
    AnsiColor.fg(106),
    AnsiColor.fg(107),
    AnsiColor.fg(108),
    AnsiColor.fg(109),
    AnsiColor.fg(110),
    AnsiColor.fg(111),
    AnsiColor.fg(112),
    AnsiColor.fg(113),
    AnsiColor.fg(114),
    AnsiColor.fg(115),
    AnsiColor.fg(116),
    AnsiColor.fg(117),
    AnsiColor.fg(118),
    AnsiColor.fg(119),
    AnsiColor.fg(120),
    AnsiColor.fg(121),
    AnsiColor.fg(122),
    AnsiColor.fg(123),
    AnsiColor.fg(124),
    AnsiColor.fg(125),
    AnsiColor.fg(126),
    AnsiColor.fg(127),
    AnsiColor.fg(128),
    AnsiColor.fg(129),
    AnsiColor.fg(130),
    AnsiColor.fg(131),
    AnsiColor.fg(132),
    AnsiColor.fg(133),
    AnsiColor.fg(134),
    AnsiColor.fg(135),
    AnsiColor.fg(136),
    AnsiColor.fg(137),
    AnsiColor.fg(138),
    AnsiColor.fg(139),
    AnsiColor.fg(140),
    AnsiColor.fg(141),
    AnsiColor.fg(142),
    AnsiColor.fg(143),
    AnsiColor.fg(144),
    AnsiColor.fg(145),
    AnsiColor.fg(146),
    AnsiColor.fg(147),
    AnsiColor.fg(148),
    AnsiColor.fg(149),
    AnsiColor.fg(150),
    AnsiColor.fg(151),
    AnsiColor.fg(152),
    AnsiColor.fg(153),
    AnsiColor.fg(154),
    AnsiColor.fg(155),
    AnsiColor.fg(156),
    AnsiColor.fg(157),
    AnsiColor.fg(158),
    AnsiColor.fg(159),
    AnsiColor.fg(160),
    AnsiColor.fg(161),
    AnsiColor.fg(162),
    AnsiColor.fg(163),
    AnsiColor.fg(164),
    AnsiColor.fg(165),
    AnsiColor.fg(166),
    AnsiColor.fg(167),
    AnsiColor.fg(168),
    AnsiColor.fg(169),
    AnsiColor.fg(170),
    AnsiColor.fg(171),
    AnsiColor.fg(172),
    AnsiColor.fg(173),
    AnsiColor.fg(174),
    AnsiColor.fg(175),
    AnsiColor.fg(176),
    AnsiColor.fg(177),
    AnsiColor.fg(178),
    AnsiColor.fg(179),
    AnsiColor.fg(180),
    AnsiColor.fg(181),
    AnsiColor.fg(182),
    AnsiColor.fg(183),
    AnsiColor.fg(184),
    AnsiColor.fg(185),
    AnsiColor.fg(186),
    AnsiColor.fg(187),
    AnsiColor.fg(188),
    AnsiColor.fg(189),
    AnsiColor.fg(190),
    AnsiColor.fg(191),
    AnsiColor.fg(192),
    AnsiColor.fg(193),
    AnsiColor.fg(194),
    AnsiColor.fg(195),
    AnsiColor.fg(196),
    AnsiColor.fg(197),
    AnsiColor.fg(198),
    AnsiColor.fg(199),
    AnsiColor.fg(200),
    AnsiColor.fg(201),
    AnsiColor.fg(202),
    AnsiColor.fg(203),
    AnsiColor.fg(204),
    AnsiColor.fg(205),
    AnsiColor.fg(206),
    AnsiColor.fg(207),
    AnsiColor.fg(208),
    AnsiColor.fg(209),
    AnsiColor.fg(210),
    AnsiColor.fg(211),
    AnsiColor.fg(212),
    AnsiColor.fg(213),
    AnsiColor.fg(214),
    AnsiColor.fg(215),
    AnsiColor.fg(216),
    AnsiColor.fg(217),
    AnsiColor.fg(218),
    AnsiColor.fg(219),
    AnsiColor.fg(220),
    AnsiColor.fg(221),
    AnsiColor.fg(222),
    AnsiColor.fg(223),
    AnsiColor.fg(224),
    AnsiColor.fg(225),
  ];

  static final levelBgColors = [
    AnsiColor.bg(0),
    AnsiColor.bg(27),
    AnsiColor.bg(22),
    AnsiColor.bg(130),
    AnsiColor.bg(1),
    AnsiColor.bg(129),
  ];

  static final levelTags = [
    AnsiColor.bg(0)(" V "),
    AnsiColor.bg(27)(" D "),
    AnsiColor.bg(22)(" I "),
    AnsiColor.bg(130)(" W "),
    AnsiColor.bg(1)(" E "),
    AnsiColor.bg(129)("WTF")
  ];

  static final Map<String, _TerminalColor> tagColors = {};

  static final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0));
  static const persistentLogName = "guru_app";
  static int recordCount = 0;
  static final DoubleLinkedQueue<LogRecord> latestLogRecords = DoubleLinkedQueue();
  static final StreamController<LogRecord> _logStream = StreamController.broadcast();
  static bool listening = false;

  static List<MapEntry<String, String>> dumpRecords({String? filterTag}) {
    return latestLogRecords
        .where((record) => filterTag == null || filterTag == _appName || record.tag == filterTag)
        .map((record) => MapEntry(
            "[${record.sequence}] ${record.tag == null ? "" : "#${record.tag}#"} ${DateTimeUtils.standardDateFormat.format(record.time)}",
            record.msg))
        .toList();
  }

  static bool isDebug = kDebugMode;

  static int _persistentLevel = LogLevel.debug;

  static Stream<LogRecord> get observableLogStream => _logStream.stream;

  static Future init(String appName,
      {int persistentLogFileSize = 1024 * 1024 * 10,
      int persistentLogCount = 7,
      int persistentLevel = LogLevel.debug}) async {
    Log._appName = appName;
    tagColors[_appName] = CmdColor("${AnsiColor.ansiEsc}33;1m");
    _persistentLevel = persistentLevel;
    if (persistentLevel != LogLevel.nothing) {
      await PersistentLog.createLogger(
          logName: persistentLogName,
          fileSizeLimit: persistentLogFileSize,
          fileCount: persistentLogCount);
    }
  }

  static void setListen(bool listen) {
    listening = listen;
  }

  static String _toColorTag(String? tag) {
    if (tag == null) {
      return "";
    }
    _TerminalColor? color = tagColors[tag];
    if (color == null) {
      color = tagCandidateColor[RandomUtils.nextInt(tagCandidateColor.length)];
      tagColors[tag] = color;
    }
    return color(" #$tag#");

    // "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; 加粗
    // "${AnsiColor.ansiEsc}33;3m$name${AnsiColor.ansiDefault}"; 斜体
    // "${AnsiColor.ansiEsc}33;4m$name${AnsiColor.ansiDefault}"; 下滑线
    return "${AnsiColor.ansiEsc}33;1m$tag${AnsiColor.ansiDefault}";
  }

  static String _toColorName(String name) {
    if (!kIsWeb && Platform.isAndroid) {
      // "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; 加粗
      // "${AnsiColor.ansiEsc}33;3m$name${AnsiColor.ansiDefault}"; 斜体
      // "${AnsiColor.ansiEsc}33;4m$name${AnsiColor.ansiDefault}"; 下滑线
      return "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}";
    }
    return name;
  }

  static String _toDateTime(DateTime dateTime) {
    final humanDt =
        "${NumberUtils.twoDigits(dateTime.hour)}:${NumberUtils.twoDigits(dateTime.minute)}:${NumberUtils.twoDigits(dateTime.second)}.${NumberUtils.threeDigits(dateTime.millisecondsSinceEpoch % 1000)}";
    if (!kIsWeb && Platform.isAndroid) {
      return "${AnsiColor.ansiEsc}36;3m<$humanDt>${AnsiColor.ansiDefault}";
    }
    return "<$humanDt>";
  }

  static void _log(int level, String msgData,
      {String? tag,
      dynamic error,
      StackTrace? stackTrace,
      bool syncFirebase = false,
      bool persistent = false}) {
    final now = DateTime.now();
    final msg = msgData.substring(0, min(4096, msgData.length));
    recordCount++;
    // latestLogRecords.addFirst(MapEntry("[$recordCount] ${DateTimeUtils.humanTime}", msg));
    final logRecord =
        LogRecord(recordCount, now, LogLevel.levels[level], tag, "$msg${error != null ? '!![$error]!!' : ''}");
    latestLogRecords.addFirst(logRecord);
    if (latestLogRecords.length > 2000) {
      latestLogRecords.removeLast();
    }

    if (stackTrace != null) {
      _logger.log(LogLevel.levels[level], "[$_appName] ${tag == null ? "" : "#$tag#"} $msg",
          error: error, stackTrace: stackTrace);
    } else {
      final color = levelFgColors[level];
      String output;
      if (error != null) {
        output = "${color(msg)}[${errorFgColor(error.toString())}]";
      } else {
        output = color(msg);
      }

      if (isDebug) {
        // ignore: avoid_print
        print(
            "${_toColorName("[$_appName]")} ${_toDateTime(now)} ${levelTags[level]}${_toColorTag(tag)} $output");
      }
    }

    if (!kIsWeb && (level >= _persistentLevel || persistent)) {
      PersistentLog.log(
          logName: persistentLogName, message: "$msg ${error != null ? error.toString() : ''}");
    }

    if (syncFirebase) {
      // Analytics.instance.logFirebase("$msg ${error != null ? error.toString() : ''}");
      // if (error != null) {
      //   if (syncCrashlytics) {
      //     Analytics.instance
      //         .logException(M2BlocksException(error, cause: error), stacktrace: stackTrace);
      //   }
      // } else {
      //   Analytics.instance.logFirebase(msg);
      // }
    }

    if (listening) {
      _logStream.add(logRecord);
    }
  }

  static void v(String msg,
      {String? tag,
      dynamic error,
      StackTrace? stackTrace,
      bool syncFirebase = false,
      bool syncCrashlytics = false,
      bool persistent = false}) {
    _log(LogLevel.verbose, msg,
        tag: tag,
        error: error,
        stackTrace: stackTrace,
        syncFirebase: syncFirebase,
        persistent: persistent);
  }

  static void d(String msg,
      {String? tag,
      dynamic error,
      StackTrace? stackTrace,
      bool syncFirebase = false,
      bool syncCrashlytics = false,
      bool persistent = false}) {
    _log(LogLevel.debug, msg,
        tag: tag,
        error: error,
        stackTrace: stackTrace,
        syncFirebase: syncFirebase,
        persistent: persistent);
  }

  static void i(String msg,
      {String? tag,
      dynamic error,
      StackTrace? stackTrace,
      bool syncFirebase = false,
      bool syncCrashlytics = false,
      bool persistent = false}) {
    _log(LogLevel.info, msg,
        tag: tag,
        error: error,
        stackTrace: stackTrace,
        syncFirebase: syncFirebase,
        persistent: persistent);
  }

  static void w(String msg,
      {String? tag,
      dynamic error,
      StackTrace? stackTrace,
      bool syncFirebase = false,
      bool syncCrashlytics = false,
      bool persistent = false}) {
    _log(LogLevel.warning, msg,
        tag: tag,
        error: error,
        stackTrace: stackTrace,
        syncFirebase: syncFirebase,
        persistent: persistent);
  }

  static void e(String msg,
      {String? tag,
      dynamic error,
      StackTrace? stackTrace,
      bool syncFirebase = false,
      bool syncCrashlytics = false,
      bool persistent = false}) {
    _log(LogLevel.error, msg,
        tag: tag,
        error: error,
        stackTrace: stackTrace,
        syncFirebase: syncFirebase,
        persistent: persistent);
  }
}
