export { default as FontSystem } from "./font";
import { fonts, FontTypes } from "./font";

const line = "━";

/**
 * Formats a title string by extracting and rearranging emojis and non-emoji characters based on a pattern.
 *
 * @param {string} str - The input string containing emojis and text.
 * @param {string} [pattern] - The format pattern where `{word}` represents non-emoji text and `{emojis}` represents extracted emojis.
 * @returns {string} - The formatted title string.
 */
export function forceTitleFormat(str: string, pattern?: string): string {
  pattern ??= `{word} ${UNIRedux.charm} {emojis}`;
  const emojiRegex = /\p{Emoji}/gu;

  let emojis = [...str].filter((char) => emojiRegex.test(char)).join("");
  let nonEmojis = [...str]
    .filter((char) => !emojiRegex.test(char))
    .join("")
    .trim()
    .replaceAll("|", "");

  const res = pattern
    .replaceAll("{word}", nonEmojis)
    .replaceAll("{emojis}", emojis);

  return res;
}

interface FormatOptions {
  title: string;
  content: string;
  titleFont?: FontTypes;
  contentFont?: FontTypes;
  titlePattern?: string;
  noFormat?: boolean;
  lineLength?: number;
}

/**
 * Formats title and content text.
 */
export function format(
  title: string,
  content: string,
  contentFont?: FontTypes
): string;

/**
 * Formats title and content text with optional font styles and title patterns.
 */
export function format({
  title,
  content,
  contentFont,
  titleFont,
  titlePattern,
  noFormat,
  lineLength,
}: FormatOptions): string;

/**
 * Formats title and content text with optional font styles and title patterns.
 */
export function format(
  arg1: string | FormatOptions,
  arg2?: string,
  arg3?: FontTypes | undefined
): string {
  let options: FormatOptions;

  if (typeof arg1 === "string" && typeof arg2 === "string") {
    options = { title: arg1, content: arg2, contentFont: arg3 };
  } else if (typeof arg1 === "object" && arg1 !== null) {
    options = arg1;
  } else {
    throw new Error("Invalid arguments");
  }

  options.titleFont ??= "bold";
  options.contentFont ??= "fancy";
  options.titlePattern ??= undefined;
  options.noFormat ??= false;
  options.lineLength ??= 15;

  return `${fonts[options.titleFont](
    !options.noFormat
      ? forceTitleFormat(options.title, options.titlePattern)
      : options.title
  )}\n${line.repeat(options.lineLength)}\n${fonts[options.contentFont](
    autoBold(options.content)
  )}`;
}

/**
 * A collection of special Unicode characters and symbols.
 * Provides commonly used characters like line separators, trademarks, mathematical symbols, and more.
 */
export class UNIRedux {
  /** Special invisible space character */
  static specialSpace = "ᅠ";

  /** Standard line repeated 1 time */
  static singleLine = line;

  /** Burger menu icon */
  static burger: "☰" = "☰";

  /** Standard line repeated 15 times */
  static standardLine = line.repeat(15);

  /** Section sign */
  static section: "§" = "§";

  /** Pilcrow sign */
  static paragraph: "¶" = "¶";

  /** Registered trademark sign */
  static registered: "®" = "®";

  /** Trademark sign */
  static trademark: "™" = "™";

  /** Copyright sign */
  static copyright: "©" = "©";

  /** Degree sign */
  static degree: "°" = "°";

  /** Micro sign */
  static micro: "µ" = "µ";

  /** Bullet point */
  static bullet: "•" = "•";

  /** En dash */
  static enDash: "–" = "–";

  /** Em dash */
  static emDash: "—" = "—";

  /** Prime symbol */
  static prime: "′" = "′";

  /** Double prime symbol */
  static doublePrime: "″" = "″";

  /** Dagger symbol */
  static daggers: "†" = "†";

  /** Double dagger symbol */
  static doubleDagger: "‡" = "‡";

  /** Ellipsis */
  static ellipsis: "…" = "…";

  /** Infinity symbol */
  static infinity: "∞" = "∞";

  /** Generic currency sign */
  static currency: "¤" = "¤";

  /** Yen sign */
  static yen: "¥" = "¥";

  /** Euro sign */
  static euro: "€" = "€";

  /** Pound sign */
  static pound: "£" = "£";

  /** Plus-minus sign */
  static plusMinus: "±" = "±";

  /** Approximately equal sign */
  static approximately: "≈" = "≈";

