import { avoidTreeShaking } from '@genesislcap/foundation-utils';
import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core';
import {
  AI_COLOUR_AMBER,
  AI_COLOUR_CYAN,
  AI_COLOUR_PINK,
  AI_COLOUR_VIOLET,
} from '../styles/ai-colours';
import { AiHaloOverlay } from './halo-overlay';

const WAVES_DEFAULT_SIZE = 56;
/** CSS-ready form of `WAVES_DEFAULT_SIZE` (the `css` tag rejects raw numbers). */
const WAVES_DEFAULT_SIZE_CSS = `${WAVES_DEFAULT_SIZE}px`;

/** SVG coordinate space the waves are drawn in (square; the circle fills it). */
const VIEWBOX = 120;
const CENTRE = VIEWBOX / 2;
/** Horizontal sampling step when tracing each wave path. Lower = smoother. */
const SAMPLE_STEP = 6;
/** Milliseconds in one second. */
const MS_PER_SECOND = 1000;
/** Baseline frame rate (fps) the wave speeds are tuned against. */
const BASELINE_FPS = 60;
/**
 * Frame duration (ms) at the baseline rate. The paths are driven by elapsed
 * wall-clock time divided by this — not by the rAF callback count — so they
 * advance at a constant real-world rate regardless of the display's refresh
 * rate or dropped frames. At the baseline rate the resulting frame index
 * matches the old per-callback counter, preserving the current look.
 */
const FRAME_MS = MS_PER_SECOND / BASELINE_FPS;

/**
 * Per-wave parameters. Each wave is a travelling sine (amplitude/frequency/phase)
 * plus a slower, longer-wavelength term that makes the line "slosh" like liquid.
 */
interface WaveConfig {
  colour: string;
  /** Peak height of the travelling wave, in viewBox units. */
  amplitude: number;
  /** Angular frequency of the travelling wave (radians per viewBox unit). */
  frequency: number;
  /** How fast the travelling wave scrolls (radians per frame). */
  phaseSpeed: number;
  /** Vertical centre offset so the waves stack rather than overlap exactly. */
  verticalOffset: number;
  /** Amplitude of the slow sloshing term. */
  slosh: number;
  /** Angular frequency of the sloshing term (radians per viewBox unit). */
  sloshFrequency: number;
  /** How fast the sloshing term evolves (radians per frame). */
  sloshSpeed: number;
}

const WAVES: WaveConfig[] = [
  {
    colour: AI_COLOUR_AMBER,
    amplitude: 11,
    frequency: 0.085,
    phaseSpeed: 0.05,
    verticalOffset: -6,
    slosh: 6,
    sloshFrequency: 0.018,
    sloshSpeed: 0.021,
  },
  {
    colour: AI_COLOUR_PINK,
    amplitude: 14,
    frequency: 0.07,
    phaseSpeed: -0.043,
    verticalOffset: -2,
    slosh: 7,
    sloshFrequency: 0.022,
    sloshSpeed: -0.017,
  },
  {
    colour: AI_COLOUR_CYAN,
    amplitude: 13,
    frequency: 0.095,
    phaseSpeed: 0.037,
    verticalOffset: 2,
    slosh: 5,
    sloshFrequency: 0.015,
    sloshSpeed: 0.025,
  },
  {
    colour: AI_COLOUR_VIOLET,
    amplitude: 10,
    frequency: 0.06,
    phaseSpeed: -0.055,
    verticalOffset: 6,
    slosh: 8,
    sloshFrequency: 0.025,
    sloshSpeed: -0.013,
  },
];

const wavePathsMarkup = WAVES.map(
  (w, i) => `<path class="wave" data-wave="${i}" stroke="${w.colour}" />`,
).join('');

/**
 * Animated "waves inside a circle" loading indicator — coloured sine waves that
 * slosh like glowing liquid inside a circular window, ringed by a rotating
 * gradient halo (the same effect as `<ai-halo-overlay>`).
 *
 * Visual sibling of the dots loading indicator; the two are interchangeable
 * styles of the same "assistant is working" state.
 *
 * @example
 * ```html
 * <ai-waves-indicator size="56"></ai-waves-indicator>
 * ```
 *
 * @beta
 */
