import os
import sys
import configparser
from dataclasses import dataclass, asdict
import json
import shutil

verbose = False

@dataclass
class IndexEntry:
    id: str
    name: str
    vendor: str
    incubating: bool
    warningMessage: str
    infoMessage: str

@dataclass
class Manifest:
    name: str
    version: str
    new_install_prompt_erase: str
    funding_url: str
    builds: list

@dataclass
class ManifestBuild:
    chipFamily: str
    parts: list

@dataclass
class ManifestBuildPart:
    path: str
    offset: int

@dataclass
class DeviceIndex:
    version: str
    devices: list

if sys.platform == "win32":
    shell_color_red = ""
    shell_color_orange = ""
    shell_color_reset = ""
else:
    shell_color_red = "\033[91m"
    shell_color_orange = "\033[93m"
    shell_color_reset = "\033[m"

def print_warning(message):
    print(f"{shell_color_orange}WARNING: {message}{shell_color_reset}")

def print_error(message):
    print(f"{shell_color_red}ERROR: {message}{shell_color_reset}")

def print_help():
    print("Usage: python generate-files.py [inPath] [outPath] [version]")
    print("     inPath   path with the extracted release files")
    print("     outPath  path where the CDN files will become available")
    print("     version  technical version name (e.g. 1.2.0)")
    print("Options:")
    print("  --verbose                      Show extra console output")

def exit_with_error(message):
    print_error(message)
    sys.exit(1)

def read_properties_file(path):
    config = configparser.RawConfigParser()
    config.read(path)
    return config

def read_device_properties(device_id):
    mapping_file_path = os.path.join("Boards", device_id, "devices.properties")
    if not os.path.isfile(mapping_file_path):
        exit_with_error(f"Mapping file not found: {mapping_file_path}")
    return read_properties_file(mapping_file_path)

def to_manifest_chip_name(name):
    if name == "esp32":
        return "ESP32"
    elif name == "esp32s2":
        return "ESP32-S2"
    elif name == "esp32s3":
        return "ESP32-S3"
    elif name == "esp32c3":
        return "ESP32-C3"
    elif name == "esp32c5":
        return "ESP32-C5"
    elif name == "esp32c6":
        return "ESP32-C6"
    elif name == "esp32p4":
        return "ESP32-P4"
    else:
        exit_with_error(f"to_manifest_chip_name() doesn't support {name} yet")
        return ""


def process_device(in_path: str, out_path: str, device_directory: str, device_id: str, device_properties: configparser, version: str):
    in_device_path = os.path.join(in_path, device_directory)
    in_device_binaries_path = os.path.join(in_device_path, "Binaries")
    assert os.path.isdir(in_device_binaries_path)
    flasher_args_path = os.path.join(in_device_binaries_path, "flasher_args.json")
    assert os.path.isfile(flasher_args_path)
    with open(flasher_args_path) as json_data:
        flasher_args = json.load(json_data)
        json_data.close()
        flash_files = flasher_args["flash_files"]
        manifest = Manifest(
            name=f"Tactility for {device_properties["general"]["vendor"]} {device_properties["general"]["boardName"]}",
            version=version,
            new_install_prompt_erase="true",
            funding_url="https://github.com/sponsors/ByteWelder",
            builds=[
                ManifestBuild(
                    chipFamily=to_manifest_chip_name(flasher_args["extra_esptool_args"]["chip"]),
                    parts=[]
                )
            ]
        )
        for offset in flash_files:
            flash_file_entry = flash_files[offset]
            flash_file_entry_name = os.path.basename(flash_file_entry)
            in_flash_file_path = os.path.join(in_device_binaries_path, flash_file_entry)
            out_flash_file_name = f"{device_id}-{flash_file_entry_name}"
            out_flash_file_path = os.path.join(out_path, out_flash_file_name)
            if verbose:
                print(f"Copying {in_flash_file_path} -> {out_flash_file_path}")
            shutil.copy(in_flash_file_path, out_flash_file_path)
            manifest.builds[0].parts.append(
                ManifestBuildPart(
                    path=out_flash_file_name,
                    offset=int(offset, 16)
                )
            )

        json_manifest_path = os.path.join(out_path, f"{device_id}.json")
        with open(json_manifest_path, 'w') as json_manifest_file:
            json.dump(asdict(manifest), json_manifest_file, indent=2)
            json_manifest_file.close()

def main(in_path: str, out_path: str, version: str):
    if not os.path.exists(in_path):
        exit_with_error(f"Input path not found: {in_path}")
    if os.path.exists(out_path):
        shutil.rmtree(out_path)
    os.mkdir(out_path)
    device_directories = os.listdir(in_path)
    device_index = DeviceIndex(version, [])
    for device_directory in device_directories:
        if not device_directory.endswith("-symbols"):
            device_id = device_directory[10:]
            device_properties = read_device_properties(device_id)
            if device_id not in device_properties.sections():
                exit_with_error(f"Mapping for {device_id} not found in mapping file")
            device_properties_general = device_properties["general"]
            device_properties_cdn = device_properties["cdn"]
            process_device(in_path, out_path, device_directory, device_id, device_properties, version)
            if "warningMessage" in device_properties_cdn.keys():
                warning_message = device_properties_cdn["warningMessage"]
            else:
                warning_message = None
            if "infoMessage" in device_properties_cdn.keys():
                info_message = device_properties_cdn["infoMessage"]
            else:
                info_message = None
            if "incubating" in device_properties_general.keys():
                incubating = device_properties_general["incubating"].lower() == 'true'
            else:
                incubating = False
            device_names = device_names["name"].split(',')
            for device_name in device_names:
                device_index.devices.append(asdict(IndexEntry(
                    id=device_id,
                    name=device_name,
                    vendor=device_properties_general["vendor"],
                    incubating=incubating,
                    warningMessage=warning_message,
                    infoMessage=info_message
                )))
    index_file_path = os.path.join(out_path, "index.json")
    with open(index_file_path, "w") as index_file:
        json.dump(asdict(device_index), index_file, indent=2)
        index_file.close()

if __name__ == "__main__":
    print("Tactility CDN File Generator")
    if "--help" in sys.argv:
        print_help()
        sys.exit()
    # Argument validation
    if len(sys.argv) < 4:
        print_help()
        sys.exit()
    if "--verbose" in sys.argv:
        verbose = True
        sys.argv.remove("--verbose")
    main(in_path=sys.argv[1], out_path=sys.argv[2], version=sys.argv[3])