package tracker

import (
	"fmt"
	"math"
	"slices"
	"strconv"

	"github.com/vsariola/sointu"
	"github.com/vsariola/sointu/vm"
)

type (
	Parameter interface {
		IntData
		Type() ParameterType
		Name() string
		Hint() string
		LargeStep() int
		Reset()
	}

	parameter struct {
		m    *Model
		unit *sointu.Unit
	}

	NamedParameter struct {
		parameter
		up *sointu.UnitParameter
	}

	DelayTimeParameter struct {
		parameter
		index int
	}

	DelayLinesParameter struct{ parameter }
	GmDlsEntryParameter struct{ parameter }
	ReverbParameter     struct{ parameter }

	Params Model

	ParamYieldFunc func(Parameter)

	ParameterType int
)

const (
	IntegerParameter ParameterType = iota
	BoolParameter
	IDParameter
)

// Model methods

func (m *Model) Params() *Params { return (*Params)(m) }

// parameter methods

func (p parameter) change(kind string) func() {
	return p.m.change("Parameter."+kind, PatchChange, MinorChange)
}

// ParamList

func (pl *Params) List() List             { return List{pl} }
func (pl *Params) Selected() int          { return pl.d.ParamIndex }
func (pl *Params) Selected2() int         { return pl.Selected() }
func (pl *Params) SetSelected(value int)  { pl.d.ParamIndex = intMax(intMin(value, pl.Count()-1), 0) }
func (pl *Params) SetSelected2(value int) {}
func (pl *Params) cancel()                { (*Model)(pl).changeCancel = true }

func (pl *Params) change(n string, severity ChangeSeverity) func() {
	return (*Model)(pl).change("ParamList."+n, PatchChange, severity)
}

func (pl *Params) Count() int {
	count := 0
	pl.Iterate(func(p Parameter) {
		count++
	})
	return count
}

func (pl *Params) SelectedItem() (ret Parameter) {
	index := pl.Selected()
	pl.Iterate(func(param Parameter) {
		if index == 0 {
			ret = param
		}
		index--
	})
	return
}

func (pl *Params) Iterate(yield ParamYieldFunc) {
	if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
		return
	}
	if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) {
		return
	}
	unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex]
	unitType, ok := sointu.UnitTypes[unit.Type]
	if !ok {
		return
	}
	for i := range unitType {
		if !unitType[i].CanSet {
			continue
		}
		if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 {
			break // don't show the sample related params unless necessary
		}
		yield(NamedParameter{
			parameter: parameter{m: (*Model)(pl), unit: unit},
			up:        &unitType[i],
		})
	}
	if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
		yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
	}
	switch {
	case unit.Type == "delay":
		if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 {
			unit.VarArgs = append(unit.VarArgs, 1)
		}
		yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
		yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
		for i := range unit.VarArgs {
			yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i})
		}
	}
}

// NamedParameter

func (p NamedParameter) Name() string       { return p.up.Name }
func (p NamedParameter) Range() intRange    { return intRange{Min: p.up.MinValue, Max: p.up.MaxValue} }
func (p NamedParameter) Value() int         { return p.unit.Parameters[p.up.Name] }
func (p NamedParameter) setValue(value int) { p.unit.Parameters[p.up.Name] = value }

func (p NamedParameter) Reset() {
	v, ok := defaultUnits[p.unit.Type].Parameters[p.up.Name]
	if !ok || p.unit.Parameters[p.up.Name] == v {
		return
	}
	defer p.parameter.change("Reset")()
	p.unit.Parameters[p.up.Name] = v
}

func (p NamedParameter) Type() ParameterType {
	if p.unit.Type == "send" && p.up.Name == "target" {
		return IDParameter
	}
	if p.up.MinValue == 0 && p.up.MaxValue == 1 {
		return BoolParameter
	}
	return IntegerParameter
}

func (p NamedParameter) Hint() string {
	val := p.Value()
	text := p.m.d.Song.Patch.ParamHintString(p.m.d.InstrIndex, p.m.d.UnitIndex, p.up.Name)
	if text != "" {
		text = fmt.Sprintf("%v / %v", val, text)
	} else {
		text = strconv.Itoa(val)
	}
	return text
}

func (p NamedParameter) LargeStep() int {
	if p.up.Name == "transpose" {
		return 12
	}
	return 16
}

// GmDlsEntryParameter

func (p GmDlsEntryParameter) Name() string        { return "sample" }
func (p GmDlsEntryParameter) Type() ParameterType { return IntegerParameter }
func (p GmDlsEntryParameter) Range() intRange     { return intRange{Min: 0, Max: len(GmDlsEntries)} }
func (p GmDlsEntryParameter) LargeStep() int      { return 16 }
func (p GmDlsEntryParameter) Reset()              { return }

func (p GmDlsEntryParameter) Value() int {
	key := vm.SampleOffset{Start: uint32(p.unit.Parameters["samplestart"]), LoopStart: uint16(p.unit.Parameters["loopstart"]), LoopLength: uint16(p.unit.Parameters["looplength"])}
	if v, ok := gmDlsEntryMap[key]; ok {
		return v + 1
	}
	return 0
}

