import os from "os";
import path from "path";
import crypto from "crypto";
import { Readable, Writable } from "stream";

import "colors";
import mime from "mime";
import axios from "axios";
import fs from "fs-extra";
import { v4 as uuid } from "uuid";
import { format as dateFormat } from "date-fns";
import CRC32 from "crc-32";
import randomstring from "randomstring";
import _ from "lodash";
import { CronJob } from "cron";

import HTTP_STATUS_CODE from "./http-status-codes.ts";

const autoIdMap = new Map();

const util = {
  is2DArrays(value: any) {
    return (
      _.isArray(value) &&
      (!value[0] || (_.isArray(value[0]) && _.isArray(value[value.length - 1])))
    );
  },

  uuid: (separator = true) => (separator ? uuid() : uuid().replace(/\-/g, "")),

  autoId: (prefix = "") => {
    let index = autoIdMap.get(prefix);
    if (index > 999999) index = 0; //超过最大数字则重置为0
    autoIdMap.set(prefix, (index || 0) + 1);
    return `${prefix}${index || 1}`;
  },

  ignoreJSONParse(value: string) {
    const result = _.attempt(() => JSON.parse(value));
    if (_.isError(result)) return null;
    return result;
  },

  generateRandomString(options: any): string {
    return randomstring.generate(options);
  },

  getResponseContentType(value: any): string | null {
    return value.headers
      ? value.headers["content-type"] || value.headers["Content-Type"]
      : null;
  },

  mimeToExtension(value: string) {
    let extension = mime.getExtension(value);
    if (extension == "mpga") return "mp3";
    return extension;
  },

  extractURLExtension(value: string) {
    const extname = path.extname(new URL(value).pathname);
    return extname.substring(1).toLowerCase();
  },

  createCronJob(cronPatterns: any, callback?: Function) {
    if (!_.isFunction(callback))
      throw new Error("callback must be an Function");
    return new CronJob(
      cronPatterns,
      () => callback(),
      null,
      false,
      "Asia/Shanghai"
    );
  },

  getDateString(format = "yyyy-MM-dd", date = new Date()) {
    return dateFormat(date, format);
  },

  getIPAddressesByIPv4(): string[] {
    const interfaces = os.networkInterfaces();
    const addresses = [];
    for (let name in interfaces) {
      const networks = interfaces[name];
      const results = networks.filter(
        (network) =>
          network.family === "IPv4" &&
          network.address !== "127.0.0.1" &&
          !network.internal
      );
      if (results[0] && results[0].address) addresses.push(results[0].address);
    }
    return addresses;
  },

  getMACAddressesByIPv4(): string[] {
    const interfaces = os.networkInterfaces();
    const addresses = [];
    for (let name in interfaces) {
      const networks = interfaces[name];
      const results = networks.filter(
        (network) =>
          network.family === "IPv4" &&
          network.address !== "127.0.0.1" &&
          !network.internal
      );
      if (results[0] && results[0].mac) addresses.push(results[0].mac);
    }
    return addresses;
  },

  generateSSEData(event?: string, data?: string, retry?: number) {
    return `event: ${event || "message"}\ndata: ${(data || "")
      .replace(/\n/g, "\\n")
      .replace(/\s/g, "\\s")}\nretry: ${retry || 3000}\n\n`;
  },

  buildDataBASE64(type, ext, buffer) {
    return `data:${type}/${ext.replace("jpg", "jpeg")};base64,${buffer.toString(
      "base64"
    )}`;
  },

  isLinux() {
    return os.platform() !== "win32";
  },

  isIPAddress(value) {
    return (
      _.isString(value) &&
      (/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test(
        value
      ) ||
        /\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*/.test(
          value
        ))
    );
  },

  isPort(value) {
    return _.isNumber(value) && value > 0 && value < 65536;
  },

  isReadStream(value): boolean {
    return (
      value &&
      (value instanceof Readable || "readable" in value || value.readable)
    );
  },

  isWriteStream(value): boolean {
    return (
      value &&
      (value instanceof Writable || "writable" in value || value.writable)
    );
  },

  isHttpStatusCode(value) {
    return _.isNumber(value) && Object.values(HTTP_STATUS_CODE).includes(value);
  },

  isURL(value) {
    return !_.isUndefined(value) && /^(http|https)/.test(value);
  },

  isSrc(value) {
    return !_.isUndefined(value) && /^\/.+\.[0-9a-zA-Z]+(\?.+)?$/.test(value);
  },

  isBASE64(value) {
    return !_.isUndefined(value) && /^[a-zA-Z0-9\/\+]+(=?)+$/.test(value);
  },

  isBASE64Data(value) {
    return /^data:/.test(value);
  },

  extractBASE64DataFormat(value): string | null {
    const match = value.trim().match(/^data:(.+);base64,/);
    if (!match) return null;
    return match[1];
  },

  removeBASE64DataHeader(value): string {
    return value.replace(/^data:(.+);base64,/, "");
  },

  isDataString(value): boolean {
    return /^(base64|json):/.test(value);
  },

  isStringNumber(value) {
    return _.isFinite(Number(value));
  },

  isUnixTimestamp(value) {
    return /^[0-9]{10}$/.test(`${value}`);
  },

  isTimestamp(value) {
    return /^[0-9]{13}$/.test(`${value}`);
  },

  isEmail(value) {
    return /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test(
      value
    );
  },

  isAsyncFunction(value) {
    return Object.prototype.toString.call(value) === "[object AsyncFunction]";
  },

  async isAPNG(filePath) {
    let head;
    const readStream = fs.createReadStream(filePath, { start: 37, end: 40 });
    const readPromise = new Promise((resolve, reject) => {
      readStream.once("end", resolve);
      readStream.once("error", reject);
    });
    readStream.once("data", (data) => (head = data));
    await readPromise;
    return head.compare(Buffer.from([0x61, 0x63, 0x54, 0x4c])) === 0;
  },

  unixTimestamp() {
    return parseInt(`${Date.now() / 1000}`);
  },

  timestamp() {
    return Date.now();
  },

  urlJoin(...values) {
    let url = "";
    for (let i = 0; i < values.length; i++)
      url += `${i > 0 ? "/" : ""}${values[i]
        .replace(/^\/*/, "")
        .replace(/\/*$/, "")}`;
    return url;
  },

  millisecondsToHmss(milliseconds) {
    if (_.isString(milliseconds)) return milliseconds;
    milliseconds = parseInt(milliseconds);
    const sec = Math.floor(milliseconds / 1000);
    const hours = Math.floor(sec / 3600);
    const minutes = Math.floor((sec - hours * 3600) / 60);
    const seconds = sec - hours * 3600 - minutes * 60;
    const ms = (milliseconds % 60000) - seconds * 1000;
    return `${hours > 9 ? hours : "0" + hours}:${
      minutes > 9 ? minutes : "0" + minutes
    }:${seconds > 9 ? seconds : "0" + seconds}.${ms}`;
  },

  millisecondsToTimeString(milliseconds) {
    if (milliseconds < 1000) return `${milliseconds}ms`;
    if (milliseconds < 60000)
      return `${parseFloat((milliseconds / 1000).toFixed(2))}s`;
    return `${Math.floor(milliseconds / 1000 / 60)}m${Math.floor(
      (milliseconds / 1000) % 60
    )}s`;
  },

  rgbToHex(r, g, b): string {
    return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  },

  hexToRgb(hex) {
    const value = parseInt(hex.replace(/^#/, ""), 16);
    return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
  },

  md5(value) {
    return crypto.createHash("md5").update(value).digest("hex");
  },

  crc32(value) {
    return _.isBuffer(value) ? CRC32.buf(value) : CRC32.str(value);
  },

  arrayParse(value): any[] {
    return _.isArray(value) ? value : [value];
  },

  booleanParse(value) {
    return value === "true" || value === true ? true : false;
  },

  encodeBASE64(value) {
    return Buffer.from(value).toString("base64");
  },

  decodeBASE64(value) {
    return Buffer.from(value, "base64").toString();
  },

  async fetchFileBASE64(url: string) {
    const result = await axios.get(url, {
      responseType: "arraybuffer",
    });
    return result.data.toString("base64");
  },

  /**
   * 计算 ArrayBuffer 的 CRC32 值
   * @param buffer ArrayBuffer 数据
   * @returns CRC32 十六进制字符串
   */
  calculateCRC32(buffer: ArrayBuffer): string {
    const crcTable = [];
    for (let i = 0; i < 256; i++) {
      let crc = i;
      for (let j = 0; j < 8; j++) {
        crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
      }
      crcTable[i] = crc;
    }

    let crc = 0 ^ (-1);
    const bytes = new Uint8Array(buffer);
    for (let i = 0; i < bytes.length; i++) {
      crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
    }
    return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
  },
};

export default util;
