from collections import defaultdict


class ExtractFileWriter:
    indent_width = 4
    indent_level = 0
    indention = ""
    content = ""

    def __init__(self, name):
        self.name = name.upper()

    def to_file(self, path):
        with open(path, "w") as f:
            f.write(self.content)

    def indent(self):
        self.indent_level += self.indent_width
        self.indention = " " * self.indent_level

    def unindent(self):
        self.indent_level -= self.indent_width
        self.indention = " " * self.indent_level

    def output_anim_sequence_struct(self):
        self.output("typedef struct AnimationSequence {\n")
        self.indent()
        self.output("BzTileID startFrame;\n")
        self.output("i32    frameCount;\n")
        self.unindent()
        self.output("} AnimationSequence;\n")
        self.empty_line()

    def output_anim_frame_struct(self):
        self.output("typedef struct AnimationFrame {\n")
        self.indent()
        self.output("BzTileID frame;\n")
        self.output("f32    duration;\n")
        self.unindent()
        self.output("} AnimationFrame;\n")
        self.empty_line()

    def header_guard_start(self):
        self.content += f"#ifndef {self.name}_H\n"
        self.content += f"#define {self.name}_H\n"
        self.content += "\n"

    def header_guard_stop(self):
        self.content += f"#endif // {self.name}_H\n"

    def include(self, header):
        self.content += f"{self.indention}#include {header}\n"

    def enum_start(self, name):
        self.content += f"{self.indention}typedef enum {name} {{\n"
        self.indent()

    def enum_stop(self, name):
        self.unindent()
        self.content += f"}} {name};\n\n"

    def enum_list(self, name, enums):
        self.enum_start(name)
        for enum in enums:
            if enum.endswith("_NONE"):
                self.content += f"{self.indention}{enum} = -1,\n"
            else:
                self.content += f"{self.indention}{enum},\n"
        self.enum_stop(name)

    def enum_dict(self, name, enums):
        self.enum_start(name)
        for enum, v in enums.items():
            self.content += f"{self.indention}{enum} = {v},\n"
        self.enum_stop(name)

    def output(self, out):
        self.content += self.indention
        self.content += out

    def empty_line(self):
        self.content += '\n'

    def only_output(self, out):
        self.content += out

    def block_start(self):
        self.content += f"{{\n"
        self.indent()

    def block_end(self):
        self.unindent()
        self.content += f"{self.indention}}}\n"


def group_by_class(tiles):
    groups = defaultdict(list)
    for tile in tiles:
        if 'type' not in tile:
            continue
        tile_class = tile['type']
        groups[tile_class].append(tile)
    return groups


