package org.openrndr.extra.dnk3

import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.cubemap.glslEvaluateSH
import org.openrndr.extra.dnk3.cubemap.glslFetchSH
import org.openrndr.extra.dnk3.cubemap.genGlslGatherSH
import org.openrndr.extra.shaderphrases.phrases.phraseTbnMatrix
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.normalMatrix
import java.nio.ByteBuffer
import kotlin.math.cos

private val noise128 by lazy {
    val cb = colorBuffer(128, 128)
    val items = cb.width * cb.height * cb.format.componentCount
    val buffer = ByteBuffer.allocateDirect(items)
    for (y in 0 until cb.height) {
        for (x in 0 until cb.width) {
            for (i in 0 until 4)
                buffer.put((Math.random() * 255).toInt().toByte())
        }
    }
    buffer.rewind()
    cb.write(buffer)
    cb.generateMipmaps()
    cb.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR)
    cb.wrapU = WrapMode.REPEAT
    cb.wrapV = WrapMode.REPEAT
    cb
}

private fun PointLight.fs(index: Int, hasNormalAttribute: Boolean): String = """
|{
|   vec3 Lr = p_lightPosition$index - v_worldPosition;
|   float distance = length(Lr);
|   float attenuation = 1.0 / (p_lightConstantAttenuation$index +
|   p_lightLinearAttenuation$index * distance + p_lightQuadraticAttenuation$index * distance * distance);
|   vec3 L = normalize(Lr);
|
|   float side = ${if (hasNormalAttribute) "dot(L, N)" else "3.1415"};
|   f_diffuse += attenuation * max(0, side / 3.1415) * p_lightColor$index.rgb * m_color.rgb;
|   f_specular += attenuation * ggx(N, V, L, m_roughness, m_f0) * p_lightColor$index.rgb * m_color.rgb;
}
""".trimMargin()

private fun AmbientLight.fs(index: Int): String = "f_ambient += p_lightColor$index.rgb * ((1.0 - m_metalness) * m_color.rgb);"

private fun DirectionalLight.fs(index: Int, hasNormalAttribute: Boolean) = """
|{
|    vec3 L = normalize(-p_lightDirection$index);
|    float attenuation = 1.0;
|    vec3 H = normalize(V + L);
|    float NoL = ${if (hasNormalAttribute) "clamp(dot(N, L), 0.0, 1.0)" else "1"};
|    float LoH = clamp(dot(L, H), 0.0, 1.0);
|    float NoH = ${if (hasNormalAttribute) "clamp(dot(N, H), 0.0, 1.0)" else "1"};
|    vec3 Lr = (p_lightPosition$index - v_worldPosition);
//|    vec3 L = normalize(Lr);
|    ${shadows.fs(index)}
|    
|    f_diffuse += NoL * attenuation * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb * m_ambientOcclusion;;
|    float Dg = D_GGX(m_roughness * m_roughness, NoH, H);
|    float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL);
|    vec3 F = F_Schlick(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH);
|    vec3 Fr = (Dg * Vs) * F;
|    f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb *  m_ambientOcclusion;;
|}
""".trimMargin()

private fun HemisphereLight.fs(index: Int, hasNormalAttribute: Boolean): String = """
|{
|   float f = ${if (hasNormalAttribute) "dot(N, p_lightDirection$index) * 0.5 + 0.5" else "1"};
|   vec3 irr = ${irradianceMap?.let { "texture(p_lightIrradianceMap$index, N).rgb" } ?: "vec3(1.0)"};
|   f_diffuse += mix(p_lightDownColor$index.rgb, p_lightUpColor$index.rgb, f) * irr * ((1.0 - m_metalness) * m_color.rgb) * m_ambientOcclusion;
|}
""".trimMargin()

