package car

//go:generate go tool stringer -type=renditionAttributeType,renditionLayoutType,resourceID,compressionType -output=car_string.go .

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"image"
	"strings"
	"time"

	"github.com/apex/log"
	"github.com/blacktop/go-termimg"
	"github.com/blacktop/ipsw/internal/utils"
	"github.com/dustin/go-humanize"
	"github.com/fatih/color"
)

var (
	colorTitle    = color.New(color.Bold, color.FgHiMagenta).SprintFunc()
	colorField    = color.New(color.Bold, color.FgHiBlue).SprintFunc()
	colorSubField = color.New(color.Bold, color.FgHiCyan).SprintFunc()
)

func (a *Asset) String() string {
	var out strings.Builder
	out.WriteString(colorTitle("Asset\n") + "=====\n") // title
	out.WriteString(colorField("Header") + ":\n")
	out.WriteString(fmt.Sprintf(
		colorSubField("  Version")+":             %s"+
			colorSubField("  CoreUI Version")+":      %d\n"+
			colorSubField("  Storage Version")+":     %d\n"+
			colorSubField("  Storage Timestamp")+":   %s\n"+
			colorSubField("  Rendition Count")+":     %d\n"+
			colorSubField("  UUID")+":                %s\n"+
			colorSubField("  Associated Checksum")+": %#x\n"+
			colorSubField("  Schema Version")+":      %d\n"+
			colorSubField("  ColorSpaceID")+":        %s\n"+
			colorSubField("  Key Semantics")+":       %d\n",
		string(bytes.Trim(a.MainVersionString[:], "\x00")),
		a.CoreUiVersion,
		a.StorageVersion,
		time.Unix(int64(a.StorageTimestamp), 0).String(),
		a.RenditionCount,
		a.UUID.String(),
		a.AssociatedChecksum,
		a.SchemaVersion,
		a.ColorSpaceID,
		a.KeySemantics,
	))
	out.WriteString(colorField("Metadata") + ":\n")
	out.WriteString(fmt.Sprintf(
		"  Authoring Tool:      %s"+
			"  Thinning Args:       %s\n"+
			"  Deployment Platform: %s %s\n",
		string(bytes.Trim(a.Metadata.AuthoringTool[:], "\x00")),
		strings.ReplaceAll(string(bytes.Trim(a.Metadata.ThinningArguments[:], "\x00")), "<", "\n    <"),
		string(bytes.Trim(a.Metadata.DeploymentPlatform[:], "\x00")),
		string(bytes.Trim(a.Metadata.DeploymentPlatformVersion[:], "\x00")),
	))
	if len(a.KeyFormat) > 0 {
		out.WriteString(colorField("KeyFormats") + ":\n")
		for _, k := range a.KeyFormat {
			out.WriteString(fmt.Sprintf("  - %s\n", k))
		}
	}
	if len(a.AppearanceDB) > 0 {
		out.WriteString(colorField("Appearances") + ":\n")
		for k, v := range a.AppearanceDB {
			out.WriteString(fmt.Sprintf("  %s: %d\n", colorSubField(k), v))
		}
	}
	if len(a.ColorDB) > 0 {
		out.WriteString(colorField("Colors") + ":\n")
		for k, v := range a.ColorDB {
			if a.conf.Verbose {
				if tout, err := colorInTerminal(v); err == nil {
					out.WriteString(fmt.Sprintf("- %s:\n\n%s\n\n", k, tout))
				}
			} else {
				out.WriteString(fmt.Sprintf("  %s: %#v\n", colorSubField(k), v))

			}
		}
	}
	if len(a.Localizations) > 0 {
		out.WriteString(colorField("Localizations") + ":\n")
		for k, v := range a.Localizations {
			out.WriteString(fmt.Sprintf("  %s: %d\n", colorSubField(k), v))
		}
	}
	if len(a.ImageDB) > 0 {
		out.WriteString(fmt.Sprintf(colorTitle("Assets")+": (%d)\n", len(a.ImageDB)))
		for _, ass := range a.ImageDB {
			out.WriteString(" ╭╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴\n")
			var asset strings.Builder
			switch t := ass.Asset.(type) {
			case csiColor:
				asset.WriteString(fmt.Sprintf(colorField("Colorspace")+": %s\n", t.Info.ColorSpaceID()))
				if len(t.Components) > 0 {
					asset.WriteString("  " + colorSubField("Components") + ":\n")
					for _, c := range t.Components {
						asset.WriteString(fmt.Sprintf("    - %v\n", c))
					}
					if a.conf.Verbose {
						if tout, err := t.ToTerminal(); err == nil {
							asset.WriteString(fmt.Sprintf(colorField("Color Preview")+":\n%s\n", tout))
						}
					}
				}
			case *BGRA:
				asset.WriteString(fmt.Sprintf(colorField("Image Size")+": %dx%d\n", t.Rect.Max.X, t.Rect.Max.Y))
				if a.conf.Verbose {
					if tout, err := termimg.New(t).Render(); err == nil {
						asset.WriteString(fmt.Sprintf("Image Preview:\n%s\n", tout))
					}
				}
			case *GA8:
				asset.WriteString(fmt.Sprintf(colorField("Image Size")+": %dx%d\n", t.Rect.Max.X, t.Rect.Max.Y))
				if a.conf.Verbose {
					if tout, err := termimg.New(t).Render(); err == nil {
						asset.WriteString(fmt.Sprintf("Image Preview:\n%s\n", tout))
					}
				}
			case *image.RGBA:
				asset.WriteString(fmt.Sprintf(colorField("Image Size")+": %dx%d\n", t.Rect.Max.X, t.Rect.Max.Y))
				if a.conf.Verbose {
					if tout, err := termimg.New(t).Render(); err == nil {
						asset.WriteString(fmt.Sprintf("Image Preview:\n%s\n", tout))
					}
				}
			case csiNamedGradient:
				asset.WriteString(colorField("Gradient") + ":\n")
				for i := range t.ColorCount {
					asset.WriteString(fmt.Sprintf("  - %s (%.3f, %.3f)\n", bytes.Trim(t.Stops[i].Name, "\x00"), float32(t.StartStops[i].Start), float32(t.StartStops[i].Stop)))
				}
			case csiMultisizeImageSet:
				asset.WriteString(colorField("MultiSized") + ":\n")
				for _, size := range t.ImageSizes {
					asset.WriteString(fmt.Sprintf("  - index %d: %dx%d\n", size.Index, size.Width, size.Height))
				}
			case []byte:
				// Raw data (PDF, JPEG, HEIF, etc.)
				asset.WriteString(fmt.Sprintf(colorField("Data Size")+": %s (%d bytes)\n", humanize.Bytes(uint64(len(t))), len(t)))
			default:
				log.Debugf("%s has unknown asset type: %T", ass.RenditionName, t)
			}
			var attrs strings.Builder
			if len(ass.Attributes) > 0 {
				attrs.WriteString(colorField("Attributes") + ":\n")
				for _, kf := range a.KeyFormat {
					if value, ok := ass.Attributes[kf.String()]; ok {
						attrs.WriteString(fmt.Sprintf("  %s%d\n", colorSubField(fmt.Sprintf("%-20s", kf.String())), value))
					}
				}
			}
			var rscs strings.Builder
			if len(ass.Resources) > 0 {
				rscs.WriteString(colorField("Resources") + ":\n")
				for _, rsc := range ass.Resources {
					switch rsc.ID {
					case SliceID:
						var slice sliceResource
						if err := slice.UnmarshalBinary(rsc.Data); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: (%d)\n", colorSubField(rsc.ID), slice.NumSlices))
						for _, s := range slice.Slices {
							rscs.WriteString(fmt.Sprintf("    - pos(%03d,%03d) size(%03d,%03d)\n", s.X, s.Y, s.Width, s.Height))
						}
					case MetricsID:
						var metrics metricsResource
						if err := metrics.UnmarshalBinary(rsc.Data); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: (%d)\n", colorSubField(rsc.ID), metrics.NumMetrics))
						for _, m := range metrics.Metrics {
							rscs.WriteString(fmt.Sprintf("    - %s(%d,%d,%d,%d) %s(%03d,%03d)\n", colorField("insets"), m.LeftInset, m.TopInset, m.RightInset, m.BottomInset, colorField("size"), m.Width, m.Height))
						}
					case LayerReferenceID:
						layer := new(layerResource)
						if err := layer.UnmarshalBinary(rsc.Data); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: (%d):\n", colorSubField("Layers"), layer.NumLayers))
						for _, layer := range layer.Layers {
							rscs.WriteString(fmt.Sprintf("    %s(%03d,%03d) %s(%03d,%03d) %s=%d %s=%.2f\n",
								colorField("pos"), layer.Frame.X, layer.Frame.Y, colorField("size"), layer.Frame.Width, layer.Frame.Height, colorField("blend"), layer.BlendMode, colorField("opacity"), layer.Opacity))
							rscs.WriteString(fmt.Sprintf("    %s", utils.HexDump(layer.Data, 0)))
						}
					case InternalLinkID:
						var link csiInternalLinkData
						if err := link.UnmarshalBinary(bytes.NewReader(rsc.Data)); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: %s(%d,%d) %s(%d)\n", colorSubField(rsc.ID), colorSubField("frame"), link.Frame.X, link.Frame.Y, colorSubField("layout"), link.Layout))
						for _, ref := range link.Reference {
							rscs.WriteString(fmt.Sprintf("    %s: %d\n", colorSubField(renditionAttributeType(ref.Name)), ref.Value))
						}
					case CompositingOptionsID:
						var comp compositingResource
						if err := binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &comp); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s:\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s:\n    %s: %d\n    %s:   %.2f\n", colorSubField(rsc.ID), colorField("BlendMode"), comp.BlendMode, colorField("Opacity"), comp.Opacity))
					case MetaDataID:
						var meta metadataResource
						if err := meta.UnmarshalBinary(rsc.Data); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: %s\n", colorSubField(rsc.ID), bytes.Trim(meta.Data[:], "\x00")))
					case MetaDataEXIFOrientationID:
						var orient uint32
						if err := binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &orient); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: %d\n", colorSubField(rsc.ID), orient))
					case ImageRowBytesID:
						var rowBytes uint32
						if err := binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &rowBytes); err != nil {
							rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
						}
						rscs.WriteString(fmt.Sprintf("  %s: %s (%d)\n", colorSubField(rsc.ID), humanize.Bytes(uint64(rowBytes)), rowBytes))
					default:
						rscs.WriteString(fmt.Sprintf("  %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0)))
					}
				}
			}
			var nameStr string
			if name := a.GetName(ass.ID()); len(name) > 0 {
				nameStr = colorField("Name") + fmt.Sprintf(": %s\n", name)
				if name != ass.RenditionName {
					nameStr += colorField("Rendition") + fmt.Sprintf(": %s\n", ass.RenditionName)
				}
			} else if len(ass.RenditionName) > 0 {
				nameStr = colorField("Rendition") + fmt.Sprintf(": %s\n", ass.RenditionName)
			}
			out.WriteString(fmt.Sprintf(
				"%s"+
					colorField("Type")+": %s\n"+
					colorField("Size")+": %s (%d)\n"+
					"%s%s%s",
				// "%s%s%s",
				nameStr,
				ass.Type,
				humanize.Bytes(uint64(ass.Size)), ass.Size,
				asset.String(),
				attrs.String(),
				rscs.String(),
			))
			out.WriteString(" ╰╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴\n")
		}
	}
	return out.String()
}

