<template>
  <div class="angle-editor-page p-40">
    <button class="open-btn px-24 py-10 color-fff border-none round-8 fs-14 pointer" @click="openModal">打开多角度编辑器</button>

    <div v-if="visible" class="modal-mask fixed flex-center" @click.self="closeModal">
      <div class="modal round-12 hidden">
        <header class="px-16 py-14">
          <div class="flex-between mb-10">
            <div class="color-fff">多角度编辑器</div>
            <span class="close-x fs-28 lh-22 color-888 pointer" @click="closeModal">×</span>
          </div>
          <div class="flex flex-wrap gap-6">
            <button
              v-for="p in presets"
              :key="p.value"
              class="preset-btn py-6 px-12 color-aaa round-6 fs-12 pointer"
              :class="{ active: activePreset === p.value }"
              @click="selectPreset(p)"
            >
              {{ p.label }}
            </button>
          </div>
        </header>

        <div class="modal-body flex gap-20 p-24">
          <div ref="containerRef" class="visualizer relative wh-300 round-14">
            <span class="arrow arrow-up absolute color-666 fs-12 pointer" @click="rotatePitch(-30)">▲</span>
            <span class="arrow arrow-down absolute color-666 fs-12 pointer" @click="rotatePitch(30)">▼</span>
            <span class="arrow arrow-left absolute color-666 fs-12 pointer" @click="rotateYaw(-30)">◀</span>
            <span class="arrow arrow-right absolute color-666 fs-12 pointer" @click="rotateYaw(30)">▶</span>
          </div>

          <div class="flex-1 flex-col gap-22 justify-center">
            <div class="control-item flex-col gap-8">
              <label>水平环绕</label>
              <div class="flex-items-center gap-12">
                <input type="range" v-model.number="yaw" min="-180" max="180" class="slider flex-1 h-4 round-2 pointer" />
                <span class="minw-40 text-right fs-13 color-fff">{{ yaw }}°</span>
              </div>
            </div>

            <div class="control-item flex-col gap-8">
              <label>垂直俯仰</label>
              <div class="flex-items-center gap-12">
                <input type="range" v-model.number="pitch" min="-90" max="90" class="slider blue flex-1 h-4 round-2 pointer" />
                <span class="minw-40 text-right fs-13 color-fff">{{ pitch }}°</span>
              </div>
            </div>

            <div class="control-item flex-col gap-8">
              <label>景别缩放</label>
              <div class="flex-items-center gap-12">
                <select v-model="shotScale" class="select-shot flex-1 h-32 color-fff round-6 px-8 pointer">
                  <option v-for="s in shotScales" :key="s" :value="s">{{ s }}</option>
                </select>
              </div>
            </div>

            <div class="control-item flex-col gap-8">
              <label>提示词</label>
              <div class="flex-items-center gap-12">
                <button
                  class="toggle w-40 h-20 round-10 border-none relative pointer"
                  :class="{ on: promptEnabled }"
                  @click="promptEnabled = !promptEnabled"
                >
                  <span class="toggle-knob absolute top-2 left-2 wh-16 round bg-fff"></span>
                </button>
              </div>
            </div>
          </div>
        </div>

        <footer class="modal-footer flex-between px-16 py-14">
          <button class="reset-btn flex-items-center gap-6 py-6 px-14 border-none round-6 fs-13 pointer" @click="resetParams">
            <span class="fs-14">↺</span> 重置参数
          </button>
          <div class="flex-items-center gap-12">
            <span class="cost fs-13">⚡1</span>
            <button class="generate-btn py-8 px-20 color-fff border-none round-6 fs-13 pointer" @click="generate">⬆ 生成</button>
          </div>
        </footer>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, onBeforeUnmount, nextTick } from 'vue';
import * as THREE from 'three';
import meinvUrl from '@/static/image/meinv.png';

const visible = ref(false);
const containerRef = ref<HTMLDivElement | null>(null);

// ============ 控制状态 ============
const yaw = ref(0);
const pitch = ref(60);
const shotScale = ref('中景');
const promptEnabled = ref(false);
const activePreset = ref('frontal-top');

const shotScales = ['远景', '全景', '中景', '近景', '特写'];

const presets = [
  { label: '自定义', value: 'custom', yaw: 0, pitch: 0 },
  { label: '鱼眼视角', value: 'fisheye', yaw: 0, pitch: 0 },
  { label: '倾斜视角', value: 'oblique', yaw: 30, pitch: 30 },
  { label: '正面俯拍', value: 'frontal-top', yaw: 0, pitch: 60 },
  { label: '正面仰拍', value: 'frontal-low', yaw: 0, pitch: -30 },
  { label: '全景俯拍', value: 'panoramic', yaw: 0, pitch: 88 },
  { label: '背面视角', value: 'rear', yaw: 180, pitch: 0 },
];

