uniform sampler2D HeightTex;
uniform sampler2D PropertyTex1;
uniform sampler2D PropertyTex2;
uniform sampler2D PropertyTex3;

uniform vec3 cShaderTex1OffsetScaleAndCutoff;
uniform vec2 cShaderTex2OffsetScale;
uniform vec4 uGlobalColor;
uniform vec4 PropertyTexNoValueColor;
uniform vec2 cHeightNormalizationOffsetScale;
uniform vec2 cVertexScaleOffset;
uniform vec2 cPrimitiveScaleOffset;
uniform bool bNoShading, bFlatShading;

uniform bool isCullQuads;
uniform int nLODStep;

uniform vec4 uGlobalLineColor;
uniform float rLineWidth;

in vec3 vPosition;
in vec3 vObjPos;

in vec2 vHeightTexCoord, vValueTexCoord;
in vec2 vTexCoordFull;
in vec2 vCullQuads;
in vec2 vLimit;

vec2 SampleGradient(ivec2 pos)
{
  float val = texelFetch(HeightTex, pos, 0).r;
  if (!IsHeightValid(val))
  {
    return vec2(0.0);
  }
  val = (val + cHeightNormalizationOffsetScale.x) * cHeightNormalizationOffsetScale.y;

  vec4 vals = vec4(
    texelFetch(HeightTex, pos - ivec2(1, 0), 0).r,
    texelFetch(HeightTex, pos + ivec2(1, 0), 0).r,
    texelFetch(HeightTex, pos - ivec2(0, 1), 0).r,
    texelFetch(HeightTex, pos + ivec2(0, 1), 0).r);

  bvec4 valid = IsHeightValid(vals);
  if (!any(valid))
  {
    return vec2(0.0);
  }

  vals = (vals + cHeightNormalizationOffsetScale.x) * cHeightNormalizationOffsetScale.y;
  return vec2(vals.x - val, vals.z - val) * vec2(valid.xz) +
         vec2(val - vals.y, val - vals.w) * vec2(valid.yw);
}

vec3 GetNormal(vec2 pos)
{
  ivec2 uv0 = ivec2(pos - 0.5);
  vec2 grad0 = SampleGradient(uv0 + ivec2(0, 0));
  vec2 grad1 = SampleGradient(uv0 + ivec2(1, 0));
  vec2 grad2 = SampleGradient(uv0 + ivec2(0, 1));
  vec2 grad3 = SampleGradient(uv0 + ivec2(1, 1));
  vec2 t = fract(pos - 0.5);
  vec2 grad = mix(mix(grad0, grad1, t.x), mix(grad2, grad3, t.x), t.y);
  return vec3(grad, 2.0);
}

float getLineCov(vec2 fw, vec2 hit)
{
  vec2 checkPos = abs(fract(hit) - 0.5);
  vec2 hw = fw * rLineWidth * 0.5;

  if (all(greaterThan(checkPos, hw + fw)))
  {
    return 0.0;
  }

  vec2 p = smoothstep(hw + fw, hw - fw, checkPos);
  return max(p.x, p.y);
}

vec4 PixelOver(vec4 a, vec4 b)
{
  // pre-multiply
  a.rgb *= a.a;
  b.rgb *= b.a;

  vec4 c = a + b * (1.0 - a.a);

  // unpre-multiply
  if (abs(c.a) > 0.001)
  {
    c.rgb /= c.a;
  }
  else
  {
    c = vec4(0.0);
  }

  return c;
}

vec4 GetShaderColor();
vec4 GetLineShaderColor();
vec4 LightFragment(vec3 v, vec3 N, vec4 cShaderColor, vec3 sceneColor, vec3 materialAmbient, vec3 materialDiffuse, vec3 materialSpecular, float materialShininess);

