//
//  VulkanMatMul.cpp
//  MNN
//
//  Created by MNN on 2020/03/06.
//  Copyright © 2018, Alibaba Group Holding Limited
//

#include "VulkanMatMul.hpp"
#include "core/TensorUtils.hpp"
namespace MNN {

class TexturePosComputer {
public:
    TexturePosComputer(const Tensor* tensor) {
        mFormat = TensorUtils::getDescribe(tensor)->dimensionFormat;
        mNHWC = VulkanTensor::tensorShapeFormat(tensor);
    }
    ~ TexturePosComputer() {
        // Do nothing
    }

    // X, Y, rgba
    std::array<int, 3> computePos(int offset) const {
        std::array<int, 4> nhwcPos;
        if (mFormat == MNN_DATA_FORMAT_NHWC) {
            // NHWC
            nhwcPos[3] = offset % mNHWC[3];
            offset = (offset - nhwcPos[3]) / mNHWC[3];
            nhwcPos[2] = offset % mNHWC[2];
            offset = (offset - nhwcPos[2]) / mNHWC[2];
            nhwcPos[1] = offset % mNHWC[1];
            nhwcPos[0] = offset / mNHWC[1];
        } else {
            // NCHW
            nhwcPos[2] = offset % mNHWC[2];
            offset = offset / mNHWC[2];
            nhwcPos[1] = offset % mNHWC[1];
            offset = offset / mNHWC[1];
            nhwcPos[3] = offset % mNHWC[3];
            nhwcPos[0] = offset / mNHWC[3];
        }
//        MNN_PRINT("n, c, h, w: %d, %d, %d, %d\n", nhwcPos[0], nhwcPos[3], nhwcPos[1], nhwcPos[2]);
        std::array<int, 3> res;
        res[2] = nhwcPos[3] % 4;
        auto c4 = nhwcPos[3] / 4;
        res[0] = c4 * mNHWC[2] + nhwcPos[2];
        res[1] = nhwcPos[0] * mNHWC[1] + nhwcPos[1];
        return res;
    }
private:
    MNN_DATA_FORMAT mFormat;
    std::array<int, 4> mNHWC;
};

VulkanMatMul::Reorder::Reorder(const VulkanBackend* bn, bool transpose, bool revert) {
    std::vector<VkDescriptorType> types{
        VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
    };
    if (revert) {
        mFirst = bn->getPipeline("glsl_nc4hw4Tonchw_comp", types);
    } else {
        mFirst = bn->getPipeline("glsl_nchwTonc4hw4_comp", types);
    }
    mBufferBufferSet.reset(mFirst->createSet());
    mBackend = bn;
    mUnitBuffer.reset(new VulkanBuffer(bn->getMemoryPool(), false, sizeof(nchwBuffer), nullptr, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT));
    std::string imageShaderName = "glsl_packAsImage4x4";
    std::vector<VkDescriptorType> secondTypes{
        VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
        VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
    };

    if (revert) {
        secondTypes[0] = VK_DESCRIPTOR_TYPE_SAMPLER;
        imageShaderName = "glsl_unPackImage4x4";
    }
    if (transpose) {
        imageShaderName = imageShaderName + "_TRANSPOSE_comp";
    } else {
        imageShaderName = imageShaderName + "_comp";
    }
    mSecond = bn->getPipeline(imageShaderName, secondTypes);
    mImageBufferSet.reset(mSecond->createSet());
}

void VulkanMatMul::Reorder::encode(VkBuffer source, size_t sourceSize, VkBuffer middleBuffer, size_t middelBufferSize, const VulkanImage* dest, const VulkanCommandPool::Buffer* cmdBuffer, const VulkanMatMul::Reorder::nchwBuffer& buffer) {
    // First: nchw to nc4hw4
    auto ptr = (nchwBuffer*)mUnitBuffer->map();
    ::memcpy(ptr, &buffer, sizeof(buffer));
    mUnitBuffer->unmap();
    auto c = buffer.size[1];
    auto b = buffer.size[0];
    auto w = buffer.size[3];
    auto h = buffer.size[2];
    auto cDiv4 = UP_DIV(c, 4);
    mBufferBufferSet->writeBuffer(middleBuffer, 1, middelBufferSize);
    mBufferBufferSet->writeBuffer(source, 0, sourceSize, 0);
    mBufferBufferSet->writeBuffer(mUnitBuffer->buffer(), 2, mUnitBuffer->size());
    auto totalNumber = cDiv4 * w * h * b;

    mFirst->bind(cmdBuffer->get(), mBufferBufferSet->get());
    cmdBuffer->barrierSource(source, 0, sourceSize);
    vkCmdDispatch(cmdBuffer->get(), UP_DIV(totalNumber, 256), 1, 1);
    
    // Second: nc4hw4 to image2d
    dest->barrierWrite(cmdBuffer->get());
    mImageBufferSet->writeImage(dest->view(), mBackend->getCommonSampler()->get(), VK_IMAGE_LAYOUT_GENERAL, 0);
    mImageBufferSet->writeBuffer(middleBuffer, 1, middelBufferSize);
    mImageBufferSet->writeBuffer(mUnitBuffer->buffer(), 2, mUnitBuffer->size());
    mSecond->bind(cmdBuffer->get(), mImageBufferSet->get());
    cmdBuffer->barrierSource(middleBuffer, 0, middelBufferSize);
    auto totalSchedule = cDiv4 * w * h * UP_DIV(b, 4);
    vkCmdDispatch(cmdBuffer->get(), UP_DIV(totalSchedule, 256), 1, 1);
}
int VulkanMatMul::Reorder::computeMiddleBufferSize(int b, int h, int w, int c) const {
    auto cDiv4 = UP_DIV(c, 4);
    auto totalNumber = cDiv4 * w * h * b;
    return totalNumber * 4;
}
void VulkanMatMul::Reorder::revert(VkBuffer dest, size_t destSize, VkBuffer middleBuffer, size_t middelBufferSize, const VulkanImage* source, const VulkanCommandPool::Buffer* cmdBuffer, const VulkanMatMul::Reorder::nchwBuffer& buffer) {
    // First: nchw to nc4hw4
    auto ptr = (nchwBuffer*)mUnitBuffer->map();
    ::memcpy(ptr, &buffer, sizeof(buffer));
    mUnitBuffer->unmap();
    auto c = buffer.size[1];
    auto b = buffer.size[0];
    auto w = buffer.size[3];
    auto h = buffer.size[2];
    auto cDiv4 = UP_DIV(c, 4);
    // First: image2d to nc4hw4
    mImageBufferSet->writeImage(source->view(), mBackend->getCommonSampler()->get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0);
    mImageBufferSet->writeBuffer(middleBuffer, 1, middelBufferSize);
    mImageBufferSet->writeBuffer(mUnitBuffer->buffer(), 2, mUnitBuffer->size());
    mSecond->bind(cmdBuffer->get(), mImageBufferSet->get());
    auto totalSchedule = cDiv4 * w * h * UP_DIV(b, 4);
    source->barrierRead(cmdBuffer->get());
    // cmdBuffer->barrierImage(source->get(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
    vkCmdDispatch(cmdBuffer->get(), UP_DIV(totalSchedule, 256), 1, 1);

    // Second: nc4hw4 to nchw
    mBufferBufferSet->writeBuffer(middleBuffer, 1, middelBufferSize);
    mBufferBufferSet->writeBuffer(dest, 0, destSize, 0);
    mBufferBufferSet->writeBuffer(mUnitBuffer->buffer(), 2, mUnitBuffer->size());
    auto totalNumber = cDiv4 * w * h * b;
    mFirst->bind(cmdBuffer->get(), mBufferBufferSet->get());
    cmdBuffer->barrierSource(middleBuffer, 0, middelBufferSize);
    vkCmdDispatch(cmdBuffer->get(), UP_DIV(totalNumber, 256), 1, 1);
}

VulkanMatMul::VulkanMatMul(bool transposeA, bool transposeB, Backend* bn) : VulkanBasicExecution(bn) {
    mTransposeA = transposeA;
    mTransposeB = transposeB;
}

bool VulkanMatMul::encode(const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs,
            const VulkanCommandPool::Buffer *cmdBuffer, const MatMulInfo& info) {
    mTempBuffer.clear();
    mSets.clear();
    auto input0T = reinterpret_cast<VulkanTensor*>(inputs[0]->deviceId());
    auto input1T = reinterpret_cast<VulkanTensor*>(inputs[1]->deviceId());
    auto outputT = reinterpret_cast<VulkanTensor*>(outputs[0]->deviceId());
    if (input0T->imageSize() > 1 || input1T->imageSize() > 1 || outputT->imageSize() > 1) {
        // TODO: Copy to temp buffer
        return false;
    }
    auto types = std::vector<VkDescriptorType>{
        VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
        VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
        VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
    };
    const VulkanPipeline* mInputPipline = nullptr;
    const VulkanPipeline* mWeightPipline = nullptr;
    const VulkanPipeline* mOutputPipeline = nullptr;
    auto vkBn = static_cast<VulkanBackend*>(backend());
    int e = info.e;
    int l = info.l;
    int h = info.h;
    struct OffsetBuffer {
        ivec4 size;
        float transpose[16];
    };
    {
        // Pretreat B
        // Turn bStride(e, l, h) -> x,y + offset
//        MNN_PRINT("Compute B %d x %d:\n", inputs[1]->length(0), inputs[1]->length(1));
        TexturePosComputer comp(inputs[1]);
        auto offset0 = comp.computePos(info.offsetB);
        if (offset0[2] != 0) {
            return false;
        }
        auto offsetl = comp.computePos(info.bStride[1]);
        auto offseth = comp.computePos(info.bStride[2]);
        bool transposeB = offsetl[2] == 1;
        if (l == 1) {
            // TODO: Find better way
            transposeB = offseth[2] == 0;
        }
        int l_W = offsetl[0] * 4 + offsetl[2];
        int l_H = offsetl[1] * 4;
        int h_W = offseth[0] * 4 + offseth[2];
        int h_H = offseth[1] * 4;
//        MNN_PRINT("Compute B: %d x %d, %d-%d-%d,  %d-%d-%d\n", info.bStride[1], info.bStride[2], offsetl[0], offsetl[1], offsetl[2], offseth[0], offseth[1], offseth[2]);
        if (transposeB) {
            mWeightPipline = vkBn->getPipeline("glsl_matmul_input_TRANSPOSE_comp", types);
        } else {
            mWeightPipline = vkBn->getPipeline("glsl_matmul_input_comp", types);
        }
        mKernelImage.reset(new VulkanImage(vkBn->getDynamicMemoryPool(), false, {ALIGN_UP4(l), UP_DIV(h, 4)}));
        std::shared_ptr<VulkanPipeline::DescriptorSet> des(mWeightPipline->createSet());
        OffsetBuffer buffer;
        buffer.size[0] = UP_DIV(l, 4);
        buffer.size[1] = UP_DIV(h, 4);
        if (transposeB) {
            buffer.size[2] = h;
        } else {
            buffer.size[2] = l;
        }
        buffer.size[2] = buffer.size[2] + offset0[1];
        buffer.size[3] = UP_DIV(l, 4) * UP_DIV(h, 4);
        ::memset(buffer.transpose, 0, 16 * sizeof(float));
        buffer.transpose[4 * 0 + 0] = l_W;
        buffer.transpose[4 * 0 + 1] = h_W;
        buffer.transpose[4 * 0 + 2] = 0;
        buffer.transpose[4 * 0 + 3] = offset0[0];

        buffer.transpose[4 * 1 + 0] = l_H;
        buffer.transpose[4 * 1 + 1] = h_H;
        buffer.transpose[4 * 1 + 2] = 0;
        buffer.transpose[4 * 1 + 3] = offset0[1];
        std::shared_ptr<VulkanBuffer> uniformBuffer(new VulkanBuffer(vkBn->getMemoryPool(), false, sizeof(OffsetBuffer), &buffer, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT));
        des->writeBuffer(uniformBuffer->buffer(), 2, uniformBuffer->size());
        des->writeImage(mKernelImage->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_GENERAL, 0);
        des->writeImage(input1T->image()->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 1);
        mTempBuffer.emplace_back(uniformBuffer);
        mSets.emplace_back(des);
        mWeightPipline->bind(cmdBuffer->get(), des->get());

        input1T->image()->barrierRead(cmdBuffer->get());
        mKernelImage->barrierWrite(cmdBuffer->get());

        vkCmdDispatch(cmdBuffer->get(), UP_DIV(buffer.size[3], 256), 1, 1);
    }
    {
        // Pretreat A
        TexturePosComputer comp(inputs[0]);
        auto offset0 = comp.computePos(info.offsetA);
        if (offset0[2] != 0) {
            return false;
        }
        auto offsetl = comp.computePos(info.aStride[1]);
        auto offsete = comp.computePos(info.aStride[0]);
        int l_W = offsetl[0] * 4 + offsetl[2];
        int l_H = offsetl[1] * 4;
        int e_W = offsete[0] * 4 + offsete[2];
        int e_H = offsete[1] * 4;
        bool transposeA = offsetl[2] != 1;
        if (l == 1) {
            // TODO: Find better way
            transposeA = offsete[2] == 1;
        }
        if (!transposeA) {
            mInputPipline = vkBn->getPipeline("glsl_matmul_input_comp", types);
        } else {
            mInputPipline = vkBn->getPipeline("glsl_matmul_input_TRANSPOSE_comp", types);
        }
        mInputImage.reset(new VulkanImage(vkBn->getDynamicMemoryPool(), false, {ALIGN_UP4(l), UP_DIV(e, 4)}));
        std::shared_ptr<VulkanPipeline::DescriptorSet> des(mInputPipline->createSet());
        OffsetBuffer buffer;
        buffer.size[0] = UP_DIV(l, 4);
        buffer.size[1] = UP_DIV(e, 4);
        if (transposeA) {
            buffer.size[2] = l;
        } else {
            buffer.size[2] = e;
        }
        buffer.size[2] = buffer.size[2] + offset0[1];
        buffer.size[3] = UP_DIV(l, 4) * UP_DIV(e, 4);
        ::memset(buffer.transpose, 0, 16 * sizeof(float));
        buffer.transpose[4 * 0 + 0] = l_W;
        buffer.transpose[4 * 0 + 1] = e_W;
        buffer.transpose[4 * 0 + 2] = 0;
        buffer.transpose[4 * 0 + 3] = offset0[0];

        buffer.transpose[4 * 1 + 0] = l_H;
        buffer.transpose[4 * 1 + 1] = e_H;
        buffer.transpose[4 * 1 + 2] = 0;
        buffer.transpose[4 * 1 + 3] = offset0[1];
        std::shared_ptr<VulkanBuffer> uniformBuffer(new VulkanBuffer(vkBn->getMemoryPool(), false, sizeof(OffsetBuffer), &buffer, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT));
        des->writeBuffer(uniformBuffer->buffer(), 2, uniformBuffer->size());
        des->writeImage(mInputImage->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_GENERAL, 0);
        des->writeImage(input0T->image()->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 1);
        mTempBuffer.emplace_back(uniformBuffer);
        mSets.emplace_back(des);
        mInputPipline->bind(cmdBuffer->get(), des->get());

        input0T->image()->barrierRead(cmdBuffer->get());
        mInputImage->barrierWrite(cmdBuffer->get());

        vkCmdDispatch(cmdBuffer->get(), UP_DIV(buffer.size[3], 256), 1, 1);
    }
    mCore.reset(new VulkanMatrixMultier4x4(vkBn, nullptr, l, h, 1, mKernelImage));
    mOutputImage.reset(new VulkanImage(vkBn->getDynamicMemoryPool(), false, {ALIGN_UP4(h), UP_DIV(e, 4)}));
    mCore->prepare(cmdBuffer, e, mOutputImage, mInputImage);
    mCore->compute(cmdBuffer);
    mInputImage->release();
    mKernelImage->release();
    {
        // Posttreat C
        if (inputs.size() > 2) {
            auto ntypes = std::vector<VkDescriptorType>{
                VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
                VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
                VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            };
            mOutputPipeline = vkBn->getPipeline("glsl_matmul_output_BIAS_comp", ntypes);
        } else {
            mOutputPipeline = vkBn->getPipeline("glsl_matmul_output_comp", types);
        }
        TexturePosComputer comp(outputs[0]);
        auto offset0 = comp.computePos(info.offsetC);
        if (offset0[2] != 0) {
            return false;
        }
        auto offsete = comp.computePos(info.cStride[0]);
        auto offseth = comp.computePos(info.cStride[2]);

        std::shared_ptr<VulkanPipeline::DescriptorSet> des(mOutputPipeline->createSet());
        OffsetBuffer buffer;
        buffer.size[0] = UP_DIV(h, 4);
        buffer.size[1] = UP_DIV(e, 4);
        buffer.size[2] = e + offset0[1];
        buffer.size[3] = UP_DIV(h, 4) * UP_DIV(e, 4);
        ::memset(buffer.transpose, 0, 16 * sizeof(float));
        buffer.transpose[4 * 0 + 0] = 1;
        buffer.transpose[4 * 0 + 1] = 0;
        buffer.transpose[4 * 0 + 2] = 0;
        buffer.transpose[4 * 0 + 3] = offset0[0];

        buffer.transpose[4 * 1 + 0] = 0;
        buffer.transpose[4 * 1 + 1] = 4;
        buffer.transpose[4 * 1 + 2] = 0;
        buffer.transpose[4 * 1 + 3] = offset0[1];

        std::shared_ptr<VulkanBuffer> uniformBuffer(new VulkanBuffer(vkBn->getMemoryPool(), false, sizeof(OffsetBuffer), &buffer, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT));
        des->writeBuffer(uniformBuffer->buffer(), 2, uniformBuffer->size());
        des->writeImage(outputT->image()->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_GENERAL, 0);
        des->writeImage(mOutputImage->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 1);
        if (inputs.size() > 2) {
            float biasTranspose[16];
            std::shared_ptr<VulkanBuffer> biasuniformBuffer(new VulkanBuffer(vkBn->getMemoryPool(), false, sizeof(biasTranspose), &biasTranspose, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT));
            des->writeImage(reinterpret_cast<VulkanTensor*>(inputs[2]->deviceId())->image()->view(), vkBn->getCommonSampler()->get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 3);
            des->writeBuffer(biasuniformBuffer->buffer(), 4, 16 * sizeof(float));
            mTempBuffer.emplace_back(biasuniformBuffer);
        }
        mSets.emplace_back(des);
        mOutputPipeline->bind(cmdBuffer->get(), des->get());

        outputT->image()->barrierWrite(cmdBuffer->get());
        mOutputImage->barrierRead(cmdBuffer->get());
        vkCmdDispatch(cmdBuffer->get(), UP_DIV(buffer.size[3], 256), 1, 1);

        mTempBuffer.emplace_back(uniformBuffer);
    }
    mOutputImage->release();
    return true;
}

ErrorCode VulkanMatMul::onEncode(const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs,
                           const VulkanCommandPool::Buffer *cmdBuffer) {
    Tensor* C       = outputs[0];
    auto w0         = inputs[0]->length(1);
    auto h0         = inputs[0]->length(0);
    auto e = C->length(0);
    auto h = C->length(1);
    auto l = w0;
    if (mTransposeA) {
        l = h0;
    }
    MatMulInfo info;
    info.e = e;
    info.l = l;
    info.h = h;
    if (mTransposeA) {
        info.aStride[0] = 1;
        info.aStride[1] = e;
        info.aStride[2] = 0;
    } else {
        info.aStride[0] = l;
        info.aStride[1] = 1;
        info.aStride[2] = 0;
    }
    if (mTransposeB) {
        info.bStride[0] = 0;
        info.bStride[1] = 1;
        info.bStride[2] = l;
    } else {
        info.bStride[0] = 0;
        info.bStride[1] = h;
        info.bStride[2] = 1;
    }
    info.cStride[0] = h;
    info.cStride[1] = 0;
    info.cStride[2] = 1;
    auto res = encode(inputs, outputs, cmdBuffer, info);
    if (!res) {
        return NOT_SUPPORT;
    }
    return NO_ERROR;
}
class VulkanMatMulCreator : public VulkanBackend::Creator {
public:
    virtual VulkanBasicExecution* onCreate(const std::vector<Tensor*>& inputs, const std::vector<Tensor*>& outputs, const MNN::Op* op, Backend* bn) const override {
        Tensor* C       = outputs[0];
        auto w0         = inputs[0]->length(1);
        auto h0         = inputs[0]->length(0);
        auto e = C->length(0);
        auto h = C->length(1);
        auto l = w0;
        auto vkBn = static_cast<VulkanBackend*>(bn);
        const auto mat = op->main_as_MatMul();
        if (mat->transposeA()) {
            l = h0;
        }
        auto limit = vkBn->proty().limits.maxImageDimension2D;
        if (e > limit || h > limit || l > limit) {
            return nullptr;
        }
        return new VulkanMatMul(mat->transposeA(), mat->transposeB(), bn);
    }
};

static bool gResistor = []() {
    VulkanBackend::addCreator(OpType_MatMul, new VulkanMatMulCreator);
    return true;
}();

}
