import os
import platform
import subprocess
from copy import copy, deepcopy
from pathlib import Path
from ursina.mesh import Mesh
from ursina import application, color
from time import perf_counter
from ursina.string_utilities import print_info, print_warning
from ursina.vec3 import Vec3
import panda3d.core as p3d
import gltf
import builtins
from ursina.sequence import Func


imported_meshes = dict()
blender_scenes = dict()

def load_model(name, path=Func(getattr, application, 'asset_folder'), file_types=('.bam', '.ursinamesh', '.obj', '.glb', '.gltf', '.blend'), use_deepcopy=False, gltf_no_srgb=Func(getattr, application, 'gltf_no_srgb')):
    if callable(path):
        path = path()
    if callable(gltf_no_srgb):
        gltf_no_srgb = gltf_no_srgb()

    if not isinstance(name, str):
        raise TypeError(f"Argument save must be of type str, not {type(str)}")

    if '.' in name:
        full_name = name
        name = full_name.split('.')[0]
        file_types = ('.' + full_name.split('.',1)[1],)

    if name in imported_meshes:
        # print('load cached model', name)
        try:
            if not use_deepcopy:
                instance = copy(imported_meshes[name])
            else:
                instance = deepcopy(imported_meshes[name])

            instance.clearTexture()
            return instance

        except:
            pass

    for filetype in file_types:
        if use_deepcopy and filetype == '.bam':
            continue
        # warning: glob is case-insensitive on windows, so m.path will be all lowercase
        for filename in path.glob(f'**/{name}{filetype}'):
            if filetype == '.bam':
                # print_info('loading bam')
                return builtins.loader.loadModel(filename)  # type: ignore

            if filetype == '.gltf' or filetype == '.glb':
                gltf_settings = gltf.GltfSettings()
                gltf_settings.no_srgb = gltf_no_srgb
                model_root = gltf.load_model(str(filename), gltf_settings=gltf_settings)
                imported_meshes[name] = p3d.NodePath(model_root)
                return p3d.NodePath(model_root)

            if filetype == '.ursinamesh':
                try:
                    with open(filename) as f:
                        m = eval(f.read())
                        m.path = filename
                        m.name = name
                        m.vertices = [Vec3(*v) for v in m.vertices]
                        imported_meshes[name] = m
                        return m
                except:
                    print_warning('invalid ursinamesh file:', filename)


            if filetype == '.obj':
                # print('found obj', filename)
                m = obj_to_ursinamesh(path=path, name=name, return_mesh=True)
                m.path = filename
                m.name = name
                imported_meshes[name] = m
                if not use_deepcopy:
                    m.save(f'{name}.bam')

                return m

            elif filetype == '.blend':
                print_info('found blend file:', filename)
                if compress_models(path=path, name=name):
                    # obj_to_ursinamesh(name=name)
                    return load_model(name, path, use_deepcopy=use_deepcopy)
            else:
                try:
                    return builtins.loader.loadModel(filename)  # type: ignore
                except:
                    pass

    return None


# find blender installations
if application.development_mode:

    if platform.system() == 'Windows':
        # get blender path by getting default program for '.blend' file extension
        import shlex
        import winreg

        try:
            class_root = winreg.QueryValue(winreg.HKEY_CLASSES_ROOT, '.blend')
            with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, fr'{class_root}\shell\open\command') as key:
                command = winreg.QueryValueEx(key, '')[0]
                default_blender = shlex.split(command)[0]
                default_blender = Path(default_blender)
                application.blender_paths['default'] = default_blender
                blender_foundation_directory = default_blender.parent.parent

                for blender_installation in blender_foundation_directory.glob('*'):
                    first_folder = tuple(blender_installation.glob('*'))[0] # version
                    version_name = first_folder.name[:3]
                    blender_path = list(blender_installation.glob('blender.exe'))[0]
                    if application.blender_path['default'] == blender_path:
                        continue
                    application.blender_paths[version_name] = blender_path
        except:
            pass

    elif platform.system() == 'Linux':
        # Use "which" command to find blender
        which_process = subprocess.run(('which', 'blender'), stdout=subprocess.PIPE)
        if which_process.returncode == 0:
            blender_exec = which_process.stdout.decode().strip()
            application.blender_paths['default'] = blender_exec


