import { OptimizedBuffer, RGBA, TextAttributes } from "@opentui/core"
import { go } from "../logo"

const PERIOD = 4600
const RINGS = 3
const WIDTH = 3.8
const TAIL = 9.5
const AMP = 0.55
const TAIL_AMP = 0.16
const BREATH_AMP = 0.05
const BREATH_SPEED = 0.0008
// Offset so the bg ring emits from the estimated GO center when the logo shimmer peaks.
const PHASE_OFFSET = 0.29
const LOGO_GAP = 1
const LOGO_TOP_BIAS = -1
const LOGO_LEFT_WIDTH = go.left[0]?.length ?? 0
const LOGO_LINES = go.left.map((line, index) => line + " ".repeat(LOGO_GAP) + go.right[index])
const LOGO_WIDTH = LOGO_LINES[0]?.length ?? 0
const LOGO_HEIGHT = LOGO_LINES.length
const SPACE = " ".codePointAt(0)!
const TOP_HALF = "▀".codePointAt(0)!
const FULL_BLOCK = "█".codePointAt(0)!
const RING_SCALE = 1 / RINGS
const TAIL_SCALE = 1 / TAIL
const LOGO_REACH = Math.hypot(LOGO_WIDTH, LOGO_HEIGHT * 2) + 3

const enum LogoCellKind {
  Background,
  Top,
  ShadowTop,
  Solid,
  Char,
}

type LogoTemplateCell = {
  x: number
  y: number
  kind: LogoCellKind
  charCode: number
  attributes: number
  topDist: number
  bottomDist: number
}

const LOGO_TEMPLATE: LogoTemplateCell[] = LOGO_LINES.flatMap((line, y) =>
  Array.from(line)
    .map((char, x) => {
      if (char === " ") return
      const kind =
        char === "_"
          ? LogoCellKind.Background
          : char === "^"
            ? LogoCellKind.Top
            : char === "~"
              ? LogoCellKind.ShadowTop
              : char === "█"
                ? LogoCellKind.Solid
                : LogoCellKind.Char
      return {
        x,
        y,
        kind,
        charCode: char.codePointAt(0) ?? SPACE,
        attributes: x > LOGO_LEFT_WIDTH ? TextAttributes.BOLD : 0,
        topDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 - LOGO_HEIGHT),
        bottomDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 + 1 - LOGO_HEIGHT),
      }
    })
    .filter((cell): cell is LogoTemplateCell => !!cell),
)

export type Rgb = [number, number, number]

export type GoUpsellArtRenderOptions = {
  deltaTime?: number
  rgb?: boolean
  cache?: boolean
}

const CACHE_FRAME_COUNT = Math.round(PERIOD / (1000 / 30))
const CACHE_FRAMES_PER_RENDER = 1

export function toRgb(color: RGBA): Rgb {
  const [r, g, b] = color.toInts()
  return [r, g, b]
}

function clamp(n: number) {
  return Math.max(0, Math.min(1, n))
}

function writeRgb(buffer: Uint16Array, offset: number, r: number, g: number, b: number, a = 255) {
  buffer[offset] = r
  buffer[offset + 1] = g
  buffer[offset + 2] = b
  buffer[offset + 3] = a
}

function mixChannel(base: number, overlay: number, alpha: number) {
  return Math.round(base + (overlay - base) * clamp(alpha))
}

function writeLogoTint(
  buffer: Uint16Array,
  offset: number,
  base: Rgb,
  primary: Rgb,
  primaryMix: number,
  peakMix: number,
) {
  const p = clamp(primaryMix)
  const q = clamp(peakMix)
  const r = mixChannel(mixChannel(base[0], primary[0], p), 255, q)
  const g = mixChannel(mixChannel(base[1], primary[1], p), 255, q)
  const b = mixChannel(mixChannel(base[2], primary[2], p), 255, q)
  writeRgb(buffer, offset, r, g, b)
}

function sameRgb(a: Rgb, b: Rgb) {
  return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]
}

export class GoUpsellArtPainter {
  private panelRgb: Rgb = [0, 0, 0]
  private primaryRgb: Rgb = [255, 255, 255]
  private logoBaseRgb: Rgb = [180, 180, 180]
  private elapsed = 0
  private distances = new Float32Array(0)
  private edgeFalloff = new Float32Array(0)
  private geometryWidth = 0
  private geometryHeight = 0
  private reach = 1
  private logoX = 0
  private logoY = 0
  private logoIndexes = new Int32Array(0)
  private logoRgb: boolean | undefined
  private pulsePeak = 0
  private pulsePrimary = 0
  private cacheDirty = true
  private frameCache: Array<{ fg: Uint16Array; bg: Uint16Array }> = []
  private cacheBuildIndex = 0

