package devicetree

import (
	"archive/zip"
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"slices"
	"time"

	"fmt"
	"io"
	"math"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"

	"github.com/apex/log"
	"github.com/blacktop/go-macho/types"
	"github.com/blacktop/ipsw/internal/utils"
	"github.com/blacktop/ipsw/pkg/img3"
	"github.com/blacktop/ipsw/pkg/img4"
	"github.com/blacktop/ipsw/pkg/lzfse"
)

// Img4 DeviceTree object
type Img4 struct {
	IM4P     string
	Name     string
	Version  string
	Data     []byte
	KbagData []byte `asn1:"optional"`
}

// Node object
type Node struct {
	NumProperties uint32 // Number of props[] elements (0 => end)
	NumChildren   uint32 // Number of children[] elements
}

// NodeProperty object
type NodeProperty struct {
	Name   [32]byte // NUL terminated property name (max length 32)
	Length uint32   // Length (bytes) of folloing prop value
}

// Properties object
type Properties map[string]any

// DeviceTree object
type DeviceTree map[string]Properties

type dtCPU struct {
	Name string
	Type string
	ARM  string
}

// Summary object
type Summary struct {
	ProductName        string
	ProductDescription string
	ProductType        string
	BoardConfig        string
	SocName            string
	DeviceType         string
	SocGeneration      string
	CPUs               []dtCPU
	Timestamp          time.Time
}

// Summary prints out a summary of the DeviceTree
func (dtree *DeviceTree) Summary() (*Summary, error) {
	summary := &Summary{}

	children := (*dtree)["device-tree"]["children"]

	if children == nil {
		return nil, fmt.Errorf("failed to get device tree node children")
	}

	switch reflect.TypeOf(children).Kind() {
	case reflect.Slice:
		s := reflect.ValueOf(children)
		for i := range s.Len() {
			child := s.Index(i)
			c := child.Interface().(DeviceTree)
			if product, ok := (c)["product"]["product-name"].(string); ok {
				summary.ProductName = product
			}
			if productDesc, ok := (c)["product"]["product-description"].(string); ok {
				summary.ProductDescription = productDesc
			}
			if socName, ok := (c)["product"]["product-soc-name"].(string); ok {
				summary.SocName = socName
			}
			if devType, ok := (c)["arm-io"]["compatible"].(string); ok {
				summary.DeviceType = strings.TrimPrefix(devType, "arm-io,")
			}
			if socGeneration, ok := (c)["arm-io"]["soc-generation"].(string); ok {
				summary.SocGeneration = socGeneration
			}
			if cpus, ok := (c)["cpus"]["children"]; ok {
				for idx, cpu := range cpus.([]DeviceTree) {
					if cpuN, ok := cpu[fmt.Sprintf("cpu%d", idx)]; ok {
						if compat, ok := cpuN["compatible"].([]string); ok {
							c := dtCPU{}
							if len(compat) == 2 {
								c.Name = strings.TrimPrefix(compat[0], "apple,")
								c.ARM = strings.TrimPrefix(compat[1], "ARM,")
							}
							if clusterType, ok := cpuN["cluster-type"].(string); ok {
								c.Type = clusterType
							}
							summary.CPUs = append(summary.CPUs, c)
						}
					}
				}
			}
		}
	}

	if model, ok := (*dtree)["device-tree"]["model"].(string); ok {
		summary.ProductType = model
		compatible := (*dtree)["device-tree"]["compatible"]
		switch reflect.TypeOf(compatible).Kind() {
		case reflect.Slice:
			s := reflect.ValueOf(compatible)
			for i := range s.Len() {
				elem := s.Index(i).String()
				if !strings.Contains(elem, "Apple") && !strings.Contains(elem, model) {
					summary.BoardConfig = elem
				}
			}
		}
	} else {
		return nil, fmt.Errorf("devicetree model is not a string")
	}

	if stamp, ok := (*dtree)["device-tree"]["time-stamp"].(string); ok {
		layout := "Mon Jan 2 15:04:05 MST 2006"
		if location, err := time.LoadLocation("PST8PDT"); err == nil {
			t, err := time.ParseInLocation(layout, stamp, location)
			if err != nil {
				return nil, err
			}
			zone, _ := time.Now().Zone()
			location, err = time.LoadLocation(zone)
			if err != nil {
				// return nil, fmt.Errorf("failed to load location %s: %v", zone, err)
				summary.Timestamp = t
			} else {
				summary.Timestamp = t.In(location)
			}
		} else {
			summary.Timestamp, err = time.Parse(layout, stamp)
			if err != nil {
				return nil, fmt.Errorf("failed to parse device-tree time-stamp: %v", err)
			}
		}
	}

	return summary, nil
}

