import hashlib
import json
import os
import zipfile
from typing import Optional, Any

import Utils
from .Locations import AdventureLocation, LocationData
from Utils import OptionsType
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
from itertools import chain

import bsdiff4

ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"


class AdventureAutoCollectLocation:
    short_location_id: int = 0
    room_id: int = 0

    def __init__(self, short_location_id: int, room_id: int):
        self.short_location_id = short_location_id
        self.room_id = room_id

    def get_dict(self):
        return {
            "short_location_id": self.short_location_id,
            "room_id": self.room_id,
        }


class AdventureForeignItemInfo:
    short_location_id: int = 0
    room_id: int = 0
    room_x: int = 0
    room_y: int = 0

    def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int):
        self.short_location_id = short_location_id
        self.room_id = room_id
        self.room_x = room_x
        self.room_y = room_y

    def get_dict(self):
        return {
            "short_location_id": self.short_location_id,
            "room_id": self.room_id,
            "room_x": self.room_x,
            "room_y": self.room_y,
        }


class BatNoTouchLocation:
    short_location_id: int
    room_id: int
    room_x: int
    room_y: int
    local_item: int

    def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None):
        self.short_location_id = short_location_id
        self.room_id = room_id
        self.room_x = room_x
        self.room_y = room_y
        self.local_item = local_item

    def get_dict(self):
        ret_dict = {
            "short_location_id": self.short_location_id,
            "room_id": self.room_id,
            "room_x": self.room_x,
            "room_y": self.room_y,
        }
        if self.local_item is not None:
            ret_dict["local_item"] = self.local_item
        else:
            ret_dict["local_item"] = 255
        return ret_dict


