import { 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';

/**
 * SVG coordinate space the bands are drawn in. The bands stretch to fill the
 * host box (the band SVGs use `preserveAspectRatio="none"`), so this aspect need
 * not match the host's. `VIEW_H` is kept just tall enough to bound the crests,
 * leaving minimal dead space above/below the line — otherwise the waves float in
 * the vertical middle of the host and read as sitting too low beside the avatar.
 */
const VIEW_W = 225;
const VIEW_H = 50;
const CENTRE_Y = VIEW_H / 2;
/** Number of samples traced across each band. */
const POINTS = 90;
/** Fraction of the width over which thickness ramps from 0 up to full. */
const EDGE = 0.16;
/** Global multiplier on the harmonic drift speeds — raise to morph faster. */
const MORPH_SPEED = 3;
/** Global multiplier on the harmonic frequencies — raise for narrower peaks. */
const FREQ_SCALE = 2;
/** Milliseconds in one second. */
const MS_PER_SECOND = 1000;
/** Baseline frame rate (fps) the harmonic speeds are tuned against. */
const BASELINE_FPS = 60;
/**
 * Frame duration (ms) at the baseline rate. The morph is driven by elapsed
 * wall-clock time divided by this — not by the rAF callback count — so it
 * advances 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;
/**
 * Half-thickness (viewBox units) each band keeps even between peaks, so the
 * waves emerge from a continuous line rather than leaving gaps. The four bands
 * overlap along this line and screen-blend to white.
 */
const LINE_THICKNESS = 1.2;
/**
 * Amplitude boost added toward the middle of the width (a hump that is 0 at the
 * ends, max in the centre). Makes peaks more likely to pop up centrally, while
 * still allowing tall crests to appear nearer the edges.
 */
const CENTRE_BIAS = 4;

/** One sine component of a band's thickness profile. */
interface Harmonic {
  /** Cycles across the band width. */
  freq: number;
  /** Contribution to the half-thickness, in viewBox units. */
  amp: number;
  /** Phase drift per frame (radians); differing speeds make the bumps morph. */
  speed: number;
  /** Starting phase offset. */
  phase: number;
}

/**
 * One band's thickness profile: a baseline plus a few sine harmonics. Because
 * the harmonics drift at different speeds, their sum continually changes shape —
 * bumps grow, shrink, merge and split — and where the sum dips below zero the
 * band pinches to nothing, so a wave may have a single bump one moment and
 * several the next. A different harmonic mix per band keeps the colours from
 * lining up.
 */
interface BandConfig {
  /**
   * Vertical offset the harmonics oscillate around. Negative values act as a
   * threshold: only the taller crests clear zero, so the band shows fewer,
   * narrower, isolated peaks (the small ripples between crests are clipped away).
   */
  baseline: number;
  harmonics: Harmonic[];
}

const BANDS: BandConfig[] = [
  {
    baseline: -4,
    harmonics: [
      { freq: 1.0, amp: 11, speed: 0.03, phase: 0.0 },
      { freq: 2.3, amp: 6, speed: -0.021, phase: 1.3 },
      { freq: 3.4, amp: 3, speed: 0.041, phase: 2.7 },
    ],
  },
  {
    baseline: -4,
    harmonics: [
      { freq: 1.3, amp: 10, speed: -0.026, phase: 0.8 },
      { freq: 2.0, amp: 7, speed: 0.034, phase: 2.1 },
      { freq: 3.1, amp: 3, speed: -0.045, phase: 0.4 },
    ],
  },
  {
    baseline: -4,
    harmonics: [
      { freq: 0.9, amp: 9, speed: 0.038, phase: 1.9 },
      { freq: 2.6, amp: 6, speed: 0.019, phase: 3.0 },
      { freq: 3.7, amp: 3, speed: -0.03, phase: 0.6 },
    ],
  },
  {
    baseline: -4,
    harmonics: [
      { freq: 1.1, amp: 8, speed: -0.034, phase: 2.4 },
      { freq: 2.4, amp: 5, speed: 0.027, phase: 0.2 },
      { freq: 3.2, amp: 3, speed: 0.048, phase: 1.5 },
    ],
  },
];

// Each band is its own stacked <svg> LAYER. The white-on-overlap effect uses
// `mix-blend-mode: screen`, which composites reliably between HTML-level layers
// but NOT between sibling SVG <path>/<g> elements in many browsers — hence one
// svg per band rather than four paths in a single svg.
const bandLayersMarkup = BANDS.map(
  (_b, i) =>
    `<svg class="layer band-layer" viewBox="0 0 ${VIEW_W} ${VIEW_H}" preserveAspectRatio="none"><path class="band b${i + 1}" data-band="${i}" /></svg>`,
).join('');

/**
 * Animated "flowing waves" loading indicator — coloured organic waves (in the
 * four brand colours) whose bumps swell, shrink, merge and split across a line,
 * tapering to nothing at both ends. Where the colours overlap they blend
 * additively toward white.
 *
 * Visual sibling of the dots loading indicator; interchangeable with the other
 * loading styles for the "assistant is working" state.
 *
 * @example
 * ```html
 * <ai-flowing-waves-indicator></ai-flowing-waves-indicator>
 * ```
 *
 * @beta
 */
@customElement({
  name: 'ai-flowing-waves-indicator',
  template: html<AiFlowingWavesIndicator>`
    <div class="flow" role="img" aria-label="Assistant is working">${bandLayersMarkup}</div>
  `,
  styles: css`
    :host {
      display: inline-block;
      width: 180px;
      /* Height matches the message avatar (32px) so the wave line sits level
         with it. The bands stretch to fill via preserveAspectRatio="none". */
      height: 32px;
    }

    /* Transparent — sits directly on the chat surface (no backdrop). 'isolation:
       isolate' scopes the band layers' 'screen' blend to each other, so where
       colours overlap they add toward white, while the group still composites
       normally onto the page (works on light and dark, no wash-out). */
    .flow {
      position: relative;
      width: 100%;
      height: 100%;
      isolation: isolate;
    }

    .layer {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }

    .band-layer {
      mix-blend-mode: screen;
    }

    .band {
      filter: drop-shadow(0 0 1.5px currentColor);
    }
    .band.b1 {
      color: ${AI_COLOUR_AMBER};
      fill: ${AI_COLOUR_AMBER};
    }
    .band.b2 {
      color: ${AI_COLOUR_PINK};
      fill: ${AI_COLOUR_PINK};
    }
    .band.b3 {
      color: ${AI_COLOUR_CYAN};
      fill: ${AI_COLOUR_CYAN};
    }
    .band.b4 {
      color: ${AI_COLOUR_VIOLET};
      fill: ${AI_COLOUR_VIOLET};
    }
  `,
})
export class AiFlowingWavesIndicator extends GenesisElement {
  // A rAF loop morphs the band shapes: pure CSS can't continuously reshape the
  // bumps (different sizes appearing at different times), and the per-frame sine
  // sums are cheap. Same approach as the sine-tracing waves indicator. The morph
  // is keyed to elapsed wall-clock time (see FRAME_MS), not the rAF callback
  // count, so it holds a constant speed even when frames are dropped.

  private startTime?: number;
  private animFrame?: number;
  private bandPaths?: 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 morph 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.bandPaths) {
      const paths = this.shadowRoot?.querySelectorAll<SVGPathElement>('.band');
      if (paths?.length) this.bandPaths = 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.bandPaths?.forEach((path, i) => {
      const cfg = BANDS[i];
      if (cfg) path.setAttribute('d', AiFlowingWavesIndicator.buildBand(cfg, frame));
    });
    this.animFrame = requestAnimationFrame((t) => this.tick(t));
  }

  /**
   * Trace one band for the given frame: a baseline-plus-harmonics half-thickness
   * profile, clamped at 0 (so the band pinches to nothing between bumps) and
   * tapered to 0 at both ends.
   */
  private static buildBand(cfg: BandConfig, frame: number): string {
    const top: Array<[number, number]> = [];
    const bottom: Array<[number, number]> = [];
    for (let i = 0; i <= POINTS; i += 1) {
      const u = i / POINTS;
      const x = u * VIEW_W;
      // Plateau edge taper: 0 at both ends, full across the middle.
      const edge = Math.min(1, Math.min(u, 1 - u) / EDGE);
      // Sum the drifting harmonics, then clamp so dips pinch the band to nothing.
      let wave = 0;
      for (const h of cfg.harmonics) {
        wave +=
          h.amp *
          Math.sin(u * Math.PI * 2 * h.freq * FREQ_SCALE + frame * h.speed * MORPH_SPEED + h.phase);
      }
      // Hump that favours the middle (0 at the ends, 1 at the centre).
      const centre = Math.sin(Math.PI * u);
      const env = edge * (LINE_THICKNESS + Math.max(0, cfg.baseline + wave + CENTRE_BIAS * centre));
      top.push([x, CENTRE_Y - env]);
      bottom.push([x, CENTRE_Y + env]);
    }
    let d = `M${top[0][0].toFixed(1)} ${top[0][1].toFixed(1)}`;
    for (let i = 1; i < top.length; i += 1)
      d += ` L${top[i][0].toFixed(1)} ${top[i][1].toFixed(1)}`;
    for (let i = bottom.length - 1; i >= 0; i -= 1) {
      d += ` L${bottom[i][0].toFixed(1)} ${bottom[i][1].toFixed(1)}`;
    }
    return `${d} Z`;
  }
}
