'use strict';

const { getPetAsset, getAvailablePetTypes } = require('./assets/pets');

const DEFAULT_CONFIG = {
  enabled: true,
  petCount: 1,
  petTypes: ['cat'],
  maxToys: 1,
  toyCooldownMs: 20000,
  successToyChance: 0.35,
  chaseCursorChance: 0.04,
  walkSpeed: 0.45,
  animationSpeed: 0.5,
  sleepMinDurationMs: 30000,
  sleepMaxDurationMs: 180000,
  debugFrames: false,
  reducedMotion: false,
  onlyActivePane: true
};

const listenersByUid = new Map();
const sessionStateByUid = new Map();
let runtimeConfig = DEFAULT_CONFIG;

const ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g;
const ERROR_RE = /\b(command not found|not recognized|no such file|error|exception|failed|traceback)\b/i;
const PROMPT_RE = /(?:^|\n)[^\n]{0,80}(?:\$|%|>|❯|➜|λ)\s?$/;

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

function normalizeConfig(input) {
  const next = Object.assign({}, DEFAULT_CONFIG, input || {});
  const availablePetTypes = getAvailablePetTypes();
  next.petCount = clamp(Number(next.petCount) || 1, 1, 2);
  next.maxToys = clamp(Number(next.maxToys) || 1, 0, 3);
  next.toyCooldownMs = Math.max(Number(next.toyCooldownMs) || DEFAULT_CONFIG.toyCooldownMs, 3000);
  next.successToyChance = clamp(Number(next.successToyChance), 0, 1);
  next.chaseCursorChance = clamp(Number(next.chaseCursorChance), 0, 1);
  next.walkSpeed = Math.max(Number(next.walkSpeed) || DEFAULT_CONFIG.walkSpeed, 0.2);
  next.animationSpeed = Math.max(Number(next.animationSpeed) || DEFAULT_CONFIG.animationSpeed, 0.15);
  next.sleepMinDurationMs = Math.max(Number(next.sleepMinDurationMs) || DEFAULT_CONFIG.sleepMinDurationMs, 1000);
  next.sleepMaxDurationMs = Math.max(Number(next.sleepMaxDurationMs) || DEFAULT_CONFIG.sleepMaxDurationMs, next.sleepMinDurationMs);
  next.debugFrames = Boolean(next.debugFrames);
  next.petTypes = Array.isArray(next.petTypes) && next.petTypes.length ? next.petTypes : DEFAULT_CONFIG.petTypes;
  next.petTypes = next.petTypes.filter((type) => availablePetTypes.includes(type));
  if (next.petTypes.length === 0) {
    next.petTypes = DEFAULT_CONFIG.petTypes.slice();
  }
  return next;
}

function findHyperPetsConfig(value, depth) {
  if (!value || typeof value !== 'object' || depth > 4) {
    return null;
  }

  if (value.hyperPets && typeof value.hyperPets === 'object') {
    return value.hyperPets;
  }

  const keys = Object.keys(value);
  for (let index = 0; index < keys.length; index += 1) {
    const nested = value[keys[index]];
    if (nested && typeof nested === 'object') {
      const found = findHyperPetsConfig(nested, depth + 1);
      if (found) {
        return found;
      }
    }
  }

  return null;
}

function refreshRuntimeConfigFromRenderer() {
  try {
    if (typeof window === 'undefined') {
      return runtimeConfig;
    }

    const candidates = [
      window.__hyperpetsRuntimeConfig,
      window.config,
      window.config && window.config.getConfig ? window.config.getConfig() : null,
      window.store && window.store.getState ? window.store.getState() : null
    ];

    for (let index = 0; index < candidates.length; index += 1) {
      const found = findHyperPetsConfig(candidates[index], 0);
      if (found) {
        const normalized = normalizeConfig(found);
        runtimeConfig = normalized;
        window.__hyperpetsRuntimeConfig = normalized;
        return normalized;
      }
    }
  } catch (error) {
    console.warn('[hyperpets] failed to refresh renderer config', error);
  }

  return runtimeConfig;
}

function randomIdleClip() {
  const variants = ['idle0', 'idle1', 'idle2', 'idle3'];
  return variants[Math.floor(Math.random() * variants.length)];
}