func printNode(out *strings.Builder, node Properties, depth int) {
	for k, v := range node {
		switch k {
		case "children":
			switch reflect.TypeOf(v).Kind() {
			case reflect.Slice:
				s := reflect.ValueOf(v)
				for i := range s.Len() {
					child := s.Index(i)
					for kk, vv := range child.Interface().(DeviceTree) {
						out.WriteString(fmt.Sprintf("%s%s:\n", strings.Repeat(" ", depth+2), kk))
						printNode(out, vv, depth+4)
					}
				}
			}
		default:
			switch vv := v.(type) {
			case int:
				if vv == 0 || vv < 1000 {
					out.WriteString(fmt.Sprintf("%s%s: %d\n", strings.Repeat(" ", depth), k, vv))
				} else {
					out.WriteString(fmt.Sprintf("%s%s: %#x\n", strings.Repeat(" ", depth), k, vv))
				}
			case uint16:
				if vv == 0 || vv < 1000 {
					out.WriteString(fmt.Sprintf("%s%s: %d\n", strings.Repeat(" ", depth), k, vv))
				} else {
					out.WriteString(fmt.Sprintf("%s%s: %#x\n", strings.Repeat(" ", depth), k, vv))
				}
			case uint32:
				if vv == 0 || vv < 1000 {
					out.WriteString(fmt.Sprintf("%s%s: %d\n", strings.Repeat(" ", depth), k, vv))
				} else {
					out.WriteString(fmt.Sprintf("%s%s: %#x\n", strings.Repeat(" ", depth), k, vv))
				}
			case uint64:
				if vv == 0 || vv < 1000 {
					out.WriteString(fmt.Sprintf("%s%s: %d\n", strings.Repeat(" ", depth), k, vv))
				} else {
					out.WriteString(fmt.Sprintf("%s%s: %#x\n", strings.Repeat(" ", depth), k, vv))
				}
			case string:
				out.WriteString(fmt.Sprintf("%s%s: \"%s\"\n", strings.Repeat(" ", depth), k, vv))
			case []string:
				for _, s := range vv {
					out.WriteString(fmt.Sprintf("%s%s: \"%s\"\n", strings.Repeat(" ", depth+2), k, s))
				}
			case pmgr_dev:
				out.WriteString(fmt.Sprintf("%s%s: \n%s\n", strings.Repeat(" ", depth), k, vv.String(depth+2)))
			case []pmgr_dev:
				for _, dev := range vv {
					out.WriteString(fmt.Sprintf("%s%s: \n%s\n", strings.Repeat(" ", depth), k, dev.String(depth+2)))
				}
			case pmgr_map:
				out.WriteString(fmt.Sprintf("%s%s: reg=%#x off=%#x unk=%#x\n", strings.Repeat(" ", depth), k, vv.Reg, vv.Off, vv.Unk))
			case []pmgr_map:
				for _, m := range vv {
					out.WriteString(fmt.Sprintf("%s%s: reg=%#x off=%#x unk=%#x\n", strings.Repeat(" ", depth), k, m.Reg, m.Off, m.Unk))
				}
			case pmgr_reg:
				out.WriteString(fmt.Sprintf("%s%s: addr=%#x sz=%#x\n", strings.Repeat(" ", depth), k, vv.Addr, vv.Size))
			case []pmgr_reg:
				for _, reg := range vv {
					out.WriteString(fmt.Sprintf("%s%s: addr=%#x sz=%#x\n", strings.Repeat(" ", depth+2), k, reg.Addr, reg.Size))
				}
			case PmapIORange:
				out.WriteString(fmt.Sprintf("%s%s: %#v\n", strings.Repeat(" ", depth), k, vv))
			case []PmapIORange:
				out.WriteString(fmt.Sprintf("%s%s:\n", strings.Repeat(" ", depth), k))
				for _, pmap := range vv {
					out.WriteString(fmt.Sprintf("%s\"%s\" start=%#x sz=%#x flags=%#x\n", strings.Repeat(" ", depth+2), pmap.Name[:], pmap.Start, pmap.Size, pmap.Flags))
				}
			case []region:
				out.WriteString(fmt.Sprintf("%s%s:\n", strings.Repeat(" ", depth), k))
				for _, reg := range vv {
					out.WriteString(fmt.Sprintf("%sstart=%#06x end=%#06x\n", strings.Repeat(" ", depth+2), reg.Start, reg.End))
				}
			default:
				out.WriteString(fmt.Sprintf("%s%s: %v\n", strings.Repeat(" ", depth), k, vv))
			}
		}
	}
}

