import BoundingRectangle from "../Core/BoundingRectangle.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import PixelFormat from "../Core/PixelFormat.js";
import ClearCommand from "../Renderer/ClearCommand.js";
import Framebuffer from "../Renderer/Framebuffer.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import RenderState from "../Renderer/RenderState.js";
import Sampler from "../Renderer/Sampler.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import Texture from "../Renderer/Texture.js";
import PassThrough from "../Shaders/PostProcessStages/PassThrough.js";
import PassThroughDepth from "../Shaders/PostProcessStages/PassThroughDepth.js";
import BlendingState from "./BlendingState.js";
import StencilConstants from "./StencilConstants.js";
import StencilFunction from "./StencilFunction.js";
import StencilOperation from "./StencilOperation.js";

/**
 * @private
 */
function GlobeDepth() {
  this._globeColorTexture = undefined;
  this._primitiveColorTexture = undefined;
  this._depthStencilTexture = undefined;
  this._globeDepthTexture = undefined;
  this._tempGlobeDepthTexture = undefined;
  this._tempCopyDepthTexture = undefined;

  this._globeColorFramebuffer = undefined;
  this._primitiveColorFramebuffer = undefined;
  this._copyDepthFramebuffer = undefined;
  this._tempCopyDepthFramebuffer = undefined;
  this._updateDepthFramebuffer = undefined;

  this._clearGlobeColorCommand = undefined;
  this._clearPrimitiveColorCommand = undefined;
  this._copyColorCommand = undefined;
  this._copyDepthCommand = undefined;
  this._tempCopyDepthCommand = undefined;
  this._updateDepthCommand = undefined;
  this._mergeColorCommand = undefined;

  this._viewport = new BoundingRectangle();
  this._rs = undefined;
  this._rsBlend = undefined;
  this._rsUpdate = undefined;

  this._useScissorTest = false;
  this._scissorRectangle = undefined;

  this._useLogDepth = undefined;
  this._useHdr = undefined;
  this._clearGlobeDepth = undefined;

  this._debugGlobeDepthViewportCommand = undefined;
}

Object.defineProperties(GlobeDepth.prototype, {
  framebuffer: {
    get: function () {
      return this._globeColorFramebuffer;
    },
  },
  primitiveFramebuffer: {
    get: function () {
      return this._primitiveColorFramebuffer;
    },
  },
});

function executeDebugGlobeDepth(globeDepth, context, passState, useLogDepth) {
  if (
    !defined(globeDepth._debugGlobeDepthViewportCommand) ||
    useLogDepth !== globeDepth._useLogDepth
  ) {
    var fsSource =
      "uniform highp sampler2D u_depthTexture;\n" +
      "varying vec2 v_textureCoordinates;\n" +
      "void main()\n" +
      "{\n" +
      "    float z_window = czm_unpackDepth(texture2D(u_depthTexture, v_textureCoordinates));\n" +
      "    z_window = czm_reverseLogDepth(z_window); \n" +
      "    float n_range = czm_depthRange.near;\n" +
      "    float f_range = czm_depthRange.far;\n" +
      "    float z_ndc = (2.0 * z_window - n_range - f_range) / (f_range - n_range);\n" +
      "    float scale = pow(z_ndc * 0.5 + 0.5, 8.0);\n" +
      "    gl_FragColor = vec4(mix(vec3(0.0), vec3(1.0), scale), 1.0);\n" +
      "}\n";
    var fs = new ShaderSource({
      defines: [useLogDepth ? "LOG_DEPTH" : ""],
      sources: [fsSource],
    });

    globeDepth._debugGlobeDepthViewportCommand = context.createViewportQuadCommand(
      fs,
      {
        uniformMap: {
          u_depthTexture: function () {
            return globeDepth._globeDepthTexture;
          },
        },
        owner: globeDepth,
      }
    );

    globeDepth._useLogDepth = useLogDepth;
  }

  globeDepth._debugGlobeDepthViewportCommand.execute(context, passState);
}