def load_blender_scene(name, path=Func(getattr, application, 'asset_folder'), reload=False, skip_hidden=True, models_only=False, uvs=True, vertex_colors=True, normals=True, triangulate=True, decimals=4):
    if callable(path):
        path = path()

    scenes_folder = Path(application.asset_folder / 'scenes')
    if not scenes_folder.exists():
        scenes_folder.mkdir()

    out_file_path = scenes_folder / f'{name}.ursina_blender_scene'
    # print('loading:', out_file_path)
    if reload or not out_file_path.exists():
        print_info('reload:')
        blend_file = tuple(path.glob(f'**/{name}.blend'))
        if not blend_file:
            raise ValueError('no blender file found at:', path / name)

        blend_file = blend_file[0]
        print_info('loading blender scene:', blend_file, '-->', out_file_path)

        args = [
            get_blender(blend_file),
            blend_file,
            '--background',
            '--python',
            application.internal_scripts_folder / '_blender_scene_to_ursina.py',
            out_file_path,
            '--skip_hidden' if skip_hidden else '',
            '--models_only' if models_only else '',
            '--uvs' if uvs else '',
            '--normals' if normals else '',
            '--vertex_colors' if vertex_colors else '',
            '--triangulate' if triangulate else '',
            f'--decimals={decimals}',
        ]

        subprocess.run(args)


    with open(out_file_path) as f:
        file_content = f.read()
        loc = {}
        exec(file_content, globals(), loc)

        if models_only:
            blender_scenes[name] = loc['meshes']
            return loc['meshes']

        blender_scenes[name] = loc['scene_parent']
        return loc['scene_parent']



def get_blender(blend_file):    # try to get a matching blender version in case we have multiple blender version installed
    if not application.blender_paths:
        raise Exception('error: trying to load .blend file, but no blender installation was found. blender_paths:', application.blender_paths)

    if len(application.blender_paths) == 1:
        return application.blender_paths['default']

    with open(blend_file, 'rb') as f:
        try:
            blender_version_number = (f.read(12).decode("utf-8"))[-3:]   # get version from start of .blend file e.g. 'BLENDER-v280'
            blender_version_number = blender_version_number[0] + '.' + blender_version_number[1:2]
            print_info('blender_version:', blender_version_number)
            if blender_version_number in application.blender_paths:
                return application.blender_paths[blender_version_number]

            print_info('using default blender version')
            return application.blender_paths['default']
        except:
            print_info('using default blender version')
            return application.blender_paths['default']


def compress_models(path=None, out_path=Func(getattr, application, 'compressed_models_folder'), name='*'):
    if callable(out_path):
        out_path = out_path()

    if "/" in name or '\\' in name:
        raise Exception(f'Path character "/" or "\\" found in blender name ({name}). To successfully import .blend files, use only the file name.')

    if not application.compressed_models_folder.exists():
        application.compressed_models_folder.mkdir()

    export_script_path = application.internal_scripts_folder / '_blend_export.py'
    exported = []

    for blend_file in path.glob(f'**/{name}.blend'):
        blender = get_blender(blend_file)

        out_file_path = out_path / (blend_file.stem + '.obj')
        print_info('converting .blend file to .obj:', blend_file, '-->', out_file_path, 'using:', blender)

        if platform.system() == 'Windows':
            subprocess.call(f'''"{blender}" "{blend_file}" --background --python "{export_script_path}" "{out_file_path}"''')
        else:
            subprocess.run((blender, blend_file, '--background', '--python', export_script_path, out_file_path))

        exported.append(blend_file)

    return exported