private fun SpotLight.fs(index: Int, hasNormalAttribute: Boolean): String {
    val shadows = shadows
    return """
|{
|   vec3 Lr = p_lightPosition$index - v_worldPosition;
|   float distance = length(Lr);
|   float attenuation = 1.0 / (p_lightConstantAttenuation$index +
|   p_lightLinearAttenuation$index * distance + p_lightQuadraticAttenuation$index * distance * distance);
|   attenuation = 1.0;
|   vec3 L = normalize(Lr);

|   float NoL = ${if (hasNormalAttribute) "clamp(dot(N, L), 0.0, 1.0)" else "1"};
|   float side = dot(L, N);
|   float hit = max(dot(-L, p_lightDirection$index), 0.0);
|   float falloff = clamp((hit - p_lightOuterCos$index) / (p_lightInnerCos$index - p_lightOuterCos$index), 0.0, 1.0);
|   attenuation *= falloff;
|   ${shadows.fs(index)}
|   {
|       vec3 H = normalize(V + L);
|       float LoH = clamp(dot(L, H), 0.0, 1.0);
|       float NoH = ${if (hasNormalAttribute) "clamp(dot(N, H), 0.0, 1.0)" else 1.0};
|       f_diffuse += NoL * (0.1+0.9*attenuation) * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb ;
|       float Dg = D_GGX(m_roughness * m_roughness, NoH, H);
|       float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL);
|       vec3 F = F_Schlick(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH);
|       vec3 Fr = (Dg * Vs) * F;
|       f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb;
|   }
}
""".trimMargin()
}

private fun Fog.fs(index: Int): String = """
|{
|    float dz = min(1.0, -v_viewPosition.z/p_fogEnd$index);
|    f_fog = vec4(p_fogColor$index.rgb, dz);
|}
""".trimMargin()

sealed class TextureSource
object DummySource : TextureSource() {
    override fun toString(): String {
        return "DummySource()"
    }
}

abstract class TextureFromColorBuffer(var texture: ColorBuffer, var textureFunction: TextureFunction) : TextureSource()

class TextureFromCode(val code: String) : TextureSource() {
    override fun hashCode(): Int {
        return code.hashCode()
    }
}

private fun TextureFromCode.fs(index: Int, target: TextureTarget) = """
|vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0);
|{
|vec4 texOut;
|$code;
|tex$index = texOut;
|}
"""

enum class TextureFunction(val function: (String, String) -> String) {
    TILING({ texture, uv -> "texture($texture, $uv)" }),
    NOT_TILING({ texture, uv -> "textureNoTile(p_textureNoise, $texture, x_noTileOffset, $uv)" })
    ;
}

/**
 * @param texture the texture to sample from
 * @param input input coordinates, default is "va_texCoord0.xy"
 * @param textureFunction the texture function to use, default is TextureFunction.TILING
 * @param pre the pre-fetch shader code to inject, can only adjust "x_texCoord"
 * @param post the post-fetch shader code to inject, can only adjust "x_texture"
 */
class ModelCoordinates(texture: ColorBuffer,
                       var input: String = "va_texCoord0.xy",
                       var tangentInput: String? = null,
                       textureFunction: TextureFunction = TextureFunction.TILING,
                       var pre: String? = null,
                       var post: String? = null) : TextureFromColorBuffer(texture, textureFunction) {
    override fun toString(): String {
        return "ModelCoordinates(texture: $texture, input: $input, $tangentInput: $tangentInput, textureFunction: $textureFunction, pre: $pre, post: $post)"
    }

    override fun hashCode(): Int {
        var result = input.hashCode()
        result = 31 * result + (tangentInput?.hashCode() ?: 0)
        result = 31 * result + (pre?.hashCode() ?: 0)
        result = 31 * result + (post?.hashCode() ?: 0)
        return result
    }
}


class Triplanar(texture: ColorBuffer,
                var scale: Double = 1.0,
                var offset: Vector3 = Vector3.ZERO,
                var sharpness: Double = 2.0,
                textureFunction: TextureFunction = TextureFunction.TILING,
                var pre: String? = null,
                var post: String? = null) : TextureFromColorBuffer(texture, textureFunction) {

    init {
        texture.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR)
        texture.wrapU = WrapMode.REPEAT
        texture.wrapV = WrapMode.REPEAT
    }

    override fun hashCode(): Int {
        var result = scale.hashCode()
        result = 31 * result + offset.hashCode()
        result = 31 * result + sharpness.hashCode()
        result = 31 * result + (pre?.hashCode() ?: 0)
        result = 31 * result + (post?.hashCode() ?: 0)
        return result
    }


}

private fun ModelCoordinates.fs(index: Int) = """
|vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0); 
|{
|   vec2 x_texCoord = $input;
|   vec2 x_noTileOffset = vec2(0.0);
|   vec4 x_texture;
|   ${if (pre != null) "{ $pre } " else ""}
|   x_texture = ${textureFunction.function("p_texture$index", "x_texCoord")};
|   ${if (post != null) "{ $post } " else ""}
|   ${if (tangentInput != null) {
    """
