// Shape models: BaseShape, TempShape, Shape

import { context, pointerList } from '../state.js';
import { Bezier } from '../bezier.js';
import { Quadtree } from '../quadtree.js';

// Helper function for UUID generation
function uuidv4() {
  return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
    (
      +c ^
      (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
    ).toString(16),
  );
}

// Forward declarations for dependencies that will be injected
let growBoundingBox = null;
let lerp = null;
let lerpColor = null;
let uuidToColor = null;
let simplifyPolyline = null;
let fitCurve = null;
let createMissingTexturePattern = null;
let debugQuadtree = null;
let d3 = null;

// Initialize function to be called from main.js
export function initializeShapeDependencies(deps) {
  growBoundingBox = deps.growBoundingBox;
  lerp = deps.lerp;
  lerpColor = deps.lerpColor;
  uuidToColor = deps.uuidToColor;
  simplifyPolyline = deps.simplifyPolyline;
  fitCurve = deps.fitCurve;
  createMissingTexturePattern = deps.createMissingTexturePattern;
  debugQuadtree = deps.debugQuadtree;
  d3 = deps.d3;
}

class BaseShape {
  constructor(startx, starty) {
    this.startx = startx;
    this.starty = starty;
    this.curves = [];
    this.regions = [];
    this.boundingBox = {
      x: { min: startx, max: starty },
      y: { min: starty, max: starty },
    };
  }
  recalculateBoundingBox() {
    this.boundingBox = undefined;
    for (let curve of this.curves) {
      if (!this.boundingBox) {
        this.boundingBox = curve.bbox();
      }
      growBoundingBox(this.boundingBox, curve.bbox());
    }
  }
  draw(context) {
    let ctx = context.ctx;
    ctx.lineWidth = this.lineWidth;
    ctx.lineCap = "round";

    // Create a repeating pattern for indicating selected shapes
    if (!this.patternCanvas) {
      this.patternCanvas = document.createElement('canvas');
      this.patternCanvas.width = 2;
      this.patternCanvas.height = 2;
      let patternCtx = this.patternCanvas.getContext('2d');
      // Draw the pattern:
      // black,       transparent,
      // transparent, white
      patternCtx.fillStyle = 'black';
      patternCtx.fillRect(0, 0, 1, 1);
      patternCtx.clearRect(1, 0, 1, 1);
      patternCtx.clearRect(0, 1, 1, 1);
      patternCtx.fillStyle = 'white';
      patternCtx.fillRect(1, 1, 1, 1);
    }
    let pattern = ctx.createPattern(this.patternCanvas, 'repeat'); // repeat the pattern across the canvas

    if (this.filled) {
      ctx.beginPath();
      if (this.fillImage && this.fillImage instanceof Element) {
        let pat;
        if (this.fillImage instanceof Element ||
          Object.keys(this.fillImage).length !== 0) {
          pat = ctx.createPattern(this.fillImage, "no-repeat");
        } else {
          pat = createMissingTexturePattern(ctx)
        }
        ctx.fillStyle = pat;
      } else {
        ctx.fillStyle = this.fillStyle;
      }
      if (context.debugColor) {
        ctx.fillStyle = context.debugColor;
      }
      if (this.curves.length > 0) {
        ctx.moveTo(this.curves[0].points[0].x, this.curves[0].points[0].y);
        for (let curve of this.curves) {
          ctx.bezierCurveTo(
            curve.points[1].x,
            curve.points[1].y,
            curve.points[2].x,
            curve.points[2].y,
            curve.points[3].x,
            curve.points[3].y,
          );
        }
      }
      ctx.fill();
      if (context.selected) {
        ctx.fillStyle = pattern
        ctx.fill()
      }
    }
    function drawCurve(curve, selected) {
      ctx.strokeStyle = curve.color;
      ctx.beginPath();
      ctx.moveTo(curve.points[0].x, curve.points[0].y);
      ctx.bezierCurveTo(
        curve.points[1].x,
        curve.points[1].y,
        curve.points[2].x,
        curve.points[2].y,
        curve.points[3].x,
        curve.points[3].y,
      );
      ctx.stroke();
      if (selected) {
        ctx.strokeStyle = pattern
        ctx.stroke()
      }
    }
    if (this.stroked && !context.debugColor) {
      for (let curve of this.curves) {
        drawCurve(curve, context.selected)

        // // Debug, show curve control points
        // ctx.beginPath()
        // ctx.arc(curve.points[1].x,curve.points[1].y, 5, 0, 2*Math.PI)
        // ctx.arc(curve.points[2].x,curve.points[2].y, 5, 0, 2*Math.PI)
        // ctx.arc(curve.points[3].x,curve.points[3].y, 5, 0, 2*Math.PI)
        // ctx.fill()
      }
    }
    if (context.activeCurve && this==context.activeCurve.shape) {
      drawCurve(context.activeCurve.current, true)
    }
    if (context.activeVertex && this==context.activeVertex.shape) {
      const curves = {
        ...context.activeVertex.current.startCurves,
        ...context.activeVertex.current.endCurves
      }
      for (let i in curves) {
        let curve = curves[i]
        drawCurve(curve, true)
      }
      ctx.fillStyle = "#000000aa";
        ctx.beginPath();
        let vertexSize = 15 / context.zoomLevel;
        ctx.rect(
          context.activeVertex.current.point.x - vertexSize / 2,
          context.activeVertex.current.point.y - vertexSize / 2,
          vertexSize,
          vertexSize,
        );
        ctx.fill();
    }
    // Debug, show quadtree
    if (debugQuadtree && this.quadtree && !context.debugColor) {
      this.quadtree.draw(ctx);
    }
  }
  lerpShape(shape2, t) {
    if (this.curves.length == 0) return this;
    let path1 = [
      {
        type: "M",
        x: this.curves[0].points[0].x,
        y: this.curves[0].points[0].y,
      },
    ];
    for (let curve of this.curves) {
      path1.push({
        type: "C",
        x1: curve.points[1].x,
        y1: curve.points[1].y,
        x2: curve.points[2].x,
        y2: curve.points[2].y,
        x: curve.points[3].x,
        y: curve.points[3].y,
      });
    }
    let path2 = [];
    if (shape2.curves.length > 0) {
      path2.push({
        type: "M",
        x: shape2.curves[0].points[0].x,
        y: shape2.curves[0].points[0].y,
      });
      for (let curve of shape2.curves) {
        path2.push({
          type: "C",
          x1: curve.points[1].x,
          y1: curve.points[1].y,
          x2: curve.points[2].x,
          y2: curve.points[2].y,
          x: curve.points[3].x,
          y: curve.points[3].y,
        });
      }
    }
    const interpolator = d3.interpolatePathCommands(path1, path2);
    let current = interpolator(t);
    let curves = [];
    let start = current.shift();
    let { x, y } = start;
    let bezier;
    for (let curve of current) {
      bezier = new Bezier(
        x,
        y,
        curve.x1,
        curve.y1,
        curve.x2,
        curve.y2,
        curve.x,
        curve.y,
      )
      bezier.color = lerpColor(this.strokeStyle, shape2.strokeStyle)
      curves.push(bezier);
      x = curve.x;
      y = curve.y;
    }
    let lineWidth = lerp(this.lineWidth, shape2.lineWidth, t);
    let strokeStyle = lerpColor(
      this.strokeStyle,
      shape2.strokeStyle,
      t,
    );
    let fillStyle;
    if (!this.fillImage) {
      fillStyle = lerpColor(this.fillStyle, shape2.fillStyle, t);
    }
    return new TempShape(
      start.x,
      start.y,
      curves,
      lineWidth,
      this.stroked,
      this.filled,
      strokeStyle,
      fillStyle,
    )
  }
}