  /** Not equal to sign */
  static notEqual: "≠" = "≠";

  /** Less than or equal to sign */
  static lessThanOrEqual: "≤" = "≤";

  /** Greater than or equal to sign */
  static greaterThanOrEqual: "≥" = "≥";

  /** Summation sign */
  static summation: "∑" = "∑";

  /** Integral sign */
  static integral: "∫" = "∫";

  /** Square root sign */
  static squareRoot: "√" = "√";

  /** Partial differential sign */
  static partialDifferential: "∂" = "∂";

  /** Angle symbol */
  static angle: "∠" = "∠";

  /** Degree Fahrenheit sign */
  static degreeFahrenheit: "℉" = "℉";

  /** Degree Celsius sign */
  static degreeCelsius: "℃" = "℃";

  /** Floral Heart symbol */
  static floralHeart: "❧" = "❧";

  /** Star Flower symbol */
  static starFlower: "✻" = "✻";

  /** Heavy Star symbol */
  static heavyStar: "★" = "★";

  /** Sparkle symbol */
  static sparkle: "✦" = "✦";

  /** Asterisk symbol */
  static asterisk: "✱" = "✱";

  /** Heavy Check Mark */
  static heavyCheckMark: "✔" = "✔";

  /** Heavy Ballot X */
  static heavyBallotX: "✖" = "✖";

  /** Heart symbol */
  static heart: "♥" = "♥";

  /** Diamond symbol */
  static diamond: "♦" = "♦";

  /** Club symbol */
  static club: "♣" = "♣";

  /** Spade symbol */
  static spade: "♠" = "♠";

  /** Musical Note symbol */
  static musicalNote: "♪" = "♪";

  /** Double Musical Note symbol */
  static doubleMusicalNote: "♫" = "♫";

  /** Snowflake symbol */
  static snowflake: "❄" = "❄";

  /** Sparkle Star symbol */
  static sparkleStar: "✨" = "✨";

  /** Anchor symbol */
  static anchor: "⚓" = "⚓";

  /** Umbrella symbol */
  static umbrella: "☔" = "☔";

  /** Hourglass symbol */
  static hourglass: "⌛" = "⌛";

  /** Hourglass Not Done symbol */
  static hourglassNotDone: "⏳" = "⏳";

  /** Charm symbol */
  static charm: "✦" = "✦";

  /** Disc symbol */
  static disc: "⦿" = "⦿";

  /** Arrow symbol */
  static arrow: "➤" = "➤";

  /** Arrow (Black and White) symbol */
  static arrowBW: "➣" = "➣";

  /** Arrow from Top symbol */
  static arrowFromT: "➥" = "➥";

  /** Arrow from Bottom symbol */
  static arrowFromB: "➦" = "➦";

  /** Restart symbol */
  static restart: "⟳" = "⟳";

  /** Arrow Outline symbol */
  static arrowOutline: "➩" = "➩";
}

/**
 * Abbreviates a number using K (thousand), M (million), B (billion), etc.
 *
 * @param {number|string} value - The number to abbreviate.
 * @param {number} [places=2] - The number of decimal places to round to.
 * @param {boolean} [isFull=false] - If true, returns the full name instead of letter notation (e.g., "Thousand" instead of "K").
 * @returns {string} - The abbreviated number.
 */
export function abbreviateNumber(
  value: number | string,
  places = 2,
  isFull = false
): string {
  let num = Number(value);
  if (isNaN(num)) return "Invalid input";
  if (num < 1000) {
    return num.toFixed(places).replace(/\.?0+$/, "");
  }

  const suffixes = ["", "K", "M", "B", "T", "P", "E"];
  const fullSuffixes = [
    "",
    "Thousand",
    "Million",
    "Billion",
    "Trillion",
    "Quadrillion",
    "Quintillion",
  ];

  const magnitude = Math.floor(Math.log10(num) / 3);

  if (magnitude === 0) {
    return num % 1 === 0
      ? num.toString()
      : num.toFixed(places).replace(/\.?0+$/, "");
  }

  const abbreviatedValue = num / Math.pow(1000, magnitude);
  const suffix = isFull ? fullSuffixes[magnitude] : suffixes[magnitude];

  if (abbreviatedValue % 1 === 0) {
    return `${Math.round(abbreviatedValue)}${isFull ? ` ${suffix}` : suffix}`;
  }

  const formattedValue = abbreviatedValue.toFixed(places).replace(/\.?0+$/, "");

  return `${formattedValue}${isFull ? ` ${suffix}` : suffix}`;
}