class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
    hash = ADVENTUREHASH
    game = "Adventure"
    patch_file_ending = ".apadvn"
    zip_version: int = 2

    # locations: [], autocollect: [], seed_name: bytes,
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        patch_only = True
        if "autocollect" in kwargs:
            patch_only = False
            self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y)
                                  for loc in kwargs["locations"]]

            self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"]
            self.seedName: bytes = kwargs["seed_name"]
            self.local_item_locations: {} = kwargs["local_item_locations"]
            self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"]
            self.diff_a_mode: int = kwargs["diff_a_mode"]
            self.diff_b_mode: int = kwargs["diff_b_mode"]
            self.bat_logic: int = kwargs["bat_logic"]
            self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"]
            self.rom_deltas: {int, int} = kwargs["rom_deltas"]
            del kwargs["locations"]
            del kwargs["autocollect"]
            del kwargs["seed_name"]
            del kwargs["local_item_locations"]
            del kwargs["dragon_speed_reducer_info"]
            del kwargs["diff_a_mode"]
            del kwargs["diff_b_mode"]
            del kwargs["bat_logic"]
            del kwargs["bat_no_touch_locations"]
            del kwargs["rom_deltas"]
        super(AdventureDeltaPatch, self).__init__(*args, **kwargs)

    def write_contents(self, opened_zipfile: zipfile.ZipFile):
        super(AdventureDeltaPatch, self).write_contents(opened_zipfile)
        # write Delta
        opened_zipfile.writestr("zip_version",
                                self.zip_version.to_bytes(1, "little"),
                                compress_type=zipfile.ZIP_STORED)
        if self.foreign_items is not None:
            loc_bytes = []
            for foreign_item in self.foreign_items:
                loc_bytes.append(foreign_item.short_location_id)
                loc_bytes.append(foreign_item.room_id)
                loc_bytes.append(foreign_item.room_x)
                loc_bytes.append(foreign_item.room_y)
            opened_zipfile.writestr("adventure_locations",
                                    bytes(loc_bytes),
                                    compress_type=zipfile.ZIP_LZMA)
        if self.autocollect_items is not None:
            loc_bytes = []
            for item in self.autocollect_items:
                loc_bytes.append(item.short_location_id)
                loc_bytes.append(item.room_id)
            opened_zipfile.writestr("adventure_autocollect",
                                    bytes(loc_bytes),
                                    compress_type=zipfile.ZIP_LZMA)
        if self.player_name is not None:
            opened_zipfile.writestr("player",
                                    self.player_name,  # UTF-8
                                    compress_type=zipfile.ZIP_STORED)
        if self.seedName is not None:
            opened_zipfile.writestr("seedName",
                                    self.seedName,
                                    compress_type=zipfile.ZIP_STORED)
        if self.local_item_locations is not None:
            opened_zipfile.writestr("local_item_locations",
                                    json.dumps(self.local_item_locations),
                                    compress_type=zipfile.ZIP_LZMA)
        if self.dragon_speed_reducer_info is not None:
            opened_zipfile.writestr("dragon_speed_reducer_info",
                                    json.dumps(self.dragon_speed_reducer_info),
                                    compress_type=zipfile.ZIP_LZMA)
        if self.diff_a_mode is not None:
            opened_zipfile.writestr("diff_a_mode",
                                    self.diff_a_mode.to_bytes(1, "little"),
                                    compress_type=zipfile.ZIP_STORED)
        if self.diff_b_mode is not None:
            opened_zipfile.writestr("diff_b_mode",
                                    self.diff_b_mode.to_bytes(1, "little"),
                                    compress_type=zipfile.ZIP_STORED)
        if self.bat_logic is not None:
            opened_zipfile.writestr("bat_logic",
                                    self.bat_logic.to_bytes(1, "little"),
                                    compress_type=zipfile.ZIP_STORED)
        if self.bat_no_touch_locations is not None:
            loc_bytes = []
            for loc in self.bat_no_touch_locations:
                loc_bytes.append(loc.short_location_id)  # used for AP items managed by script
                loc_bytes.append(loc.room_id)  # used for local items placed in rom
                loc_bytes.append(loc.room_x)
                loc_bytes.append(loc.room_y)
                loc_bytes.append(0xff if loc.local_item is None else loc.local_item)
            opened_zipfile.writestr("bat_no_touch_locations",
                                    bytes(loc_bytes),
                                    compress_type=zipfile.ZIP_LZMA)
        if self.rom_deltas is not None:
            # this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter
            # if you're looking at doing something like this for another game, consider encoding your rom changes
            # in a more efficient way
            opened_zipfile.writestr("rom_deltas",
                                    json.dumps(self.rom_deltas),
                                    compress_type=zipfile.ZIP_LZMA)

    def read_contents(self, opened_zipfile: zipfile.ZipFile):
        super(AdventureDeltaPatch, self).read_contents(opened_zipfile)
        self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile)
        self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile)

    @classmethod
    def get_source_data(cls) -> bytes:
        return get_base_rom_bytes()

    @classmethod
    def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool:
        version_bytes = opened_zipfile.read("zip_version")
        version = 0
        if version_bytes is not None:
            version = int.from_bytes(version_bytes, "little")
        if version != cls.zip_version:
            return False
        return True

    @classmethod
    def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str):
        seedbytes: bytes = opened_zipfile.read("seedName")
        namebytes: bytes = opened_zipfile.read("player")
        namestr: str = namebytes.decode("utf-8")
        return seedbytes, namestr

    @classmethod
    def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int):
        diff_a_bytes = opened_zipfile.read("diff_a_mode")
        diff_b_bytes = opened_zipfile.read("diff_b_mode")
        diff_a = 0
        diff_b = 0
        if diff_a_bytes is not None:
            diff_a = int.from_bytes(diff_a_bytes, "little")
        if diff_b_bytes is not None:
            diff_b = int.from_bytes(diff_b_bytes, "little")
        return diff_a, diff_b

    @classmethod
    def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int:
        bat_logic = opened_zipfile.read("bat_logic")
        if bat_logic is None:
            return 0
        return int.from_bytes(bat_logic, "little")

    @classmethod
    def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
        foreign_items = []
        readbytes: bytes = opened_zipfile.read("adventure_locations")
        bytelist = list(readbytes)
        for i in range(round(len(bytelist) / 4)):
            offset = i * 4
            foreign_items.append(AdventureForeignItemInfo(bytelist[offset],
                                                          bytelist[offset + 1],
                                                          bytelist[offset + 2],
                                                          bytelist[offset + 3]))
        return foreign_items

    @classmethod
    def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]:
        locations = []
        readbytes: bytes = opened_zipfile.read("bat_no_touch_locations")
        bytelist = list(readbytes)
        for i in range(round(len(bytelist) / 5)):
            offset = i * 5
            locations.append(BatNoTouchLocation(bytelist[offset],
                                                bytelist[offset + 1],
                                                bytelist[offset + 2],
                                                bytelist[offset + 3],
                                                bytelist[offset + 4]))
        return locations

    @classmethod
    def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
        autocollect_items = []
        readbytes: bytes = opened_zipfile.read("adventure_autocollect")
        bytelist = list(readbytes)
        for i in range(round(len(bytelist) / 2)):
            offset = i * 2
            autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1]))
        return autocollect_items

    @classmethod
    def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
        readbytes: bytes = opened_zipfile.read("local_item_locations")
        readstr: str = readbytes.decode()
        return json.loads(readstr)

    @classmethod
    def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}:
        readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info")
        readstr: str = readbytes.decode()
        return json.loads(readstr)

    @classmethod
    def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}:
        readbytes: bytes = opened_zipfile.read("rom_deltas")
        readstr: str = readbytes.decode()
        return json.loads(readstr)

    @classmethod
    def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray:
        rom_bytes = bytearray(base_bytes)
        for offset, value in rom_deltas.items():
            int_offset = int(offset)
            rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little")
        return rom_bytes


def apply_basepatch(base_rom_bytes: bytes) -> bytes:
    with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch:
        delta: bytes = basepatch.read()
    return bsdiff4.patch(base_rom_bytes, delta)


def get_base_rom_bytes(file_name: str = "") -> bytes:
    file_name = get_base_rom_path(file_name)
    with open(file_name, "rb") as file:
        base_rom_bytes = bytes(file.read())
    basemd5 = hashlib.md5()
    basemd5.update(base_rom_bytes)
    if ADVENTUREHASH != basemd5.hexdigest():
        raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. "
                        "Get the correct game and version, then dump it")
    return base_rom_bytes


def get_base_rom_path(file_name: str = "") -> str:
    options: OptionsType = Utils.get_options()
    if not file_name:
        file_name = options["adventure_options"]["rom_file"]
    if not os.path.exists(file_name):
        file_name = Utils.user_path(file_name)
    return file_name