import { computeAccurateBounds } from './bezier'

import type {
  HandleMirroring,
  VectorNetwork,
  VectorRegion,
  VectorSegment,
  VectorVertex,
  WindingRule
} from '../scene-graph'
import type { Rect } from '../types'
import type { CanvasKit, Path } from 'canvaskit-wasm'

// --- vectorNetworkBlob binary format ---
// Header:  [numVertices:u32, numSegments:u32, numRegions:u32]  (12 bytes)
// Vertex:  [styleOverrideIdx:u32, x:f32, y:f32]               (12 bytes)
// Segment: [styleOverrideIdx:u32, start:u32, tsX:f32, tsY:f32, end:u32, teX:f32, teY:f32]  (28 bytes)
// Region:  [windingRule:u32, numLoops:u32, {numSegs:u32, segIdx...}... ]  (variable)

interface StyleOverride {
  styleID: number
  handleMirroring?: string
}

export function decodeVectorNetworkBlob(
  data: Uint8Array,
  styleOverrideTable?: StyleOverride[]
): VectorNetwork {
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
  let o = 0

  const nV = view.getUint32(o, true)
  o += 4
  const nS = view.getUint32(o, true)
  o += 4
  const nR = view.getUint32(o, true)
  o += 4

  const styleMap = new Map<number, StyleOverride>()
  if (styleOverrideTable) {
    for (const entry of styleOverrideTable) {
      styleMap.set(entry.styleID, entry)
    }
  }

  const vertices: VectorVertex[] = []
  for (let i = 0; i < nV; i++) {
    const styleIdx = view.getUint32(o, true)
    o += 4
    const x = view.getFloat32(o, true)
    o += 4
    const y = view.getFloat32(o, true)
    o += 4

    const override = styleMap.get(styleIdx)
    vertices.push({
      x,
      y,
      handleMirroring: (override?.handleMirroring as HandleMirroring | undefined) ?? 'NONE'
    })
  }

  const segments: VectorSegment[] = []
  for (let i = 0; i < nS; i++) {
    o += 4 // styleOverrideIdx (unused for segments currently)
    const start = view.getUint32(o, true)
    o += 4
    const tsX = view.getFloat32(o, true)
    o += 4
    const tsY = view.getFloat32(o, true)
    o += 4
    const end = view.getUint32(o, true)
    o += 4
    const teX = view.getFloat32(o, true)
    o += 4
    const teY = view.getFloat32(o, true)
    o += 4

    segments.push({
      start,
      end,
      tangentStart: { x: tsX, y: tsY },
      tangentEnd: { x: teX, y: teY }
    })
  }

  const regions: VectorRegion[] = []
  for (let i = 0; i < nR; i++) {
    const windingRuleU32 = view.getUint32(o, true)
    o += 4
    const windingRule: WindingRule = windingRuleU32 === 0 ? 'EVENODD' : 'NONZERO'
    const numLoops = view.getUint32(o, true)
    o += 4
    const loops: number[][] = []
    for (let j = 0; j < numLoops; j++) {
      const numSegs = view.getUint32(o, true)
      o += 4
      const loop: number[] = []
      for (let k = 0; k < numSegs; k++) {
        loop.push(view.getUint32(o, true))
        o += 4
      }
      loops.push(loop)
    }
    regions.push({ windingRule, loops })
  }

  return { vertices, segments, regions }
}

/** Build a styleOverrideTable from vertex handleMirroring values.
 *  Returns a map from handleMirroring value to styleID, plus the table array. */
export function buildStyleOverrideTable(network: VectorNetwork): {
  table: StyleOverride[]
  mirroringToId: Map<string, number>
} {
  const mirroringToId = new Map<string, number>()
  const table: StyleOverride[] = []
  let nextId = 1

  for (const v of network.vertices) {
    const hm = v.handleMirroring ?? 'NONE'
    if (hm === 'NONE') continue
    if (!mirroringToId.has(hm)) {
      mirroringToId.set(hm, nextId)
      table.push({ styleID: nextId, handleMirroring: hm })
      nextId++
    }
  }

  return { table, mirroringToId }
}