def obj_to_ursinamesh(path=Func(getattr, application, 'compressed_models_folder'), out_path=Func(getattr, application, 'compressed_models_folder'), name='*', return_mesh=True, save_to_file=False, delete_obj=False):
    if callable(path):
        path = path()
    if callable(out_path):
        out_path = out_path()

    if name.endswith('.obj'):
        name = name[:-4]

    for f in path.glob(f'**/{name}.obj'):
        # filepath = path / (os.path.splitext(f)[0] + '.obj')
        print('read obj at:', f)


        with f.open('r') as file:
            lines = file.readlines()

        verts = []
        tris = []

        uv_indices = []
        uvs = []
        norm_indices = []
        norms = []
        normals = [] # final normals made getting norms with norm_indices

        vertex_colors = []
        vertex_colors_packed = []
        current_color = None
        mtl_data = None
        mtl_dict = {}

        # parse the obj file to a Mesh
        for i, l in enumerate(lines):  # noqa: E741
            if l.startswith('v '):
                parts = [float(v) for v in l[2:].strip().split(' ')]
                vert = parts[:3]
                vert[0] = -vert[0]
                verts.append(tuple(vert))
                if len(parts) > 3:
                    # current_color = color.rgb(*parts[3:])
                    vertex_colors_packed.append(color.rgb(*parts[3:]))

            elif l.startswith('vn '):
                n = l[3:].strip().split(' ')
                norms.append(tuple(float(e) for e in n))

            elif l.startswith('vt '):
                uv = l[3:].strip()
                uv = uv.split(' ')
                uvs.append(tuple(float(e) for e in uv))

            elif l.startswith('f '):
                l = l[2:]  # noqa: E741
                l = l.split(' ')  # noqa: E741

                try:
                    tri = tuple(int(t.split('/')[0])-1 for t in l if t != '\n')
                except:
                    print_warning('error in obj file line:', i, ':', l)
                    return
                if len(tri) == 3:
                    tris.extend(tri)
                    if current_color:
                        vertex_colors.extend([current_color for i in range(3)])
                    elif vertex_colors_packed:
                        vertex_colors.extend([vertex_colors_packed[idx] for idx in tri])

                elif len(tri) == 4:
                    tris.extend((tri[0], tri[1], tri[2], tri[2], tri[3], tri[0]))
                    if current_color:
                        vertex_colors.extend([current_color for i in range(6)])
                    elif vertex_colors_packed:
                        vertex_colors.extend([vertex_colors_packed[idx] for idx in (tri[0], tri[1], tri[2], tri[2], tri[3], tri[0])])

                else: # ngon
                    for i in range(1, len(tri)-1):
                        tris.extend((tri[i], tri[i+1], tri[0]))
                    if current_color:
                        vertex_colors.extend([current_color for i in range(len(tri))])
                    elif vertex_colors_packed:
                        for i in range(1, len(tri)-1):
                            vertex_colors.extend([vertex_colors_packed[idx] for idx in (tri[i], tri[i+1], tri[0])])

                try:
                    uv = tuple(int(t.split('/')[1])-1 for t in l)
                    if len(uv) == 3:
                        uv_indices.extend(uv)
                    elif len(uv) == 4:
                        uv_indices.extend((uv[0], uv[1], uv[2], uv[2], uv[3], uv[0]))
                    else: # ngon
                        for i in range(1, len(uv)-1):
                            uv_indices.extend((uv[i], uv[i+1], uv[0]))
                except: # if no uvs
                    pass

                try:
                    n = tuple(int(t.split('/')[2])-1 for t in l)
                    if len(n) == 3:
                        norm_indices.extend(n)
                    elif len(uv) == 4:
                        norm_indices.extend((n[0], n[1], n[2], n[2], n[3], n[0]))
                    else: # ngon
                        for i in range(1, len(n)-1):
                            norm_indices.extend((n[i], n[i+1], n[0]))
                except: # if no normals
                    pass

            elif l.startswith('mtllib '):    # load mtl file
                mtl_file_name = Path(f).with_suffix('.mtl')
                if mtl_file_name.exists():
                    with open(mtl_file_name) as mtl_file:
                        mtl_data = mtl_file.readlines()

                        for i in range(len(mtl_data)-1):
                            if mtl_data[i].startswith('newmtl '):
                                material_name = mtl_data[i].strip()[7:] # remove 'newmtl '
                                for j in range(i+1, min(i+8, len(mtl_data))):
                                    if mtl_data[j].startswith('newmtl'):
                                        break
                                    if mtl_data[j].startswith('Kd '):
                                        material_color = [float(e) for e in mtl_data[j].strip()[3:].split(' ')]
                                        mtl_dict[material_name] = *material_color, 1


            elif l.startswith('usemtl ') and mtl_data: # apply material color
                material_name = l[7:].strip()    # remove 'usemtl '
                if material_name in mtl_dict:
                    current_color = mtl_dict[material_name]


        if norms: # make sure we have normals and not just normal indices (weird edge case).
            normals = [(-norms[nid][0], norms[nid][1], norms[nid][2]) for nid in norm_indices]

        if return_mesh:
            return Mesh(
                vertices=[verts[t] for t in tris],
                normals=normals,
                uvs=[uvs[uid] for uid in uv_indices],
                colors=vertex_colors
            )

        meshstring = ''
        meshstring += 'Mesh('

        meshstring += '\nvertices='
        meshstring += str(tuple(verts[t] for t in tris))

        if vertex_colors:
            meshstring += '\ncolors='
            meshstring += str(tuple(col for col in vertex_colors))

        if uv_indices:
            meshstring += ', \nuvs='
            meshstring += str(tuple(uvs[uid] for uid in uv_indices))

        if normals:
            meshstring += ', \nnormals='
            meshstring += str(normals)

        meshstring += ''', \nmode='triangle')'''

        if not save_to_file:
            return meshstring

        out_path = out_path / (os.path.splitext(f)[0] + '.ursinamesh')
        with open(out_path, 'w') as file:
            file.write(meshstring)

        if delete_obj:
            os.remove(out_path)

        print_info('saved ursinamesh to:', out_path)