class EnumWriter:
    def __init__(self, writer: ExtractFileWriter, tiles, prefix, anim_prefix=None):
        self.writer = writer
        self.prefix = prefix
        self.all_tiles = tiles
        self.tiles = group_by_class(tiles)
        self.enums = []
        self.enums.append(self.to_enum("none"))
        self.enums += [self.to_enum(x) for x in self.tiles.keys()]
        self.enums.append(self.to_enum("count"))
        self.enum_type = f"{prefix.capitalize()}Type"
        if anim_prefix:
            self.anim_prefix = anim_prefix
            self.anim_type = f"{anim_prefix.capitalize()}Type"
            self.anim_map = defaultdict(lambda: defaultdict(list))
            self.animations = []
            self.all_tiles = tiles

            animations = []

            for tile in self.all_tiles:
                if 'animation' not in tile:
                    continue
                if 'properties' not in tile:
                    continue
                if 'type' not in tile:
                    continue

                enum = self.to_enum(tile['type'])
                props = tile['properties']
                anim_type = [prop['value'] for prop in props if prop['name'] == 'animation'][0]
                anim_type = self.to_anim_enum(anim_type)
                animations.append(anim_type)

                self.anim_map[enum][anim_type] = tile['animation']

            animations.append(self.to_anim_enum("count"))
            animations.append(self.to_anim_enum("none"))
            anim_seen = set()
            anim_add = anim_seen.add
            self.animations = [x for x in animations if not (x in anim_seen or anim_add(x))]

    def to_enum(self, name):
        return f"{self.prefix.upper()}_{name.upper()}"

    def to_anim_enum(self, name):
        return f"{self.anim_prefix.upper()}_{name.upper()}"

    def output_enum(self):
        self.writer.enum_list(self.enum_type, self.enums)

    def output_tile_to_enum(self, func_name):
        writer = self.writer
        writer.output(f"static {self.enum_type} {func_name}(BzTileID tile) ")
        writer.block_start()

        writer.output("switch (tile) ")
        writer.block_start()

        for key, tiles in self.tiles.items():
            ids = [x['id'] for x in tiles]
            for id in ids:
                writer.output(f"case {id}:\n")
            writer.output(f"    return {self.to_enum(key)};\n")
        writer.output(f"default:\n")
        writer.output(f"    return {self.enums[-1]};\n")

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_enum_to_tile(self, func_name):
        writer = self.writer
        writer.output(f"static BzTileID {func_name}({self.enum_type} type) ")
        writer.block_start()

        writer.output("switch (type) ")
        writer.block_start()

        for key, tiles in self.tiles.items():
            ids = sorted([x['id'] for x in tiles])
            writer.output(f"case {self.to_enum(key)}: return {ids[0]};\n")
        writer.output(f"default: return -1;\n")

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_str_to_enum(self, func_name):
        writer = self.writer
        writer.output(f"static {self.enum_type} {func_name}(const char *str) ")
        writer.block_start()

        for name in self.tiles.keys():
            writer.output(f"if (strncmp(\"{name}\", str, {len(name)}) == 0) return {self.to_enum(name)};\n")
        writer.output(f"return {self.enums[-1]};\n")

        writer.block_end()
        writer.empty_line()

    def output_enum_to_str(self, func_name):
        writer = self.writer
        writer.output(f"static const char *{func_name}({self.enum_type} type) ")
        writer.block_start()

        writer.output("switch (type) ")
        writer.block_start()

        for name in self.tiles.keys():
            writer.output(f"case {self.to_enum(name)}: return \"{name}\";\n")
        writer.output(f"default: return NULL;\n")

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_enum_tile_size(self, func_name):
        writer = self.writer
        writer.output(
            f"static {self.enum_type} {func_name}({self.enum_type} type, i32 *outWidth, i32 *outHeight) ")
        writer.block_start()

        writer.output("switch (type) ")
        writer.block_start()

        for key, tiles in self.tiles.items():
            ids = [x['id'] for x in tiles]
            w = 1
            h = 1
            for curr, next in zip(ids, ids[1:]):
                if next > curr + 1:
                    h += 1
                elif h == 1:
                    w += 1
            writer.output(f"case {self.to_enum(key)}:\n")
            writer.indent()
            writer.output(f"if (outWidth)  *outWidth  = {w};\n")
            writer.output(f"if (outHeight) *outHeight = {h};\n")
            writer.output("break;\n")
            writer.unindent()
        writer.output("default:\n")
        writer.indent()
        writer.output("if (outWidth)  *outWidth  = 0;\n")
        writer.output("if (outHeight) *outHeight = 0;\n")
        writer.output("break;\n")
        writer.unindent()

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_tile_has_anim(self, func_name):
        writer = self.writer
        writer.output(f"static bool {func_name}(BzTileID tile) ")
        writer.block_start()

        writer.output("switch (tile) ")
        writer.block_start()
        ids = sorted([x['id'] for x in self.all_tiles if 'animation' in x])
        for id in ids:
            writer.output(f"case {id}:\n")
        writer.indent()
        writer.output("return true;\n")
        writer.unindent()
        writer.output("default:\n")
        writer.indent()
        writer.output("return false;\n")
        writer.unindent()

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_tile_anim_sequence(self, func_name):
        writer = self.writer
        writer.output(f"static AnimationSequence {func_name}(BzTileID tile) ")
        writer.block_start()

        writer.output("switch (tile) ")
        writer.block_start()
        anim_tiles = [x for x in self.all_tiles if 'animation' in x]
        for tile in anim_tiles:
            anim = tile['animation']
            num_frames = len(anim)
            anim_id = min([x['tileid'] for x in anim])
            ret = f"(AnimationSequence) {{.startFrame = {anim_id}, .frameCount = {num_frames}}}"
            writer.output(f"case {tile['id']}: return {ret};\n")
        writer.output("default:\n")
        writer.indent()
        writer.output("BZ_ASSERT(0);\n")
        writer.output("return (AnimationSequence) {0, 0};\n")
        writer.unindent()

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_tile_anim_frame(self, func_name):
        writer = self.writer
        writer.output(f"static AnimationFrame {func_name}(BzTileID tile, i32 frameIdx) ")
        writer.block_start()

        writer.output("switch (tile) ")
        writer.block_start()
        anim_tiles = [x for x in self.all_tiles if 'animation' in x]
        for tile in anim_tiles:
            anim = tile['animation']
            # num_frames = len(anim)
            frames = [str(x['tileid']) for x in anim]
            durations = [x['duration'] * 0.001 for x in anim]

            anim_frames = [f"{{{frame}, {format(duration, '.4f')}f}}" for frame, duration in zip(frames, durations)]

            ret = f"((AnimationFrame []) {{{', '.join(anim_frames)}}}) [frameIdx]"
            writer.output(f"case {tile['id']}: return {ret};\n")
        writer.output("default:\n")
        writer.indent()
        writer.output("BZ_ASSERT(0);\n")
        writer.output("return (AnimationFrame) {-1, -1.0f};\n")
        writer.unindent()
        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_anim_enum(self):
        writer = self.writer
        writer.enum_list(self.anim_type, self.animations)

    def output_anim_enum_to_str(self, func_name):
        writer = self.writer
        writer.output(f"static const char *{func_name}({self.anim_type} type) ")
        writer.block_start()

        writer.output("switch (type) ")
        writer.block_start()

        for name in self.animations:
            writer.output(f"case {name}: return \"{name}\";\n")
        writer.output(f"default: return NULL;\n")

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_has_anim(self, func_name):
        writer = self.writer
        writer.output(f"static bool {func_name}({self.enum_type} entity, {self.anim_type} type) ")
        writer.block_start()

        writer.output("switch (entity) ")
        writer.block_start()
        for entity, anims in self.anim_map.items():
            writer.output(f"case {entity}:\n")
            writer.indent()
            writer.output("switch (type) ")
            writer.block_start()

            for anim in anims:
                writer.output(f"case {anim}:\n")
            writer.indent()
            writer.output("return true;\n")
            writer.unindent()
            writer.output("default:\n")
            writer.indent()
            writer.output("return false;\n")
            writer.unindent()

            writer.block_end()
            writer.unindent()
        writer.output("default:\n")
        writer.indent()
        writer.output("return false;\n")
        writer.unindent()

        writer.block_end()
        writer.block_end()
        writer.empty_line()

    def output_anim_sequence(self, func_name):
        writer = self.writer
        writer.output(f"static AnimationSequence {func_name}({self.enum_type} entity, {self.anim_type} type) ")
        writer.block_start()

        writer.output("switch (entity) ")
        writer.block_start()
        for entity, animation_types in self.anim_map.items():
            writer.output(f"case {entity}:\n")
            writer.indent()
            writer.output("switch (type) ")
            writer.block_start()
            for anim_type in animation_types:
                anim = self.anim_map[entity][anim_type]
                num_frames = len(anim)
                anim_id = anim[0]['tileid']
                ret = f"(AnimationSequence) {{.startFrame = {anim_id}, .frameCount = {num_frames}}}"
                writer.output(f"case {anim_type}: return {ret};\n")
            writer.output("default: break;\n")
            writer.block_end()
            writer.unindent()
        writer.output("default: break;\n")
        writer.block_end()

        writer.output("BZ_ASSERT(0);\n")
        writer.output("return (AnimationSequence) {0, 0};\n")
        writer.block_end()
        writer.empty_line()

    def output_anim_frame(self, func_name):
        writer = self.writer
        writer.output(
            f"static AnimationFrame {func_name}({self.enum_type} entity, {self.anim_type} type, i32 frameIdx) ")
        writer.block_start()

        writer.output("switch (entity) ")
        writer.block_start()
        for entity, animation_types in self.anim_map.items():
            writer.output(f"case {entity}:\n")
            writer.indent()
            writer.output("switch (type) ")
            writer.block_start()
            for anim_type in animation_types:
                anim = self.anim_map[entity][anim_type]
                frames = [str(x['tileid']) for x in anim]
                durations = [x['duration'] * 0.001 for x in anim]

                anim_frames = [f"{{{frame}, {format(duration, '0.4f')}f}}" for frame, duration in
                               zip(frames, durations)]
                ret = f"((AnimationFrame []) {{{', '.join(anim_frames)}}}) [frameIdx]"
                writer.output(f"case {anim_type}: return {ret};\n")
            writer.output("default: break;\n")
            writer.block_end()
            writer.unindent()
        writer.output("default: break;\n")
        writer.block_end()

        writer.output("BZ_ASSERT(0);\n")
        writer.output("return (AnimationFrame) {-1, -1.0f};\n")
        writer.block_end()
        writer.empty_line()
