package vm

import (
	"sort"

	"github.com/vsariola/sointu"
)

type (
	// FeatureSet defines what opcodes / parameters are included in the compiled virtual machine
	// It is used by the compiler to decide how to encode opcodes
	FeatureSet interface {
		Opcode(unitType string) (int, bool)
		TransformCount(unitType string) int
		Instructions() []string
		InputNumber(unitType string, paramName string) int
		SupportsParamValue(unitType string, paramName string, value int) bool
		SupportsParamValueOtherThan(unitType string, paramName string, value int) bool
		SupportsModulation(unitType string, paramName string) bool
		SupportsPolyphony() bool
		SupportsGlobalSend() bool
	}

	// AllFeatures is used by the library compilation / bridging to configure a virtual machine
	// that supports every conceivable parameter, so it needs no members and just returns "true" to all
	// queries about what it supports. Contrast this NecessaryFeatures that only returns true if the patch
	// needs support for that feature
	AllFeatures struct {
	}

	// NecessaryFeatures returns true only if the patch actually needs the support for the feature
	NecessaryFeatures struct {
		opcodes            map[string]int
		instructions       []string
		supportsParamValue map[paramKey](map[int]bool)
		supportsModulation map[paramKey]bool
		globalSend         bool
		polyphony          bool
	}
)

type paramKey struct {
	Unit  string
	Param string
}

var allOpcodes map[string]int
var allInstructions []string
var allInputs map[paramKey]int
var allTransformCounts map[string]int

func init() {
	allInstructions = make([]string, len(sointu.UnitTypes))
	allOpcodes = map[string]int{}
	allTransformCounts = map[string]int{}
	allInputs = map[paramKey]int{}
	i := 0
	for k, v := range sointu.UnitTypes {
		inputCount := 0
		transformCount := 0
		for _, t := range v {
			if t.CanModulate {
				allInputs[paramKey{k, t.Name}] = inputCount
				inputCount++
			}
			if t.CanModulate && t.CanSet {
				transformCount++
			}
		}
		allInstructions[i] = k // Opcode 0 is reserved for instrument advance, so opcodes start from 1
		allTransformCounts[k] = transformCount
		i++
	}
	sort.Strings(allInstructions) // sort the opcodes to have predictable ordering, as maps don't guarantee the order the items
	for i, instruction := range allInstructions {
		allOpcodes[instruction] = (i + 1) * 2 // make a map to find out the opcode number based on the type
	}
}

func (_ AllFeatures) SupportsParamValue(unit string, paramName string, value int) bool {
	return true
}

func (_ AllFeatures) SupportsParamValueOtherThan(unit string, paramName string, value int) bool {
	return true
}

func (_ AllFeatures) SupportsModulation(unit string, port string) bool {
	return true
}

func (_ AllFeatures) SupportsPolyphony() bool {
	return true
}

func (_ AllFeatures) SupportsGlobalSend() bool {
	return true
}

func (_ AllFeatures) Opcode(unitType string) (int, bool) {
	code, ok := allOpcodes[unitType]
	return code, ok
}

func (_ AllFeatures) TransformCount(unitType string) int {
	return allTransformCounts[unitType]
}

func (_ AllFeatures) Instructions() []string {
	return allInstructions
}

func (_ AllFeatures) InputNumber(unitType string, paramName string) int {
	return allInputs[paramKey{unitType, paramName}]
}

func NecessaryFeaturesFor(patch sointu.Patch) NecessaryFeatures {
	features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
	for instrIndex, instrument := range patch {
		for _, unit := range instrument.Units {
			if unit.Type == "" || unit.Disabled {
				continue
			}
			if _, ok := features.opcodes[unit.Type]; !ok {
				features.instructions = append(features.instructions, unit.Type)
				features.opcodes[unit.Type] = len(features.instructions) * 2 // note that the first opcode gets value 1, as 0 is always reserved for advance
			}
			for _, paramType := range sointu.UnitTypes[unit.Type] {
				v := unit.Parameters[paramType.Name]
				key := paramKey{unit.Type, paramType.Name}
				if features.supportsParamValue[key] == nil {
					features.supportsParamValue[key] = map[int]bool{}
				}
				features.supportsParamValue[key][v] = true
			}
			if unit.Type == "send" {
				targetInstrIndex, targetUnitIndex, err := patch.FindUnit(unit.Parameters["target"])
				if err != nil {
					continue
				}
				targetUnit := patch[targetInstrIndex].Units[targetUnitIndex]
				portList := sointu.Ports[targetUnit.Type]
				portIndex := unit.Parameters["port"]
				if portIndex < 0 || portIndex >= len(portList) {
					continue
				}
				if targetInstrIndex != instrIndex || unit.Parameters["voice"] > 0 {
					features.globalSend = true
				}
				features.supportsModulation[paramKey{targetUnit.Type, portList[portIndex]}] = true
			}
		}
		if instrument.NumVoices > 1 {
			features.polyphony = true
		}
	}
	return features
}

func (n NecessaryFeatures) SupportsParamValue(unit string, paramName string, value int) bool {
	m, ok := n.supportsParamValue[paramKey{unit, paramName}]
	if !ok {
		return false
	}
	return m[value]
}

func (n NecessaryFeatures) SupportsParamValueOtherThan(unit string, paramName string, value int) bool {
	for paramValue := range n.supportsParamValue[paramKey{unit, paramName}] {
		if paramValue != value {
			return true
		}
	}
	return false
}

func (n NecessaryFeatures) SupportsModulation(unit string, param string) bool {
	return n.supportsModulation[paramKey{unit, param}]
}

func (n NecessaryFeatures) SupportsPolyphony() bool {
	return n.polyphony
}

func (n NecessaryFeatures) Opcode(unitType string) (int, bool) {
	code, ok := n.opcodes[unitType]
	return code, ok
}

func (n NecessaryFeatures) Instructions() []string {
	return n.instructions
}

func (n NecessaryFeatures) InputNumber(unitType string, paramName string) int {
	return allInputs[paramKey{unitType, paramName}]
}

func (_ NecessaryFeatures) TransformCount(unitType string) int {
	return allTransformCounts[unitType]
}

func (n NecessaryFeatures) SupportsGlobalSend() bool {
	return n.globalSend
}