// Package macho provides functionality for parsing Mach-O files.
package macho

import (
	"cmp"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strings"
	"syscall"

	"github.com/alecthomas/chroma/v2/quick"
	"github.com/apex/log"
	"github.com/blacktop/go-macho"
	"github.com/blacktop/go-macho/pkg/swift"
	"github.com/blacktop/go-macho/types"
	"github.com/blacktop/go-macho/types/objc"
	"github.com/blacktop/go-plist"
	"github.com/blacktop/ipsw/pkg/dyld"
	"github.com/blacktop/ipsw/pkg/tbd"
)

// ErrNoObjc is returned when a MachO does not contain objc info
var ErrNoObjc = errors.New("macho does not contain objc info")

var baseFrameworks = []string{"Foundation", "CoreFoundation", "libobjc.A.dylib"}

// ObjcConfig for MachO ObjC parser
type ObjcConfig struct {
	Name     string
	Verbose  bool
	Addrs    bool
	Headers  bool
	ObjcRefs bool
	Deps     bool
	Generic  bool
	Demangle bool

	IpswVersion string

	Color  bool
	Theme  string
	Output string
}

// Imports represents the imported symbols, local symbols, classes, and protocols for a ObjC header
type Imports struct {
	Imports []string
	Locals  []string
	Classes []string
	Protos  []string
}

func (i *Imports) uniq(foundation map[string][]string) {
	slices.Sort(i.Imports)
	slices.Sort(i.Locals)
	slices.Sort(i.Classes)
	slices.Sort(i.Protos)
	i.Imports = slices.Compact(i.Imports)
	i.Locals = slices.Compact(i.Locals)
	i.Classes = slices.Compact(i.Classes)
	i.Protos = slices.Compact(i.Protos)
	i.Imports = slices.DeleteFunc(i.Imports, func(l string) bool {
		l = strings.TrimSuffix(l, "-Protocol.h")
		l = strings.TrimSuffix(l, ".h")
		_, foundC := slices.BinarySearch(foundation["classes"], l)
		_, foundP := slices.BinarySearch(foundation["protocols"], l)
		return foundC || foundP
	})
	i.Locals = slices.DeleteFunc(i.Locals, func(l string) bool {
		l = strings.TrimSuffix(l, "-Protocol.h")
		l = strings.TrimSuffix(l, ".h")
		_, foundC := slices.BinarySearch(foundation["classes"], l)
		_, foundP := slices.BinarySearch(foundation["protocols"], l)
		return foundC || foundP
	})
	// remove Foundation classes
	i.Classes = slices.DeleteFunc(i.Classes, func(c string) bool {
		_, found := slices.BinarySearch(foundation["classes"], c)
		return found
	})
	// remove Foundation protocols
	i.Protos = slices.DeleteFunc(i.Protos, func(p string) bool {
		_, found := slices.BinarySearch(foundation["protocols"], p)
		return found
	})
}

type headerInfo struct {
	FileName      string
	IpswVersion   string
	BuildVersions []string
	SourceVersion string
	IsUmbrella    bool
	Name          string
	Imports       Imports
	Object        string
}

func (o *ObjC) maybeDemangle(text string) string {
	if !o.conf.Demangle {
		return text
	}
	return swift.DemangleBlob(text)
}

type objcDescriber interface {
	Verbose() string
	WithAddrs() string
}

func (o *ObjC) verboseOrAddrs(v objcDescriber) string {
	if o.conf.Addrs {
		return o.maybeDemangle(v.WithAddrs())
	}
	return o.maybeDemangle(v.Verbose())
}

// ObjC represents a MachO ObjC parser
type ObjC struct {
	conf  *ObjcConfig
	file  *macho.File
	cache *dyld.File
	deps  []*macho.File

	baseFWs map[string][]string
}

// NewObjC returns a new MachO ObjC parser instance
func NewObjC(file *macho.File, dsc *dyld.File, conf *ObjcConfig) (*ObjC, error) {
	if !file.HasObjC() {
		return nil, ErrNoObjc
	}

	o := &ObjC{
		conf:    conf,
		file:    file,
		cache:   dsc,
		baseFWs: make(map[string][]string),
	}

	if o.conf.Deps {
		if dsc == nil {
			return nil, fmt.Errorf("dyld shared cache is required to dump imported private frameworks")
		}
		deps, err := loadImportedMachODependencies(
			o.cache,
			o.conf.Name,
			file.ImportedLibraries(),
			o.conf.Headers,
		)
		if err != nil {
			return nil, err
		}
		o.deps = deps
	}

	return o, nil
}

// DumpClass returns a ObjC classes matching a given pattern from a MachO
func (o *ObjC) DumpClass(pattern string) error {
	re, err := regexp.Compile(pattern)
	if err != nil {
		return fmt.Errorf("failed to compile regex: %v", err)
	}
	ms := []*macho.File{o.file}
	if o.conf.Deps {
		ms = append(ms, o.deps...)
	}
	for _, m := range ms {
		classes, err := m.GetObjCClasses()
		if err != nil {
			if errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			return err
		}

		slices.SortStableFunc(classes, func(a, b objc.Class) int {
			return cmp.Compare(a.Name, b.Name)
		})

		for _, class := range classes {
			if re.MatchString(class.Name) {
				if o.conf.Color {
					quick.Highlight(os.Stdout, o.verboseOrAddrs(&class), "objc", "terminal256", o.conf.Theme)
					quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
				} else {
					fmt.Println(o.verboseOrAddrs(&class))
				}
			}
		}
	}
	return nil
}