  setBackgroundPanel(value: RGBA | Rgb | undefined) {
    if (!value) return false
    const next = value instanceof RGBA ? toRgb(value) : value
    if (sameRgb(this.panelRgb, next)) return false
    this.panelRgb = next
    this.invalidateCache()
    return true
  }

  setLogoBase(value: RGBA | Rgb | undefined) {
    if (!value) return false
    const next = value instanceof RGBA ? toRgb(value) : value
    if (sameRgb(this.logoBaseRgb, next)) return false
    this.logoBaseRgb = next
    this.invalidateCache()
    return true
  }

  setPrimary(value: RGBA | Rgb | undefined) {
    if (!value) return false
    const next = value instanceof RGBA ? toRgb(value) : value
    if (sameRgb(this.primaryRgb, next)) return false
    this.primaryRgb = next
    this.invalidateCache()
    return true
  }

  render(frameBuffer: OptimizedBuffer, options: GoUpsellArtRenderOptions = {}) {
    const rgb = options.rgb === true
    this.elapsed = (this.elapsed + (options.deltaTime ?? 0)) % PERIOD
    this.rebuildGeometry(frameBuffer, rgb)
    if (options.cache !== false) {
      this.drawCached(frameBuffer, rgb)
      return
    }
    this.drawBackground(frameBuffer, this.elapsed)
    this.drawLogo(frameBuffer, this.elapsed, rgb)
  }

  private invalidateCache() {
    this.cacheDirty = true
    this.cacheBuildIndex = 0
    this.frameCache = []
  }

