import os
import subprocess
from typing import Dict, List, Tuple

import bpy
from bpy.types import Material
from bpy.types import Object

import lnx.api
import lnx.assets as assets
import lnx.exporter
import lnx.log as log
import lnx.material.cycles as cycles
import lnx.material.make_decal as make_decal
import lnx.material.make_depth as make_depth
import lnx.material.make_mesh as make_mesh
import lnx.material.make_overlay as make_overlay
import lnx.material.make_transluc as make_transluc
import lnx.material.make_refract as make_refract
import lnx.material.make_voxel as make_voxel
import lnx.material.mat_state as mat_state
import lnx.material.mat_utils as mat_utils
from lnx.material.shader import Shader, ShaderContext, ShaderData
import lnx.utils

if lnx.is_reload(__name__):
    lnx.api = lnx.reload_module(lnx.api)
    assets = lnx.reload_module(assets)
    lnx.exporter = lnx.reload_module(lnx.exporter)
    log = lnx.reload_module(log)
    cycles = lnx.reload_module(cycles)
    make_decal = lnx.reload_module(make_decal)
    make_depth = lnx.reload_module(make_depth)
    make_mesh = lnx.reload_module(make_mesh)
    make_overlay = lnx.reload_module(make_overlay)
    make_transluc = lnx.reload_module(make_transluc)
    make_voxel = lnx.reload_module(make_voxel)
    mat_state = lnx.reload_module(mat_state)
    mat_utils = lnx.reload_module(mat_utils)
    lnx.material.shader = lnx.reload_module(lnx.material.shader)
    from lnx.material.shader import Shader, ShaderContext, ShaderData
    lnx.utils = lnx.reload_module(lnx.utils)
else:
    lnx.enable_reload(__name__)

rpass_hook = None


def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxusers) -> Tuple:
    mat_state.mat_users = mat_users
    mat_state.mat_lnxusers = mat_lnxusers
    mat_state.material = material
    mat_state.nodes = material.node_tree.nodes
    mat_state.data = ShaderData(material)
    mat_state.output_node = cycles.node_by_type(mat_state.nodes, 'OUTPUT_MATERIAL')
    if mat_state.output_node is None:
        # Place empty material output to keep compiler happy..
        mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial')

    wrd = bpy.data.worlds['Lnx']
    rpdat = lnx.utils.get_rp()
    rpasses = mat_utils.get_rpasses(material)
    matname = lnx.utils.safesrc(lnx.utils.asset_name(material))
    rel_path = lnx.utils.build_dir() + '/compiled/Shaders/'
    full_path = lnx.utils.get_fp() + '/' + rel_path
    if not os.path.exists(full_path):
        os.makedirs(full_path)

    make_instancing_and_skinning(material, mat_users)

    bind_constants = dict()
    bind_textures = dict()

    for rp in rpasses:
        car = []
        bind_constants[rp] = car
        mat_state.bind_constants = car
        tar = []
        bind_textures[rp] = tar
        mat_state.bind_textures = tar

        con = None

        if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['make_rpass'] is not None:
            con = lnx.api.drivers[rpdat.rp_driver]['make_rpass'](rp)

        if con is not None:
            pass

        elif rp == 'mesh':
            con = make_mesh.make(rp, rpasses)

        elif rp == 'shadowmap':
            con = make_depth.make(rp, rpasses, shadowmap=True)

        elif rp == 'shadowmap_transparent':
            con = make_depth.make(rp, rpasses, shadowmap=True, shadowmap_transparent=True)

        elif rp == 'translucent':
            con = make_transluc.make(rp)

        elif rp == 'refraction':
            con = make_refract.make(rp)

        elif rp == 'overlay':
            con = make_overlay.make(rp)

        elif rp == 'decal':
            con = make_decal.make(rp)

        elif rp == 'depth':
            con = make_depth.make(rp, rpasses)

        elif rp == 'voxel':
            con = make_voxel.make(rp)

        elif rpass_hook is not None:
            con = rpass_hook(rp)

        write_shaders(rel_path, con, rp, matname)

    shader_data_name = matname + '_data'

    if wrd.lnx_single_data_file:
        if 'shader_datas' not in lnx.exporter.current_output:
            lnx.exporter.current_output['shader_datas'] = []
        lnx.exporter.current_output['shader_datas'].append(mat_state.data.get()['shader_datas'][0])
    else:
        lnx.utils.write_lnx(full_path + '/' + matname + '_data.lnx', mat_state.data.get())
        shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx'
        assets.add_shader_data(shader_data_path)

    return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures


def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> None:
    keep_cache = mat_state.material.lnx_cached
    write_shader(rel_path, con.vert, 'vert', rpass, matname, keep_cache=keep_cache)
    write_shader(rel_path, con.frag, 'frag', rpass, matname, keep_cache=keep_cache)
    write_shader(rel_path, con.geom, 'geom', rpass, matname, keep_cache=keep_cache)
    write_shader(rel_path, con.tesc, 'tesc', rpass, matname, keep_cache=keep_cache)
    write_shader(rel_path, con.tese, 'tese', rpass, matname, keep_cache=keep_cache)


def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: str, keep_cache=True) -> None:
    if shader is None or shader.is_linked:
        return

    # TODO: blend context
    if rpass == 'mesh' and mat_state.material.lnx_blending:
        rpass = 'blend'

    file_ext = '.glsl'
    if shader.noprocessing:
        # Use hlsl directly
        hlsl_dir = lnx.utils.build_dir() + '/compiled/Hlsl/'
        if not os.path.exists(hlsl_dir):
            os.makedirs(hlsl_dir)
        file_ext = '.hlsl'
        rel_path = rel_path.replace('/compiled/Shaders/', '/compiled/Hlsl/')

    shader_file = matname + '_' + rpass + '.' + ext + file_ext
    shader_path = lnx.utils.get_fp() + '/' + rel_path + '/' + shader_file
    assets.add_shader(shader_path)
    if not os.path.isfile(shader_path) or not keep_cache:
        with open(shader_path, 'w') as f:
            f.write(shader.get())

        if shader.noprocessing:
            cwd = os.getcwd()
            os.chdir(lnx.utils.get_fp() + '/' + rel_path)
            hlslbin_path = lnx.utils.get_sdk_path() + '/lib/leenkx_tools/hlslbin/hlslbin.exe'
            prof = 'vs_5_0' if ext == 'vert' else 'ps_5_0' if ext == 'frag' else 'gs_5_0'
            # noprocessing flag - gets renamed to .d3d11
            args = [hlslbin_path.replace('/', '\\').replace('\\\\', '\\'), shader_file, shader_file[:-4] + 'glsl', prof]
            if ext == 'vert':
                args.append('-i')
                args.append('pos')
            proc = subprocess.call(args)
            os.chdir(cwd)


def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[Object]]) -> None:
    """Build material with instancing or skinning if enabled.
    If the material is a custom material, only validation checks for instancing are performed."""
    global_elems = []
    if mat_users is not None and mat in mat_users:
        # Whether there are both an instanced object and a not instanced object with this material
        instancing_usage = [False, False]
        mat_state.uses_instancing = False

        for bo in mat_users[mat]:
            if mat.lnx_custom_material == '':
                # Morph Targets
                if lnx.utils.export_morph_targets(bo):
                    global_elems.append({'name': 'morph', 'data': 'short2norm'})
                # GPU Skinning
                if lnx.utils.export_bone_data(bo):
                    global_elems.append({'name': 'bone', 'data': 'short4norm'})
                    global_elems.append({'name': 'weight', 'data': 'short4norm'})

            # Instancing
            inst = bo.lnx_instanced
            if inst != 'Off' or mat.lnx_particle_flag:
                instancing_usage[0] = True
                mat_state.uses_instancing = True

                if mat.lnx_custom_material == '':
                    global_elems.append({'name': 'ipos', 'data': 'float3'})
                    if 'Rot' in inst:
                        global_elems.append({'name': 'irot', 'data': 'float3'})
                    if 'Scale' in inst:
                        global_elems.append({'name': 'iscl', 'data': 'float3'})

            elif inst == 'Off':
                # Ignore children of instanced objects, they are instanced even when set to 'Off'
                instancing_usage[1] = bo.parent is None or bo.parent.lnx_instanced == 'Off'

        if instancing_usage[0] and instancing_usage[1]:
            # Display a warning for invalid instancing configurations
            # See https://github.com/leenkx3d/leenkx/issues/2072
            log.warn(f'Material "{mat.name}" has both instanced and not instanced objects, objects might flicker!')

    if mat.lnx_custom_material == '':
        mat_state.data.global_elems = global_elems