// DumpProtocol returns a ObjC protocols matching a given pattern from a MachO
func (o *ObjC) DumpProtocol(pattern string) error {
	re, err := regexp.Compile(pattern)
	if err != nil {
		return fmt.Errorf("failed to compile regex: %v", err)
	}
	ms := []*macho.File{o.file}
	if o.conf.Deps {
		ms = append(ms, o.deps...)
	}
	for _, m := range ms {
		protos, err := m.GetObjCProtocols()
		if err != nil {
			if errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			return err
		}

		slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
			return cmp.Compare(a.Name, b.Name)
		})
		seen := make(map[uint64]bool)

		for _, proto := range protos {
			if re.MatchString(proto.Name) {
				if _, ok := seen[proto.Ptr]; !ok { // prevent displaying duplicates
					if o.conf.Color {
						quick.Highlight(os.Stdout, o.verboseOrAddrs(&proto), "objc", "terminal256", o.conf.Theme)
						quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
					} else {
						fmt.Println(o.verboseOrAddrs(&proto))
					}
					seen[proto.Ptr] = true
				}
			}
		}
	}
	return nil
}

// DumpCategory returns a ObjC categories matching a given pattern from a MachO
func (o *ObjC) DumpCategory(pattern string) error {
	re, err := regexp.Compile(pattern)
	if err != nil {
		return fmt.Errorf("failed to compile regex: %v", err)
	}
	ms := []*macho.File{o.file}
	if o.conf.Deps {
		ms = append(ms, o.deps...)
	}
	for _, m := range ms {
		cats, err := m.GetObjCCategories()
		if err != nil {
			if errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			return err
		}

		slices.SortStableFunc(cats, func(a, b objc.Category) int {
			return cmp.Compare(a.Name, b.Name)
		})

		for _, cat := range cats {
			if re.MatchString(cat.Name) {
				if o.conf.Color {
					quick.Highlight(os.Stdout, o.verboseOrAddrs(&cat), "objc", "terminal256", o.conf.Theme)
					quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
				} else {
					fmt.Println(o.verboseOrAddrs(&cat))
				}
			}
		}
	}
	return nil
}

// Dump outputs ObjC info from a MachO
func (o *ObjC) Dump() error {
	ms := []*macho.File{o.file}
	if o.conf.Deps {
		ms = append(ms, o.deps...)
	}
	for _, m := range ms {
		if o.conf.Verbose {
			if info, err := m.GetObjCImageInfo(); err == nil {
				fmt.Println(info.Flags)
			} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			fmt.Println(m.GetObjCToc())
		}
		/* ObjC Protocols */
		if protos, err := m.GetObjCProtocols(); err == nil {
			slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
				return cmp.Compare(a.Name, b.Name)
			})
			seen := make(map[uint64]bool)
			for _, proto := range protos {
				if _, ok := seen[proto.Ptr]; !ok { // prevent displaying duplicates
					if o.conf.Verbose {
						if o.conf.Color {
							quick.Highlight(os.Stdout, o.verboseOrAddrs(&proto), "objc", "terminal256", o.conf.Theme)
							quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
						} else {
							fmt.Println(o.verboseOrAddrs(&proto))
						}
					} else {
						if o.conf.Color {
							quick.Highlight(os.Stdout, proto.String()+"\n", "objc", "terminal256", o.conf.Theme)
						} else {
							fmt.Println(proto.String())
						}
					}
					seen[proto.Ptr] = true
				}
			}
		} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
			return err
		}
		/* ObjC Classes */
		if classes, err := m.GetObjCClasses(); err == nil {
			slices.SortStableFunc(classes, func(a, b objc.Class) int {
				return cmp.Compare(a.Name, b.Name)
			})
			for _, class := range classes {
				if o.conf.Verbose {
					if o.conf.Color {
						quick.Highlight(os.Stdout, o.verboseOrAddrs(&class), "objc", "terminal256", o.conf.Theme)
						quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
					} else {
						fmt.Println(o.verboseOrAddrs(&class))
					}
				} else {
					if o.conf.Color {
						quick.Highlight(os.Stdout, class.String()+"\n", "objc", "terminal256", o.conf.Theme)
					} else {
						fmt.Println(class.String())
					}
				}
			}
		} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
			return err
		}
		/* ObjC Categories */
		if cats, err := m.GetObjCCategories(); err == nil {
			slices.SortStableFunc(cats, func(a, b objc.Category) int {
				return cmp.Compare(a.Name, b.Name)
			})
			for _, cat := range cats {
				if o.conf.Verbose {
					if o.conf.Color {
						quick.Highlight(os.Stdout, o.verboseOrAddrs(&cat), "objc", "terminal256", o.conf.Theme)
						quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
					} else {
						fmt.Println(o.verboseOrAddrs(&cat))
					}
				} else {
					if o.conf.Color {
						quick.Highlight(os.Stdout, cat.String()+"\n", "objc", "terminal256", o.conf.Theme)
					} else {
						fmt.Println(cat.String())
					}
				}
			}
		} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
			return err
		}
		if o.conf.ObjcRefs {
			if protRefs, err := m.GetObjCProtoReferences(); err == nil {
				fmt.Printf("\n@protocol refs\n")
				for off, prot := range protRefs {
					fmt.Printf("0x%011x => 0x%011x: %s\n", off, prot.Ptr, prot.Name)
				}
			} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			if clsRefs, err := m.GetObjCClassReferences(); err == nil {
				fmt.Printf("\n@class refs\n")
				for off, cls := range clsRefs {
					fmt.Printf("0x%011x => 0x%011x: %s\n", off, cls.ClassPtr, cls.Name)
				}
			} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			if supRefs, err := m.GetObjCSuperReferences(); err == nil {
				fmt.Printf("\n@super refs\n")
				for off, sup := range supRefs {
					fmt.Printf("0x%011x => 0x%011x: %s\n", off, sup.ClassPtr, sup.Name)
				}
			} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			if selRefs, err := m.GetObjCSelectorReferences(); err == nil {
				fmt.Printf("\n@selectors refs\n")
				for off, sel := range selRefs {
					fmt.Printf("0x%011x => 0x%011x: %s\n", off, sel.VMAddr, sel.Name)
				}
			} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
			if o.conf.Verbose {
				if classes, err := m.GetObjCClassNames(); err == nil {
					fmt.Printf("\n@objc_classname\n")
					for vmaddr, className := range classes {
						fmt.Printf("0x%011x: %s\n", vmaddr, className)
					}
				} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
					return err
				}
				if methods, err := m.GetObjCMethodNames(); err == nil {
					fmt.Printf("\n@objc_methname\n")
					for vmaddr, method := range methods {
						fmt.Printf("0x%011x: %s\n", vmaddr, method)
					}
				} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
					return err
				}
			}
		}
	}
	return nil
}

