#!/usr/bin/env python3
import os
import sys
from os import DirEntry
from typing import List, Tuple, Optional, Dict

from pyffi.formats.nif import NifFormat
from PIL import Image
import json


class Texture:
    file: str
    wrap_s: bool
    wrap_t: bool
    uv_set: int

    def __init__(self, file: str, wrap_s: bool = True, wrap_t: bool = True, uv_set: int = 0):
        self.file = file
        self.wrap_s = wrap_s
        self.wrap_t = wrap_t
        self.uv_set = uv_set

    def __dict__(self):
        return {
            'file': self.file,
            'wrapS': self.wrap_s,
            'wrapT': self.wrap_t,
            'uvSet': self.uv_set,
        }


class Shape:
    name: str
    vertices: List[Tuple[float, float, float]]
    normals: List[Tuple[float, float, float]]
    faces: List[Tuple[float, float, float]]
    uv_sets: List[List[float]]
    translation: Tuple[float, float, float]
    rotation: List[Tuple[float, float, float]]
    texture: Optional[Texture]

    def __init__(self, name: str = ""):
        self.name = name
        self.texture = None

    def __dict__(self):
        return {
            'name': self.name,
            'vertices': self.vertices,
            'normals': self.normals,
            'faces': self.faces,
            'uvSets': self.uv_sets,
            'translation': self.translation,
            'rotation': self.rotation,
            'texture': self.texture.__dict__(),
        }


class ShapeCollection:
    name: str
    shapes: List[Shape]

    def __init__(self, name: str = ""):
        self.name = name
        self.shapes = []

    def __dict__(self):
        return {
            'name': self.name,
            'shapes': [s.__dict__() for s in self.shapes],
        }


def export(path) -> List[ShapeCollection]:
    data = NifFormat.Data()
    with open(path, 'rb') as f:
        data.inspect(f)
        data.read(f)

    objects = []
    for root in data.roots:
        name = root.name.decode('utf-8').lower()
        # For this project, only hair and head matters
        if 'hair' in name or 'head' in name:
            sys.stderr.write("> " + name + "\n")
        else:
            sys.stderr.write("  " + name + "\n")
            continue

        if isinstance(root, NifFormat.NiNode):
            objects.append(export_object(root))

        # Some NiTriShape's are a root object
        if isinstance(root, NifFormat.NiTriShape):
            coll = ShapeCollection(name)
            coll.shapes = [export_shape(root)]

            objects.append(coll)

    return objects


def export_object(root: NifFormat.NiNode) -> List[ShapeCollection]:
    obj = ShapeCollection(root.name.decode('utf-8'))

    for shape in root.tree():
        if not isinstance(shape, NifFormat.NiTriShape):
            continue

        obj.shapes.append(export_shape(shape))

    return obj


def export_shape(shape: NifFormat.NiTriShape) -> Shape:
    vertices = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.vertices)]
    faces = shape.data.get_triangles()
    uv_sets = [[(coord.u, coord.v) for coord in uv_set] for uv_set in shape.data.uv_sets]
    normals = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.normals)]

    res_shape = Shape()
    for property in shape.get_properties():
        if isinstance(property, NifFormat.NiTexturingProperty):
            res_shape.texture = export_texture(property.base_texture)

    res_shape.name = shape.name.decode('utf-8')
    res_shape.vertices = vertices
    res_shape.faces = faces
    res_shape.uv_sets = uv_sets
    res_shape.normals = normals
    res_shape.translation = (shape.translation.x, shape.translation.y, shape.translation.z)
    res_shape.rotation = shape.rotation.as_list()

    return res_shape


def export_texture(texture) -> Texture:
    wrap_s = texture.clamp_mode in [2, 3]
    wrap_t = texture.clamp_mode in [1, 3]
    source = texture.source

    return Texture(source.file_name.decode('utf-8'), wrap_s, wrap_t, texture.uv_set)


# Dumb file map allowing case-insensitive matching
# And Morrowind's fucky file resolving
class TextureMap:
    path: str
    files: Dict[str, DirEntry]
    dds_used: int
    original_dds: int
    original_used: int

    def __init__(self, path):
        self.path = path
        self.files = {}
        self.dds_used = 0
        self.original_dds = 0
        self.original_used = 0

    def build(self):
        for item in os.scandir(self.path):
            self.files[item.name.lower()] = item

    def get(self, name):
        lower_name = name.lower()
        dds_instead = lower_name.rsplit('.', maxsplit=1)[0] + '.dds'

        if dds_instead in self.files:
            if dds_instead == lower_name:
                self.original_dds += 1
            else:
                self.dds_used += 1
            return self.files[dds_instead].path

        self.original_used += 1
        return self.files[lower_name].path


def main():
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <nif dir> <texture dir> <output dir>")
        exit(1)

    nif_dir = sys.argv[1]
    texture_dir = sys.argv[2]
    texture_dir_map = TextureMap(texture_dir)
    texture_dir_map.build()

    output_dir = sys.argv[3]

    output_texture_dir = os.path.join(output_dir, 'texture')
    output_shape_dir = os.path.join(output_dir, 'shape')
    os.makedirs(output_shape_dir, exist_ok=True)
    os.makedirs(output_texture_dir, exist_ok=True)

    for item in os.scandir(nif_dir):
        if not item.is_file():
            continue
        file_objects = export(item.path)

        for file_object in file_objects:
            name = file_object.name = file_object.name.lower()

            for shape in file_object.shapes:
                if shape.texture is not None:
                    new_texture_name = shape.texture.file.rsplit('.', maxsplit=1)[0] + '.png'
                    new_texture_name = new_texture_name.lower()
                    new_file_name = os.path.join(output_texture_dir, new_texture_name)

                    texture_image = Image.open(texture_dir_map.get(shape.texture.file))
                    texture_image.save(new_file_name, 'png')
                    shape.texture.file = new_texture_name

            shape_file_name = os.path.join(output_shape_dir, name + '.json')

            with open(shape_file_name, 'w') as sf:
                json.dump(file_object.__dict__(), sf)

    print(f"Textures: {texture_dir_map.original_dds} Orig. DDS, {texture_dir_map.dds_used} New DDS, {texture_dir_map.original_used} Orig. Non-DDS")


if __name__ == '__main__':
    main()
