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 TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";
import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
import TextureWrap from "../Renderer/TextureWrap.js";
import PassThrough from "../Shaders/PostProcessStages/PassThrough.js";
import BlendingState from "./BlendingState.js";
import StencilConstants from "./StencilConstants.js";
import StencilFunction from "./StencilFunction.js";
import StencilOperation from "./StencilOperation.js";

/**
 * @private
 */
function InvertClassification() {
  this.previousFramebuffer = undefined;
  this._previousFramebuffer = undefined;

  this._texture = undefined;
  this._classifiedTexture = undefined;
  this._depthStencilTexture = undefined;
  this._fbo = undefined;
  this._fboClassified = undefined;

  this._rsUnclassified = undefined;
  this._rsClassified = undefined;

  this._unclassifiedCommand = undefined;
  this._classifiedCommand = undefined;
  this._translucentCommand = undefined;

  this._clearColorCommand = new ClearCommand({
    color: new Color(0.0, 0.0, 0.0, 0.0),
    owner: this,
  });
  this._clearCommand = new ClearCommand({
    color: new Color(0.0, 0.0, 0.0, 0.0),
    depth: 1.0,
    stencil: 0,
  });

  var that = this;
  this._uniformMap = {
    colorTexture: function () {
      return that._texture;
    },
    depthTexture: function () {
      return that._depthStencilTexture;
    },
    classifiedTexture: function () {
      return that._classifiedTexture;
    },
  };
}

Object.defineProperties(InvertClassification.prototype, {
  unclassifiedCommand: {
    get: function () {
      return this._unclassifiedCommand;
    },
  },
});

InvertClassification.isTranslucencySupported = function (context) {
  return context.depthTexture && context.fragmentDepth;
};

var rsUnclassified = {
  depthMask: false,
  stencilTest: {
    enabled: true,
    frontFunction: StencilFunction.EQUAL,
    frontOperation: {
      fail: StencilOperation.KEEP,
      zFail: StencilOperation.KEEP,
      zPass: StencilOperation.KEEP,
    },
    backFunction: StencilFunction.NEVER,
    reference: 0,
    mask: StencilConstants.CLASSIFICATION_MASK,
  },
  blending: BlendingState.ALPHA_BLEND,
};

var rsClassified = {
  depthMask: false,
  stencilTest: {
    enabled: true,
    frontFunction: StencilFunction.NOT_EQUAL,
    frontOperation: {
      fail: StencilOperation.KEEP,
      zFail: StencilOperation.KEEP,
      zPass: StencilOperation.KEEP,
    },
    backFunction: StencilFunction.NEVER,
    reference: 0,
    mask: StencilConstants.CLASSIFICATION_MASK,
  },
  blending: BlendingState.ALPHA_BLEND,
};

// Set the 3D Tiles bit when rendering back into the scene's framebuffer. This is only needed if
// invert classification does not use the scene's depth-stencil texture, which is the case if the invert
// classification color is translucent.
var rsDefault = {
  depthMask: true,
  depthTest: {
    enabled: true,
  },
  stencilTest: StencilConstants.setCesium3DTileBit(),
  stencilMask: StencilConstants.CESIUM_3D_TILE_MASK,
  blending: BlendingState.ALPHA_BLEND,
};

