package cpp

import (
	"encoding/binary"
	"slices"

	"github.com/blacktop/go-macho"
	"github.com/blacktop/go-macho/pkg/fixupchains"
	"github.com/blacktop/go-macho/types"
)

func getMetaHasBoundedTail(data []byte, start int) bool {
	if start < 0 || start+4 > len(data) {
		return false
	}
	limit := min(start+5*4, len(data))
	sawCall := false
	for off := start; off+4 <= limit; off += 4 {
		raw := binary.LittleEndian.Uint32(data[off : off+4])
		switch {
		case raw == 0xd65f03c0 || raw == 0xd65f0fff:
			return true
		case (raw & 0xfc000000) == 0x14000000:
			return true
		case (raw & 0xfc000000) == 0x94000000:
			if sawCall {
				return false
			}
			sawCall = true
		}
	}
	return false
}

func (s *Scanner) findClassGetMetaClassCandidates(m *macho.File, metaPtr uint64) []uint64 {
	if metaPtr == 0 {
		return nil
	}
	key := fileAddrKey{file: m, addr: metaPtr}
	if cached, ok := s.getMetaCands[key]; ok {
		return cached
	}
	seen := make(map[uint64]struct{})
	out := make([]uint64, 0, 2)
	addResults := func(results map[uint64][]uint64) {
		for _, addr := range results[metaPtr] {
			if addr == 0 {
				continue
			}
			if _, ok := seen[addr]; ok {
				continue
			}
			seen[addr] = struct{}{}
			out = append(out, addr)
		}
	}
	if m != nil {
		addResults(s.scanForGetMetaClassFunctions(m))
	}
	if s.root != nil && s.root != m {
		addResults(s.scanForGetMetaClassFunctions(s.root))
	}
	s.getMetaCands[key] = out
	return out
}

func (s *Scanner) scanForGetMetaClassFunctions(m *macho.File) map[uint64][]uint64 {
	if results, ok := s.getMetaMap[m]; ok {
		return results
	}

	results := make(map[uint64][]uint64)
	addCandidate := func(metaAddr uint64, funcAddr uint64) {
		if !validKernelPointer(metaAddr) || funcAddr == 0 {
			return
		}
		list := results[metaAddr]
		if slices.Contains(list, funcAddr) {
			return
		}
		results[metaAddr] = append(list, funcAddr)
	}

	for _, sec := range m.Sections {
		if sec.Name != "__text" {
			continue
		}
		if sec.Seg != "__TEXT_EXEC" && sec.Seg != "__TEXT" {
			continue
		}
		data, err := s.readSectionData(m, sec)
		if err != nil {
			continue
		}

		base := sec.Addr
		for i := 0; i+12 <= len(data); i += 4 {
			instrAddr := base + uint64(i)
			raw0 := binary.LittleEndian.Uint32(data[i : i+4])
			raw1 := binary.LittleEndian.Uint32(data[i+4 : i+8])
			raw2 := binary.LittleEndian.Uint32(data[i+8 : i+12])

			var metaclassAddr uint64
			funcAddr := instrAddr
			if i >= 4 {
				rawPrev := binary.LittleEndian.Uint32(data[i-4 : i])
				if (rawPrev & 0xfffff01f) == 0xd503201f {
					funcAddr = instrAddr - 4
				}
			}

			isADRP := (raw0 & 0x9f000000) == 0x90000000
			isADR := (raw0 & 0x9f000000) == 0x10000000
			isLDR := (raw0&0x3f000000) == 0x18000000 || (raw0&0x3f000000) == 0x58000000

			if isADRP {
				isADD := (raw1 & 0x1f800000) == 0x11000000
				if isADD && getMetaHasBoundedTail(data, i+8) {
					adrpRd := raw0 & 0x1f
					addRn := (raw1 >> 5) & 0x1f
					addRd := raw1 & 0x1f
					if adrpRd == addRn && addRd == 0 {
						immhi := int64((raw0 >> 5) & 0x7ffff)
						immlo := int64((raw0 >> 29) & 0x3)
						offset := (immhi << 2) | immlo
						if offset&(1<<20) != 0 {
							offset |= ^int64((1 << 21) - 1)
						}
						pc := instrAddr &^ 0xfff
						addImm := uint64((raw1 >> 10) & 0xfff)
						metaclassAddr = uint64(int64(pc)+(offset<<12)) + addImm
					}
				}
			}

			if metaclassAddr == 0 && isADR {
				adrRd := raw0 & 0x1f
				isRET1 := raw1 == 0xd65f03c0 || raw1 == 0xd65f0fff
				isNOP := raw1 == 0xd503201f
				isRET2 := raw2 == 0xd65f03c0 || raw2 == 0xd65f0fff
				if adrRd == 0 && (isRET1 || (isNOP && isRET2) || getMetaHasBoundedTail(data, i+4)) {
					immhi := int64((raw0 >> 5) & 0x7ffff)
					immlo := int64((raw0 >> 29) & 0x3)
					offset := (immhi << 2) | immlo
					if offset&(1<<20) != 0 {
						offset |= ^int64((1 << 21) - 1)
					}
					metaclassAddr = uint64(int64(instrAddr) + offset)
				}
			}

			if metaclassAddr == 0 && isLDR {
				ldrRd := raw0 & 0x1f
				isRET := raw1 == 0xd65f03c0
				isBR := isBranchRegisterRaw(raw1)
				if ldrRd == 0 && (isRET || isBR) {
					imm19 := int64((raw0 >> 5) & 0x7ffff)
					if imm19&(1<<18) != 0 {
						imm19 |= ^int64((1 << 19) - 1)
					}
					literalAddr := uint64(int64(instrAddr) + (imm19 << 2))
					if ptr, ok := s.resolvePointerAtReason(m, literalAddr, pointerReasonGetMetaLiteral); ok {
						metaclassAddr = ptr
					}
				}
			}

			addCandidate(metaclassAddr, funcAddr)
		}
	}

	s.getMetaMap[m] = results
	for _, sec := range m.Sections {
		if sec.Name == "__text" {
			delete(s.sectionData, sectionKey{file: m, addr: sec.Addr})
		}
	}
	return results
}