const selectPreset = (p: { value: string; yaw: number; pitch: number }) => {
  activePreset.value = p.value;
  yaw.value = p.yaw;
  pitch.value = p.pitch;
};

// ============ Three.js 场景 ============
let renderer: THREE.WebGLRenderer | null = null;
let scene: THREE.Scene | null = null;
let camera: THREE.PerspectiveCamera | null = null;
let group: THREE.Group | null = null;
let rafId = 0;
const R = 1.2;

// 线框材质：正面实线、背面虚线（通过世界 Z 判断前后，背面段按 aLineU 做虚线 discard）
const makeLineMaterial = () =>
  new THREE.ShaderMaterial({
    transparent: true,
    depthWrite: false,
    uniforms: {
      uColor: { value: new THREE.Color(0xffffff) },
      uFrontOpacity: { value: 0.55 },
      uBackOpacity: { value: 0.28 },
      uDashCount: { value: 24.0 },
    },
    vertexShader: `
      attribute float aLineU;
      varying float vLineU;
      varying float vWorldZ;
      void main() {
        vLineU = aLineU;
        vec4 wp = modelMatrix * vec4(position, 1.0);
        vWorldZ = wp.z;
        gl_Position = projectionMatrix * viewMatrix * wp;
      }
    `,
    fragmentShader: `
      uniform vec3 uColor;
      uniform float uFrontOpacity;
      uniform float uBackOpacity;
      uniform float uDashCount;
      varying float vLineU;
      varying float vWorldZ;
      void main() {
        float alpha;
        if (vWorldZ < 0.0) {
          alpha = uBackOpacity;
          float d = fract(vLineU * uDashCount);
          if (d < 0.5) discard;
        } else {
          alpha = uFrontOpacity;
        }
        gl_FragColor = vec4(uColor, alpha);
      }
    `,
  });

const lineMaterial = makeLineMaterial();
// 正面经线高亮材质（用于 theta=0 参考线，让 yaw 旋转可见）
const lineMaterialAccent = makeLineMaterial();
lineMaterialAccent.uniforms.uColor.value = new THREE.Color(0x409eff);
lineMaterialAccent.uniforms.uFrontOpacity.value = 0.9;
lineMaterialAccent.uniforms.uBackOpacity.value = 0.5;

const buildLatLine = (phi: number): THREE.LineLoop => {
  const pts: THREE.Vector3[] = [];
  const us: number[] = [];
  const N = 64;
  for (let i = 0; i < N; i++) {
    const theta = (i / N) * Math.PI * 2;
    pts.push(new THREE.Vector3(R * Math.cos(phi) * Math.sin(theta), R * Math.sin(phi), R * Math.cos(phi) * Math.cos(theta)));
    us.push(i / N);
  }
  const geo = new THREE.BufferGeometry().setFromPoints(pts);
  geo.setAttribute('aLineU', new THREE.Float32BufferAttribute(us, 1));
  return new THREE.LineLoop(geo, lineMaterial);
};

const buildLonLine = (theta: number, material: THREE.ShaderMaterial = lineMaterial): THREE.LineSegments => {
  const pts: THREE.Vector3[] = [];
  const us: number[] = [];
  const N = 48;
  for (let i = 0; i < N; i++) {
    const a = -Math.PI / 2 + (i / N) * Math.PI;
    const b = -Math.PI / 2 + ((i + 1) / N) * Math.PI;
    pts.push(
      new THREE.Vector3(R * Math.cos(a) * Math.sin(theta), R * Math.sin(a), R * Math.cos(a) * Math.cos(theta)),
      new THREE.Vector3(R * Math.cos(b) * Math.sin(theta), R * Math.sin(b), R * Math.cos(b) * Math.cos(theta))
    );
    us.push(i / N, (i + 1) / N);
  }
  const geo = new THREE.BufferGeometry().setFromPoints(pts);
  geo.setAttribute('aLineU', new THREE.Float32BufferAttribute(us, 1));
  return new THREE.LineSegments(geo, material);
};