// Headers outputs ObjC class-dump headers from a MachO
func (o *ObjC) Headers() error {

	// scan DSC for Foundation/CoreFoundation classes and protocols
	if err := o.scanBaseFrameworks(); err != nil {
		return err
	}

	writeHeaders := func(m *macho.File) error {
		var headers []string

		if !m.HasObjC() {
			return nil
		}

		if id := m.DylibID(); id != nil {
			o.conf.Name = filepath.Base(id.Name)
		}
		var buildVersions []string
		if bvers := m.GetLoadsByName("LC_BUILD_VERSION"); len(bvers) > 0 {
			for _, bv := range bvers {
				buildVersions = append(buildVersions, bv.String())
			}
		}
		var sourceVersion string
		if svers := m.GetLoadsByName("LC_SOURCE_VERSION"); len(svers) > 0 {
			sourceVersion = svers[0].String()
		}

		imps, err := o.processForwardDeclarations(m)
		if err != nil {
			return err
		}

		/* generate ObjC class headers */
		classes, err := m.GetObjCClasses()
		if err != nil {
			if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
		}
		slices.SortStableFunc(classes, func(a, b objc.Class) int {
			return cmp.Compare(a.Name, b.Name)
		})
		for _, class := range classes {
			var props []string
			var setters []string
			for _, prop := range class.Props {
				props = append(props, prop.Name)
				setters = append(setters, "set"+strings.ToUpper(prop.Name[:1])+prop.Name[1:]+":")
			}
			slices.Sort(props)
			slices.Sort(setters)
			// remove ivars that are properties
			class.Ivars = slices.DeleteFunc(class.Ivars, func(i objc.Ivar) bool {
				// return slices.Contains(props, i.Name) || slices.Contains(props, strings.TrimPrefix(i.Name, "_")) TODO: use this instead
				return slices.Contains(props, strings.TrimPrefix(i.Name, "_"))
			})
			// remove methods that are property getter/setter
			class.InstanceMethods = slices.DeleteFunc(class.InstanceMethods, func(m objc.Method) bool {
				return slices.Contains(props, m.Name) || slices.Contains(setters, m.Name)
			})
			fname := filepath.Join(o.conf.Output, o.conf.Name, class.Name+".h")
			if err := writeHeader(&headerInfo{
				FileName:      fname,
				IpswVersion:   o.conf.IpswVersion,
				BuildVersions: buildVersions,
				SourceVersion: sourceVersion,
				Name:          class.Name,
				Imports:       imps[class.Name],
				Object:        o.verboseOrAddrs(&class),
			}); err != nil {
				return err
			}
			headers = append(headers, filepath.Base(fname))
		}

		/* generate ObjC protocol headers */
		protos, err := m.GetObjCProtocols()
		if err != nil {
			if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
		}
		slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
			return cmp.Compare(a.Name, b.Name)
		})
		seen := make(map[uint64]bool)
		for _, proto := range protos {
			if !slices.Contains(baseFrameworks, o.conf.Name) {
				if _, found := slices.BinarySearch(o.baseFWs["protocols"], proto.Name); found {
					continue // skip Foundation protocols
				}
			}
			if _, ok := seen[proto.Ptr]; !ok { // prevent displaying duplicates
				var props []string
				var setters []string
				for _, prop := range proto.InstanceProperties {
					props = append(props, prop.Name)
					setters = append(setters, "set"+strings.ToUpper(prop.Name[:1])+prop.Name[1:]+":")
				}
				slices.Sort(props)
				slices.Sort(setters)
				// remove methods that are property getter/setter
				proto.InstanceMethods = slices.DeleteFunc(proto.InstanceMethods, func(m objc.Method) bool {
					return slices.Contains(props, m.Name) || slices.Contains(setters, m.Name)
				})
				proto.OptionalInstanceMethods = slices.DeleteFunc(proto.OptionalInstanceMethods, func(m objc.Method) bool {
					return slices.Contains(props, m.Name) || slices.Contains(setters, m.Name)
				})
				fname := filepath.Join(o.conf.Output, o.conf.Name, proto.Name+"-Protocol.h")
				if err := writeHeader(&headerInfo{
					FileName:      fname,
					IpswVersion:   o.conf.IpswVersion,
					BuildVersions: buildVersions,
					SourceVersion: sourceVersion,
					Name:          proto.Name + "_Protocol",
					Imports:       imps[proto.Name+"-Protocol"],
					Object:        o.verboseOrAddrs(&proto),
				}); err != nil {
					return err
				}
				headers = append(headers, filepath.Base(fname))
				seen[proto.Ptr] = true
			}
		}

		/* generate ObjC category headers */
		cats, err := m.GetObjCCategories()
		if err != nil {
			if !errors.Is(err, macho.ErrObjcSectionNotFound) {
				return err
			}
		}
		slices.SortStableFunc(cats, func(a, b objc.Category) int {
			return cmp.Compare(a.Name, b.Name)
		})
		for _, cat := range cats {
			fname := filepath.Join(o.conf.Output, o.conf.Name, cat.Name+".h")
			if cat.Class != nil && cat.Class.Name != "" {
				fname = filepath.Join(o.conf.Output, o.conf.Name, cat.Class.Name+"+"+cat.Name+".h")
			}
			var name string
			if cat.Class != nil && cat.Class.Name != "" {
				name = cat.Class.Name + "_" + cat.Name
			} else {
				name = cat.Name
			}
			if err := writeHeader(&headerInfo{
				FileName:      fname,
				IpswVersion:   o.conf.IpswVersion,
				BuildVersions: buildVersions,
				SourceVersion: sourceVersion,
				Name:          name,
				Imports:       imps[cat.Name],
				Object:        o.verboseOrAddrs(&cat),
			}); err != nil {
				return err
			}
			headers = append(headers, filepath.Base(fname))
		}

		/* generate umbrella header */
		if len(headers) > 0 {
			var umbrella string
			if slices.Contains(headers, o.conf.Name+".h") {
				umbrella = o.conf.Name + "-Umbrella"
			} else {
				umbrella = o.conf.Name
			}

			for i, header := range headers {
				headers[i] = "#import \"" + header + "\""
			}

			fname := filepath.Join(o.conf.Output, o.conf.Name, umbrella+".h")
			if err := writeHeader(&headerInfo{
				FileName:      fname,
				IpswVersion:   o.conf.IpswVersion,
				BuildVersions: buildVersions,
				SourceVersion: sourceVersion,
				IsUmbrella:    true,
				Name:          strings.ReplaceAll(umbrella, "-", "_"),
				Object:        strings.Join(headers, "\n") + "\n",
			}); err != nil {
				return err
			}
		}

		return nil
	}

	if len(o.deps) > 0 {
		for _, m := range o.deps {
			if err := writeHeaders(m); err != nil {
				return err
			}
		}
	}

	return writeHeaders(o.file)
}

