package iokit

import (
	"fmt"
	"sort"
)

type dispatchEntry struct {
	object               uint64
	function             uint64
	adjustor             uint64
	scalarInputCount     uint32
	structInputSize      uint32
	scalarOutputCount    uint32
	structOutputSize     uint32
	flags                int64
	checkEntitlementAddr uint64
	checkEntitlement     string
	count0               uint64
	count1               uint64
}

func (a *analyzer) dispatchRecords(info *classInfo, analysis methodAnalysis) ([]Record, error) {
	if analysis.kind == DispatchExternalMethod2022 && analysis.stride != dispatchSize2022 {
		return nil, fmt.Errorf("IOExternalMethodDispatch2022 layout mismatch: stride=%d assumed=%d class=%s", analysis.stride, dispatchSize2022, info.Name)
	}
	if len(analysis.selectedEntries) > 0 {
		return a.selectedDispatchRecords(info, analysis), nil
	}
	if analysis.note == "bounds_unknown" || analysis.count <= 0 {
		return []Record{a.unresolvedMethodRecord(info, analysis, -1, "bounds_unknown")}, nil
	}
	bases := dispatchTableBases(analysis)
	records := make([]Record, 0, len(bases)*analysis.count)
	for tableIndex, tableBase := range bases {
		tableAnalysis := analysis
		tableAnalysis.arrayBase = tableBase
		if analysis.note == "conditional_array" {
			tableAnalysis.note = ""
		}
		for _, selector := range dispatchSelectors(analysis) {
			rec := a.dispatchRecord(info, tableAnalysis, selector)
			if analysis.note == "conditional_array" {
				addConditionalTableExtra(rec.Extra, tableIndex, tableBase)
			}
			records = append(records, rec)
		}
	}
	return records, nil
}

func dispatchSelectors(analysis methodAnalysis) []int {
	if analysis.count <= 0 {
		return nil
	}
	start := max(analysis.selectorLowerBound, 0)
	selectors := make([]int, 0, analysis.count)
	for idx := 0; idx < analysis.count; idx++ {
		selectors = append(selectors, start+idx)
	}
	return selectors
}

func (a *analyzer) selectedDispatchRecords(info *classInfo, analysis methodAnalysis) []Record {
	selectors := make([]int, 0, len(analysis.selectedEntries))
	for selector := range analysis.selectedEntries {
		selectors = append(selectors, selector)
	}
	sort.Ints(selectors)
	records := make([]Record, 0, len(selectors))
	for _, selector := range selectors {
		entryAnalysis := analysis
		entryAnalysis.arrayBase = analysis.selectedEntries[selector]
		rec := a.dispatchRecord(info, entryAnalysis, 0)
		rec.Selector = selector
		rec.Extra["slice_notes"] = appendNote(rec.Extra["slice_notes"], "selected_entry")
		records = append(records, rec)
	}
	return records
}

func dispatchTableBases(analysis methodAnalysis) []uint64 {
	if analysis.note == "conditional_array" && len(analysis.arrayBases) > 0 {
		return analysis.arrayBases
	}
	return []uint64{analysis.arrayBase}
}

func addConditionalTableExtra(extra map[string]string, tableIndex int, tableBase uint64) {
	if extra == nil {
		return
	}
	extra["table"] = fmt.Sprintf("table_%d", tableIndex)
	extra["table_base"] = hexAddr(tableBase)
	extra["slice_notes"] = appendNote(extra["slice_notes"], "conditional_array")
}

func (a *analyzer) dispatchRecord(info *classInfo, analysis methodAnalysis, selector int) Record {
	entry, note := a.readDispatchEntry(analysis, selector-analysis.selectorLowerBound)
	rec := Record{
		Kind:              KindMethod,
		Class:             info.Name,
		Bundle:            info.Bundle,
		Selector:          selector,
		MethodAddr:        hexAddr(entry.function),
		MethodSymbol:      a.symbolName(entry.function),
		DispatchKind:      analysis.kind,
		ScalarInputCount:  int64(entry.scalarInputCount),
		ScalarOutputCount: int64(entry.scalarOutputCount),
		StructInputSize:   int64(entry.structInputSize),
		StructOutputSize:  int64(entry.structOutputSize),
		Flags:             entry.flags,
		Resolved:          entry.function != 0 && note == "",
		Extra:             map[string]string{},
	}
	if note != "" {
		rec.ScalarInputCount = -1
		rec.ScalarOutputCount = -1
		rec.StructInputSize = -1
		rec.StructOutputSize = -1
		rec.Flags = -1
		rec.Extra["slice_notes"] = note
	}
	if entry.checkEntitlement != "" {
		rec.Extra["check_entitlement"] = entry.checkEntitlement
	} else if entry.checkEntitlementAddr != 0 {
		rec.Extra["check_entitlement_addr"] = hexAddr(entry.checkEntitlementAddr)
	}
	return rec
}

