import {Vector2} from "osu-classes";
import p5 from "p5";
import {loadImageAsync} from "@/utils";
import {Md5} from "ts-md5";
import {Judgements, OsuRenderer} from "@/osu/OsuRenderer";

export class Drawer {

  private static imageCache: Record<string, p5.Graphics> = {};

  static images = {
    cursor: undefined as any as p5.Image,
    cursor2: undefined as any as p5.Image,
    cursortrail: undefined as any as p5.Image,
    hitcircle: undefined as any as p5.Image,
    hitcircleoverlay: undefined as any as p5.Image,
    radix: undefined as any as p5.Image,
    sliderfollowcircle: undefined as any as p5.Image,
    sliderb0: undefined as any as p5.Image,
    default0: undefined as any as p5.Image,
    default1: undefined as any as p5.Image,
    default2: undefined as any as p5.Image,
    default3: undefined as any as p5.Image,
    default4: undefined as any as p5.Image,
    default5: undefined as any as p5.Image,
    default6: undefined as any as p5.Image,
    default7: undefined as any as p5.Image,
    default8: undefined as any as p5.Image,
    default9: undefined as any as p5.Image,
    hitmiss: undefined as any as p5.Image,
    hit50: undefined as any as p5.Image,
    hit100: undefined as any as p5.Image
  };
  private static p: p5;

  static setP(_p: p5) {
    this.p = _p;
  }

  static async loadDefaultImages() {
    const imageLoadPromises = Object.keys(Drawer.images).map(imageName =>
        loadImageAsync(`/${imageName}.png`).then(
            image => {
              Drawer.images[imageName as keyof typeof Drawer.images] = image;
            },
            error => {
              console.error(`Failed to load image ${imageName}:`, error);
            }
        )
    );

    return Promise.allSettled(imageLoadPromises)
  }

  static setImages(images: typeof this.images) {
    this.images = images;
  }

  static setDrawingOpacity(opacity: number) {
    //@ts-ignore
    this.p.drawingContext.globalAlpha = opacity;
  }

  /**
   * Draws the error bar for the given judgements.
   * @param judgements
   */
  static drawErrorBar(judgements: Judgements[]) {
    let width = 512;
    let height = 384;

    // @https://osu.ppy.sh/wiki/en/Beatmap/Overall_difficulty
    let hitWindow300 = 80 - 6 * OsuRenderer.beatmap.difficulty.overallDifficulty;
    let hitWindow100 = 140 - 8 * OsuRenderer.beatmap.difficulty.overallDifficulty;
    let hitWindow50 = 200 - 10 * OsuRenderer.beatmap.difficulty.overallDifficulty;

    let barWidth = 512 * 0.8; // Width of the error bar, 80% of canvas width
    let barHeight = 20; // The height of the error bar
    let barX = (width - barWidth) / 2; // Center the bar on the canvas
    let barY = height + 52;

    // Draw the background for the 50's hit window (biggest)
    Drawer.p.fill(217, 174, 71);
    Drawer.p.rect(barX + barWidth / 2 - (hitWindow50), barY, hitWindow50 * 2, barHeight);

    // Draw the background for the 100's hit window (middle)
    Drawer.p.fill(87, 225, 19);
    Drawer.p.rect(barX + barWidth / 2 - (hitWindow100), barY, hitWindow100 * 2, barHeight);

    // Draw the background for the 300's hit window (smallest/closer to the center)
    Drawer.p.fill(50, 188, 231);
    Drawer.p.rect(barX + barWidth / 2 - (hitWindow300), barY, hitWindow300 * 2, barHeight);

    let tickDensity = new Array(Math.ceil(barWidth / 5)).fill(0); // Example: Each segment is 5 pixels wide
    judgements.forEach(judgement => {
      let judgementX = barX + barWidth / 2 + judgement.error / 2;
      this.drawJudgementTick(judgementX, barY, barHeight, tickDensity);
    });

    // Draw a vertical bar in the exact center of the hit error bar
    // Drawer.p.push();
    // Drawer.p.noFill();
    // Drawer.p.stroke(255);
    // Drawer.p.strokeWeight(2);
    // Drawer.p.line(barX + barWidth / 2, barY - 5, barX + barWidth / 2, barY + barHeight + 5);

    // Drawer.p.pop();
  }

