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

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

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

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

rpass_hook = None


def build(material: Material, mat_users: Dict[Material, List[Object]], mat_armusers) -> Tuple:
    mat_state.mat_users = mat_users
    mat_state.mat_armusers = mat_armusers
    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['Arm']
    rpdat = arm.utils.get_rp()
    rpasses = mat_utils.get_rpasses(material)
    is_emissive = mat_utils.is_emmisive(material)
    if is_emissive and '_Emission' not in wrd.world_defs:
        wrd.world_defs += '_Emission'
    matname = arm.utils.safesrc(arm.utils.asset_name(material))
    rel_path = arm.utils.build_dir() + '/compiled/Shaders/'
    full_path = arm.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 != 'Armory' and arm.api.drivers[rpdat.rp_driver]['make_rpass'] is not None:
            con = arm.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 == 'translucent':
            con = make_transluc.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.arm_single_data_file:
        if 'shader_datas' not in arm.exporter.current_output:
            arm.exporter.current_output['shader_datas'] = []
        arm.exporter.current_output['shader_datas'].append(mat_state.data.get()['shader_datas'][0])
    else:
        arm.utils.write_arm(full_path + '/' + matname + '_data.arm', mat_state.data.get())
        shader_data_path = arm.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.arm'
        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.arm_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.arm_blending:
        rpass = 'blend'

    file_ext = '.glsl'
    if shader.noprocessing:
        # Use hlsl directly
        hlsl_dir = arm.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 = arm.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(arm.utils.get_fp() + '/' + rel_path)
            hlslbin_path = arm.utils.get_sdk_path() + '/lib/armory_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]

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

            # Instancing
            inst = bo.arm_instanced
            if inst != 'Off' or mat.arm_particle_flag:
                instancing_usage[0] = True

                if mat.arm_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.arm_instanced == 'Off'

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

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