//go:build darwin && cgo

package dyld

/*
#cgo CFLAGS: -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include
#cgo CFLAGS: -Wno-nullability-completeness
#cgo LDFLAGS: -ldl
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include <dlfcn.h>
int
dsc_extract(void *f, const char* shared_cache_file_path, const char* extraction_root_path){
    int (*extractor_proc)(const char* shared_cache_file_path, const char* extraction_root_path,
                          void (^progress)(unsigned current, unsigned total));
    extractor_proc = f;
    int result = (*extractor_proc)(shared_cache_file_path, extraction_root_path,
                                   ^(unsigned c, unsigned total) { printf("%d/%d\n", c, total); });
    return result;
}
*/
import "C"

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
	"unsafe"

	"github.com/apex/log"
	"github.com/blacktop/go-macho"
	"github.com/blacktop/go-plist"
	"github.com/blacktop/ipsw/internal/utils"
	"github.com/pkg/errors"
)

var xcodePaths = []string{
	"/Applications/Xcode-beta.app",
	"/Applications/Xcode.app",
}

// LibHandle represents an open handle to a library
type LibHandle struct {
	Handle  unsafe.Pointer
	Libname string
}

type XCodeInfoPlist struct {
	ExtractorVersion string    `plist:"DSC Extractor Version,omitempty"`
	DateCollected    time.Time `plist:"DateCollected,omitempty"`
	XCodeVersion     string    `plist:"Version,omitempty"`
}
type XCodeAppInfoPlist struct {
	CFBundleShortVersionString string `plist:"CFBundleShortVersionString,omitempty"`
}

// GetHandle returns a handle to a library
func GetHandle(libs []string) *LibHandle {
	for _, name := range libs {
		libname := C.CString(name)
		defer C.free(unsafe.Pointer(libname))
		handle := C.dlopen(libname, C.RTLD_LAZY)
		if handle != nil {
			utils.Indent(log.Debug, 2)(fmt.Sprintf("Using bundle: %s", name))
			h := &LibHandle{
				Handle:  handle,
				Libname: name,
			}
			return h
		}
	}
	return nil
}

// GetSymbolPointer takes a symbol name and returns a pointer to the symbol.
func (l *LibHandle) GetSymbolPointer(symbol string) (unsafe.Pointer, error) {
	sym := C.CString(symbol)
	defer C.free(unsafe.Pointer(sym))

	C.dlerror()
	p := C.dlsym(l.Handle, sym)
	e := C.dlerror()
	if e != nil {
		return nil, fmt.Errorf("error resolving symbol %q: %v", symbol, errors.New(C.GoString(e)))
	}

	return p, nil
}

// Close closes a LibHandle.
func (l *LibHandle) Close() error {
	C.dlerror()
	C.dlclose(l.Handle)
	e := C.dlerror()
	if e != nil {
		return fmt.Errorf("error closing %v: %v", l.Libname, errors.New(C.GoString(e)))
	}

	return nil
}