func (s *Scanner) buildPointerIndex(m *macho.File) map[uint64][]uint64 {
	if s.pointerBuilt[m] {
		return s.pointerIndex[m]
	}

	if s.root != nil && s.root.FileHeader.Type == types.MH_FILESET {
		s.seedRootFixupIndexes()
	}
	index, fwd := s.ensurePointerMaps(m)

	if m == nil {
		return index
	}
	addFixupPointers := func() {
		if !m.HasDyldChainedFixups() {
			return
		}
		dcf, err := m.DyldChainedFixups()
		if err != nil {
			return
		}
		if _, err := dcf.Parse(); err != nil {
			return
		}
		vmOwner := m
		if s.root != nil {
			vmOwner = s.root
		}
		for _, start := range dcf.Starts {
			for _, fx := range start.Fixups {
				slotAddr, err := vmOwner.GetVMAddress(fx.Offset())
				if err != nil {
					continue
				}
				s.captureSlotAuth(slotAddr, fx)
				if s.fileForVMAddr(slotAddr) != m {
					continue
				}
				if _, seen := fwd[slotAddr]; seen {
					continue
				}
				ptr := m.SlidePointer(fx.Raw())
				if !validKernelPointer(ptr) {
					continue
				}
				index[ptr] = append(index[ptr], slotAddr)
				fwd[slotAddr] = ptr
			}
		}
	}
	addSectionPointers := func(sec *types.Section) {
		if sec == nil || sec.Size < 8 {
			return
		}
		data, err := s.readSectionData(m, sec)
		if err != nil {
			return
		}

		ownerDecoder := newSectionPointerDecoder(m, sec, sec)

		for off := 0; off+8 <= len(data); off += 8 {
			raw := binary.LittleEndian.Uint64(data[off : off+8])
			if raw == 0 {
				continue
			}
			addr := sec.Addr + uint64(off)
			if _, seen := fwd[addr]; seen {
				continue
			}
			if ptr, ok := ownerDecoder.decode(raw, off); ok && ptr != 0 {
				index[ptr] = append(index[ptr], addr)
				fwd[addr] = ptr
			}
		}
	}

	addFixupPointers()
	for _, sec := range m.Sections {
		if sec == nil || sec.Size < 8 {
			continue
		}
		if sec.Seg == "__TEXT" || sec.Seg == "__TEXT_EXEC" {
			continue
		}
		if sec.Name == "__bss" || sec.Name == "__common" {
			continue
		}
		addSectionPointers(sec)
	}

	s.pointerIndex[m] = index
	s.forwardPointers[m] = fwd
	s.pointerBuilt[m] = true
	s.stats.pointerIndexEntries += len(index)
	return index
}