func (dtree *DeviceTree) String() string {
	var out strings.Builder
	for k, v := range *dtree {
		out.WriteString(fmt.Sprintf("%s:\n", k))
		printNode(&out, v, 2)
	}
	return out.String()
}

// GetProductName returns the device-trees product names
func (dtree *DeviceTree) GetProductName() (string, error) {
	children := (*dtree)["device-tree"]["children"]

	switch reflect.TypeOf(children).Kind() {
	case reflect.Slice:
		s := reflect.ValueOf(children)
		for i := range s.Len() {
			child := s.Index(i)
			c := child.Interface().(DeviceTree)
			if product, ok := (c)["product"]["product-name"].(string); ok {
				return product, nil
			}
		}
	}
	return "", fmt.Errorf("failed to get product-name")
}

// GetBoardConfig returns the device-trees board config
func (dtree *DeviceTree) GetBoardConfig() (string, error) {
	if model, ok := (*dtree)["device-tree"]["model"].(string); ok {
		utils.Indent(log.Info, 2)(fmt.Sprintf("Model: %s", model))
		compatible := (*dtree)["device-tree"]["compatible"]
		switch reflect.TypeOf(compatible).Kind() {
		case reflect.Slice:
			s := reflect.ValueOf(compatible)
			for i := range s.Len() {
				elem := s.Index(i).String()
				if !strings.Contains(elem, "Apple") && !strings.Contains(elem, model) {
					return elem, nil
				}
			}
		}
	}
	return "", fmt.Errorf("failed to get board-config")
}

// GetModel returns the device-trees model
func (dtree *DeviceTree) GetModel() (string, error) {
	if model, ok := (*dtree)["device-tree"]["model"].(string); ok {
		return model, nil
	}
	return "", fmt.Errorf("failed to get model")
}

func isZero(bytes []byte) bool {
	b := byte(0)
	for _, s := range bytes {
		b |= s
	}
	return b == 0
}

func parseInt(value []byte) any {
	if len(value) == 0 {
		return nil
	}
	switch len(value) {
	case binary.Size(uint8(0)):
		return uint8(value[0])
	case binary.Size(uint16(0)):
		if bytes.HasSuffix(value, []byte("\xff")) {
			return int16(binary.LittleEndian.Uint16(value))
		} else {
			return uint16(binary.LittleEndian.Uint16(value))
		}
	case binary.Size(uint32(0)):
		if bytes.HasSuffix(value, []byte("\xff")) {
			return int32(binary.LittleEndian.Uint32(value))
		} else {
			return uint32(binary.LittleEndian.Uint32(value))
		}
	case binary.Size(uint64(0)):
		if bytes.HasSuffix(value, []byte("\xff")) {
			return int64(binary.LittleEndian.Uint64(value))
		} else {
			return uint64(binary.LittleEndian.Uint64(value))
		}
	default:
		return parseValue(value)
	}
}