function randomSleepDuration() {
  const min = runtimeConfig.sleepMinDurationMs;
  const max = runtimeConfig.sleepMaxDurationMs;
  if (max <= min) {
    return min;
  }
  return min + Math.round(Math.random() * (max - min));
}

function isIdleClipName(clipName) {
  return typeof clipName === 'string' && /^idle\d*$/.test(clipName);
}

function getDefaultClipName(petAsset) {
  if (petAsset.clips.idle0) {
    return 'idle0';
  }
  if (petAsset.clips.idle) {
    return 'idle';
  }
  return Object.keys(petAsset.clips)[0];
}

function getEffectiveClipFps(clip, reducedMotion) {
  const baseFps = clip.fps * runtimeConfig.animationSpeed;
  return reducedMotion ? Math.max(baseFps * 0.6, 1) : Math.max(baseFps, 1);
}

function modeToClip(pet) {
  const mode = pet.mode;
  const facingRight = pet.facing >= 0;
  const petAsset = getPetAsset(pet.type);
  if (mode === 'sleep') {
    return 'sleep';
  }
  if (mode === 'inspect') {
    return facingRight ? 'inspectRight' : 'inspectLeft';
  }
  if (mode === 'chase-cursor' || mode === 'chase-toy') {
    return facingRight ? 'walkRight' : 'walkLeft';
  }
  if (mode === 'walk') {
    return facingRight ? 'walkRight' : 'walkLeft';
  }
  if (pet.idleClip && petAsset.clips[pet.idleClip]) {
    return pet.idleClip;
  }
  if (isIdleClipName(pet.clip) && petAsset.clips[pet.clip]) {
    return pet.clip;
  }
  return getDefaultClipName(petAsset);
}

function getClipDuration(petType, clipName, reducedMotion) {
  const petAsset = getPetAsset(petType);
  const clip = petAsset.clips[clipName];
  if (!clip) {
    return 1000;
  }

  const fps = getEffectiveClipFps(clip, reducedMotion);
  return (clip.frames.length / fps) * 1000;
}

function resolveAnimationState(pet, desiredClip, now, reducedMotion) {
  const petAsset = getPetAsset(pet.type);
  const currentClip = pet.clip || desiredClip;
  const activeClip = petAsset.clips[currentClip] ? currentClip : desiredClip;
  const activeDef = petAsset.clips[activeClip];
  const elapsed = now - (pet.clipStartedAt || now);
  const activeDuration = getClipDuration(pet.type, activeClip, reducedMotion);

  if (activeDef && activeDef.nextClip && elapsed >= activeDuration) {
    const nextClip = pet.transitionTo || activeDef.nextClip;
    return {
      clip: nextClip,
      clipStartedAt: now,
      transitionTo: null
    };
  }

  if (activeClip === desiredClip) {
    return {
      clip: activeClip,
      clipStartedAt: pet.clipStartedAt || now,
      transitionTo: null
    };
  }

  const transitionClip = petAsset.transitions[`${activeClip}->${desiredClip}`] || null;
  if (transitionClip) {
    return {
      clip: transitionClip,
      clipStartedAt: now,
      transitionTo: desiredClip
    };
  }

  return {
    clip: desiredClip,
    clipStartedAt: now,
    transitionTo: null
  };
}

function getAnimatedFrame(pet, reducedMotion, now) {
  const petAsset = getPetAsset(pet.type);
  const defaultClipName = getDefaultClipName(petAsset);
  const clipName = pet.clip || defaultClipName;
  const clip = petAsset.clips[clipName] || petAsset.clips[defaultClipName];
  const fps = getEffectiveClipFps(clip, reducedMotion);
  const safeIndex = typeof pet.frameIndex === 'number' && clip.frames[pet.frameIndex]
    ? pet.frameIndex
    : 0;
  const frame = clip.frames[safeIndex] || petAsset.clips[defaultClipName].frames[0];
  return {
    clipName,
    fps,
    frame,
    frameIndex: safeIndex,
    frameCount: clip.frames.length
  };
}

function subscribe(uid, listener) {
  if (!uid) {
    return () => {};
  }

  if (!listenersByUid.has(uid)) {
    listenersByUid.set(uid, new Set());
  }

  const listeners = listenersByUid.get(uid);
  listeners.add(listener);

  return () => {
    listeners.delete(listener);
    if (listeners.size === 0) {
      listenersByUid.delete(uid);
    }
  };
}