@customElement({
  name: 'ai-waves-indicator',
  template: html<AiWavesIndicator>`
    <div class="window" role="img" aria-label="Assistant is working">
      <svg class="waves" viewBox="0 0 ${VIEWBOX} ${VIEWBOX}" preserveAspectRatio="none">
        <defs>
          <filter id="wave-glow" x="-20%" y="-20%" width="140%" height="140%">
            <feGaussianBlur stdDeviation="1.6" result="blur" />
            <feMerge>
              <feMergeNode in="blur" />
              <feMergeNode in="SourceGraphic" />
            </feMerge>
          </filter>
        </defs>
        <g filter="url(#wave-glow)">${wavePathsMarkup}</g>
      </svg>
      <ai-halo-overlay active border-size="2" glow-opacity="0.5" glow-spread="55"></ai-halo-overlay>
    </div>
  `,
  styles: css`
    :host {
      display: inline-block;
      width: var(--waves-size, ${WAVES_DEFAULT_SIZE_CSS});
      height: var(--waves-size, ${WAVES_DEFAULT_SIZE_CSS});
    }

    .window {
      position: relative;
      width: 100%;
      height: 100%;
      border-radius: 50%;
      overflow: hidden;
      background: radial-gradient(circle at 50% 32%, #2b3140 0%, #11141c 55%, #05070c 100%);
    }

    .waves {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
    }

    .wave {
      fill: none;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
  `,
})
export class AiWavesIndicator extends GenesisElement {
  /** Diameter of the circular window in px. Default: 56. */
  @attr({ converter: { fromView: Number, toView: String } }) size: number = WAVES_DEFAULT_SIZE;

  sizeChanged() {
    this.style.setProperty('--waves-size', `${this.size}px`);
  }

  // A rAF loop drives the wave paths for the same reason `<ai-halo-overlay>`
  // hand-drives its rotation: a pure-CSS approach can't produce per-frame sine
  // geometry, and SMIL/`<animate>` can't express the combined travel + slosh.
  // The paths are keyed to elapsed wall-clock time (see FRAME_MS), not the rAF
  // callback count, so they hold a constant speed even when frames are dropped.

  private startTime?: number;
  private animFrame?: number;
  private wavePaths?: SVGPathElement[];

  connectedCallback() {
    super.connectedCallback();
    // Guard against a reconnect starting a second concurrent loop.
    if (this.animFrame === undefined) {
      this.tick();
    }
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    if (this.animFrame !== undefined) {
      cancelAnimationFrame(this.animFrame);
      this.animFrame = undefined;
    }
    // Reset the clock so a later reconnect restarts the waves cleanly at frame 0.
    this.startTime = undefined;
  }

  private tick(now?: number) {
    // Stop ticking once disconnected (belt-and-braces alongside the cancel in
    // disconnectedCallback) so no frames are scheduled while detached.
    if (!this.isConnected) {
      this.animFrame = undefined;
      return;
    }
    if (!this.wavePaths) {
      const paths = this.shadowRoot?.querySelectorAll<SVGPathElement>('.wave');
      if (paths?.length) this.wavePaths = Array.from(paths);
    }
    // Anchor to the first real rAF timestamp; the initial synchronous call (no
    // timestamp) renders frame 0. `frame` is then elapsed time in 60fps frames.
    if (now !== undefined && this.startTime === undefined) this.startTime = now;
    const frame =
      now === undefined || this.startTime === undefined ? 0 : (now - this.startTime) / FRAME_MS;
    this.wavePaths?.forEach((path, i) => {
      path.setAttribute('d', AiWavesIndicator.buildWavePath(WAVES[i], frame));
    });
    this.animFrame = requestAnimationFrame((t) => this.tick(t));
  }

  /** Trace one wave's polyline `d` attribute for the given frame. */
  private static buildWavePath(cfg: WaveConfig, frame: number): string {
    const segments: string[] = [];
    for (let x = 0; x <= VIEWBOX; x += SAMPLE_STEP) {
      const y =
        CENTRE +
        cfg.verticalOffset +
        cfg.amplitude * Math.sin(x * cfg.frequency + frame * cfg.phaseSpeed) +
        cfg.slosh * Math.sin(x * cfg.sloshFrequency + frame * cfg.sloshSpeed);
      segments.push(`${x},${y.toFixed(2)}`);
    }
    return `M ${segments.join(' L ')}`;
  }
}

// Ensure the halo overlay used for the ring is registered alongside this component.
avoidTreeShaking(AiHaloOverlay);