func parseValue(value []byte) any {
	if len(value) == 0 {
		return nil
	}

	if !bytes.HasPrefix(value, []byte("\x00")) {
		// remove trailing NULLs
		str := bytes.TrimRight(value[:], "\x00")
		// value is a string
		if utils.IsASCII(string(str)) {
			if len(str) == 0 && len(value) <= binary.Size(uint64(0)) { // detect 0 (not empty string)
				if i, err := binary.Uvarint(value); err > 0 {
					return i
				}
			}
			return string(str)
		}
		if len(value) > 4 {
			size := binary.LittleEndian.Uint32(value[:4])
			if size <= uint32(len(value)-4) && !bytes.Contains(value[4:4+size], []byte("\x00")) {
				if utils.IsASCII(string(value[4 : 4+size])) {
					return string(value[4 : 4+size])
				}
			}
		}
		parts := bytes.Split(str, []byte("\x00"))
		if len(parts) > 1 { // value is a string array
			var values []string
			for _, part := range parts {
				if len(string(part)) > 0 {
					if utils.IsASCII(string(part)) {
						values = append(values, string(part))
					} // else {
					// 	values = append(values, base64.StdEncoding.EncodeToString(value))
					// }
				}
			}
			if len(values) > 0 {
				return values
			}
		}
	}

	if isZero(value) {
		return 0
	}

	switch len(value) {
	case binary.Size(uint8(0)):
		return uint8(value[0])
	case binary.Size(uint16(0)):
		if bytes.HasSuffix(value, []byte("\xff")) {
			return int16(binary.LittleEndian.Uint16(value))
		} else {
			return uint16(binary.LittleEndian.Uint16(value))
		}
	case binary.Size(uint32(0)):
		if bytes.HasSuffix(value, []byte("\xff")) {
			return int32(binary.LittleEndian.Uint32(value))
		} else {
			return uint32(binary.LittleEndian.Uint32(value))
		}
	case binary.Size(uint64(0)):
		if bytes.HasSuffix(value, []byte("\xff")) {
			return int64(binary.LittleEndian.Uint64(value))
		} else {
			return uint64(binary.LittleEndian.Uint64(value))
		}
	}
	// value is data
	return base64.StdEncoding.EncodeToString(value)
}

func parseOffSz(value []byte) any {
	switch {
	case len(value) > 4:
		kind := binary.LittleEndian.Uint32(value[:4])
		_ = kind
		switch len(value[4:]) {
		case binary.Size(uint8(0)):
			return uint8(value[4])
		case binary.Size(uint16(0)):
			return uint16(binary.LittleEndian.Uint16(value[4:]))
		case binary.Size(uint32(0)):
			return uint32(binary.LittleEndian.Uint32(value[4:]))
		case binary.Size(uint64(0)):
			return uint64(binary.LittleEndian.Uint64(value[4:]))
		}
	}
	return parseValue(value)
}

func parseReg(value []byte) any {
	switch len(value) {
	case binary.Size(pmgr_reg{}):
		var reg pmgr_reg
		if err := binary.Read(bytes.NewReader(value), binary.LittleEndian, &reg); err != nil {
			return parseValue(value)
		}
		return reg
	case binary.Size(uint32(0)):
		var reg uint32
		if err := binary.Read(bytes.NewReader(value), binary.LittleEndian, &reg); err != nil {
			return parseValue(value)
		}
		return reg
	default:
		if len(value)%binary.Size(pmgr_reg{}) == 0 {
			regs := make([]pmgr_reg, len(value)/binary.Size(pmgr_reg{}))
			if err := binary.Read(bytes.NewReader(value), binary.LittleEndian, &regs); err != nil {
				return parseValue(value)
			}
			return regs
		}
		return parseValue(value)
	}
}

func parseAddr(value []byte) any {
	if len(value) == binary.Size(uint64(0)) {
		var addr uint64
		if err := binary.Read(bytes.NewReader(value), binary.LittleEndian, &addr); err != nil {
			return parseValue(value)
		}
		return addr
	}
	return parseValue(value)
}