|        vec3 normal = normalize(va_normal.xyz);
|        vec3 tangent = normalize(${tangentInput}.xyz);
|        vec3 bitangent = cross(normal, tangent) * ${tangentInput}.w;
|        mat3 tbn = mat3(tangent, bitangent, normal);
|        x_texture.rgb = tbn * normalize( (x_texture.rgb - vec3(0.5, 0.5, 0.0))*vec3(2.0, 2.0, 1.0)) ;
""".trimMargin()

} else ""}   
|   tex$index = x_texture;
|}
""".trimMargin()

private fun Triplanar.fs(index: Int, target: TextureTarget) = """
|vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0);
|{
|   vec3 x_normal = va_normal;
|   vec3 x_position = va_position;
|   float x_scale = p_textureTriplanarScale$index;
|   vec3 x_offset = p_textureTriplanarOffset$index;
|   vec2 x_noTileOffset = vec2(0.0);
|   ${if (pre != null) "{ $pre } " else ""}
|   vec3 n = normalize(x_normal);
|   vec3 an = abs(n);
|   vec2 uvY = x_position.xz * x_scale + x_offset.x;
|   vec2 uvX = x_position.zy * x_scale + x_offset.y;
|   vec2 uvZ = x_position.xy * x_scale + x_offset.z;
|   vec4 tY = ${textureFunction.function("p_texture$index", "uvY")};
|   vec4 tX = ${textureFunction.function("p_texture$index", "uvX")};
|   vec4 tZ = ${textureFunction.function("p_texture$index", "uvZ")};
|   vec3 weights = pow(an, vec3(p_textureTriplanarSharpness$index));
|   weights = weights / (weights.x + weights.y + weights.z);
|   tex$index = tX * weights.x + tY * weights.y + weights.z * tZ;
|   ${if (target == TextureTarget.NORMAL) """
    |   vec3 tnX = normalize( tX.xyz - vec3(0.5, 0.5, 0.0));
    |   vec3 tnY = normalize( tY.xyz - vec3(0.5, 0.5, 0.0)) * vec3(1.0, -1.0, 1.0);
    |   vec3 tnZ = normalize( tZ.xyz - vec3(0.5, 0.5, 0.0));
    |   vec3 nX = vec3(0.0, tnX.yx);
    |   vec3 nY = vec3(tnY.x, 0.0, tnY.y);
    |   vec3 nZ = vec3(tnZ.xy, 0.0);
    |   vec3 normal = normalize(nX * weights.x + nY * weights.y + nZ * weights.z + n);
    |   tex$index = vec4(normal, 0.0);
""".trimMargin() else ""}
|}
    ${if (post != null) """
        vec4 x_texture = tex$index;
        {
            $post
        }
        tex$index = x_texture;
    """.trimIndent() else ""}
""".trimMargin()

sealed class TextureTarget(val name: String) {
    object NONE : TextureTarget("NONE")
    object COLOR : TextureTarget("COLOR")
    object ROUGHNESS : TextureTarget("ROUGHNESS")
    object METALNESS : TextureTarget("METALNESS")
    object METALNESS_ROUGHNESS : TextureTarget("METALNESS_ROUGHNESS")
    object EMISSION : TextureTarget("EMISSION")
    object NORMAL : TextureTarget("NORMAL")
    object AMBIENT_OCCLUSION : TextureTarget("AMBIENT_OCCLUSION")
    class Height(var scale: Double = 1.0) : TextureTarget("Height")

    override fun toString(): String {
        return "TextureTarget(name: $name)"
    }

    override fun hashCode(): Int {
        return name.hashCode()
    }
}

class Texture(var source: TextureSource,
              var target: TextureTarget) {
    fun copy(): Texture {
        val copied = Texture(source, target)
        return copied
    }

    override fun toString(): String {
        return "Texture(source: $source, target: $target)"
    }

    override fun hashCode(): Int {
        var result = source.hashCode()
        result = 31 * result + target.hashCode()
        return result
    }
}

private var fragmentIDCounter = 1

data class SubsurfaceScatter(var enabled: Boolean) {
    var color: ColorRGBa = ColorRGBa.WHITE
    var shape = 1.0

    fun fs(): String {
        return if (enabled) """
            f_diffuse.rgb += pow(smoothstep(1.0, 0.0, abs(dot(normalize(N),normalize(V)))), p_sssShape) * clamp(evaluateSH(-V, sh), vec3(0.0), vec3(1.0)) * p_sssColor.rgb;            
        """ else ""
    }

