import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import CesiumMath from "../Core/Math.js";
import ClearCommand from "../Renderer/ClearCommand.js";
import Framebuffer from "../Renderer/Framebuffer.js";
import Texture from "../Renderer/Texture.js";

/**
 * Creates a minimal amount of textures and framebuffers.
 *
 * @alias PostProcessStageTextureCache
 * @constructor
 *
 * @param {PostProcessStageCollection} postProcessStageCollection The post process collection.
 *
 * @private
 */
function PostProcessStageTextureCache(postProcessStageCollection) {
  this._collection = postProcessStageCollection;

  this._framebuffers = [];
  this._stageNameToFramebuffer = {};

  this._width = undefined;
  this._height = undefined;
  this._updateDependencies = false;
}

function getLastStageName(stage) {
  while (defined(stage.length)) {
    stage = stage.get(stage.length - 1);
  }
  return stage.name;
}

function getStageDependencies(
  collection,
  context,
  dependencies,
  stage,
  previousName
) {
  if (!stage.enabled || !stage._isSupported(context)) {
    return previousName;
  }

  var stageDependencies = (dependencies[stage.name] = {});
  if (defined(previousName)) {
    var previous = collection.getStageByName(previousName);
    stageDependencies[getLastStageName(previous)] = true;
  }
  var uniforms = stage.uniforms;
  if (defined(uniforms)) {
    var uniformNames = Object.getOwnPropertyNames(uniforms);
    var uniformNamesLength = uniformNames.length;
    for (var i = 0; i < uniformNamesLength; ++i) {
      var value = uniforms[uniformNames[i]];
      if (typeof value === "string") {
        var dependent = collection.getStageByName(value);
        if (defined(dependent)) {
          stageDependencies[getLastStageName(dependent)] = true;
        }
      }
    }
  }

  return stage.name;
}

function getCompositeDependencies(
  collection,
  context,
  dependencies,
  composite,
  previousName
) {
  if (
    (defined(composite.enabled) && !composite.enabled) ||
    (defined(composite._isSupported) && !composite._isSupported(context))
  ) {
    return previousName;
  }

  var originalDependency = previousName;

  var inSeries =
    !defined(composite.inputPreviousStageTexture) ||
    composite.inputPreviousStageTexture;
  var currentName = previousName;
  var length = composite.length;
  for (var i = 0; i < length; ++i) {
    var stage = composite.get(i);
    if (defined(stage.length)) {
      currentName = getCompositeDependencies(
        collection,
        context,
        dependencies,
        stage,
        previousName
      );
    } else {
      currentName = getStageDependencies(
        collection,
        context,
        dependencies,
        stage,
        previousName
      );
    }
    // Stages in a series only depend on the previous stage
    if (inSeries) {
      previousName = currentName;
    }
  }

  // Stages not in a series depend on every stage executed before it since it could reference it as a uniform.
  // This prevents looking at the dependencies of each stage in the composite, but might create more framebuffers than necessary.
  // In practice, there are only 2-3 stages in these composites.
  var j;
  var name;
  if (!inSeries) {
    for (j = 1; j < length; ++j) {
      name = getLastStageName(composite.get(j));
      var currentDependencies = dependencies[name];
      for (var k = 0; k < j; ++k) {
        currentDependencies[getLastStageName(composite.get(k))] = true;
      }
    }
  } else {
    for (j = 1; j < length; ++j) {
      name = getLastStageName(composite.get(j));
      if (!defined(dependencies[name])) {
        dependencies[name] = {};
      }
      dependencies[name][originalDependency] = true;
    }
  }

  return currentName;
}

