import * as THREE from "./three.module.js";

const EXT_COLOR = "#ffb300";
const TRAVEL_COLOR = "#7986cb";

function cssVar(name, fallback) {
  const v = getComputedStyle(document.body).getPropertyValue(name).trim();
  return v || fallback;
}

class GcodeViewerCard extends HTMLElement {
  static getConfigElement() {
    return document.createElement("gcode-viewer-card-editor");
  }

  static getStubConfig() {
    return {};
  }

  constructor() {
    super();
    this._config = {};
    this._hass = null;
    this._threeReady = false;
    this._loading = false;
    this._renderer = null;
    this._scene = null;
    this._camera = null;
    this._extSegs = null;
    this._travelSegs = null;
    this._grid = null;
    this._target = new THREE.Vector3();
    this._radius = 100;
    this._yaw = 0.8;
    this._pitch = 0.6;
    this._attached = false;
    this._resizeObs = null;
    this.attachShadow({ mode: "open" });
  }

  setConfig(config) {
    this._config = { ...config };
  }

  set hass(hass) {
    this._hass = hass;
    if (this._attached && !this._loading && !this._loadedOnce && this._threeReady) {
      this._load();
    }
  }

  getCardSize() {
    return 8;
  }

  connectedCallback() {
    this._attached = true;
    this._ensureBuilt();
  }

  disconnectedCallback() {
    this._attached = false;
    if (this._resizeObs) {
      this._resizeObs.disconnect();
      this._resizeObs = null;
    }
    if (this._renderer) {
      this._renderer.dispose();
      this._renderer = null;
    }
  }

  async _initThree() {
    this._threeReady = true;
    if (this._attached) {
      this._ensureBuilt();
    }
  }

  _buildDom() {
    const style = document.createElement("style");
    style.textContent = `
      :host {
        display: block;
        height: 520px;
        min-height: 420px;
        --gcode-extrusion-color: #ffb300;
        --gcode-travel-color: #7986cb;
      }
      .wrapper {
        position: relative;
        width: 100%;
        height: 100%;
        overflow: hidden;
        border-radius: var(--ha-card-border-radius, 12px);
        box-shadow: var(--ha-card-box-shadow, 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12));
        background: var(--card-background-color, var(--primary-background-color, #0b0b10));
      }
      .toolbar {
        position: absolute;
        top: 8px;
        right: 8px;
        display: flex;
        gap: 4px;
        z-index: 2;
      }
      .toolbar ha-icon-button {
        background: rgba(0,0,0,.35);
        border-radius: 50%;
        color: var(--primary-text-color, #fff);
        --mdc-icon-button-size: 32px;
      }
      .statusbar {
        position: absolute;
        left: 12px;
        right: 12px;
        bottom: 10px;
        z-index: 2;
        color: var(--secondary-text-color, #aaa);
        font-size: 12px;
        display: flex;
        align-items: center;
        gap: 8px;
        pointer-events: none;
      }
      .statusbar .msg { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; flex: 1; }
      .statusbar .pct { flex: none; }
      .bar {
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        height: 3px;
        background: rgba(255,255,255,.12);
        z-index: 3;
        display: none;
      }
      .bar .fill {
        height: 100%;
        width: 0%;
        background: var(--gcode-extrusion-color, #ffb300);
        transition: width .15s linear;
      }
      .bar.show { display: block; }
      .err {
        position: absolute;
        inset: 0;
        display: none;
        align-items: center;
        justify-content: center;
        text-align: center;
        color: var(--secondary-text-color, #aaa);
        font-size: 14px;
        padding: 24px;
        z-index: 1;
      }
      .err.show { display: flex; }
      .err .inner { max-width: 340px; }
      .err ha-icon { color: var(--error-color, #f24); }
      canvas { display: block; width: 100%; height: 100%; }
    `;
    this.shadowRoot.appendChild(style);

    const wrapper = document.createElement("div");
    wrapper.className = "wrapper";

    const bar = document.createElement("div");
    bar.className = "bar";
    bar.innerHTML = '<div class="fill"></div>';
    wrapper.appendChild(bar);

    const toolbar = document.createElement("div");
    toolbar.className = "toolbar";

    const fitBtn = document.createElement("ha-icon-button");
    fitBtn.icon = "mdi:fit-to-screen-outline";
    fitBtn.title = "Fit view";
    fitBtn.addEventListener("click", () => this._fit());
    toolbar.appendChild(fitBtn);

    const refreshBtn = document.createElement("ha-icon-button");
    refreshBtn.icon = "mdi:refresh";
    refreshBtn.title = "Reload G-code";
    refreshBtn.addEventListener("click", () => this._load());
    toolbar.appendChild(refreshBtn);

    wrapper.appendChild(toolbar);

    const statusbar = document.createElement("div");
    statusbar.className = "statusbar";
    statusbar.innerHTML =
      '<span class="msg"></span><span class="pct"></span>';
    wrapper.appendChild(statusbar);

    const err = document.createElement("div");
    err.className = "err";
    err.innerHTML =
      '<div class="inner"><ha-icon icon="mdi:alert-circle-outline" style="font-size:40px"></ha-icon><div></div></div>';
    wrapper.appendChild(err);

    this.shadowRoot.appendChild(wrapper);

    this._els = {
      bar,
      fill: bar.querySelector(".fill"),
      statusbar,
      msg: statusbar.querySelector(".msg"),
      pct: statusbar.querySelector(".pct"),
      err,
      errMsg: err.querySelector("div div"),
    };

    this._setupThree();
    this._setupControls(wrapper);
    this._resizeObs = new ResizeObserver(() => this._resize());
    this._resizeObs.observe(wrapper);
  }

