import type {Gallery_Client_Performance} from "../protocol/gallery_types";
interface Timed_Sample {timestamp: number; value: number;}
interface Statistics {average: number; deviation: number; p50: number; p95: number; p99: number;}
function statistics(values: number[]): Statistics {if (!values.length) return {average: 0, deviation: 0, p50: 0, p95: 0, p99: 0}; const sorted = values.toSorted((left, right) => left - right); const average = sorted.reduce((sum, value) => sum + value, 0) / sorted.length; const deviation = Math.sqrt(sorted.reduce((sum, value) => sum + (value - average) ** 2, 0) / sorted.length); const percentile = (ratio: number) => sorted[Math.max(0, Math.ceil(ratio * sorted.length) - 1)]; return {average, deviation, p50: percentile(.5), p95: percentile(.95), p99: percentile(.99)};}
function trim(samples: Timed_Sample[], after: number): void {while (samples.length && samples[0].timestamp < after) samples.shift();}
function rate(samples: Timed_Sample[], now: number): number {trim(samples, now - 1000); return samples.length < 2 ? 0 : (samples.length - 1) * 1000 / Math.max(1, samples.at(-1)!.timestamp - samples[0].timestamp);}

export class Client_Performance {
    private transport_samples: Timed_Sample[] = []; private presentation_samples: Timed_Sample[] = []; private display_samples: Timed_Sample[] = []; private round_trip_samples: Timed_Sample[] = [];
    private last_animation_frame = 0; private last_pixel_receive = 0; private last_pixel_change = 0; private previous_signature: number | null = null;
    private changed_frames = 0; private duplicate_frames = 0; private overwritten_frames = 0; private timeout_count = 0;
    record_animation_frame(now: number): void {if (this.last_animation_frame) this.display_samples.push({timestamp: now, value: Math.max(0, now - this.last_animation_frame)}); this.last_animation_frame = now; trim(this.display_samples, now - 10_000);}
    record_pixel(now: number, signature: number, overwritten: boolean): void {this.transport_samples.push({timestamp: now, value: 0}); this.last_pixel_receive = now; if (signature !== this.previous_signature) {this.changed_frames++; this.last_pixel_change = now;} else this.duplicate_frames++; this.previous_signature = signature; if (overwritten) this.overwritten_frames++;}
    record_presentation(now: number): void {this.presentation_samples.push({timestamp: now, value: 0});}
    record_round_trip(now: number, value: number): void {this.round_trip_samples.push({timestamp: now, value}); trim(this.round_trip_samples, now - 10_000);}
    record_timeout(): void {this.timeout_count++;}
    reset(): void {this.transport_samples=[]; this.presentation_samples=[]; this.display_samples=[]; this.round_trip_samples=[]; this.last_animation_frame=0; this.last_pixel_receive=0; this.last_pixel_change=0; this.previous_signature=null; this.changed_frames=0; this.duplicate_frames=0; this.overwritten_frames=0; this.timeout_count=0;}
    snapshot(now: number, websocket_buffered_bytes: number): Gallery_Client_Performance {trim(this.round_trip_samples, now - 10_000); trim(this.display_samples, now - 10_000); const display=statistics(this.display_samples.map(item=>item.value)); const round_trip=statistics(this.round_trip_samples.map(item=>item.value)); return {transport_fps:rate(this.transport_samples,now),presentation_fps:rate(this.presentation_samples,now),display_interval_latest_ms:this.display_samples.at(-1)?.value??0,display_interval_ms:display.p50,display_interval_average_ms:display.average,display_interval_p95_ms:display.p95,display_interval_p99_ms:display.p99,display_interval_deviation_ms:display.deviation,frame_round_trip_ms:this.round_trip_samples.at(-1)?.value??0,frame_round_trip_average_ms:round_trip.average,frame_round_trip_p95_ms:round_trip.p95,frame_round_trip_p99_ms:round_trip.p99,frame_round_trip_deviation_ms:round_trip.deviation,changed_pixel_frames:this.changed_frames,duplicate_pixel_frames:this.duplicate_frames,overwritten_pixel_frames:this.overwritten_frames,frame_request_timeout_count:this.timeout_count,last_pixel_receive_age_ms:this.last_pixel_receive?Math.max(0,now-this.last_pixel_receive):0,last_pixel_change_age_ms:this.last_pixel_change?Math.max(0,now-this.last_pixel_change):0,websocket_buffered_bytes};}
}