function getDependencies(collection, context) {
  var dependencies = {};

  if (defined(collection.ambientOcclusion)) {
    var ao = collection.ambientOcclusion;
    var bloom = collection.bloom;
    var tonemapping = collection._tonemapping;
    var fxaa = collection.fxaa;

    var previousName = getCompositeDependencies(
      collection,
      context,
      dependencies,
      ao,
      undefined
    );
    previousName = getCompositeDependencies(
      collection,
      context,
      dependencies,
      bloom,
      previousName
    );
    previousName = getStageDependencies(
      collection,
      context,
      dependencies,
      tonemapping,
      previousName
    );
    previousName = getCompositeDependencies(
      collection,
      context,
      dependencies,
      collection,
      previousName
    );
    getStageDependencies(collection, context, dependencies, fxaa, previousName);
  } else {
    getCompositeDependencies(
      collection,
      context,
      dependencies,
      collection,
      undefined
    );
  }

  return dependencies;
}

function getFramebuffer(cache, stageName, dependencies) {
  var collection = cache._collection;
  var stage = collection.getStageByName(stageName);

  var textureScale = stage._textureScale;
  var forcePowerOfTwo = stage._forcePowerOfTwo;
  var pixelFormat = stage._pixelFormat;
  var pixelDatatype = stage._pixelDatatype;
  var clearColor = stage._clearColor;

  var i;
  var framebuffer;
  var framebuffers = cache._framebuffers;
  var length = framebuffers.length;
  for (i = 0; i < length; ++i) {
    framebuffer = framebuffers[i];

    if (
      textureScale !== framebuffer.textureScale ||
      forcePowerOfTwo !== framebuffer.forcePowerOfTwo ||
      pixelFormat !== framebuffer.pixelFormat ||
      pixelDatatype !== framebuffer.pixelDatatype ||
      !Color.equals(clearColor, framebuffer.clearColor)
    ) {
      continue;
    }

    var stageNames = framebuffer.stages;
    var stagesLength = stageNames.length;
    var foundConflict = false;
    for (var j = 0; j < stagesLength; ++j) {
      if (dependencies[stageNames[j]]) {
        foundConflict = true;
        break;
      }
    }

    if (!foundConflict) {
      break;
    }
  }

  if (defined(framebuffer) && i < length) {
    framebuffer.stages.push(stageName);
    return framebuffer;
  }

  framebuffer = {
    textureScale: textureScale,
    forcePowerOfTwo: forcePowerOfTwo,
    pixelFormat: pixelFormat,
    pixelDatatype: pixelDatatype,
    clearColor: clearColor,
    stages: [stageName],
    buffer: undefined,
    clear: undefined,
  };

  framebuffers.push(framebuffer);
  return framebuffer;
}

function createFramebuffers(cache, context) {
  var dependencies = getDependencies(cache._collection, context);
  for (var stageName in dependencies) {
    if (dependencies.hasOwnProperty(stageName)) {
      cache._stageNameToFramebuffer[stageName] = getFramebuffer(
        cache,
        stageName,
        dependencies[stageName]
      );
    }
  }
}

function releaseResources(cache) {
  var framebuffers = cache._framebuffers;
  var length = framebuffers.length;
  for (var i = 0; i < length; ++i) {
    var framebuffer = framebuffers[i];
    framebuffer.buffer = framebuffer.buffer && framebuffer.buffer.destroy();
    framebuffer.buffer = undefined;
  }
}

function updateFramebuffers(cache, context) {
  var width = cache._width;
  var height = cache._height;

  var framebuffers = cache._framebuffers;
  var length = framebuffers.length;
  for (var i = 0; i < length; ++i) {
    var framebuffer = framebuffers[i];

    var scale = framebuffer.textureScale;
    var textureWidth = Math.ceil(width * scale);
    var textureHeight = Math.ceil(height * scale);

    var size = Math.min(textureWidth, textureHeight);
    if (framebuffer.forcePowerOfTwo) {
      if (!CesiumMath.isPowerOfTwo(size)) {
        size = CesiumMath.nextPowerOfTwo(size);
      }
      textureWidth = size;
      textureHeight = size;
    }

    framebuffer.buffer = new Framebuffer({
      context: context,
      colorTextures: [
        new Texture({
          context: context,
          width: textureWidth,
          height: textureHeight,
          pixelFormat: framebuffer.pixelFormat,
          pixelDatatype: framebuffer.pixelDatatype,
        }),
      ],
    });
    framebuffer.clear = new ClearCommand({
      color: framebuffer.clearColor,
      framebuffer: framebuffer.buffer,
    });
  }
}