func (s *Scanner) seedRootFixupIndexes() {
	if s.rootFixupsSeeded || s.root == nil || !s.root.HasDyldChainedFixups() {
		return
	}
	dcf, err := s.root.DyldChainedFixups()
	if err != nil {
		return
	}
	if _, err := dcf.Parse(); err != nil {
		return
	}

	for _, start := range dcf.Starts {
		for _, fx := range start.Fixups {
			slotAddr, err := s.root.GetVMAddress(fx.Offset())
			if err != nil {
				continue
			}
			s.captureSlotAuth(slotAddr, fx)
			owner := s.fileForVMAddr(slotAddr)
			if owner == nil {
				continue
			}
			ptr := s.root.SlidePointer(fx.Raw())
			if !validKernelPointer(ptr) {
				continue
			}
			index, fwd := s.ensurePointerMaps(owner)
			if _, seen := fwd[slotAddr]; seen {
				continue
			}
			index[ptr] = append(index[ptr], slotAddr)
			fwd[slotAddr] = ptr
		}
	}
	s.rootFixupsSeeded = true
}

func (s *Scanner) ensurePointerMaps(m *macho.File) (map[uint64][]uint64, map[uint64]uint64) {
	index := s.pointerIndex[m]
	if index == nil {
		index = make(map[uint64][]uint64)
		s.pointerIndex[m] = index
	}
	fwd := s.forwardPointers[m]
	if fwd == nil {
		fwd = make(map[uint64]uint64)
		s.forwardPointers[m] = fwd
	}
	return index, fwd
}

type sectionPointerDecoder struct {
	file       *macho.File
	dcf        *fixupchains.DyldChainedFixups
	offsetBase uint64
	base       uint64
	preferred  uint64
}

func newSectionPointerDecoder(file *macho.File, targetSec, mappedSec *types.Section) sectionPointerDecoder {
	if file == nil || targetSec == nil || mappedSec == nil {
		return sectionPointerDecoder{}
	}
	offsetBase := uint64(mappedSec.Offset)
	if mappedSec != targetSec {
		offsetBase += targetSec.Addr - mappedSec.Addr
	}
	decoder := sectionPointerDecoder{
		file:       file,
		offsetBase: offsetBase,
		base:       file.GetBaseAddress(),
	}
	if text := file.Segment("__TEXT"); text != nil {
		decoder.preferred = text.Addr
	}
	if file.HasDyldChainedFixups() {
		if dcf, err := file.DyldChainedFixups(); err == nil {
			decoder.dcf = dcf
		}
	}
	return decoder
}

func (d sectionPointerDecoder) decode(raw uint64, relOff int) (uint64, bool) {
	if d.file == nil || raw == 0 {
		return 0, false
	}
	if d.dcf != nil {
		fileOffset := d.offsetBase + uint64(relOff)
		if target, err := d.dcf.RebaseRaw(fileOffset, raw, d.base); err == nil {
			ptr := target + d.preferred
			if validKernelPointer(ptr) {
				return ptr, true
			}
		}
	}
	ptr := d.file.SlidePointer(raw)
	if validKernelPointer(ptr) {
		return ptr, true
	}
	return 0, false
}

