// Package ding renders the AfterTouch "ding" signature sound — a
// two-chirp tone derived from the braille letters S and T that
// make up the AfterTouch logo. Used as the Health-tab test
// playback target: pushed to a speaker as a custom-radio
// ContentItem so operators can confirm a freshly migrated speaker
// emits audio without depending on TuneIn or any external service.
//
// Mapping:
//
//	Braille S = ⠎ = dots 2, 3, 4
//	Braille T = ⠞ = dots 2, 3, 4, 5
//
//	Dot positions in the 6-dot grid:
//	   1   4
//	   2   5
//	   3   6
//
//	Columns → stereo channels (left=1,2,3 / right=4,5,6).
//	Rows    → pitches: top=PitchHigh, mid=PitchMid, bottom=PitchLow.
//
// So S (dots 2,3,4) renders as L=PitchMid+PitchLow, R=PitchHigh,
// and T (dots 2,3,4,5) adds R=PitchMid on top of S.
//
// Render(opts) returns a self-contained 16-bit stereo PCM WAV.
// Default options produce a ~600 ms / 52 KB clip; callers can
// override any subset and let the rest fall back to defaults
// (see DefaultOptions).
package ding

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"math"
)

// Options controls the synthesis. A zero-valued Options struct
// is *not* usable directly; the WithDefaults method fills in
// sensible numbers for unset fields so callers can supply only
// the parameters they want to override.
type Options struct {
	SampleRate int // Hz. Default 22050.

	PitchHigh float64 // Hz, top row (A5=880).
	PitchMid  float64 // Hz, middle row (E5=659.2551).
	PitchLow  float64 // Hz, bottom row (A4=440).

	ChirpDuration   float64 // seconds per chirp. Default 0.25.
	GapDuration     float64 // seconds between chirps. Default 0.10.
	AttackDuration  float64 // seconds of fade-in per chirp. Default 0.020.
	ReleaseDuration float64 // seconds of fade-out per chirp. Default 0.060.

	Peak float64 // final-mix headroom; 0 < Peak <= 1.0. Default 0.85.

	// Repeat is the total number of times the complete ding is played.
	// Speakers need a moment to start buffering after receiving a
	// ContentItem, so the first repetition may be missed; later ones
	// will be heard. Default 3.
	Repeat int

	// RepeatGapDuration is the silence inserted between successive
	// repetitions, in seconds. Default 0.40.
	RepeatGapDuration float64
}

// DefaultOptions returns the canonical option set used by the
// runtime handler when no overrides are supplied.
func DefaultOptions() Options {
	return Options{
		SampleRate:        22050,
		PitchHigh:         880.00,
		PitchMid:          659.2551,
		PitchLow:          440.00,
		ChirpDuration:     0.25,
		GapDuration:       0.10,
		AttackDuration:    0.020,
		ReleaseDuration:   0.060,
		Peak:              0.85,
		Repeat:            3,
		RepeatGapDuration: 0.40,
	}
}

// WithDefaults returns a copy of o with any zero-valued fields
// filled in from DefaultOptions. Lets callers write
//
//	ding.Options{PitchHigh: 1000}.WithDefaults()
//
// instead of restating every field.
func (o Options) WithDefaults() Options {
	d := DefaultOptions()

	if o.SampleRate <= 0 || o.SampleRate > int(maxSampleRate) {
		o.SampleRate = d.SampleRate
	}

	if o.PitchHigh <= 0 {
		o.PitchHigh = d.PitchHigh
	}

	if o.PitchMid <= 0 {
		o.PitchMid = d.PitchMid
	}

	if o.PitchLow <= 0 {
		o.PitchLow = d.PitchLow
	}

	if o.ChirpDuration <= 0 {
		o.ChirpDuration = d.ChirpDuration
	}

	if o.GapDuration <= 0 {
		o.GapDuration = d.GapDuration
	}

	if o.AttackDuration <= 0 {
		o.AttackDuration = d.AttackDuration
	}

	if o.ReleaseDuration <= 0 {
		o.ReleaseDuration = d.ReleaseDuration
	}

	if o.Peak <= 0 || o.Peak > 1.0 {
		o.Peak = d.Peak
	}

	if o.Repeat <= 0 {
		o.Repeat = d.Repeat
	}

	if o.RepeatGapDuration <= 0 {
		o.RepeatGapDuration = d.RepeatGapDuration
	}

	return o
}

