// retoor <retoor@molodetz.nl>

import { assetUrl } from "../assetVersion.js";

const CSS_ID = "devii-avatar-css";
const DEFAULT_CHARACTER = "Clippy";
const CORNER_MARGIN = 16;
const FALLBACK_WIDTH = 124;
const FALLBACK_HEIGHT = 93;
const ACTION_SAFETY_MS = 18000;

export default class DeviiAvatarElement extends HTMLElement {
  constructor() {
    super();
    this._clippy = null;
    this._character = DEFAULT_CHARACTER;
    this._tts = false;
    this._assistant = null;
    this._positioned = false;
    this._shown = false;
    this._chain = null;
  }

  connectedCallback() {
    this._ensureCss();
    this._character = this.getAttribute("character") || DEFAULT_CHARACTER;
    this._tts = this.hasAttribute("tts");
    if (globalThis.app && typeof globalThis.app.register === "function") {
      globalThis.app.register(this);
    }
    if (this.hasAttribute("autoshow")) {
      this.execute("show", {}).catch(() => {});
    }
  }

  _ensureCss() {
    if (document.getElementById(CSS_ID)) return;
    const link = document.createElement("link");
    link.id = CSS_ID;
    link.rel = "stylesheet";
    link.href = assetUrl("/static/css/devii-avatar.css");
    document.head.appendChild(link);
  }

  async _app() {
    if (!this._clippy) {
      const module = await import(assetUrl("/static/vendor/md-clippy/index.js"));
      this._clippy = module.app;
    }
    return this._clippy;
  }

  async _ensureLoaded(app) {
    if (!app.agent || app.name !== this._character) {
      await app.load(this._character);
      this._positioned = false;
      this._shown = false;
    }
  }

  _placeDefault(app) {
    if (this._positioned) return;
    const element = app.agent && app.agent._el;
    if (!element) return;
    const width = element.offsetWidth || FALLBACK_WIDTH;
    const height = element.offsetHeight || FALLBACK_HEIGHT;
    element.style.left = `${Math.max(0, window.innerWidth - width - CORNER_MARGIN)}px`;
    element.style.top = `${Math.max(0, window.innerHeight - height - CORNER_MARGIN)}px`;
    this._positioned = true;
  }

  async _ensureVisible(app) {
    await this._ensureLoaded(app);
    this._placeDefault(app);
    if (!this._shown) {
      app.agent.show();
      this._shown = true;
    }
  }

  _afterQueue(app) {
    const agent = app.agent;
    if (!agent) return Promise.resolve();
    return new Promise((resolve) => {
      let settled = false;
      const finish = () => {
        if (!settled) {
          settled = true;
          resolve();
        }
      };
      try {
        agent._addToQueue(function (complete) {
          complete();
          finish();
        });
      } catch (error) {
        finish();
        return;
      }
      window.setTimeout(finish, ACTION_SAFETY_MS);
    });
  }

  execute(action, args = {}) {
    const run = () => this._run(action, args);
    const result = (this._chain || Promise.resolve()).then(run, run);
    this._chain = result.then(
      () => {},
      () => {},
    );
    return result;
  }

  async _run(action, args = {}) {
    const app = await this._app();
    switch (action) {
      case "show":
        await this._ensureLoaded(app);
        this._placeDefault(app);
        app.agent.show();
        this._shown = true;
        await this._afterQueue(app);
        return "devii is visible";
      case "hide":
        app.hide();
        this._shown = false;
        await this._afterQueue(app);
        return "devii is hidden";
      case "speak":
        await this._ensureVisible(app);
        app.speak(String(args.text), { hold: !!args.hold, tts: this._tts });
        await this._afterQueue(app);
        return "spoke";
      case "play_animation": {
        await this._ensureVisible(app);
        if (!app.agent.hasAnimation(args.name)) {
          return { error: `no animation named '${args.name}'` };
        }
        app.agent.play(args.name);
        await this._afterQueue(app);
        return `playing ${args.name}`;
      }
      case "random_animation":
        await this._ensureVisible(app);
        app.animate();
        await this._afterQueue(app);
        return "playing a random animation";
      case "move_to": {
        await this._ensureVisible(app);
        const mx = Number(args.x);
        const my = Number(args.y);
        if (!Number.isFinite(mx) || !Number.isFinite(my)) {
          return { error: "move_to needs numeric x and y" };
        }
        const requested = args.duration != null ? Number(args.duration) : undefined;
        const duration = document.hidden ? 0 : requested;
        app.moveTo(mx, my, duration);
        await this._afterQueue(app);
        return "moving";
      }
      case "gesture_at":
        await this._ensureVisible(app);
        app.agent.gestureAt(Number(args.x), Number(args.y));
        await this._afterQueue(app);
        return "gesturing";
      case "stop":
        app.stop();
        return "stopped";
      case "switch_character":
        this._character = args.name;
        await app.load(args.name);
        this._positioned = false;
        this._shown = false;
        this._placeDefault(app);
        app.agent.show();
        this._shown = true;
        await this._afterQueue(app);
        return `now appearing as ${args.name}`;
      case "list_animations":
        await this._ensureVisible(app);
        return app.agent.animations();
      case "list_characters":
        return app.names();
      case "get_viewport": {
        const agent = app.agent;
        const rect = agent ? agent._el.getBoundingClientRect() : null;
        return {
          width: window.innerWidth,
          height: window.innerHeight,
          character: app.name,
          position: rect ? { x: Math.round(rect.left), y: Math.round(rect.top) } : null,
        };
      }
      default:
        return { error: `unknown avatar action '${action}'` };
    }
  }

  async ask(text) {
    const app = await this._app();
    if (!this._assistant) {
      this._assistant = await app.connectAI({ tts: this._tts });
    }
    return app.ask(text);
  }
}

customElements.define("devii-avatar", DeviiAvatarElement);