type PmapIORange struct {
	Start uint64
	Size  uint64
	Flags uint32
	Name  [4]byte
}

func (p *PmapIORange) MarshalJSON() ([]byte, error) {
	return json.Marshal(&struct {
		Start uint64 `json:"start,omitempty"`
		Size  uint64 `json:"size,omitempty"`
		Flags uint32 `json:"flags,omitempty"`
		Name  string `json:"name,omitempty"`
	}{
		Start: p.Start,
		Size:  p.Size,
		Flags: p.Flags,
		Name:  string(p.Name[:]),
	})
}

func parsePmapIORanges(value []byte) any {
	var ranges []PmapIORange
	r := bytes.NewReader(value)
	for {
		var pmap PmapIORange
		err := binary.Read(r, binary.LittleEndian, &pmap)
		if err != nil {
			if err == io.EOF {
				break
			}
			return parseValue(value)
		}
		slices.Reverse(pmap.Name[:])
		ranges = append(ranges, pmap)
	}
	return ranges
}

type region struct {
	Start uint64
	End   uint64
}

func parseRegions(value []byte) any {
	var regions []region
	r := bytes.NewReader(value)
	for {
		var reg region
		err := binary.Read(r, binary.LittleEndian, &reg)
		if err != nil {
			if err == io.EOF {
				break
			}
			return parseValue(value)
		}
		regions = append(regions, reg)
	}
	return regions
}

func parseNode(buffer io.Reader) (Node, error) {
	var node Node
	// Read a Node from the buffer
	if err := binary.Read(buffer, binary.LittleEndian, &node); err != nil {
		return Node{}, err
	}
	return node, nil
}

func parseNodeProperty(buffer io.Reader, propName string) (string, any, error) {
	var nProp NodeProperty
	// Read a NodeProperty from the buffer
	if err := binary.Read(buffer, binary.LittleEndian, &nProp); err != nil {
		return "", nil, err
	}
	// 4 byte align the length
	nProp.Length &= math.MaxInt32
	if (nProp.Length % 4) != 0 {
		nProp.Length = nProp.Length + (4 - (nProp.Length % 4))
	}
	// Read property value from the buffer
	dat := make([]byte, nProp.Length)
	if err := binary.Read(buffer, binary.LittleEndian, &dat); err != nil {
		return "", nil, err
	}

	key := string(bytes.TrimRight(nProp.Name[:], "\x00"))
	var value any
	switch key {
	case "AAPL,phandle":
		value = parseInt(dat)
	case "platform-name":
		value = string(bytes.TrimRight(dat[:], "\x00"))
	case "pmap-io-ranges":
		value = parsePmapIORanges(dat)
	case "ps-regs":
		value = parsePmgrMap(dat)
	case "devices":
		value = parsePmgrDevices(dat)
	case "regions":
		value = parseRegions(dat)
	case "reg-private":
		value = parseAddr(dat)
	case "value":
		if strings.HasPrefix(propName, "__MACHO") {
			value = parseOffSz(dat)
		} else {
			value = parseValue(dat)
		}
	case "reg":
		value = parseReg(dat)
	case "uuid":
		if len(dat) == 16 {
			value = types.UUID(dat).String()
		} else {
			value = parseValue(dat)
		}
	default:
		value = parseValue(dat)
	}

	return key, value, nil
}

func getProperties(buffer io.Reader, node Node) (string, DeviceTree, error) {

	var nodeName string
	props := Properties{}

	for range int(node.NumProperties) {
		key, value, err := parseNodeProperty(buffer, nodeName)
		if err != nil {
			return "", DeviceTree{}, err
		}
		// log.WithFields(log.Fields{"key": key, "value": value}).Debug("extracted property")
		if strings.EqualFold("name", key) {
			if str, ok := value.(string); ok {
				nodeName = str
			} else {
				return "", DeviceTree{}, fmt.Errorf("failed to assigned nodeName to: %#v", value)
			}
		} else {
			props[key] = value
		}
	}

	return nodeName, DeviceTree{nodeName: props}, nil
}