# faster, but does not apply modifiers
def compress_models_fast(model_name=None, write_to_disk=False):
    print_info('find models')
    from tinyblend import BlenderFile  # type: ignore
    application.compressed_models_folder.mkdir(parents=True, exist_ok=True)

    files = os.listdir(application.models_folder)

    for f in files:
        if f.endswith('.blend'):
            # print('f:', application.compressed_models_folder + '/' + f)
            print('compress______', f)
            blend = BlenderFile(application.models_folder + '/' + f)
            number_of_objects = len(blend.list('Object'))

            for o in blend.list('Object'):
                if not o.data.mvert:
                    continue
                # print(o.id.name.decode("utf-8", "strict"))
                object_name = o.id.name.decode( "utf-8").replace(".", "_")[2:]
                object_name = object_name.split('\0', 1)[0]
                print('name:', object_name)

                verts = [v.co for v in o.data.mvert]
                verts = tuple(verts)

                file_content = 'Mesh(' + str(verts)

                file_name = ''.join([f.split('.')[0], '.ursinamesh'])
                if number_of_objects > 1:
                    file_name = ''.join([f.split('.')[0], '_', object_name, '.ursinamesh'])
                file_path = os.path.join(application.compressed_models_folder, file_name)
                print(file_path)

                tris = tuple(triindex.v for triindex in o.data.mloop)
                flippedtris = []
                for i in range(0, len(tris)-3, 3):
                    flippedtris.append(tris[i+2])
                    flippedtris.append(tris[i+1])
                    flippedtris.append(tris[i+0])

                file_content += ', triangles=' + str(flippedtris)

                if o.data.mloopuv:
                    uvs = tuple(v.uv for v in o.data.mloopuv)
                    file_content += ', uvs=' + str(uvs)

                file_content += ''', mode='triangle')'''

                if write_to_disk:
                    with open(file_path, 'w') as file:
                        file.write(file_content)

                return file_content