    fun applyToShadeStyle(shadeStyle: ShadeStyle) {
        if (enabled) {
            shadeStyle.parameter("sssColor", color)
            shadeStyle.parameter("sssShape", shape)
        }
    }
}

data class CubemapReflection(var cubemap: Cubemap? = null) {
    var color: ColorRGBa = ColorRGBa.WHITE

    fun fs(): String {
        return if (cubemap != null) {
            """
            vec2 dfg = PrefilteredDFG_Karis(m_roughness, NoV);
            vec3 sc = m_metalness * m_color.rgb + (1.0-m_metalness) * vec3(0.04);
            f_specular.rgb += sc * (texture(p_radianceMap, reflect(-V, normalize(f_worldNormal)),  m_roughness*7.0 ).rgb * dfg.x + dfg.y) * p_radianceColor.rgb;
            """
        } else { "" }
    }
    fun applyToShadeStyle(shadeStyle: ShadeStyle) {
        if (cubemap != null) {
            shadeStyle.parameter("radianceMap", cubemap!!)
            shadeStyle.parameter("radianceColor", color)
        }
    }
}


class PBRMaterial : Material {
    override var name: String? = null
    override fun toString(): String {
        return "PBRMaterial(name: $name, fragmentID: $fragmentID, doubleSided: $doubleSided, textures: $textures, color: $color, metalness: $metalness, roughness: $roughness, emissive: $emission))"
    }

    override var fragmentID = fragmentIDCounter.apply {
        fragmentIDCounter++
    }

    override var doubleSided: Boolean = false
    override var transparent: Boolean = false
    var environmentMap = false
    var color = ColorRGBa.WHITE
    var metalness = 0.5
    var roughness = 1.0
    var emission = ColorRGBa.BLACK

    var subsurfaceScatter = SubsurfaceScatter(false)
    var cubemapReflection = CubemapReflection(null)


    var fragmentPreamble: String? = null
    var vertexPreamble: String? = null
    var vertexTransform: String? = null
    var parameters = mutableMapOf<String, Any>()
    var textures = mutableListOf<Texture>()

    val shadeStyles = mutableMapOf<ContextKey, ShadeStyle>()