function destroyTextures(globeDepth) {
  globeDepth._globeColorTexture =
    globeDepth._globeColorTexture &&
    !globeDepth._globeColorTexture.isDestroyed() &&
    globeDepth._globeColorTexture.destroy();
  globeDepth._depthStencilTexture =
    globeDepth._depthStencilTexture &&
    !globeDepth._depthStencilTexture.isDestroyed() &&
    globeDepth._depthStencilTexture.destroy();
  globeDepth._globeDepthTexture =
    globeDepth._globeDepthTexture &&
    !globeDepth._globeDepthTexture.isDestroyed() &&
    globeDepth._globeDepthTexture.destroy();
}

function destroyFramebuffers(globeDepth) {
  globeDepth._globeColorFramebuffer =
    globeDepth._globeColorFramebuffer &&
    !globeDepth._globeColorFramebuffer.isDestroyed() &&
    globeDepth._globeColorFramebuffer.destroy();
  globeDepth._copyDepthFramebuffer =
    globeDepth._copyDepthFramebuffer &&
    !globeDepth._copyDepthFramebuffer.isDestroyed() &&
    globeDepth._copyDepthFramebuffer.destroy();
}

function destroyUpdateDepthResources(globeDepth) {
  globeDepth._tempCopyDepthFramebuffer =
    globeDepth._tempCopyDepthFramebuffer &&
    !globeDepth._tempCopyDepthFramebuffer.isDestroyed() &&
    globeDepth._tempCopyDepthFramebuffer.destroy();
  globeDepth._updateDepthFramebuffer =
    globeDepth._updateDepthFramebuffer &&
    !globeDepth._updateDepthFramebuffer.isDestroyed() &&
    globeDepth._updateDepthFramebuffer.destroy();
  globeDepth._tempGlobeDepthTexture =
    globeDepth._tempGlobeDepthTexture &&
    !globeDepth._tempGlobeDepthTexture.isDestroyed() &&
    globeDepth._tempGlobeDepthTexture.destroy();
}

function createUpdateDepthResources(
  globeDepth,
  context,
  width,
  height,
  passState
) {
  globeDepth._tempGlobeDepthTexture = new Texture({
    context: context,
    width: width,
    height: height,
    pixelFormat: PixelFormat.RGBA,
    pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
    sampler: Sampler.NEAREST,
  });
  globeDepth._tempCopyDepthFramebuffer = new Framebuffer({
    context: context,
    colorTextures: [globeDepth._tempGlobeDepthTexture],
    destroyAttachments: false,
  });
  globeDepth._updateDepthFramebuffer = new Framebuffer({
    context: context,
    colorTextures: [globeDepth._globeDepthTexture],
    depthStencilTexture: passState.framebuffer.depthStencilTexture,
    destroyAttachments: false,
  });
}

function createTextures(globeDepth, context, width, height, hdr) {
  var pixelDatatype = hdr
    ? context.halfFloatingPointTexture
      ? PixelDatatype.HALF_FLOAT
      : PixelDatatype.FLOAT
    : PixelDatatype.UNSIGNED_BYTE;
  globeDepth._globeColorTexture = new Texture({
    context: context,
    width: width,
    height: height,
    pixelFormat: PixelFormat.RGBA,
    pixelDatatype: pixelDatatype,
    sampler: Sampler.NEAREST,
  });

  globeDepth._depthStencilTexture = new Texture({
    context: context,
    width: width,
    height: height,
    pixelFormat: PixelFormat.DEPTH_STENCIL,
    pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8,
  });

  globeDepth._globeDepthTexture = new Texture({
    context: context,
    width: width,
    height: height,
    pixelFormat: PixelFormat.RGBA,
    pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
    sampler: Sampler.NEAREST,
  });
}

function createFramebuffers(globeDepth, context) {
  globeDepth._globeColorFramebuffer = new Framebuffer({
    context: context,
    colorTextures: [globeDepth._globeColorTexture],
    depthStencilTexture: globeDepth._depthStencilTexture,
    destroyAttachments: false,
  });

  globeDepth._copyDepthFramebuffer = new Framebuffer({
    context: context,
    colorTextures: [globeDepth._globeDepthTexture],
    destroyAttachments: false,
  });
}

