import multiprocessing
import logging
import os
from collections import Counter

from flipper.utils.fff import FlipperFormatFile
from flipper.utils.templite import Templite
from .icon import ImageTools, file2image


def _convert_image_to_bm(pair: set):
    source_filename, destination_filename = pair
    image = file2image(source_filename)
    image.write(destination_filename)


def _convert_image(source_filename: str):
    image = file2image(source_filename)
    return image.data


class DolphinBubbleAnimation:
    FILE_TYPE = "Flipper Animation"
    FILE_VERSION = 1

    def __init__(
        self,
        name: str,
        min_butthurt: int,
        max_butthurt: int,
        min_level: int,
        max_level: int,
        weight: int,
    ):
        # Manifest
        self.name = name
        self.min_butthurt = min_butthurt
        self.max_butthurt = max_butthurt
        self.min_level = min_level
        self.max_level = max_level
        self.weight = weight
        # Meta and data
        self.meta = {}
        self.frames = []
        self.bubbles = []
        self.bubble_slots = None
        # Logging
        self.logger = logging.getLogger("DolphinBubbleAnimation")

    def load(self, animation_directory: str):
        if not os.path.isdir(animation_directory):
            raise Exception(f"Animation folder doesn't exist: { animation_directory }")

        meta_filename = os.path.join(animation_directory, "meta.txt")
        if not os.path.isfile(meta_filename):
            raise Exception(f"Animation meta file doesn't exist: { meta_filename }")

        self.logger.debug(f"Loading meta from {meta_filename}")
        file = FlipperFormatFile()
        file.load(meta_filename)

        # Check file header
        filetype, version = file.getHeader()
        assert filetype == self.FILE_TYPE
        assert version == self.FILE_VERSION

        max_frame_number = None
        unique_frames = None
        total_frames_count = None

        try:
            # Main meta
            self.meta["Width"] = file.readKeyInt("Width")
            self.meta["Height"] = file.readKeyInt("Height")
            self.meta["Passive frames"] = file.readKeyInt("Passive frames")
            self.meta["Active frames"] = file.readKeyInt("Active frames")
            self.meta["Frames order"] = file.readKeyIntArray("Frames order")
            self.meta["Active cycles"] = file.readKeyInt("Active cycles")
            self.meta["Frame rate"] = file.readKeyInt("Frame rate")
            self.meta["Duration"] = file.readKeyInt("Duration")
            self.meta["Active cooldown"] = file.readKeyInt("Active cooldown")
            self.bubble_slots = file.readKeyInt("Bubble slots")

            # Sanity Check
            assert self.meta["Width"] > 0 and self.meta["Width"] <= 128
            assert self.meta["Height"] > 0 and self.meta["Height"] <= 128
            assert self.meta["Passive frames"] > 0
            assert self.meta["Active frames"] >= 0
            assert self.meta["Frames order"]
            if self.meta["Active frames"] > 0:
                assert self.meta["Active cooldown"] > 0
                assert self.meta["Active cycles"] > 0
            else:
                assert self.meta["Active cooldown"] == 0
                assert self.meta["Active cycles"] == 0
            assert self.meta["Frame rate"] > 0
            assert self.meta["Duration"] >= 0

            # Frames sanity check
            max_frame_number = max(self.meta["Frames order"])
            ordered_frames_count = len(self.meta["Frames order"])
            for i in range(max_frame_number + 1):
                frame_filename = os.path.join(animation_directory, f"frame_{i}.png")
                assert os.path.isfile(frame_filename)
                self.frames.append(frame_filename)
            # Sanity check
            unique_frames = set(self.meta["Frames order"])
            unique_frames_count = len(unique_frames)
            if unique_frames_count != max_frame_number + 1:
                self.logger.warning(f"Not all frames were used in {self.name}")
            total_frames_count = self.meta["Passive frames"] + (
                self.meta["Active frames"] * self.meta["Active cycles"]
            )

            # Extra checks
            assert self.meta["Passive frames"] <= total_frames_count
            assert self.meta["Active frames"] <= total_frames_count
            assert (
                self.meta["Passive frames"] + self.meta["Active frames"]
                == ordered_frames_count
            )
        except EOFError:
            raise Exception("Invalid meta file: too short")
        except AssertionError as e:
            self.logger.exception(e)
            self.logger.error(f"Animation {self.name} got incorrect meta")
            raise Exception("Meta file is invalid: incorrect data")

        # Bubbles
        while True:
            try:
                # Bubble data
                bubble = {}
                bubble["Slot"] = file.readKeyInt("Slot")
                bubble["X"] = file.readKeyInt("X")
                bubble["Y"] = file.readKeyInt("Y")
                bubble["Text"] = file.readKey("Text")
                bubble["AlignH"] = file.readKey("AlignH")
                bubble["AlignV"] = file.readKey("AlignV")
                bubble["StartFrame"] = file.readKeyInt("StartFrame")
                bubble["EndFrame"] = file.readKeyInt("EndFrame")

                # Sanity check
                assert bubble["Slot"] <= self.bubble_slots
                assert bubble["X"] >= 0 and bubble["X"] < 128
                assert bubble["Y"] >= 0 and bubble["Y"] < 128
                assert len(bubble["Text"]) > 0
                assert bubble["AlignH"] in ["Left", "Center", "Right"]
                assert bubble["AlignV"] in ["Bottom", "Center", "Top"]
                assert bubble["StartFrame"] < total_frames_count
                assert bubble["EndFrame"] < total_frames_count
                assert bubble["EndFrame"] >= bubble["StartFrame"]

                # Store bubble
                self.bubbles.append(bubble)
            except AssertionError as e:
                self.logger.exception(e)
                self.logger.error(
                    f"Animation {self.name} bubble slot {bubble['Slot']} got incorrect data: {bubble}"
                )
                raise Exception("Meta file is invalid: incorrect bubble data")
            except EOFError:
                break

    def prepare(self):
        bubbles_in_slots = Counter([bubble["Slot"] for bubble in self.bubbles])

        last_slot = -1
        bubble_index = 0
        for bubble in self.bubbles:
            slot = bubble["Slot"]
            if slot == last_slot:
                bubble_index += 1
            else:
                last_slot = slot
                bubble_index = 0
            bubble["_BubbleIndex"] = bubble_index

            bubbles_in_slots[slot] -= 1
            if bubbles_in_slots[slot] != 0:
                bubble["_NextBubbleIndex"] = bubble_index + 1

    def save(self, output_directory: str):
        animation_directory = os.path.join(output_directory, self.name)
        os.makedirs(animation_directory, exist_ok=True)
        meta_filename = os.path.join(animation_directory, "meta.txt")

        file = FlipperFormatFile()
        file.setHeader(self.FILE_TYPE, self.FILE_VERSION)
        file.writeEmptyLine()

        # Write meta data
        file.writeKey("Width", self.meta["Width"])
        file.writeKey("Height", self.meta["Height"])
        file.writeKey("Passive frames", self.meta["Passive frames"])
        file.writeKey("Active frames", self.meta["Active frames"])
        file.writeKey("Frames order", self.meta["Frames order"])
        file.writeKey("Active cycles", self.meta["Active cycles"])
        file.writeKey("Frame rate", self.meta["Frame rate"])
        file.writeKey("Duration", self.meta["Duration"])
        file.writeKey("Active cooldown", self.meta["Active cooldown"])
        file.writeEmptyLine()

        file.writeKey("Bubble slots", self.bubble_slots)
        file.writeEmptyLine()

        # Write bubble data
        for bubble in self.bubbles:
            file.writeKey("Slot", bubble["Slot"])
            file.writeKey("X", bubble["X"])
            file.writeKey("Y", bubble["Y"])
            file.writeKey("Text", bubble["Text"])
            file.writeKey("AlignH", bubble["AlignH"])
            file.writeKey("AlignV", bubble["AlignV"])
            file.writeKey("StartFrame", bubble["StartFrame"])
            file.writeKey("EndFrame", bubble["EndFrame"])
            file.writeEmptyLine()

        file.save(meta_filename)

        to_pack = []
        for index, frame in enumerate(self.frames):
            to_pack.append(
                (frame, os.path.join(animation_directory, f"frame_{index}.bm"))
            )

        if ImageTools.is_processing_slow():
            pool = multiprocessing.Pool()
            pool.map(_convert_image_to_bm, to_pack)
        else:
            for image in to_pack:
                _convert_image_to_bm(image)

    def process(self):
        if ImageTools.is_processing_slow():
            pool = multiprocessing.Pool()
            self.frames = pool.map(_convert_image, self.frames)
        else:
            self.frames = list(_convert_image(frame) for frame in self.frames)