var translucentFS =
  "#extension GL_EXT_frag_depth : enable\n" +
  "uniform sampler2D colorTexture;\n" +
  "uniform sampler2D depthTexture;\n" +
  "uniform sampler2D classifiedTexture;\n" +
  "varying vec2 v_textureCoordinates;\n" +
  "void main()\n" +
  "{\n" +
  "    vec4 color = texture2D(colorTexture, v_textureCoordinates);\n" +
  "    if (color.a == 0.0)\n" +
  "    {\n" +
  "        discard;\n" +
  "    }\n" +
  "    bool isClassified = all(equal(texture2D(classifiedTexture, v_textureCoordinates), vec4(0.0)));\n" +
  "#ifdef UNCLASSIFIED\n" +
  "    vec4 highlightColor = czm_invertClassificationColor;\n" +
  "    if (isClassified)\n" +
  "    {\n" +
  "        discard;\n" +
  "    }\n" +
  "#else\n" +
  "    vec4 highlightColor = vec4(1.0);\n" +
  "    if (!isClassified)\n" +
  "    {\n" +
  "        discard;\n" +
  "    }\n" +
  "#endif\n" +
  "    gl_FragColor = color * highlightColor;\n" +
  "    gl_FragDepthEXT = texture2D(depthTexture, v_textureCoordinates).r;\n" +
  "}\n";

var opaqueFS =
  "uniform sampler2D colorTexture;\n" +
  "varying vec2 v_textureCoordinates;\n" +
  "void main()\n" +
  "{\n" +
  "    vec4 color = texture2D(colorTexture, v_textureCoordinates);\n" +
  "    if (color.a == 0.0)\n" +
  "    {\n" +
  "        discard;\n" +
  "    }\n" +
  "#ifdef UNCLASSIFIED\n" +
  "    gl_FragColor = color * czm_invertClassificationColor;\n" +
  "#else\n" +
  "    gl_FragColor = color;\n" +
  "#endif\n" +
  "}\n";

