#!/usr/bin/env python3
import argparse
import json
import math
import subprocess
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

MQTT_BROKER = "119.45.167.177"
MQTT_PORT = 1883
MQTT_TOPIC = "app2dev/CN040500000000"
MQTT_USERNAME = "admin"
MQTT_PASSWORD = "admin"

HTML_PAGE = """<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>PTZ 轮盘测试</title>
  <style>
    :root { color-scheme: dark; }
    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
      background: #0f172a;
      color: #e2e8f0;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }
    .wrap {
      width: min(900px, 95vw);
      background: #111827;
      border: 1px solid #334155;
      border-radius: 14px;
      padding: 18px;
      box-shadow: 0 20px 50px rgba(0, 0, 0, .35);
    }
    .title {
      font-size: 20px;
      font-weight: 700;
      margin-bottom: 8px;
    }
    .tip {
      color: #94a3b8;
      margin-bottom: 14px;
    }
    .row {
      display: grid;
      grid-template-columns: 360px 1fr;
      gap: 16px;
    }
    @media (max-width: 800px) {
      .row { grid-template-columns: 1fr; }
    }
    .panel {
      background: #0b1220;
      border: 1px solid #253146;
      border-radius: 12px;
      padding: 12px;
    }
    #pad {
      width: 100%;
      max-width: 360px;
      aspect-ratio: 1 / 1;
      touch-action: none;
      display: block;
      margin: 0 auto;
      background: radial-gradient(circle at 50% 50%, #1e293b, #0b1220);
      border-radius: 50%;
      border: 1px solid #334155;
      cursor: grab;
    }
    #pad.dragging { cursor: grabbing; }
    .kv {
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      font-size: 14px;
      line-height: 1.8;
      white-space: pre-wrap;
      background: #020617;
      border: 1px solid #1e293b;
      border-radius: 10px;
      padding: 10px;
      overflow-wrap: anywhere;
    }
    .btns {
      margin-top: 10px;
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }
    button {
      border: 1px solid #334155;
      background: #1d4ed8;
      color: #fff;
      border-radius: 8px;
      padding: 8px 12px;
      cursor: pointer;
    }
    button.secondary { background: #334155; }
    .small {
      color: #94a3b8;
      font-size: 13px;
      margin-top: 8px;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="title">PTZ 轮盘测试页（message_type=7）</div>
    <div class="tip">拖动左侧轮盘测试。x/y/r 全部按字符串输出，松开自动回到 "0"。</div>
    <div class="row">
      <div class="panel">
        <canvas id="pad" width="360" height="360"></canvas>
        <div class="small">规则：x/y ∈ [-1,1]，r ∈ [0,1]，r=0 表示停住。</div>
      </div>
      <div class="panel">
        <div id="values" class="kv"></div>
        <div class="btns">
          <button id="copyBtn">复制 JSON</button>
          <button id="sendBtn" class="secondary">发到本地 /api/joystick</button>
          <button id="zeroBtn" class="secondary">发送全0</button>
        </div>
        <div class="small" id="sendStatus">状态：未发送</div>
      </div>
    </div>
  </div>

  <script>
    const canvas = document.getElementById("pad");
    const ctx = canvas.getContext("2d");
    const valuesEl = document.getElementById("values");
    const sendStatusEl = document.getElementById("sendStatus");
    const copyBtn = document.getElementById("copyBtn");
    const sendBtn = document.getElementById("sendBtn");
    const zeroBtn = document.getElementById("zeroBtn");

    const cx = canvas.width / 2;
    const cy = canvas.height / 2;
    const maxR = 140;
    const knobR = 26;

    let dragging = false;
    let px = 0; // pixel offset
    let py = 0;
    let lastSend = 0;
    const sendInterval = 80; // ms

    function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
    function round3(v) { return (Math.round(v * 1000) / 1000).toFixed(3); }

    function getNorm() {
      const nx = clamp(px / maxR, -1, 1);
      const nyCanvas = clamp(py / maxR, -1, 1);
      const ny = -nyCanvas; // 上为正，下为负
      const nr = clamp(Math.sqrt(nx * nx + ny * ny), 0, 1);
      return { x: nx, y: ny, r: nr };
    }

    function buildPayload(n) {
      return {
        head: { message_type: 7 },
        body: {
          joystick_ctrl: {
            x: String(round3(n.x)),
            y: String(round3(n.y)),
            r: String(round3(n.r))
          }
        }
      };
    }

    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      ctx.save();
      ctx.strokeStyle = "#334155";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(cx, cy, maxR, 0, Math.PI * 2);
      ctx.stroke();

      ctx.strokeStyle = "#1e293b";
      for (let i = 1; i <= 3; i++) {
        ctx.beginPath();
        ctx.arc(cx, cy, (maxR / 3) * i, 0, Math.PI * 2);
        ctx.stroke();
      }

      ctx.beginPath(); ctx.moveTo(cx - maxR, cy); ctx.lineTo(cx + maxR, cy); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(cx, cy - maxR); ctx.lineTo(cx, cy + maxR); ctx.stroke();
      ctx.restore();

      const kx = cx + px;
      const ky = cy + py;

      ctx.save();
      const grad = ctx.createRadialGradient(kx - 7, ky - 9, 6, kx, ky, knobR + 6);
      grad.addColorStop(0, "#60a5fa");
      grad.addColorStop(1, "#1d4ed8");
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(kx, ky, knobR, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    function updateText() {
      const n = getNorm();
      const payload = buildPayload(n);
      valuesEl.textContent = JSON.stringify(payload, null, 2);
    }

    function setByPointer(clientX, clientY) {
      const rect = canvas.getBoundingClientRect();
      const x = clientX - rect.left;
      const y = clientY - rect.top;
      let dx = x - cx;
      let dy = y - cy;
      const dist = Math.sqrt(dx * dx + dy * dy);
      if (dist > maxR) {
        const ratio = maxR / dist;
        dx *= ratio;
        dy *= ratio;
      }
      px = dx;
      py = dy;
      draw();
      updateText();
    }

    function resetZero() {
      px = 0;
      py = 0;
      draw();
      updateText();
    }

    async function sendLocal() {
      const n = getNorm();
      const payload = buildPayload(n);
      try {
        const res = await fetch("/api/joystick", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload)
        });
        sendStatusEl.textContent = "状态：发送成功 " + new Date().toLocaleTimeString() + " (HTTP " + res.status + ")";
      } catch (e) {
        sendStatusEl.textContent = "状态：发送失败 " + e;
      }
    }

    function maybeAutoSend() {
      const now = Date.now();
      if (now - lastSend >= sendInterval) {
        lastSend = now;
        sendLocal();
      }
    }

    canvas.addEventListener("pointerdown", (e) => {
      dragging = true;
      canvas.classList.add("dragging");
      canvas.setPointerCapture(e.pointerId);
      setByPointer(e.clientX, e.clientY);
      maybeAutoSend();
    });
    canvas.addEventListener("pointermove", (e) => {
      if (!dragging) return;
      setByPointer(e.clientX, e.clientY);
      maybeAutoSend();
    });
    canvas.addEventListener("pointerup", () => {
      dragging = false;
      canvas.classList.remove("dragging");
      resetZero();
      sendLocal();
    });
    canvas.addEventListener("pointercancel", () => {
      dragging = false;
      canvas.classList.remove("dragging");
      resetZero();
      sendLocal();
    });

    copyBtn.addEventListener("click", async () => {
      const n = getNorm();
      const payload = buildPayload(n);
      await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
      sendStatusEl.textContent = "状态：已复制 JSON";
    });
    sendBtn.addEventListener("click", sendLocal);
    zeroBtn.addEventListener("click", () => {
      resetZero();
      sendLocal();
    });

    draw();
    updateText();
  </script>
</body>
</html>
"""


