// @ts-nocheck
// https://github.com/davidfig/pixi-dashed-line
//
// Copyright 2021 David Figatner
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge, publish, distribute,
// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import * as PIXI from 'pixi.js';

/** Define the dash: [dash length, gap size, dash size, gap size, ...] */
export type Dashes = number[];

export interface DashLineOptions {
  dash?: Dashes;
  width?: number;
  color?: number;
  alpha?: number;
  scale?: number;
  useTexture?: boolean;
  useDots?: boolean;
  cap?: PIXI.LINE_CAP;
  join?: PIXI.LINE_JOIN;
  alignment?: number;
}

const dashLineOptionsDefault: Partial<DashLineOptions> = {
  dash: [10, 5], // eslint-disable-line no-magic-numbers
  width: 1,
  color: 0xffffff,
  alpha: 1,
  scale: 1,
  useTexture: false,
  alignment: 0.5,
};

export class DashLine {
  graphics: PIXI.Graphics;

  /** current length of the line */
  lineLength: number;

  /** cursor location */
  cursor = new PIXI.Point();

  /** desired scale of line */
  scale = 1;

  private start: PIXI.Point;

  private dashSize: number;
  private dash: number[];

  private useTexture: boolean;
  private options: DashLineOptions;

  // cache of PIXI.Textures for dashed lines
  static dashTextureCache: Record<string, PIXI.Texture> = {};

  /**
   * Create a DashLine
   * @param graphics
   * @param [options]
   * @param [options.useTexture=false] - use the texture based render (useful for very large or very small dashed lines)
   * @param [options.dashes=[10,5] - an array holding the dash and gap (eg, [10, 5, 20, 5, ...])
   * @param [options.width=1] - width of the dashed line
   * @param [options.alpha=1] - alpha of the dashed line
   * @param [options.color=0xffffff] - color of the dashed line
   * @param [options.cap] - add a PIXI.LINE_CAP style to dashed lines (only works for useTexture: false)
   * @param [options.join] - add a PIXI.LINE_JOIN style to the dashed lines (only works for useTexture: false)
   * @param [options.alignment] - The alignment of any lines drawn (0.5 = middle, 1 = outer, 0 = inner)
   */
  constructor(graphics: PIXI.Graphics, options: DashLineOptions = {}) {
    this.graphics = graphics;
    options = { ...dashLineOptionsDefault, ...options };
    this.dash = options.dash;
    this.dashSize = this.dash.reduce((a, b) => a + b);
    this.useTexture = options.useTexture;
    this.options = options;
    this.setLineStyle();
  }

  /** resets line style to enable dashed line (useful if lineStyle was changed on graphics element) */
  setLineStyle() {
    const options = this.options;
    if (this.useTexture) {
      const texture = DashLine.getTexture(options, this.dashSize);
      this.graphics.lineTextureStyle({
        width: options.width * options.scale,
        color: options.color,
        alpha: options.alpha,
        texture,
        alignment: options.alignment,
      });
    } else {
      this.graphics.lineStyle({
        width: options.width * options.scale,
        color: options.color,
        alpha: options.alpha,
        cap: options.cap,
        join: options.join,
        alignment: options.alignment,
      });
    }
    this.scale = options.scale;
  }

  private static distance(x1: number, y1: number, x2: number, y2: number): number {
    return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  }

  moveTo(x: number, y: number): this {
    this.lineLength = 0;
    this.cursor.set(x, y);
    this.start = new PIXI.Point(x, y);
    this.graphics.moveTo(this.cursor.x, this.cursor.y);
    return this;
  }