def ursina_mesh_to_obj(mesh, name='', out_path=Func(getattr, application, 'compressed_models_folder'), max_decimals=5, flip_faces=True):
    if callable(out_path):
        out_path = out_path()

    from ursina.string_utilities import camel_to_snake

    obj = ''
    obj += f'mtllib {name}.mtl\n'
    obj += f'usemtl {name}\n'

    if not name:
        name = camel_to_snake(mesh.__class__.__name__)
    obj += 'o ' + name + '\n'
    verts = mesh.vertices

    for v in verts:
        v = [round(e, max_decimals) for e in v]
        obj += f'v {v[0]} {v[1]} {v[2]}\n'

    if mesh.uvs:
        for uv in mesh.uvs:
            uv = [round(e, max_decimals) for e in uv]
            obj += f'vt {uv[0]} {uv[1]}\n'

    obj += 's off\n'

    if mesh.triangles:
        tris = mesh.triangles

        if isinstance(tris[0], tuple): # convert from tuples to flat
            new_tris = []
            for t in tris:
                if len(t) == 3:
                    if not flip_faces:
                        new_tris.extend([t[0], t[1], t[2]])
                    else:
                        new_tris.extend([t[2], t[1], t[0]])
                elif len(t) == 4: # turn quad into tris
                    if not flip_faces:
                        new_tris.extend([t[0], t[1], t[2], t[2], t[3], t[0]])
                    else:
                        new_tris.extend([t[2], t[1], t[0], t[0], t[3], t[2]])

            tris = new_tris


    if mesh.mode == 'ngon':
        tris = []
        for i in range(1, len(mesh.vertices)-1):
            tris.extend((i, i+1, 0))


    # tris must be a list of indices
    for i, t in enumerate(tris):
        if i % 3 == 0:
            obj += '\nf '
        obj += str(t+1)
        if mesh.uvs:
            obj += '/'+str(t+1)
        obj += ' '

    obj += '\n'
    # print(obj)
    with open(out_path / (name + '.obj'), 'w') as f:
        f.write(obj)
        print_info('saved obj:', out_path / (name + '.obj'))



def compress_internal():
    compress_models(application.internal_models_folder)
    obj_to_ursinamesh(
        application.internal_models_compressed_folder,
        application.internal_models_compressed_folder,
        return_mesh=False, save_to_file=True, delete_obj=True
        )


if __name__ == '__main__':
    # compress_internal()
    from ursina import *
    from ursina import Ursina, Entity, EditorCamera, Sky
    app = Ursina()
    # print('imported_meshes:\n', imported_meshes)
    # Entity(model='quad').model.save('quad.bam')
    # m = obj_to_ursinamesh(path=application.asset_folder.parent / 'samples', name='procedural_rock_0')
    # Entity(model=m)
    # EditorCamera()


    # application.asset_folder = application.asset_folder.parent / 'samples'
    # application.asset_folder = Path(r'C:\\Users\\Petter\\Downloads\\')
    # Entity(model='c1a0')
    # from ursina.shaders import lit_with_shadows_shader
    # Entity.default_shader = lit_with_shadows_shader
    # Entity(model='race')
    # Entity(model='ambulance', x=1.5)

    application.asset_folder = Path(r'''C:\Users\Petter\Downloads''')
    t = perf_counter()
    Entity(model='untitled')
    print('-------', perf_counter() - t)
    m = load_model('cube', use_deepcopy=True)
    # ground = Entity(model='plane', scale=10, texture='brick', texture_scale=Vec2(4))
    # DirectionalLight()

    # blender_scene = load_blender_scene(path=application.asset_folder, name='desert', reload=True)
    # blender_scene = load_blender_scene(path=application.asset_folder, name='blender_level_editor_test_scene_2')
    # print('-------', time.time() - t)

    # print('--------', blender_scene.children)
    # for e in blender_scene.children:
    #     # e.color = color.random_color()
    #     e.shader = rim_shader
    #     e.texture='matcap_4'
    #
    #
    # blender_scene.Plane_002.collider = 'mesh'
    # from ursina.prefabs.first_person_controller import FirstPersonController
    # player = FirstPersonController()

    # def input(key):
    #     if key == '+':
    #         for e in blender_scene.children:
    #             e.texture_scale = Vec2(e.texture_scale[0], e.texture_scale[1]+.1)
    #     if key == '-':
    #         for e in blender_scene.children:
    #             e.texture_scale = Vec2(e.texture_scale[0], e.texture_scale[1]-.1)
    #         print(blender_scene.children[0].texture_scale)
    #
    EditorCamera()
    Sky(texture='sky_sunset')
    # def update():
    #     blender_scene.Cube.x += (held_keys['d'] - held_keys['a']) * time.dt * 10


    app.run()
    # e = Entity(model=Cylinder(16))
    # ursina_mesh_to_obj(e.model, name='quad_export_test')
