import type { Op, OpSet, ResolvedOptions } from './core.js';
import type { RenderHelper } from './fillers/filler-interface.js';
import type { Point } from './geometry.js';

import { absolutize } from '../path-data-parser/absolutize.js';
import { normalize } from '../path-data-parser/normalize.js';
import { parsePath } from '../path-data-parser/parser.js';
import { getFiller } from './fillers/filler.js';
import { Random } from './math.js';

interface EllipseParams {
  rx: number;
  ry: number;
  increment: number;
}

const helper: RenderHelper = {
  randOffset,
  randOffsetWithRange,
  ellipse,
  doubleLineOps: doubleLineFillOps,
};

export function line(
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  o: ResolvedOptions
): OpSet {
  return { type: 'path', ops: _doubleLine(x1, y1, x2, y2, o) };
}

export function linearPath(
  points: Point[],
  close: boolean,
  o: ResolvedOptions
): OpSet {
  const len = (points || []).length;
  if (len > 2) {
    const ops: Op[] = [];
    for (let i = 0; i < len - 1; i++) {
      ops.push(
        ..._doubleLine(
          points[i][0],
          points[i][1],
          points[i + 1][0],
          points[i + 1][1],
          o
        )
      );
    }
    if (close) {
      ops.push(
        ..._doubleLine(
          points[len - 1][0],
          points[len - 1][1],
          points[0][0],
          points[0][1],
          o
        )
      );
    }
    return { type: 'path', ops };
  } else if (len === 2) {
    return line(points[0][0], points[0][1], points[1][0], points[1][1], o);
  }
  return { type: 'path', ops: [] };
}

export function polygon(points: Point[], o: ResolvedOptions): OpSet {
  return linearPath(points, true, o);
}

export function rectangle(
  x: number,
  y: number,
  width: number,
  height: number,
  o: ResolvedOptions
): OpSet {
  const points: Point[] = [
    [x, y],
    [x + width, y],
    [x + width, y + height],
    [x, y + height],
  ];
  return polygon(points, o);
}

export function curve(points: Point[], o: ResolvedOptions): OpSet {
  let o1 = _curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o);
  if (!o.disableMultiStroke) {
    const o2 = _curveWithOffset(
      points,
      1.5 * (1 + o.roughness * 0.22),
      cloneOptionsAlterSeed(o)
    );
    o1 = o1.concat(o2);
  }
  return { type: 'path', ops: o1 };
}

export interface EllipseResult {
  opset: OpSet;
  estimatedPoints: Point[];
}

export function ellipse(
  x: number,
  y: number,
  width: number,
  height: number,
  o: ResolvedOptions
): OpSet {
  const params = generateEllipseParams(width, height, o);
  return ellipseWithParams(x, y, o, params).opset;
}

export function generateEllipseParams(
  width: number,
  height: number,
  o: ResolvedOptions
): EllipseParams {
  const psq = Math.sqrt(
    Math.PI *
      2 *
      Math.sqrt((Math.pow(width / 2, 2) + Math.pow(height / 2, 2)) / 2)
  );
  const stepCount = Math.ceil(
    Math.max(o.curveStepCount, (o.curveStepCount / Math.sqrt(200)) * psq)
  );
  const increment = (Math.PI * 2) / stepCount;
  let rx = Math.abs(width / 2);
  let ry = Math.abs(height / 2);
  const curveFitRandomness = 1 - o.curveFitting;
  rx += _offsetOpt(rx * curveFitRandomness, o);
  ry += _offsetOpt(ry * curveFitRandomness, o);
  return { increment, rx, ry };
}

export function ellipseWithParams(
  x: number,
  y: number,
  o: ResolvedOptions,
  ellipseParams: EllipseParams
): EllipseResult {
  const [ap1, cp1] = _computeEllipsePoints(
    ellipseParams.increment,
    x,
    y,
    ellipseParams.rx,
    ellipseParams.ry,
    1,
    ellipseParams.increment * _offset(0.1, _offset(0.4, 1, o), o),
    o
  );
  let o1 = _curve(ap1, null, o);
  if (!o.disableMultiStroke && o.roughness !== 0) {
    const [ap2] = _computeEllipsePoints(
      ellipseParams.increment,
      x,
      y,
      ellipseParams.rx,
      ellipseParams.ry,
      1.5,
      0,
      o
    );
    const o2 = _curve(ap2, null, o);
    o1 = o1.concat(o2);
  }
  return {
    estimatedPoints: cp1,
    opset: { type: 'path', ops: o1 },
  };
}