type XCFrameworkAvailableLibrary struct {
	BinaryPath               string   `plist:"BinaryPath"`
	LibraryIdentifier        string   `plist:"LibraryIdentifier"`
	LibraryPath              string   `plist:"libraryPath"`
	SupportedArchitectures   []string `plist:"SupportedArchitectures"`
	SupportedPlatform        string   `plist:"SupportedPlatform"`
	SupportedPlatformVariant string   `plist:"SupportedPlatformVariant,omitempty"`
}

type XCFrameworkInfoPlist struct {
	AvailableLibraries       []XCFrameworkAvailableLibrary `plist:"AvailableLibraries"`
	CFBundlePackageType      string                        `plist:"CFBundlePackageType"`
	XCFrameworkFormatVersion string                        `plist:"XCFrameworkFormatVersion"`
}
type XCFrameworkLibraryInfoPlist struct {
	BuildMachineOSBuild           string   `plist:"BuildMachineOSBuild"`
	CFBundleDevelopmentRegion     string   `plist:"CFBundleDevelopmentRegion"`
	CFBundleExecutable            string   `plist:"CFBundleExecutable"`
	CFBundleIdentifier            string   `plist:"CFBundleIdentifier"`
	CFBundleInfoDictionaryVersion string   `plist:"CFBundleInfoDictionaryVersion"`
	CFBundleName                  string   `plist:"CFBundleName"`
	CFBundlePackageType           string   `plist:"CFBundlePackageType"`
	CFBundleShortVersionString    string   `plist:"CFBundleShortVersionString"`
	CFBundleSignature             string   `plist:"CFBundleSignature"`
	CFBundleSupportedPlatforms    []string `plist:"CFBundleSupportedPlatforms"`
	CFBundleVersion               string   `plist:"CFBundleVersion"`
	DTCompiler                    string   `plist:"DTCompiler"`
	DTPlatformBuild               string   `plist:"DTPlatformBuild"`
	DTPlatformName                string   `plist:"DTPlatformName"`
	DTPlatformVersion             string   `plist:"DTPlatformVersion"`
	DTSDKBuild                    string   `plist:"DTSDKBuild"`
	DTSDKName                     string   `plist:"DTSDKName"`
	DTXcode                       string   `plist:"DTXcode"`
	DTXcodeBuild                  string   `plist:"DTXcodeBuild"`
	LSMinimumSystemVersion        string   `plist:"LSMinimumSystemVersion,omitempty"`
	MinimumOSVersion              string   `plist:"MinimumOSVersion,omitempty"`
	UIDeviceFamily                []uint64 `plist:"UIDeviceFamily,omitempty"`
}

type XCFrameworkConfig struct {
	LibraryIdentifier        string
	SupportedArchitectures   []string
	SupportedPlatform        string
	SupportedPlatformVariant string
	CFBundleVersion          string
	DTPlatformVersion        string
	LSMinimumSystemVersion   string
}

