import os
import glob
import platform
import subprocess
from copy import copy, deepcopy
from pathlib import Path
from ursina.mesh import Mesh
from ursina import application
from panda3d.core import CullFaceAttrib
from time import perf_counter

imported_meshes = dict()
blender_scenes = dict()

def load_model(name, path=application.asset_folder, file_types=('.bam', '.ursinamesh', '.obj', '.glb', '.gltf', '.blend')):
    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:
            return deepcopy(imported_meshes[name])
        except:
            pass

    for filetype in file_types:
        # 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('loading bam')
                return loader.loadModel(filename)

            if filetype == '.ursinamesh':
                try:
                    with open(filename) as f:
                        m = eval(f.read())
                        m.path = filename
                        m.name = name
                        imported_meshes[name] = m
                        return m
                except:
                    print('invalid ursinamesh file:', filename)


            if filetype == '.obj':
                # print('found obj', filename)
                # m = loader.loadModel(filename)
                # m.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullCounterClockwise))
                m = obj_to_ursinamesh(path=path, name=name, return_mesh=True)
                m.path = filename
                m.name = name
                imported_meshes[name] = m
                return m

            elif filetype == '.blend':
                print('found blend file:', filename)
                if compress_models(path=path, name=name):
                    # obj_to_ursinamesh(name=name)
                    return load_model(name, path)

            else:
                try:
                    return loader.loadModel(filename)
                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 extention
        import shlex
        import winreg

        try:
            class_root = winreg.QueryValue(winreg.HKEY_CLASSES_ROOT, '.blend')
            with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'{}\shell\open\command'.format(class_root)) 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]
                    application.blender_paths[version_name] = list(blender_installation.glob('blender.exe'))[0]
        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

    from pprint import pprint
    print('blender_paths:')
    pprint(application.blender_paths)


def load_blender_scene(name, path=application.asset_folder, load=True, reload=False, skip_hidden=True, models_only=False):
    scenes_folder = Path(application.asset_folder / 'scenes')
    if not scenes_folder.exists():
        scenes_folder.mkdir()

    out_file_path = scenes_folder / f'{name}.py'
    # print('loading:', out_file_path)
    if reload or not out_file_path.exists():
        print('reload:')
        t = perf_counter()
        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('loading blender scene:', blend_file, '-->', out_file_path)
        blender = get_blender(blend_file)
        script_path = application.internal_scripts_folder / '_blender_scene_to_ursina.py'

        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 '',
        ]

        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.')

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

    with open(blend_file, 'rb') as f:
        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('blender_version:', blender_version_number)
        if blender_version_number in application.blender_paths:
            return application.blender_paths[blender_version_number]
        else:
            print('using default blender version')
            return application.blender_paths['default']


def compress_models(path=None, outpath=application.compressed_models_folder, name='*'):

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

    export_script_path = application.internal_scripts_folder / '_blend_export.py'
    exported = list()

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

        out_file_path = outpath / (blend_file.stem + '.obj')
        print('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=application.compressed_models_folder,
    outpath=application.compressed_models_folder,
    name='*',
    return_mesh=True,
    save_to_file=False,
    delete_obj=False
    ):

    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:', filepath)


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

        verts = list()
        tris = list()

        uv_indices = list()
        uvs = list()
        norm_indices = list()
        norms = list()

        # parse the obj file to a Mesh
        for i, l in enumerate(lines):
            if l.startswith('v '):
                vert = [float(v) for v in l[2:].strip().split(' ')]
                vert[0] = -vert[0]
                verts.append(tuple(vert))

            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:]
                l = l.split(' ')

                try:
                    tri = tuple([int(t.split('/')[0])-1 for t in l if t != '\n'])
                except:
                    print('error in obj file line:', i, ':', l)
                    return
                if len(tri) == 3:
                    tris.extend(tri)
                elif len(tri) == 4:
                    tris.extend((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]))


                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

        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]
            )

        meshstring = ''
        meshstring += 'Mesh('

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

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

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

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

        if not save_to_file:
            return meshstring

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

        if delete_obj:
            os.remove(filepath)

        print('saved ursinamesh to:', outfilepath)

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

    files = os.listdir(application.models_folder)
    compressed_files = os.listdir(application.compressed_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 = list()
                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=application.compressed_models_folder, max_decimals=3):
    from ursina.string_utilities import camel_to_snake

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


    for v in mesh.vertices:
        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 = list()
            for t in tris:
                if len(t) == 3:
                    new_tris.extend([t[0], t[1], t[2]])
                elif len(t) == 4: # turn quad into tris
                    new_tris.extend([t[0], t[1], t[2], t[2], t[3], t[0]])

            tris = new_tris


    if mesh.mode == 'ngon':
        tris = list()
        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 += ' '


    # print(obj)
    with open(out_path / (name + '.obj'), 'w') as f:
        f.write(obj)
        print('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 *
    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'
    # from ursina.shaders import basic_lighting_shader
    from ursina.shaders import normals_shader as rim_shader
    from ursina.shaders import matcap_shader as rim_shader
    # from ursina.shaders import height_shader as rim_shader


    t = time.time()
    # 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')