func (p GmDlsEntryParameter) setValue(v int) {
	if v < 1 || v > len(GmDlsEntries) {
		return
	}
	e := GmDlsEntries[v-1]
	p.unit.Parameters["samplestart"] = e.Start
	p.unit.Parameters["loopstart"] = e.LoopStart
	p.unit.Parameters["looplength"] = e.LoopLength
	p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose
}

func (p GmDlsEntryParameter) Hint() string {
	if v := p.Value(); v > 0 {
		return fmt.Sprintf("%v / %v", v, GmDlsEntries[v-1].Name)
	}
	return "0 / custom"
}

// DelayTimeParameter

func (p DelayTimeParameter) Name() string        { return "delaytime" }
func (p DelayTimeParameter) Type() ParameterType { return IntegerParameter }
func (p DelayTimeParameter) LargeStep() int      { return 16 }
func (p DelayTimeParameter) Reset()              { return }

func (p DelayTimeParameter) Value() int {
	if p.index < 0 || p.index >= len(p.unit.VarArgs) {
		return 1
	}
	return p.unit.VarArgs[p.index]
}

func (p DelayTimeParameter) setValue(v int) {
	p.unit.VarArgs[p.index] = v
}

func (p DelayTimeParameter) Range() intRange {
	if p.unit.Parameters["notetracking"] == 2 {
		return intRange{Min: 1, Max: 576}
	}
	return intRange{Min: 1, Max: 65535}
}

func (p DelayTimeParameter) Hint() string {
	val := p.Value()
	var text string
	switch p.unit.Parameters["notetracking"] {
	default:
	case 0:
		text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(p.m.d.Song.SamplesPerRow()))
	case 1:
		relPitch := float64(val) / 10787
		semitones := -math.Log2(relPitch) * 12
		text = fmt.Sprintf("%v / %.3f st", val, semitones)
	case 2:
		k := 0
		v := val
		for v&1 == 0 { // divide val by 2 until it is odd
			v >>= 1
			k++
		}
		switch v {
		case 1:
			if k <= 7 {
				text = fmt.Sprintf(" (1/%d triplet)", 1<<(7-k))
			}
		case 3:
			if k <= 6 {
				text = fmt.Sprintf(" (1/%d)", 1<<(6-k))
			}
			break
		case 9:
			if k <= 5 {
				text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k))
			}
		}
		text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text)
	}
	if p.unit.Parameters["stereo"] == 1 {
		if p.index < len(p.unit.VarArgs)/2 {
			text += " R"
		} else {
			text += " L"
		}
	}
	return text
}

// DelayLinesParameter

func (p DelayLinesParameter) Name() string        { return "delaylines" }
func (p DelayLinesParameter) Type() ParameterType { return IntegerParameter }
func (p DelayLinesParameter) Range() intRange     { return intRange{Min: 1, Max: 32} }
func (p DelayLinesParameter) LargeStep() int      { return 4 }
func (p DelayLinesParameter) Reset()              { return }
func (p DelayLinesParameter) Hint() string        { return strconv.Itoa(p.Value()) }

func (p DelayLinesParameter) Value() int {
	val := len(p.unit.VarArgs)
	if p.unit.Parameters["stereo"] == 1 {
		val /= 2
	}
	return val
}

func (p DelayLinesParameter) setValue(v int) {
	targetLines := v
	if p.unit.Parameters["stereo"] == 1 {
		targetLines *= 2
	}
	for len(p.unit.VarArgs) < targetLines {
		p.unit.VarArgs = append(p.unit.VarArgs, 1)
	}
	p.unit.VarArgs = p.unit.VarArgs[:targetLines]
}

// ReverbParameter

func (p ReverbParameter) Name() string        { return "reverb" }
func (p ReverbParameter) Type() ParameterType { return IntegerParameter }
func (p ReverbParameter) Range() intRange     { return intRange{Min: 0, Max: len(reverbs)} }
func (p ReverbParameter) LargeStep() int      { return 1 }
func (p ReverbParameter) Reset()              { return }

func (p ReverbParameter) Value() int {
	i := slices.IndexFunc(reverbs, func(d delayPreset) bool {
		return d.stereo == p.unit.Parameters["stereo"] && p.unit.Parameters["notetracking"] == 0 && slices.Equal(d.varArgs, p.unit.VarArgs)
	})
	return i + 1
}

func (p ReverbParameter) setValue(v int) {
	if v < 1 || v > len(reverbs) {
		return
	}
	entry := reverbs[v-1]
	p.unit.Parameters["stereo"] = entry.stereo
	p.unit.Parameters["notetracking"] = 0
	p.unit.VarArgs = make([]int, len(entry.varArgs))
	copy(p.unit.VarArgs, entry.varArgs)
}

func (p ReverbParameter) Hint() string {
	i := p.Value()
	if i > 0 {
		return fmt.Sprintf("%v / %v", i, reverbs[i-1].name)
	}
	return "0 / custom"
}