function createPrimitiveFramebuffer(globeDepth, context, width, height, hdr) {
  var pixelDatatype = hdr
    ? context.halfFloatingPointTexture
      ? PixelDatatype.HALF_FLOAT
      : PixelDatatype.FLOAT
    : PixelDatatype.UNSIGNED_BYTE;
  globeDepth._primitiveColorTexture = new Texture({
    context: context,
    width: width,
    height: height,
    pixelFormat: PixelFormat.RGBA,
    pixelDatatype: pixelDatatype,
    sampler: Sampler.NEAREST,
  });

  globeDepth._primitiveColorFramebuffer = new Framebuffer({
    context: context,
    colorTextures: [globeDepth._primitiveColorTexture],
    depthStencilTexture: globeDepth._depthStencilTexture,
    destroyAttachments: false,
  });
}

function destroyPrimitiveFramebuffer(globeDepth) {
  globeDepth._primitiveColorTexture =
    globeDepth._primitiveColorTexture &&
    !globeDepth._primitiveColorTexture.isDestroyed() &&
    globeDepth._primitiveColorTexture.destroy();
  globeDepth._primitiveColorFramebuffer =
    globeDepth._primitiveColorFramebuffer &&
    !globeDepth._primitiveColorFramebuffer.isDestroyed() &&
    globeDepth._primitiveColorFramebuffer.destroy();
}

function updateFramebuffers(
  globeDepth,
  context,
  width,
  height,
  hdr,
  clearGlobeDepth
) {
  var colorTexture = globeDepth._globeColorTexture;
  var textureChanged =
    !defined(colorTexture) ||
    colorTexture.width !== width ||
    colorTexture.height !== height ||
    hdr !== globeDepth._useHdr;
  if (textureChanged) {
    destroyTextures(globeDepth);
    destroyFramebuffers(globeDepth);
    createTextures(globeDepth, context, width, height, hdr, clearGlobeDepth);
    createFramebuffers(globeDepth, context, clearGlobeDepth);
  }

  if (textureChanged || clearGlobeDepth !== globeDepth._clearGlobeDepth) {
    destroyPrimitiveFramebuffer(globeDepth);
    if (clearGlobeDepth) {
      createPrimitiveFramebuffer(globeDepth, context, width, height, hdr);
    }
  }
}