PostProcessStageTextureCache.prototype.updateDependencies = function () {
  this._updateDependencies = true;
};

/**
 * Called before the stages in the collection are executed. Creates the minimum amount of framebuffers for a post-process collection.
 *
 * @param {Context} context The context.
 */
PostProcessStageTextureCache.prototype.update = function (context) {
  var collection = this._collection;
  var updateDependencies = this._updateDependencies;
  var aoEnabled =
    defined(collection.ambientOcclusion) &&
    collection.ambientOcclusion.enabled &&
    collection.ambientOcclusion._isSupported(context);
  var bloomEnabled =
    defined(collection.bloom) &&
    collection.bloom.enabled &&
    collection.bloom._isSupported(context);
  var tonemappingEnabled =
    defined(collection._tonemapping) &&
    collection._tonemapping.enabled &&
    collection._tonemapping._isSupported(context);
  var fxaaEnabled =
    defined(collection.fxaa) &&
    collection.fxaa.enabled &&
    collection.fxaa._isSupported(context);
  var needsCheckDimensionsUpdate =
    !defined(collection._activeStages) ||
    collection._activeStages.length > 0 ||
    aoEnabled ||
    bloomEnabled ||
    tonemappingEnabled ||
    fxaaEnabled;
  if (
    updateDependencies ||
    (!needsCheckDimensionsUpdate && this._framebuffers.length > 0)
  ) {
    releaseResources(this);
    this._framebuffers.length = 0;
    this._stageNameToFramebuffer = {};
    this._width = undefined;
    this._height = undefined;
  }

  if (!updateDependencies && !needsCheckDimensionsUpdate) {
    return;
  }

  if (this._framebuffers.length === 0) {
    createFramebuffers(this, context);
  }

  var width = context.drawingBufferWidth;
  var height = context.drawingBufferHeight;
  var dimensionsChanged = this._width !== width || this._height !== height;
  if (!updateDependencies && !dimensionsChanged) {
    return;
  }

  this._width = width;
  this._height = height;
  this._updateDependencies = false;
  releaseResources(this);
  updateFramebuffers(this, context);
};

/**
 * Clears all of the framebuffers.
 *
 * @param {Context} context The context.
 */
PostProcessStageTextureCache.prototype.clear = function (context) {
  var framebuffers = this._framebuffers;
  for (var i = 0; i < framebuffers.length; ++i) {
    framebuffers[i].clear.execute(context);
  }
};

/**
 * Gets the stage with the given name.
 * @param {String} name The name of the stage.
 * @return {PostProcessStage|PostProcessStageComposite}
 */
PostProcessStageTextureCache.prototype.getStageByName = function (name) {
  return this._collection.getStageByName(name);
};

/**
 * Gets the output texture for a stage with the given name.
 * @param {String} name The name of the stage.
 * @return {Texture|undefined} The output texture of the stage with the given name.
 */
PostProcessStageTextureCache.prototype.getOutputTexture = function (name) {
  return this._collection.getOutputTexture(name);
};

/**
 * Gets the framebuffer for a stage with the given name.
 *
 * @param {String} name The name of the stage.
 * @return {Framebuffer|undefined} The framebuffer for the stage with the given name.
 */
PostProcessStageTextureCache.prototype.getFramebuffer = function (name) {
  var framebuffer = this._stageNameToFramebuffer[name];
  if (!defined(framebuffer)) {
    return undefined;
  }
  return framebuffer.buffer;
};

/**
 * 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 PostProcessStageTextureCache#destroy
 */
PostProcessStageTextureCache.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 PostProcessStageTextureCache#isDestroyed
 */
PostProcessStageTextureCache.prototype.destroy = function () {
  releaseResources(this);
  return destroyObject(this);
};
export default PostProcessStageTextureCache;