func (a *analyzer) legacyDispatchRecords(info *classInfo, analysis methodAnalysis) ([]Record, error) {
	if analysis.stride != dispatchSizeLegacy {
		return nil, fmt.Errorf("IOExternalMethod layout mismatch: stride=%d assumed=%d class=%s", analysis.stride, dispatchSizeLegacy, info.Name)
	}
	if analysis.note == "bounds_unknown" || analysis.count <= 0 {
		return []Record{a.unresolvedMethodRecord(info, analysis, -1, "bounds_unknown")}, nil
	}
	records := make([]Record, 0, analysis.count)
	for selector := 0; selector < analysis.count; selector++ {
		records = append(records, a.legacyDispatchRecord(info, analysis, selector))
	}
	return records, nil
}

func (a *analyzer) legacyDispatchRecord(info *classInfo, analysis methodAnalysis, selector int) Record {
	entry, note := a.readLegacyDispatchEntry(analysis, selector)
	scalarIn, scalarOut, structIn, structOut, countNote := legacyCountsForFlags(entry.flags, entry.count0, entry.count1)
	note = appendNote(note, countNote)
	rec := Record{
		Kind:              KindMethod,
		Class:             info.Name,
		Bundle:            info.Bundle,
		Selector:          selector,
		MethodAddr:        hexAddr(entry.function),
		MethodSymbol:      a.symbolName(entry.function),
		DispatchKind:      DispatchExternalMethodLegacy,
		ScalarInputCount:  scalarIn,
		ScalarOutputCount: scalarOut,
		StructInputSize:   structIn,
		StructOutputSize:  structOut,
		Flags:             entry.flags,
		Resolved:          entry.function != 0 && note == "",
		Extra:             map[string]string{},
	}
	if entry.object != 0 {
		rec.Extra["target_addr"] = hexAddr(entry.object)
	}
	if entry.adjustor != 0 {
		rec.Extra["method_adjustor"] = hexAddr(entry.adjustor)
	}
	if note != "" {
		rec.ScalarInputCount = -1
		rec.ScalarOutputCount = -1
		rec.StructInputSize = -1
		rec.StructOutputSize = -1
		rec.Flags = -1
		rec.Extra["slice_notes"] = note
	}
	return rec
}

func (a *analyzer) readLegacyDispatchEntry(analysis methodAnalysis, selector int) (dispatchEntry, string) {
	addr := analysis.arrayBase + uint64(selector)*analysis.stride
	entry := dispatchEntry{}
	if object, ok := a.scanner.ReadPointerAt(analysis.owner, addr); ok {
		entry.object = object
	}
	function, ok := a.scanner.ReadPointerAt(analysis.owner, addr+8)
	if !ok {
		if raw, err := a.scanner.ReadUint64At(analysis.owner, addr+8); err == nil {
			function = raw
		}
	}
	if function == 0 {
		return entry, "indirect"
	}
	entry.function = function
	if adjustor, err := a.scanner.ReadUint64At(analysis.owner, addr+16); err == nil {
		entry.adjustor = adjustor
	}
	flags, err := a.scanner.ReadUint32At(analysis.owner, addr+24)
	if err != nil {
		return entry, "indirect"
	}
	entry.flags = int64(flags)
	count0, err := a.scanner.ReadUint64At(analysis.owner, addr+32)
	if err != nil {
		return entry, "indirect"
	}
	entry.count0 = count0
	count1, err := a.scanner.ReadUint64At(analysis.owner, addr+40)
	if err != nil {
		return entry, "indirect"
	}
	entry.count1 = count1
	return entry, ""
}

func legacyCountsForFlags(flags int64, count0, count1 uint64) (int64, int64, int64, int64, string) {
	var scalarIn, scalarOut, structIn, structOut int64
	switch uint32(flags) & 0xf {
	case 0:
		scalarIn = legacyCountToInt64(count0)
		scalarOut = legacyCountToInt64(count1)
	case 2:
		scalarIn = legacyCountToInt64(count0)
		structOut = legacyCountToInt64(count1)
	case 3:
		structIn = legacyCountToInt64(count0)
		structOut = legacyCountToInt64(count1)
	case 4:
		scalarIn = legacyCountToInt64(count0)
		structIn = legacyCountToInt64(count1)
	default:
		return -1, -1, -1, -1, "legacy_flags_unknown"
	}
	if scalarIn < 0 || scalarOut < 0 || structIn < 0 || structOut < 0 {
		return -1, -1, -1, -1, "legacy_count_overflow"
	}
	return scalarIn, scalarOut, structIn, structOut, ""
}

