import defined from "../Core/defined.js";
import ShaderSource from "../Renderer/ShaderSource.js";

/**
 * @private
 */
function ShadowMapShader() {}

ShadowMapShader.getShadowCastShaderKeyword = function (
  isPointLight,
  isTerrain,
  usesDepthTexture,
  isOpaque
) {
  return (
    "castShadow " +
    isPointLight +
    " " +
    isTerrain +
    " " +
    usesDepthTexture +
    " " +
    isOpaque
  );
};

ShadowMapShader.createShadowCastVertexShader = function (
  vs,
  isPointLight,
  isTerrain
) {
  var defines = vs.defines.slice(0);
  var sources = vs.sources.slice(0);

  defines.push("SHADOW_MAP");

  if (isTerrain) {
    defines.push("GENERATE_POSITION");
  }

  var positionVaryingName = ShaderSource.findPositionVarying(vs);
  var hasPositionVarying = defined(positionVaryingName);

  if (isPointLight && !hasPositionVarying) {
    var length = sources.length;
    for (var j = 0; j < length; ++j) {
      sources[j] = ShaderSource.replaceMain(sources[j], "czm_shadow_cast_main");
    }

    var shadowVS =
      "varying vec3 v_positionEC; \n" +
      "void main() \n" +
      "{ \n" +
      "    czm_shadow_cast_main(); \n" +
      "    v_positionEC = (czm_inverseProjection * gl_Position).xyz; \n" +
      "}";
    sources.push(shadowVS);
  }

  return new ShaderSource({
    defines: defines,
    sources: sources,
  });
};

ShadowMapShader.createShadowCastFragmentShader = function (
  fs,
  isPointLight,
  usesDepthTexture,
  opaque
) {
  var defines = fs.defines.slice(0);
  var sources = fs.sources.slice(0);

  var positionVaryingName = ShaderSource.findPositionVarying(fs);
  var hasPositionVarying = defined(positionVaryingName);
  if (!hasPositionVarying) {
    positionVaryingName = "v_positionEC";
  }

  var length = sources.length;
  for (var i = 0; i < length; ++i) {
    sources[i] = ShaderSource.replaceMain(sources[i], "czm_shadow_cast_main");
  }

  var fsSource = "";

  if (isPointLight) {
    if (!hasPositionVarying) {
      fsSource += "varying vec3 v_positionEC; \n";
    }
    fsSource += "uniform vec4 shadowMap_lightPositionEC; \n";
  }

  if (opaque) {
    fsSource += "void main() \n" + "{ \n";
  } else {
    fsSource +=
      "void main() \n" +
      "{ \n" +
      "    czm_shadow_cast_main(); \n" +
      "    if (gl_FragColor.a == 0.0) \n" +
      "    { \n" +
      "       discard; \n" +
      "    } \n";
  }

  if (isPointLight) {
    fsSource +=
      "    float distance = length(" +
      positionVaryingName +
      "); \n" +
      "    if (distance >= shadowMap_lightPositionEC.w) \n" +
      "    { \n" +
      "        discard; \n" +
      "    } \n" +
      "    distance /= shadowMap_lightPositionEC.w; // radius \n" +
      "    gl_FragColor = czm_packDepth(distance); \n";
  } else if (usesDepthTexture) {
    fsSource += "    gl_FragColor = vec4(1.0); \n";
  } else {
    fsSource += "    gl_FragColor = czm_packDepth(gl_FragCoord.z); \n";
  }

  fsSource += "} \n";

  sources.push(fsSource);

  return new ShaderSource({
    defines: defines,
    sources: sources,
  });
};

ShadowMapShader.getShadowReceiveShaderKeyword = function (
  shadowMap,
  castShadows,
  isTerrain,
  hasTerrainNormal
) {
  var usesDepthTexture = shadowMap._usesDepthTexture;
  var polygonOffsetSupported = shadowMap._polygonOffsetSupported;
  var isPointLight = shadowMap._isPointLight;
  var isSpotLight = shadowMap._isSpotLight;
  var hasCascades = shadowMap._numberOfCascades > 1;
  var debugCascadeColors = shadowMap.debugCascadeColors;
  var softShadows = shadowMap.softShadows;

  return (
    "receiveShadow " +
    usesDepthTexture +
    polygonOffsetSupported +
    isPointLight +
    isSpotLight +
    hasCascades +
    debugCascadeColors +
    softShadows +
    castShadows +
    isTerrain +
    hasTerrainNormal
  );
};

ShadowMapShader.createShadowReceiveVertexShader = function (
  vs,
  isTerrain,
  hasTerrainNormal
) {
  var defines = vs.defines.slice(0);
  var sources = vs.sources.slice(0);

  defines.push("SHADOW_MAP");

  if (isTerrain) {
    if (hasTerrainNormal) {
      defines.push("GENERATE_POSITION_AND_NORMAL");
    } else {
      defines.push("GENERATE_POSITION");
    }
  }

  return new ShaderSource({
    defines: defines,
    sources: sources,
  });
};

