(function (global) {
  "use strict";

  const VERSION = "1.0.0";
  const DEFAULT_MESSAGES = ["欢迎回来", "今天也要摸鱼吗", "我已经收藏了"];
  const DEFAULTS = {
    sprite: "",
    rows: 5,
    cols: 14,
    height: 180,
    minPeople: 4,
    maxPeople: 18,
    people: null,
    mobileMaxPeople: 6,
    speedMin: 16,
    speedMax: 36,
    speedMultiplier: 1,
    bubbleChance: 0.18,
    visibleSourceRatio: 0.55,
    flipSprite: true,
    labels: [],
    labelChance: 0,
    messages: DEFAULT_MESSAGES,
    respectReducedMotion: true,
  };

  function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  function random(min, max) {
    return min + Math.random() * (max - min);
  }

  function choice(list) {
    return list[Math.floor(Math.random() * list.length)];
  }

  function normalizeMessages(messages) {
    const source = Array.isArray(messages) && messages.length ? messages : DEFAULT_MESSAGES;
    return source
      .map((item) => {
        if (typeof item === "string") return item.trim() ? { text: item.trim(), url: "" } : null;
        if (item && typeof item === "object" && typeof item.text === "string") {
          return { text: item.text.trim(), url: typeof item.url === "string" ? item.url : "" };
        }
        return null;
      })
      .filter(Boolean);
  }

  function normalizeLabels(labels) {
    const source =
      typeof labels === "string"
        ? labels.split(/[,，、|]/)
        : Array.isArray(labels)
          ? labels
          : [];
    return source
      .map((item) => {
        if (typeof item === "string") return item.trim();
        if (item && typeof item === "object" && typeof item.text === "string") return item.text.trim();
        return "";
      })
      .filter(Boolean)
      .slice(0, 50);
  }

  function resolveTarget(target) {
    if (typeof target === "string") return document.querySelector(target);
    if (target && target.nodeType === 1) return target;
    return null;
  }

  function roundedRect(ctx, x, y, width, height, radius) {
    const r = Math.min(radius, width / 2, height / 2);
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + width - r, y);
    ctx.quadraticCurveTo(x + width, y, x + width, y + r);
    ctx.lineTo(x + width, y + height - r);
    ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
    ctx.lineTo(x + r, y + height);
    ctx.quadraticCurveTo(x, y + height, x, y + height - r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.closePath();
  }

  class LittlePeopleWidget {
    constructor(target, options) {
      this.host = resolveTarget(target);
      if (!this.host) {
        throw new Error("LittlePeople.init target was not found.");
      }

      this.options = Object.assign({}, DEFAULTS, options || {});
      this.options.rows = Math.max(1, parseInt(this.options.rows, 10) || DEFAULTS.rows);
      this.options.cols = Math.max(1, parseInt(this.options.cols, 10) || DEFAULTS.cols);
      this.options.height = Math.max(80, parseInt(this.options.height, 10) || DEFAULTS.height);
      this.options.minPeople = Math.max(0, parseInt(this.options.minPeople, 10) || DEFAULTS.minPeople);
      this.options.maxPeople = Math.max(this.options.minPeople, parseInt(this.options.maxPeople, 10) || DEFAULTS.maxPeople);
      this.options.mobileMaxPeople = Math.max(0, parseInt(this.options.mobileMaxPeople, 10) || DEFAULTS.mobileMaxPeople);
      this.options.bubbleChance = clamp(Number(this.options.bubbleChance), 0, 1);
      this.options.visibleSourceRatio = clamp(Number(this.options.visibleSourceRatio) || DEFAULTS.visibleSourceRatio, 0.45, 1);
      const speedMultiplier = Number(this.options.speedMultiplier);
      this.speedMultiplier = clamp(
        Number.isFinite(speedMultiplier) ? speedMultiplier : DEFAULTS.speedMultiplier,
        0.2,
        3
      );
      this.options.speedMultiplier = this.speedMultiplier;
      this.options.flipSprite = this.options.flipSprite !== false && this.options.flipSprite !== "false";
      const labelChance = Number(this.options.labelChance);
      this.options.labelChance = clamp(
        Number.isFinite(labelChance) ? labelChance : DEFAULTS.labelChance,
        0,
        1
      );
      this.messages = normalizeMessages(this.options.messages);
      this.labels = normalizeLabels(this.options.labels);

      this.canvas = document.createElement("canvas");
      this.canvas.className = "little-people-canvas";
      this.canvas.setAttribute("aria-hidden", "true");
      this.canvas.setAttribute("role", "presentation");
      this.ctx = this.canvas.getContext("2d");

      this.image = null;
      this.cells = [];
      this.people = [];
      this.hovered = null;
      this.animationFrame = 0;
      this.lastTime = 0;
      this.destroyed = false;
      this.ready = null;
      this.width = 0;
      this.height = 0;
      this.dpr = 1;
      this.previousInlineHeight = this.host.style.getPropertyValue("--little-people-height");
      this.isReducedMotion =
        this.options.respectReducedMotion &&
        typeof global.matchMedia === "function" &&
        global.matchMedia("(prefers-reduced-motion: reduce)").matches;

      this.onPointerMove = this.handlePointerMove.bind(this);
      this.onPointerLeave = this.handlePointerLeave.bind(this);
      this.onClick = this.handleClick.bind(this);
      this.onResize = this.handleResize.bind(this);

      this.mount();
      this.ready = this.loadSprite()
        .then(() => this.start())
        .catch((error) => {
          if (!this.destroyed) {
            console.warn("LittlePeople: sprite failed to load.", error);
          }
        });
    }

    mount() {
      this.host.classList.add("little-people-host");
      this.host.style.setProperty("--little-people-height", `${this.options.height}px`);
      this.host.appendChild(this.canvas);
      this.host.addEventListener("pointermove", this.onPointerMove);
      this.host.addEventListener("pointerleave", this.onPointerLeave);
      this.host.addEventListener("click", this.onClick);

      if ("ResizeObserver" in global) {
        this.resizeObserver = new ResizeObserver(this.onResize);
        this.resizeObserver.observe(this.host);
      } else {
        global.addEventListener("resize", this.onResize);
      }
    }

    loadSprite() {
      if (!this.options.sprite) {
        return Promise.reject(new Error("Missing sprite option."));
      }

      return new Promise((resolve, reject) => {
        const img = new Image();
        const spriteUrl = String(this.options.sprite);
        if (/^https?:\/\//i.test(spriteUrl) && !spriteUrl.startsWith(global.location.origin)) {
          img.crossOrigin = "anonymous";
        }
        img.onload = () => {
          this.image = img;
          resolve();
        };
        img.onerror = () => reject(new Error(`Unable to load ${spriteUrl}`));
        img.src = spriteUrl;
      });
    }

    start() {
      if (this.destroyed || !this.image) return;
      this.createCells();
      this.resizeCanvas();
      const initialPeople =
        this.requestedPeople == null
          ? this.options.people == null
            ? this.options.maxPeople
            : this.options.people
          : this.requestedPeople;
      this.setPeopleCount(initialPeople);
      this.host.dataset.littlePeopleReady = "true";

      if (this.isReducedMotion) {
        this.render(0);
        return;
      }

      this.lastTime = performance.now();
      this.animationFrame = global.requestAnimationFrame((time) => this.tick(time));
    }

    createCells() {
      const cellW = this.image.naturalWidth / this.options.cols;
      const cellH = this.image.naturalHeight / this.options.rows;
      this.cells = [];
      for (let row = 0; row < this.options.rows; row += 1) {
        for (let col = 0; col < this.options.cols; col += 1) {
          this.cells.push({ sx: col * cellW, sy: row * cellH, sw: cellW, sh: cellH });
        }
      }
    }

    currentMaxPeople() {
      const width = this.host.clientWidth || global.innerWidth || 0;
      const cap = width <= 640 ? this.options.mobileMaxPeople : this.options.maxPeople;
      return Math.max(0, cap);
    }

    targetPeopleCount(count) {
      const max = this.currentMaxPeople();
      const min = Math.min(this.options.minPeople, max);
      return clamp(Math.round(Number(count) || 0), min, max);
    }

    setPeopleCount(count) {
      this.requestedPeople = this.targetPeopleCount(count);
      while (this.people.length > this.requestedPeople) this.people.pop();
      while (this.people.length < this.requestedPeople && this.cells.length) {
        this.people.push(this.createPerson(this.people.length, true));
      }
      this.render(performance.now());
      return this;
    }

    setDensity(count) {
      return this.setPeopleCount(count);
    }

    setSpeed(multiplier) {
      const nextMultiplier = Number(multiplier);
      this.speedMultiplier = clamp(
        Number.isFinite(nextMultiplier) ? nextMultiplier : DEFAULTS.speedMultiplier,
        0.2,
        3
      );
      this.options.speedMultiplier = this.speedMultiplier;
      for (const person of this.people) {
        if (!person.baseSpeed) person.baseSpeed = person.speed || this.options.speedMin;
        person.speed = person.baseSpeed * this.speedMultiplier;
      }
      this.render(performance.now());
      return this;
    }

    setMessages(messages) {
      this.messages = normalizeMessages(messages);
      return this;
    }

    setLabels(labels) {
      this.labels = normalizeLabels(labels);
      for (const person of this.people) {
        person.label = this.pickLabel();
      }
      this.render(performance.now());
      return this;
    }

    createPerson(index, scatter) {
      const cell = choice(this.cells);
      const direction = Math.random() > 0.5 ? 1 : -1;
      const visibleHeight = random(this.height * 0.72, this.height * 1.02);
      const fullHeight = visibleHeight / this.options.visibleSourceRatio;
      const scale = fullHeight / cell.sh;
      const displayWidth = cell.sw * scale;
      const laneDepth = random(0, this.height * 0.1);
      const baseY = this.height - visibleHeight - laneDepth;
      const startX = direction > 0 ? -displayWidth - random(5, 80) : this.width + random(5, 80);
      const endX = direction > 0 ? this.width + displayWidth + 40 : -displayWidth - 40;
      const travelX = scatter ? random(-displayWidth, this.width + displayWidth) : startX;
      const message = this.pickBubble();
      const label = this.pickLabel();
      const baseSpeed = random(this.options.speedMin, this.options.speedMax);

      return {
        cell,
        x: scatter ? travelX : startX,
        y: Math.max(0, baseY),
        startX,
        endX,
        width: displayWidth,
        height: fullHeight,
        visibleHeight,
        direction,
        baseSpeed,
        speed: baseSpeed * this.speedMultiplier,
        phase: random(0, Math.PI * 2),
        bob: random(0.8, 2.2),
        message: message ? message.text : "",
        label,
        url: message ? message.url : "",
        bubbleUntil: message ? performance.now() + random(7000, 16000) : 0,
        paused: false,
        anchorY: Math.max(0, baseY) + visibleHeight,
        index,
      };
    }

    pickBubble() {
      if (!this.messages.length || Math.random() > this.options.bubbleChance) return null;
      const activeBubbles = this.people.filter((person) => person.message).length;
      if (activeBubbles >= 6) return null;
      return choice(this.messages);
    }

    pickLabel() {
      if (!this.labels.length || Math.random() > this.options.labelChance) return "";
      return choice(this.labels);
    }

    resizeCanvas() {
      const rect = this.host.getBoundingClientRect();
      const nextWidth = Math.max(1, Math.round(rect.width || this.host.clientWidth || 1));
      const nextHeight = Math.max(1, Math.round(rect.height || this.options.height));
      const nextDpr = Math.max(1, Math.min(2, global.devicePixelRatio || 1));
      const changed = nextWidth !== this.width || nextHeight !== this.height || nextDpr !== this.dpr;

      if (!changed) return false;

      this.width = nextWidth;
      this.height = nextHeight;
      this.dpr = nextDpr;
      this.canvas.width = Math.round(this.width * this.dpr);
      this.canvas.height = Math.round(this.height * this.dpr);
      this.canvas.style.width = `${this.width}px`;
      this.canvas.style.height = `${this.height}px`;

      for (const person of this.people) {
        person.y = clamp(person.y, 0, Math.max(0, this.height - person.visibleHeight));
        person.anchorY = person.y + person.visibleHeight;
      }

      const cappedCount = this.targetPeopleCount(this.requestedPeople || this.options.maxPeople);
      if (cappedCount !== this.people.length) {
        this.setPeopleCount(cappedCount);
      }
      return true;
    }

    handleResize() {
      if (this.destroyed) return;
      this.resizeCanvas();
      this.render(performance.now());
    }

    tick(time) {
      if (this.destroyed) return;
      const dt = Math.min(0.05, Math.max(0, (time - this.lastTime) / 1000));
      this.lastTime = time;
      this.resizeCanvas();
      this.update(dt, time);
      this.render(time);
      this.animationFrame = global.requestAnimationFrame((nextTime) => this.tick(nextTime));
    }

    update(dt, time) {
      for (let i = 0; i < this.people.length; i += 1) {
        const person = this.people[i];
        if (!person.paused) {
          person.x += person.direction * person.speed * dt;
        }
        if (person.message && person.bubbleUntil < time) {
          person.message = "";
          person.url = "";
          person.bubbleUntil = 0;
        }

        const exitedRight = person.direction > 0 && person.x > person.endX;
        const exitedLeft = person.direction < 0 && person.x < person.endX;
        if (exitedRight || exitedLeft) {
          this.people[i] = this.createPerson(i, false);
        }
      }
    }

    render(time) {
      if (!this.ctx || !this.image || !this.width || !this.height) return;
      const ctx = this.ctx;
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";

      const sorted = this.people.slice().sort((a, b) => a.anchorY - b.anchorY);
      for (const person of sorted) {
        if (person !== this.hovered) this.drawPerson(ctx, person, time);
      }
      if (this.hovered) this.drawPerson(ctx, this.hovered, time);
    }

    drawPerson(ctx, person, time) {
      const bob = this.isReducedMotion ? 0 : Math.sin(time / 220 + person.phase) * person.bob;
      const drawY = person.y + bob;
      const flip = this.options.flipSprite && person.direction < 0;

      ctx.save();
      if (flip) {
        ctx.translate(person.x + person.width, drawY);
        ctx.scale(-1, 1);
        ctx.drawImage(
          this.image,
          person.cell.sx,
          person.cell.sy,
          person.cell.sw,
          person.cell.sh,
          0,
          0,
          person.width,
          person.height
        );
      } else {
        ctx.drawImage(
          this.image,
          person.cell.sx,
          person.cell.sy,
          person.cell.sw,
          person.cell.sh,
          person.x,
          drawY,
          person.width,
          person.height
        );
      }
      ctx.restore();

      if (person.label) this.drawLabel(ctx, person, drawY);
      if (person.message) this.drawBubble(ctx, person, drawY);
    }

    drawLabel(ctx, person, drawY) {
      const label = String(person.label || "").trim();
      if (!label) return;

      const scale = clamp(person.visibleHeight / 170, 0.78, 1.2);
      const accentColors = ["#367edc", "#20a669", "#e64841", "#f2be1e", "#805ce8"];
      const labelIndex = Math.max(0, this.labels.indexOf(label));
      const maxWidth = Math.min(76 * scale, Math.max(36 * scale, person.width * 0.56));
      const minWidth = Math.min(maxWidth, 34 * scale);
      const fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif';
      const characters = Array.from(label);
      const text = characters.length > 6 ? characters.slice(0, 6).join("") : label;
      let fontSize = Math.round(12 * scale);

      ctx.save();
      ctx.textBaseline = "middle";
      ctx.textAlign = "center";
      ctx.font = `700 ${fontSize}px ${fontFamily}`;

      while (fontSize > 8 && ctx.measureText(text).width > maxWidth - 16 * scale) {
        fontSize -= 1;
        ctx.font = `700 ${fontSize}px ${fontFamily}`;
      }

      const textWidth = ctx.measureText(text).width;
      const badgeWidth = clamp(textWidth + 18 * scale, minWidth, maxWidth);
      const badgeHeight = 18 * scale;
      const minX = person.x + person.width * 0.2;
      const maxX = person.x + person.width * 0.8 - badgeWidth;
      let x = person.x + person.width * 0.5 - badgeWidth / 2;
      if (maxX >= minX) {
        x = clamp(x, minX, maxX);
      }
      x = clamp(x, -badgeWidth + 5, this.width - 5);

      const minY = drawY + person.visibleHeight * 0.48;
      const maxY = Math.min(this.height - badgeHeight - 2, drawY + person.visibleHeight - badgeHeight - 4);
      let y = drawY + person.visibleHeight * 0.63;
      if (maxY >= minY) {
        y = clamp(y, minY, maxY);
      }

      roundedRect(ctx, x, y, badgeWidth, badgeHeight, 3 * scale);
      ctx.fillStyle = "rgba(250, 253, 255, 0.96)";
      ctx.fill();
      ctx.strokeStyle = "rgba(20, 23, 28, 0.78)";
      ctx.lineWidth = Math.max(1, 1.2 * scale);
      ctx.stroke();

      roundedRect(ctx, x, y, Math.min(5 * scale, badgeWidth), badgeHeight, 3 * scale);
      ctx.fillStyle = accentColors[labelIndex % accentColors.length];
      ctx.fill();

      ctx.fillStyle = "#14171c";
      ctx.fillText(text, x + badgeWidth / 2 + 2 * scale, y + badgeHeight / 2 + 0.2 * scale);
      ctx.restore();
    }

    drawBubble(ctx, person, drawY) {
      const paddingX = 9;
      const paddingY = 7;
      const lineHeight = 17;
      const maxWidth = 230;
      const font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
      ctx.font = font;
      const lines = this.wrapText(ctx, person.message, maxWidth);
      const textWidth = Math.max(...lines.map((line) => ctx.measureText(line).width), 1);
      const bubbleWidth = Math.min(maxWidth + paddingX * 2, textWidth + paddingX * 2);
      const bubbleHeight = lines.length * lineHeight + paddingY * 2;
      const anchorX = person.x + person.width / 2;
      let x = anchorX - bubbleWidth / 2;
      let y = drawY - bubbleHeight - 18;
      x = clamp(x, 6, Math.max(6, this.width - bubbleWidth - 6));
      y = Math.max(6, y);

      ctx.save();
      roundedRect(ctx, x, y, bubbleWidth, bubbleHeight, 7);
      const tailX = clamp(anchorX, x + 14, x + bubbleWidth - 14);
      ctx.moveTo(tailX - 7, y + bubbleHeight);
      ctx.lineTo(anchorX, y + bubbleHeight + 10);
      ctx.lineTo(tailX + 7, y + bubbleHeight);
      ctx.closePath();
      ctx.fillStyle = "rgba(255, 255, 255, 0.95)";
      ctx.fill();
      ctx.strokeStyle = "rgba(22, 24, 27, 0.14)";
      ctx.lineWidth = 1.25;
      ctx.stroke();
      ctx.fillStyle = "#24272b";
      ctx.font = font;
      ctx.textBaseline = "top";
      for (let i = 0; i < lines.length; i += 1) {
        const line = lines[i];
        const lineX = x + (bubbleWidth - ctx.measureText(line).width) / 2;
        ctx.fillText(line, lineX, y + paddingY + i * lineHeight);
      }
      ctx.restore();
    }

    wrapText(ctx, text, maxWidth) {
      const tokens = String(text).match(/[\u4e00-\u9fa5]|[^\s\u4e00-\u9fa5]+|\s+/g) || [String(text)];
      const lines = [];
      let current = "";
      for (const token of tokens) {
        const next = current + token;
        if (current && ctx.measureText(next).width > maxWidth) {
          lines.push(current.trim());
          current = token.trimStart();
        } else {
          current = next;
        }
      }
      if (current.trim()) lines.push(current.trim());
      return lines.slice(0, 3);
    }

    personAt(x, y) {
      for (let i = this.people.length - 1; i >= 0; i -= 1) {
        const person = this.people[i];
        if (!person.url) continue;
        if (x >= person.x && x <= person.x + person.width && y >= person.y && y <= person.y + person.height) {
          return person;
        }
      }
      return null;
    }

    pointerPosition(event) {
      const rect = this.canvas.getBoundingClientRect();
      return {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top,
      };
    }

    handlePointerMove(event) {
      if (this.destroyed) return;
      const point = this.pointerPosition(event);
      const next = this.personAt(point.x, point.y);
      if (next === this.hovered) return;
      if (this.hovered) this.hovered.paused = false;
      this.hovered = next;
      if (this.hovered) this.hovered.paused = true;
      this.host.style.cursor = this.hovered ? "pointer" : "";
      this.render(performance.now());
    }

    handlePointerLeave() {
      if (this.hovered) this.hovered.paused = false;
      this.hovered = null;
      this.host.style.cursor = "";
      this.render(performance.now());
    }

    handleClick(event) {
      if (!this.hovered || !this.hovered.url) return;
      event.preventDefault();
      global.open(this.hovered.url, "_blank", "noopener");
    }

    destroy() {
      this.destroyed = true;
      if (this.animationFrame) global.cancelAnimationFrame(this.animationFrame);
      if (this.resizeObserver) {
        this.resizeObserver.disconnect();
      } else {
        global.removeEventListener("resize", this.onResize);
      }
      this.host.removeEventListener("pointermove", this.onPointerMove);
      this.host.removeEventListener("pointerleave", this.onPointerLeave);
      this.host.removeEventListener("click", this.onClick);
      this.host.classList.remove("little-people-host");
      delete this.host.dataset.littlePeopleReady;
      this.host.style.cursor = "";
      if (this.previousInlineHeight) {
        this.host.style.setProperty("--little-people-height", this.previousInlineHeight);
      } else {
        this.host.style.removeProperty("--little-people-height");
      }
      if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);
      this.people = [];
      this.hovered = null;
    }
  }

  global.LittlePeople = {
    version: VERSION,
    init(target, options) {
      return new LittlePeopleWidget(target, options);
    },
  };
})(window);