InvertClassification.prototype.update = function (context) {
  var texture = this._texture;
  var previousFramebufferChanged =
    !defined(texture) || this.previousFramebuffer !== this._previousFramebuffer;
  this._previousFramebuffer = this.previousFramebuffer;

  var width = context.drawingBufferWidth;
  var height = context.drawingBufferHeight;

  var textureChanged =
    !defined(texture) || texture.width !== width || texture.height !== height;
  if (textureChanged || previousFramebufferChanged) {
    this._texture = this._texture && this._texture.destroy();
    this._classifiedTexture =
      this._classifiedTexture && this._classifiedTexture.destroy();
    this._depthStencilTexture =
      this._depthStencilTexture && this._depthStencilTexture.destroy();

    this._texture = new Texture({
      context: context,
      width: width,
      height: height,
      pixelFormat: PixelFormat.RGBA,
      pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
      sampler: new Sampler({
        wrapS: TextureWrap.CLAMP_TO_EDGE,
        wrapT: TextureWrap.CLAMP_TO_EDGE,
        minificationFilter: TextureMinificationFilter.LINEAR,
        magnificationFilter: TextureMagnificationFilter.LINEAR,
      }),
    });

    if (!defined(this._previousFramebuffer)) {
      this._classifiedTexture = new Texture({
        context: context,
        width: width,
        height: height,
        pixelFormat: PixelFormat.RGBA,
        pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
        sampler: new Sampler({
          wrapS: TextureWrap.CLAMP_TO_EDGE,
          wrapT: TextureWrap.CLAMP_TO_EDGE,
          minificationFilter: TextureMinificationFilter.LINEAR,
          magnificationFilter: TextureMagnificationFilter.LINEAR,
        }),
      });
      this._depthStencilTexture = new Texture({
        context: context,
        width: width,
        height: height,
        pixelFormat: PixelFormat.DEPTH_STENCIL,
        pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8,
      });
    }
  }

  if (!defined(this._fbo) || textureChanged || previousFramebufferChanged) {
    this._fbo = this._fbo && this._fbo.destroy();
    this._fboClassified = this._fboClassified && this._fboClassified.destroy();

    var depthStencilTexture;
    var depthStencilRenderbuffer;
    if (defined(this._previousFramebuffer)) {
      depthStencilTexture = this._previousFramebuffer.depthStencilTexture;
      depthStencilRenderbuffer = this._previousFramebuffer
        .depthStencilRenderbuffer;
    } else {
      depthStencilTexture = this._depthStencilTexture;
    }

    this._fbo = new Framebuffer({
      context: context,
      colorTextures: [this._texture],
      depthStencilTexture: depthStencilTexture,
      depthStencilRenderbuffer: depthStencilRenderbuffer,
      destroyAttachments: false,
    });

    if (!defined(this._previousFramebuffer)) {
      this._fboClassified = new Framebuffer({
        context: context,
        colorTextures: [this._classifiedTexture],
        depthStencilTexture: depthStencilTexture,
        destroyAttachments: false,
      });
    }
  }

  if (!defined(this._rsUnclassified)) {
    this._rsUnclassified = RenderState.fromCache(rsUnclassified);
    this._rsClassified = RenderState.fromCache(rsClassified);
    this._rsDefault = RenderState.fromCache(rsDefault);
  }

  if (!defined(this._unclassifiedCommand) || previousFramebufferChanged) {
    if (defined(this._unclassifiedCommand)) {
      this._unclassifiedCommand.shaderProgram =
        this._unclassifiedCommand.shaderProgram &&
        this._unclassifiedCommand.shaderProgram.destroy();
      this._classifiedCommand.shaderProgram =
        this._classifiedCommand.shaderProgram &&
        this._classifiedCommand.shaderProgram.destroy();
    }

    var fs = defined(this._previousFramebuffer) ? opaqueFS : translucentFS;
    var unclassifiedFSSource = new ShaderSource({
      defines: ["UNCLASSIFIED"],
      sources: [fs],
    });
    var classifiedFSSource = new ShaderSource({
      sources: [fs],
    });
    this._unclassifiedCommand = context.createViewportQuadCommand(
      unclassifiedFSSource,
      {
        renderState: defined(this._previousFramebuffer)
          ? this._rsUnclassified
          : this._rsDefault,
        uniformMap: this._uniformMap,
        owner: this,
      }
    );
    this._classifiedCommand = context.createViewportQuadCommand(
      classifiedFSSource,
      {
        renderState: defined(this._previousFramebuffer)
          ? this._rsClassified
          : this._rsDefault,
        uniformMap: this._uniformMap,
        owner: this,
      }
    );

    if (defined(this._translucentCommand)) {
      this._translucentCommand.shaderProgram =
        this._translucentCommand.shaderProgram &&
        this._translucentCommand.shaderProgram.destroy();
    }
    if (!defined(this._previousFramebuffer)) {
      this._translucentCommand = context.createViewportQuadCommand(
        PassThrough,
        {
          renderState: this._rsUnclassified,
          uniformMap: this._uniformMap,
          owner: this,
        }
      );
    }
  }
};

InvertClassification.prototype.clear = function (context, passState) {
  var framebuffer = passState.framebuffer;

  if (defined(this._previousFramebuffer)) {
    passState.framebuffer = this._fbo;
    this._clearColorCommand.execute(context, passState);
  } else {
    passState.framebuffer = this._fbo;
    this._clearCommand.execute(context, passState);
    passState.framebuffer = this._fboClassified;
    this._clearCommand.execute(context, passState);
  }

  passState.framebuffer = framebuffer;
};

InvertClassification.prototype.executeClassified = function (
  context,
  passState
) {
  if (!defined(this._previousFramebuffer)) {
    var framebuffer = passState.framebuffer;

    passState.framebuffer = this._fboClassified;
    this._translucentCommand.execute(context, passState);

    passState.framebuffer = framebuffer;
  }
  this._classifiedCommand.execute(context, passState);
};

InvertClassification.prototype.executeUnclassified = function (
  context,
  passState
) {
  this._unclassifiedCommand.execute(context, passState);
};

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

InvertClassification.prototype.destroy = function () {
  this._fbo = this._fbo && this._fbo.destroy();
  this._texture = this._texture && this._texture.destroy();
  this._depthStencilTexture =
    this._depthStencilTexture && this._depthStencilTexture.destroy();

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

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