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 DrawCommand from "../Renderer/DrawCommand.js";
import FramebufferManager from "../Renderer/FramebufferManager.js";
import Pass from "../Renderer/Pass.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 CompareAndPackTranslucentDepth from "../Shaders/CompareAndPackTranslucentDepth.js";
import CompositeTranslucentClassification from "../Shaders/PostProcessStages/CompositeTranslucentClassification.js";
import BlendingState from "./BlendingState.js";
import StencilConstants from "./StencilConstants.js";
import StencilFunction from "./StencilFunction.js";

const debugShowPackedDepth = false;

/**
 * Handles buffers, drawing, and deriving commands needed for classifying translucent 3D Tiles.
 * Uses a depth texture, so classification on translucent 3D Tiles is not available in Internet Explorer.
 *
 * @private
 */
function TranslucentTileClassification(context) {
  this._drawClassificationFBO = new FramebufferManager({
    createDepthAttachments: false,
  });
  this._accumulationFBO = new FramebufferManager({
    createDepthAttachments: false,
  });
  this._packFBO = new FramebufferManager();

  this._opaqueDepthStencilTexture = undefined;

  // Reference to either colorTexture or accumulationTexture
  this._textureToComposite = undefined;

  this._translucentDepthStencilTexture = undefined;

  this._packDepthCommand = undefined;
  this._accumulateCommand = undefined;
  this._compositeCommand = undefined;
  this._copyCommand = undefined;

  this._clearColorCommand = new ClearCommand({
    color: new Color(0.0, 0.0, 0.0, 0.0),
    owner: this,
  });

  this._clearDepthStencilCommand = new ClearCommand({
    depth: 1.0,
    stencil: 0,
    owner: this,
  });

  this._supported = context.depthTexture;

  this._viewport = new BoundingRectangle();
  this._rsDepth = undefined;
  this._rsAccumulate = undefined;
  this._rsComp = undefined;
  this._useScissorTest = undefined;
  this._scissorRectangle = undefined;

  this._hasTranslucentDepth = false;
  this._frustumsDrawn = 0;
}

Object.defineProperties(TranslucentTileClassification.prototype, {
  /**
   * Gets whether or not translucent depth was rendered.
   * @memberof TranslucentTileClassification.prototype
   *
   * @type {boolean}
   * @readonly
   */
  hasTranslucentDepth: {
    get: function () {
      return this._hasTranslucentDepth;
    },
  },
});

function destroyTextures(transpClass) {
  transpClass._textureToComposite = undefined;

  transpClass._translucentDepthStencilTexture =
    transpClass._translucentDepthStencilTexture &&
    !transpClass._translucentDepthStencilTexture.isDestroyed() &&
    transpClass._translucentDepthStencilTexture.destroy();
}

function destroyFramebuffers(transpClass) {
  transpClass._drawClassificationFBO.destroy();
  transpClass._accumulationFBO.destroy();
  transpClass._packFBO.destroy();
}

function updateTextures(transpClass, context, width, height) {
  destroyTextures(transpClass);

  transpClass._translucentDepthStencilTexture = new Texture({
    context: context,
    width: width,
    height: height,
    pixelFormat: PixelFormat.DEPTH_STENCIL,
    pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8,
    sampler: Sampler.NEAREST,
  });
}

function updateFramebuffers(transpClass, context, width, height) {
  destroyFramebuffers(transpClass);
  transpClass._drawClassificationFBO.setDepthStencilTexture(
    transpClass._translucentDepthStencilTexture,
  );
  transpClass._drawClassificationFBO.update(context, width, height);

  transpClass._accumulationFBO.setDepthStencilTexture(
    transpClass._translucentDepthStencilTexture,
  );
  transpClass._accumulationFBO.update(context, width, height);

  transpClass._packFBO.update(context, width, height);
}