    override fun generateShadeStyle(materialContext: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle {
        val cached = shadeStyles.getOrPut(ContextKey(materialContext, primitiveContext)) {
            val needLight = needLight(materialContext)
            val preambleFS = """
            vec4 m_color = p_color;
            uint f_fragmentID = uint(p_fragmentID);
            float m_f0 = 0.5;
            float m_roughness = p_roughness;
            float m_metalness = p_metalness;
            float m_ambientOcclusion = 1.0;
            vec3 m_emission = p_emission.rgb;
            vec3 m_normal = vec3(0.0, 0.0, 1.0);
            vec4 f_fog = vec4(0.0, 0.0, 0.0, 0.0);
            vec3 f_worldNormal = v_worldNormal;
            vec3 f_emission = m_emission;
        """.trimIndent()

            val textureFs = if (needLight) {
                (textures.mapIndexed { index, it ->
                    when (val source = it.source) {
                        DummySource -> "vec4 tex$index = vec4(1.0);"
                        is ModelCoordinates -> source.fs(index)
                        is Triplanar -> source.fs(index, it.target)
                        is TextureFromCode -> source.fs(index, it.target)
                        else -> TODO()
                    }
                } + textures.mapIndexed { index, texture ->
                    when (texture.target) {
                        TextureTarget.NONE -> ""
                        TextureTarget.COLOR -> "m_color.rgb *= pow(tex$index.rgb, vec3(2.2)); m_color.a *= tex$index.a;"
                        TextureTarget.METALNESS -> "m_metalness = tex$index.r;"
                        TextureTarget.ROUGHNESS -> "m_roughness = tex$index.r;"
                        TextureTarget.METALNESS_ROUGHNESS -> "m_metalness = tex$index.r; m_roughness = tex$index.g;"
                        TextureTarget.EMISSION -> "m_emission *= tex$index.rgb;"
                        TextureTarget.NORMAL -> "f_worldNormal = normalize((v_modelNormalMatrix * vec4(tex$index.xyz,0.0)).xyz);"
                        TextureTarget.AMBIENT_OCCLUSION -> "m_ambientOcclusion *= tex$index.r;"
                        is TextureTarget.Height -> ""
                    }
                }).joinToString("\n")
            } else ""
            val displacers = textures.filter { it.target is TextureTarget.Height }

            val skinVS = if (primitiveContext.hasSkinning) """
                    uvec4 j = a_joints;
                    mat4 skinTransform = p_jointTransforms[j.x] * a_weights.x 
                    + p_jointTransforms[j.y] * a_weights.y 
                    + p_jointTransforms[j.z] * a_weights.z 
                    + p_jointTransforms[j.w] * a_weights.w;                    
                    ${if (primitiveContext.hasNormalAttribute) """
                    x_normal = normalize(mat3(skinTransform) * x_normal);                        
                    """.trimIndent() else ""}
                    
                    x_position = (skinTransform * vec4(x_position,1)).xyz;
            """.trimIndent() else ""

            val textureVS = if (displacers.isNotEmpty()) textures.mapIndexed { index, it ->
                if (it.target is TextureTarget.Height) {
                    when (val source = it.source) {
                        DummySource -> "vec4 tex$index = vec4(1.0);"
                        is ModelCoordinates -> source.fs(index)
                        is Triplanar -> source.fs(index, it.target)
                        is TextureFromCode -> source.fs(index, it.target)
                        else -> TODO()
                    } + """
                x_position += x_normal * tex$index.r * p_textureHeightScale$index;
            """.trimIndent()
                } else ""
            }.joinToString("\n") else ""

            val lights = materialContext.lights

            val doubleSidedFS = if (doubleSided) {
                """
                    if (dot(V, N) <0) {
                        N *= -1.0;
                    }
                """.trimIndent()
            } else ""
            val lightFS = if (needLight) """
        vec3 f_diffuse = vec3(0.0);
        vec3 f_specular = vec3(0.0);
        vec3 f_ambient = vec3(0.0);
        float f_occlusion = 1.0;
        vec3 N = normalize(f_worldNormal);
        
        vec3 ep = (p_viewMatrixInverse * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
        vec3 Vr = ep - v_worldPosition;
        vec3 V = normalize(Vr);

        float NoV = ${if (primitiveContext.hasNormalAttribute) "abs(dot(N, V)) + 1e-5" else "1"};

        ${if (environmentMap && materialContext.meshCubemaps.isNotEmpty() && primitiveContext.hasNormalAttribute) """
           {
                vec2 dfg = PrefilteredDFG_Karis(m_roughness, NoV);
                vec3 sc = m_metalness * m_color.rgb + (1.0-m_metalness) * vec3(0.04);
                
                f_specular.rgb += sc * (texture(p_environmentMap, reflect(-V, normalize(f_worldNormal))).rgb * dfg.x + dfg.y) * m_ambientOcclusion;
            }
        """.trimIndent() else ""}

        ${lights.mapIndexed { index, (node, light) ->
                when (light) {
                    is AmbientLight -> light.fs(index)
                    is PointLight -> light.fs(index, primitiveContext.hasNormalAttribute)
                    is SpotLight -> light.fs(index, primitiveContext.hasNormalAttribute)
                    is DirectionalLight -> light.fs(index, primitiveContext.hasNormalAttribute)
                    is HemisphereLight -> light.fs(index, primitiveContext.hasNormalAttribute)
                    else -> TODO()
                }
            }.joinToString("\n")}


        ${if (materialContext.irradianceSH?.shMap != null) """
                vec3[9] sh;
                gatherSH(p_shMap, v_worldPosition, sh);
                vec3 irradiance = clamp(evaluateSH(normalize(N), sh), vec3(0.0), vec3(1.0)) * m_color.rgb;
                vec3 ks = F_SchlickRoughness(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), m_roughness+0.1, min(NoV, 1.0-1.0e-6));
                f_diffuse.rgb = irradiance * ks;
                f_ambient.rgb = (1.0-ks) * irradiance;
                ${subsurfaceScatter.fs()}
                ${cubemapReflection.fs()}
        """.trimIndent() else ""
            }
        
        ${materialContext.fogs.mapIndexed { index, (node, fog) ->
                
                fog.fs(index)
            }.joinToString("\n")}

    """.trimIndent() else ""
            val rt = RenderTarget.active

            val combinerFS = materialContext.pass.combiners.map {
                it.generateShader()
            }.joinToString("\n")

            val fs = preambleFS + textureFs + lightFS + combinerFS
            val vs = (this@PBRMaterial.vertexTransform ?: "") + textureVS + skinVS

            shadeStyle {
                fragmentPreamble = this@PBRMaterial.fragmentPreamble ?: ""
                vertexPreamble = """
                    $shaderNoRepetitionVert
                     ${(this@PBRMaterial.vertexPreamble) ?: ""}
                """.trimIndent()
                fragmentPreamble += """
                    ${if (materialContext.irradianceSH?.shMap != null) {
                    """
                $glslEvaluateSH
                $glslFetchSH
                ${genGlslGatherSH(materialContext.irradianceSH!!.xCount, materialContext.irradianceSH!!.yCount,
                            materialContext.irradianceSH!!.zCount, materialContext.irradianceSH!!.spacing, materialContext.irradianceSH!!.offset)}
                """
                } else {
                    ""
                }
                }
            |$shaderLinePlaneIntersect
            |$shaderProjectOnPlane
            |$shaderSideOfPlane
            |$shaderGGX
            |$shaderVSM
            |$shaderNoRepetition
            |$phraseTbnMatrix
            """.trimMargin()
                this.suppressDefaultOutput = true
                this.vertexTransform = vs
                fragmentTransform = fs

                materialContext.pass.combiners.map {
                    if (rt is ProgramRenderTarget || materialContext.pass === DefaultPass || materialContext.pass === DefaultOpaquePass || materialContext.pass == DefaultTransparentPass || materialContext.pass == IrradianceProbePass || materialContext.pass.skipTarget )  {
                        this.output(it.targetOutput, ShadeStyleOutput(0))
                    } else {
                        val index = rt.colorAttachmentIndexByName(it.targetOutput)?:error("attachment ${it.targetOutput} not found")
                        val type = rt.colorBuffer(index).type
                        val format = rt.colorBuffer(index).format
                        this.output(it.targetOutput, ShadeStyleOutput(index, format, type))
                    }
                }
            }
        }
        return cached
    }