  //@ts-ignore
  static drawJudgementTick(x, y, height, tickDensity) {
    let barWidth = 512 * 0.8;
    let barX = (512 - barWidth) / 2; // Center the bar on the canvas

    const segmentIndex = Math.floor((x - barX) / 5); // Assuming 5-pixel segments
    tickDensity[segmentIndex]++;

    const opacity = this.calculateOpacity(tickDensity[segmentIndex]);

    Drawer.p.stroke(255, 255, 255, 255);
    Drawer.p.strokeWeight(2);
    Drawer.p.strokeCap(Drawer.p.SQUARE); // Slightly thicker ends for clarity
    Drawer.p.colorMode(Drawer.p.RGB);
    Drawer.p.stroke(Drawer.p.color(255, 255, 255, 255 * opacity)); // Apply opacity
    Drawer.p.line(x, y, x, y + height);
  }

  /**
   * Calculate the opacity of the judgement tick. Lower opacity means less amount.
   *
   * @param count The amount of ticks
   * @returns The opacity of the tick (0-1)
   */
  static calculateOpacity(count: number): number {
    const MIN_OPACITY = 0.55; // Minimum opacity enforced
    const MAX_OPACITY = 1; // Maximum opacity
    const MAX_DENSITY = 50; // Opacity reaches maximum at this tick count

    // Calculate linear opacity based on count
    let opacity = (count / MAX_DENSITY) * (MAX_OPACITY - MIN_OPACITY) + MIN_OPACITY;

    // Ensure opacity is at least MIN_OPACITY and at most MAX_OPACITY
    opacity = Math.max(opacity, MIN_OPACITY);
    opacity = Math.min(opacity, MAX_OPACITY);

    return opacity;
  }


  static drawJudgement(
    position: Vector2,
    judgement: string,
    opacity: number
  ) {
    Drawer.p.push();
    //@ts-ignore
    Drawer.p.drawingContext.globalAlpha = opacity;
    if (judgement === "ONE_HUNDRED") {
        Drawer.p.image(
            this.images.hit100,
            position.x,
            position.y,
            this.images.hit100.width,
            this.images.hit100.height
        );
    }
    if (judgement === "FIFTY") {
        Drawer.p.image(
            this.images.hit50,
            position.x,
            position.y,
            this.images.hit50.width,
            this.images.hit50.height
        );
    }
    if (judgement === "MISS") {
        Drawer.p.image(
            this.images.hitmiss,
            position.x,
            position.y,
            this.images.hitmiss.width,
            this.images.hitmiss.height
        );
    }
    Drawer.p.pop();
  }

  static drawSliderFollowPoint(position: Vector2, radius: number) {
    Drawer.p.image(
      this.images.sliderfollowcircle,
      position.x,
      position.y,
      radius * 2,
      radius * 2
    );
    Drawer.p.push();
    Drawer.p.fill(255, 255, 255, 18);
    Drawer.p.circle(position.x, position.y, radius * 4);
    Drawer.p.pop();
  }

  static drawHitCircle(position: Vector2, radius: number, comboNumber: number) {
    Drawer.p.push();
    Drawer.p.noStroke();
    Drawer.p.fill(160);
    Drawer.p.image(
      this.images.hitcircle,
      position.x,
      position.y,
      radius * 2,
      radius * 2
    );
    Drawer.p.image(
      this.images.hitcircleoverlay,
      position.x,
      position.y,
      radius * 2,
      radius * 2
    );
    this.drawNumberWithSprites(
      comboNumber + 1,
      new Vector2(position.x - 0.5, position.y),
      radius * 0.4
    );

    Drawer.p.pop();
  }

  static drawApproachCircle(
    position: Vector2,
    radius: number,
    arScale: number
  ) {
    if (arScale == 1) return;
    Drawer.p.push();
    Drawer.p.noFill();
    Drawer.p.stroke("white");
    Drawer.p.strokeWeight(3);
    Drawer.p.circle(position.x, position.y, radius * 2 * arScale);
    Drawer.p.pop();
  }

  static drawSliderBody(origin: Vector2, path: Vector2[], radius: number) {
    Drawer.p.push();

    const cacheKey = Md5.hashStr(JSON.stringify(origin) + JSON.stringify(path) + JSON.stringify(radius));
    if (!this.imageCache[cacheKey]) {
      const g = Drawer.p.createGraphics(512 * 4, 384 * 4);
      g.scale(2);
      g.translate(512 - 256, 384 - 192);
      //@ts-ignore
      const ctx = g.drawingContext;
      ctx.lineCap = "round";
      ctx.lineJoin = "round";
      g.translate(origin.x, origin.y);
      g.noFill();

      g.strokeWeight(radius * 2 - 10);

      g.stroke(255);
      g.beginShape();
      for (const node of path) {
        g.vertex(node.x, node.y);
      }
      g.endShape();

      g.strokeWeight(radius * 2 - 17);
      g.stroke(10);
      g.beginShape();
      for (const node of path) {
        g.vertex(node.x, node.y);
      }

      g.endShape();

      for (let i = 0; i < radius * 2 - 17; i += 2) {
        g.strokeWeight(radius * 2 - 17 - i);
        g.stroke(Math.round((i / (radius * 2 - 17)) * 45));
        g.beginShape();
        for (const node of path) {
          g.vertex(node.x, node.y);
        }

        g.endShape();
      }

      this.imageCache[cacheKey] = g;
    }
    Drawer.p.imageMode(Drawer.p.CORNER);
    Drawer.p.image(this.imageCache[cacheKey], -256, -192, 512 * 2, 384 * 2);

    Drawer.p.pop();
  }