export function encodeVectorNetworkBlob(
  network: VectorNetwork,
  mirroringToId?: Map<string, number>
): Uint8Array {
  const { vertices, segments, regions } = network

  let regionBytes = 0
  for (const region of regions) {
    regionBytes += 8 // windingRule + numLoops
    for (const loop of region.loops) {
      regionBytes += 4 + loop.length * 4 // numSegs + indices
    }
  }

  const totalBytes = 12 + vertices.length * 12 + segments.length * 28 + regionBytes
  const buf = new ArrayBuffer(totalBytes)
  const view = new DataView(buf)
  let o = 0

  view.setUint32(o, vertices.length, true)
  o += 4
  view.setUint32(o, segments.length, true)
  o += 4
  view.setUint32(o, regions.length, true)
  o += 4

  for (const v of vertices) {
    const hm = v.handleMirroring ?? 'NONE'
    const styleIdx = (hm !== 'NONE' && mirroringToId?.get(hm)) || 0
    view.setUint32(o, styleIdx, true)
    o += 4
    view.setFloat32(o, v.x, true)
    o += 4
    view.setFloat32(o, v.y, true)
    o += 4
  }

  for (const seg of segments) {
    view.setUint32(o, 0, true)
    o += 4 // styleOverrideIdx
    view.setUint32(o, seg.start, true)
    o += 4
    view.setFloat32(o, seg.tangentStart.x, true)
    o += 4
    view.setFloat32(o, seg.tangentStart.y, true)
    o += 4
    view.setUint32(o, seg.end, true)
    o += 4
    view.setFloat32(o, seg.tangentEnd.x, true)
    o += 4
    view.setFloat32(o, seg.tangentEnd.y, true)
    o += 4
  }

  for (const region of regions) {
    view.setUint32(o, region.windingRule === 'EVENODD' ? 0 : 1, true)
    o += 4
    view.setUint32(o, region.loops.length, true)
    o += 4
    for (const loop of region.loops) {
      view.setUint32(o, loop.length, true)
      o += 4
      for (const segIdx of loop) {
        view.setUint32(o, segIdx, true)
        o += 4
      }
    }
  }

  return new Uint8Array(buf)
}

export function vectorNetworkToPath(ck: CanvasKit, network: VectorNetwork): Path[] {
  const { vertices, segments, regions } = network

  if (regions.length > 0) {
    const paths: Path[] = []
    for (const region of regions) {
      const regionPath = new ck.Path()
      for (const loop of region.loops) {
        addLoopToPath(regionPath, loop, segments, vertices)
      }
      regionPath.setFillType(
        region.windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding
      )
      paths.push(regionPath)
    }
    return paths
  }

  // No regions — draw all segments as open paths, tracking direction
  const path = new ck.Path()
  const visited = new Set<number>()
  const chains = buildChains(segments, vertices.length)

  for (const chain of chains) {
    if (chain.length === 0) continue
    // Determine starting vertex by tracing chain direction
    let current = findChainStart(chain, segments)
    path.moveTo(vertices[current].x, vertices[current].y)

    for (const segIdx of chain) {
      visited.add(segIdx)
      const seg = segments[segIdx]
      const forward = seg.start === current
      addSegmentDirected(path, seg, vertices, forward)
      current = forward ? seg.end : seg.start
    }
  }

  for (let i = 0; i < segments.length; i++) {
    if (visited.has(i)) continue
    const seg = segments[i]
    path.moveTo(vertices[seg.start].x, vertices[seg.start].y)
    addSegmentDirected(path, seg, vertices, true)
  }

  return [path]
}

function addLoopToPath(
  path: Path,
  loop: number[],
  segments: VectorSegment[],
  vertices: VectorVertex[]
): void {
  if (loop.length === 0) return

  const firstSeg = segments[loop[0]]

  // Determine the starting vertex — if the loop has multiple segments,
  // the first segment's direction is determined by which vertex connects
  // to the second segment.
  let current: number
  if (loop.length === 1) {
    current = firstSeg.start
  } else {
    const secondSeg = segments[loop[1]]
    if (firstSeg.end === secondSeg.start || firstSeg.end === secondSeg.end) {
      current = firstSeg.start
    } else {
      current = firstSeg.end
    }
  }

  path.moveTo(vertices[current].x, vertices[current].y)

  for (const segIdx of loop) {
    const seg = segments[segIdx]
    const forward = seg.start === current
    addSegmentDirected(path, seg, vertices, forward)
    current = forward ? seg.end : seg.start
  }

  path.close()
}

