/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "WebGLContext.h"

#include "GLContext.h"
#include "GLScreenBuffer.h"
#include "mozilla/Maybe.h"
#include "mozilla/Preferences.h"
#include "MozFramebuffer.h"
#include "nsString.h"
#include "WebGLBuffer.h"
#include "WebGLContextUtils.h"
#include "WebGLFramebuffer.h"
#include "WebGLProgram.h"
#include "WebGLRenderbuffer.h"
#include "WebGLShader.h"
#include "WebGLTexture.h"
#include "WebGLVertexArray.h"

namespace mozilla {

void WebGLContext::SetEnabled(const GLenum cap, const Maybe<GLuint> i,
                              const bool enabled) {
  const FuncScope funcScope(*this, "enable(i)/disable(i)");
  if (IsContextLost()) return;

  if (!ValidateCapabilityEnum(cap)) return;

  if (i) {
    if (cap != LOCAL_GL_BLEND) {
      ErrorInvalidEnumArg("cap", cap);
      return;
    }

    const auto limit = MaxValidDrawBuffers();
    if (*i >= limit) {
      ErrorInvalidValue("`index` (%u) must be < %s (%u)", *i,
                        "MAX_DRAW_BUFFERS", limit);
      return;
    }
  }

  const auto slot = GetStateTrackingSlot(cap, i ? *i : 0);
  if (slot) {
    *slot = enabled;
  } else if (cap == LOCAL_GL_BLEND) {
    if (i) {
      mBlendEnabled[*i] = enabled;
    } else {
      if (enabled) {
        mBlendEnabled.set();
      } else {
        mBlendEnabled.reset();
      }
    }
  }

  switch (cap) {
    case LOCAL_GL_DEPTH_TEST:
    case LOCAL_GL_STENCIL_TEST:
      break;  // Lazily applied, so don't tell GL yet or we will desync.

    default:
      // Non-lazy caps.
      if (i) {
        if (enabled) {
          gl->fEnablei(cap, *i);
        } else {
          gl->fDisablei(cap, *i);
        }
      } else {
        gl->SetEnabled(cap, enabled);
      }
      break;
  }
}

bool WebGLContext::GetStencilBits(GLint* const out_stencilBits) const {
  *out_stencilBits = 0;
  if (mBoundDrawFramebuffer) {
    if (!mBoundDrawFramebuffer->IsCheckFramebufferStatusComplete()) {
      // Error, we don't know which stencil buffer's bits to use
      ErrorInvalidFramebufferOperation(
          "getParameter: framebuffer has two stencil buffers bound");
      return false;
    }

    if (mBoundDrawFramebuffer->StencilAttachment().HasAttachment() ||
        mBoundDrawFramebuffer->DepthStencilAttachment().HasAttachment()) {
      *out_stencilBits = 8;
    }
  } else if (mOptions.stencil) {
    *out_stencilBits = 8;
  }

  return true;
}

Maybe<double> WebGLContext::GetParameter(const GLenum pname) {
  const FuncScope funcScope(*this, "getParameter");
  if (IsContextLost()) return {};

  if (IsWebGL2() || IsExtensionEnabled(WebGLExtensionID::WEBGL_draw_buffers)) {
    if (pname == LOCAL_GL_MAX_COLOR_ATTACHMENTS) {
      return Some(MaxValidDrawBuffers());

    } else if (pname == LOCAL_GL_MAX_DRAW_BUFFERS) {
      return Some(MaxValidDrawBuffers());

    } else if (pname >= LOCAL_GL_DRAW_BUFFER0 &&
               pname < GLenum(LOCAL_GL_DRAW_BUFFER0 + MaxValidDrawBuffers())) {
      const auto slotId = pname - LOCAL_GL_DRAW_BUFFER0;
      GLenum ret = LOCAL_GL_NONE;
      if (!mBoundDrawFramebuffer) {
        if (slotId == 0) {
          ret = mDefaultFB_DrawBuffer0;
        }
      } else {
        const auto& fb = *mBoundDrawFramebuffer;
        const auto& bs = fb.DrawBufferEnabled();
        if (bs[slotId]) {
          ret = LOCAL_GL_COLOR_ATTACHMENT0 + slotId;
        }
      }
      return Some(ret);
    }
  }

  if (IsExtensionEnabled(WebGLExtensionID::EXT_disjoint_timer_query)) {
    switch (pname) {
      case LOCAL_GL_TIMESTAMP_EXT: {
        uint64_t val = 0;
        if (Has64BitTimestamps()) {
          gl->fGetInteger64v(pname, (GLint64*)&val);
        } else {
          gl->fGetIntegerv(pname, (GLint*)&val);
        }
        // TODO: JS doesn't support 64-bit integers. Be lossy and
        // cast to double (53 bits)
        return Some(val);
      }

      case LOCAL_GL_GPU_DISJOINT_EXT: {
        realGLboolean val = false;  // Not disjoint by default.
        if (gl->IsExtensionSupported(gl::GLContext::EXT_disjoint_timer_query)) {
          gl->fGetBooleanv(pname, &val);
        }
        return Some(bool(val));
      }

      default:
        break;
    }
  }

  if (IsWebGL2() ||
      IsExtensionEnabled(WebGLExtensionID::OES_standard_derivatives)) {
    if (pname == LOCAL_GL_FRAGMENT_SHADER_DERIVATIVE_HINT) {
      GLint i = 0;
      gl->fGetIntegerv(pname, &i);
      return Some(i);
    }
  }

  if (IsExtensionEnabled(WebGLExtensionID::EXT_texture_filter_anisotropic)) {
    if (pname == LOCAL_GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT) {
      GLfloat f = 0.f;
      gl->fGetFloatv(pname, &f);
      return Some(f);
    }
  }

  if (IsExtensionEnabled(WebGLExtensionID::MOZ_debug)) {
    if (pname == dom::MOZ_debug_Binding::DOES_INDEX_VALIDATION) {
      return Some(mNeedsIndexValidation);
    }
  }

  switch (pname) {
    ////////////////////////////////
    // Single-value params

    // unsigned int
    case LOCAL_GL_CULL_FACE_MODE:
    case LOCAL_GL_FRONT_FACE:
    case LOCAL_GL_ACTIVE_TEXTURE:
    case LOCAL_GL_STENCIL_FUNC:
    case LOCAL_GL_STENCIL_FAIL:
    case LOCAL_GL_STENCIL_PASS_DEPTH_FAIL:
    case LOCAL_GL_STENCIL_PASS_DEPTH_PASS:
    case LOCAL_GL_STENCIL_BACK_FUNC:
    case LOCAL_GL_STENCIL_BACK_FAIL:
    case LOCAL_GL_STENCIL_BACK_PASS_DEPTH_FAIL:
    case LOCAL_GL_STENCIL_BACK_PASS_DEPTH_PASS:
    case LOCAL_GL_DEPTH_FUNC:
    case LOCAL_GL_BLEND_SRC_RGB:
    case LOCAL_GL_BLEND_SRC_ALPHA:
    case LOCAL_GL_BLEND_DST_RGB:
    case LOCAL_GL_BLEND_DST_ALPHA:
    case LOCAL_GL_BLEND_EQUATION_RGB:
    case LOCAL_GL_BLEND_EQUATION_ALPHA: {
      GLint i = 0;
      gl->fGetIntegerv(pname, &i);
      return Some(i);
    }

    case LOCAL_GL_GENERATE_MIPMAP_HINT:
      return Some(mGenerateMipmapHint);

    case LOCAL_GL_IMPLEMENTATION_COLOR_READ_FORMAT:
    case LOCAL_GL_IMPLEMENTATION_COLOR_READ_TYPE: {
      const webgl::FormatUsageInfo* usage;
      uint32_t width, height;
      if (!BindCurFBForColorRead(&usage, &width, &height,
                                 LOCAL_GL_INVALID_OPERATION))
        return Nothing();

      const auto implPI = ValidImplementationColorReadPI(usage);

      GLenum ret;
      if (pname == LOCAL_GL_IMPLEMENTATION_COLOR_READ_FORMAT) {
        ret = implPI.format;
      } else {
        ret = implPI.type;
      }
      return Some(ret);
    }

    // int
    case LOCAL_GL_STENCIL_REF:
    case LOCAL_GL_STENCIL_BACK_REF: {
      GLint stencilBits = 0;
      if (!GetStencilBits(&stencilBits)) return Nothing();

      // Assuming stencils have 8 bits
      const GLint stencilMask = (1 << stencilBits) - 1;

      GLint refValue = 0;
      gl->fGetIntegerv(pname, &refValue);

      return Some(refValue & stencilMask);
    }

    case LOCAL_GL_SAMPLE_BUFFERS:
    case LOCAL_GL_SAMPLES: {
      const auto& fb = mBoundDrawFramebuffer;
      auto samples = [&]() -> Maybe<uint32_t> {
        if (!fb) {
          if (!EnsureDefaultFB()) return Nothing();
          return Some(mDefaultFB->mSamples);
        }

        if (!fb->IsCheckFramebufferStatusComplete()) return Some(0);

        DoBindFB(fb, LOCAL_GL_FRAMEBUFFER);
        return Some(gl->GetIntAs<uint32_t>(LOCAL_GL_SAMPLES));
      }();
      if (samples && pname == LOCAL_GL_SAMPLE_BUFFERS) {
        samples = Some(uint32_t(bool(samples.value())));
      }
      if (!samples) return Nothing();
      return Some(samples.value());
    }

    case LOCAL_GL_STENCIL_CLEAR_VALUE:
    case LOCAL_GL_SUBPIXEL_BITS: {
      GLint i = 0;
      gl->fGetIntegerv(pname, &i);
      return Some(i);
    }

    case LOCAL_GL_RED_BITS:
    case LOCAL_GL_GREEN_BITS:
    case LOCAL_GL_BLUE_BITS:
    case LOCAL_GL_ALPHA_BITS:
    case LOCAL_GL_DEPTH_BITS:
    case LOCAL_GL_STENCIL_BITS: {
      const auto format = [&]() -> const webgl::FormatInfo* {
        const auto& fb = mBoundDrawFramebuffer;
        if (fb) {
          if (!fb->IsCheckFramebufferStatusComplete()) return nullptr;

          const auto& attachment = [&]() -> const auto& {
            switch (pname) {
              case LOCAL_GL_DEPTH_BITS:
                if (fb->DepthStencilAttachment().HasAttachment())
                  return fb->DepthStencilAttachment();
                return fb->DepthAttachment();

              case LOCAL_GL_STENCIL_BITS:
                if (fb->DepthStencilAttachment().HasAttachment())
                  return fb->DepthStencilAttachment();
                return fb->StencilAttachment();

              default:
                return fb->ColorAttachment0();
            }
          }
          ();

          const auto imageInfo = attachment.GetImageInfo();
          if (!imageInfo) return nullptr;
          return imageInfo->mFormat->format;
        }

        auto effFormat = webgl::EffectiveFormat::RGB8;
        switch (pname) {
          case LOCAL_GL_DEPTH_BITS:
            if (mOptions.depth) {
              effFormat = webgl::EffectiveFormat::DEPTH24_STENCIL8;
            }
            break;

          case LOCAL_GL_STENCIL_BITS:
            if (mOptions.stencil) {
              effFormat = webgl::EffectiveFormat::DEPTH24_STENCIL8;
            }
            break;

          default:
            if (mOptions.alpha) {
              effFormat = webgl::EffectiveFormat::RGBA8;
            }
            break;
        }
        return webgl::GetFormat(effFormat);
      }();
      int32_t ret = 0;
      if (format) {
        switch (pname) {
          case LOCAL_GL_RED_BITS:
            ret = format->r;
            break;
          case LOCAL_GL_GREEN_BITS:
            ret = format->g;
            break;
          case LOCAL_GL_BLUE_BITS:
            ret = format->b;
            break;
          case LOCAL_GL_ALPHA_BITS:
            ret = format->a;
            break;
          case LOCAL_GL_DEPTH_BITS:
            ret = format->d;
            break;
          case LOCAL_GL_STENCIL_BITS:
            ret = format->s;
            break;
        }
      }
      return Some(ret);
    }

    case LOCAL_GL_MAX_RENDERBUFFER_SIZE:
      return Some(mGLMaxRenderbufferSize);

    case LOCAL_GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS:
      return Some(mGLMaxVertexTextureImageUnits);

    case LOCAL_GL_MAX_TEXTURE_IMAGE_UNITS:
      return Some(mGLMaxFragmentTextureImageUnits);

    case LOCAL_GL_MAX_VERTEX_UNIFORM_VECTORS:
      return Some(mGLMaxVertexUniformVectors);

    case LOCAL_GL_MAX_FRAGMENT_UNIFORM_VECTORS:
      return Some(mGLMaxFragmentUniformVectors);

    case LOCAL_GL_MAX_VARYING_VECTORS:
      return Some(mGLMaxFragmentInputVectors);

    // unsigned int. here we may have to return very large values like 2^32-1
    // that can't be represented as javascript integer values. We just return
    // them as doubles and javascript doesn't care.
    case LOCAL_GL_STENCIL_BACK_VALUE_MASK:
      return Some(mStencilValueMaskBack);
      // pass as FP value to allow large values such as 2^32-1.

    case LOCAL_GL_STENCIL_BACK_WRITEMASK:
      return Some(mStencilWriteMaskBack);

    case LOCAL_GL_STENCIL_VALUE_MASK:
      return Some(mStencilValueMaskFront);

    case LOCAL_GL_STENCIL_WRITEMASK:
      return Some(mStencilWriteMaskFront);

    case LOCAL_GL_COLOR_WRITEMASK:
      return Some(mColorWriteMask0);

    // float
    case LOCAL_GL_LINE_WIDTH:
      return Some((double)mLineWidth);

    case LOCAL_GL_DEPTH_CLEAR_VALUE:
    case LOCAL_GL_POLYGON_OFFSET_FACTOR:
    case LOCAL_GL_POLYGON_OFFSET_UNITS:
    case LOCAL_GL_SAMPLE_COVERAGE_VALUE: {
      GLfloat f = 0.f;
      gl->fGetFloatv(pname, &f);
      return Some(f);
    }

    // bool
    case LOCAL_GL_DEPTH_TEST:
      return Some((bool)mDepthTestEnabled);
    case LOCAL_GL_STENCIL_TEST:
      return Some((bool)mStencilTestEnabled);

    case LOCAL_GL_BLEND:
    case LOCAL_GL_CULL_FACE:
    case LOCAL_GL_DITHER:
    case LOCAL_GL_POLYGON_OFFSET_FILL:
    case LOCAL_GL_SCISSOR_TEST:
    case LOCAL_GL_SAMPLE_COVERAGE_INVERT:
    case LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE:
    case LOCAL_GL_SAMPLE_COVERAGE:
    case LOCAL_GL_DEPTH_WRITEMASK: {
      realGLboolean b = 0;
      gl->fGetBooleanv(pname, &b);
      return Some(bool(b));
    }

    default:
      break;
  }

  ErrorInvalidEnumInfo("pname", pname);
  return Nothing();
}

bool WebGLContext::IsEnabled(GLenum cap) {
  const FuncScope funcScope(*this, "isEnabled");
  if (IsContextLost()) return false;

  if (!ValidateCapabilityEnum(cap)) return false;

  const auto& slot = GetStateTrackingSlot(cap, 0);
  if (slot) return *slot;

  return gl->fIsEnabled(cap);
}

bool WebGLContext::ValidateCapabilityEnum(GLenum cap) {
  switch (cap) {
    case LOCAL_GL_BLEND:
    case LOCAL_GL_CULL_FACE:
    case LOCAL_GL_DEPTH_TEST:
    case LOCAL_GL_DITHER:
    case LOCAL_GL_POLYGON_OFFSET_FILL:
    case LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE:
    case LOCAL_GL_SAMPLE_COVERAGE:
    case LOCAL_GL_SCISSOR_TEST:
    case LOCAL_GL_STENCIL_TEST:
      return true;
    case LOCAL_GL_RASTERIZER_DISCARD:
      return IsWebGL2();
    default:
      ErrorInvalidEnumInfo("cap", cap);
      return false;
  }
}

bool* WebGLContext::GetStateTrackingSlot(GLenum cap, GLuint i) {
  switch (cap) {
    case LOCAL_GL_DEPTH_TEST:
      return &mDepthTestEnabled;
    case LOCAL_GL_DITHER:
      return &mDitherEnabled;
    case LOCAL_GL_RASTERIZER_DISCARD:
      return &mRasterizerDiscardEnabled;
    case LOCAL_GL_SCISSOR_TEST:
      return &mScissorTestEnabled;
    case LOCAL_GL_STENCIL_TEST:
      return &mStencilTestEnabled;
  }

  return nullptr;
}

}  // namespace mozilla