const initScene = () => {
  const container = containerRef.value;
  if (!container) return;

  const w = container.clientWidth || 300;
  const h = container.clientHeight || 300;

  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 100);
  camera.position.set(0, 0, 5);
  camera.lookAt(0, 0, 0);

  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setSize(w, h);
  renderer.setPixelRatio(window.devicePixelRatio);
  container.appendChild(renderer.domElement);

  group = new THREE.Group();
  scene.add(group);
  const g = group;

  // 纬线（每 15°）
  [-75, -60, -45, -30, -15, 0, 15, 30, 45, 60, 75].forEach((deg) => {
    g.add(buildLatLine((deg * Math.PI) / 180));
  });
  // 经线（每 15°，覆盖整圈 0~345°，左右半球都有经线；theta=0 用高亮色作参考线）
  Array.from({ length: 24 }, (_, i) => i * 15).forEach((deg) => {
    const mat = deg === 0 ? lineMaterialAccent : lineMaterial;
    g.add(buildLonLine((deg * Math.PI) / 180, mat));
  });

  // 球体外轮廓（赤道高亮）
  const outlineGeo = new THREE.BufferGeometry().setFromPoints(
    Array.from({ length: 65 }, (_, i) => {
      const t = (i / 64) * Math.PI * 2;
      return new THREE.Vector3(R * Math.cos(t), 0, R * Math.sin(t));
    })
  );
  const outline = new THREE.LineLoop(outlineGeo, new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 }));
  group.add(outline);

  // 中心圆形缩略图（不随球体旋转，始终面向相机）
  const tex = new THREE.TextureLoader().load(meinvUrl);
  tex.colorSpace = THREE.SRGBColorSpace;
  const circleGeo = new THREE.CircleGeometry(0.4, 64);
  const circleMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.85, depthTest: false });
  const circle = new THREE.Mesh(circleGeo, circleMat);
  circle.renderOrder = 2;
  scene.add(circle);

  // 指示线 + 端点
  const lineGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, R)]);
  const lineMat = new THREE.LineBasicMaterial({ color: 0xffffff });
  const indicatorLine = new THREE.Line(lineGeo, lineMat);
  group.add(indicatorLine);

  const dotGeo = new THREE.SphereGeometry(0.06, 16, 16);
  const dotMat = new THREE.MeshBasicMaterial({ color: 0x1a1a2e });
  const dot = new THREE.Mesh(dotGeo, dotMat);
  dot.position.set(0, 0, R);
  group.add(dot);

  applyRotation();
  animate();
};

const applyRotation = () => {
  if (!group) return;
  group.rotation.y = (yaw.value * Math.PI) / 180;
  group.rotation.x = (pitch.value * Math.PI) / 180;
};

const animate = () => {
  rafId = requestAnimationFrame(animate);
  if (renderer && scene && camera) {
    renderer.render(scene, camera);
  }
};

const onResize = () => {
  const container = containerRef.value;
  if (!container || !renderer || !camera) return;
  const w = container.clientWidth || 300;
  const h = container.clientHeight || 300;
  renderer.setSize(w, h);
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
};

// ============ 拖拽旋转 ============
let dragging = false;
let dragStartX = 0;
let dragStartY = 0;
let dragStartYaw = 0;
let dragStartPitch = 0;
const SENS = 0.5;

const onPointerDown = (e: PointerEvent) => {
  dragging = true;
  activePreset.value = 'custom';
  dragStartX = e.clientX;
  dragStartY = e.clientY;
  dragStartYaw = yaw.value;
  dragStartPitch = pitch.value;
  window.addEventListener('pointermove', onPointerMove);
  window.addEventListener('pointerup', onPointerUp);
};

const onPointerMove = (e: PointerEvent) => {
  if (!dragging) return;
  const dx = e.clientX - dragStartX;
  const dy = e.clientY - dragStartY;
  yaw.value = Math.round(wrap(dragStartYaw + dx * SENS));
  pitch.value = Math.round(clamp(dragStartPitch + dy * SENS, -90, 90));
};

const onPointerUp = () => {
  dragging = false;
  window.removeEventListener('pointermove', onPointerMove);
  window.removeEventListener('pointerup', onPointerUp);
};

const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(max, v));
const wrap = (v: number) => {
  while (v > 180) v -= 360;
  while (v < -180) v += 360;
  return v;
};

// 点击四周图标：球体旋转 30°（带缓动动画）
const ANIM_MS = 450;
let animId: number | null = null;
let animStart = 0;
let animFromYaw = 0;
let animToYaw = 0;
let animFromPitch = 0;
let animToPitch = 0;
let animActive = false;

const easeOutQuad = (t: number) => t * (2 - t);