// XCFramework outputs and XCFramework for a DSC dylib
func (o *ObjC) XCFramework() error {
	var xcfw XCFrameworkConfig

	xcfolder := filepath.Join(o.conf.Output, o.conf.Name+".xcframework")
	if err := os.MkdirAll(xcfolder, 0o750); err != nil {
		return fmt.Errorf("failed to create XCFramework folder: %w", err)
	}

	image, err := o.cache.Image(o.conf.Name)
	if err != nil {
		return fmt.Errorf("failed to get image %s: %w", o.conf.Name, err)
	}
	m, err := image.GetMacho()
	if err != nil {
		return fmt.Errorf("failed to get macho from image %s: %w", o.conf.Name, err)
	}

	if bvs := m.BuildVersions(); len(bvs) == 0 { // TODO: support universal MachOs (with multiple architectures)
		return fmt.Errorf("no build versions found in %s", o.conf.Name)
	} else {
		for _, bv := range bvs {
			switch bv.Platform {
			case types.Platform_iOsSimulator, types.Platform_tvOsSimulator, types.Platform_watchOsSimulator, types.Platform_visionOsSimulator:
				xcfw.LibraryIdentifier += "_simulator"
				xcfw.SupportedPlatformVariant = "simulator"
			default:
				xcfw.LibraryIdentifier += strings.ToLower(bv.Platform.String())
				xcfw.SupportedPlatform = strings.ToLower(bv.Platform.String())
				xcfw.DTPlatformVersion = bv.Sdk.String()
				xcfw.LSMinimumSystemVersion = bv.Minos.String()
				switch m.CPU {
				case types.CPUAmd64:
					xcfw.LibraryIdentifier += "_x86_64"
					xcfw.SupportedArchitectures = append(xcfw.SupportedArchitectures, "x86_64")
				case types.CPUArm64:
					if m.SubCPU.String(m.CPU) == "arm64e" {
						xcfw.LibraryIdentifier += "_arm64e"
					} else {
						xcfw.LibraryIdentifier += "_arm64"
					}
				}
			}
		}
	}

	if id := m.DylibID(); id != nil {
		xcfw.CFBundleVersion = id.CurrentVersion.String()
	}

	/* generate XCFramework Info.plist */
	f, err := os.Create(filepath.Join(xcfolder, "Info.plist"))
	if err != nil {
		return fmt.Errorf("failed to create %s: %w", filepath.Join(xcfolder, "Info.plist"), err)
	}
	defer f.Close()

	enc := plist.NewEncoder(f)
	enc.Indent("    ")
	if err := enc.Encode(XCFrameworkInfoPlist{
		AvailableLibraries: []XCFrameworkAvailableLibrary{
			{
				BinaryPath:               o.conf.Name + ".framework/" + o.conf.Name + ".tbd",
				LibraryIdentifier:        xcfw.LibraryIdentifier,
				LibraryPath:              o.conf.Name + ".framework",
				SupportedArchitectures:   xcfw.SupportedArchitectures,
				SupportedPlatform:        xcfw.SupportedPlatform,
				SupportedPlatformVariant: xcfw.SupportedPlatformVariant,
			},
		},
		CFBundlePackageType:      "XFWK",
		XCFrameworkFormatVersion: "1.0",
	}); err != nil {
		return fmt.Errorf("failed to create XCFramework Info.plist")
	}

	/* create folder structure */
	fwfolder := filepath.Join(xcfolder, xcfw.LibraryIdentifier, o.conf.Name+".framework")
	if err := os.MkdirAll(filepath.Join(fwfolder, "Headers"), 0o750); err != nil {
		return fmt.Errorf("failed to create Headers folder: %w", err)
	}
	if err := os.MkdirAll(filepath.Join(fwfolder, "Modules"), 0o750); err != nil {
		return fmt.Errorf("failed to create Modules folder: %w", err)
	}

	/* generate framework tbd */
	var reexports []string
	if rexps := m.GetLoadsByName("LC_REEXPORT_DYLIB"); len(rexps) > 0 {
		for _, rexp := range rexps {
			reexports = append(reexports, rexp.(*macho.ReExportDylib).Name)
		}
	}
	t, err := tbd.NewTBD(image, reexports, false)
	if err != nil {
		return fmt.Errorf("failed to create tbd: %w", err)
	}
	outTBD, err := t.Generate()
	if err != nil {
		return fmt.Errorf("failed to generate tbd: %w", err)
	}
	outTBD += "...\n"
	tbdFile := filepath.Join(fwfolder, o.conf.Name+".tbd")
	if err = os.WriteFile(tbdFile, []byte(outTBD), 0o660); err != nil {
		return fmt.Errorf("failed to write tbd file %s: %v", tbdFile, err)
	}

	/* generate modulemap */
	if err := os.WriteFile(filepath.Join(fwfolder, "Modules", "module.modulemap"), fmt.Appendf(nil,
		"module %s [system] {\n"+
			"    header \"Headers/%s.h\"\n"+ // NOTE: this SHOULD be the umbrella header
			"    export *\n"+
			"}\n", o.conf.Name, o.conf.Name,
	), 0o660); err != nil {
		return fmt.Errorf("failed to write module.modulemap file: %v", err)
	}

	/* generate XCFramework Library Info.plist */
	f2, err := os.Create(filepath.Join(fwfolder, "Info.plist"))
	if err != nil {
		return fmt.Errorf("failed to create %s: %w", filepath.Join(fwfolder, "Info.plist"), err)
	}
	defer f2.Close()
	enc = plist.NewEncoder(f2)
	enc.Indent("    ")
	if err := enc.Encode(XCFrameworkLibraryInfoPlist{
		BuildMachineOSBuild:           "23E224",
		CFBundleDevelopmentRegion:     "en",
		CFBundleExecutable:            o.conf.Name + ".tbd",
		CFBundleIdentifier:            "com.apple." + strings.ToLower(o.conf.Name),
		CFBundleInfoDictionaryVersion: "6.0",
		CFBundleName:                  o.conf.Name,
		CFBundlePackageType:           "FMWK",
		CFBundleShortVersionString:    "1.0",
		CFBundleSignature:             "????",
		CFBundleVersion:               xcfw.CFBundleVersion,
		DTCompiler:                    "com.apple.compilers.llvm.clang.1_0",
		CFBundleSupportedPlatforms:    []string{xcfw.SupportedPlatform}, // TODO: add variants (or universal dylib/macho support)
		DTPlatformName:                xcfw.SupportedPlatform,
		DTPlatformVersion:             xcfw.DTPlatformVersion,
		DTPlatformBuild:               "",
		DTSDKBuild:                    "23E224",
		DTSDKName:                     fmt.Sprintf("%s%s.internal", xcfw.SupportedPlatform, xcfw.DTPlatformVersion),
		DTXcode:                       "1500",
		DTXcodeBuild:                  "15E6079e", // Xcode 15.3
		LSMinimumSystemVersion:        xcfw.LSMinimumSystemVersion,
	}); err != nil {
		return fmt.Errorf("failed to create XCFramework Info.plist")
	}

	/* generate Headers */
	o.conf.Headers = true
	o.conf.Output = filepath.Join(fwfolder, "Headers")
	return o.Headers()
}