function emit(uid, event) {
  const listeners = listenersByUid.get(uid);
  if (!listeners) {
    return;
  }

  listeners.forEach((listener) => {
    try {
      listener(event);
    } catch (error) {
      console.error('[hyperpets] listener error', error);
    }
  });
}

function stripAnsi(value) {
  return String(value || '').replace(ANSI_RE, '');
}

function inspectSessionData(uid, data) {
  const clean = stripAnsi(data);
  const state = sessionStateByUid.get(uid) || {
    tail: '',
    promptSeen: false,
    lastPrompt: '',
    sawError: false,
    lastToyAt: 0
  };

  state.tail = (state.tail + clean).slice(-500);
  if (ERROR_RE.test(clean)) {
    state.sawError = true;
    emit(uid, { type: 'error-output' });
  }

  const promptMatch = state.tail.match(PROMPT_RE);
  const promptText = promptMatch ? promptMatch[0] : '';
  if (promptText && promptText !== state.lastPrompt) {
    if (state.promptSeen && !state.sawError) {
      const now = Date.now();
      const ready = now - state.lastToyAt >= runtimeConfig.toyCooldownMs;
      if (ready && Math.random() <= runtimeConfig.successToyChance) {
        state.lastToyAt = now;
        emit(uid, { type: 'command-success' });
      }
    }

    state.promptSeen = true;
    state.lastPrompt = promptText;
    state.sawError = false;
  }

  sessionStateByUid.set(uid, state);
}

exports.decorateConfig = (config) => {
  runtimeConfig = normalizeConfig(config.hyperPets);
  try {
    if (typeof window !== 'undefined') {
      window.__hyperpetsRuntimeConfig = runtimeConfig;
    }
  } catch (error) {
    // Ignore window access errors during non-renderer evaluation.
  }
  console.log('[hyperpets] decorateConfig', {
    input: config.hyperPets || null,
    normalized: runtimeConfig
  });
  return Object.assign({}, config, {
    css: `
      ${config.css || ''}
      .hyperpets-layer {
        position: absolute;
        inset: 0;
        overflow: hidden;
        pointer-events: none;
        z-index: 12;
      }
      .hyperpets-floor {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 6px;
        height: 42px;
        opacity: 0.12;
        background: linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.15) 100%);
      }
      .hyperpets-pet {
        position: absolute;
        bottom: 12px;
        transform-origin: center bottom;
        will-change: transform, left;
      }
      .hyperpets-toy {
        position: absolute;
        bottom: 10px;
        width: 12px;
        height: 12px;
        border-radius: 999px;
        box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
      }
      .hyperpets-toy.ball {
        background: #d85f5f;
      }
      .hyperpets-toy.bone {
        width: 16px;
        height: 8px;
        border-radius: 999px;
        background: #dac7a2;
      }
      .hyperpets-pet.excited {
        filter: saturate(1.25);
      }
      .hyperpets-pet-svg {
        width: 48px;
        height: 32px;
        overflow: visible;
      }
      .hyperpets-frame {
        position: relative;
        overflow: hidden;
        transform-origin: center bottom;
      }
      .hyperpets-frame-debug {
        outline: 1px dashed rgba(80, 220, 255, 0.7);
        background-color: rgba(80, 220, 255, 0.06);
      }
      .hyperpets-frame svg {
        display: block;
        width: 100%;
        height: 100%;
      }
      .hyperpets-debug-label {
        position: absolute;
        left: -12px;
        bottom: 56px;
        min-width: 150px;
        padding: 4px 6px;
        border-radius: 6px;
        background: rgba(8, 12, 18, 0.86);
        color: #9ee7ff;
        font: 10px/1.35 Menlo, Monaco, monospace;
        white-space: pre;
        pointer-events: none;
      }
      .hyperpets-debug-hud {
        position: absolute;
        top: 8px;
        left: 8px;
        max-width: min(420px, calc(100% - 16px));
        padding: 6px 8px;
        border-radius: 8px;
        background: rgba(8, 12, 18, 0.9);
        color: #9ee7ff;
        font: 11px/1.4 Menlo, Monaco, monospace;
        white-space: pre-wrap;
        pointer-events: none;
        z-index: 20;
      }
      .hyperpets-z {
        position: absolute;
        bottom: 28px;
        font-size: 10px;
        opacity: 0.45;
        color: rgba(255,255,255,0.75);
      }
    `
  });
};