class Handler(BaseHTTPRequestHandler):
    mqtt_broker = MQTT_BROKER
    mqtt_port = MQTT_PORT
    mqtt_topic = MQTT_TOPIC
    mqtt_username = MQTT_USERNAME
    mqtt_password = MQTT_PASSWORD

    def _write_json(self, code, obj):
        data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self):
        if self.path in ("/", "/index.html"):
            data = HTML_PAGE.encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)
            return
        self._write_json(404, {"ok": False, "msg": "not found"})

    def do_POST(self):
        if self.path != "/api/joystick":
            self._write_json(404, {"ok": False, "msg": "not found"})
            return

        try:
            length = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            length = 0
        raw = self.rfile.read(length)

        try:
            payload = json.loads(raw.decode("utf-8"))
            ts = time.strftime("%H:%M:%S")
            payload_text = json.dumps(payload, ensure_ascii=False)
            print(f"[{ts}] recv: {payload_text}", flush=True)
        except Exception as e:
            self._write_json(400, {"ok": False, "msg": f"bad json: {e}"})
            return

        cmd = [
            "mosquitto_pub",
            "-h", self.mqtt_broker,
            "-p", str(self.mqtt_port),
            "-u", self.mqtt_username,
            "-P", self.mqtt_password,
            "-t", self.mqtt_topic,
            "-m", payload_text,
        ]
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, check=False)
            if result.returncode != 0:
                self._write_json(500, {
                    "ok": False,
                    "msg": "mqtt publish failed",
                    "stderr": (result.stderr or "").strip()
                })
                return
        except Exception as e:
            self._write_json(500, {"ok": False, "msg": f"mqtt publish exception: {e}"})
            return

        self._write_json(200, {"ok": True, "topic": self.mqtt_topic})

    def log_message(self, fmt, *args):
        return


def main():
    parser = argparse.ArgumentParser(description="PTZ joystick web tester")
    parser.add_argument("--host", default="0.0.0.0", help="listen host")
    parser.add_argument("--port", type=int, default=85, help="listen port, default 85")
    parser.add_argument("--mqtt-host", default=MQTT_BROKER, help="mqtt broker host")
    parser.add_argument("--mqtt-port", type=int, default=MQTT_PORT, help="mqtt broker port")
    parser.add_argument("--mqtt-topic", default=MQTT_TOPIC, help="mqtt publish topic")
    parser.add_argument("--mqtt-user", default=MQTT_USERNAME, help="mqtt username")
    parser.add_argument("--mqtt-pass", default=MQTT_PASSWORD, help="mqtt password")
    args = parser.parse_args()

    Handler.mqtt_broker = args.mqtt_host
    Handler.mqtt_port = args.mqtt_port
    Handler.mqtt_topic = args.mqtt_topic
    Handler.mqtt_username = args.mqtt_user
    Handler.mqtt_password = args.mqtt_pass

    server = None
    bind_port = args.port
    try:
        server = ThreadingHTTPServer((args.host, bind_port), Handler)
    except PermissionError:
        if bind_port < 1024:
            fallback_port = 8085
            print(
                f"Port {bind_port} permission denied, fallback to {fallback_port}.",
                flush=True
            )
            bind_port = fallback_port
            server = ThreadingHTTPServer((args.host, bind_port), Handler)
        else:
            raise

    print(f"PTZ test web running on http://{args.host}:{bind_port}", flush=True)
    print(
        f"MQTT target: {Handler.mqtt_broker}:{Handler.mqtt_port} topic={Handler.mqtt_topic} user={Handler.mqtt_username}",
        flush=True
    )
    print("Tip: use sudo if you must bind port <1024.", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


if __name__ == "__main__":
    main()