function updateResources(
  transpClass,
  context,
  passState,
  globeDepthStencilTexture,
) {
  if (!transpClass.isSupported()) {
    return;
  }

  transpClass._opaqueDepthStencilTexture = globeDepthStencilTexture;

  const width = transpClass._opaqueDepthStencilTexture.width;
  const height = transpClass._opaqueDepthStencilTexture.height;
  if (transpClass._drawClassificationFBO.isDirty(width, height)) {
    updateTextures(transpClass, context, width, height);
    updateFramebuffers(transpClass, context, width, height);
  }

  let fs;
  let uniformMap;

  if (!defined(transpClass._packDepthCommand)) {
    fs = new ShaderSource({
      sources: [CompareAndPackTranslucentDepth],
    });

    uniformMap = {
      u_opaqueDepthTexture: function () {
        return transpClass._opaqueDepthStencilTexture;
      },
      u_translucentDepthTexture: function () {
        return transpClass._translucentDepthStencilTexture;
      },
    };

    transpClass._packDepthCommand = context.createViewportQuadCommand(fs, {
      uniformMap: uniformMap,
      owner: transpClass,
    });
  }

  if (!defined(transpClass._compositeCommand)) {
    fs = new ShaderSource({
      sources: [CompositeTranslucentClassification],
    });

    uniformMap = {
      colorTexture: function () {
        return transpClass._textureToComposite;
      },
    };

    if (debugShowPackedDepth) {
      fs.defines = ["DEBUG_SHOW_DEPTH"];
      uniformMap.u_packedTranslucentDepth = function () {
        return transpClass._packFBO.getColorTexture();
      };
    }

    transpClass._compositeCommand = context.createViewportQuadCommand(fs, {
      uniformMap: uniformMap,
      owner: transpClass,
    });

    const compositeCommand = transpClass._compositeCommand;
    const compositeProgram = compositeCommand.shaderProgram;
    const compositePickProgram = context.shaderCache.createDerivedShaderProgram(
      compositeProgram,
      "pick",
      {
        vertexShaderSource: compositeProgram.vertexShaderSource,
        fragmentShaderSource: new ShaderSource({
          sources: fs.sources,
          defines: ["PICK"],
        }),
        attributeLocations: compositeProgram._attributeLocations,
      },
    );
    const compositePickCommand = DrawCommand.shallowClone(compositeCommand);
    compositePickCommand.shaderProgram = compositePickProgram;
    compositeCommand.derivedCommands.pick = compositePickCommand;
  }

  if (!defined(transpClass._copyCommand)) {
    fs = new ShaderSource({
      sources: [CompositeTranslucentClassification],
    });

    uniformMap = {
      colorTexture: function () {
        return transpClass._drawClassificationFBO.getColorTexture();
      },
    };

    transpClass._copyCommand = context.createViewportQuadCommand(fs, {
      uniformMap: uniformMap,
      owner: transpClass,
    });
  }

  if (!defined(transpClass._accumulateCommand)) {
    fs = new ShaderSource({
      sources: [CompositeTranslucentClassification],
    });

    uniformMap = {
      colorTexture: function () {
        return transpClass._drawClassificationFBO.getColorTexture();
      },
    };

    transpClass._accumulateCommand = context.createViewportQuadCommand(fs, {
      uniformMap: uniformMap,
      owner: transpClass,
    });
  }

  transpClass._viewport.width = width;
  transpClass._viewport.height = height;

  const useScissorTest = !BoundingRectangle.equals(
    transpClass._viewport,
    passState.viewport,
  );
  let updateScissor = useScissorTest !== transpClass._useScissorTest;
  transpClass._useScissorTest = useScissorTest;

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

  if (
    !defined(transpClass._rsDepth) ||
    !BoundingRectangle.equals(
      transpClass._viewport,
      transpClass._rsDepth.viewport,
    ) ||
    updateScissor
  ) {
    transpClass._rsDepth = RenderState.fromCache({
      viewport: transpClass._viewport,
      scissorTest: {
        enabled: transpClass._useScissorTest,
        rectangle: transpClass._scissorRectangle,
      },
    });
  }

  if (defined(transpClass._packDepthCommand)) {
    transpClass._packDepthCommand.renderState = transpClass._rsDepth;
  }

  if (
    !defined(transpClass._rsAccumulate) ||
    !BoundingRectangle.equals(
      transpClass._viewport,
      transpClass._rsAccumulate.viewport,
    ) ||
    updateScissor
  ) {
    transpClass._rsAccumulate = RenderState.fromCache({
      viewport: transpClass._viewport,
      scissorTest: {
        enabled: transpClass._useScissorTest,
        rectangle: transpClass._scissorRectangle,
      },
      stencilTest: {
        enabled: true,
        frontFunction: StencilFunction.EQUAL,
        reference: StencilConstants.CESIUM_3D_TILE_MASK,
      },
    });
  }

  if (defined(transpClass._accumulateCommand)) {
    transpClass._accumulateCommand.renderState = transpClass._rsAccumulate;
  }

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

  if (defined(transpClass._compositeCommand)) {
    transpClass._compositeCommand.renderState = transpClass._rsComp;
    transpClass._compositeCommand.derivedCommands.pick.renderState =
      transpClass._rsComp;
  }
}

