import { Viewer } from '@photo-sphere-viewer/core';

const viewerContainer = document.getElementById('viewer');
const defaultZoomLevel = 50;
const autorotateStep = 0.0038;
const autorotateIntervalMs = 16;
const interactiveStatePreviewThrottleMs = 180;
const interactiveStateSettleDelayMs = 90;
const resizeSettleDelayMs = 90;
const fullCircle = Math.PI * 2;
const angleClampEpsilon = 1e-4;
const resetViewAngleTolerance = 0.0025;
const resetViewZoomTolerance = 0.5;
const hostCommandTypes = Object.freeze({
  load: 'load',
  zoomIn: 'zoomIn',
  zoomOut: 'zoomOut',
  resetView: 'resetView',
  toggleAutoRotate: 'toggleAutoRotate',
});
const hostMessageTypes = Object.freeze({
  hostReady: 'host-ready',
  loaded: 'loaded',
  state: 'state',
  shortcut: 'shortcut',
  error: 'error',
});
const hostShortcutActions = Object.freeze({
  toggleInfo: 'toggleInfo',
  goHome: 'goHome',
  openWith: 'openWith',
  openFileLocation: 'openFileLocation',
  openSettings: 'openSettings',
  copy: 'copy',
  copyPath: 'copyPath',
  undoDelete: 'undoDelete',
  delete: 'delete',
  deletePermanent: 'deletePermanent',
  openContextMenu: 'openContextMenu',
});

let viewer = null;
let autorotateTimer = null;
let initialPosition = { yaw: 0, pitch: 0 };
let defaultViewState = { yaw: 0, pitch: 0, zoomLevel: defaultZoomLevel };
let hasDefaultViewState = false;
let loadSequence = 0;
let pendingStateTimer = null;
let lastStateEmitTimestamp = 0;
let panoViewLimits = null;
let pendingResizeSettleTimer = null;
let resizeFramePending = false;
let resizeGeneration = 0;

function postMessage(message) {
  if (window.chrome && window.chrome.webview) {
    window.chrome.webview.postMessage(message);
  }
}

function postError(message, details = null) {
  postMessage({
    type: hostMessageTypes.error,
    message,
    details,
  });
}

function toDegrees(radians) {
  return radians * 180 / Math.PI;
}

function normalizeYaw(yaw) {
  let value = yaw % fullCircle;
  if (value < 0) {
    value += fullCircle;
  }

  return value;
}

function shortestAngularDistance(from, to) {
  let delta = normalizeYaw(to - from);
  if (delta > Math.PI) {
    delta -= fullCircle;
  }

  return delta;
}

function buildPanoViewLimits(panoData) {
  if (!panoData) {
    return null;
  }

  const fullWidth = Number(panoData.fullWidth);
  const fullHeight = Number(panoData.fullHeight);
  const croppedWidth = Number(panoData.croppedWidth ?? fullWidth);
  const croppedHeight = Number(panoData.croppedHeight ?? fullHeight);
  const croppedX = Number(panoData.croppedX ?? 0);
  const croppedY = Number(panoData.croppedY ?? 0);

  if (!(fullWidth > 0) || !(fullHeight > 0) || !(croppedWidth > 0) || !(croppedHeight > 0)) {
    return null;
  }

  const horizontalSpan = croppedWidth / fullWidth * fullCircle;
  const hasYawLimit = horizontalSpan < fullCircle - angleClampEpsilon;
  const yawStart = normalizeYaw(croppedX / fullWidth * fullCircle + Math.PI);
  const yawEnd = normalizeYaw((croppedX + croppedWidth) / fullWidth * fullCircle + Math.PI);

  const minPitch = Math.max(
    -Math.PI / 2,
    Math.PI / 2 - (croppedY + croppedHeight) / fullHeight * Math.PI);
  const maxPitch = Math.min(
    Math.PI / 2,
    Math.PI / 2 - croppedY / fullHeight * Math.PI);
  const hasPitchLimit = croppedHeight < fullHeight;

  return {
    hasYawLimit,
    yawStart,
    yawEnd,
    hasPitchLimit,
    minPitch,
    maxPitch,
  };
}