exports.middleware = (store) => (next) => (action) => {
  if (action && action.type === 'SESSION_ADD_DATA' && action.uid && action.data) {
    inspectSessionData(action.uid, action.data);
  }

  return next(action);
};

exports.decorateTerm = (Term, { React }) => {
  class HyperPetsLayer extends React.Component {
    constructor(props) {
      super(props);
      this.state = this.buildInitialState();
      this._mounted = false;
      this._unsubscribe = null;
      this._raf = null;
      this._lastFrameAt = 0;
      this._container = null;
      this._cursorFrame = null;
      this._mouseX = null;
      this._mouseInfluenceUntil = 0;
      this._resizeHandler = this.handleResize.bind(this);
      this._mouseHandler = this.handleMouseMove.bind(this);
      this._tick = this.tick.bind(this);
      this._debugFrameHistory = new Map();
      this._debugLastLoggedToken = new Map();
    }

    buildInitialState() {
      const config = runtimeConfig;
      const pets = [];
      for (let index = 0; index < config.petCount; index += 1) {
        const now = Date.now();
        const type = config.petTypes[index % config.petTypes.length] || 'cat';
        const petAsset = getPetAsset(type);
        const idleClip = petAsset.clips.idle0 ? randomIdleClip() : getDefaultClipName(petAsset);
        pets.push({
          id: `pet-${index}`,
          type,
          x: 24 + index * 48,
          vx: 0.1 + Math.random() * 0.14,
          facing: 1,
          mode: 'idle',
          mood: 'calm',
          targetX: null,
          sleepUntil: 0,
          nextDecisionAt: now + 2200 + Math.random() * 1800,
          nextAttentionAt: now + 2500 + Math.random() * 2500,
          attentionUntil: 0,
          pendingMode: null,
          pendingModeAt: 0,
          pendingFacing: 1,
          idleClip,
          zOffset: Math.round(Math.random() * 6),
          clip: idleClip,
          clipStartedAt: now,
          frameIndex: 0,
          nextFrameAt: now + (1000 / getEffectiveClipFps(petAsset.clips[idleClip], runtimeConfig.reducedMotion)),
          transitionTo: null
        });
      }

      return {
        width: 640,
        height: 220,
        active: this.props.isTermActive !== false,
        pets,
        toys: []
      };
    }

    componentDidMount() {
      this._mounted = true;
      this._container = this.props.termRef || null;
      refreshRuntimeConfigFromRenderer();
      if (runtimeConfig.debugFrames) {
        console.log('[hyperpets] HyperPetsLayer mounted', {
          uid: this.props.uid,
          runtimeConfig
        });
      }
      this.measure();
      window.addEventListener('resize', this._resizeHandler);
      if (this._container) {
        this._container.addEventListener('mousemove', this._mouseHandler, { passive: true });
      }
      this._unsubscribe = subscribe(this.props.uid, (event) => this.handlePetEvent(event));
      this._raf = window.requestAnimationFrame(this._tick);
    }

    componentWillUnmount() {
      this._mounted = false;
      if (this._unsubscribe) {
        this._unsubscribe();
      }
      if (this._raf) {
        window.cancelAnimationFrame(this._raf);
      }
      window.removeEventListener('resize', this._resizeHandler);
      if (this._container) {
        this._container.removeEventListener('mousemove', this._mouseHandler);
      }
    }

    componentDidUpdate(prevProps) {
      if (prevProps.termRef !== this.props.termRef) {
        if (this._container) {
          this._container.removeEventListener('mousemove', this._mouseHandler);
        }
        this._container = this.props.termRef || null;
        if (this._container) {
          this._container.addEventListener('mousemove', this._mouseHandler, { passive: true });
          this.measure();
        }
      }

      if (this.props.cursorFrame !== prevProps.cursorFrame) {
        this._cursorFrame = this.props.cursorFrame || null;
      }

      if (this.props.isTermActive !== prevProps.isTermActive) {
        this.setState({ active: this.props.isTermActive !== false });
      }
    }

    handleResize() {
      this.measure();
    }

    handleMouseMove(event) {
      if (!this._container) {
        return;
      }

      const bounds = this._container.getBoundingClientRect();
      this._mouseX = clamp(event.clientX - bounds.left, 0, bounds.width);
      this._mouseInfluenceUntil = Date.now() + 2200;
    }

    handlePetEvent(event) {
      if (event.type === 'command-success') {
        this.spawnToy();
      } else if (event.type === 'error-output') {
        this.setState((prevState) => ({
          pets: prevState.pets.map((pet) => Object.assign({}, pet, {
            mood: 'alert',
            mode: pet.mode === 'sleep' ? 'idle' : pet.mode,
            sleepUntil: 0,
            attentionUntil: 0,
            pendingMode: null,
            pendingModeAt: 0,
            nextAttentionAt: Date.now() + 2200,
            nextDecisionAt: Date.now() + 1600
          }))
        }));
      }
    }

    measure() {
      if (!this.props.termRef) {
        return;
      }

      const bounds = this.props.termRef.getBoundingClientRect();
      this.setState({
        width: Math.max(140, Math.round(bounds.width || 0)),
        height: Math.max(80, Math.round(bounds.height || 0))
      });
    }

    spawnToy() {
      if (!this._mounted || runtimeConfig.maxToys <= 0) {
        return;
      }

      this.setState((prevState) => {
        const width = prevState.width;
        const keptToyCount = Math.max(runtimeConfig.maxToys - 1, 0);
        const nextToys = keptToyCount === 0 ? [] : prevState.toys.slice(-keptToyCount);
        const kind = Math.random() > 0.5 ? 'ball' : 'bone';
        const toy = {
          id: `toy-${Date.now()}-${Math.round(Math.random() * 9999)}`,
          kind,
          x: clamp(48 + Math.random() * (width - 96), 12, width - 20),
          expiresAt: Date.now() + 12000
        };
        const leadToy = toy;
        const pets = prevState.pets.map((pet, index) => {
          if (index > 0) {
            return pet;
          }
          return Object.assign({}, pet, {
            mode: 'chase-toy',
            mood: 'excited',
            targetX: leadToy.x,
            sleepUntil: 0,
            attentionUntil: Date.now() + 2400,
            nextAttentionAt: Date.now() + 5000,
            nextDecisionAt: Date.now() + 3200
          });
        });

        return {
          toys: nextToys.concat(toy),
          pets
        };
      });
    }

    tick(timestamp) {
      if (!this._mounted) {
        return;
      }

      const dt = this._lastFrameAt ? Math.min(timestamp - this._lastFrameAt, 40) : 16;
      this._lastFrameAt = timestamp;
      const now = Date.now();

      this.setState((prevState) => {
        const width = prevState.width;
        const active = runtimeConfig.onlyActivePane ? prevState.active : true;
        const liveToys = prevState.toys.filter((toy) => toy.expiresAt > now);
        const targetToy = liveToys[0] || null;
        const cursorTarget = this._cursorFrame ? this._cursorFrame.x : null;
        const mouseActive = this._mouseX != null && this._mouseInfluenceUntil > now;

        const pets = prevState.pets.map((pet) => {
          const next = Object.assign({}, pet);
          const laneMax = Math.max(18, width - 44);
          const speedScale = runtimeConfig.reducedMotion ? 0.45 : 1;
          const travel = next.vx * runtimeConfig.walkSpeed * dt * speedScale;
          const attentionActive = next.attentionUntil > now;
          const isMovingMode = next.mode === 'walk' || next.mode === 'chase-toy' || next.mode === 'chase-cursor';

          if (!active) {
            next.mode = 'sleep';
            next.sleepUntil = now + randomSleepDuration();
            next.mood = 'calm';
            next.targetX = null;
            next.attentionUntil = 0;
            next.pendingMode = null;
            next.pendingModeAt = 0;
          } else if (next.pendingMode && next.mode === 'idle' && now >= next.pendingModeAt) {
            next.mode = next.pendingMode;
            next.pendingMode = null;
            next.pendingModeAt = 0;
            next.targetX = null;
            next.facing = next.pendingFacing || next.facing;
            if (next.mode === 'sleep') {
              next.sleepUntil = now + randomSleepDuration();
              next.attentionUntil = 0;
            } else if (next.mode === 'inspect') {
              next.attentionUntil = now + 1600 + Math.random() * 1400;
              next.nextDecisionAt = now + 1800 + Math.random() * 1200;
            }
          } else if (targetToy) {
            next.targetX = targetToy.x;
            next.mode = 'chase-toy';
            next.mood = 'excited';
          } else if (!attentionActive && now >= next.nextAttentionAt) {
            const shouldNoticeMouse = mouseActive && Math.random() < runtimeConfig.chaseCursorChance;
            const shouldNoticeCursor = cursorTarget != null && Math.random() < runtimeConfig.chaseCursorChance * 0.65;

            if (shouldNoticeMouse) {
              next.targetX = this._mouseX;
              next.mode = 'chase-cursor';
              next.mood = 'curious';
              next.attentionUntil = now + 1200 + Math.random() * 1200;
              next.nextAttentionAt = now + 5000 + Math.random() * 5000;
              next.nextDecisionAt = now + 1800 + Math.random() * 1200;
            } else if (shouldNoticeCursor) {
              next.pendingFacing = cursorTarget >= next.x ? 1 : -1;
              if (isMovingMode) {
                next.targetX = null;
                next.mode = 'idle';
                next.pendingMode = 'inspect';
                next.pendingModeAt = now + 900;
                next.attentionUntil = 0;
                next.nextDecisionAt = now + 900;
              } else {
                next.targetX = null;
                next.mode = 'inspect';
                next.mood = 'curious';
                next.facing = next.pendingFacing;
                next.attentionUntil = now + 900 + Math.random() * 900;
                next.nextAttentionAt = now + 4200 + Math.random() * 4200;
                next.nextDecisionAt = now + 1500 + Math.random() * 900;
              }
            } else {
              next.nextAttentionAt = now + 1800 + Math.random() * 2800;
            }
          } else if (now >= next.nextDecisionAt) {
            const roll = Math.random();
            if (roll < 0.2) {
              if (isMovingMode) {
                next.mode = 'idle';
                next.targetX = null;
                next.attentionUntil = 0;
                next.pendingMode = 'sleep';
                next.pendingModeAt = now + 900;
              } else {
                next.mode = 'sleep';
                next.sleepUntil = now + randomSleepDuration();
                next.targetX = null;
                next.attentionUntil = 0;
              }
            } else if (roll < 0.78) {
              next.mode = 'walk';
              next.targetX = clamp(24 + Math.random() * (width - 48), 16, laneMax);
              next.attentionUntil = now + 1200 + Math.random() * 1600;
              next.pendingMode = null;
              next.pendingModeAt = 0;
            } else {
              next.mode = 'idle';
              next.targetX = null;
              next.attentionUntil = 0;
              next.pendingMode = null;
              next.pendingModeAt = 0;
              next.idleClip = randomIdleClip();
            }
            next.mood = 'calm';
            next.nextDecisionAt = now + 3200 + Math.random() * 3400;
          }

          if (next.mode === 'sleep' && now < next.sleepUntil) {
            return next;
          }

          if (next.mode === 'sleep' && now >= next.sleepUntil) {
            next.mode = 'idle';
            next.nextDecisionAt = now + 1600 + Math.random() * 1600;
            next.nextAttentionAt = now + 2000 + Math.random() * 2000;
            next.pendingMode = null;
            next.pendingModeAt = 0;
            next.idleClip = randomIdleClip();
          }

          if (typeof next.targetX === 'number' && (next.mode === 'walk' || next.mode === 'chase-toy' || next.mode === 'chase-cursor')) {
            const dx = next.targetX - next.x;
            if (Math.abs(dx) < 4) {
              next.targetX = null;
              next.attentionUntil = 0;
              next.mode = 'idle';
              next.pendingMode = null;
              next.pendingModeAt = 0;
              next.idleClip = randomIdleClip();
            } else {
              next.facing = dx >= 0 ? 1 : -1;
              next.x = clamp(next.x + Math.sign(dx) * travel, 12, laneMax);
              if (next.mode !== 'chase-toy' && next.mode !== 'inspect' && next.mode !== 'chase-cursor') {
                next.mode = 'walk';
              }
            }
          } else if (typeof next.targetX === 'number') {
            next.targetX = null;
          } else if (next.mode === 'walk') {
            next.mode = 'idle';
          }

          if (next.x <= 12 || next.x >= laneMax) {
            next.facing *= -1;
          }

          const desiredClip = modeToClip(next);
          const animationState = resolveAnimationState(next, desiredClip, now, runtimeConfig.reducedMotion);
          const previousClip = next.clip;
          next.clip = animationState.clip;
          next.clipStartedAt = animationState.clipStartedAt;
          next.transitionTo = animationState.transitionTo;

          const petAsset = getPetAsset(next.type);
          const defaultClipName = getDefaultClipName(petAsset);
          const activeClip = petAsset.clips[next.clip] || petAsset.clips[defaultClipName];
          const frameDuration = 1000 / getEffectiveClipFps(activeClip, runtimeConfig.reducedMotion);

          if (previousClip !== next.clip) {
            next.frameIndex = 0;
            next.nextFrameAt = now + frameDuration;
          } else if (now >= (next.nextFrameAt || 0)) {
            const lastIndex = Math.max(activeClip.frames.length - 1, 0);
            const currentIndex = typeof next.frameIndex === 'number' ? next.frameIndex : 0;
            next.frameIndex = lastIndex === 0 ? 0 : currentIndex + 1;
            if (next.frameIndex > lastIndex) {
              next.frameIndex = activeClip.nextClip ? lastIndex : 0;
            }
            next.nextFrameAt = now + frameDuration;
          }

          return next;
        });

        return {
          toys: liveToys,
          pets
        };
      });

      this._raf = window.requestAnimationFrame(this._tick);
    }

    renderToy(toy) {
      return React.createElement('div', {
        key: toy.id,
        className: `hyperpets-toy ${toy.kind}`,
        style: {
          left: `${toy.x}px`
        }
      });
    }

    renderPet(pet) {
      const petAsset = getPetAsset(pet.type);
      const frameInfo = getAnimatedFrame(pet, runtimeConfig.reducedMotion, Date.now());
      const frame = frameInfo.frame;
      const desiredFacing = pet.facing >= 0 ? 'right' : 'left';
      const basisFacing = frame.facingBasis || 'left';
      const flipX = desiredFacing !== basisFacing;
      const frameRow = frame.frameY != null ? Math.round(frame.frameY / frame.frameHeight) + 1 : null;
      const frameCol = frame.frameX != null ? Math.round(frame.frameX / frame.frameWidth) + 1 : null;
      const debugToken = frameRow != null && frameCol != null
        ? `${frameInfo.clipName}:${frameInfo.frameIndex + 1}/${frameInfo.frameCount}@r${frameRow}c${frameCol}`
        : `${frameInfo.clipName}:${frameInfo.frameIndex + 1}/${frameInfo.frameCount}@svg`;
      const existingHistory = this._debugFrameHistory.get(pet.id) || [];
      const nextHistory = existingHistory.length > 0 && existingHistory[existingHistory.length - 1] === debugToken
        ? existingHistory
        : existingHistory.concat(debugToken).slice(-5);
      this._debugFrameHistory.set(pet.id, nextHistory);
      if (runtimeConfig.debugFrames) {
        const previousToken = this._debugLastLoggedToken.get(pet.id);
        if (previousToken !== debugToken) {
          this._debugLastLoggedToken.set(pet.id, debugToken);
          console.log(
            `[hyperpets] ${pet.id} mode=${pet.mode} facing=${desiredFacing} basis=${basisFacing} clip=${frameInfo.clipName} frame=${frameInfo.frameIndex + 1}/${frameInfo.frameCount}` +
            (frameRow != null && frameCol != null ? ` cell=r${frameRow}c${frameCol}` : ' cell=svg')
          );
        }
      }
      const className = [
        'hyperpets-pet',
        pet.mood === 'excited' ? 'excited' : ''
      ].filter(Boolean).join(' ');
      const frameNode = frame.svg
        ? React.createElement('div', {
          className: 'hyperpets-frame',
          style: {
            width: `${petAsset.width}px`,
            height: `${petAsset.height}px`,
            transform: `scaleX(${flipX ? -1 : 1}) translateZ(0)`
          },
          dangerouslySetInnerHTML: { __html: frame.svg }
        })
        : React.createElement('div', {
          className: `hyperpets-frame${runtimeConfig.debugFrames ? ' hyperpets-frame-debug' : ''}`,
          style: {
            width: `${petAsset.width}px`,
            height: `${petAsset.height}px`,
            transform: `scaleX(${flipX ? -1 : 1}) translateZ(0)`,
            backgroundImage: `url("${frame.sheetUrl}")`,
            backgroundRepeat: 'no-repeat',
            backgroundSize: `${(frame.sheetWidth / frame.frameWidth) * petAsset.width}px ${(frame.sheetHeight / frame.frameHeight) * petAsset.height}px`,
            backgroundPosition: `-${(frame.frameX / frame.frameWidth) * petAsset.width}px -${(frame.frameY / frame.frameHeight) * petAsset.height}px`
          }
        });

      return React.createElement(
        'div',
        {
          key: pet.id,
          className,
          style: {
            left: `${pet.x}px`,
            width: `${petAsset.width}px`,
            height: `${petAsset.height}px`
          }
        },
        frameNode,
        runtimeConfig.debugFrames
          ? React.createElement(
            'div',
            { className: 'hyperpets-debug-label' },
            [
              `${frameInfo.clipName} ${frameInfo.frameIndex + 1}/${frameInfo.frameCount}`,
              `face ${desiredFacing} basis ${basisFacing}`,
              frameRow != null && frameCol != null
                ? `cell r${frameRow} c${frameCol}`
                : 'svg-frame',
              `hist ${nextHistory.join(' | ')}`
            ].join('\n')
          )
          : null,
        pet.mode === 'sleep'
          ? React.createElement('div', {
            className: 'hyperpets-z',
            style: { left: '30px', bottom: `${26 + pet.zOffset}px` }
          }, 'z')
          : null
      );
    }

    render() {
      refreshRuntimeConfigFromRenderer();
      if (!runtimeConfig.enabled) {
        return null;
      }

      const debugHud = runtimeConfig.debugFrames
        ? React.createElement(
          'div',
          { className: 'hyperpets-debug-hud' },
          this.state.pets.map((pet) => {
            const history = this._debugFrameHistory.get(pet.id) || [];
            return [
              `${pet.id} mode=${pet.mode} facing=${pet.facing >= 0 ? 'right' : 'left'}`,
              history.length ? `hist ${history.join(' | ')}` : 'hist (waiting for frames)'
            ].join('\n');
          }).join('\n\n')
        )
        : null;

      return React.createElement(
        'div',
        { className: 'hyperpets-layer' },
        debugHud,
        React.createElement('div', { className: 'hyperpets-floor' }),
        this.state.toys.map((toy) => this.renderToy(toy)),
        this.state.pets.map((pet) => this.renderPet(pet))
      );
    }
  }

  return class HyperPetsTerm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        termRef: null,
        cursorFrame: null
      };
      this._lastCursorSyncAt = 0;
      this._lastCursorX = null;
      this.handleDecorated = this.handleDecorated.bind(this);
      this.handleCursorMove = this.handleCursorMove.bind(this);
    }

    handleDecorated(term) {
      if (this.props.onDecorated) {
        this.props.onDecorated(term);
      }

      this.setState({
        termRef: term ? term.termRef : null
      });
    }

    handleCursorMove(cursorFrame) {
      if (this.props.onCursorMove) {
        this.props.onCursorMove(cursorFrame);
      }

      const now = Date.now();
      const x = cursorFrame && typeof cursorFrame.x === 'number' ? cursorFrame.x : null;
      const movedEnough = x == null || this._lastCursorX == null || Math.abs(x - this._lastCursorX) >= 10;
      const staleEnough = now - this._lastCursorSyncAt >= 180;

      if (movedEnough && staleEnough) {
        this._lastCursorSyncAt = now;
        this._lastCursorX = x;
        this.setState({ cursorFrame });
      }
    }

    render() {
      const customChildren = []
        .concat(this.props.customChildren || [])
        .concat(React.createElement(HyperPetsLayer, {
          key: 'hyperpets-layer',
          uid: this.props.uid,
          termRef: this.state.termRef,
          cursorFrame: this.state.cursorFrame,
          isTermActive: this.props.isTermActive
        }));

      return React.createElement(Term, Object.assign({}, this.props, {
        onDecorated: this.handleDecorated,
        onCursorMove: this.handleCursorMove,
        customChildren
      }));
    }
  };
};