const animateTo = (toYaw: number, toPitch: number) => {
  if (animId !== null) cancelAnimationFrame(animId);
  animFromYaw = yaw.value;
  animFromPitch = pitch.value;
  animToYaw = toYaw;
  animToPitch = toPitch;
  animStart = performance.now();
  animActive = true;
  const step = (now: number) => {
    const t = Math.min(1, (now - animStart) / ANIM_MS);
    const e = easeOutQuad(t);
    yaw.value = Math.round(animFromYaw + (animToYaw - animFromYaw) * e);
    pitch.value = Math.round(animFromPitch + (animToPitch - animFromPitch) * e);
    if (t < 1) {
      animId = requestAnimationFrame(step);
    } else {
      yaw.value = Math.round(wrap(animToYaw));
      pitch.value = Math.round(clamp(animToPitch, -90, 90));
      animId = null;
      animActive = false;
    }
  };
  animId = requestAnimationFrame(step);
};

const rotateYaw = (delta: number) => {
  activePreset.value = 'custom';
  animateTo(yaw.value + delta, pitch.value);
};
const rotatePitch = (delta: number) => {
  activePreset.value = 'custom';
  animateTo(yaw.value, clamp(pitch.value + delta, -90, 90));
};

// ============ 生命周期 ============
const openModal = () => {
  visible.value = true;
  nextTick(() => {
    initScene();
    const c = containerRef.value;
    if (c) c.addEventListener('pointerdown', onPointerDown);
    window.addEventListener('resize', onResize);
  });
};

const closeModal = () => {
  visible.value = false;
  cleanup();
};

const cleanup = () => {
  cancelAnimationFrame(rafId);
  if (animId !== null) cancelAnimationFrame(animId);
  window.removeEventListener('resize', onResize);
  window.removeEventListener('pointermove', onPointerMove);
  window.removeEventListener('pointerup', onPointerUp);
  const c = containerRef.value;
  if (c) c.removeEventListener('pointerdown', onPointerDown);
  if (renderer) {
    renderer.dispose();
    renderer.forceContextLoss();
    if (renderer.domElement.parentNode) {
      renderer.domElement.parentNode.removeChild(renderer.domElement);
    }
    renderer = null;
  }
  scene = null;
  camera = null;
  group = null;
};

watch([yaw, pitch], () => applyRotation());

onBeforeUnmount(() => cleanup());

// ============ 操作 ============
const resetParams = () => {
  yaw.value = 0;
  pitch.value = 0;
  shotScale.value = '中景';
  promptEnabled.value = false;
  activePreset.value = 'custom';
};

const generate = () => {
  console.log('生成参数', {
    yaw: yaw.value,
    pitch: pitch.value,
    shotScale: shotScale.value,
    promptEnabled: promptEnabled.value,
  });
};
</script>

<style scoped lang="scss">
.open-btn {
  background: #409eff;
  &:hover {
    background: #5599ff;
  }
}

.modal-mask {
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 9999;
}

.modal {
  width: 680px;
  background: #1a1a2e;
  border: 1px solid #333;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}

.preset-btn {
  background: transparent;
  transition: all 0.2s;
  border: 1px solid #3a3a4e;
  &:hover {
    background: #3a3a4e;
    color: #fff;
  }
  &.active {
    background: #444;
    color: #fff;
    border: 1px solid #444;
  }
}

.close-x {
  &:hover {
    color: #fff;
  }
}

.modal-body {
  border-bottom: 1px solid #2a2a3e;
}

.visualizer {
  flex-shrink: 0;
  background: #ffffff10;
  cursor: grab;
  &:active {
    cursor: grabbing;
  }
}

.arrow {
  user-select: none;
  z-index: 2;
  border-radius: 4px;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  color: #ccc;
  &:hover {
    color: #fff;
  }
}
.arrow-up {
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
}
.arrow-down {
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
}
.arrow-left {
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
}
.arrow-right {
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
}

.control-item {
  label {
    font-size: 13px;
    color: #ccc;
  }
}

.slider {
  -webkit-appearance: none;
  appearance: none;
  background: #2a2a3e;
  outline: none;

  &::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #888;
    cursor: pointer;
  }

  &.blue::-webkit-slider-thumb {
    background: #409eff;
  }
}

.select-shot {
  background: #2a2a3e;
  border: 1px solid #3a3a4e;
  outline: none;
}

.toggle {
  background: #3a3a4e;
  transition: background 0.2s;

  &.on {
    background: #409eff;
  }
}

.toggle-knob {
  transition: transform 0.2s;
}

.toggle.on .toggle-knob {
  transform: translateX(20px);
}

.modal-footer {
  border-top: 1px solid #2a2a3e;
}

.reset-btn {
  background: #2a2a3e;
  color: #ccc;
  &:hover {
    background: #3a3a4e;
    color: #fff;
  }
}

.cost {
  color: #ffa500;
}

.generate-btn {
  background: #409eff;
  &:hover {
    background: #5599ff;
  }
}
</style>