  lineTo(x: number, y: number, closePath?: boolean): this {
    if (typeof this.lineLength === undefined) {
      this.moveTo(0, 0);
    }
    const length = DashLine.distance(this.cursor.x, this.cursor.y, x, y);
    const angle = Math.atan2(y - this.cursor.y, x - this.cursor.x);
    const closed = closePath && x === this.start.x && y === this.start.y;
    if (this.useTexture) {
      this.graphics.moveTo(this.cursor.x, this.cursor.y);
      this.adjustLineStyle(angle);
      if (closed && this.dash.length % 2 === 0) {
        const gap = Math.min(this.dash[this.dash.length - 1], length);
        this.graphics.lineTo(x - Math.cos(angle) * gap, y - Math.sin(angle) * gap);
        this.graphics.closePath();
      } else {
        this.graphics.lineTo(x, y);
      }
    } else {
      const cos = Math.cos(angle);
      const sin = Math.sin(angle);
      let x0 = this.cursor.x;
      let y0 = this.cursor.y;

      // find the first part of the dash for this line
      const place = this.lineLength % (this.dashSize * this.scale);
      let dashIndex = 0,
        dashStart = 0;
      let dashX = 0;
      for (let i = 0; i < this.dash.length; i++) {
        const dashSize = this.dash[i] * this.scale;
        if (place < dashX + dashSize) {
          dashIndex = i;
          dashStart = place - dashX;
          break;
        } else {
          dashX += dashSize;
        }
      }

      let remaining = length;
      // let count = 0
      while (remaining > 0) {
        // && count++ < 1000) {
        const dashSize = this.dash[dashIndex] * this.scale - dashStart;
        const dist = remaining > dashSize ? dashSize : remaining;
        if (closed) {
          const remainingDistance = DashLine.distance(x0 + cos * dist, y0 + sin * dist, this.start.x, this.start.y);
          if (remainingDistance <= dist) {
            if (dashIndex % 2 === 0) {
              const lastDash = DashLine.distance(x0, y0, this.start.x, this.start.y) - this.dash[this.dash.length - 1] * this.scale;
              x0 += cos * lastDash;
              y0 += sin * lastDash;
              this.graphics.lineTo(x0, y0);
            }
            break;
          }
        }

        x0 += cos * dist;
        y0 += sin * dist;
        if (dashIndex % 2) {
          this.graphics.moveTo(x0, y0);
        } else {
          this.graphics.lineTo(x0, y0);
        }
        remaining -= dist;

        dashIndex++;
        dashIndex = dashIndex === this.dash.length ? 0 : dashIndex;
        dashStart = 0;
      }
      // if (count >= 1000) console.log('failure', this.scale)
    }
    this.lineLength += length;
    this.cursor.set(x, y);
    return this;
  }

  closePath() {
    this.lineTo(this.start.x, this.start.y, true);
  }

  drawCircle(x: number, y: number, radius: number, points = 80, matrix?: PIXI.Matrix): this {
    const interval = (Math.PI * 2) / points;
    let angle = 0,
      first: PIXI.Point;
    if (matrix) {
      first = new PIXI.Point(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius);
      matrix.apply(first, first);
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      this.moveTo(first[0], first[1]);
    } else {
      first = new PIXI.Point(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius);
      this.moveTo(first.x, first.y);
    }
    angle += interval;
    for (let i = 1; i < points + 1; i++) {
      const next = i === points ? first : [x + Math.cos(angle) * radius, y + Math.sin(angle) * radius];
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      this.lineTo(next[0], next[1]);
      angle += interval;
    }
    return this;
  }

  drawEllipse(x: number, y: number, radiusX: number, radiusY: number, points = 80, matrix?: PIXI.Matrix): this {
    const interval = (Math.PI * 2) / points;
    let first: { x: number; y: number };
    const point = new PIXI.Point();
    for (let i = 0; i < Math.PI * 2; i += interval) {
      let x0 = x - radiusX * Math.sin(i);
      let y0 = y - radiusY * Math.cos(i);
      if (matrix) {
        point.set(x0, y0);
        matrix.apply(point, point);
        x0 = point.x;
        y0 = point.y;
      }
      if (i === 0) {
        this.moveTo(x0, y0);
        first = { x: x0, y: y0 };
      } else {
        this.lineTo(x0, y0);
      }
    }
    this.lineTo(first.x, first.y, true);
    return this;
  }

