class AudioMeter extends HTMLElement
{
	static FLOOR_DB = -60;             // bottom of the scale; -Infinity maps here
	static PEAK_DECAY_DB_PER_SEC = 30; // how fast the peak-hold marker falls

	constructor()
	{
		super();

		this._streamInfo = null;
		this._rafId = null;
		this._lastFrameTime = 0;

		this._bars = [];     // { mask, peak, value } per channel
		this._peaks = [];    // peak-hold value (dB) per channel

		this.attachShadow({ mode: 'open' });
		this.shadowRoot.innerHTML = `
			<style>
				:host {
					display: block;
					width: 100%;
					box-sizing: border-box;
					font-family: sans-serif;
				}
				.caption {
					display: flex;
					align-items: center;
					gap: 6px;
					font-size: .8em;
					color: #555;
					margin-bottom: 3px;
					min-height: 1.2em;
				}
				.name {
					flex: 1 1 auto;
					min-width: 0;
					overflow: hidden;
					text-overflow: ellipsis;
					white-space: nowrap;
					font-weight: 600;
				}
				.delay {
					color: #888;
					font-variant-numeric: tabular-nums;
				}
				.mute {
					display: none; /* toggled to inline-flex when muted */
					align-items: center;
					color: #e74c3c;
				}
				.mute svg {
					width: 1.1em;
					height: 1.1em;
				}
				.meters {
					display: flex;
					flex-direction: column;
					gap: 4px;
				}
				.row {
					display: flex;
					align-items: center;
					gap: 6px;
				}
				.label {
					width: 1.1em;
					text-align: center;
					font-size: .75em;
					color: #888;
				}
				.track {
					position: relative;
					flex: 1 1 0;
					min-width: 0;
					height: 14px;
					border-radius: 3px;
					overflow: hidden;
					background: linear-gradient(to right, #2ecc71 0%, #2ecc71 70%, #f1c40f 85%, #e74c3c 100%);
				}
				.mask {
					position: absolute;
					top: 0;
					right: 0;
					bottom: 0;
					width: 100%;
					background: #e2e2e2;
				}
				.peak {
					position: absolute;
					top: 0;
					bottom: 0;
					left: 0;
					width: 2px;
					margin-left: -1px;
					background: #2c3e50;
					opacity: 0;
				}
				.value {
					width: 2.6em;
					text-align: right;
					font-size: .72em;
					color: #555;
					font-variant-numeric: tabular-nums;
				}
			</style>
			<div class="caption">
				<span class="name"></span>
				<span class="delay"></span>
				<span class="mute" title="Muted"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg></span>
			</div>
			<div class="meters"></div>
		`;

		this._nameEl = this.shadowRoot.querySelector('.name');
		this._delayEl = this.shadowRoot.querySelector('.delay');
		this._muteEl = this.shadowRoot.querySelector('.mute');
		this._metersEl = this.shadowRoot.querySelector('.meters');

		// Cached caption state to avoid redundant DOM writes each frame
		this._lastDelay = null;
		this._lastMuted = null;
	}

	static get observedAttributes()
	{
		return ['label'];
	}

	attributeChangedCallback(name, oldValue, newValue)
	{
		if (name === 'label')
			this._nameEl.textContent = newValue != null ? newValue : '';
	}

	set label(value)
	{
		this._nameEl.textContent = value != null ? value : '';
	}

	get label()
	{
		return this._nameEl.textContent;
	}

	// Hide the delay readout in the caption (it keeps updating internally but stays hidden)
	hideDelay()
	{
		this._delayEl.style.display = 'none';
	}

	set streamInfo(info)
	{
		this._streamInfo = info;
		this._build();
	}

	get streamInfo()
	{
		return this._streamInfo;
	}

	connectedCallback()
	{
		if (this._bars.length === 0)
			this._build();

		this._start();
	}

	disconnectedCallback()
	{
		this._stop();
	}

