<template>
  <div
    ref="containerRef"
    class="floating-lines-container"
    :style="containerStyle"
  ></div>
</template>

<script setup>
import { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'
import {
  Scene,
  OrthographicCamera,
  WebGLRenderer,
  PlaneGeometry,
  Mesh,
  ShaderMaterial,
  Vector3,
  Vector2,
  Clock
} from 'three'

const props = defineProps({
  lineCount: { type: [Array, Number], default: () => [10] },
  numPoints: { type: Number, default: 0 },
  pointXValues: { type: Array, default: () => [] },
  pointYValues: { type: Array, default: () => [] },
  pointColors: { type: Array, default: () => [] },
  lineSpread: { type: Number, default: 0.05 },
  fanSpread: { type: Number, default: 0.05 },
  lineSharpness: { type: Number, default: 8.0 },
  waveFrequency: { type: Number, default: 7.0 },
  bezierCurvature: { type: Number, default: 0.2 },
  circleRadiusPx: { type: Number, default: 75 },
  circleGlowSize: { type: Number, default: 18 },
  circleGlowStrength: { type: Number, default: 1.5 },
  lineBrightness: { type: Number, default: 1.0 },
  scrollContainer: { type: Object, default: null },
  scrollUvScale: { type: Number, default: 0 },
  animationSpeed: { type: Number, default: 1 },
  linesGradient: { type: Array, default: () => ['#e947f5', '#2f4ba2', '#0a0a12'] },
  bgColorCenter: { type: String, default: '#0a0514' },
  bgColorEdge: { type: String, default: '#000000' },
  backgroundImage: { type: String, default: '' },
  mixBlendMode: { type: String, default: 'screen' },
  parallax: { type: Boolean, default: false }
})

// FPS display
const fpsDisplay = ref(0)
const dprDisplay = ref('0.0')

const containerStyle = computed(() => {
  const style = {}
  if (props.backgroundImage) {
    style.backgroundImage = `url('${props.backgroundImage}')`
    style.backgroundSize = 'cover'
    style.backgroundPosition = 'center'
    style.backgroundRepeat = 'no-repeat'
  }
  return style
})

// --- Shader Definitions ---
const vertexShader = `
precision highp float;

void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`

const fragmentShader = `
precision mediump float;

uniform float iTime;
uniform vec3  iResolution;
uniform float animationSpeed;

uniform int   middleLineCount;
uniform int   numPoints;
uniform float pointX[16];
uniform float pointY[16];
uniform float lineSpread;
uniform float fanSpread;
uniform float lineSharpness;
uniform float waveFrequency;
uniform float bezierCurvature;
uniform float lineBrightness;
uniform vec3  pointColor[16];

uniform bool parallax;
uniform vec2 parallaxOffset;

uniform vec3 lineGradient[8];
uniform int lineGradientCount;
uniform vec3 bgColorCenter;
uniform vec3 bgColorEdge;

float bezierClosestT(vec2 q, vec2 p0, vec2 pc, vec2 p1) {
  float bestT = 0.0;
  float bestD = 1e9;
  for (int k = 0; k <= 8; ++k) {
    float t  = float(k) / 8.0;
    float mt = 1.0 - t;
    vec2  b  = mt*mt*p0 + 2.0*mt*t*pc + t*t*p1;
    float d  = dot(q - b, q - b);
    if (d < bestD) { bestD = d; bestT = t; }
  }

  vec2  A   = pc - p0;
  vec2  B   = p0 - 2.0*pc + p1;
  vec2  D   = p0 - q;
  float a   = 2.0*dot(B,B);
  float bco = 6.0*dot(A,B);
  float c   = 4.0*dot(A,A) + 2.0*dot(D,B);
  float dco = 2.0*dot(D,A);

  float t = clamp(bestT, 0.001, 0.999);
  for (int k = 0; k < 4; ++k) {
    float f  = a*t*t*t + bco*t*t + c*t + dco;
    float fp = 3.0*a*t*t + 2.0*bco*t + c;
    if (abs(fp) > 1e-8) t -= f / fp;
    t = clamp(t, -0.08, 1.08);
  }
  return t;
}

// Accepts precomputed bezier values (t, curvePos, norm) — computed once per segment
float waveFocal(vec2 uv, float fi, float totalLines, float t, vec2 curvePos, vec2 norm) {
  float s = dot(uv - curvePos, norm);

  float time        = iTime * animationSpeed;
  float normalizedI = totalLines > 1.0 ? fi / (totalLines - 1.0) : 0.5;

  float envelope = sin(t * 3.14159265359);
  float linePos  = (normalizedI - 0.5) * fanSpread * envelope;
  float amp      = lineSpread * 0.3 * envelope;
  float waveDisp = sin(t * waveFrequency + fi * 1.3 + time * 0.4) * amp
                 * sin(fi * 0.9 + time * 0.18);

  float dist = s - linePos - waveDisp;
  float fade = smoothstep(-0.06, 0.04, t) * smoothstep(1.06, 0.96, t);

  return fade * (0.013 / max(abs(dist) * lineSharpness + 0.004, 1e-4) + 0.003);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
  baseUv.y *= -1.0;

  if (parallax) {
    baseUv += parallaxOffset;
  }

  vec3 col = vec3(0.0);

  const int MAX_PTS  = 16;
  const int MAX_SEGS = 15;

  for (int s = 0; s < MAX_SEGS; ++s) {
    if (s >= numPoints - 1) break;

    vec2 sp = vec2(pointX[s], pointY[s]);
    vec2 ep = vec2(pointX[s + 1], pointY[s + 1]);

    vec2  segD   = ep - sp;
    float segL   = length(segD);
    vec2  segDir = segL > 0.001 ? segD / segL : vec2(1.0, 0.0);
    vec2  sPerp  = vec2(-segDir.y, segDir.x);
    vec2  pc     = (sp + ep) * 0.5 + sPerp * segL * bezierCurvature;

    float t_seg  = clamp(dot(baseUv - sp, segDir) / segL, 0.0, 1.0);
    vec3  lineCol = mix(pointColor[s], pointColor[s + 1], t_seg);

    // bezierClosestT computed ONCE per segment — shared by fog + all lines
    float bt    = bezierClosestT(baseUv, sp, pc, ep);
    float bmt   = 1.0 - bt;
    vec2  bPos  = bmt*bmt*sp + 2.0*bmt*bt*pc + bt*bt*ep;
    vec2  bTang = normalize(bmt*(pc - sp) + bt*(ep - pc));
    vec2  bNorm = vec2(-bTang.y, bTang.x);

    float bDist   = length(baseUv - bPos);
    float fogFade = smoothstep(-0.06, 0.05, bt) * smoothstep(1.06, 0.95, bt);
    float fogEnv  = sin(bt * 3.14159265359);
    float segFog  = fogFade * fogEnv * 0.0018 / max(bDist * bDist * 4.0 + 0.012, 0.001);
    col += lineCol * segFog;

    for (int i = 0; i < middleLineCount; ++i) {
      col += lineCol * waveFocal(baseUv, float(i), float(middleLineCount), bt, bPos, bNorm);
    }
  }

  col *= lineBrightness;

  float dist = length(baseUv) / 1.8;
  vec3 bg = mix(bgColorCenter, bgColorEdge, clamp(dist, 0.0, 1.0));
  fragColor = vec4(clamp(bg + col, 0.0, 1.0), 1.0);
}

void main() {
  vec4 color = vec4(0.0);
  mainImage(color, gl_FragCoord.xy);
  gl_FragColor = color;
}
`

// --- Helpers ---
const MAX_GRADIENT_STOPS = 8

function hexToVec3(hex) {
  let value = hex.trim()
  if (value.startsWith('#')) value = value.slice(1)
  let r = 255, g = 255, b = 255
  if (value.length === 3) {
    r = parseInt(value[0] + value[0], 16)
    g = parseInt(value[1] + value[1], 16)
    b = parseInt(value[2] + value[2], 16)
  } else if (value.length === 6) {
    r = parseInt(value.slice(0, 2), 16)
    g = parseInt(value.slice(2, 4), 16)
    b = parseInt(value.slice(4, 6), 16)
  }
  return new Vector3(r / 255, g / 255, b / 255)
}

// --- Component Logic ---
const containerRef = ref(null)

let scene = null
let camera = null
let renderer = null
let material = null
let geometry = null
let mesh = null
let clock = null
let rafId = null
let resizeObserver = null
let uniforms = null
let scrollHandler = null
let scrollIdleTimer = null
let visibilityHandler = null

// Parallax tracking
let targetParallax = null
let currentParallax = null
const parallaxDamping = 0.05

function getLineCount() {
  return typeof props.lineCount === 'number' ? props.lineCount : (props.lineCount[0] ?? 6)
}

function applyGradient() {
  if (!uniforms) return
  const lines = props.linesGradient.filter(s => s && s.trim().length > 0)
  const stops = lines.slice(0, MAX_GRADIENT_STOPS)
  uniforms.lineGradientCount.value = stops.length
  stops.forEach((hex, i) => {
    const c = hexToVec3(hex)
    uniforms.lineGradient.value[i].set(c.x, c.y, c.z)
  })
}

function applyPointColors() {
  if (!uniforms) return
  for (let i = 0; i < 16; i++) {
    const hex = props.pointColors[i]
    if (hex) {
      const c = hexToVec3(hex)
      uniforms.pointColor.value[i].set(c.x, c.y, c.z)
    } else {
      uniforms.pointColor.value[i].set(1, 1, 1)
    }
  }
}

function applyBgColors() {
  if (!uniforms) return
  const center = hexToVec3(props.bgColorCenter)
  uniforms.bgColorCenter.value.set(center.x, center.y, center.z)
  const edge = hexToVec3(props.bgColorEdge)
  uniforms.bgColorEdge.value.set(edge.x, edge.y, edge.z)
}

// Watch all props for live updates
watch(() => props.animationSpeed, (v) => { if (uniforms) uniforms.animationSpeed.value = v })
watch(() => props.lineCount, () => {
  if (!uniforms) return
  uniforms.middleLineCount.value = getLineCount()
})
watch(() => props.lineSpread, (v) => { if (uniforms) uniforms.lineSpread.value = v })
watch(() => props.fanSpread, (v) => { if (uniforms) uniforms.fanSpread.value = v })
watch(() => props.lineSharpness, (v) => { if (uniforms) uniforms.lineSharpness.value = v })
watch(() => props.waveFrequency, (v) => { if (uniforms) uniforms.waveFrequency.value = v })
watch(() => props.bezierCurvature, (v) => { if (uniforms) uniforms.bezierCurvature.value = v })
watch(() => props.lineBrightness, (v) => { if (uniforms) uniforms.lineBrightness.value = v })
watch(() => props.numPoints, (v) => { if (uniforms) uniforms.numPoints.value = v })
watch(() => props.pointXValues, (values) => {
  if (!uniforms) return
  for (let i = 0; i < 16; i++) {
    uniforms.pointX.value[i] = values[i] ?? 0
  }
}, { deep: true })
watch(() => props.pointYValues, (values) => {
  if (!uniforms) return
  for (let i = 0; i < 16; i++) {
    uniforms.pointY.value[i] = values[i] ?? 0
  }
}, { deep: true })
watch(() => props.pointColors, applyPointColors, { deep: true })
watch(() => props.linesGradient, applyGradient, { deep: true })
watch(() => props.bgColorCenter, applyBgColors)
watch(() => props.bgColorEdge, applyBgColors)

onMounted(() => {
  if (!containerRef.value) return

  targetParallax = new Vector2(0, 0)
  currentParallax = new Vector2(0, 0)

  scene = new Scene()
  camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1)
  camera.position.z = 1

  // Adaptive DPR: full quality when idle, reduced during scroll
  const nativeDpr = Math.min(window.devicePixelRatio || 1, 2)
  const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
  const DPR_IDLE = isMobile ? Math.min(nativeDpr, 1.6) : nativeDpr
  const DPR_SCROLL = isMobile ? 0.75 : nativeDpr     // reduced on mobile during scroll
  const DPR_RESTORE_DELAY = 100                       // ms after scroll stops to restore quality
  let currentDpr = DPR_IDLE
  let scrolling = false

  renderer = new WebGLRenderer({ antialias: !isMobile, alpha: false, powerPreference: 'high-performance' })
  renderer.setPixelRatio(currentDpr)
  renderer.domElement.style.width = '100%'
  renderer.domElement.style.height = '100%'
  renderer.domElement.style.display = 'block'
  renderer.domElement.style.mixBlendMode = props.mixBlendMode
  containerRef.value.appendChild(renderer.domElement)

  const middleLineCount = getLineCount()

  // Initial point positions (UV space, no flip)
  const initX = [...props.pointXValues].slice(0, 16).concat(Array(16).fill(0)).slice(0, 16)
  const initY = [...props.pointYValues].slice(0, 16).concat(Array(16).fill(0)).slice(0, 16)

  uniforms = {
    iTime: { value: 0 },
    iResolution: { value: new Vector3(1, 1, 1) },
    animationSpeed: { value: props.animationSpeed },

    middleLineCount: { value: middleLineCount },
    numPoints: { value: props.numPoints },
    pointX: { value: initX },
    pointY: { value: initY },
    lineSpread: { value: props.lineSpread },
    fanSpread: { value: props.fanSpread },
    lineSharpness: { value: props.lineSharpness },
    waveFrequency: { value: props.waveFrequency },
    bezierCurvature: { value: props.bezierCurvature },
    lineBrightness: { value: props.lineBrightness },
    pointColor: {
      value: Array.from({ length: 16 }, () => new Vector3(1, 1, 1))
    },

    parallax: { value: props.parallax },
    parallaxOffset: { value: new Vector2(0, 0) },

    lineGradient: {
      value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))
    },
    lineGradientCount: { value: 0 },
    bgColorCenter: { value: new Vector3(0, 0, 0) },
    bgColorEdge: { value: new Vector3(0, 0, 0) }
  }

  // Apply initial values
  applyGradient()
  applyBgColors()
  applyPointColors()

  material = new ShaderMaterial({
    uniforms,
    vertexShader,
    fragmentShader
  })

  geometry = new PlaneGeometry(2, 2)
  mesh = new Mesh(geometry, material)
  scene.add(mesh)

  clock = new Clock()

  // Resize
  const setSize = () => {
    if (!containerRef.value || !renderer) return
    const width = containerRef.value.clientWidth || 1
    const height = containerRef.value.clientHeight || 1
    renderer.setSize(width, height, false)
    const canvasWidth = renderer.domElement.width
    const canvasHeight = renderer.domElement.height
    uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1)
  }
  setSize()

  resizeObserver = new ResizeObserver(setSize)
  resizeObserver.observe(containerRef.value)

  // Pointer events (parallax only)
  if (props.parallax) {
    const handlePointerMove = (event) => {
      const rect = renderer.domElement.getBoundingClientRect()
      const x = event.clientX - rect.left
      const y = event.clientY - rect.top
      const centerX = rect.width / 2
      const centerY = rect.height / 2
      targetParallax.set(
        ((x - centerX) / rect.width) * 0.2,
        (-(y - centerY) / rect.height) * 0.2
      )
    }
    renderer.domElement.addEventListener('pointermove', handlePointerMove)
  }

  // Scroll sync: update cached scrollLeft + trigger adaptive DPR reduction.
  let cachedScrollLeft = 0

  function setDpr(dpr) {
    if (dpr === currentDpr) return
    currentDpr = dpr
    renderer.setPixelRatio(dpr)
    // Re-apply size so resolution updates
    if (containerRef.value) {
      const w = containerRef.value.clientWidth || 1
      const h = containerRef.value.clientHeight || 1
      renderer.setSize(w, h, false)
      uniforms.iResolution.value.set(renderer.domElement.width, renderer.domElement.height, 1)
    }
  }

  scrollHandler = () => {
    if (props.scrollContainer) {
      cachedScrollLeft = props.scrollContainer.scrollLeft || 0
    }
    // Drop DPR while scrolling on mobile
    if (!scrolling && DPR_SCROLL < DPR_IDLE) {
      scrolling = true
      setDpr(DPR_SCROLL)
    }
    clearTimeout(scrollIdleTimer)
    scrollIdleTimer = setTimeout(() => {
      scrolling = false
      setDpr(DPR_IDLE)
    }, DPR_RESTORE_DELAY)
  }

  if (props.scrollContainer) {
    cachedScrollLeft = props.scrollContainer.scrollLeft || 0
    props.scrollContainer.addEventListener('scroll', scrollHandler, { passive: true })
  }

  // Fast inline scroll sync — reads cached scrollLeft instead of DOM during render
  function syncScrollFromCache() {
    if (props.scrollUvScale > 0) {
      const sl = cachedScrollLeft
      for (let i = 0; i < 16; i++) {
        uniforms.pointX.value[i] = (props.pointXValues[i] ?? 0) - sl * props.scrollUvScale
      }
    }
  }

  // FPS tracking + adaptive DPR
  let frameCount = 0
  let fpsLastTime = performance.now()

  // Adaptive DPR steps based on measured FPS (mobile only)
  // Steps are tried from lowest to highest – first step where fps >= threshold wins
  const DPR_STEPS = isMobile ? [
    { threshold: 50, dpr: DPR_IDLE },                        // good fps  → full quality
    { threshold: 35, dpr: Math.max(DPR_IDLE * 0.75, 0.75) }, // mid fps   → 75%
    { threshold: 0,  dpr: Math.max(DPR_IDLE * 0.5,  0.5)  }, // low fps   → 50%
  ] : null
  let adaptiveDprTarget = DPR_IDLE

  // Pause rendering when app/tab is hidden (e.g. iPhone home screen)
  visibilityHandler = () => {
    if (document.hidden) {
      if (rafId) cancelAnimationFrame(rafId)
      rafId = null
    } else if (!rafId) {
      fpsLastTime = performance.now()
      frameCount = 0
      renderLoop()
    }
  }
  document.addEventListener('visibilitychange', visibilityHandler)

  // Render loop
  const renderLoop = () => {
    // FPS counter + adaptive DPR
    frameCount++
    const now = performance.now()
    if (now - fpsLastTime >= 1000) {
      const fps = Math.round(frameCount / ((now - fpsLastTime) / 1000))
      fpsDisplay.value = fps
      frameCount = 0
      fpsLastTime = now

      // Adapt DPR based on measured FPS (only when not scroll-throttled)
      if (DPR_STEPS && !scrolling) {
        const step = DPR_STEPS.find(s => fps >= s.threshold) ?? DPR_STEPS[DPR_STEPS.length - 1]
        if (step.dpr !== adaptiveDprTarget) {
          adaptiveDprTarget = step.dpr
          setDpr(adaptiveDprTarget)
        }
      }

      dprDisplay.value = currentDpr.toFixed(2)
    }

    // Read latest scrollLeft from DOM in case scroll event was missed
    if (props.scrollContainer) {
      cachedScrollLeft = props.scrollContainer.scrollLeft || 0
    }

    uniforms.iTime.value = clock.getElapsedTime()

    if (props.parallax) {
      currentParallax.lerp(targetParallax, parallaxDamping)
      uniforms.parallaxOffset.value.copy(currentParallax)
    }

    syncScrollFromCache()

    renderer.render(scene, camera)
    rafId = requestAnimationFrame(renderLoop)
  }
  renderLoop()
})

defineExpose({ fpsDisplay, dprDisplay })

onBeforeUnmount(() => {
  if (rafId) cancelAnimationFrame(rafId)
  if (visibilityHandler) document.removeEventListener('visibilitychange', visibilityHandler)
  if (resizeObserver) resizeObserver.disconnect()
  if (props.scrollContainer && scrollHandler) {
    props.scrollContainer.removeEventListener('scroll', scrollHandler)
  }
  clearTimeout(scrollIdleTimer)
  if (geometry) geometry.dispose()
  if (material) material.dispose()
  if (renderer) {
    renderer.dispose()
    if (renderer.domElement && renderer.domElement.parentNode) {
      renderer.domElement.parentNode.removeChild(renderer.domElement)
    }
  }
})
</script>

<style scoped>
.floating-lines-container {
  position: absolute;
  inset: 0;
  overflow: hidden;
  will-change: transform;
  transform: translateZ(0);
}
</style>