func (s *Scanner) inferVtableFromPointer(m *macho.File, ptrLoc uint64) (uint64, int, bool) {
	if ptrLoc < 16 {
		return 0, 0, false
	}
	sec := m.FindSectionForVMAddr(ptrLoc)
	if sec == nil || ptrLoc < sec.Addr+16 {
		return 0, 0, false
	}
	data, err := s.readSectionData(m, sec)
	if err != nil {
		return 0, 0, false
	}
	if ptrLoc >= sec.Addr+uint64(len(data)) {
		return 0, 0, false
	}

	zerosSeen := 0
	var headerAddr uint64
	for step := 1; step <= 256; step++ {
		addr := ptrLoc - uint64(step*8)
		if addr < sec.Addr {
			break
		}
		off := int(addr - sec.Addr)
		if off < 0 || off+8 > len(data) {
			break
		}
		val := binary.LittleEndian.Uint64(data[off : off+8])
		if val == 0 {
			zerosSeen++
			if zerosSeen == 2 {
				headerAddr = addr
				break
			}
			continue
		}
		zerosSeen = 0
	}
	if headerAddr == 0 {
		return 0, 0, false
	}

	vtableAddr := headerAddr + 16
	if ptrLoc < vtableAddr {
		return 0, 0, false
	}
	offset := ptrLoc - vtableAddr
	if offset%8 != 0 {
		return 0, 0, false
	}
	return vtableAddr, int(offset / 8), true
}

func (s *Scanner) findVtableViaGetMetaClass(m *macho.File, metaVtableAddr uint64, getMetaClassAddr uint64) uint64 {
	index := s.buildPointerIndex(m)
	locs := index[getMetaClassAddr]
	if len(locs) == 0 {
		if fn, err := s.functionForAddr(m, getMetaClassAddr); err == nil {
			locs = index[fn.StartAddr]
		}
	}

	var best uint64
	ambiguous := false
	for _, loc := range locs {
		vtableAddr, slot, ok := s.inferVtableFromPointer(m, loc)
		if !ok || slot != s.getMetaClassIndex {
			continue
		}
		if vtableAddr == metaVtableAddr || vtableAddr == metaVtableAddr+16 {
			continue
		}
		if !s.validateVtableCandidate(m, vtableAddr, getMetaClassAddr) {
			continue
		}
		if best == 0 {
			best = vtableAddr
			continue
		}
		if vtableAddr != best {
			ambiguous = true
		}
	}
	if ambiguous {
		return 0
	}
	return best
}

func (s *Scanner) validateVtableCandidate(m *macho.File, vtableAddr uint64, getMetaClassAddr uint64) bool {
	ptr, ok := s.resolvePointerAtReason(m, vtableAddr+uint64(s.getMetaClassIndex*8), pointerReasonGetMetaLiteral)
	if !ok {
		return false
	}
	if ptr == getMetaClassAddr {
		return true
	}
	if fn, err := s.functionForAddr(m, getMetaClassAddr); err == nil && ptr == fn.StartAddr {
		return true
	}
	return false
}

func (s *Scanner) recoverVtableNearMeta(m *macho.File, metaVtableAddr uint64, getMetaCandidates []uint64) uint64 {
	if m == nil || metaVtableAddr == 0 || len(getMetaCandidates) == 0 {
		return 0
	}
	sec := m.FindSectionForVMAddr(metaVtableAddr)
	if sec == nil {
		return 0
	}
	start := max(metaVtableAddr, sec.Addr+16)
	lowerBound := sec.Addr + 16
	upperBound := sec.Addr + sec.Size
	const maxRadius = 0x2000

	check := func(addr uint64) uint64 {
		if addr < lowerBound || addr+8 > upperBound {
			return 0
		}
		if addr == metaVtableAddr || addr == metaVtableAddr+16 {
			return 0
		}
		if !s.looksLikeVtableStart(m, addr) {
			return 0
		}
		for _, cand := range getMetaCandidates {
			if s.validateVtableCandidate(m, addr, cand) {
				return addr
			}
		}
		return 0
	}

	for delta := uint64(8); delta <= maxRadius; delta += 8 {
		if start >= lowerBound+delta {
			if vt := check(start - delta); vt != 0 {
				return vt
			}
		}
		if start+delta < upperBound {
			if vt := check(start + delta); vt != 0 {
				return vt
			}
		}
	}
	return 0
}