	// (Re)build the bar rows to match the stream's channel count
	_build()
	{
		const channels = (this._streamInfo != null && !this._streamInfo.isMono) ? 2 : 1;

		if (channels === this._bars.length)
			return;

		this._metersEl.innerHTML = '';
		this._bars = [];
		this._peaks = [];

		const labels = channels === 1 ? ['M'] : ['L', 'R'];

		for (let c = 0; c < channels; c++)
		{
			const row = document.createElement('div');
			row.className = 'row';
			row.innerHTML = `
				<span class="label">${labels[c]}</span>
				<span class="track"><span class="mask"></span><span class="peak"></span></span>
				<span class="value">-&#8734;</span>
			`;

			this._metersEl.appendChild(row);
			this._bars.push({
				mask: row.querySelector('.mask'),
				peak: row.querySelector('.peak'),
				value: row.querySelector('.value')
			});
			this._peaks.push(AudioMeter.FLOOR_DB);
		}
	}

	_start()
	{
		if (this._rafId != null)
			return;

		this._lastFrameTime = performance.now();
		this._rafId = requestAnimationFrame((t) => this._tick(t));
	}

	_stop()
	{
		if (this._rafId != null)
		{
			cancelAnimationFrame(this._rafId);
			this._rafId = null;
		}
	}

	_tick(now)
	{
		this._rafId = requestAnimationFrame((t) => this._tick(t));

		const dt = Math.max(0, (now - this._lastFrameTime) / 1000);
		this._lastFrameTime = now;

		const info = this._streamInfo;
		const mono = this._bars.length === 1;

		for (let c = 0; c < this._bars.length; c++)
		{
			let db = AudioMeter.FLOOR_DB;

			if (info != null)
				db = (mono || c === 0) ? info.leftLevel : info.rightLevel;

			this._renderBar(c, db, dt);
		}

		this._updateCaption(info);
	}

	// Refresh the delay readout and mute icon (only when they change)
	_updateCaption(info)
	{
		const delay = info != null ? Math.round(info.delay) : null;

		if (delay !== this._lastDelay)
		{
			this._delayEl.textContent = delay != null ? delay + ' ms' : '';
			this._lastDelay = delay;
		}

		const muted = info != null && info.isMuted === true;

		if (muted !== this._lastMuted)
		{
			this._muteEl.style.display = muted ? 'inline-flex' : 'none';
			this._lastMuted = muted;
		}
	}

	_renderBar(index, db, dt)
	{
		const bar = this._bars[index];
		const pct = this._toPct(db);

		// Fill: cover the unlit part from the right so the fixed gradient shows up to the level
		bar.mask.style.width = (100 - pct) + '%';

		// Peak-hold: jump up to new peaks, otherwise decay
		let peak = this._peaks[index];

		if (db > peak)
			peak = db;
		else
			peak = Math.max(AudioMeter.FLOOR_DB, peak - AudioMeter.PEAK_DECAY_DB_PER_SEC * dt);

		this._peaks[index] = peak;

		const peakPct = this._toPct(peak);
		bar.peak.style.left = peakPct + '%';
		bar.peak.style.opacity = peakPct > 0 ? '1' : '0';

		// Numeric readout
		bar.value.textContent = (!isFinite(db) || db <= AudioMeter.FLOOR_DB) ? '-∞' : Math.round(db).toString();
	}

	// Map a dBFS value to a 0..100 fill percentage
	_toPct(db)
	{
		if (!isFinite(db))
			return db > 0 ? 100 : 0; // +Infinity (shouldn't happen) -> full; -Infinity -> empty

		const p = (db - AudioMeter.FLOOR_DB) / (0 - AudioMeter.FLOOR_DB) * 100;

		return Math.max(0, Math.min(100, p));
	}
}

if (!customElements.get('audio-meter'))
	customElements.define('audio-meter', AudioMeter);

export { AudioMeter };
