package tracker

import (
	"fmt"
	"github.com/vsariola/sointu"
	"iter"
	"slices"
)

/*
	from modelData we can derive useful information that can be cached for performance
	or easy access, because of nested iterations over the score or patch data.
	i.e. this needs to update when the model changes, and only then.
*/

type (
	derivedModelData struct {
		// map Unit by ID, other entities by their respective index
		forUnit    map[int]derivedForUnit
		forTrack   []derivedForTrack
		forPattern []derivedForPattern
	}

	derivedForUnit struct {
		unit            sointu.Unit
		instrument      sointu.Instrument
		instrumentIndex int
		unitIndex       int

		// map param by Name
		forParameter map[string]derivedForParameter
	}

	derivedForParameter struct {
		sendTooltip string
		sendSources []sendSourceData
	}

	sendSourceData struct {
		unitId          int
		paramName       string
		amount          int
		instrumentIndex int
		instrumentName  string
	}

	derivedForTrack struct {
		instrumentRange          []int
		tracksWithSameInstrument []int
		title                    string
	}

	derivedForPattern struct {
		useCount []int
	}
)

// public access functions

func (m *Model) InstrumentForUnit(id int) (sointu.Instrument, int, bool) {
	forUnit, ok := m.derived.forUnit[id]
	if !ok {
		return sointu.Instrument{}, -1, false
	}
	return forUnit.instrument, forUnit.instrumentIndex, true
}

func (m *Model) UnitInfo(id int) (instrName string, units []sointu.Unit, unitIndex int, ok bool) {
	fu, ok := m.derived.forUnit[id]
	return fu.instrument.Name, fu.instrument.Units, fu.unitIndex, ok
}

func (m *Model) UnitHintInfo(id int) (instrIndex int, unitType string, ok bool) {
	fu, ok := m.derived.forUnit[id]
	return fu.instrumentIndex, fu.unit.Type, ok
}

func (m *Model) ParameterInfo(unitId int, paramName string) (isSendTarget bool, tooltip string, exists bool) {
	du, ok1 := m.derived.forUnit[unitId]
	if !ok1 {
		return false, "", false
	}
	dp, ok2 := du.forParameter[paramName]
	if !ok2 {
		return false, "", false
	}
	return len(dp.sendSources) > 0, dp.sendTooltip, true
}

func (m *Model) TrackTitle(index int) string {
	if index < 0 || index > len(m.derived.forTrack) {
		return ""
	}
	return m.derived.forTrack[index].title
}

func (m *Model) PatternUseCount(index int) []int {
	if index < 0 || index > len(m.derived.forPattern) {
		return nil
	}
	return m.derived.forPattern[index].useCount
}

func (m *Model) PatternUnique(t, p int) bool {
	if t < 0 || t > len(m.derived.forPattern) {
		return false
	}
	forPattern := m.derived.forPattern[t]
	if p < 0 || p >= len(forPattern.useCount) {
		return false
	}
	return forPattern.useCount[p] == 1
}

// public getters with further model information

func (m *Model) TracksWithSameInstrumentAsCurrent() []int {
	currentTrack := m.d.Cursor.Track
	if currentTrack > len(m.derived.forTrack) {
		return nil
	}
	return m.derived.forTrack[currentTrack].tracksWithSameInstrument
}

func (m *Model) CountNextTracksForCurrentInstrument() int {
	currentTrack := m.d.Cursor.Track
	count := 0
	for t := range m.TracksWithSameInstrumentAsCurrent() {
		if t > currentTrack {
			count++
		}
	}
	return count
}

// init / update methods

func (m *Model) initDerivedData() {
	m.derived = derivedModelData{
		forUnit:    make(map[int]derivedForUnit),
		forTrack:   make([]derivedForTrack, 0),
		forPattern: make([]derivedForPattern, 0),
	}
	m.updateDerivedScoreData()
	m.updateDerivedPatchData()
}

func (m *Model) updateDerivedScoreData() {
	m.derived.forTrack = m.derived.forTrack[:0]
	m.derived.forPattern = m.derived.forPattern[:0]
	for index, track := range m.d.Song.Score.Tracks {
		firstInstr, lastInstr, _ := m.instrumentRangeFor(index)
		m.derived.forTrack = append(
			m.derived.forTrack,
			derivedForTrack{
				instrumentRange:          []int{firstInstr, lastInstr},
				tracksWithSameInstrument: slices.Collect(m.tracksWithSameInstrument(index)),
				title:                    m.buildTrackTitle(index),
			},
		)
		m.derived.forPattern = append(
			m.derived.forPattern,
			derivedForPattern{
				useCount: m.calcPatternUseCounts(track),
			},
		)
	}
}

func (m *Model) updateDerivedPatchData() {
	clear(m.derived.forUnit)
	for i, instr := range m.d.Song.Patch {
		for u, unit := range instr.Units {
			m.derived.forUnit[unit.ID] = derivedForUnit{
				unit:            unit,
				unitIndex:       u,
				instrument:      instr,
				instrumentIndex: i,
				forParameter:    make(map[string]derivedForParameter),
			}
			m.updateDerivedParameterData(unit)
		}
	}
}

