import { HorizontalAlign, VerticalAlign } from "@jimp/core";
import { JimpClass } from "@jimp/types";
import { methods as blitMethods } from "@jimp/plugin-blit";
import { z } from "zod";

import { measureText, measureTextHeight, splitLines } from "./measure-text.js";
import { BmCharacter, BmFont } from "./types.js";

export { measureText, measureTextHeight } from "./measure-text.js";
export * from "./types.js";

const PrintOptionsSchema = z.object({
  /** the x position to draw the image */
  x: z.number(),
  /** the y position to draw the image */
  y: z.number(),
  /** the text to print */
  text: z.union([
    z.union([z.string(), z.number()]),
    z.object({
      text: z.union([z.string(), z.number()]),
      alignmentX: z.nativeEnum(HorizontalAlign).optional(),
      alignmentY: z.nativeEnum(VerticalAlign).optional(),
    }),
  ]),
  /** the boundary width to draw in */
  maxWidth: z.number().optional(),
  /** the boundary height to draw in */
  maxHeight: z.number().optional(),
  /** a callback for when complete that ahs the end co-ordinates of the text */
  cb: z
    .function(z.tuple([z.object({ x: z.number(), y: z.number() })]))
    .optional(),
});

export type PrintOptions = z.infer<typeof PrintOptionsSchema>;

function xOffsetBasedOnAlignment<I extends JimpClass>(
  font: BmFont<I>,
  line: string,
  maxWidth: number,
  alignment: HorizontalAlign
) {
  if (alignment === HorizontalAlign.LEFT) {
    return 0;
  }

  if (alignment === HorizontalAlign.CENTER) {
    return (maxWidth - measureText(font, line)) / 2;
  }

  return maxWidth - measureText(font, line);
}

function drawCharacter<I extends JimpClass>(
  image: I,
  font: BmFont<I>,
  x: number,
  y: number,
  char: BmCharacter
) {
  if (char.width > 0 && char.height > 0) {
    const characterPage = font.pages[char.page];

    if (characterPage) {
      image = blitMethods.blit(image, {
        src: characterPage,
        x: x + char.xoffset,
        y: y + char.yoffset,
        srcX: char.x,
        srcY: char.y,
        srcW: char.width,
        srcH: char.height,
      });
    }
  }

  return image;
}

function printText<I extends JimpClass>(
  image: I,
  font: BmFont<I>,
  x: number,
  y: number,
  text: string,
  defaultCharWidth: number
) {
  for (let i = 0; i < text.length; i++) {
    const stringChar = text[i]!;

    let char;

    if (font.chars[stringChar]) {
      char = stringChar;
    } else if (/\s/.test(stringChar)) {
      char = "";
    } else {
      char = "?";
    }

    const fontChar = font.chars[char] || { xadvance: undefined };
    const fontKerning = font.kernings[char];

    if (fontChar) {
      drawCharacter(image, font, x, y, fontChar as BmCharacter);
    }

    const nextChar = text[i + 1];
    const kerning =
      fontKerning && nextChar && fontKerning[nextChar]
        ? fontKerning[nextChar] || 0
        : 0;

    x += kerning + (fontChar.xadvance || defaultCharWidth);
  }
}

export const methods = {
  /**
   * Draws a text on a image on a given boundary
   * @param font a bitmap font loaded from `Jimp.loadFont` command
   * @param x the x position to start drawing the text
   * @param y the y position to start drawing the text
   * @param text the text to draw (string or object with `text`, `alignmentX`, and/or `alignmentY`)
   * @example
   * ```ts
   * import { Jimp } from "jimp";
   *
   * const image = await Jimp.read("test/image.png");
   * const font = await Jimp.loadFont(Jimp.FONT_SANS_32_BLACK);
   *
   * image.print({ font, x: 10, y: 10, text: "Hello world!" });
   * ```
   */
  print<I extends JimpClass>(
    image: I,
    {
      font,
      ...options
    }: PrintOptions & {
      /** the BMFont instance */
      font: BmFont<I>;
    }
  ) {
    let {
      // eslint-disable-next-line prefer-const
      x,
      y,
      text,
      // eslint-disable-next-line prefer-const
      maxWidth = Infinity,
      // eslint-disable-next-line prefer-const
      maxHeight = Infinity,
      // eslint-disable-next-line prefer-const
      cb = () => {},
    } = PrintOptionsSchema.parse(options);

    let alignmentX: HorizontalAlign;
    let alignmentY: VerticalAlign;

    if (
      typeof text === "object" &&
      text.text !== null &&
      text.text !== undefined
    ) {
      alignmentX = text.alignmentX || HorizontalAlign.LEFT;
      alignmentY = text.alignmentY || VerticalAlign.TOP;
      ({ text } = text);
    } else {
      alignmentX = HorizontalAlign.LEFT;
      alignmentY = VerticalAlign.TOP;
      text = text.toString();
    }

    if (typeof text === "number") {
      text = text.toString();
    }

    if (maxHeight !== Infinity && alignmentY === VerticalAlign.BOTTOM) {
      y += maxHeight - measureTextHeight(font, text, maxWidth);
    } else if (maxHeight !== Infinity && alignmentY === VerticalAlign.MIDDLE) {
      y += maxHeight / 2 - measureTextHeight(font, text, maxWidth) / 2;
    }

    const defaultCharWidth = Object.entries(font.chars).find(
      (c) => c[1].xadvance
    )?.[1].xadvance;

    if (typeof defaultCharWidth !== "number") {
      throw new Error("Could not find default character width");
    }

    const { lines, longestLine } = splitLines(font, text, maxWidth);

    lines.forEach((line) => {
      const lineString = line.join(" ");
      const alignmentWidth = xOffsetBasedOnAlignment(
        font,
        lineString,
        maxWidth,
        alignmentX
      );

      printText(
        image,
        font,
        x + alignmentWidth,
        y,
        lineString,
        defaultCharWidth
      );
      y += font.common.lineHeight;
    });

    cb.bind(image)({ x: x + longestLine, y });

    return image;
  },
};
