from segtypes.n64.segment import N64Segment
from pathlib import Path
import struct
from util.n64 import Yay0decompress
from util.n64.Yay0decompress import Yay0Decompressor
from segtypes.n64.palette import iter_in_groups
from util.color import unpack_color
from util import options
import png
import xml.etree.ElementTree as ET
import struct

import pylibyaml
import yaml as yaml_loader

class Sprite:
    def __init__(self):
        self.max_components = 0
        self.num_variations = 0

        self.images = []
        self.palettes = []
        self.animations = []

        self.image_names = []
        self.palette_names = []
        self.animation_names = []
        self.variation_names = []

    @staticmethod
    def from_bytes(data):
        self = Sprite()

        image_offsets = Sprite.read_offset_list(data[int.from_bytes(data[0:4], byteorder="big"):])
        palette_offsets = Sprite.read_offset_list(data[int.from_bytes(data[4:8], byteorder="big"):])
        self.max_components = int.from_bytes(data[8:0xC], byteorder="big")
        self.num_variations = int.from_bytes(data[0xC:0x10], byteorder="big")
        animation_offsets = Sprite.read_offset_list(data[0x10:])

        for offset in palette_offsets:
            # 16 colors
            color_data = data[offset : offset + 16 * 2]
            self.palettes.append([unpack_color(c) for c in iter_in_groups(color_data, 2)])

        for offset in image_offsets:
            img = Image.from_bytes(data[offset:], data)
            self.images.append(img)

        for offset in animation_offsets:
            anim = []

            for comp_offset in Sprite.read_offset_list(data[offset:]):
                comp = Component.from_bytes(data[comp_offset:], data)
                anim.append(comp)

            self.animations.append(anim)

        return self

    @staticmethod
    def read_offset_list(data):
        l = []
        pos = 0

        for offset in struct.iter_unpack(">i", data):
            if offset[0] == -1:
                break
            l.append(offset[0])

        return l

    def write_to_dir(self, path):
        if len(self.variation_names) > 1:
            SpriteSheet = ET.Element("SpriteSheet", {
                "maxComponents": str(self.max_components),
                "paletteGroups": str(self.num_variations),
                "variations": ",".join(self.variation_names),
            })
        else:
            SpriteSheet = ET.Element("SpriteSheet", {
                "maxComponents": str(self.max_components),
                "paletteGroups": str(self.num_variations),
            })

        PaletteList = ET.SubElement(SpriteSheet, "PaletteList")
        RasterList = ET.SubElement(SpriteSheet, "RasterList")
        AnimationList = ET.SubElement(SpriteSheet, "AnimationList")

        palette_to_raster = {}

        for i, image in enumerate(self.images):
            name = self.image_names[i] if self.image_names else f"Raster{i:02X}"
            image.write(path / (name + ".png"), self.palettes[image.palette_index])

            if image.palette_index not in palette_to_raster:
                palette_to_raster[image.palette_index] = []
            palette_to_raster[image.palette_index].append(image)

            ET.SubElement(RasterList, "Raster", {
                "id": f"{i:X}",
                "palette": f"{image.palette_index:X}",
                "src": name + ".png",
            })

        for i, palette in enumerate(self.palettes):
            name = self.palette_names[i] if (self.palette_names and i < len(self.palette_names)) else f"Pal{i:02X}"

            if i in palette_to_raster:
                img = palette_to_raster[i][0]
            else:
                img = self.images[0]

            img.write(path / (name + ".png"), palette)

            ET.SubElement(PaletteList, "Palette", {
                "id": f"{i:X}",
                "src": name + ".png",
            })

        for i, components in enumerate(self.animations):
            Animation = ET.SubElement(AnimationList, "Animation", {
                "name": self.animation_names[i] if self.animation_names else f"Anim{i:02X}",
            })

            for j, comp in enumerate(components):
                Component = ET.SubElement(Animation, "Component", {
                    "name": f"Comp_{j:X}",
                    "xyz": ",".join(map(str, [comp.x, comp.y, comp.z])),
                })

                for cmd in comp.commands:
                    ET.SubElement(Component, "Command", {"val": f"{cmd:X}"})

        xml = ET.ElementTree(SpriteSheet)

        # pretty print (Python 3.9+)
        if hasattr(ET, "indent"):
            ET.indent(xml, "    ")

        xml.write(str(path / "SpriteSheet.xml"), encoding="unicode")

    @staticmethod
    def from_dir(path, read_images=True):
        self = Sprite()

        xml = ET.parse(str(path / "SpriteSheet.xml"))
        SpriteSheet = xml.getroot()

        true_max_components = 0
        self.max_components = int(SpriteSheet.get("a") or SpriteSheet.get("maxComponents")) # ignored
        self.num_variations = int(SpriteSheet.get("b") or SpriteSheet.get("paletteGroups"))
        self.variation_names = SpriteSheet.get("variations", default="").split(",")

        for Palette in SpriteSheet.findall("./PaletteList/Palette"):
            if read_images:
                img = png.Reader(str(path / Palette.get("src")))
                img.preamble(True)
                palette = img.palette(alpha="force")

                palette = palette[0:16]
                assert len(palette) == 16

                self.palettes.append(palette)

            self.palette_names.append(Palette.get("name", Palette.get("src").split(".png")[0]))

        for Raster in SpriteSheet.findall("./RasterList/Raster"):
            if read_images:
                img_path = str(path / Raster.get("src"))
                width, height, raster, info = png.Reader(img_path).read_flat()

                image = Image()
                image.width = width
                image.height = height
                image.raster = raster
                image.palette_index = int(Raster.get("palette"), base=16)

                assert (image.width % 8) == 0, f"{img_path} width is not a multiple of 8"
                assert (image.height % 8) == 0, f"{img_path} height is not a multiple of 8"

                self.images.append(image)

            self.image_names.append(Raster.get("src").split(".png")[0])

        for i, Animation in enumerate(SpriteSheet.findall("./AnimationList/Animation")):
            components = []

            for ComponentEl in Animation.findall("Component"):
                comp = Component()

                x, y, z = ComponentEl.get("xyz", "0,0,0").split(",")
                comp.x = int(x)
                comp.y = int(y)
                comp.z = int(z)

                for Command in ComponentEl:
                    comp.commands.append(int(Command.get("val"), base=16))

                components.append(comp)

            self.animation_names.append(Animation.get("name"))
            self.animations.append(components)

            if len(components) > true_max_components:
                true_max_components = len(components)

        self.max_components = true_max_components
        #assert self.max_components == true_max_components, f"{true_max_components} component(s) used, but SpriteSheet.a = {self.max_components}"

        return self