export function arc(
  x: number,
  y: number,
  width: number,
  height: number,
  start: number,
  stop: number,
  closed: boolean,
  roughClosure: boolean,
  o: ResolvedOptions
): OpSet {
  const cx = x;
  const cy = y;
  let rx = Math.abs(width / 2);
  let ry = Math.abs(height / 2);
  rx += _offsetOpt(rx * 0.01, o);
  ry += _offsetOpt(ry * 0.01, o);
  let strt = start;
  let stp = stop;
  while (strt < 0) {
    strt += Math.PI * 2;
    stp += Math.PI * 2;
  }
  if (stp - strt > Math.PI * 2) {
    strt = 0;
    stp = Math.PI * 2;
  }
  const ellipseInc = (Math.PI * 2) / o.curveStepCount;
  const arcInc = Math.min(ellipseInc / 2, (stp - strt) / 2);
  const ops = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1, o);
  if (!o.disableMultiStroke) {
    const o2 = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1.5, o);
    ops.push(...o2);
  }
  if (closed) {
    if (roughClosure) {
      ops.push(
        ..._doubleLine(
          cx,
          cy,
          cx + rx * Math.cos(strt),
          cy + ry * Math.sin(strt),
          o
        ),
        ..._doubleLine(
          cx,
          cy,
          cx + rx * Math.cos(stp),
          cy + ry * Math.sin(stp),
          o
        )
      );
    } else {
      ops.push(
        { op: 'lineTo', data: [cx, cy] },
        {
          op: 'lineTo',
          data: [cx + rx * Math.cos(strt), cy + ry * Math.sin(strt)],
        }
      );
    }
  }
  return { type: 'path', ops };
}

export function svgPath(path: string, o: ResolvedOptions): OpSet {
  const segments = normalize(absolutize(parsePath(path)));
  const ops: Op[] = [];
  let first: Point = [0, 0];
  let current: Point = [0, 0];
  for (const { key, data } of segments) {
    switch (key) {
      case 'M': {
        const ro = 1 * (o.maxRandomnessOffset || 0);
        const pv = o.preserveVertices;
        ops.push({
          op: 'move',
          data: data.map(d => d + (pv ? 0 : _offsetOpt(ro, o))),
        });
        current = [data[0], data[1]];
        first = [data[0], data[1]];
        break;
      }
      case 'L':
        ops.push(..._doubleLine(current[0], current[1], data[0], data[1], o));
        current = [data[0], data[1]];
        break;
      case 'C': {
        const [x1, y1, x2, y2, x, y] = data;
        ops.push(..._bezierTo(x1, y1, x2, y2, x, y, current, o));
        current = [x, y];
        break;
      }
      case 'Z':
        ops.push(..._doubleLine(current[0], current[1], first[0], first[1], o));
        current = [first[0], first[1]];
        break;
    }
  }
  return { type: 'path', ops };
}

// Fills

export function solidFillPolygon(
  polygonList: Point[][],
  o: ResolvedOptions
): OpSet {
  const ops: Op[] = [];
  for (const points of polygonList) {
    if (points.length) {
      const offset = o.maxRandomnessOffset || 0;
      const len = points.length;
      if (len > 2) {
        ops.push({
          op: 'move',
          data: [
            points[0][0] + _offsetOpt(offset, o),
            points[0][1] + _offsetOpt(offset, o),
          ],
        });
        for (let i = 1; i < len; i++) {
          ops.push({
            op: 'lineTo',
            data: [
              points[i][0] + _offsetOpt(offset, o),
              points[i][1] + _offsetOpt(offset, o),
            ],
          });
        }
      }
    }
  }
  return { type: 'fillPath', ops };
}