function addSegmentDirected(
  path: Path,
  seg: VectorSegment,
  vertices: VectorVertex[],
  forward: boolean
): void {
  const p0 = forward ? vertices[seg.start] : vertices[seg.end]
  const p3 = forward ? vertices[seg.end] : vertices[seg.start]
  const ts = seg.tangentStart
  const te = seg.tangentEnd

  const isLine = ts.x === 0 && ts.y === 0 && te.x === 0 && te.y === 0
  if (isLine) {
    path.lineTo(p3.x, p3.y)
  } else if (forward) {
    path.cubicTo(p0.x + ts.x, p0.y + ts.y, p3.x + te.x, p3.y + te.y, p3.x, p3.y)
  } else {
    // Reversed cubic: swap control points
    path.cubicTo(p0.x + te.x, p0.y + te.y, p3.x + ts.x, p3.y + ts.y, p3.x, p3.y)
  }
}

function findChainStart(chain: number[], segments: VectorSegment[]): number {
  if (chain.length < 2) return segments[chain[0]].start

  const first = segments[chain[0]]
  const second = segments[chain[1]]
  // The shared vertex between first and second is the "end" of the first
  // segment in this chain — so the start is the other vertex.
  if (first.start === second.start || first.start === second.end) return first.end
  return first.start
}

function buildChains(segments: VectorSegment[], _vertexCount: number): number[][] {
  if (segments.length === 0) return []

  // Build adjacency: for each vertex, which segments connect to it
  const adj = new Map<number, number[]>()
  for (let i = 0; i < segments.length; i++) {
    const s = segments[i]
    if (!adj.has(s.start)) adj.set(s.start, [])
    if (!adj.has(s.end)) adj.set(s.end, [])
    adj.get(s.start)?.push(i)
    adj.get(s.end)?.push(i)
  }

  const visited = new Set<number>()
  const chains: number[][] = []

  // Start from degree-1 vertices (endpoints) or any unvisited
  const degree1 = [...adj.entries()].filter(([, segs]) => segs.length === 1).map(([v]) => v)

  const startVertices = degree1.length > 0 ? degree1 : [segments[0].start]

  for (const startVertex of startVertices) {
    let current = startVertex
    const chain: number[] = []

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    while (true) {
      const segs = adj.get(current)
      if (!segs) break

      const nextSeg = segs.find((s) => !visited.has(s))
      if (nextSeg === undefined) break

      visited.add(nextSeg)
      chain.push(nextSeg)

      const seg = segments[nextSeg]
      current = seg.start === current ? seg.end : seg.start
    }

    if (chain.length > 0) chains.push(chain)
  }

  return chains
}

export function computeVectorBounds(network: VectorNetwork): Rect {
  return computeAccurateBounds(network)
}

const CMD_CLOSE = 0
const CMD_MOVE_TO = 1
const CMD_LINE_TO = 2
const CMD_CUBIC_TO = 4

export function geometryBlobToPath(
  ck: CanvasKit,
  blob: Uint8Array,
  windingRule: WindingRule
): Path {
  const path = new ck.Path()
  if (!(blob.buffer instanceof ArrayBuffer)) return path
  const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)
  let o = 0

  while (o < blob.length) {
    const cmd = blob[o++]
    switch (cmd) {
      case CMD_CLOSE:
        path.close()
        break
      case CMD_MOVE_TO: {
        const x = dv.getFloat32(o, true)
        const y = dv.getFloat32(o + 4, true)
        o += 8
        path.moveTo(x, y)
        break
      }
      case CMD_LINE_TO: {
        const x = dv.getFloat32(o, true)
        const y = dv.getFloat32(o + 4, true)
        o += 8
        path.lineTo(x, y)
        break
      }
      case CMD_CUBIC_TO: {
        const x1 = dv.getFloat32(o, true)
        const y1 = dv.getFloat32(o + 4, true)
        const x2 = dv.getFloat32(o + 8, true)
        const y2 = dv.getFloat32(o + 12, true)
        const x = dv.getFloat32(o + 16, true)
        const y = dv.getFloat32(o + 20, true)
        o += 24
        path.cubicTo(x1, y1, x2, y2, x, y)
        break
      }
      default:
        return path
    }
  }

  path.setFillType(windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding)
  return path
}