float SampleValue(sampler2D s, vec2 cPos, float rCutoff)
{
  cPos = cPos * textureSize(s, 0) - 0.5;
  ivec2 uv = ivec2(cPos);
  vec2 t = fract(cPos);

  vec4 cVals = vec4(texelFetch(s, uv + ivec2(0, 0), 0).r,
                    texelFetch(s, uv + ivec2(1, 0), 0).r,
                    texelFetch(s, uv + ivec2(0, 1), 0).r,
                    texelFetch(s, uv + ivec2(1, 1), 0).r);

  vec4 bc = t.x < 1.0 - t.y ? vec4( 1.0 - t.x - t.y, t.x, t.y, 0.0) :
                              vec4( 0.0, 1.0 - t.y, 1.0 - t.x, t.x + t.y - 1.0);

  if (isCullQuads)
  {
    bc = t.x > t.y ? vec4(1.0 - t.x, t.x - t.y, 0.0, t.y) :
                     vec4(1.0 - t.y, 0.0, t.y - t.x, t.x);
  }

  return dot(cVals, bc) * cVertexScaleOffset.x + cVertexScaleOffset.y;
}

uniform sampler3D tileCache, pageTable;
uniform vec3 virtualTextureSize, tileCacheVoxelSize;
uniform vec4 pageTableRoot, pageTableRootExtent;

vec4 SampleTileCache(vec3 t)
{
  return texture(tileCache, t);
}

vec4 LookupPageTable(vec3 cUVW)
{
  vec4 v = pageTableRoot;
  vec3 textureToPageTableScale = virtualTextureSize / pageTableRootExtent.xyz;
  vec3 pos = cUVW * textureToPageTableScale;
  for (int level = 0; level < 24 && v.w < 0.0; ++level)
  {
    v = texelFetch(pageTable, ivec3(v.xyz) - ivec3(pos * v.w), 0);
  }
  return vec4(v.xyz + cUVW * v.w * virtualTextureSize * tileCacheVoxelSize, v.w);
}

void main()
{
  // derivatives needs to be calculated before discarding
  vec3 fn = normalize(cross(dFdx(vObjPos), dFdy(vObjPos)));
  vec2 fw = fwidth(vHeightTexCoord);

  DepthPeel();
  if (isCullQuads)
  {
    ivec2 pos1 = ivec2(vHeightTexCoord  - fract(vCullQuads) * nLODStep);
    ivec2 pos2 = ivec2(min(pos1 + nLODStep, vLimit));

    vec4 vals = vec4(
      texelFetch(HeightTex, ivec2(pos1.x, pos1.y), 0).r,
      texelFetch(HeightTex, ivec2(pos2.x, pos1.y), 0).r,
      texelFetch(HeightTex, ivec2(pos1.x, pos2.y), 0).r,
      texelFetch(HeightTex, ivec2(pos2.x, pos2.y), 0).r);

    if (all(IsHeightValid(vals)))
    {
      discard;
    }
  }
  vec4 cShaderColor = GetShaderColor();

  if (!bNoShading && cShaderColor.a > 0.0)
  {
    vec3 N;
    if (bFlatShading)
    {
      N = fn;
    }
    else
    {
      N = normalize(gl_NormalMatrix * GetNormal(vHeightTexCoord));
      if (!gl_FrontFacing)
      {
        N = -N;
      }
    }
    vec3 v = vPosition;
    gl_FragColor = LightFragment(v, N, cShaderColor, gl_FrontLightModelProduct.sceneColor.rgb, gl_FrontMaterial.ambient.rgb, gl_FrontMaterial.diffuse.rgb, gl_FrontMaterial.specular.rgb, gl_FrontMaterial.shininess);
  }
  else
  {
    gl_FragColor = cShaderColor;
  }

  if (rLineWidth > 0.0)
  {
    float rCov = getLineCov(fw, vHeightTexCoord);
    vec4 vLineColor = GetLineShaderColor();
    gl_FragColor = PixelOver(vec4(vLineColor.rgb, vLineColor.a * rCov), gl_FragColor);
  }
}