func (m *Model) updateDerivedParameterData(unit sointu.Unit) {
	fu, _ := m.derived.forUnit[unit.ID]
	for name := range fu.unit.Parameters {
		sendSources := slices.Collect(m.collectSendSources(unit, name))
		fu.forParameter[name] = derivedForParameter{
			sendSources: sendSources,
			sendTooltip: m.buildSendTargetTooltip(fu.instrumentIndex, sendSources),
		}
	}
}

// internals...

func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[sendSourceData] {
	return func(yield func(sendSourceData) bool) {
		for i, instr := range m.d.Song.Patch {
			for _, u := range instr.Units {
				if u.Type != "send" {
					continue
				}
				targetId, ok := u.Parameters["target"]
				if !ok || targetId != unit.ID {
					continue
				}
				port := u.Parameters["port"]
				unitParam, ok := sointu.FindParamForModulationPort(unit.Type, port)
				if !ok || unitParam.Name != paramName {
					continue
				}
				sourceData := sendSourceData{
					unitId:          u.ID,
					paramName:       paramName,
					instrumentIndex: i,
					instrumentName:  instr.Name,
					amount:          u.Parameters["amount"],
				}
				if !yield(sourceData) {
					return
				}
			}
		}
	}
}

func (m *Model) buildSendTargetTooltip(ownInstrIndex int, sendSources []sendSourceData) string {
	if len(sendSources) == 0 {
		return ""
	}
	amounts := ""
	for _, sendSource := range sendSources {
		sourceInfo := ""
		if sendSource.instrumentIndex != ownInstrIndex {
			sourceInfo = fmt.Sprintf(" from \"%s\"", sendSource.instrumentName)
		}
		if amounts == "" {
			amounts = fmt.Sprintf("x %d%s", sendSource.amount, sourceInfo)
		} else {
			amounts = fmt.Sprintf("%s, x %d%s", amounts, sendSource.amount, sourceInfo)
		}
	}
	count := "1 send"
	if len(sendSources) > 1 {
		count = fmt.Sprintf("%d sends", len(sendSources))
	}
	return fmt.Sprintf("%s [%s]", count, amounts)
}

func (m *Model) instrumentRangeFor(trackIndex int) (int, int, error) {
	track := m.d.Song.Score.Tracks[trackIndex]
	if track.NumVoices <= 0 {
		return 0, 0, fmt.Errorf("track %d has no voices", trackIndex)
	}
	firstVoice := m.d.Song.Score.FirstVoiceForTrack(trackIndex)
	lastVoice := firstVoice + track.NumVoices - 1
	firstIndex, err1 := m.d.Song.Patch.InstrumentForVoice(firstVoice)
	if err1 != nil {
		return trackIndex, trackIndex, err1
	}
	lastIndex, err2 := m.d.Song.Patch.InstrumentForVoice(lastVoice)
	if err2 != nil {
		return trackIndex, trackIndex, err2
	}
	return firstIndex, lastIndex, nil
}

func (m *Model) buildTrackTitle(x int) (title string) {
	title = "?"
	if x < 0 || x >= len(m.d.Song.Score.Tracks) {
		return
	}
	firstIndex, lastIndex, err := m.instrumentRangeFor(x)
	if err != nil {
		return
	}
	switch diff := lastIndex - firstIndex; diff {
	case 0:
		title = m.d.Song.Patch[firstIndex].Name
	default:
		n1 := m.d.Song.Patch[firstIndex].Name
		n2 := m.d.Song.Patch[firstIndex+1].Name
		if len(n1) > 0 {
			n1 = string(n1[0])
		} else {
			n1 = "?"
		}
		if len(n2) > 0 {
			n2 = string(n2[0])
		} else {
			n2 = "?"
		}
		if diff > 1 {
			title = n1 + "/" + n2 + "..."
		} else {
			title = n1 + "/" + n2
		}
	}
	return
}

func (m *Model) instrumentForTrack(trackIndex int) (int, bool) {
	voiceIndex := m.d.Song.Score.FirstVoiceForTrack(trackIndex)
	instrument, err := m.d.Song.Patch.InstrumentForVoice(voiceIndex)
	return instrument, err == nil
}

func (m *Model) tracksWithSameInstrument(trackIndex int) iter.Seq[int] {
	return func(yield func(int) bool) {

		currentInstrument, currentExists := m.instrumentForTrack(trackIndex)
		if !currentExists {
			return
		}

		for i := 0; i < len(m.d.Song.Score.Tracks); i++ {
			instrument, exists := m.instrumentForTrack(i)
			if !exists {
				return
			}
			if instrument != currentInstrument {
				continue
			}
			if !yield(i) {
				return
			}
		}
	}
}

func (m *Model) calcPatternUseCounts(track sointu.Track) []int {
	result := make([]int, len(m.d.Song.Score.Tracks))
	for j, _ := range result {
		result[j] = 0
	}
	for j := 0; j < m.d.Song.Score.Length; j++ {
		if j >= len(track.Order) {
			break
		}
		p := track.Order[j]
		for len(result) <= p {
			result = append(result, 0)
		}
		if p < 0 {
			continue
		}
		result[p]++
	}
	return result
}