func parseProperties(r io.Reader, node Node, parent DeviceTree) (DeviceTree, error) {

	name, parent, err := getProperties(r, node)
	if err != nil {
		return DeviceTree{}, err
	}

	children := []DeviceTree{}
	for range int(node.NumChildren) {
		cNode, err := parseNode(r)
		if err != nil {
			return DeviceTree{}, err
		}

		cProps, err := parseProperties(r, cNode, DeviceTree{})
		if err != nil {
			return DeviceTree{}, err
		}
		children = append(children, cProps)
	}
	parent[name]["children"] = children

	return parent, nil
}

func parseDeviceTree(r io.Reader) (*DeviceTree, error) {

	// Read a Node from the buffer
	node, err := parseNode(r)
	if err != nil {
		return nil, err
	}

	dtree, err := parseProperties(r, node, DeviceTree{})
	if err != nil {
		return nil, err
	}

	return &dtree, nil
}

func ParseData(r io.Reader) (*DeviceTree, error) {
	return parseDeviceTree(r)
}

func DecryptIm4pData(data, iv, key []byte) ([]byte, error) {
	i, err := img4.ParsePayload(data)
	if err != nil {
		return nil, fmt.Errorf("failed to parse IM4P: %v", err)
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("failed to create AES cipher: %v", err)
	}

	if len(i.Data) < aes.BlockSize {
		return nil, fmt.Errorf("im4p data too short")
	}

	// CBC mode always works in whole blocks.
	if (len(i.Data) % aes.BlockSize) != 0 {
		return nil, fmt.Errorf("im4p data is not a multiple of the block size")
	}

	mode := cipher.NewCBCDecrypter(block, iv)

	mode.CryptBlocks(i.Data, i.Data)

	if bytes.Contains(i.Data[:4], []byte("bvx2")) {
		utils.Indent(log.Debug, 2)("Detected LZFSE compression")
		dat, err := lzfse.NewDecoder(i.Data).DecodeBuffer()
		if err != nil {
			return nil, fmt.Errorf("failed to lzfse decompress: %v", err)
		}
		return dat, nil
	}

	return i.Data, nil
}

// Parse parses plist files in a local ipsw file
func Parse(ipswPath string, keys ...string) (map[string]*DeviceTree, error) {
	dt := make(map[string]*DeviceTree)

	zr, err := zip.OpenReader(ipswPath)
	if err != nil {
		return nil, fmt.Errorf("failed to open zip: %s", err)
	}
	defer zr.Close()

	for _, f := range zr.File {
		if regexp.MustCompile(`.*DeviceTree.*im4p$`).MatchString(f.Name) {
			dtData := make([]byte, f.UncompressedSize64)
			rc, err := f.Open()
			if err != nil {
				return nil, fmt.Errorf("failed to open %s: %v", f.Name, err)
			}
			_, err = io.ReadFull(rc, dtData)
			rc.Close()
			if err != nil {
				return nil, fmt.Errorf("failed to read %s: %v", f.Name, err)
			}
			if len(keys) > 0 {
				ivkey, err := hex.DecodeString(keys[0])
				if err != nil {
					return nil, fmt.Errorf("failed to decode --iv-key: %v", err)
				}
				data, err := DecryptIm4pData(dtData, ivkey[:aes.BlockSize], ivkey[aes.BlockSize:])
				if err != nil {
					return nil, fmt.Errorf("failed to decrypt Img4 DeviceTree: %w", err)
				}
				dt[filepath.Base(f.Name)], err = parseDeviceTree(bytes.NewReader(data))
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img4 device tree data: %w", err)
				}
			} else {
				dt[filepath.Base(f.Name)], err = ParseImg4Data(dtData)
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img4 DeviceTree: %w", err)
				}
			}
		} else if regexp.MustCompile(`.*DeviceTree.*img3$`).MatchString(f.Name) {
			dtData := make([]byte, f.UncompressedSize64)
			rc, _ := f.Open()
			io.ReadFull(rc, dtData)
			rc.Close()
			if len(keys) > 0 {
				ivkey, err := hex.DecodeString(keys[0])
				if err != nil {
					return nil, fmt.Errorf("failed to decode --iv-key: %v", err)
				}
				data, err := img3.Decrypt(dtData, ivkey[:aes.BlockSize], ivkey[aes.BlockSize:])
				if err != nil {
					return nil, fmt.Errorf("failed to decrypt Img3 DeviceTree: %w", err)
				}
				dt[filepath.Base(f.Name)], err = parseDeviceTree(bytes.NewReader(data))
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img3 device tree data: %w", err)
				}
			} else {
				dt[filepath.Base(f.Name)], err = ParseImg3Data(dtData)
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img3 DeviceTree: %w", err)
				}
			}
		}
	}

	return dt, nil
}