// KeyFormatName converts renditionAttributeType to its Apple name
func (r renditionAttributeType) KeyFormatName() string {
	switch r {
	case ThemeLook:
		return "kCRThemeLookName"
	case Element:
		return "kCRThemeElementName"
	case Part:
		return "kCRThemePartName"
	case Size:
		return "kCRThemeSizeName"
	case Direction:
		return "kCRThemeDirectionName"
	case Placeholder:
		return "kCRThemePlaceholderName"
	case Value:
		return "kCRThemeValueName"
	case ThemeAppearance:
		return "kCRThemeAppearanceName"
	case Dimension1:
		return "kCRThemeDimension1Name"
	case Dimension2:
		return "kCRThemeDimension2Name"
	case State:
		return "kCRThemeStateName"
	case Layer:
		return "kCRThemeLayerName"
	case Scale:
		return "kCRThemeScaleName"
	case Localization:
		return "kCRThemeLocalizationName"
	case PresentationState:
		return "kCRThemePresentationStateName"
	case Idiom:
		return "kCRThemeIdiomName"
	case Subtype:
		return "kCRThemeSubtypeName"
	case Identifier:
		return "kCRThemeIdentifierName"
	case PreviousValue:
		return "kCRThemePreviousValueName"
	case PreviousState:
		return "kCRThemePreviousStateName"
	case HorizontalSizeClass:
		return "kCRThemeHorizontalSizeClassName"
	case VerticalSizeClass:
		return "kCRThemeVerticalSizeClassName"
	case MemoryLevelClass:
		return "kCRThemeMemoryLevelClassName"
	case GraphicsFeatureSetClass:
		return "kCRThemeGraphicsFeatureSetClassName"
	case DisplayGamut:
		return "kCRThemeDisplayGamutName"
	case DeploymentTarget:
		return "kCRThemeDeploymentTargetName"
	default:
		return fmt.Sprintf("renditionAttributeType(%d)", r)
	}
}