  drawPolygon(points: PIXI.Point[] | number[], matrix?: PIXI.Matrix): this {
    const p = new PIXI.Point();
    if (typeof points[0] === 'number') {
      if (matrix) {
        p.set(points[0] as number, points[1] as number);
        matrix.apply(p, p);
        this.moveTo(p.x, p.y);
        for (let i = 2; i < points.length; i += 2) {
          p.set(points[i] as number, points[i + 1] as number);
          matrix.apply(p, p);
          this.lineTo(p.x, p.y, i === points.length - 2);
        }
      } else {
        this.moveTo(points[0] as number, points[1] as number);
        for (let i = 2; i < points.length; i += 2) {
          this.lineTo(points[i] as number, points[i + 1] as number, i === points.length - 2);
        }
      }
    } else {
      if (matrix) {
        const point = points[0] as PIXI.Point;
        p.copyFrom(point);
        matrix.apply(p, p);
        this.moveTo(p.x, p.y);
        for (let i = 1; i < points.length; i++) {
          const point = points[i] as PIXI.Point;
          p.copyFrom(point);
          matrix.apply(p, p);
          this.lineTo(p.x, p.y, i === points.length - 1);
        }
      } else {
        const point = points[0] as PIXI.Point;
        this.moveTo(point.x, point.y);
        for (let i = 1; i < points.length; i++) {
          const point = points[i] as PIXI.Point;
          this.lineTo(point.x, point.y, i === points.length - 1);
        }
      }
    }
    return this;
  }

  drawRect(x: number, y: number, width: number, height: number, matrix?: PIXI.Matrix): this {
    if (matrix) {
      const p = new PIXI.Point();

      // moveTo(x, y)
      p.set(x, y);
      matrix.apply(p, p);
      this.moveTo(p.x, p.y);

      // lineTo(x + width, y)
      p.set(x + width, y);
      matrix.apply(p, p);
      this.lineTo(p.x, p.y);

      // lineTo(x + width, y + height)
      p.set(x + width, y + height);
      matrix.apply(p, p);
      this.lineTo(p.x, p.y);

      // lineto(x, y + height)
      p.set(x, y + height);
      matrix.apply(p, p);
      this.lineTo(p.x, p.y);

      // lineTo(x, y, true)
      p.set(x, y);
      matrix.apply(p, p);
      this.lineTo(p.x, p.y, true);
    } else {
      this.moveTo(x, y)
        .lineTo(x + width, y)
        .lineTo(x + width, y + height)
        .lineTo(x, y + height)
        .lineTo(x, y, true);
    }
    return this;
  }

  // adjust the matrix for the dashed texture
  private adjustLineStyle(angle: number) {
    const lineStyle = this.graphics.line;
    lineStyle.matrix = new PIXI.Matrix();
    if (angle) {
      lineStyle.matrix.rotate(angle);
    }
    if (this.scale !== 1) {
      lineStyle.matrix.scale(this.scale, this.scale);
    }
    const textureStart = -this.lineLength;
    lineStyle.matrix.translate(this.cursor.x + textureStart * Math.cos(angle), this.cursor.y + textureStart * Math.sin(angle));
    this.graphics.lineStyle(lineStyle);
  }

  // creates or uses cached texture
  private static getTexture(options: DashLineOptions, dashSize: number): PIXI.Texture | undefined {
    const key = options.dash.toString();
    if (DashLine.dashTextureCache[key]) {
      return DashLine.dashTextureCache[key];
    }
    const canvas = document.createElement('canvas');
    canvas.width = dashSize;
    canvas.height = Math.ceil(options.width);
    const context = canvas.getContext('2d');
    if (!context) {
      console.warn('Did not get context from canvas');
      return undefined;
    }
    context.strokeStyle = 'white';
    context.globalAlpha = options.alpha;
    context.lineWidth = options.width;
    let x = 0;
    const y = options.width / 2;
    context.moveTo(x, y);
    for (let i = 0; i < options.dash.length; i += 2) {
      x += options.dash[i];
      context.lineTo(x, y);
      if (options.dash.length !== i + 1) {
        x += options.dash[i + 1];
        context.moveTo(x, y);
      }
    }
    context.stroke();
    const texture = (DashLine.dashTextureCache[key] = PIXI.Texture.from(canvas));
    texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
    return texture;
  }
}