/**
 * Transforms the input text by applying bold and bold-italic formatting.
 *
 * The function looks for text wrapped in `***` and `**` and replaces them with
 * bold-italic and bold formatting respectively.
 *
 * @param text - The input text to be transformed.
 * @returns The transformed text with bold and bold-italic formatting applied.
 */
export function autoBold(text: string) {
  text = String(text);
  text = text.replace(/\*\*\*(.*?)\*\*\*/g, (_: string, text: string) =>
    fonts.bold_italic(text)
  );
  text = text.replace(/\*\*(.*?)\*\*/g, (_: string, text: string) =>
    fonts.bold(text)
  );
  return text;
}

/**
 * Replaces custom font tags in the given text with corresponding font styles.
 *
 * The function looks for patterns in the format `[font=fontName]text[:font=fontName]`
 * and replaces them with the corresponding font styles if the font names match.
 *
 * @param text - The input text containing custom font tags.
 * @returns The text with font tags replaced by corresponding font styles.
 */
export function fontTag(text: string) {
  text = String(text);
  text = text.replace(
    /\[font=(.*?)\]\s*(.*?)\s*\[:font=(.*?)\]/g,
    (_, font, text, font2) =>
      font === font2 ? fonts[font as FontTypes](text) : text
  );
  return text;
}

type StrictMessageForm = {
  body?: string;
  attachment?: ReadableStream | ReadableStream[] | any | any[];
  mentions?: Mention[];
  location?: { latitude: number; longitude: number; current: boolean };
};
type MessageForm = string | StrictMessageForm;

type FCAID = string | number;

type Mention = {
  tag: string;
  id: FCAID;
  fromIndex: number;
};

interface LiaIOQueue {
  form: MessageForm;
  senderID?: FCAID;
  replyTo?: FCAID | undefined;
  style?: FormatOptions;
  resolve?: (value: any) => any;
  reject?: (reason?: any) => any;
  event?: any;
  api?: any;
}

/**
 * @lianecagara
 * Class representing the LiaIOLite/Box for handling message input/output operations.
 * This class is responsible for sending, replying, and receiving messages,
 * as well as managing message reactions and handling events related to messages.
 *
 * @class Box
 */
export class Box {
  #api: any = null;
  #event: any = null;
  public style: FormatOptions | undefined;

  /**
   * Creates an instance of the LiaIO class to manage message interactions.
   *
   * @param {API} api - The API instance for interacting with the messaging service.
   * @param {FCAMessageReplyEvent | any} event - The event that triggered the interaction.
   * @memberof Box
   */
  constructor(api: any, event: any, style?: FormatOptions) {
    this.#api = api;
    this.#event = event;
    this.style = style;
  }

  static queue: LiaIOQueue[] = [];

  /**
   * Sends an output message, which can be a reply or a new message.
   *
   * @param params - The parameters for sending the message.
   * @param params.form - The form of the message to be sent.
   * @param params.senderID - The ID of the sender (optional).
   * @param params.replyTo - The ID of the message being replied to (optional).
   * @param style
   * @returns A promise resolving to the sent message event.
   * @memberof Box
   */
  async out(param0: {
    form: MessageForm;
    senderID?: FCAID;
    replyTo?: FCAID;
    style?: FormatOptions;
  }): Promise<any> {
    const {
      form: oform,
      senderID = this.#event.threadID,
      replyTo = undefined,
      style = null,
    } = param0;
    const form = normalizeMessageForm(oform) as StrictMessageForm;

    let exMents: Mention[] = [];
    if (typeof form.body === "string") {
      const ments = form.body.match(/@\[(.*?)=(.*?)\]/g);
      if (Array.isArray(ments)) {
        for (const ment of ments) {
          const [tag, uid] = ment.slice(2, -1).split("=");
          form.body = form.body.replace(ment, `@${tag}`);
          exMents.push({
            id: uid,
            tag,
            fromIndex: form.body.indexOf(`@${tag}`),
          });
        }
      }
    }
    let styler: FormatOptions | undefined = this.style;
    if (style) {
      styler = style;
    }
    if (styler && form.body && styler !== undefined && styler.title) {
      const combined: FormatOptions = {
        ...styler,
        content: form.body,
      };
      form.body = format(combined);
    }

    return new Promise(async (resolve, reject) => {
      form.mentions = [...exMents, ...(form.mentions ?? [])];
      for (const key in form) {
        if (
          form[key as keyof StrictMessageForm] === null ||
          form[key as keyof StrictMessageForm] === undefined
        ) {
          delete form[key as keyof StrictMessageForm];
        }
        if (!form.mentions || form.mentions.length < 1) {
          delete form.mentions;
        }
      }
      console.log(`Form to send:`, form, senderID, replyTo);
      /**
       * @type {LiaIOQueue}
       */
      const queueItem: LiaIOQueue = {
        ...param0,
        senderID,
        replyTo,
        style: styler,
        form,
        resolve,
        reject,
        api: this.#api,
        event: this.#event,
      };
      Box.queue.push(queueItem);

      if (Box.queue.length === 1) {
        Box._processQueue();
      }
    });
  }