export function patternFillPolygons(
  polygonList: Point[][],
  o: ResolvedOptions
): OpSet {
  return getFiller(o, helper).fillPolygons(polygonList, o);
}

export function patternFillArc(
  x: number,
  y: number,
  width: number,
  height: number,
  start: number,
  stop: number,
  o: ResolvedOptions
): OpSet {
  const cx = x;
  const cy = y;
  let rx = Math.abs(width / 2);
  let ry = Math.abs(height / 2);
  rx += _offsetOpt(rx * 0.01, o);
  ry += _offsetOpt(ry * 0.01, o);
  let strt = start;
  let stp = stop;
  while (strt < 0) {
    strt += Math.PI * 2;
    stp += Math.PI * 2;
  }
  if (stp - strt > Math.PI * 2) {
    strt = 0;
    stp = Math.PI * 2;
  }
  const increment = (stp - strt) / o.curveStepCount;
  const points: Point[] = [];
  for (let angle = strt; angle <= stp; angle = angle + increment) {
    points.push([cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)]);
  }
  points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]);
  points.push([cx, cy]);
  return patternFillPolygons([points], o);
}

export function randOffset(x: number, o: ResolvedOptions): number {
  return _offsetOpt(x, o);
}

export function randOffsetWithRange(
  min: number,
  max: number,
  o: ResolvedOptions
): number {
  return _offset(min, max, o);
}

export function doubleLineFillOps(
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  o: ResolvedOptions
): Op[] {
  return _doubleLine(x1, y1, x2, y2, o, true);
}

// Private helpers

function cloneOptionsAlterSeed(ops: ResolvedOptions): ResolvedOptions {
  const result: ResolvedOptions = { ...ops };
  result.randomizer = undefined;
  if (ops.seed) {
    result.seed = ops.seed + 1;
  }
  return result;
}

function random(ops: ResolvedOptions): number {
  if (!ops.randomizer) {
    ops.randomizer = new Random(ops.seed || 0);
  }
  return ops.randomizer.next();
}

function _offset(
  min: number,
  max: number,
  ops: ResolvedOptions,
  roughnessGain = 1
): number {
  return ops.roughness * roughnessGain * (random(ops) * (max - min) + min);
}

function _offsetOpt(
  x: number,
  ops: ResolvedOptions,
  roughnessGain = 1
): number {
  return _offset(-x, x, ops, roughnessGain);
}

function _doubleLine(
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  o: ResolvedOptions,
  filling = false
): Op[] {
  const singleStroke = filling
    ? o.disableMultiStrokeFill
    : o.disableMultiStroke;
  const o1 = _line(x1, y1, x2, y2, o, true, false);
  if (singleStroke) {
    return o1;
  }
  const o2 = _line(x1, y1, x2, y2, o, true, true);
  return o1.concat(o2);
}