  private rebuildGeometry(frameBuffer: OptimizedBuffer, rgb: boolean) {
    const width = frameBuffer.width
    const height = frameBuffer.height
    const geometryChanged = width !== this.geometryWidth || height !== this.geometryHeight
    const logoTemplateChanged = this.logoRgb !== rgb
    if (!geometryChanged && !logoTemplateChanged) return

    if (geometryChanged) {
      this.geometryWidth = width
      this.geometryHeight = height
      this.logoX = Math.max(0, Math.floor((width - LOGO_WIDTH) / 2))
      this.logoY = Math.max(
        0,
        Math.min(Math.max(0, height - LOGO_HEIGHT), Math.round((height - LOGO_HEIGHT) / 2) + LOGO_TOP_BIAS),
      )

      const centerX = this.logoX + LOGO_WIDTH / 2
      const centerY = this.logoY + LOGO_HEIGHT / 2
      this.reach = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2) + TAIL
      this.distances = new Float32Array(width * height)
      this.edgeFalloff = new Float32Array(width * height)

      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const index = y * width + x
          const dist = Math.hypot(x + 0.5 - centerX, (y + 0.5 - centerY) * 2)
          this.distances[index] = dist
          this.edgeFalloff[index] = Math.max(0, 1 - (dist / (this.reach * 0.85)) ** 2)
        }
      }
    }

    this.logoRgb = rgb
    this.invalidateCache()
    this.rebuildCellTemplate(frameBuffer, rgb)
  }

  private drawCached(frameBuffer: OptimizedBuffer, rgb: boolean) {
    if (this.cacheDirty) this.startFrameCache(frameBuffer, rgb)
    if (this.cacheBuildIndex < CACHE_FRAME_COUNT) {
      this.buildFrameCache(frameBuffer, rgb)
      this.drawBackground(frameBuffer, this.elapsed)
      this.drawLogo(frameBuffer, this.elapsed, rgb)
      return
    }

    const frame = this.frameCache[Math.floor((this.elapsed / PERIOD) * CACHE_FRAME_COUNT) % CACHE_FRAME_COUNT]
    if (frame) {
      frameBuffer.buffers.fg.set(frame.fg)
      frameBuffer.buffers.bg.set(frame.bg)
    }
  }

  private startFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
    this.frameCache = []
    this.cacheBuildIndex = 0
    this.rebuildCellTemplate(frameBuffer, rgb)
    this.cacheDirty = false
  }

  private buildFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
    const end = Math.min(CACHE_FRAME_COUNT, this.cacheBuildIndex + CACHE_FRAMES_PER_RENDER)
    for (; this.cacheBuildIndex < end; this.cacheBuildIndex++) {
      const t = (this.cacheBuildIndex / CACHE_FRAME_COUNT) * PERIOD
      this.drawBackground(frameBuffer, t)
      this.drawLogo(frameBuffer, t, rgb)
      this.frameCache.push({
        fg: new Uint16Array(frameBuffer.buffers.fg),
        bg: new Uint16Array(frameBuffer.buffers.bg),
      })
    }
  }

  private rebuildCellTemplate(frameBuffer: OptimizedBuffer, rgb: boolean) {
    const buffers = frameBuffer.buffers
    buffers.char.fill(SPACE)
    buffers.attributes.fill(0)

    if (this.geometryWidth < LOGO_WIDTH || this.geometryHeight < LOGO_HEIGHT) {
      this.logoIndexes = new Int32Array(0)
      return
    }

    this.logoIndexes = new Int32Array(LOGO_TEMPLATE.length)
    for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
      const cell = LOGO_TEMPLATE[i]!
      const index = (this.logoY + cell.y) * this.geometryWidth + this.logoX + cell.x
      this.logoIndexes[i] = index
      buffers.attributes[index] = cell.attributes
      buffers.char[index] =
        cell.kind === LogoCellKind.Background
          ? SPACE
          : cell.kind === LogoCellKind.Top || cell.kind === LogoCellKind.ShadowTop
            ? TOP_HALF
            : cell.kind === LogoCellKind.Solid
              ? rgb
                ? TOP_HALF
                : FULL_BLOCK
              : cell.charCode
    }
  }

  private drawBackground(frameBuffer: OptimizedBuffer, t: number) {
    const buffers = frameBuffer.buffers
    const fg = buffers.fg
    const bg = buffers.bg
    const distances = this.distances
    const edgeFalloff = this.edgeFalloff
    const baseR = this.panelRgb[0]
    const baseG = this.panelRgb[1]
    const baseB = this.panelRgb[2]
    const deltaR = this.primaryRgb[0] - baseR
    const deltaG = this.primaryRgb[1] - baseG
    const deltaB = this.primaryRgb[2] - baseB
    const breath = (0.5 + 0.5 * Math.sin(t * BREATH_SPEED)) * BREATH_AMP

    const phase0 = (t / PERIOD - PHASE_OFFSET + 1) % 1
    const phase1 = (t / PERIOD + 1 / RINGS - PHASE_OFFSET + 1) % 1
    const phase2 = (t / PERIOD + 2 / RINGS - PHASE_OFFSET + 1) % 1
    const envelope0 = Math.sin(phase0 * Math.PI)
    const envelope1 = Math.sin(phase1 * Math.PI)
    const envelope2 = Math.sin(phase2 * Math.PI)
    const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
    const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
    const eased2 = envelope2 * envelope2 * (3 - 2 * envelope2)
    const head0 = phase0 * this.reach
    const head1 = phase1 * this.reach
    const head2 = phase2 * this.reach

    for (let index = 0; index < distances.length; index++) {
      const dist = distances[index]
      const delta0 = dist - head0
      const abs0 = delta0 < 0 ? -delta0 : delta0
      const crest0 = abs0 < WIDTH ? 0.5 + 0.5 * Math.cos((delta0 / WIDTH) * Math.PI) : 0
      const tail0 = delta0 < 0 && delta0 > -TAIL ? (1 + delta0 * TAIL_SCALE) ** 2.3 : 0

      const delta1 = dist - head1
      const abs1 = delta1 < 0 ? -delta1 : delta1
      const crest1 = abs1 < WIDTH ? 0.5 + 0.5 * Math.cos((delta1 / WIDTH) * Math.PI) : 0
      const tail1 = delta1 < 0 && delta1 > -TAIL ? (1 + delta1 * TAIL_SCALE) ** 2.3 : 0

      const delta2 = dist - head2
      const abs2 = delta2 < 0 ? -delta2 : delta2
      const crest2 = abs2 < WIDTH ? 0.5 + 0.5 * Math.cos((delta2 / WIDTH) * Math.PI) : 0
      const tail2 = delta2 < 0 && delta2 > -TAIL ? (1 + delta2 * TAIL_SCALE) ** 2.3 : 0

      const level =
        (crest0 * AMP + tail0 * TAIL_AMP) * eased0 +
        (crest1 * AMP + tail1 * TAIL_AMP) * eased1 +
        (crest2 * AMP + tail2 * TAIL_AMP) * eased2
      const rawStrength = (level * RING_SCALE + breath) * edgeFalloff[index]
      const strength = (rawStrength > 1 ? 1 : rawStrength) * 0.7
      const offset = index * 4
      const r = Math.round(baseR + deltaR * strength)
      const g = Math.round(baseG + deltaG * strength)
      const b = Math.round(baseB + deltaB * strength)
      bg[offset] = fg[offset] = r
      bg[offset + 1] = fg[offset + 1] = g
      bg[offset + 2] = fg[offset + 2] = b
      bg[offset + 3] = fg[offset + 3] = 255
    }
  }

  private setLogoPulse(dist: number, head0: number, eased0: number, head1: number, eased1: number) {
    let peak = 0.04
    let primary = 0

    const delta0 = dist - head0
    const core0 = Math.exp(-(Math.abs(delta0 / 1.2) ** 1.8))
    const soft0 = Math.exp(-(Math.abs(delta0 / 7) ** 1.6))
    const tail0 = delta0 < 0 && delta0 > -7 ? (1 + delta0 / 7) ** 2.6 : 0
    peak += core0 * 0.65 * eased0
    primary += (soft0 * 0.16 + tail0 * 0.22) * eased0

    const delta1 = dist - head1
    const core1 = Math.exp(-(Math.abs(delta1 / 1.2) ** 1.8))
    const soft1 = Math.exp(-(Math.abs(delta1 / 7) ** 1.6))
    const tail1 = delta1 < 0 && delta1 > -7 ? (1 + delta1 / 7) ** 2.6 : 0
    peak += core1 * 0.65 * eased1
    primary += (soft1 * 0.16 + tail1 * 0.22) * eased1

    this.pulsePeak = peak > 1 ? 1 : peak
    this.pulsePrimary = primary > 1 ? 1 : primary
  }

  private drawLogo(frameBuffer: OptimizedBuffer, t: number, rgb: boolean) {
    if (this.logoIndexes.length === 0) return

    const buffers = frameBuffer.buffers
    const fg = buffers.fg
    const bg = buffers.bg
    const shadow: Rgb = [
      mixChannel(this.panelRgb[0], this.logoBaseRgb[0], 0.25),
      mixChannel(this.panelRgb[1], this.logoBaseRgb[1], 0.25),
      mixChannel(this.panelRgb[2], this.logoBaseRgb[2], 0.25),
    ]
    const phase0 = (t / PERIOD) % 1
    const phase1 = (t / PERIOD + 0.5) % 1
    const envelope0 = Math.sin(phase0 * Math.PI)
    const envelope1 = Math.sin(phase1 * Math.PI)
    const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
    const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
    const head0 = phase0 * LOGO_REACH
    const head1 = phase1 * LOGO_REACH

    for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
      const cell = LOGO_TEMPLATE[i]!
      const index = this.logoIndexes[i]!
      const offset = index * 4
      this.setLogoPulse(cell.topDist, head0, eased0, head1, eased1)
      const topPeak = this.pulsePeak
      const topPrimary = this.pulsePrimary
      this.setLogoPulse(cell.bottomDist, head0, eased0, head1, eased1)
      const bottomPeak = this.pulsePeak
      const bottomPrimary = this.pulsePrimary

      if (cell.kind === LogoCellKind.Background) {
        writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, Math.max(topPeak, bottomPeak) * 0.18)
        continue
      }

      if (cell.kind === LogoCellKind.Top) {
        writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
        writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, bottomPeak * 0.18)
        continue
      }

      if (cell.kind === LogoCellKind.ShadowTop) {
        writeLogoTint(fg, offset, shadow, this.primaryRgb, 0, topPeak * 0.18)
        continue
      }

      if (cell.kind === LogoCellKind.Solid && rgb) {
        writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
        writeLogoTint(bg, offset, this.logoBaseRgb, this.primaryRgb, bottomPrimary, bottomPeak)
        continue
      }

      writeLogoTint(
        fg,
        offset,
        this.logoBaseRgb,
        this.primaryRgb,
        (topPrimary + bottomPrimary) / 2,
        (topPeak + bottomPeak) / 2,
      )
    }
  }
}