func legacyCountToInt64(count uint64) int64 {
	const maxInt64Uint64 = uint64(1<<63 - 1)
	if count > maxInt64Uint64 {
		return -1
	}
	return int64(count)
}

func (a *analyzer) readDispatchEntry(analysis methodAnalysis, selector int) (dispatchEntry, string) {
	if selector < 0 {
		return dispatchEntry{}, "selector_out_of_range"
	}
	addr := analysis.arrayBase + uint64(selector)*analysis.stride
	function, ok := a.scanner.ReadPointerAt(analysis.owner, addr)
	if !ok || function == 0 {
		return dispatchEntry{}, "indirect"
	}
	if !a.dispatchFunctionResolves(function) {
		return dispatchEntry{}, "indirect"
	}
	scalarIn, err := a.scanner.ReadUint32At(analysis.owner, addr+8)
	if err != nil {
		return dispatchEntry{function: function}, "indirect"
	}
	structIn, err := a.scanner.ReadUint32At(analysis.owner, addr+12)
	if err != nil {
		return dispatchEntry{function: function}, "indirect"
	}
	scalarOut, err := a.scanner.ReadUint32At(analysis.owner, addr+16)
	if err != nil {
		return dispatchEntry{function: function}, "indirect"
	}
	structOut, err := a.scanner.ReadUint32At(analysis.owner, addr+20)
	if err != nil {
		return dispatchEntry{function: function}, "indirect"
	}
	entry := dispatchEntry{
		function:          function,
		scalarInputCount:  scalarIn,
		structInputSize:   structIn,
		scalarOutputCount: scalarOut,
		structOutputSize:  structOut,
		flags:             0,
	}
	if analysis.kind == DispatchExternalMethod2022 {
		flags, err := a.scanner.ReadUint32At(analysis.owner, addr+24)
		if err != nil {
			return entry, "indirect"
		}
		entry.flags = int64(flags & 0xff)
		if entitlement, ok := a.scanner.ReadPointerAt(analysis.owner, addr+32); ok {
			entry.checkEntitlementAddr = entitlement
			if str, err := a.scanner.ReadCStringAt(analysis.owner, entitlement); err == nil && validLiteralString(str) {
				entry.checkEntitlement = str
			}
		}
	}
	return entry, ""
}

// dispatchFunctionResolves reports whether a dispatch-entry function pointer
// lands in a real function body. The forward pointer cache is populated both
// from genuine chained-fixup rebases and from a raw scan of every 8-byte slot
// in __DATA_CONST. A scalar field (for example a 0/1/4 count) that the raw
// scan slid into a kernel-range value passes ReadPointerAt but does not point
// at code, so it must be rejected as "indirect" rather than rendered as a
// bogus method.
func (a *analyzer) dispatchFunctionResolves(function uint64) bool {
	_, err := a.scanner.FunctionBodyAt(function)
	return err == nil
}

func (a *analyzer) unresolvedMethodRecord(info *classInfo, analysis methodAnalysis, selector int, note string) Record {
	return Record{
		Kind:              KindMethod,
		Class:             info.Name,
		Bundle:            info.Bundle,
		Selector:          selector,
		MethodAddr:        "0x0",
		MethodSymbol:      "",
		DispatchKind:      analysis.kind,
		ScalarInputCount:  -1,
		ScalarOutputCount: -1,
		StructInputSize:   -1,
		StructOutputSize:  -1,
		Flags:             -1,
		Resolved:          false,
		Extra:             map[string]string{"slice_notes": note},
	}
}

func (a *analyzer) switchRecords(info *classInfo, analysis methodAnalysis) []Record {
	count := analysis.count
	if count <= 0 || count > maxSelectorCount {
		return []Record{a.unresolvedMethodRecord(info, analysis, -1, "bounds_unknown")}
	}
	records := make([]Record, 0, count)
	for selector := range count {
		extra := map[string]string{"slice_notes": "switch"}
		var methodAddr uint64
		if caseInfo, ok := analysis.switchCases[selector]; ok {
			methodAddr = caseInfo.methodAddr
			if caseInfo.readsStructureInput {
				extra["reads_structure_input"] = "true"
			}
			if caseInfo.readsStructureInputSize {
				extra["reads_structure_input_size"] = "true"
			}
		}
		records = append(records, Record{
			Kind:              KindMethod,
			Class:             info.Name,
			Bundle:            info.Bundle,
			Selector:          selector,
			MethodAddr:        hexAddr(methodAddr),
			MethodSymbol:      a.symbolName(methodAddr),
			DispatchKind:      DispatchSwitch,
			ScalarInputCount:  -1,
			ScalarOutputCount: -1,
			StructInputSize:   -1,
			StructOutputSize:  -1,
			Flags:             -1,
			Resolved:          methodAddr != 0,
			Extra:             extra,
		})
	}
	return records
}