// Split extracts all the dyld_shared_cache libraries
func Split(dyldSharedCachePath, destinationPath, xcodePath string, xcodeCache bool) error {
	var xcodeVersion string
	var xcodePathProvided bool

	if len(xcodePath) == 0 {
		var err error
		xcodePath, err = utils.GetXCodePath()
		if err != nil {
			return fmt.Errorf("failed to get Xcode path: %v", err)
		}
		xcodePathProvided = false
	} else {
		if !strings.HasSuffix(xcodePath, "Contents/Developer") {
			xcodePath = filepath.Join(xcodePath, "Contents/Developer")
		}
		xcodePathProvided = true
	}

	dscExtractorPath := filepath.Join(xcodePath, "Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle")
	_, err := os.Stat(dscExtractorPath)
	if err != nil {
		err_msg := "failed to find DSC extractor library in %s"
		if !xcodePathProvided {
			err_msg += " (use --xcode to specify the path to Xcode)"
		}
		return fmt.Errorf(err_msg, dscExtractorPath)
	}

	dscExtractor := GetHandle([]string{dscExtractorPath})
	if dscExtractor == nil {
		err_msg := "unable to open a handle to the DSC extractor library in %s"
		if !xcodePathProvided {
			err_msg += " (use --xcode to specify the path to Xcode)"
		}

		return fmt.Errorf(err_msg, dscExtractorPath)
	}

	if xcodeCache {
		// get ExtractorVersion
		extVer := "1040.2.2.0.0"

		fat, err := macho.OpenFat(dscExtractor.Libname)
		switch err {
		case nil:
			// successfully opened fat mach-o
			if len(fat.Arches) > 0 {
				if sv := fat.Arches[0].SourceVersion(); sv != nil {
					extVer = sv.Version.String()
				}
			}
			fat.Close()

		case macho.ErrNotFat:
			// fall back on thin mach-o loading
			m, err := macho.Open(dscExtractor.Libname)
			if err != nil {
				return fmt.Errorf("failed to open mach-o %s, %v", dscExtractor.Libname, err)
			}
			if sv := m.SourceVersion(); sv != nil {
				extVer = sv.Version.String()
			}
			m.Close()

		default:
			// failed to load fat mach-o
			return fmt.Errorf("failed to open fat mach-o %s: %v", dscExtractor.Libname, err)
		}

		// get XCodeVersion
		xcodeContentPath := strings.TrimSuffix(dscExtractor.Libname, "/Developer/Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle")
		xcodeContentPath = filepath.Join(xcodeContentPath, "Info.plist")
		data, err := os.ReadFile(xcodeContentPath)
		if err != nil {
			return fmt.Errorf("failed to read %s: %v", xcodeContentPath, err)
		}
		appInfo := XCodeAppInfoPlist{}
		if err := plist.NewDecoder(bytes.NewReader(data)).Decode(&appInfo); err != nil {
			return fmt.Errorf("failed to decode %s: %v", xcodeContentPath, err)
		}
		xcodeVersion = "14.0"
		if len(appInfo.CFBundleShortVersionString) > 0 {
			xcodeVersion = appInfo.CFBundleShortVersionString
		}
		// write Info.plist
		infoPlistPath := filepath.Join(destinationPath, "Info.plist")
		data, err = plist.MarshalIndent(XCodeInfoPlist{
			ExtractorVersion: extVer,
			DateCollected:    time.Now(),
			XCodeVersion:     xcodeVersion,
		}, plist.XMLFormat, "\t")
		if err != nil {
			return fmt.Errorf("failed to marshal Xcode cache Info.plist: %v", err)
		}
		if err := os.WriteFile(infoPlistPath, data, 0644); err != nil {
			return fmt.Errorf("failed to write %s: %v", infoPlistPath, err)
		}

		destinationPath = filepath.Join(destinationPath, "Symbols")

		dscCopyPath := filepath.Join(destinationPath, "private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/")

		matches, err := filepath.Glob(filepath.Join(filepath.Dir(dyldSharedCachePath), "dyld_shared_cache_*"))
		if err != nil {
			return fmt.Errorf("failed to glob dyld_shared_cache_*: %v", err)
		}
		if err := os.MkdirAll(dscCopyPath, 0750); err != nil {
			return fmt.Errorf("failed to create output directory %s: %v", dscCopyPath, err)
		}
		for _, match := range matches {
			f, err := os.Create(filepath.Join(dscCopyPath, ".copied_"+filepath.Base(match)))
			if err != nil {
				return fmt.Errorf("failed to create .copied_%s: %v", match, err)
			}
			if err := f.Close(); err != nil {
				return fmt.Errorf("failed to close %s: %v", f.Name(), err)
			}
			if err := utils.Copy(match, filepath.Join(dscCopyPath, filepath.Base(match))); err != nil {
				return fmt.Errorf("failed to copy %s to %s: %v", match, dscCopyPath, err)
			}
		}
	}

	extractorProc, err := dscExtractor.GetSymbolPointer("dyld_shared_cache_extract_dylibs_progress")
	if err != nil {
		return fmt.Errorf("failed to get symbol 'dyld_shared_cache_extract_dylibs_progress' pointer: %v", err)
	}

	dscPath := C.CString(dyldSharedCachePath)
	defer C.free(unsafe.Pointer(dscPath))

	destPath := C.CString(destinationPath)
	defer C.free(unsafe.Pointer(destPath))

	result := C.dsc_extract(extractorProc, dscPath, destPath)
	if result != 0 {
		return fmt.Errorf("failed to run dsc_extract: returned %d", result)
	}

	if err := dscExtractor.Close(); err != nil {
		return fmt.Errorf("failed to close dylib %s: %v", dscExtractor.Libname, err)
	}

	return nil
}
