package sointu

import (
	"errors"
	"fmt"
	"math"
)

// Patch is simply a list of instruments used in a song
type Patch []Instrument

// Copy makes a deep copy of a Patch.
func (p Patch) Copy() Patch {
	instruments := make([]Instrument, len(p))
	for i, instr := range p {
		instruments[i] = instr.Copy()
	}
	return instruments
}

// NumVoices returns the total number of voices used in the patch; summing the
// voices of every instrument
func (p Patch) NumVoices() int {
	ret := 0
	for _, i := range p {
		ret += i.NumVoices
	}
	return ret
}

// NumDelayLines return the total number of delay lines used in the patch;
// summing the number of delay lines of every delay unit in every instrument
func (p Patch) NumDelayLines() int {
	total := 0
	for _, instr := range p {
		for _, unit := range instr.Units {
			if unit.Type == "delay" {
				total += len(unit.VarArgs) * instr.NumVoices
			}
		}
	}
	return total
}

// NumSyns return the total number of sync outputs used in the patch; summing
// the number of sync outputs of every sync unit in every instrument
func (p Patch) NumSyncs() int {
	total := 0
	for _, instr := range p {
		for _, unit := range instr.Units {
			if unit.Type == "sync" {
				total += instr.NumVoices
			}
		}
	}
	return total
}

// FirstVoiceForInstrument returns the index of the first voice of given
// instrument. For example, if the Patch has three instruments (0, 1 and 2),
// with 1, 3, 2 voices, respectively, then FirstVoiceForInstrument(0) returns 0,
// FirstVoiceForInstrument(1) returns 1 and FirstVoiceForInstrument(2) returns
// 4. Essentially computes just the cumulative sum.
func (p Patch) FirstVoiceForInstrument(instrIndex int) int {
	ret := 0
	for _, t := range p[:instrIndex] {
		ret += t.NumVoices
	}
	return ret
}

// InstrumentForVoice returns the instrument number for the given voice index.
// For example, if the Patch has three instruments (0, 1 and 2), with 1, 3, 2
// voices, respectively, then InstrumentForVoice(0) returns 0,
// InstrumentForVoice(1) returns 1 and InstrumentForVoice(3) returns 1.
func (p Patch) InstrumentForVoice(voice int) (int, error) {
	if voice < 0 {
		return 0, errors.New("voice cannot be negative")
	}
	for i, instr := range p {
		if voice < instr.NumVoices {
			return i, nil
		}
		voice -= instr.NumVoices
	}
	return 0, errors.New("voice number is beyond the total voices of an instrument")
}

// FindSendTarget searches the instrument number and unit index for a unit with
// the given id. Two units should never have the same id, but if they do, then
// the first match is returned. Id 0 is interpreted as "no id", thus searching
// for id 0 returns an error. Error is also returned if the searched id is not
// found.
func (p Patch) FindSendTarget(id int) (int, int, error) {
	if id == 0 {
		return 0, 0, errors.New("send targets unit id 0")
	}
	for i, instr := range p {
		for u, unit := range instr.Units {
			if unit.ID == id {
				return i, u, nil
			}
		}
	}
	return 0, 0, fmt.Errorf("send targets an unit with id %v, could not find a unit with such an ID in the patch", id)
}

// ParamHintString returns a human readable string representing the current
// value of a given unit parameter.
func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
	if instrIndex < 0 || instrIndex >= len(p) {
		return ""
	}
	instr := p[instrIndex]
	if unitIndex < 0 || unitIndex >= len(instr.Units) {
		return ""
	}
	unit := instr.Units[unitIndex]
	value := unit.Parameters[param]
	switch unit.Type {
	case "envelope":
		switch param {
		case "attack":
			return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100)
		case "decay":
			return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * (1 - float64(unit.Parameters["sustain"])/128))
		case "release":
			return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * float64(unit.Parameters["sustain"]) / 128)
		}
	case "oscillator":
		switch param {
		case "type":
			switch value {
			case Sine:
				return "Sine"
			case Trisaw:
				return "Trisaw"
			case Pulse:
				return "Pulse"
			case Gate:
				return "Gate"
			case Sample:
				return "Sample"
			default:
				return "Unknown"
			}
		case "transpose":
			relvalue := value - 64
			octaves := relvalue / 12
			semitones := relvalue % 12
			if octaves != 0 {
				return fmt.Sprintf("%v oct, %v st", octaves, semitones)
			}
			return fmt.Sprintf("%v st", semitones)
		case "detune":
			return fmt.Sprintf("%v st", float32(value-64)/64.0)
		}
	case "compressor":
		switch param {
		case "attack":
			fallthrough
		case "release":
			alpha := math.Pow(2, -24*float64(value)/128) // alpha is the "smoothing factor" of first order low pass iir
			sec := -1 / (44100 * math.Log(1-alpha))      // from smoothing factor to time constant, https://en.wikipedia.org/wiki/Exponential_smoothing
			return engineeringTime(sec)
		case "ratio":
			return fmt.Sprintf("1 : %.3f", 1-float64(value)/128)
		}
	case "send":
		switch param {
		case "voice":
			if value == 0 {
				return "auto"
			}
			return fmt.Sprintf("%v", value)
		case "target":
			instrIndex, unitIndex, err := p.FindSendTarget(unit.Parameters["target"])
			if err != nil {
				return "invalid target"
			}
			instr := p[instrIndex]
			unit := instr.Units[unitIndex]
			return fmt.Sprintf("%v / %v%v", instr.Name, unit.Type, unitIndex)
		case "port":
			instrIndex, unitIndex, err := p.FindSendTarget(unit.Parameters["target"])
			if err != nil {
				return fmt.Sprintf("%v ???", value)
			}
			portList := Ports[p[instrIndex].Units[unitIndex].Type]
			if value < 0 || value >= len(portList) {
				return fmt.Sprintf("%v ???", value)
			}
			return fmt.Sprintf(portList[value])
		}
	case "delay":
		switch param {
		case "notetracking":
			switch value {
			case 0:
				return "fixed"
			case 1:
				return "tracks pitch"
			case 2:
				return "tracks BPM"
			}
		}
	}
	return ""
}

func engineeringTime(sec float64) string {
	if sec < 1e-3 {
		return fmt.Sprintf("%.2f us", sec*1e6)
	} else if sec < 1 {
		return fmt.Sprintf("%.2f ms", sec*1e3)
	}
	return fmt.Sprintf("%.2f s", sec)
}