import Cartesian2 from "../Core/Cartesian2.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 Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";

/**
 * A post process stage that will get the luminance value at each pixel and
 * uses parallel reduction to compute the average luminance in a 1x1 texture.
 * This texture can be used as input for tone mapping.
 *
 * @constructor
 * @private
 */
function AutoExposure() {
  this._uniformMap = undefined;
  this._command = undefined;

  this._colorTexture = undefined;
  this._depthTexture = undefined;

  this._ready = false;

  this._name = "czm_autoexposure";

  this._logDepthChanged = undefined;
  this._useLogDepth = undefined;

  this._framebuffers = undefined;
  this._previousLuminance = undefined;

  this._commands = undefined;
  this._clearCommand = undefined;

  this._minMaxLuminance = new Cartesian2();

  /**
   * Whether or not to execute this post-process stage when ready.
   *
   * @type {Boolean}
   */
  this.enabled = true;
  this._enabled = true;

  /**
   * The minimum value used to clamp the luminance.
   *
   * @type {Number}
   * @default 0.1
   */
  this.minimumLuminance = 0.1;

  /**
   * The maximum value used to clamp the luminance.
   *
   * @type {Number}
   * @default 10.0
   */
  this.maximumLuminance = 10.0;
}

Object.defineProperties(AutoExposure.prototype, {
  /**
   * Determines if this post-process stage is ready to be executed. A stage is only executed when both <code>ready</code>
   * and {@link AutoExposure#enabled} are <code>true</code>. A stage will not be ready while it is waiting on textures
   * to load.
   *
   * @memberof AutoExposure.prototype
   * @type {Boolean}
   * @readonly
   */
  ready: {
    get: function () {
      return this._ready;
    },
  },
  /**
   * The unique name of this post-process stage for reference by other stages.
   *
   * @memberof AutoExposure.prototype
   * @type {String}
   * @readonly
   */
  name: {
    get: function () {
      return this._name;
    },
  },

  /**
   * A reference to the texture written to when executing this post process stage.
   *
   * @memberof AutoExposure.prototype
   * @type {Texture}
   * @readonly
   * @private
   */
  outputTexture: {
    get: function () {
      var framebuffers = this._framebuffers;
      if (!defined(framebuffers)) {
        return undefined;
      }
      return framebuffers[framebuffers.length - 1].getColorTexture(0);
    },
  },
});

function destroyFramebuffers(autoexposure) {
  var framebuffers = autoexposure._framebuffers;
  if (!defined(framebuffers)) {
    return;
  }

  var length = framebuffers.length;
  for (var i = 0; i < length; ++i) {
    framebuffers[i].destroy();
  }
  autoexposure._framebuffers = undefined;

  autoexposure._previousLuminance.destroy();
  autoexposure._previousLuminance = undefined;
}

function createFramebuffers(autoexposure, context) {
  destroyFramebuffers(autoexposure);

  var width = autoexposure._width;
  var height = autoexposure._height;

  var pixelFormat = PixelFormat.RGBA;
  var pixelDatatype = context.halfFloatingPointTexture
    ? PixelDatatype.HALF_FLOAT
    : PixelDatatype.FLOAT;

  var length = Math.ceil(Math.log(Math.max(width, height)) / Math.log(3.0));
  var framebuffers = new Array(length);
  for (var i = 0; i < length; ++i) {
    width = Math.max(Math.ceil(width / 3.0), 1.0);
    height = Math.max(Math.ceil(height / 3.0), 1.0);
    framebuffers[i] = new Framebuffer({
      context: context,
      colorTextures: [
        new Texture({
          context: context,
          width: width,
          height: height,
          pixelFormat: pixelFormat,
          pixelDatatype: pixelDatatype,
          sampler: Sampler.NEAREST,
        }),
      ],
    });
  }

  var lastTexture = framebuffers[length - 1].getColorTexture(0);
  autoexposure._previousLuminance = new Framebuffer({
    context: context,
    colorTextures: [
      new Texture({
        context: context,
        width: lastTexture.width,
        height: lastTexture.height,
        pixelFormat: pixelFormat,
        pixelDatatype: pixelDatatype,
        sampler: Sampler.NEAREST,
      }),
    ],
  });

  autoexposure._framebuffers = framebuffers;
}

function destroyCommands(autoexposure) {
  var commands = autoexposure._commands;
  if (!defined(commands)) {
    return;
  }

  var length = commands.length;
  for (var i = 0; i < length; ++i) {
    commands[i].shaderProgram.destroy();
  }
  autoexposure._commands = undefined;
}

function createUniformMap(autoexposure, index) {
  var uniforms;
  if (index === 0) {
    uniforms = {
      colorTexture: function () {
        return autoexposure._colorTexture;
      },
      colorTextureDimensions: function () {
        return autoexposure._colorTexture.dimensions;
      },
    };
  } else {
    var texture = autoexposure._framebuffers[index - 1].getColorTexture(0);
    uniforms = {
      colorTexture: function () {
        return texture;
      },
      colorTextureDimensions: function () {
        return texture.dimensions;
      },
    };
  }

  uniforms.minMaxLuminance = function () {
    return autoexposure._minMaxLuminance;
  };
  uniforms.previousLuminance = function () {
    return autoexposure._previousLuminance.getColorTexture(0);
  };

  return uniforms;
}