  static async _processQueue() {
    console.log(`Processing Queue..`);
    while (this.queue.length > 0) {
      const currentTask = this.queue[0];
      console.log(
        `Current Queue task (total ${this.queue.length}):`,
        currentTask.form
      );

      if (this.queue.length > 1) {
        await new Promise((resolve) => setTimeout(resolve, 500));
      }

      try {
        console.log(`Sending form...`, currentTask.form);
        const {
          api,
          form: oform,
          reject,
          resolve,
          replyTo,
          senderID,
        } = currentTask;
        const form = normalizeMessageForm(oform);
        api.sendMessage(
          form,
          senderID,
          (err: any, info: any) => {
            if (err && reject) {
              reject(err);
            } else if (resolve) {
              console.log(`Form sent:`, form, senderID, replyTo);

              resolve(info);
            }
          },
          replyTo ?? undefined
        );
      } catch (error) {
        currentTask.reject?.(error);
      }
      this.queue.shift();
      console.log(`Moving to next queue`);
    }
  }

  /**
   * Sends a reply to a message, optionally targeting a specific reply.
   *
   * @param form - The form of the reply message to be sent.
   * @param replyTo - The ID of the message being replied to (optional).
   * @returns A promise resolving to the message reply event.
   * @memberof Box
   * @example
   * await liaIO.reply("Hello, world!");
   */
  reply(
    form: MessageForm,
    replyTo: FCAID = this.#event.messageID
  ): Promise<any> {
    return this.out({
      form,
      replyTo,
    });
  }
  /**
   * Sends a message to a destination, optionally specifying the destination ID.
   *
   * @param form - The form of the message to be sent.
   * @param senderID - The ID of the destination to send the message to (optional).
   * @memberof Box
   * @example
   * await liaIO.send("Hello, world!");
   */
  send(
    form: MessageForm,
    senderID: FCAID = this.#event.threadID
  ): Promise<any> {
    return this.out({
      form,
      senderID,
    });
  }

  /**
   * An easy way to handle errors.
   *
   * @param error - Error to be sent.
   */
  error(error: Error | Record<string, any>): Promise<any> {
    const errString =
      error instanceof Error
        ? String(error.stack)
        : JSON.stringify(error, null, 2);
    console.error(error);
    return this.reply(errString);
  }

  /**
   * Adds a reaction to a message, optionally targeting a specific message to react to.
   *
   * @param emoji - The reaction to be added (e.g., "like", "love").
   * @param reactTo - The ID of the message to react to (optional).
   * @returns A promise resolving to the sent reaction event.
   * @memberof Box
   */
  reaction(
    emoji: string,
    reactTo: FCAID = this.#event.messageID
  ): Promise<any> {
    return new Promise((resolve, reject) => {
      this.#api.setMessageReaction(emoji, reactTo, (err: any) => {
        if (err) {
          return reject(err);
        }
        return resolve(true);
      });
    });
  }

  clone(): Box {
    return new Box(this.#api, this.#event, this.style);
  }

  styled(style: FormatOptions) {
    return new Box(this.#api, this.#event, style);
  }
}

function normalizeMessageForm(form: MessageForm): StrictMessageForm {
  let r: Record<string, any> = {};
  if (form && r) {
    if (typeof form === "object") {
      r = form;
    }

    if (typeof form === "string") {
      r = {
        body: form,
      };
    }
    if (!Array.isArray(r.attachment) && r.attachment) {
      r.attachment = [r.attachment];
    }
    return r;
  } else {
    return {
      body: undefined,
    };
  }
}

export const LiaIOLite = Box;
