import Cartesian3 from "../Core/Cartesian3.js";
import combine from "../Core/combine.js";
import defined from "../Core/defined.js";
import ShaderBuilder from "../Renderer/ShaderBuilder.js";
import ShaderDestination from "../Renderer/ShaderDestination.js";
import VoxelUtils from "../Shaders/Voxels/VoxelUtils.js";
import VoxelFS from "../Shaders/Voxels/VoxelFS.js";
import VoxelVS from "../Shaders/Voxels/VoxelVS.js";
import IntersectionUtils from "../Shaders/Voxels/IntersectionUtils.js";
import IntersectDepth from "../Shaders/Voxels/IntersectDepth.js";
import IntersectPlane from "../Shaders/Voxels/IntersectPlane.js";
import IntersectLongitude from "../Shaders/Voxels/IntersectLongitude.js";
import IntersectBox from "../Shaders/Voxels/IntersectBox.js";
import IntersectCylinder from "../Shaders/Voxels/IntersectCylinder.js";
import IntersectEllipsoid from "../Shaders/Voxels/IntersectEllipsoid.js";
import Intersection from "../Shaders/Voxels/Intersection.js";
import convertLocalToBoxUv from "../Shaders/Voxels/convertLocalToBoxUv.js";
import convertLocalToCylinderUv from "../Shaders/Voxels/convertLocalToCylinderUv.js";
import convertLocalToEllipsoidUv from "../Shaders/Voxels/convertLocalToEllipsoidUv.js";
import Octree from "../Shaders/Voxels/Octree.js";
import Megatexture from "../Shaders/Voxels/Megatexture.js";
import VoxelMetadataOrder from "./VoxelMetadataOrder.js";

/**
 * Set up render resources, including basic shader code, for rendering
 * a Voxel primitive.
 * The shader code generated by this function may be modified in later stages.
 * @constructor
 * @param {VoxelPrimitive} primitive
 *
 * @private
 */