// ToJSON converts Asset to JSON format matching assetutil output
func (a *Asset) ToJSON() ([]byte, error) {
	output := []map[string]any{}

	// Add header information as first element
	header := map[string]any{
		"CoreUIVersion":      a.Header.CoreUiVersion,
		"StorageVersion":     a.Header.StorageVersion,
		"Timestamp":          a.Header.StorageTimestamp,
		"SchemaVersion":      a.Header.SchemaVersion,
		"MainVersion":        strings.TrimSpace(string(bytes.Trim(a.Header.MainVersionString[:], "\x00"))),
		"Authoring Tool":     strings.TrimSpace(string(bytes.Trim(a.Metadata.AuthoringTool[:], "\x00"))),
		"ThinningParameters": string(bytes.Trim(a.Metadata.ThinningArguments[:], "\x00")),
		"Platform":           string(bytes.Trim(a.Metadata.DeploymentPlatform[:], "\x00")),
		"PlatformVersion":    string(bytes.Trim(a.Metadata.DeploymentPlatformVersion[:], "\x00")),
	}

	// Add key format
	if len(a.KeyFormat) > 0 {
		keyFormats := []string{}
		for _, k := range a.KeyFormat {
			keyFormats = append(keyFormats, k.KeyFormatName())
		}
		header["Key Format"] = keyFormats
	}

	// Add appearances
	if len(a.AppearanceDB) > 0 {
		appearances := map[string]int{}
		for k, v := range a.AppearanceDB {
			appearances[k] = int(v)
		}
		header["Appearances"] = appearances
	}

	output = append(output, header)

	// Add renditions
	for _, rend := range a.ImageDB {
		rendition := map[string]any{
			"Name": rend.RenditionName,
			"Type": rend.Type,
		}

		// Add attributes
		for _, kf := range a.KeyFormat {
			if value, ok := rend.Attributes[kf.String()]; ok {
				switch kf {
				case Scale:
					rendition["Scale"] = value
				case Idiom:
					rendition["Idiom"] = getIdiomName(value)
				case Subtype:
					if value > 0 {
						rendition["Subtype"] = value
					}
				case Identifier:
					if value > 0 {
						rendition["NameIdentifier"] = value
					}
				}
			}
		}

		if rend.Size > 0 {
			rendition["SizeOnDisk"] = rend.Size
		}

		output = append(output, rendition)
	}

	return json.MarshalIndent(output, "", "  ")
}

func getIdiomName(value uint16) string {
	switch coreThemeIdiom(value) {
	case Universal:
		return "universal"
	case Phone:
		return "phone"
	case Tablet:
		return "pad"
	case Desktop:
		return "desktop"
	case Tv:
		return "tv"
	case Car:
		return "car"
	case Watch:
		return "watch"
	case Marketing:
		return "marketing"
	default:
		return fmt.Sprintf("idiom_%d", value)
	}
}