function updateCopyCommands(globeDepth, context, width, height, passState) {
  globeDepth._viewport.width = width;
  globeDepth._viewport.height = height;

  var useScissorTest = !BoundingRectangle.equals(
    globeDepth._viewport,
    passState.viewport
  );
  var updateScissor = useScissorTest !== globeDepth._useScissorTest;
  globeDepth._useScissorTest = useScissorTest;

  if (
    !BoundingRectangle.equals(globeDepth._scissorRectangle, passState.viewport)
  ) {
    globeDepth._scissorRectangle = BoundingRectangle.clone(
      passState.viewport,
      globeDepth._scissorRectangle
    );
    updateScissor = true;
  }

  if (
    !defined(globeDepth._rs) ||
    !BoundingRectangle.equals(globeDepth._viewport, globeDepth._rs.viewport) ||
    updateScissor
  ) {
    globeDepth._rs = RenderState.fromCache({
      viewport: globeDepth._viewport,
      scissorTest: {
        enabled: globeDepth._useScissorTest,
        rectangle: globeDepth._scissorRectangle,
      },
    });
    globeDepth._rsBlend = RenderState.fromCache({
      viewport: globeDepth._viewport,
      scissorTest: {
        enabled: globeDepth._useScissorTest,
        rectangle: globeDepth._scissorRectangle,
      },
      blending: BlendingState.ALPHA_BLEND,
    });

    // Copy packed depth only if the 3D Tiles bit is set
    globeDepth._rsUpdate = RenderState.fromCache({
      viewport: globeDepth._viewport,
      scissorTest: {
        enabled: globeDepth._useScissorTest,
        rectangle: globeDepth._scissorRectangle,
      },
      stencilTest: {
        enabled: true,
        frontFunction: StencilFunction.EQUAL,
        frontOperation: {
          fail: StencilOperation.KEEP,
          zFail: StencilOperation.KEEP,
          zPass: StencilOperation.KEEP,
        },
        backFunction: StencilFunction.NEVER,
        reference: StencilConstants.CESIUM_3D_TILE_MASK,
        mask: StencilConstants.CESIUM_3D_TILE_MASK,
      },
    });
  }

  if (!defined(globeDepth._copyDepthCommand)) {
    globeDepth._copyDepthCommand = context.createViewportQuadCommand(
      PassThroughDepth,
      {
        uniformMap: {
          u_depthTexture: function () {
            return globeDepth._depthStencilTexture;
          },
        },
        owner: globeDepth,
      }
    );
  }

  globeDepth._copyDepthCommand.framebuffer = globeDepth._copyDepthFramebuffer;
  globeDepth._copyDepthCommand.renderState = globeDepth._rs;

  if (!defined(globeDepth._copyColorCommand)) {
    globeDepth._copyColorCommand = context.createViewportQuadCommand(
      PassThrough,
      {
        uniformMap: {
          colorTexture: function () {
            return globeDepth._globeColorTexture;
          },
        },
        owner: globeDepth,
      }
    );
  }

  globeDepth._copyColorCommand.renderState = globeDepth._rs;

  if (!defined(globeDepth._tempCopyDepthCommand)) {
    globeDepth._tempCopyDepthCommand = context.createViewportQuadCommand(
      PassThroughDepth,
      {
        uniformMap: {
          u_depthTexture: function () {
            return globeDepth._tempCopyDepthTexture;
          },
        },
        owner: globeDepth,
      }
    );
  }

  globeDepth._tempCopyDepthCommand.framebuffer =
    globeDepth._tempCopyDepthFramebuffer;
  globeDepth._tempCopyDepthCommand.renderState = globeDepth._rs;

  if (!defined(globeDepth._updateDepthCommand)) {
    globeDepth._updateDepthCommand = context.createViewportQuadCommand(
      PassThrough,
      {
        uniformMap: {
          colorTexture: function () {
            return globeDepth._tempGlobeDepthTexture;
          },
        },
        owner: globeDepth,
      }
    );
  }

  globeDepth._updateDepthCommand.framebuffer =
    globeDepth._updateDepthFramebuffer;
  globeDepth._updateDepthCommand.renderState = globeDepth._rsUpdate;

  if (!defined(globeDepth._clearGlobeColorCommand)) {
    globeDepth._clearGlobeColorCommand = new ClearCommand({
      color: new Color(0.0, 0.0, 0.0, 0.0),
      stencil: 0.0,
      owner: globeDepth,
    });
  }

  globeDepth._clearGlobeColorCommand.framebuffer =
    globeDepth._globeColorFramebuffer;

  if (!defined(globeDepth._clearPrimitiveColorCommand)) {
    globeDepth._clearPrimitiveColorCommand = new ClearCommand({
      color: new Color(0.0, 0.0, 0.0, 0.0),
      stencil: 0.0,
      owner: globeDepth,
    });
  }

  globeDepth._clearPrimitiveColorCommand.framebuffer =
    globeDepth._primitiveColorFramebuffer;

  if (!defined(globeDepth._mergeColorCommand)) {
    globeDepth._mergeColorCommand = context.createViewportQuadCommand(
      PassThrough,
      {
        uniformMap: {
          colorTexture: function () {
            return globeDepth._primitiveColorTexture;
          },
        },
        owner: globeDepth,
      }
    );
  }

  globeDepth._mergeColorCommand.framebuffer = globeDepth._globeColorFramebuffer;
  globeDepth._mergeColorCommand.renderState = globeDepth._rsBlend;
}

GlobeDepth.prototype.executeDebugGlobeDepth = function (
  context,
  passState,
  useLogDepth
) {
  executeDebugGlobeDepth(this, context, passState, useLogDepth);
};

GlobeDepth.prototype.update = function (
  context,
  passState,
  viewport,
  hdr,
  clearGlobeDepth
) {
  var width = viewport.width;
  var height = viewport.height;

  updateFramebuffers(this, context, width, height, hdr, clearGlobeDepth);
  updateCopyCommands(this, context, width, height, passState);
  context.uniformState.globeDepthTexture = undefined;

  this._useHdr = hdr;
  this._clearGlobeDepth = clearGlobeDepth;
};