  static drawCursorPath(
    cursorImage: p5.Image,
    path: {
      position: Vector2;
      time: number;
      button: {
        mouseLeft1: boolean;
        mouseLeft2: boolean;
        mouseRight1: boolean;
        mouseRight2: boolean;
      };
    }[],
    cursor: {
      position: Vector2;
      button: {
        mouseLeft1: boolean;
        mouseLeft2: boolean;
        mouseRight1: boolean;
        mouseRight2: boolean;
      };
    }
  ) {
    Drawer.p.push();
    //@ts-ignore
    const ctx = Drawer.p.drawingContext;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    Drawer.p.noFill();
    Drawer.p.strokeWeight(1.5);
    Drawer.p.stroke("white");

    for (let i = 1; i < path.length; i++) {
      const lastFrame = path[i - 1];
      const frame = path[i];

      if(!OsuRenderer.settings.showFutureCursorPath && frame.time > OsuRenderer.time) {
        continue;
      }

      if(OsuRenderer.settings.showCursorPath) {
        Drawer.p.stroke("white");
        Drawer.p.line(
            lastFrame.position.x,
            lastFrame.position.y,
            frame.position.x,
            frame.position.y
        );
      }
    }

    if (cursor.position) {
      Drawer.p.image(
          cursorImage,
          cursor.position.x,
          cursor.position.y,
          55,
          55
      );

      for (let i = 1; i < path.length; i++) {
        const lastFrame = path[i - 1];
        const frame = path[i];

        if(!OsuRenderer.settings.showFutureCursorPath && frame.time > OsuRenderer.time) {
          continue;
        }

        if(OsuRenderer.settings.showKeyPress) {

          if(!OsuRenderer.settings.showFutureCursorPath && lastFrame.time > OsuRenderer.time) {
            continue;
          }

          let size = 4;
          if (lastFrame.button.mouseLeft1 || lastFrame.button.mouseLeft2) {
            Drawer.p.stroke("#BB6BD9");
            Drawer.p.line(lastFrame.position.x - size, lastFrame.position.y - size, lastFrame.position.x + size, lastFrame.position.y + size);
            Drawer.p.line(lastFrame.position.x + size, lastFrame.position.y - size, lastFrame.position.x - size, lastFrame.position.y + size);
          }

          if (lastFrame.button.mouseRight1 || lastFrame.button.mouseRight2) {
            Drawer.p.stroke("#F2994A");
            Drawer.p.line(lastFrame.position.x - size, lastFrame.position.y - size, lastFrame.position.x + size, lastFrame.position.y + size);
            Drawer.p.line(lastFrame.position.x + size, lastFrame.position.y - size, lastFrame.position.x - size, lastFrame.position.y + size);
          }

        }
      }
    }

    Drawer.p.pop();
  }
  static drawNumberWithSprites(
    number: number,
    position: Vector2,
    size: number
  ) {
    Drawer.p.push();
    Drawer.p.imageMode(Drawer.p.CORNER);
    const digits = number.toString().split("");
    const digitWidth = size;
    const digitHeight = size * 1.2;
    const digitSpacing = -size * 0.1;
    const totalWidth = digits.length * (digitWidth + digitSpacing);
    const x = position.x - totalWidth / 2;
    const y = position.y - digitHeight / 2;
    digits.forEach((digit, index) => {
      const indexer = `default${digit}`;
      const image = this.images[indexer as keyof typeof this.images];
      Drawer.p.image(
        image,
        x + index * (digitWidth + digitSpacing),
        y,
        digitWidth,
        digitHeight
      );
    });
    Drawer.p.pop();
  }
  static drawField() {
    Drawer.p.noFill();
    Drawer.p.stroke(255, 255, 255, 60);
    Drawer.p.rect(0, 0, 512, 384, 4);
  }

  static beginDrawing() {
    Drawer.p.push();
  }
  static endDrawing() {
    Drawer.p.pop();
  }
}