func (o *ObjC) SwiftPackage() error {
	return fmt.Errorf("not implemented yet (coming soon)")
}

/* utils */

func writeHeader(hdr *headerInfo) error {
	var out strings.Builder
	out.WriteString(fmt.Sprintf(
		"//\n"+
			"//   Generated by https://github.com/blacktop/ipsw (%s)\n"+
			"//\n"+
			"//    - LC_BUILD_VERSION:  %s\n"+
			"//    - LC_SOURCE_VERSION: %s\n"+
			"//\n"+
			"#ifndef %s_h\n"+
			"#define %s_h\n",
		hdr.IpswVersion,
		strings.Join(hdr.BuildVersions, "\n//    - LC_BUILD_VERSION:  "),
		hdr.SourceVersion,
		hdr.Name,
		hdr.Name))
	if !hdr.IsUmbrella {
		out.WriteString("@import Foundation;\n")
	}
	out.WriteString("\n")
	if len(hdr.Imports.Imports) > 0 {
		for _, imp := range hdr.Imports.Imports {
			out.WriteString(fmt.Sprintf("#include \"%s\"\n", imp))
		}
	}
	if len(hdr.Imports.Locals) > 0 {
		for _, local := range hdr.Imports.Locals {
			out.WriteString(fmt.Sprintf("#include \"%s\"\n", local))
		}
	}
	if len(hdr.Imports.Imports) > 0 || len(hdr.Imports.Locals) > 0 {
		out.WriteString("\n")
	}
	if len(hdr.Imports.Classes) > 0 {
		out.WriteString(fmt.Sprintf("@class %s;\n", strings.Join(hdr.Imports.Classes, ", ")))
	}
	if len(hdr.Imports.Protos) > 0 {
		out.WriteString(fmt.Sprintf("@protocol %s;\n", strings.Join(hdr.Imports.Protos, ", ")))
	}
	if len(hdr.Imports.Classes) > 0 || len(hdr.Imports.Protos) > 0 {
		out.WriteString("\n")
	}
	out.WriteString(fmt.Sprintf("%s\n", hdr.Object))
	out.WriteString(fmt.Sprintf("#endif /* %s_h */\n", hdr.Name))

	if err := os.MkdirAll(filepath.Dir(hdr.FileName), 0o750); err != nil {
		return err
	}
	log.Infof("Creating %s", hdr.FileName)
	if err := os.WriteFile(hdr.FileName, []byte(out.String()), 0644); err != nil {
		if pe, ok := err.(*os.PathError); ok {
			if pe.Err == syscall.ENAMETOOLONG {
				base := filepath.Base(strings.TrimSuffix(hdr.FileName, filepath.Ext(hdr.FileName)))
				hdr.FileName = filepath.Join(filepath.Dir(hdr.FileName), base[:50]+".h")
				log.Warnf("Filename too long; truncating to '%s'", hdr.FileName)
				if err := os.WriteFile(hdr.FileName, []byte(out.String()), 0644); err != nil {
					return fmt.Errorf("failed to write header %s: %v", hdr.FileName, err)
				}
			}
		}
	}

	return nil
}