GlobeDepth.prototype.executeCopyDepth = function (context, passState) {
  if (defined(this._copyDepthCommand)) {
    this._copyDepthCommand.execute(context, passState);
    context.uniformState.globeDepthTexture = this._globeDepthTexture;
  }
};

GlobeDepth.prototype.executeUpdateDepth = function (
  context,
  passState,
  clearGlobeDepth
) {
  var depthTextureToCopy = passState.framebuffer.depthStencilTexture;
  if (clearGlobeDepth || depthTextureToCopy !== this._depthStencilTexture) {
    // First copy the depth to a temporary globe depth texture, then update the
    // main globe depth texture where the stencil bit for 3D Tiles is set.
    // This preserves the original globe depth except where 3D Tiles is rendered.
    // The additional texture and framebuffer resources are created on demand.
    if (defined(this._updateDepthCommand)) {
      if (
        !defined(this._updateDepthFramebuffer) ||
        this._updateDepthFramebuffer.depthStencilTexture !==
          depthTextureToCopy ||
        this._updateDepthFramebuffer.getColorTexture(0) !==
          this._globeDepthTexture
      ) {
        var width = this._globeDepthTexture.width;
        var height = this._globeDepthTexture.height;
        destroyUpdateDepthResources(this);
        createUpdateDepthResources(this, context, width, height, passState);
        updateCopyCommands(this, context, width, height, passState);
      }
      this._tempCopyDepthTexture = depthTextureToCopy;
      this._tempCopyDepthCommand.execute(context, passState);
      this._updateDepthCommand.execute(context, passState);
    }
    return;
  }

  // Fast path - the depth texture can be copied normally.
  if (defined(this._copyDepthCommand)) {
    this._copyDepthCommand.execute(context, passState);
  }
};

GlobeDepth.prototype.executeCopyColor = function (context, passState) {
  if (defined(this._copyColorCommand)) {
    this._copyColorCommand.execute(context, passState);
  }
};

GlobeDepth.prototype.executeMergeColor = function (context, passState) {
  if (defined(this._mergeColorCommand)) {
    this._mergeColorCommand.execute(context, passState);
  }
};

GlobeDepth.prototype.clear = function (context, passState, clearColor) {
  var clear = this._clearGlobeColorCommand;
  if (defined(clear)) {
    Color.clone(clearColor, clear.color);
    clear.execute(context, passState);
  }
  clear = this._clearPrimitiveColorCommand;
  if (defined(clear) && defined(this._primitiveColorFramebuffer)) {
    clear.execute(context, passState);
  }
};

GlobeDepth.prototype.isDestroyed = function () {
  return false;
};

GlobeDepth.prototype.destroy = function () {
  destroyTextures(this);
  destroyFramebuffers(this);
  destroyPrimitiveFramebuffer(this);
  destroyUpdateDepthResources(this);

  if (defined(this._copyColorCommand)) {
    this._copyColorCommand.shaderProgram = this._copyColorCommand.shaderProgram.destroy();
  }

  if (defined(this._copyDepthCommand)) {
    this._copyDepthCommand.shaderProgram = this._copyDepthCommand.shaderProgram.destroy();
  }

  if (defined(this._tempCopyDepthCommand)) {
    this._tempCopyDepthCommand.shaderProgram = this._tempCopyDepthCommand.shaderProgram.destroy();
  }

  if (defined(this._updateDepthCommand)) {
    this._updateDepthCommand.shaderProgram = this._updateDepthCommand.shaderProgram.destroy();
  }

  if (defined(this._mergeColorCommand)) {
    this._mergeColorCommand.shaderProgram = this._mergeColorCommand.shaderProgram.destroy();
  }

  if (defined(this._debugGlobeDepthViewportCommand)) {
    this._debugGlobeDepthViewportCommand.shaderProgram = this._debugGlobeDepthViewportCommand.shaderProgram.destroy();
  }

  return destroyObject(this);
};
export default GlobeDepth;