function isYawWithinRange(yaw, limits) {
  if (!limits?.hasYawLimit) {
    return true;
  }

  if (limits.yawStart <= limits.yawEnd) {
    return yaw >= limits.yawStart && yaw <= limits.yawEnd;
  }

  return yaw >= limits.yawStart || yaw <= limits.yawEnd;
}

function clampYaw(yaw, limits) {
  const value = normalizeYaw(yaw);
  if (!limits?.hasYawLimit || isYawWithinRange(value, limits)) {
    return value;
  }

  const distanceToStart = Math.abs(shortestAngularDistance(value, limits.yawStart));
  const distanceToEnd = Math.abs(shortestAngularDistance(value, limits.yawEnd));
  return distanceToStart <= distanceToEnd ? limits.yawStart : limits.yawEnd;
}

function clampPosition(position) {
  if (!position || !panoViewLimits) {
    return position;
  }

  const nextPosition = { ...position };
  if (panoViewLimits.hasYawLimit) {
    nextPosition.yaw = clampYaw(nextPosition.yaw, panoViewLimits);
  }
  if (panoViewLimits.hasPitchLimit) {
    nextPosition.pitch = Math.min(
      panoViewLimits.maxPitch,
      Math.max(panoViewLimits.minPitch, nextPosition.pitch));
  }

  return nextPosition;
}

function clampEventPosition(event) {
  if (!event?.position || !panoViewLimits) {
    return;
  }

  const clamped = clampPosition(event.position);
  event.position.yaw = clamped.yaw;
  event.position.pitch = clamped.pitch;
}

function applyPanoViewLimits() {
  if (!viewer || !panoViewLimits) {
    return;
  }

  const position = viewer.getPosition();
  const clamped = clampPosition(position);
  if (Math.abs(clamped.yaw - position.yaw) > 1e-6 || Math.abs(clamped.pitch - position.pitch) > 1e-6) {
    viewer.rotate(clamped);
  }
}

function getCurrentViewState() {
  if (!viewer) {
    return null;
  }

  const position = viewer.getPosition();
  return {
    yaw: normalizeYaw(position.yaw),
    pitch: position.pitch,
    zoomLevel: viewer.getZoomLevel(),
  };
}

function captureDefaultViewState() {
  const currentState = getCurrentViewState();
  if (!currentState) {
    hasDefaultViewState = false;
    return;
  }

  defaultViewState = currentState;
  hasDefaultViewState = true;
}

function canResetView(currentState = null) {
  if (!hasDefaultViewState) {
    return false;
  }

  const state = currentState ?? getCurrentViewState();
  if (!state) {
    return false;
  }

  return Math.abs(shortestAngularDistance(defaultViewState.yaw, state.yaw)) > resetViewAngleTolerance
    || Math.abs(state.pitch - defaultViewState.pitch) > resetViewAngleTolerance
    || Math.abs(state.zoomLevel - defaultViewState.zoomLevel) > resetViewZoomTolerance;
}

function getState() {
  if (!viewer) {
    return {
      headingDeg: 0,
      zoomLevel: defaultZoomLevel,
      autoRotate: false,
      canResetView: false,
    };
  }

  const currentState = getCurrentViewState();
  return {
    headingDeg: ((toDegrees(currentState.yaw) % 360) + 360) % 360,
    zoomLevel: currentState.zoomLevel,
    autoRotate: autorotateTimer !== null,
    canResetView: canResetView(currentState),
  };
}

function cancelPendingStateEmit() {
  if (pendingStateTimer !== null) {
    window.clearTimeout(pendingStateTimer);
    pendingStateTimer = null;
  }
}

function postCurrentState() {
  cancelPendingStateEmit();
  lastStateEmitTimestamp = performance.now();
  postMessage({
    type: hostMessageTypes.state,
    ...getState(),
  });
}

function emitState(force = false) {
  if (force) {
    postCurrentState();
    return;
  }

  const elapsed = performance.now() - lastStateEmitTimestamp;
  if (elapsed >= interactiveStatePreviewThrottleMs) {
    postCurrentState();
    return;
  }

  if (pendingStateTimer !== null) {
    return;
  }

  pendingStateTimer = window.setTimeout(
    postCurrentState,
    interactiveStatePreviewThrottleMs - elapsed);
}