TranslucentTileClassification.prototype.executeTranslucentCommands = function (
  scene,
  executeCommand,
  passState,
  commands,
  globeDepthStencilTexture,
) {
  // Check for translucent commands that should be classified
  const useLogDepth = scene.frameState.useLogDepth;
  const context = scene.context;
  const framebuffer = passState.framebuffer;

  for (let i = 0; i < commands.length; ++i) {
    let command = commands[i];
    command = useLogDepth ? command.derivedCommands.logDepth.command : command;

    if (command.depthForTranslucentClassification) {
      this._hasTranslucentDepth = true;
      break;
    }
  }

  if (!this._hasTranslucentDepth) {
    return;
  }

  updateResources(this, context, passState, globeDepthStencilTexture);

  // Get translucent depth
  passState.framebuffer = this._drawClassificationFBO.framebuffer;

  // Clear depth for multifrustum
  this._clearDepthStencilCommand.execute(context, passState);

  for (let i = 0; i < commands.length; ++i) {
    let command = commands[i];
    command = useLogDepth ? command.derivedCommands.logDepth.command : command;

    if (!command.depthForTranslucentClassification) {
      continue;
    }

    // Depth-only commands are created for all translucent 3D Tiles commands
    const depthOnlyCommand = command.derivedCommands.depth.depthOnlyCommand;
    executeCommand(depthOnlyCommand, scene, passState);
  }

  this._frustumsDrawn += this._hasTranslucentDepth ? 1 : 0;

  // Pack depth if any translucent depth commands were performed
  if (this._hasTranslucentDepth) {
    passState.framebuffer = this._packFBO.framebuffer;
    this._packDepthCommand.execute(context, passState);
  }

  passState.framebuffer = framebuffer;
};

TranslucentTileClassification.prototype.executeClassificationCommands =
  function (scene, executeCommand, passState, frustumCommands) {
    if (!this._hasTranslucentDepth) {
      return;
    }

    const context = scene.context;
    const uniformState = context.uniformState;
    const framebuffer = passState.framebuffer;

    passState.framebuffer = this._accumulationFBO.framebuffer;
    this._accumulateCommand.execute(context, passState);

    passState.framebuffer = this._drawClassificationFBO.framebuffer;
    if (this._frustumsDrawn > 1) {
      this._clearColorCommand.execute(context, passState);
    }

    uniformState.updatePass(Pass.CESIUM_3D_TILE_CLASSIFICATION);
    const swapGlobeDepth = uniformState.globeDepthTexture;
    uniformState.globeDepthTexture = this._packFBO.getColorTexture();
    const commands =
      frustumCommands.commands[Pass.CESIUM_3D_TILE_CLASSIFICATION];
    const length = frustumCommands.indices[Pass.CESIUM_3D_TILE_CLASSIFICATION];
    for (let i = 0; i < length; ++i) {
      executeCommand(commands[i], scene, passState);
    }

    uniformState.globeDepthTexture = swapGlobeDepth;
    passState.framebuffer = framebuffer;

    if (this._frustumsDrawn === 1) {
      return;
    }

    passState.framebuffer = this._accumulationFBO.framebuffer;
    this._accumulateCommand.execute(context, passState);

    passState.framebuffer = framebuffer;
  };

TranslucentTileClassification.prototype.execute = function (scene, passState) {
  if (!this._hasTranslucentDepth) {
    return;
  }
  if (this._frustumsDrawn === 1) {
    this._textureToComposite = this._drawClassificationFBO.getColorTexture();
  } else {
    this._textureToComposite = this._accumulationFBO.getColorTexture();
  }

  const command = scene.frameState.passes.pick
    ? this._compositeCommand.derivedCommands.pick
    : this._compositeCommand;
  command.execute(scene.context, passState);

  clear(this, scene, passState);
};

function clear(translucentTileClassification, scene, passState) {
  if (!translucentTileClassification._hasTranslucentDepth) {
    return;
  }

  const framebuffer = passState.framebuffer;

  passState.framebuffer =
    translucentTileClassification._drawClassificationFBO.framebuffer;
  translucentTileClassification._clearColorCommand.execute(
    scene._context,
    passState,
  );

  passState.framebuffer = framebuffer;

  if (translucentTileClassification._frustumsDrawn > 1) {
    passState.framebuffer =
      translucentTileClassification._accumulationFBO.framebuffer;
    translucentTileClassification._clearColorCommand.execute(
      scene._context,
      passState,
    );
  }

  translucentTileClassification._hasTranslucentDepth = false;
  translucentTileClassification._frustumsDrawn = 0;
}

TranslucentTileClassification.prototype.isSupported = function () {
  return this._supported;
};

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

TranslucentTileClassification.prototype.destroy = function () {
  destroyTextures(this);
  destroyFramebuffers(this);

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

  if (defined(this._packDepthCommand)) {
    this._packDepthCommand.shaderProgram =
      this._packDepthCommand.shaderProgram &&
      this._packDepthCommand.shaderProgram.destroy();
  }
  return destroyObject(this);
};

export default TranslucentTileClassification;