function _line(
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  o: ResolvedOptions,
  move: boolean,
  overlay: boolean
): Op[] {
  const lengthSq = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
  const length = Math.sqrt(lengthSq);
  let roughnessGain = 1;
  if (length < 200) {
    roughnessGain = 1;
  } else if (length > 500) {
    roughnessGain = 0.4;
  } else {
    roughnessGain = -0.0016668 * length + 1.233334;
  }

  let offset = o.maxRandomnessOffset || 0;
  if (offset * offset * 100 > lengthSq) {
    offset = length / 10;
  }
  const halfOffset = offset / 2;
  const divergePoint = 0.2 + random(o) * 0.2;
  let midDispX = (o.bowing * o.maxRandomnessOffset * (y2 - y1)) / 200;
  let midDispY = (o.bowing * o.maxRandomnessOffset * (x1 - x2)) / 200;
  midDispX = _offsetOpt(midDispX, o, roughnessGain);
  midDispY = _offsetOpt(midDispY, o, roughnessGain);
  const ops: Op[] = [];
  const randomHalf = () => _offsetOpt(halfOffset, o, roughnessGain);
  const randomFull = () => _offsetOpt(offset, o, roughnessGain);
  const preserveVertices = o.preserveVertices;
  if (move) {
    if (overlay) {
      ops.push({
        op: 'move',
        data: [
          x1 + (preserveVertices ? 0 : randomHalf()),
          y1 + (preserveVertices ? 0 : randomHalf()),
        ],
      });
    } else {
      ops.push({
        op: 'move',
        data: [
          x1 + (preserveVertices ? 0 : _offsetOpt(offset, o, roughnessGain)),
          y1 + (preserveVertices ? 0 : _offsetOpt(offset, o, roughnessGain)),
        ],
      });
    }
  }
  if (overlay) {
    ops.push({
      op: 'bcurveTo',
      data: [
        midDispX + x1 + (x2 - x1) * divergePoint + randomHalf(),
        midDispY + y1 + (y2 - y1) * divergePoint + randomHalf(),
        midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomHalf(),
        midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomHalf(),
        x2 + (preserveVertices ? 0 : randomHalf()),
        y2 + (preserveVertices ? 0 : randomHalf()),
      ],
    });
  } else {
    ops.push({
      op: 'bcurveTo',
      data: [
        midDispX + x1 + (x2 - x1) * divergePoint + randomFull(),
        midDispY + y1 + (y2 - y1) * divergePoint + randomFull(),
        midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomFull(),
        midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomFull(),
        x2 + (preserveVertices ? 0 : randomFull()),
        y2 + (preserveVertices ? 0 : randomFull()),
      ],
    });
  }
  return ops;
}

function _curveWithOffset(
  points: Point[],
  offset: number,
  o: ResolvedOptions
): Op[] {
  const ps: Point[] = [];
  ps.push([
    points[0][0] + _offsetOpt(offset, o),
    points[0][1] + _offsetOpt(offset, o),
  ]);
  ps.push([
    points[0][0] + _offsetOpt(offset, o),
    points[0][1] + _offsetOpt(offset, o),
  ]);
  for (let i = 1; i < points.length; i++) {
    ps.push([
      points[i][0] + _offsetOpt(offset, o),
      points[i][1] + _offsetOpt(offset, o),
    ]);
    if (i === points.length - 1) {
      ps.push([
        points[i][0] + _offsetOpt(offset, o),
        points[i][1] + _offsetOpt(offset, o),
      ]);
    }
  }
  return _curve(ps, null, o);
}

function _curve(
  points: Point[],
  closePoint: Point | null,
  o: ResolvedOptions
): Op[] {
  const len = points.length;
  const ops: Op[] = [];
  if (len > 3) {
    const b = [];
    const s = 1 - o.curveTightness;
    ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
    for (let i = 1; i + 2 < len; i++) {
      const cachedVertArray = points[i];
      b[0] = [cachedVertArray[0], cachedVertArray[1]];
      b[1] = [
        cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
        cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
      ];
      b[2] = [
        points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
        points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
      ];
      b[3] = [points[i + 1][0], points[i + 1][1]];
      ops.push({
        op: 'bcurveTo',
        data: [b[1][0], b[1][1], b[2][0], b[2][1], b[3][0], b[3][1]],
      });
    }
    if (closePoint && closePoint.length === 2) {
      const ro = o.maxRandomnessOffset;
      ops.push({
        op: 'lineTo',
        data: [
          closePoint[0] + _offsetOpt(ro, o),
          closePoint[1] + _offsetOpt(ro, o),
        ],
      });
    }
  } else if (len === 3) {
    ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
    ops.push({
      op: 'bcurveTo',
      data: [
        points[1][0],
        points[1][1],
        points[2][0],
        points[2][1],
        points[2][0],
        points[2][1],
      ],
    });
  } else if (len === 2) {
    ops.push(
      ..._doubleLine(points[0][0], points[0][1], points[1][0], points[1][1], o)
    );
  }
  return ops;
}

