//
// Copyright 2021 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// ImageMtl.cpp:
//    Implements the class methods for ImageMtl.
//

#include "libANGLE/renderer/metal/ImageMtl.h"

#include "common/debug.h"
#include "libANGLE/Context.h"
#include "libANGLE/Display.h"
#include "libANGLE/renderer/metal/ContextMtl.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"
#include "libANGLE/renderer/metal/RenderBufferMtl.h"
#include "libANGLE/renderer/metal/TextureMtl.h"

namespace rx
{

namespace
{
angle::FormatID intendedFormatForMTLTexture(id<MTLTexture> texture,
                                            const egl::AttributeMap &attribs)
{
    angle::FormatID angleFormatId = mtl::Format::MetalToAngleFormatID(texture.pixelFormat);
    if (angleFormatId == angle::FormatID::NONE)
    {
        return angle::FormatID::NONE;
    }

    const angle::Format *textureAngleFormat = &angle::Format::Get(angleFormatId);
    ASSERT(textureAngleFormat);

    GLenum sizedInternalFormat = textureAngleFormat->glInternalFormat;

    if (attribs.contains(EGL_TEXTURE_INTERNAL_FORMAT_ANGLE))
    {
        const GLenum internalFormat =
            static_cast<GLenum>(attribs.get(EGL_TEXTURE_INTERNAL_FORMAT_ANGLE));
        GLenum type       = gl::GetSizedInternalFormatInfo(sizedInternalFormat).type;
        const auto format = gl::Format(internalFormat, type);
        if (!format.valid())
        {
            return angle::FormatID::NONE;
        }

        sizedInternalFormat = format.info->sizedInternalFormat;
    }

    return angle::Format::InternalFormatToID(sizedInternalFormat);
}
}  // anonymous namespace

// TextureImageSiblingMtl implementation
TextureImageSiblingMtl::TextureImageSiblingMtl(EGLClientBuffer buffer,
                                               const egl::AttributeMap &attribs)
    : mBuffer(buffer), mAttribs(attribs), mGLFormat(GL_NONE)
{}

TextureImageSiblingMtl::~TextureImageSiblingMtl() {}

// Static
egl::Error TextureImageSiblingMtl::ValidateClientBuffer(const DisplayMtl *display,
                                                        EGLClientBuffer buffer,
                                                        const egl::AttributeMap &attribs)
{
    id<MTLTexture> texture = (__bridge id<MTLTexture>)(buffer);
    if (!texture || texture.device != display->getMetalDevice())
    {
        return egl::Error(EGL_BAD_ATTRIBUTE);
    }

    if (texture.textureType != MTLTextureType2D && texture.textureType != MTLTextureTypeCube &&
        texture.textureType != MTLTextureType2DArray)
    {
        return egl::Error(EGL_BAD_ATTRIBUTE);
    }

    angle::FormatID angleFormatId = intendedFormatForMTLTexture(texture, attribs);
    const mtl::Format &format     = display->getPixelFormat(angleFormatId);
    if (!format.valid())
    {
        return egl::Error(EGL_BAD_ATTRIBUTE, "Unrecognized format");
    }

    if (format.metalFormat != texture.pixelFormat)
    {
        return egl::Error(EGL_BAD_ATTRIBUTE, "Incompatible format");
    }

    unsigned textureArraySlice =
        static_cast<unsigned>(attribs.getAsInt(EGL_METAL_TEXTURE_ARRAY_SLICE_ANGLE, 0));
    if (texture.textureType != MTLTextureType2DArray && textureArraySlice > 0)
    {
        return egl::Error(EGL_BAD_ATTRIBUTE,
                          "Invalid texture type for non-zero texture array slice");
    }
    if (textureArraySlice >= texture.arrayLength)
    {
        std::ostringstream err;
        err << "Invalid texture array slice: " << textureArraySlice;
        return egl::Error(EGL_BAD_ATTRIBUTE, err.str());
    }

    return egl::NoError();
}

egl::Error TextureImageSiblingMtl::initialize(const egl::Display *display)
{
    DisplayMtl *displayMtl = mtl::GetImpl(display);
    if (initImpl(displayMtl) != angle::Result::Continue)
    {
        return egl::Error(EGL_BAD_PARAMETER);
    }

    return egl::NoError();
}

angle::Result TextureImageSiblingMtl::initImpl(DisplayMtl *displayMtl)
{
    mNativeTexture = mtl::Texture::MakeFromMetal((__bridge id<MTLTexture>)(mBuffer));

    if (mNativeTexture->textureType() == MTLTextureType2DArray)
    {
        mtl::TextureRef baseTexture = std::move(mNativeTexture);
        unsigned textureArraySlice =
            static_cast<unsigned>(mAttribs.getAsInt(EGL_METAL_TEXTURE_ARRAY_SLICE_ANGLE, 0));
        mNativeTexture =
            baseTexture->createSliceMipView(textureArraySlice, mtl::kZeroNativeMipLevel);
    }

    angle::FormatID angleFormatId = intendedFormatForMTLTexture(mNativeTexture->get(), mAttribs);
    mFormat                       = displayMtl->getPixelFormat(angleFormatId);

    if (mNativeTexture)
    {
        size_t resourceSize = EstimateTextureSizeInBytes(
            mFormat, mNativeTexture->widthAt0(), mNativeTexture->heightAt0(),
            mNativeTexture->depthAt0(), mNativeTexture->samples(), mNativeTexture->mipmapLevels());
        mNativeTexture->setEstimatedByteSize(resourceSize);
    }

    mGLFormat = gl::Format(mFormat.intendedAngleFormat().glInternalFormat);

    mRenderable = mFormat.getCaps().depthRenderable || mFormat.getCaps().colorRenderable;

    // Some formats are not filterable but renderable such as integer formats. In this case, treat
    // them as texturable as well.
    mTextureable = mFormat.getCaps().filterable || mRenderable;

    return angle::Result::Continue;
}

void TextureImageSiblingMtl::onDestroy(const egl::Display *display)
{
    mNativeTexture = nullptr;
}

gl::Format TextureImageSiblingMtl::getFormat() const
{
    return mGLFormat;
}

bool TextureImageSiblingMtl::isRenderable(const gl::Context *context) const
{
    return mRenderable;
}

bool TextureImageSiblingMtl::isTexturable(const gl::Context *context) const
{
    return mTextureable;
}

gl::Extents TextureImageSiblingMtl::getSize() const
{
    return mNativeTexture ? mNativeTexture->sizeAt0() : gl::Extents(0, 0, 0);
}

size_t TextureImageSiblingMtl::getSamples() const
{
    uint32_t samples = mNativeTexture ? mNativeTexture->samples() : 0;
    return samples > 1 ? samples : 0;
}

bool TextureImageSiblingMtl::isYUV() const
{
    // NOTE(hqle): not supporting YUV image yet.
    return false;
}

bool TextureImageSiblingMtl::hasProtectedContent() const
{
    return false;
}

// ImageMtl implementation
ImageMtl::ImageMtl(const egl::ImageState &state, const gl::Context *context) : ImageImpl(state) {}

ImageMtl::~ImageMtl() {}

void ImageMtl::onDestroy(const egl::Display *display)
{
    mNativeTexture = nullptr;
}

egl::Error ImageMtl::initialize(const egl::Display *display)
{
    if (mState.target == EGL_METAL_TEXTURE_ANGLE)
    {
        const TextureImageSiblingMtl *externalImageSibling =
            GetImplAs<TextureImageSiblingMtl>(GetAs<egl::ExternalImageSibling>(mState.source));

        mNativeTexture = externalImageSibling->getTexture();

        switch (mNativeTexture->textureType())
        {
            case MTLTextureType2D:
            case MTLTextureType2DArray:
                mImageTextureType = gl::TextureType::_2D;
                break;
            case MTLTextureTypeCube:
                mImageTextureType = gl::TextureType::CubeMap;
                break;
            default:
                UNREACHABLE();
        }

        mImageLevel = 0;
        mImageLayer = 0;
    }
    else
    {
        UNREACHABLE();
        return egl::Error(EGL_BAD_ACCESS);
    }

    return egl::NoError();
}

angle::Result ImageMtl::orphan(const gl::Context *context, egl::ImageSibling *sibling)
{
    if (sibling == mState.source)
    {
        mNativeTexture = nullptr;
    }

    return angle::Result::Continue;
}

}  // namespace rx