ShadowMapShader.createShadowReceiveFragmentShader = function (
  fs,
  shadowMap,
  castShadows,
  isTerrain,
  hasTerrainNormal
) {
  var normalVaryingName = ShaderSource.findNormalVarying(fs);
  var hasNormalVarying =
    (!isTerrain && defined(normalVaryingName)) ||
    (isTerrain && hasTerrainNormal);

  var positionVaryingName = ShaderSource.findPositionVarying(fs);
  var hasPositionVarying = defined(positionVaryingName);

  var usesDepthTexture = shadowMap._usesDepthTexture;
  var polygonOffsetSupported = shadowMap._polygonOffsetSupported;
  var isPointLight = shadowMap._isPointLight;
  var isSpotLight = shadowMap._isSpotLight;
  var hasCascades = shadowMap._numberOfCascades > 1;
  var debugCascadeColors = shadowMap.debugCascadeColors;
  var softShadows = shadowMap.softShadows;
  var bias = isPointLight
    ? shadowMap._pointBias
    : isTerrain
    ? shadowMap._terrainBias
    : shadowMap._primitiveBias;

  var defines = fs.defines.slice(0);
  var sources = fs.sources.slice(0);

  var length = sources.length;
  for (var i = 0; i < length; ++i) {
    sources[i] = ShaderSource.replaceMain(
      sources[i],
      "czm_shadow_receive_main"
    );
  }

  if (isPointLight) {
    defines.push("USE_CUBE_MAP_SHADOW");
  } else if (usesDepthTexture) {
    defines.push("USE_SHADOW_DEPTH_TEXTURE");
  }

  if (softShadows && !isPointLight) {
    defines.push("USE_SOFT_SHADOWS");
  }

  // Enable day-night shading so that the globe is dark when the light is below the horizon
  if (hasCascades && castShadows && isTerrain) {
    if (hasNormalVarying) {
      defines.push("ENABLE_VERTEX_LIGHTING");
    } else {
      defines.push("ENABLE_DAYNIGHT_SHADING");
    }
  }

  if (castShadows && bias.normalShading && hasNormalVarying) {
    defines.push("USE_NORMAL_SHADING");
    if (bias.normalShadingSmooth > 0.0) {
      defines.push("USE_NORMAL_SHADING_SMOOTH");
    }
  }

  var fsSource = "";

  if (isPointLight) {
    fsSource += "uniform samplerCube shadowMap_textureCube; \n";
  } else {
    fsSource += "uniform sampler2D shadowMap_texture; \n";
  }

  var returnPositionEC;
  if (hasPositionVarying) {
    returnPositionEC = "    return vec4(" + positionVaryingName + ", 1.0); \n";
  } else {
    returnPositionEC =
      "#ifndef LOG_DEPTH \n" +
      "    return czm_windowToEyeCoordinates(gl_FragCoord); \n" +
      "#else \n" +
      "    return vec4(v_logPositionEC, 1.0); \n" +
      "#endif \n";
  }

  fsSource +=
    "uniform mat4 shadowMap_matrix; \n" +
    "uniform vec3 shadowMap_lightDirectionEC; \n" +
    "uniform vec4 shadowMap_lightPositionEC; \n" +
    "uniform vec4 shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness; \n" +
    "uniform vec4 shadowMap_texelSizeDepthBiasAndNormalShadingSmooth; \n" +
    "#ifdef LOG_DEPTH \n" +
    "varying vec3 v_logPositionEC; \n" +
    "#endif \n" +
    "vec4 getPositionEC() \n" +
    "{ \n" +
    returnPositionEC +
    "} \n" +
    "vec3 getNormalEC() \n" +
    "{ \n" +
    (hasNormalVarying
      ? "    return normalize(" + normalVaryingName + "); \n"
      : "    return vec3(1.0); \n") +
    "} \n" +
    // Offset the shadow position in the direction of the normal for perpendicular and back faces
    "void applyNormalOffset(inout vec4 positionEC, vec3 normalEC, float nDotL) \n" +
    "{ \n" +
    (bias.normalOffset && hasNormalVarying
      ? "    float normalOffset = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.x; \n" +
        "    float normalOffsetScale = 1.0 - nDotL; \n" +
        "    vec3 offset = normalOffset * normalOffsetScale * normalEC; \n" +
        "    positionEC.xyz += offset; \n"
      : "") +
    "} \n";

  fsSource +=
    "void main() \n" +
    "{ \n" +
    "    czm_shadow_receive_main(); \n" +
    "    vec4 positionEC = getPositionEC(); \n" +
    "    vec3 normalEC = getNormalEC(); \n" +
    "    float depth = -positionEC.z; \n";

  fsSource +=
    "    czm_shadowParameters shadowParameters; \n" +
    "    shadowParameters.texelStepSize = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.xy; \n" +
    "    shadowParameters.depthBias = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.z; \n" +
    "    shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.w; \n" +
    "    shadowParameters.darkness = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.w; \n";

  if (isTerrain) {
    // Scale depth bias based on view distance to reduce z-fighting in distant terrain
    fsSource += "    shadowParameters.depthBias *= max(depth * 0.01, 1.0); \n";
  } else if (!polygonOffsetSupported) {
    // If polygon offset isn't supported push the depth back based on view, however this
    // causes light leaking at further away views
    fsSource +=
      "    shadowParameters.depthBias *= mix(1.0, 100.0, depth * 0.0015); \n";
  }

  if (isPointLight) {
    fsSource +=
      "    vec3 directionEC = positionEC.xyz - shadowMap_lightPositionEC.xyz; \n" +
      "    float distance = length(directionEC); \n" +
      "    directionEC = normalize(directionEC); \n" +
      "    float radius = shadowMap_lightPositionEC.w; \n" +
      "    // Stop early if the fragment is beyond the point light radius \n" +
      "    if (distance > radius) \n" +
      "    { \n" +
      "        return; \n" +
      "    } \n" +
      "    vec3 directionWC  = czm_inverseViewRotation * directionEC; \n" +
      "    shadowParameters.depth = distance / radius; \n" +
      "    shadowParameters.nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); \n" +
      "    shadowParameters.texCoords = directionWC; \n" +
      "    float visibility = czm_shadowVisibility(shadowMap_textureCube, shadowParameters); \n";
  } else if (isSpotLight) {
    fsSource +=
      "    vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz); \n" +
      "    float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); \n" +
      "    applyNormalOffset(positionEC, normalEC, nDotL); \n" +
      "    vec4 shadowPosition = shadowMap_matrix * positionEC; \n" +
      "    // Spot light uses a perspective projection, so perform the perspective divide \n" +
      "    shadowPosition /= shadowPosition.w; \n" +
      "    // Stop early if the fragment is not in the shadow bounds \n" +
      "    if (any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) \n" +
      "    { \n" +
      "        return; \n" +
      "    } \n" +
      "    shadowParameters.texCoords = shadowPosition.xy; \n" +
      "    shadowParameters.depth = shadowPosition.z; \n" +
      "    shadowParameters.nDotL = nDotL; \n" +
      "    float visibility = czm_shadowVisibility(shadowMap_texture, shadowParameters); \n";
  } else if (hasCascades) {
    fsSource +=
      "    float maxDepth = shadowMap_cascadeSplits[1].w; \n" +
      "    // Stop early if the eye depth exceeds the last cascade \n" +
      "    if (depth > maxDepth) \n" +
      "    { \n" +
      "        return; \n" +
      "    } \n" +
      "    // Get the cascade based on the eye-space depth \n" +
      "    vec4 weights = czm_cascadeWeights(depth); \n" +
      "    // Apply normal offset \n" +
      "    float nDotL = clamp(dot(normalEC, shadowMap_lightDirectionEC), 0.0, 1.0); \n" +
      "    applyNormalOffset(positionEC, normalEC, nDotL); \n" +
      "    // Transform position into the cascade \n" +
      "    vec4 shadowPosition = czm_cascadeMatrix(weights) * positionEC; \n" +
      "    // Get visibility \n" +
      "    shadowParameters.texCoords = shadowPosition.xy; \n" +
      "    shadowParameters.depth = shadowPosition.z; \n" +
      "    shadowParameters.nDotL = nDotL; \n" +
      "    float visibility = czm_shadowVisibility(shadowMap_texture, shadowParameters); \n" +
      "    // Fade out shadows that are far away \n" +
      "    float shadowMapMaximumDistance = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.z; \n" +
      "    float fade = max((depth - shadowMapMaximumDistance * 0.8) / (shadowMapMaximumDistance * 0.2), 0.0); \n" +
      "    visibility = mix(visibility, 1.0, fade); \n" +
      (debugCascadeColors
        ? "    // Draw cascade colors for debugging \n" +
          "    gl_FragColor *= czm_cascadeColor(weights); \n"
        : "");
  } else {
    fsSource +=
      "    float nDotL = clamp(dot(normalEC, shadowMap_lightDirectionEC), 0.0, 1.0); \n" +
      "    applyNormalOffset(positionEC, normalEC, nDotL); \n" +
      "    vec4 shadowPosition = shadowMap_matrix * positionEC; \n" +
      "    // Stop early if the fragment is not in the shadow bounds \n" +
      "    if (any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) \n" +
      "    { \n" +
      "        return; \n" +
      "    } \n" +
      "    shadowParameters.texCoords = shadowPosition.xy; \n" +
      "    shadowParameters.depth = shadowPosition.z; \n" +
      "    shadowParameters.nDotL = nDotL; \n" +
      "    float visibility = czm_shadowVisibility(shadowMap_texture, shadowParameters); \n";
  }

  fsSource += "    gl_FragColor.rgb *= visibility; \n" + "} \n";

  sources.push(fsSource);

  return new ShaderSource({
    defines: defines,
    sources: sources,
  });
};
export default ShadowMapShader;