    private fun needLight(context: MaterialContext): Boolean {
        val needSpecular = context.pass.combiners.any { FacetType.SPECULAR in it.facets }
        val needDiffuse = context.pass.combiners.any { FacetType.DIFFUSE in it.facets }
        val needLight = needSpecular || needDiffuse
        return needLight
    }

    override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) {
        shadeStyle.parameter("emission", emission)
        shadeStyle.parameter("color", color)
        shadeStyle.parameter("metalness", metalness)
        shadeStyle.parameter("roughness", roughness)
        shadeStyle.parameter("fragmentID", fragmentID)

        if (context.irradianceProbeCount > 0) {
            shadeStyle.parameter("shMap", context.irradianceSH?.shMap!!)
        }

        parameters.forEach { (k, v) ->
            when (v) {
                is Double -> shadeStyle.parameter(k, v)
                is Int -> shadeStyle.parameter(k, v)
                is Vector2 -> shadeStyle.parameter(k, v)
                is Vector3 -> shadeStyle.parameter(k, v)
                is Vector4 -> shadeStyle.parameter(k, v)
                is BufferTexture -> shadeStyle.parameter(k, v)
                is ColorBuffer -> shadeStyle.parameter(k, v)
                else -> TODO("support ${v::class.java}")
            }
        }
        if (needLight(context)) {

            subsurfaceScatter.applyToShadeStyle(shadeStyle)
            cubemapReflection.applyToShadeStyle(shadeStyle)

            textures.forEachIndexed { index, texture ->
                when (val source = texture.source) {
                    is TextureFromColorBuffer -> {
                        shadeStyle.parameter("texture$index", source.texture)
                        if (source.textureFunction == TextureFunction.NOT_TILING) {
                            shadeStyle.parameter("textureNoise", noise128)
                        }
                    }
                    else -> {}
                }
                when (val source = texture.source) {
                    is Triplanar -> {
                        shadeStyle.parameter("textureTriplanarSharpness$index", source.sharpness)
                        shadeStyle.parameter("textureTriplanarScale$index", source.scale)
                        shadeStyle.parameter("textureTriplanarOffset$index", source.offset)
                    }
                    else -> {}
                }
                if (texture.target is TextureTarget.Height) {
                    val target = texture.target as TextureTarget.Height
                    shadeStyle.parameter("textureHeightScale$index", target.scale)
                }

            }

            val lights = context.lights
            lights.forEachIndexed { index, (node, light) ->
                shadeStyle.parameter("lightColor$index", light.color)
                when (light) {
                    is AmbientLight -> {
                    }
                    is PointLight -> {
                        shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz)
                        shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation)
                        shadeStyle.parameter("lightLinearAttenuation$index", light.linearAttenuation)
                        shadeStyle.parameter("lightQuadraticAttenuation$index", light.quadraticAttenuation)
                    }

                    is SpotLight -> {
                        shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz)
                        shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz)
                        shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation)
                        shadeStyle.parameter("lightLinearAttenuation$index", light.linearAttenuation)
                        shadeStyle.parameter("lightQuadraticAttenuation$index", light.quadraticAttenuation)
                        shadeStyle.parameter("lightInnerCos$index", cos(Math.toRadians(light.innerAngle)))
                        shadeStyle.parameter("lightOuterCos$index", cos(Math.toRadians(light.outerAngle)))

                        if (light.shadows is Shadows.MappedShadows) {
                            context.shadowMaps[light]?.let {
                                val look = light.view(node)
                                shadeStyle.parameter("lightTransform$index",
                                        light.projection(it) * look)

                                if (light.shadows is Shadows.DepthMappedShadows) {
                                    shadeStyle.parameter("lightShadowMap$index", it.depthBuffer ?: TODO())
                                }

                                if (light.shadows is Shadows.ColorMappedShadows) {
                                    shadeStyle.parameter("lightShadowMap$index", it.colorBuffer(0))
                                }
                            }
                        }
                    }
                    is DirectionalLight -> {
                        shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz)
                        shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz)
                        if (light.shadows is Shadows.MappedShadows) {
                            context.shadowMaps[light]?.let {
                                val look = light.view(node)
                                shadeStyle.parameter("lightTransform$index",
                                        light.projection(it) * look)

                                if (light.shadows is Shadows.DepthMappedShadows) {
                                    shadeStyle.parameter("lightShadowMap$index", it.depthBuffer ?: TODO())
                                }

                                if (light.shadows is Shadows.ColorMappedShadows) {
                                    shadeStyle.parameter("lightShadowMap$index", it.colorBuffer(0))
                                }
                            }
                        }
                    }

                    is HemisphereLight -> {
                        shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz)
                        shadeStyle.parameter("lightUpColor$index", light.upColor)
                        shadeStyle.parameter("lightDownColor$index", light.downColor)

                        light.irradianceMap?.let {
                            shadeStyle.parameter("lightIrradianceMap$index", it)
                        }
                    }
                }
            }
            context.fogs.forEachIndexed { index, (node, fog) ->
                shadeStyle.parameter("fogColor$index", fog.color)
                shadeStyle.parameter("fogEnd$index", fog.end)
            }
        } else {
            textures.forEachIndexed { index, texture ->
                if (texture.target is TextureTarget.Height) {
                    when (val source = texture.source) {
                        is TextureFromColorBuffer -> shadeStyle.parameter("texture$index", source.texture)
                        else -> {}
                    }
                    when (val source = texture.source) {
                        is Triplanar -> {
                            shadeStyle.parameter("textureTriplanarSharpness$index", source.sharpness)
                            shadeStyle.parameter("textureTriplanarScale$index", source.scale)
                            shadeStyle.parameter("textureTriplanarOffset$index", source.offset)
                        }
                        else -> {}
                    }
                    val target = texture.target as TextureTarget.Height
                    shadeStyle.parameter("textureHeightScale$index", target.scale)
                }
            }
        }
    }

    override fun hashCode(): Int {
        var result = fragmentID.hashCode()
        result = 31 * doubleSided.hashCode()
        result = 31 * result + transparent.hashCode()
//        result = 31 * result + environmentMap.hashCode()
        result = 31 * result + color.hashCode()
        result = 31 * result + metalness.hashCode()
        result = 31 * result + roughness.hashCode()
        result = 31 * result + emission.hashCode()
        result = 31 * result + (fragmentPreamble?.hashCode() ?: 0)
        result = 31 * result + (vertexPreamble?.hashCode() ?: 0)
        result = 31 * result + (vertexTransform?.hashCode() ?: 0)
//        result = 31 * result + parameters.hashCode()
//        result = 31 * result + textures.hashCode()
//        result = 31 * result + shadeStyles.hashCode()
        return result
    }
}