function _computeEllipsePoints(
  increment: number,
  cx: number,
  cy: number,
  rx: number,
  ry: number,
  offset: number,
  overlap: number,
  o: ResolvedOptions
): Point[][] {
  const coreOnly = o.roughness === 0;
  const corePoints: Point[] = [];
  const allPoints: Point[] = [];

  if (coreOnly) {
    increment = increment / 4;
    allPoints.push([
      cx + rx * Math.cos(-increment),
      cy + ry * Math.sin(-increment),
    ]);
    for (let angle = 0; angle <= Math.PI * 2; angle = angle + increment) {
      const p: Point = [cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)];
      corePoints.push(p);
      allPoints.push(p);
    }
    allPoints.push([cx + rx * Math.cos(0), cy + ry * Math.sin(0)]);
    allPoints.push([
      cx + rx * Math.cos(increment),
      cy + ry * Math.sin(increment),
    ]);
  } else {
    const radOffset = _offsetOpt(0.5, o) - Math.PI / 2;
    allPoints.push([
      _offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
      _offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment),
    ]);
    const endAngle = Math.PI * 2 + radOffset - 0.01;
    for (let angle = radOffset; angle < endAngle; angle = angle + increment) {
      const p: Point = [
        _offsetOpt(offset, o) + cx + rx * Math.cos(angle),
        _offsetOpt(offset, o) + cy + ry * Math.sin(angle),
      ];
      corePoints.push(p);
      allPoints.push(p);
    }
    allPoints.push([
      _offsetOpt(offset, o) +
        cx +
        rx * Math.cos(radOffset + Math.PI * 2 + overlap * 0.5),
      _offsetOpt(offset, o) +
        cy +
        ry * Math.sin(radOffset + Math.PI * 2 + overlap * 0.5),
    ]);
    allPoints.push([
      _offsetOpt(offset, o) + cx + 0.98 * rx * Math.cos(radOffset + overlap),
      _offsetOpt(offset, o) + cy + 0.98 * ry * Math.sin(radOffset + overlap),
    ]);
    allPoints.push([
      _offsetOpt(offset, o) +
        cx +
        0.9 * rx * Math.cos(radOffset + overlap * 0.5),
      _offsetOpt(offset, o) +
        cy +
        0.9 * ry * Math.sin(radOffset + overlap * 0.5),
    ]);
  }

  return [allPoints, corePoints];
}

function _arc(
  increment: number,
  cx: number,
  cy: number,
  rx: number,
  ry: number,
  strt: number,
  stp: number,
  offset: number,
  o: ResolvedOptions
) {
  const radOffset = strt + _offsetOpt(0.1, o);
  const points: Point[] = [];
  points.push([
    _offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
    _offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment),
  ]);
  for (let angle = radOffset; angle <= stp; angle = angle + increment) {
    points.push([
      _offsetOpt(offset, o) + cx + rx * Math.cos(angle),
      _offsetOpt(offset, o) + cy + ry * Math.sin(angle),
    ]);
  }
  points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]);
  points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]);
  return _curve(points, null, o);
}

function _bezierTo(
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  x: number,
  y: number,
  current: Point,
  o: ResolvedOptions
): Op[] {
  const ops: Op[] = [];
  const ros = [o.maxRandomnessOffset || 1, (o.maxRandomnessOffset || 1) + 0.3];
  let f: Point = [0, 0];
  const iterations = o.disableMultiStroke ? 1 : 2;
  const preserveVertices = o.preserveVertices;
  for (let i = 0; i < iterations; i++) {
    if (i === 0) {
      ops.push({ op: 'move', data: [current[0], current[1]] });
    } else {
      ops.push({
        op: 'move',
        data: [
          current[0] + (preserveVertices ? 0 : _offsetOpt(ros[0], o)),
          current[1] + (preserveVertices ? 0 : _offsetOpt(ros[0], o)),
        ],
      });
    }
    f = preserveVertices
      ? [x, y]
      : [x + _offsetOpt(ros[i], o), y + _offsetOpt(ros[i], o)];
    ops.push({
      op: 'bcurveTo',
      data: [
        x1 + _offsetOpt(ros[i], o),
        y1 + _offsetOpt(ros[i], o),
        x2 + _offsetOpt(ros[i], o),
        y2 + _offsetOpt(ros[i], o),
        f[0],
        f[1],
      ],
    });
  }
  return ops;
}