  _ensureBuilt() {
    if (!this._renderer) {
      this._buildDom();
    }
  }

  _setupThree() {
    const canvas = document.createElement("canvas");
    this.shadowRoot.querySelector(".wrapper").appendChild(canvas);
    this._canvas = canvas;

    const renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      alpha: false,
    });
    renderer.setClearColor(0x000000, 0);
    this._renderer = renderer;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0b0b10);
    scene.add(new THREE.AmbientLight(0xffffff, 0.55));
    const dir = new THREE.DirectionalLight(0xffffff, 0.9);
    dir.position.set(150, 250, 200);
    scene.add(dir);
    this._scene = scene;

    const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 20000);
    camera.position.set(120, 120, 180);
    camera.lookAt(0, 0, 0);
    this._camera = camera;

    this._resize();
    this._render();
  }

  _setupControls(wrapper) {
    let dragging = false;
    let lastX = 0;
    let lastY = 0;

    const onPointerDown = (ev) => {
      dragging = true;
      lastX = ev.clientX;
      lastY = ev.clientY;
      this._canvas.setPointerCapture(ev.pointerId);
    };
    const onPointerMove = (ev) => {
      if (!dragging) return;
      const dx = ev.clientX - lastX;
      const dy = ev.clientY - lastY;
      lastX = ev.clientX;
      lastY = ev.clientY;
      this._yaw -= dx * 0.008;
      this._pitch = Math.max(0.05, Math.min(Math.PI / 2 - 0.05, this._pitch + dy * 0.008));
      this._updateCamera();
      this._render();
    };
    const onPointerUp = (ev) => {
      dragging = false;
      this._canvas.releasePointerCapture(ev.pointerId);
    };
    const onWheel = (ev) => {
      ev.preventDefault();
      this._radius *= Math.exp(ev.deltaY * 0.001);
      this._radius = Math.max(5, Math.min(5000, this._radius));
      this._updateCamera();
      this._render();
    };

    this._canvas.addEventListener("pointerdown", onPointerDown);
    this._canvas.addEventListener("pointermove", onPointerMove);
    this._canvas.addEventListener("pointerup", onPointerUp);
    this._canvas.addEventListener("pointercancel", onPointerUp);
    this._canvas.addEventListener("wheel", onWheel, { passive: false });
  }

  _updateCamera() {
    const pos = new THREE.Vector3(
      this._radius * Math.cos(this._pitch) * Math.sin(this._yaw),
      this._radius * Math.sin(this._pitch),
      this._radius * Math.cos(this._pitch) * Math.cos(this._yaw)
    ).add(this._target);
    this._camera.position.copy(pos);
    this._camera.lookAt(this._target);
  }

  _resize() {
    if (!this._renderer) return;
    const w = this.shadowRoot.querySelector(".wrapper");
    const width = w ? w.clientWidth : 0;
    const height = w ? w.clientHeight : 0;
    if (!width || !height) return;
    this._renderer.setSize(width, height, false);
    this._camera.aspect = width / height;
    this._camera.updateProjectionMatrix();
    this._render();
  }

  _render() {
    if (this._renderer) {
      this._renderer.render(this._scene, this._camera);
    }
  }

  _fit() {
    if (!this._extSegs && !this._travelSegs) return;
    const box = new THREE.Box3();
    if (this._extSegs) box.expandByObject(this._extSegs);
    if (this._travelSegs) box.expandByObject(this._travelSegs);
    const sphere = box.getBoundingSphere(new THREE.Sphere());
    if (!isFinite(sphere.radius) || sphere.radius <= 0) return;
    this._target.copy(sphere.center);
    this._radius = sphere.radius / Math.tan((this._camera.fov * Math.PI) / 360);
    this._radius *= 1.35;
    this._updateCamera();
    this._render();
  }

  _setStatus(msg, isError = false) {
    if (this._els) {
      this._els.msg.textContent = msg;
      this._els.err.classList.toggle("show", isError);
      if (isError) this._els.errMsg.textContent = msg;
    }
  }

  _setProgress(pct, pctText) {
    if (!this._els) return;
    this._els.bar.classList.toggle("show", pct >= 0 && pct < 1);
    this._els.fill.style.width = `${Math.round((pct || 0) * 100)}%`;
    this._els.pct.textContent = pctText || "";
  }

  async _load() {
    if (this._loading) return;
    if (!this._hass || !this._hass.auth) {
      this._setStatus("Waiting for Home Assistant…");
      return;
    }
    this._loading = true;
    this._els.err.classList.remove("show");
    this._setStatus("Fetching G-code…");
    this._setProgress(0, "0%");
    try {
      const resp = await fetch("/api/octoprint_gcode_proxy/current", {
        headers: {
          Authorization: `Bearer ${this._hass.auth.accessToken}`,
        },
      });
      if (resp.status === 401 || resp.status === 403) {
        throw new Error("Authentication failed — token rejected.");
      }
      if (!resp.ok) {
        let detail = "";
        try {
          detail = (await resp.json()).error || "";
        } catch {
          /* ignore */
        }
        const map = {
          no_active_file: "No active print file on the printer.",
          octoprint_unreachable: "OctoPrint is unreachable.",
          octoprint_auth_failed: "OctoPrint rejected the API key.",
          missing_secret: "Missing octoprint_api_key secret.",
        };
        throw new Error(map[detail] || `Request failed (HTTP ${resp.status}).`);
      }
      const filename = resp.headers.get("X-OctoPrint-File");
      const contentLength = Number(resp.headers.get("Content-Length")) || 0;
      const reader = resp.body.getReader();
      const decoder = new TextDecoder("utf-8");
      let text = "";
      let received = 0;
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        received += value.length;
        text += decoder.decode(value, { stream: true });
        if (contentLength) {
          this._setProgress(received / contentLength, `${Math.round((received / contentLength) * 100)}%`);
        }
      }
      text += decoder.decode();
      this._setProgress(0, "");
      await this._parse(text, filename);
      this._loadedOnce = true;
    } catch (err) {
      this._setProgress(0, "");
      this._setStatus(err.message, true);
    } finally {
      this._loading = false;
    }
  }

  async _parse(text, filename) {
    this._setStatus(`Parsing ${filename || "G-code"}…`);
    this._setProgress(0, "0%");
    const lines = text.split("\n");
    const extPos = [];
    const travPos = [];

    let cx = 0, cy = 0, cz = 0, ce = 0;
    let cur = new THREE.Vector3(0, 0, 0);

    const push = (list, a, b) => {
      list.push(a.x, a.y, a.z, b.x, b.y, b.z);
    };

    const slice = 30000;
    for (let start = 0; start < lines.length; start += slice) {
      const end = Math.min(start + slice, lines.length);
      for (let i = start; i < end; i++) {
        const line = lines[i];
        const semi = line.indexOf(";");
        const code = semi >= 0 ? line.slice(0, semi) : line;
        const m = code.match(/^G[01]\s/i);
        if (!m) continue;

        let hasX = false, hasY = false, hasZ = false, hasE = false, nx = cx, ny = cy, nz = cz, ne = ce;
        const params = code.slice(2);
        let match;
        const re = /([XYZEF])(-?[\d.]+)/gi;
        while ((match = re.exec(params))) {
          const v = parseFloat(match[2]);
          switch (match[1].toUpperCase()) {
            case "X": nx = v; hasX = true; break;
            case "Y": ny = v; hasY = true; break;
            case "Z": nz = v; hasZ = true; break;
            case "E": ne = v; hasE = true; break;
          }
        }

        if (!hasX && !hasY && !hasZ && !hasE) continue;

        const prev = cur;
        cur = new THREE.Vector3(nx, ny, nz);

        const isG0 = /^G0\b/i.test(code.trim());
        const extruding = !isG0 && hasE && ne - ce > 0.0001;
        const list = extruding ? extPos : travPos;
        push(list, prev, cur);

        cx = nx; cy = ny; cz = nz; ce = ne;
      }
      this._setProgress(start / lines.length, `${Math.round((start / lines.length) * 100)}%`);
      await new Promise((r) => requestAnimationFrame(r));
    }

    const mk = (positions, color) => {
      if (!positions.length) return null;
      const geo = new THREE.BufferGeometry();
      geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
      return new THREE.LineSegments(
        geo,
        new THREE.LineBasicMaterial({ color: new THREE.Color(color), vertexColors: false })
      );
    };

    if (this._extSegs) { this._scene.remove(this._extSegs); this._extSegs.geometry.dispose(); }
    if (this._travelSegs) { this._scene.remove(this._travelSegs); this._travelSegs.geometry.dispose(); }

    const extColor = cssVar("--gcode-extrusion-color", EXT_COLOR);
    const travColor = cssVar("--gcode-travel-color", TRAVEL_COLOR);

    this._extSegs = mk(extPos, extColor);
    this._travelSegs = mk(travPos, travColor);
    if (this._extSegs) this._scene.add(this._extSegs);
    if (this._travelSegs) this._scene.add(this._travelSegs);

    if (!this._extSegs && !this._travelSegs) {
      this._setStatus("No G0/G1 moves found in the file.");
    } else {
      const segCount = extPos.length / 6 + travPos.length / 6;
      this._setStatus(
        `${filename || "G-code"} · ${lines.length.toLocaleString()} lines · ${segCount.toLocaleString()} segments`
      );
    }

    this._setProgress(0, "");
    this._fit();
  }
}

class GcodeViewerCardEditor extends HTMLElement {
  setConfig(config) {
    this._config = config;
  }
  get value() {
    return { ...this._config };
  }
  set hass(hass) {
    /* no editable options yet */
  }
  connectedCallback() {
    this.innerHTML = `
      <ha-formfield label="G-code viewer has no configuration options.">
      </ha-formfield>`;
  }
}

customElements.define("gcode-viewer-card", GcodeViewerCard);
customElements.define("gcode-viewer-card-editor", GcodeViewerCardEditor);