function emitInteractiveState() {
  const elapsed = performance.now() - lastStateEmitTimestamp;
  if (elapsed >= interactiveStatePreviewThrottleMs) {
    postCurrentState();
  }

  cancelPendingStateEmit();
  pendingStateTimer = window.setTimeout(
    postCurrentState,
    interactiveStateSettleDelayMs);
}

function stopAutoRotate(silent = false) {
  if (autorotateTimer !== null) {
    window.clearInterval(autorotateTimer);
    autorotateTimer = null;
  }

  if (!silent) {
    emitState(true);
  }
}

function startAutoRotate() {
  if (!viewer || autorotateTimer !== null) {
    return;
  }

  autorotateTimer = window.setInterval(() => {
    const position = viewer.getPosition();
    viewer.rotate({
      yaw: position.yaw + autorotateStep,
      pitch: position.pitch,
    });
  }, autorotateIntervalMs);

  emitState(true);
}

function markViewerVisible() {
  viewerContainer.style.opacity = '1';
}

function markViewerHidden() {
  viewerContainer.style.opacity = '0';
}

function notifyLoadedWhenVisible(revealDelayMs = 250, loadVersion = null) {
  if (!viewer) {
    return;
  }

  const sequence = ++loadSequence;
  let renderCount = 0;
  let settled = false;
  let fallbackTimer = null;
  let revealTimer = null;

  const cleanup = () => {
    if (viewer) {
      viewer.removeEventListener('render', onRender);
    }

    if (fallbackTimer !== null) {
      window.clearTimeout(fallbackTimer);
      fallbackTimer = null;
    }

    if (revealTimer !== null) {
      window.clearTimeout(revealTimer);
      revealTimer = null;
    }
  };

  const finish = () => {
    if (settled) {
      return;
    }

    settled = true;
    cleanup();

    if (sequence !== loadSequence) {
      return;
    }

    markViewerVisible();
    // Echo the load version so the host can ignore a completion that the user already
    // navigated past (avoids hiding the overlay over the wrong image).
    postMessage({ type: hostMessageTypes.loaded, version: loadVersion });
    emitState(true);
  };

  const scheduleFinish = () => {
    if (revealTimer !== null) {
      return;
    }

    revealTimer = window.setTimeout(finish, revealDelayMs);
  };

  const onRender = () => {
    renderCount += 1;
    if (renderCount >= 2) {
      scheduleFinish();
    }
  };

  viewer.addEventListener('render', onRender);
  viewer.needsUpdate();
  fallbackTimer = window.setTimeout(finish, Math.max(revealDelayMs + 1500, 2200));
}

async function loadPanorama(payload) {
  stopAutoRotate(true);
  cancelPendingStateEmit();
  loadSequence += 1;
  markViewerHidden();
  panoViewLimits = buildPanoViewLimits(payload.panoData);
  hasDefaultViewState = false;
  defaultViewState = { yaw: 0, pitch: 0, zoomLevel: defaultZoomLevel };

  if (viewer) {
    viewer.destroy();
    viewer = null;
  }

  initialPosition = {
    yaw: payload.defaultYaw ?? 0,
    pitch: payload.defaultPitch ?? 0,
  };

  try {
    viewer = new Viewer({
      container: viewerContainer,
      panorama: payload.imageUrl,
      panoData: payload.panoData ?? null,
      navbar: false,
      loadingTxt: null,
      caption: payload.caption ?? '',
      defaultYaw: payload.defaultYaw ?? 0,
      defaultPitch: payload.defaultPitch ?? 0,
      defaultZoomLvl: defaultZoomLevel,
      canvasBackground: 'transparent',
      minFov: 30,
      maxFov: 90,
      moveInertia: 0.8,
      mousewheelCtrlKey: false,
      touchmoveTwoFingers: false,
    });

    viewer.addEventListener('ready', () => emitState(true));
    viewer.addEventListener('before-rotate', clampEventPosition);
    viewer.addEventListener('before-animate', clampEventPosition);
    viewer.addEventListener('panorama-loaded', () => {
      applyPanoViewLimits();
      captureDefaultViewState();
      notifyLoadedWhenVisible(payload.revealDelayMs ?? 250, payload.version ?? null);
    });

    viewer.addEventListener('position-updated', emitInteractiveState);
    viewer.addEventListener('zoom-updated', emitInteractiveState);
    viewer.addEventListener('panorama-error', (event) => {
      postError(
        'Photo Sphere Viewer failed to load the panorama.',
        event?.message ?? event?.error?.message ?? null);
    });
  } catch (error) {
    postError(
      'Failed to initialize panorama viewer.',
      error instanceof Error ? error.stack ?? error.message : String(error));
  }
}