function getShaderSource(index, length) {
  var source =
    "uniform sampler2D colorTexture; \n" +
    "varying vec2 v_textureCoordinates; \n" +
    "float sampleTexture(vec2 offset) { \n";

  if (index === 0) {
    source +=
      "    vec4 color = texture2D(colorTexture, v_textureCoordinates + offset); \n" +
      "    return czm_luminance(color.rgb); \n";
  } else {
    source +=
      "    return texture2D(colorTexture, v_textureCoordinates + offset).r; \n";
  }

  source += "}\n\n";

  source +=
    "uniform vec2 colorTextureDimensions; \n" +
    "uniform vec2 minMaxLuminance; \n" +
    "uniform sampler2D previousLuminance; \n" +
    "void main() { \n" +
    "    float color = 0.0; \n" +
    "    float xStep = 1.0 / colorTextureDimensions.x; \n" +
    "    float yStep = 1.0 / colorTextureDimensions.y; \n" +
    "    int count = 0; \n" +
    "    for (int i = 0; i < 3; ++i) { \n" +
    "        for (int j = 0; j < 3; ++j) { \n" +
    "            vec2 offset; \n" +
    "            offset.x = -xStep + float(i) * xStep; \n" +
    "            offset.y = -yStep + float(j) * yStep; \n" +
    "            if (offset.x < 0.0 || offset.x > 1.0 || offset.y < 0.0 || offset.y > 1.0) { \n" +
    "                continue; \n" +
    "            } \n" +
    "            color += sampleTexture(offset); \n" +
    "            ++count; \n" +
    "        } \n" +
    "    } \n" +
    "    if (count > 0) { \n" +
    "        color /= float(count); \n" +
    "    } \n";

  if (index === length - 1) {
    source +=
      "    float previous = texture2D(previousLuminance, vec2(0.5)).r; \n" +
      "    color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n" +
      "    color = previous + (color - previous) / (60.0 * 1.5); \n" +
      "    color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n";
  }

  source += "    gl_FragColor = vec4(color); \n" + "} \n";
  return source;
}

function createCommands(autoexposure, context) {
  destroyCommands(autoexposure);
  var framebuffers = autoexposure._framebuffers;
  var length = framebuffers.length;

  var commands = new Array(length);

  for (var i = 0; i < length; ++i) {
    commands[i] = context.createViewportQuadCommand(
      getShaderSource(i, length),
      {
        framebuffer: framebuffers[i],
        uniformMap: createUniformMap(autoexposure, i),
      }
    );
  }
  autoexposure._commands = commands;
}

/**
 * A function that will be called before execute. Used to clear any textures attached to framebuffers.
 * @param {Context} context The context.
 * @private
 */
AutoExposure.prototype.clear = function (context) {
  var framebuffers = this._framebuffers;
  if (!defined(framebuffers)) {
    return;
  }

  var clearCommand = this._clearCommand;
  if (!defined(clearCommand)) {
    clearCommand = this._clearCommand = new ClearCommand({
      color: new Color(0.0, 0.0, 0.0, 0.0),
      framebuffer: undefined,
    });
  }

  var length = framebuffers.length;
  for (var i = 0; i < length; ++i) {
    clearCommand.framebuffer = framebuffers[i];
    clearCommand.execute(context);
  }
};

/**
 * A function that will be called before execute. Used to create WebGL resources and load any textures.
 * @param {Context} context The context.
 * @private
 */
AutoExposure.prototype.update = function (context) {
  var width = context.drawingBufferWidth;
  var height = context.drawingBufferHeight;

  if (width !== this._width || height !== this._height) {
    this._width = width;
    this._height = height;

    createFramebuffers(this, context);
    createCommands(this, context);

    if (!this._ready) {
      this._ready = true;
    }
  }

  this._minMaxLuminance.x = this.minimumLuminance;
  this._minMaxLuminance.y = this.maximumLuminance;

  var framebuffers = this._framebuffers;
  var temp = framebuffers[framebuffers.length - 1];
  framebuffers[framebuffers.length - 1] = this._previousLuminance;
  this._commands[
    this._commands.length - 1
  ].framebuffer = this._previousLuminance;
  this._previousLuminance = temp;
};

/**
 * Executes the post-process stage. The color texture is the texture rendered to by the scene or from the previous stage.
 * @param {Context} context The context.
 * @param {Texture} colorTexture The input color texture.
 * @private
 */
AutoExposure.prototype.execute = function (context, colorTexture) {
  this._colorTexture = colorTexture;

  var commands = this._commands;
  if (!defined(commands)) {
    return;
  }

  var length = commands.length;
  for (var i = 0; i < length; ++i) {
    commands[i].execute(context);
  }
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 * <p>
 * If this object was destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
 * </p>
 *
 * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
 *
 * @see AutoExposure#destroy
 */
AutoExposure.prototype.isDestroyed = function () {
  return false;
};

/**
 * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
 * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
 * <p>
 * Once an object is destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
 * assign the return value (<code>undefined</code>) to the object as done in the example.
 * </p>
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 * @see AutoExposure#isDestroyed
 */
AutoExposure.prototype.destroy = function () {
  destroyFramebuffers(this);
  destroyCommands(this);
  return destroyObject(this);
};
export default AutoExposure;