// ParseZipFiles parses DeviceTree in remote ipsw zip
func ParseZipFiles(files []*zip.File, keys ...string) (dt map[string]*DeviceTree, err error) {

	dt = make(map[string]*DeviceTree)

	for _, f := range files {
		if regexp.MustCompile(`.*DeviceTree.*im4p$`).MatchString(f.Name) {
			dtData := make([]byte, f.UncompressedSize64)
			rc, err := f.Open()
			if err != nil {
				return nil, fmt.Errorf("failed to open %s: %v", f.Name, err)
			}
			_, err = io.ReadFull(rc, dtData)
			rc.Close()
			if err != nil {
				return nil, fmt.Errorf("failed to read %s: %v", f.Name, err)
			}
			if len(keys) > 0 {
				ivkey, err := hex.DecodeString(keys[0])
				if err != nil {
					return nil, fmt.Errorf("failed to decode --iv-key: %v", err)
				}
				data, err := DecryptIm4pData(dtData, ivkey[:aes.BlockSize], ivkey[aes.BlockSize:])
				if err != nil {
					return nil, fmt.Errorf("failed to decrypt Img4 DeviceTree: %w", err)
				}
				dt[filepath.Base(f.Name)], err = parseDeviceTree(bytes.NewReader(data))
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img4 device tree data: %w", err)
				}
			} else {
				dt[filepath.Base(f.Name)], err = ParseImg4Data(dtData)
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img4 DeviceTree: %w", err)
				}
			}
		} else if regexp.MustCompile(`.*DeviceTree.*img3$`).MatchString(f.Name) {
			dtData := make([]byte, f.UncompressedSize64)
			rc, _ := f.Open()
			io.ReadFull(rc, dtData)
			rc.Close()
			if len(keys) > 0 {
				ivkey, err := hex.DecodeString(keys[0])
				if err != nil {
					return nil, fmt.Errorf("failed to decode --iv-key: %v", err)
				}
				data, err := img3.Decrypt(dtData, ivkey[:aes.BlockSize], ivkey[aes.BlockSize:])
				if err != nil {
					return nil, fmt.Errorf("failed to decrypt Img3 DeviceTree: %w", err)
				}
				dt[filepath.Base(f.Name)], err = parseDeviceTree(bytes.NewReader(data))
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img3 device tree data: %w", err)
				}
			} else {
				dt[filepath.Base(f.Name)], err = ParseImg3Data(dtData)
				if err != nil {
					return nil, fmt.Errorf("failed to parse Img3 DeviceTree: %w", err)
				}
			}
		}
	}

	return dt, nil
}

// Extract extracts DeviceTree(s) from ipsw
func Extract(ipsw, destPath string) error {
	_, err := utils.Unzip(ipsw, destPath, func(f *zip.File) bool {
		if regexp.MustCompile(`.*DeviceTree.*im4p$`).MatchString(f.Name) {
			return true
		}
		return regexp.MustCompile(`.*DeviceTree.*img3$`).MatchString(f.Name)
	})

	if err != nil {
		return fmt.Errorf("failed to extract DeviceTree: %w", err)
	}

	return nil
}