function VoxelRenderResources(primitive) {
  const shaderBuilder = new ShaderBuilder();
  /**
   * An object used to build a shader incrementally. Each pipeline stage
   * may add lines of shader code to this object.
   *
   * @type {ShaderBuilder}
   * @readonly
   *
   * @private
   */
  this.shaderBuilder = shaderBuilder;

  // Custom shader uniforms
  const customShader = primitive._customShader;
  const uniformMap = combine(primitive._uniformMap, customShader.uniformMap);
  primitive._uniformMap = uniformMap;

  const customShaderUniforms = customShader.uniforms;
  for (const uniformName in customShaderUniforms) {
    if (customShaderUniforms.hasOwnProperty(uniformName)) {
      const uniform = customShaderUniforms[uniformName];
      shaderBuilder.addUniform(
        uniform.type,
        uniformName,
        ShaderDestination.FRAGMENT,
      );
    }
  }
  // The reason this uniform is added by shader builder is because some of the
  // dynamically generated shader code reads from it.
  shaderBuilder.addUniform(
    "sampler3D",
    "u_megatextureTextures[METADATA_COUNT]",
    ShaderDestination.FRAGMENT,
  );

  /**
   * A dictionary mapping uniform name to functions that return the uniform
   * values.
   * @private
   * @type {Object<string, Function>}
   */
  this.uniformMap = uniformMap;

  const clippingPlanes = primitive._clippingPlanes;
  const clippingPlanesLength =
    defined(clippingPlanes) && clippingPlanes.enabled
      ? clippingPlanes.length
      : 0;

  this.clippingPlanes = clippingPlanes;
  this.clippingPlanesLength = clippingPlanesLength;

  const renderBoundPlanes = primitive._shape.renderBoundPlanes;
  const renderBoundPlanesLength = renderBoundPlanes?.length ?? 0;

  this.renderBoundPlanes = renderBoundPlanes;
  this.renderBoundPlanesLength = renderBoundPlanesLength;

  // Build shader
  shaderBuilder.addVertexLines([VoxelVS]);

  if (primitive.provider.metadataOrder === VoxelMetadataOrder.Y_UP) {
    shaderBuilder.addDefine(
      "Y_UP_METADATA_ORDER",
      undefined,
      ShaderDestination.FRAGMENT,
    );
  }
  const shapeType = primitive._provider.shape;
  if (shapeType === "BOX") {
    shaderBuilder.addDefine("SHAPE_BOX", undefined, ShaderDestination.FRAGMENT);
  } else if (shapeType === "CYLINDER") {
    shaderBuilder.addDefine(
      "SHAPE_CYLINDER",
      undefined,
      ShaderDestination.FRAGMENT,
    );
  } else if (shapeType === "ELLIPSOID") {
    shaderBuilder.addDefine(
      "SHAPE_ELLIPSOID",
      undefined,
      ShaderDestination.FRAGMENT,
    );
  }

  shaderBuilder.addFragmentLines([
    customShader.fragmentShaderText,
    "#line 0",
    Octree,
    VoxelUtils,
    Megatexture,
    IntersectionUtils,
    IntersectPlane,
    IntersectDepth,
  ]);

  if (clippingPlanesLength > 0) {
    shaderBuilder.addDefine(
      "CLIPPING_PLANES",
      undefined,
      ShaderDestination.FRAGMENT,
    );
    shaderBuilder.addDefine(
      "CLIPPING_PLANES_COUNT",
      clippingPlanesLength,
      ShaderDestination.FRAGMENT,
    );
    if (clippingPlanes.unionClippingRegions) {
      shaderBuilder.addDefine(
        "CLIPPING_PLANES_UNION",
        undefined,
        ShaderDestination.FRAGMENT,
      );
    }
  }

  if (primitive._depthTest) {
    shaderBuilder.addDefine(
      "DEPTH_TEST",
      undefined,
      ShaderDestination.FRAGMENT,
    );
  }

  if (shapeType === "BOX") {
    shaderBuilder.addFragmentLines([
      convertLocalToBoxUv,
      IntersectBox,
      Intersection,
    ]);
  } else if (shapeType === "CYLINDER") {
    shaderBuilder.addFragmentLines([
      convertLocalToCylinderUv,
      IntersectLongitude,
      IntersectCylinder,
      Intersection,
    ]);
  } else if (shapeType === "ELLIPSOID") {
    shaderBuilder.addFragmentLines([
      convertLocalToEllipsoidUv,
      IntersectLongitude,
      IntersectEllipsoid,
      Intersection,
    ]);
  }

  shaderBuilder.addFragmentLines([VoxelFS]);

  const shape = primitive._shape;
  const shapeDefines = shape.shaderDefines;
  for (const key in shapeDefines) {
    if (shapeDefines.hasOwnProperty(key)) {
      let value = shapeDefines[key];
      // if value is undefined, don't define it
      // if value is true, define it to nothing
      if (defined(value)) {
        value = value === true ? undefined : value;
        shaderBuilder.addDefine(key, value, ShaderDestination.FRAGMENT);
      }
    }
  }

  // Count how many intersections the shader will do.
  let intersectionCount = shape.shaderMaximumIntersectionsLength;
  if (clippingPlanesLength > 0) {
    shaderBuilder.addDefine(
      "CLIPPING_PLANES_INTERSECTION_INDEX",
      intersectionCount,
      ShaderDestination.FRAGMENT,
    );
    if (clippingPlanesLength === 1) {
      intersectionCount += 1;
    } else if (clippingPlanes.unionClippingRegions) {
      intersectionCount += 2;
    } else {
      intersectionCount += 1;
    }
  }
  if (primitive._depthTest) {
    shaderBuilder.addDefine(
      "DEPTH_INTERSECTION_INDEX",
      intersectionCount,
      ShaderDestination.FRAGMENT,
    );
    intersectionCount += 1;
  }
  shaderBuilder.addDefine(
    "INTERSECTION_COUNT",
    intersectionCount,
    ShaderDestination.FRAGMENT,
  );

  // Additional fragment shader defines
  if (
    !Cartesian3.equals(primitive.paddingBefore, Cartesian3.ZERO) ||
    !Cartesian3.equals(primitive.paddingAfter, Cartesian3.ZERO)
  ) {
    shaderBuilder.addDefine("PADDING", undefined, ShaderDestination.FRAGMENT);
  }
  // Allow reading from log depth texture, but don't write log depth anywhere.
  // Note: This needs to be set even if depthTest is off because it affects the
  // derived command system.
  if (primitive._useLogDepth) {
    shaderBuilder.addDefine(
      "LOG_DEPTH_READ_ONLY",
      undefined,
      ShaderDestination.FRAGMENT,
    );
  }
  const traversal = primitive._traversal;
  shaderBuilder.addDefine(
    "SAMPLE_COUNT",
    `${traversal._sampleCount}`,
    ShaderDestination.FRAGMENT,
  );
}

export default VoxelRenderResources;