class TempShape extends BaseShape {
  constructor(
    startx,
    starty,
    curves,
    lineWidth,
    stroked,
    filled,
    strokeStyle,
    fillStyle,
  ) {
    super(startx, starty);
    this.curves = curves;
    this.lineWidth = lineWidth;
    this.stroked = stroked;
    this.filled = filled;
    this.strokeStyle = strokeStyle;
    this.fillStyle = fillStyle;
    this.inProgress = false;
    this.recalculateBoundingBox();
  }
}

class Shape extends BaseShape {
  constructor(startx, starty, context, parent, uuid = undefined, shapeId = undefined) {
    super(startx, starty);
    this.parent = parent; // Reference to parent Layer (required)
    this.vertices = [];
    this.triangles = [];
    this.fillStyle = context.fillStyle;
    this.fillImage = context.fillImage;
    this.strokeStyle = context.strokeStyle;
    this.lineWidth = context.lineWidth;
    this.filled = context.fillShape;
    this.stroked = context.strokeShape;
    this.quadtree = new Quadtree(
      { x: { min: 0, max: 500 }, y: { min: 0, max: 500 } },
      4,
    );
    if (!uuid) {
      this.idx = uuidv4();
    } else {
      this.idx = uuid;
    }
    if (!shapeId) {
      this.shapeId = uuidv4();
    } else {
      this.shapeId = shapeId;
    }
    this.shapeIndex = 0;  // Default shape version index for tweening
    pointerList[this.idx] = this;
    this.regionIdx = 0;
    this.inProgress = true;

    // Timeline display settings (Phase 3)
    this.showSegment = true  // Show segment bar in timeline
    this.curvesMode = 'keyframe'  // 'segment' | 'keyframe' | 'curve'
    this.curvesHeight = 150  // Height in pixels when curves are in curve view
  }
  static fromJSON(json, parent) {
    let fillImage = undefined;
    if (json.fillImage && Object.keys(json.fillImage).length !== 0) {
      let img = new Image();
      img.src = json.fillImage.src
      fillImage = img
    } else {
      fillImage = {}
    }
    const shape = new Shape(
      json.startx,
      json.starty,
      {
        fillStyle: json.fillStyle,
        fillImage: fillImage,
        strokeStyle: json.strokeStyle,
        lineWidth: json.lineWidth,
        fillShape: json.filled,
        strokeShape: json.stroked,
      },
      parent,
      json.idx,
      json.shapeId,
    );
    for (let curve of json.curves) {
      shape.addCurve(Bezier.fromJSON(curve));
    }
    for (let region of json.regions) {
      const curves = [];
      for (let curve of region.curves) {
        curves.push(Bezier.fromJSON(curve));
      }
      shape.regions.push({
        idx: region.idx,
        curves: curves,
        fillStyle: region.fillStyle,
        filled: region.filled,
      });
    }
    // Load shapeIndex if present (for shape tweening)
    if (json.shapeIndex !== undefined) {
      shape.shapeIndex = json.shapeIndex;
    }
    return shape;
  }
  toJSON(randomizeUuid = false) {
    const json = {};
    json.type = "Shape";
    json.startx = this.startx;
    json.starty = this.starty;
    json.fillStyle = this.fillStyle;
    if (this.fillImage instanceof Element) {
      json.fillImage = {
        src: this.fillImage.src
      }
    }
    json.strokeStyle = this.fillStyle;
    json.lineWidth = this.lineWidth;
    json.filled = this.filled;
    json.stroked = this.stroked;
    if (randomizeUuid) {
      json.idx = uuidv4();
    } else {
      json.idx = this.idx;
    }
    json.shapeId = this.shapeId;
    json.shapeIndex = this.shapeIndex;  // For shape tweening
    json.curves = [];
    for (let curve of this.curves) {
      json.curves.push(curve.toJSON(randomizeUuid));
    }
    json.regions = [];
    for (let region of this.regions) {
      const curves = [];
      for (let curve of region.curves) {
        curves.push(curve.toJSON(randomizeUuid));
      }
      json.regions.push({
        idx: region.idx,
        curves: curves,
        fillStyle: region.fillStyle,
        filled: region.filled,
      });
    }
    return json;
  }
  get segmentColor() {
    return uuidToColor(this.idx);
  }
  addCurve(curve) {
    if (curve.color == undefined) {
      curve.color = context.strokeStyle;
    }
    this.curves.push(curve);
    this.quadtree.insert(curve, this.curves.length - 1);
    growBoundingBox(this.boundingBox, curve.bbox());
  }
  addLine(x, y) {
    let lastpoint;
    if (this.curves.length) {
      lastpoint = this.curves[this.curves.length - 1].points[3];
    } else {
      lastpoint = { x: this.startx, y: this.starty };
    }
    let midpoint = { x: (x + lastpoint.x) / 2, y: (y + lastpoint.y) / 2 };
    let curve = new Bezier(
      lastpoint.x,
      lastpoint.y,
      midpoint.x,
      midpoint.y,
      midpoint.x,
      midpoint.y,
      x,
      y,
    );
    curve.color = context.strokeStyle;
    this.quadtree.insert(curve, this.curves.length - 1);
    this.curves.push(curve);
  }
  bbox() {
    return this.boundingBox;
  }
  clear() {
    this.curves = [];
    this.quadtree.clear();
  }
  copy(idx) {
    let newShape = new Shape(
      this.startx,
      this.starty,
      {},
      this.parent,
      idx.slice(0, 8) + this.idx.slice(8),
      this.shapeId,
    );
    newShape.startx = this.startx;
    newShape.starty = this.starty;
    for (let curve of this.curves) {
      let newCurve = new Bezier(
        curve.points[0].x,
        curve.points[0].y,
        curve.points[1].x,
        curve.points[1].y,
        curve.points[2].x,
        curve.points[2].y,
        curve.points[3].x,
        curve.points[3].y,
      );
      newCurve.color = curve.color;
      newShape.addCurve(newCurve);
    }
    // TODO
    // for (let vertex of this.vertices) {

    // }
    newShape.updateVertices();
    newShape.fillStyle = this.fillStyle;
    if (this.fillImage instanceof Element) {
      newShape.fillImage = this.fillImage.cloneNode(true)
    } else {
      newShape.fillImage = this.fillImage;
    }
    newShape.strokeStyle = this.strokeStyle;
    newShape.lineWidth = this.lineWidth;
    newShape.filled = this.filled;
    newShape.stroked = this.stroked;

    return newShape;
  }
  fromPoints(points, error = 30) {
    console.log(error);
    this.curves = [];
    let curves = fitCurve.fitCurve(points, error);
    for (let curve of curves) {
      let bezier = new Bezier(
        curve[0][0],
        curve[0][1],
        curve[1][0],
        curve[1][1],
        curve[2][0],
        curve[2][1],
        curve[3][0],
        curve[3][1],
      );
      this.curves.push(bezier);
      this.quadtree.insert(bezier, this.curves.length - 1);
    }
    return this;
  }
  simplify(mode = "corners") {
    this.quadtree.clear();
    this.inProgress = false;
    // Mode can be corners, smooth or auto
    if (mode == "corners") {
      let points = [{ x: this.startx, y: this.starty }];
      for (let curve of this.curves) {
        points.push(curve.points[3]);
      }
      // points = points.concat(this.curves)
      let newpoints = simplifyPolyline(points, 10, false);
      this.curves = [];
      let lastpoint = newpoints.shift();
      let midpoint;
      for (let point of newpoints) {
        midpoint = {
          x: (lastpoint.x + point.x) / 2,
          y: (lastpoint.y + point.y) / 2,
        };
        let bezier = new Bezier(
          lastpoint.x,
          lastpoint.y,
          midpoint.x,
          midpoint.y,
          midpoint.x,
          midpoint.y,
          point.x,
          point.y,
        );
        this.curves.push(bezier);
        this.quadtree.insert(bezier, this.curves.length - 1);
        lastpoint = point;
      }
    } else if (mode == "smooth") {
      let error = 30;
      let points = [[this.startx, this.starty]];
      for (let curve of this.curves) {
        points.push([curve.points[3].x, curve.points[3].y]);
      }
      this.fromPoints(points, error);
    } else if (mode == "verbatim") {
      // Just keep existing shape
    }
    let epsilon = 0.01;
    let newCurves = [];
    let intersectMap = {};
    for (let i = 0; i < this.curves.length - 1; i++) {
      // for (let j=i+1; j<this.curves.length; j++) {
      for (let j of this.quadtree.query(this.curves[i].bbox())) {
        if (i >= j) continue;
        let intersects = this.curves[i].intersects(this.curves[j]);
        if (intersects.length) {
          intersectMap[i] ||= [];
          intersectMap[j] ||= [];
          for (let intersect of intersects) {
            let [t1, t2] = intersect.split("/");
            intersectMap[i].push(parseFloat(t1));
            intersectMap[j].push(parseFloat(t2));
          }
        }
      }
    }
    for (let lst in intersectMap) {
      for (let i = 1; i < intersectMap[lst].length; i++) {
        if (
          Math.abs(intersectMap[lst][i] - intersectMap[lst][i - 1]) < epsilon
        ) {
          intersectMap[lst].splice(i, 1);
          i--;
        }
      }
    }
    for (let i = this.curves.length - 1; i >= 0; i--) {
      if (i in intersectMap) {
        intersectMap[i].sort().reverse();
        let remainingFraction = 1;
        let remainingCurve = this.curves[i];
        for (let t of intersectMap[i]) {
          let split = remainingCurve.split(t / remainingFraction);
          remainingFraction = t;
          newCurves.push(split.right);
          remainingCurve = split.left;
        }
        newCurves.push(remainingCurve);
      } else {
        newCurves.push(this.curves[i]);
      }
    }
    for (let curve of newCurves) {
      curve.color = context.strokeStyle;
    }
    newCurves.reverse();
    this.curves = newCurves;
  }
  update() {
    this.recalculateBoundingBox();
    this.updateVertices();
    if (this.curves.length) {
      this.startx = this.curves[0].points[0].x;
      this.starty = this.curves[0].points[0].y;
    }
    return [this];
  }
  getClockwiseCurves(point, otherPoints) {
    // Returns array of {x, y, idx, angle}

    let points = [];
    for (let point of otherPoints) {
      points.push({ ...this.vertices[point].point, idx: point });
    }
    // Add an angle property to each point using tan(angle) = y/x
    const angles = points.map(({ x, y, idx }) => {
      return {
        x,
        y,
        idx,
        angle: (Math.atan2(y - point.y, x - point.x) * 180) / Math.PI,
      };
    });
    // Sort your points by angle
    const pointsSorted = angles.sort((a, b) => a.angle - b.angle);
    return pointsSorted;
  }
  translate(x, y) {
    this.quadtree.clear()
    let j=0;
    for (let curve of this.curves) {
      for (let i in curve.points) {
        const point = curve.points[i];
        curve.points[i] = { x: point.x + x, y: point.y + y };
      }
      this.quadtree.insert(curve, j)
      j++;
    }
    this.update();
  }
  updateVertices() {
    this.vertices = [];
    let utils = Bezier.getUtils();
    let epsilon = 1.5; // big epsilon whoa
    let tooClose;
    let i = 0;

    let region = {
      idx: `${this.idx}-r${this.regionIdx++}`,
      curves: [],
      fillStyle: context.fillStyle,
      filled: context.fillShape,
    };
    pointerList[region.idx] = region;
    this.regions = [region];
    for (let curve of this.curves) {
      this.regions[0].curves.push(curve);
    }
    if (this.regions[0].curves.length) {
      if (
        utils.dist(
          this.regions[0].curves[0].points[0],
          this.regions[0].curves[this.regions[0].curves.length - 1].points[3],
        ) < epsilon
      ) {
        this.regions[0].filled = true;
      }
    }

    // Generate vertices
    for (let curve of this.curves) {
      for (let index of [0, 3]) {
        tooClose = false;
        for (let vertex of this.vertices) {
          if (utils.dist(curve.points[index], vertex.point) < epsilon) {
            tooClose = true;
            vertex[["startCurves", , , "endCurves"][index]][i] = curve;
            break;
          }
        }
        if (!tooClose) {
          if (index == 0) {
            this.vertices.push({
              point: curve.points[index],
              startCurves: { [i]: curve },
              endCurves: {},
            });
          } else {
            this.vertices.push({
              point: curve.points[index],
              startCurves: {},
              endCurves: { [i]: curve },
            });
          }
        }
      }
      i++;
    }

    let shapes = [this];
    this.vertices.forEach((vertex, i) => {
      for (let i = 0; i < Math.min(10, this.regions.length); i++) {
        let region = this.regions[i];
        let regionVertexCurves = [];
        let vertexCurves = { ...vertex.startCurves, ...vertex.endCurves };
        if (Object.keys(vertexCurves).length == 1) {
          // endpoint
          continue;
        } else if (Object.keys(vertexCurves).length == 2) {
          // path vertex, don't need to do anything
          continue;
        } else if (Object.keys(vertexCurves).length == 3) {
          // T junction. Region doesn't change but might need to update curves?
          // Skip for now.
          continue;
        } else if (Object.keys(vertexCurves).length == 4) {
          // Intersection, split region in 2
          for (let i in vertexCurves) {
            let curve = vertexCurves[i];
            if (region.curves.includes(curve)) {
              regionVertexCurves.push(curve);
            }
          }
          let start = region.curves.indexOf(regionVertexCurves[1]);
          let end = region.curves.indexOf(regionVertexCurves[3]);
          if (end > start) {
            let newRegion = {
              idx: `${this.idx}-r${this.regionIdx++}`, // TODO: generate this deterministically so that undo/redo works
              curves: region.curves.splice(start, end - start),
              fillStyle: region.fillStyle,
              filled: true,
            };
            pointerList[newRegion.idx] = newRegion;
            this.regions.push(newRegion);
          }
        } else {
          // not sure how to handle vertices with more than 4 curves
          console.log(
            `Unexpected vertex with ${Object.keys(vertexCurves).length} curves!`,
          );
        }
      }
    });
  }
}

export { BaseShape, TempShape, Shape };