// Render synthesises the ding using opts (after defaulting) and
// returns a self-contained 16-bit PCM WAV file.
func Render(opts Options) []byte {
	opts = opts.WithDefaults()

	voicesS := []voice{
		{freq: opts.PitchMid, channel: 0},
		{freq: opts.PitchLow, channel: 0},
		{freq: opts.PitchHigh, channel: 1},
	}
	voicesT := []voice{
		{freq: opts.PitchMid, channel: 0},
		{freq: opts.PitchLow, channel: 0},
		{freq: opts.PitchHigh, channel: 1},
		{freq: opts.PitchMid, channel: 1},
	}

	chirpN := int(math.Round(float64(opts.SampleRate) * opts.ChirpDuration))
	gapN := int(math.Round(float64(opts.SampleRate) * opts.GapDuration))
	attackN := int(math.Round(float64(opts.SampleRate) * opts.AttackDuration))
	releaseN := int(math.Round(float64(opts.SampleRate) * opts.ReleaseDuration))

	samplesPerChannel := chirpN*2 + gapN
	left := make([]float64, samplesPerChannel)
	right := make([]float64, samplesPerChannel)

	renderChirp(left, right, 0, chirpN, attackN, releaseN, voicesS, opts.SampleRate)
	renderChirp(left, right, chirpN+gapN, chirpN, attackN, releaseN, voicesT, opts.SampleRate)

	// Repeat: append silence + a copy of the base audio for each
	// additional repetition. Speakers need a moment to start buffering
	// after receiving a ContentItem; repeating ensures at least one
	// instance is audible even if the first is missed.
	if opts.Repeat > 1 {
		repeatGapN := int(math.Round(float64(opts.SampleRate) * opts.RepeatGapDuration))

		baseLeft := append([]float64{}, left...)
		baseRight := append([]float64{}, right...)
		silence := make([]float64, repeatGapN)

		for i := 1; i < opts.Repeat; i++ {
			left = append(left, silence...)
			right = append(right, silence...)
			left = append(left, baseLeft...)
			right = append(right, baseRight...)
		}
	}

	normalise(left, right, opts.Peak)

	var buf bytes.Buffer

	// Defensive bound check: clamp before the conversion to
	// uint32 so even a buggy caller (or one that bypassed the
	// handler-side bound check on the query param) can't trigger
	// integer truncation in the WAV header fields.
	sampleRate32 := safeSampleRate(opts.SampleRate)

	_ = writeWAV(&buf, left, right, sampleRate32)

	return buf.Bytes()
}

type voice struct {
	freq    float64
	channel int
}

// renderChirp synthesises one chirp into the L/R buffers
// starting at offset, with a trapezoidal attack/sustain/release
// envelope.
func renderChirp(left, right []float64, offset, length, attackN, releaseN int, voices []voice, sampleRate int) {
	if attackN+releaseN > length {
		attackN = length / 3
		releaseN = length / 3
	}

	for i := 0; i < length; i++ {
		t := float64(i) / float64(sampleRate)
		env := 1.0

		switch {
		case i < attackN:
			env = float64(i) / float64(attackN)
		case i >= length-releaseN:
			remaining := length - i
			env = float64(remaining) / float64(releaseN)
		}

		for _, v := range voices {
			sample := math.Sin(2*math.Pi*v.freq*t) * env
			if v.channel == 0 {
				left[offset+i] += sample
			} else {
				right[offset+i] += sample
			}
		}
	}
}

// normalise scales L/R so the peak absolute value equals `peak`
// (≤ 1.0). Keeps the chord sum below clipping without hardcoding
// voice counts.
func normalise(left, right []float64, peak float64) {
	maxVal := 0.0

	for i := range left {
		if v := math.Abs(left[i]); v > maxVal {
			maxVal = v
		}

		if v := math.Abs(right[i]); v > maxVal {
			maxVal = v
		}
	}

	if maxVal == 0 {
		return
	}

	scale := peak / maxVal
	for i := range left {
		left[i] *= scale
		right[i] *= scale
	}
}

const (
	wavChannels = 2
	wavBitsPer  = 16
)

// maxSampleRate is the largest sample rate writeWAV will accept
// before clamping. Generous enough to allow studio-quality 192
// kHz; well below the uint32 ceiling the WAV header field can
// represent, and far below anything the byte-rate multiplication
// downstream could overflow.
const maxSampleRate uint32 = 192_000

// safeSampleRate converts the operator-supplied int sample rate
// into the uint32 the WAV header needs, clamping anything
// out-of-range to the default. Defence-in-depth: the
// handler-side sampleRateParam already rejects unreasonable
// inputs, but Render is exported so other callers (tests,
// scripts) could pass anything.
func safeSampleRate(in int) uint32 {
	if in <= 0 || in > int(maxSampleRate) {
		return uint32(DefaultOptions().SampleRate)
	}

	return uint32(in)
}

func writeWAV(w io.Writer, left, right []float64, sampleRate uint32) error {
	if len(left) != len(right) {
		return fmt.Errorf("channel length mismatch: %d vs %d", len(left), len(right))
	}

	samples := len(left)
	dataBytes := samples * wavChannels * (wavBitsPer / 8)
	totalRIFFSize := 4 + (8 + 16) + (8 + dataBytes)

	if _, err := w.Write([]byte("RIFF")); err != nil {
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, uint32(totalRIFFSize)); err != nil {
		return err
	}

	if _, err := w.Write([]byte("WAVE")); err != nil {
		return err
	}

	if _, err := w.Write([]byte("fmt ")); err != nil {
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, uint32(16)); err != nil {
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, uint16(1)); err != nil { // PCM
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, uint16(wavChannels)); err != nil {
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, sampleRate); err != nil {
		return err
	}

	byteRate := sampleRate * uint32(wavChannels) * uint32(wavBitsPer/8)
	if err := binary.Write(w, binary.LittleEndian, byteRate); err != nil {
		return err
	}

	blockAlign := uint16(wavChannels * (wavBitsPer / 8))
	if err := binary.Write(w, binary.LittleEndian, blockAlign); err != nil {
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, uint16(wavBitsPer)); err != nil {
		return err
	}

	if _, err := w.Write([]byte("data")); err != nil {
		return err
	}

	if err := binary.Write(w, binary.LittleEndian, uint32(dataBytes)); err != nil {
		return err
	}

	for i := 0; i < samples; i++ {
		if err := binary.Write(w, binary.LittleEndian, floatToInt16(left[i])); err != nil {
			return err
		}

		if err := binary.Write(w, binary.LittleEndian, floatToInt16(right[i])); err != nil {
			return err
		}
	}

	return nil
}

func floatToInt16(v float64) int16 {
	if v > 1.0 {
		v = 1.0
	} else if v < -1.0 {
		v = -1.0
	}

	return int16(math.Round(v * 32767))
}