async function handleCommand(rawCommand) {
  const command = typeof rawCommand === 'string'
    ? JSON.parse(rawCommand)
    : rawCommand;

  switch (command.type) {
    case hostCommandTypes.load:
      await loadPanorama(command);
      break;

    case hostCommandTypes.zoomIn:
      viewer?.zoomIn(10);
      emitState(true);
      break;

    case hostCommandTypes.zoomOut:
      viewer?.zoomOut(10);
      emitState(true);
      break;

    case hostCommandTypes.resetView:
      if (viewer) {
        viewer.rotate(initialPosition);
        viewer.zoom(defaultZoomLevel);
        emitState(true);
      }
      break;

    case hostCommandTypes.toggleAutoRotate:
      if (autorotateTimer !== null) {
        stopAutoRotate();
      } else {
        startAutoRotate();
      }
      break;
  }
}

function applyViewerResize() {
  if (!viewer) {
    return;
  }

  viewer.autoSize();
  viewer.needsUpdate();
}

window.addEventListener('resize', () => {
  resizeGeneration += 1;

  if (!resizeFramePending) {
    resizeFramePending = true;
    window.requestAnimationFrame(() => {
      resizeFramePending = false;
      applyViewerResize();
    });
  }

  if (pendingResizeSettleTimer !== null) {
    window.clearTimeout(pendingResizeSettleTimer);
  }

  const generation = resizeGeneration;
  pendingResizeSettleTimer = window.setTimeout(() => {
    pendingResizeSettleTimer = null;
    window.requestAnimationFrame(() => {
      if (generation === resizeGeneration) {
        applyViewerResize();
      }
    });
  }, resizeSettleDelayMs);
});

function getShortcutAction(event) {
  if (event.defaultPrevented || event.altKey || event.metaKey) {
    return null;
  }

  const key = typeof event.key === 'string' ? event.key : '';
  const lowerKey = key.toLowerCase();
  const code = typeof event.code === 'string' ? event.code : '';

  if (event.ctrlKey) {
    switch (lowerKey) {
      case 'c':
        return hostShortcutActions.copy;
      case 'l':
        return hostShortcutActions.copyPath;
      case 'z':
        return hostShortcutActions.undoDelete;
      default:
        return null;
    }
  }

  if (event.shiftKey) {
    if (key === 'Delete') {
      return hostShortcutActions.deletePermanent;
    }

    if (key === 'F10') {
      return hostShortcutActions.openContextMenu;
    }

    return null;
  }

  switch (lowerKey) {
    case 'i':
      return hostShortcutActions.toggleInfo;
    case 'e':
      return hostShortcutActions.openWith;
    case 'l':
      return hostShortcutActions.openFileLocation;
    default:
      break;
  }

  if (code === 'Comma' || key === ',') {
    return hostShortcutActions.openSettings;
  }

  if (key === 'Backspace') {
    return hostShortcutActions.goHome;
  }

  if (key === 'Delete') {
    return hostShortcutActions.delete;
  }

  if (key === 'ContextMenu' || code === 'ContextMenu') {
    return hostShortcutActions.openContextMenu;
  }

  return null;
}

window.addEventListener('keydown', (event) => {
  const action = getShortcutAction(event);
  if (!action) {
    return;
  }

  event.preventDefault();
  postMessage({
    type: hostMessageTypes.shortcut,
    action,
  });
});

window.addEventListener('error', (event) => {
  postError(
    event.message || 'Panorama host script error.',
    event.error?.stack ?? `${event.filename}:${event.lineno}:${event.colno}`);
});

window.addEventListener('unhandledrejection', (event) => {
  const reason = event.reason instanceof Error
    ? event.reason.stack ?? event.reason.message
    : String(event.reason);
  postError('Panorama host promise rejection.', reason);
});

window.panoramaHost = {
  handleCommand,
};

postMessage({ type: hostMessageTypes.hostReady });