func (o *ObjC) processForwardDeclarations(m *macho.File) (map[string]Imports, error) {
	var classNames []string
	var protoNames []string

	imps := make(map[string]Imports)

	classes, err := m.GetObjCClasses()
	if err != nil {
		if !errors.Is(err, macho.ErrObjcSectionNotFound) {
			return nil, err
		}
	}
	slices.SortStableFunc(classes, func(a, b objc.Class) int {
		return cmp.Compare(a.Name, b.Name)
	})
	for _, class := range classes {
		classNames = append(classNames, class.Name)
	}

	protos, err := m.GetObjCProtocols()
	if err != nil {
		if !errors.Is(err, macho.ErrObjcSectionNotFound) {
			return nil, err
		}
	}
	slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
		return cmp.Compare(a.Name, b.Name)
	})
	for _, proto := range protos {
		protoNames = append(protoNames, proto.Name)
	}

	for _, class := range classes {
		var imp Imports
		if superClass := class.SuperClass; superClass != "" {
			if slices.Contains(classNames, superClass) {
				imp.Locals = append(imp.Locals, superClass+".h")
			} else {
				imp.Classes = append(imp.Classes, superClass)
			}
		}
		for _, prot := range class.Protocols {
			if slices.Contains(protoNames, prot.Name) {
				imp.Locals = append(imp.Locals, prot.Name+"-Protocol.h")
			} else {
				imp.Protos = append(imp.Protos, prot.Name)
			}
		}
		for _, ivar := range class.Ivars {
			typ, _ := strings.CutSuffix(ivar.Verbose(), ivar.Name+";")
			if err := o.fillImportsForType(typ, class.Name, "", classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, prop := range class.Props {
			typ := prop.Type()
			if err := o.fillImportsForType(typ, class.Name, "", classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, method := range class.InstanceMethods {
			if method.Types == "" {
				log.Warnf("Instance method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, class.Name, method.TypesVMAddr)
				continue
			}
			if err := o.fillImportsForMethod(method, class.Name, "", classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, method := range class.ClassMethods {
			if method.Types == "" {
				log.Warnf("Class method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, class.Name, method.TypesVMAddr)
				continue
			}
			if err := o.fillImportsForMethod(method, class.Name, "", classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		imp.uniq(o.baseFWs)
		imps[class.Name] = imp
	}

	for _, proto := range protos {
		var imp Imports
		for _, prot := range proto.Prots {
			if slices.Contains(protoNames, prot.Name) {
				imp.Locals = append(imp.Locals, prot.Name+"-Protocol.h")
			} else {
				imp.Protos = append(imp.Protos, prot.Name)
			}
		}
		for _, prop := range proto.InstanceProperties {
			typ := prop.Type()
			if err := o.fillImportsForType(typ, "", proto.Name, classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, method := range proto.InstanceMethods {
			if method.Types == "" {
				log.Warnf("Protocol instance method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
				continue
			}
			if err := o.fillImportsForMethod(method, "", proto.Name, classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, method := range proto.ClassMethods {
			if method.Types == "" {
				log.Warnf("Protocol class method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
				continue
			}
			if err := o.fillImportsForMethod(method, "", proto.Name, classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, method := range proto.OptionalInstanceMethods {
			if method.Types == "" {
				log.Warnf("Protocol optional instance method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
				continue
			}
			if err := o.fillImportsForMethod(method, "", proto.Name, classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		for _, method := range proto.OptionalClassMethods {
			if method.Types == "" {
				log.Warnf("Protocol optional class method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
				continue
			}
			if err := o.fillImportsForMethod(method, "", proto.Name, classNames, protoNames, &imp); err != nil {
				return nil, err
			}
		}
		imp.uniq(o.baseFWs)
		imps[proto.Name+"-Protocol"] = imp
	}

	return imps, nil
}

func (o *ObjC) scanBaseFrameworks() error {
	o.baseFWs["classes"] = []string{}
	o.baseFWs["protocols"] = []string{}
	if o.cache != nil {
		for _, name := range baseFrameworks {
			img, err := o.cache.Image(name)
			if err != nil {
				return err
			}
			m, err := img.GetMacho()
			if err != nil {
				return err
			}

			classes, err := m.GetObjCClasses()
			if err != nil {
				if !errors.Is(err, macho.ErrObjcSectionNotFound) {
					return err
				}
			}
			slices.SortStableFunc(classes, func(a, b objc.Class) int {
				return cmp.Compare(a.Name, b.Name)
			})
			for _, class := range classes {
				o.baseFWs["classes"] = append(o.baseFWs["classes"], class.Name)
			}

			protos, err := m.GetObjCProtocols()
			if err != nil {
				if !errors.Is(err, macho.ErrObjcSectionNotFound) {
					return err
				}
			}
			slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
				return cmp.Compare(a.Name, b.Name)
			})
			for _, proto := range protos {
				o.baseFWs["protocols"] = append(o.baseFWs["protocols"], proto.Name)
			}
			slices.Sort(o.baseFWs["classes"])
			slices.Sort(o.baseFWs["protocols"])
		}
	}
	return nil
}

// fillImportsForMethod adds the imports required by a method's return type and
// its real argument types. ArgumentType indexes the full method type encoding,
// so entry 0 is the return type and entries 1 and 2 are the implicit self/_cmd
// (which never need imports); the real arguments run from entry 3 through
// NumberOfArguments() inclusive.
func (o *ObjC) fillImportsForMethod(method objc.Method, className string, protoName string, classNames []string, protoNames []string, imp *Imports) error {
	for i := 0; i <= method.NumberOfArguments(); i++ {
		typ := method.ArgumentType(i)
		// On a malformed/truncated encoding NumberOfArguments() (which counts via
		// a different parser) can exceed the decodable argument count, so
		// ArgumentType returns its out-of-range sentinel; stop before it is
		// mistaken for a real type (e.g. "<error>" looks like a protocol).
		if typ == "<error>" {
			break
		}
		if err := o.fillImportsForType(typ, className, protoName, classNames, protoNames, imp); err != nil {
			return err
		}
		if i == 0 {
			i += 2 // skip self (@) and SEL (:)
		}
	}
	return nil
}

func (o *ObjC) fillImportsForType(typ string, className string, protoName string, classNames []string, protoNames []string, imp *Imports) error {
	typ = strings.Trim(typ, ` *`)
	if typ == "" {
		return nil
	}

	// Check for outermost braces first
	if hasOutermostDelimiters(typ, "{", "}") {
		lTyp, mTyps, _, err := o.contextualSplit(typ, "{", ";", "}")
		if err != nil {
			return err
		}

		if err := o.fillImportsForType(lTyp, className, protoName, classNames, protoNames, imp); err != nil {
			return err
		}

		for _, mTyp := range mTyps {
			mTyp = strings.ReplaceAll(mTyp, `&`, ``)
			if err := o.fillImportsForType(mTyp, className, protoName, classNames, protoNames, imp); err != nil {
				return err
			}
		}

		return nil
	}

	// Check for outermost angle brackets
	if hasOutermostDelimiters(typ, "<", ">") {
		lTyp, mTyps, _, err := o.contextualSplit(typ, "<", ",", ">")
		if err != nil {
			return err
		}

		if err := o.fillImportsForType(lTyp, className, protoName, classNames, protoNames, imp); err != nil {
			return err
		}

		if strings.HasPrefix(lTyp, "struct ") || strings.Contains(lTyp, "::") {
			// C++ Template

			for _, mTyp := range mTyps {
				if err := o.fillImportsForType(mTyp, className, protoName, classNames, protoNames, imp); err != nil {
					return err
				}
			}
		} else {
			// Objective-C Qualifier

			for _, mTyp := range mTyps {
				mTyp := strings.Trim(mTyp, ` *`)
				if mTyp == "" {
					continue
				}

				if !slices.Contains(protoNames, mTyp) {
					imp.Protos = append(imp.Protos, mTyp)
					continue
				}

				if mTyp != protoName {
					imp.Locals = append(imp.Locals, mTyp+"-Protocol.h")
				}
			}
		}

		return nil
	}

	typ = o.nonBuiltInType(typ)
	if typ == "" {
		return nil
	}

	if !slices.Contains(classNames, typ) {
		imp.Classes = append(imp.Classes, typ)
		return nil
	}

	if typ != className {
		imp.Locals = append(imp.Locals, typ+".h")
	}

	return nil
}

func (o *ObjC) nonBuiltInType(typ string) string {
	if typ == "" {
		return ""
	}

	// A real type name never contains these — aggregates ({}, <>) are handled by
	// the callers above, so anything reaching here with structural punctuation
	// (including a stray '=' from a truncated struct encoding) is a decode
	// fragment, not an importable type.
	if strings.ContainsAny(typ, "()[]{}:;=") {
		return ""
	}

	switch before, after, _ := strings.Cut(typ, " "); before {
	case "_Bool", "_Complex", "_Imaginary", "BOOL", "Class", "IMP", "Ivar", "Method", "SEL", "bool", "char", "class", "double", "enum", "float", "id", "int", "long", "short", "signed", "struct", "union", "unsigned", "void":
		return ""

	case "_Atomic", "bycopy", "byref", "const", "in", "inout", "oneway", "out", "restrict", "volatile":
		return o.nonBuiltInType(after)
	}

	if idx := strings.Index(typ, "*"); idx != -1 {
		typ = typ[:idx]
	}
	return strings.TrimSpace(typ)
}

func (o *ObjC) contextualSplit(typ string, lDelim string, mDelim string, rDelim string) (string, []string, string, error) {
	// Find the outermost (last) left delimiter by scanning backwards
	lDelimIdx := -1
	level := 0

	// Scan backwards to find the outermost left delimiter
	for i := len(typ) - 1; i >= 0; i-- {
		char := string(typ[i])

		// Handle closing delimiters (going backwards)
		if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
			level++
		}
		// Handle opening delimiters (going backwards)
		if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
			level--
			// If we're at level 0 and found our target left delimiter, this is the outermost one
			if level == 0 && char == lDelim {
				lDelimIdx = i
				break
			}
		}
	}

	if lDelimIdx == -1 {
		return "", nil, "", errors.New("contextualSplit failed: left delimiter not found")
	}

	// Now find the matching right delimiter going forwards from the left delimiter
	rDelimIdx := -1
	level = 0

	for i := lDelimIdx; i < len(typ); i++ {
		char := string(typ[i])

		// Handle opening delimiters
		if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
			level++
		}
		// Handle closing delimiters
		if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
			level--
			// If we're back to level 0 and this is our target right delimiter, we found the match
			if level == 0 && char == rDelim {
				rDelimIdx = i
				break
			}
		}
	}

	if rDelimIdx == -1 {
		return "", nil, "", errors.New("contextualSplit failed: matching right delimiter not found")
	}

	l := typ[:lDelimIdx]
	m := typ[lDelimIdx+1 : rDelimIdx]
	r := typ[rDelimIdx+1:]

	// Split the middle part by mDelim, respecting nested delimiters
	var typs []string
	level = 0
	start := 0

	for i := 0; i < len(m); i++ {
		char := string(m[i])

		// Track nesting level for all bracket types
		if char == "<" || char == "{" || char == "(" || char == "[" {
			level++
		} else if char == ">" || char == "}" || char == ")" || char == "]" {
			level--
		} else if level == 0 && char == mDelim {
			// Only split on mDelim when we're at the top level (not inside brackets)
			typs = append(typs, strings.TrimSpace(m[start:i]))
			start = i + 1
		}
	}

	// Add the last part
	if start < len(m) {
		typs = append(typs, strings.TrimSpace(m[start:]))
	}

	return l, typs, r, nil
}

// hasOutermostDelimiters checks if the given delimiters exist at the outermost level
// (not nested inside other delimiters)
func hasOutermostDelimiters(typ string, lDelim string, rDelim string) bool {
	lDelimIdx := -1
	level := 0

	// Scan backwards to find the outermost left delimiter
	for i := len(typ) - 1; i >= 0; i-- {
		char := string(typ[i])

		// Handle closing delimiters (going backwards)
		if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
			level++
		}
		// Handle opening delimiters (going backwards)
		if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
			level--
			// If we're at level 0 and found our target left delimiter, this is the outermost one
			if level == 0 && char == lDelim {
				lDelimIdx = i
				break
			}
		}
	}

	if lDelimIdx == -1 {
		return false
	}

	// Now check if there's a matching right delimiter
	level = 0
	for i := lDelimIdx; i < len(typ); i++ {
		char := string(typ[i])

		// Handle opening delimiters
		if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
			level++
		}
		// Handle closing delimiters
		if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
			level--
			// If we're back to level 0 and this is our target right delimiter, we found the match
			if level == 0 && char == rDelim {
				return true
			}
		}
	}

	return false
}