class DolphinManifest:
    FILE_TYPE = "Flipper Animation Manifest"
    FILE_VERSION = 1

    TEMPLATE_DIRECTORY = os.path.join(
        os.path.dirname(os.path.realpath(__file__)), "templates"
    )
    TEMPLATE_H = os.path.join(TEMPLATE_DIRECTORY, "dolphin.h.tmpl")
    TEMPLATE_C = os.path.join(TEMPLATE_DIRECTORY, "dolphin.c.tmpl")

    def __init__(self):
        self.animations = []
        self.logger = logging.getLogger("DolphinManifest")

    def load(self, source_directory: str):
        manifest_filename = os.path.join(source_directory, "manifest.txt")

        file = FlipperFormatFile()
        file.load(manifest_filename)

        # Check file header
        filetype, version = file.getHeader()
        assert filetype == self.FILE_TYPE
        assert version == self.FILE_VERSION

        # Load animation data
        while True:
            try:
                # Read animation specification
                name = file.readKey("Name")
                min_butthurt = file.readKeyInt("Min butthurt")
                max_butthurt = file.readKeyInt("Max butthurt")
                min_level = file.readKeyInt("Min level")
                max_level = file.readKeyInt("Max level")
                weight = file.readKeyInt("Weight")

                assert len(name) > 0
                assert min_butthurt >= 0
                assert max_butthurt >= 0 and max_butthurt >= min_butthurt
                assert min_level >= 0
                assert max_level >= 0 and max_level >= min_level
                assert weight >= 0

                # Initialize animation
                animation = DolphinBubbleAnimation(
                    name, min_butthurt, max_butthurt, min_level, max_level, weight
                )

                # Load Animation meta and frames
                animation.load(os.path.join(source_directory, name))

                # Add to array
                self.animations.append(animation)
            except EOFError:
                break

    def _renderTemplate(self, template_filename: str, output_filename: str, **kwargs):
        template = Templite(filename=template_filename)
        output = template.render(**kwargs)
        with open(output_filename, "w", newline="\n") as file:
            file.write(output)

    def save2code(self, output_directory: str, symbol_name: str):
        # Process frames
        for animation in self.animations:
            animation.process()

        # Prepare substitution data
        for animation in self.animations:
            animation.prepare()

        # Render Header
        self._renderTemplate(
            self.TEMPLATE_H,
            os.path.join(output_directory, f"assets_{symbol_name}.h"),
            animations=self.animations,
            symbol_name=symbol_name,
        )
        # Render Source
        self._renderTemplate(
            self.TEMPLATE_C,
            os.path.join(output_directory, f"assets_{symbol_name}.c"),
            animations=self.animations,
            symbol_name=symbol_name,
        )

    def save2folder(self, output_directory: str):
        manifest_filename = os.path.join(output_directory, "manifest.txt")
        file = FlipperFormatFile()
        file.setHeader(self.FILE_TYPE, self.FILE_VERSION)
        file.writeEmptyLine()

        for animation in self.animations:
            file.writeKey("Name", animation.name)
            file.writeKey("Min butthurt", animation.min_butthurt)
            file.writeKey("Max butthurt", animation.max_butthurt)
            file.writeKey("Min level", animation.min_level)
            file.writeKey("Max level", animation.max_level)
            file.writeKey("Weight", animation.weight)
            file.writeEmptyLine()

            animation.save(output_directory)

        file.save(manifest_filename)

    def save(self, output_directory: str, symbol_name: str):
        os.makedirs(output_directory, exist_ok=True)
        if symbol_name:
            self.save2code(output_directory, symbol_name)
        else:
            self.save2folder(output_directory)


class Dolphin:
    def __init__(self):
        self.manifest = DolphinManifest()
        self.logger = logging.getLogger("Dolphin")

    def load(self, source_directory: str):
        assert os.path.isdir(source_directory)
        # Load Manifest
        self.logger.info(f"Loading directory {source_directory}")
        self.manifest.load(source_directory)

    def pack(self, output_directory: str, symbol_name: str = None):
        self.manifest.save(output_directory, symbol_name)