class Image:
    @staticmethod
    def from_bytes(data, sprite_data):
        self = Image()

        raster_offset = int.from_bytes(data[0:4], byteorder="big")
        self.width = data[4] & 0xFF
        self.height = data[5] & 0xFF
        self.palette_index = data[6]
        assert data[7] == 0xFF

        self.set_raster_from_bytes(sprite_data[raster_offset:])

        return self

    # CI-4
    def set_raster_from_bytes(self, data):
        self.raster = bytearray()

        for i in range(self.width * self.height // 2):
            self.raster.append(data[i] >> 4)
            self.raster.append(data[i] & 0xF)

    def write(self, path, palette):
        w = png.Writer(self.width, self.height, palette=palette)

        with open(path, "wb") as f:
            w.write_array(f, self.raster)

class Component:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.z = 0
        self.commands = []

    @staticmethod
    def from_bytes(data, sprite_data):
        self = Component()

        commands_offset = int.from_bytes(data[0:4], byteorder="big")
        commands_size = int.from_bytes(data[4:6], byteorder="big") # size in bytes, not length!
        commands_data = sprite_data[commands_offset : commands_offset + commands_size]
        self.commands = [int.from_bytes(d[0:2], byteorder="big") for d in iter_in_groups(commands_data, 2)]

        self.x = int.from_bytes(data[6:8], byteorder="big", signed=True)
        self.y = int.from_bytes(data[8:10], byteorder="big", signed=True)
        self.z = int.from_bytes(data[10:12], byteorder="big", signed=True)

        return self

class N64SegPm_npc_sprites(N64Segment):
    DEFAULT_SPRITE_NAMES = [f"{i:02X}" for i in range(0xEA)]

    def __init__(
        self,
        rom_start,
        rom_end,
        type,
        name,
        vram_start,
        args,
        yaml,
    ):
        super().__init__(
            rom_start,
            rom_end,
            type,
            name,
            vram_start,
            args=args,
            yaml=yaml,
        )

        self.files = yaml["files"]

        with (Path(__file__).parent / f"{self.name}.yaml").open("r") as f:
            self.sprite_cfg = yaml_loader.load(f.read(), Loader=yaml_loader.SafeLoader)

    def split(self, rom_bytes):
        out_dir = options.opts.asset_path / self.dir / self.name

        data = rom_bytes[self.rom_start:self.rom_end]
        pos = 0

        for i, sprite_name in enumerate(self.files):
            #self.log(f"Splitting sprite {sprite_name}...")

            sprite_dir = out_dir / sprite_name
            sprite_dir.mkdir(parents=True, exist_ok=True)

            start = int.from_bytes(data[i * 4 : (i + 1) * 4], byteorder="big")
            end = int.from_bytes(data[(i + 1) * 4 : (i + 2) * 4], byteorder="big")

            sprite_data = Yay0Decompressor.decompress_python(data[start:end])
            sprite = Sprite.from_bytes(sprite_data)

            if sprite_name in self.sprite_cfg:
                sprite.image_names = self.sprite_cfg[sprite_name].get("frames", [])
                sprite.palette_names = self.sprite_cfg[sprite_name].get("palettes", [])
                sprite.animation_names = self.sprite_cfg[sprite_name].get("animations", [])
                sprite.variation_names = self.sprite_cfg[sprite_name].get("variations", [])

            sprite.write_to_dir(sprite_dir)

    def get_linker_entries(self):
        from segtypes.linker_entry import LinkerEntry

        basepath = options.opts.asset_path / "sprite" / f"{self.name}"
        out_paths = [options.opts.asset_path / "sprite" / self.name / (f["name"] if type(f) is dict else f)
                     for f in self.files]

        return [LinkerEntry(self, out_paths, basepath, ".data")]

    def cache(self):
        return (self.yaml, self.rom_end, self.sprite_cfg)
