/*
 * Copyright (c) 2026 Huawei Device Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <memory>
#include <tuple>
#include <utility>

#include "SkCodec.h"
#include "SkData.h"
#include "SkImage.h"
#include "SkRect.h"
#include "common.h"
#include "rawfile/raw_file_manager.h"
#include "resourcemanager/ohresmgr.h"

namespace {
constexpr KInt kDefaultScale = 0;
constexpr KInt kCropScale = 1;
constexpr KInt kFitScale = 2;
constexpr KInt kInsideScale = 3;
constexpr KInt kFillBoundsScale = 4;
constexpr KInt kFillWidthScale = 5;
constexpr KInt kFillHeightScale = 6;
constexpr KInt kNoneScale = 7;

struct TargetDecodePlan {
    int width;
    int height;
    bool crop;
    SkIRect subset;
};

void ReleaseMallocData(const void* data, void*) {
    std::free(const_cast<void*>(data));
}

int CeilToBucket(int value, int step) {
    if (value <= 0) return 0;
    const int safeStep = step > 0 ? step : 16;
    const int64_t bucket =
            ((static_cast<int64_t>(value) + safeStep - 1) / safeStep) * safeStep;
    return static_cast<int>(std::min<int64_t>(bucket, std::numeric_limits<int>::max()));
}

int CeilScaledDimension(int source, double scale) {
    if (!std::isfinite(scale) || scale <= 0.0) return 1;
    return std::max(1, static_cast<int>(std::ceil(std::min<double>(source * scale, source))));
}

bool BuildTargetDecodePlan(const SkImageInfo& sourceInfo,
                           KInt targetWidth,
                           KInt targetHeight,
                           KInt contentScaleMode,
                           KInt decodeSizeBucket,
                           TargetDecodePlan* result) {
    const int sourceWidth = sourceInfo.width();
    const int sourceHeight = sourceInfo.height();
    if (result == nullptr || sourceWidth <= 0 || sourceHeight <= 0 ||
            targetWidth <= 0 || targetHeight <= 0) {
        return false;
    }

    const double widthScale = static_cast<double>(targetWidth) / sourceWidth;
    const double heightScale = static_cast<double>(targetHeight) / sourceHeight;
    double scaleX = widthScale;
    double scaleY = heightScale;
    switch (contentScaleMode) {
        case kCropScale: {
            const double scale = std::max(widthScale, heightScale);
            scaleX = scale;
            scaleY = scale;
            break;
        }
        case kFitScale: {
            const double scale = std::min(widthScale, heightScale);
            scaleX = scale;
            scaleY = scale;
            break;
        }
        case kInsideScale: {
            const double scale = std::min(1.0, std::min(widthScale, heightScale));
            scaleX = scale;
            scaleY = scale;
            break;
        }
        case kFillWidthScale:
            scaleX = widthScale;
            scaleY = widthScale;
            break;
        case kFillHeightScale:
            scaleX = heightScale;
            scaleY = heightScale;
            break;
        case kNoneScale:
            scaleX = 1.0;
            scaleY = 1.0;
            break;
        case kDefaultScale:
        case kFillBoundsScale:
            break;
        default:
            return false;
    }

    const int requestedWidth = CeilScaledDimension(sourceWidth, scaleX);
    const int requestedHeight = CeilScaledDimension(sourceHeight, scaleY);
    int sampleSize = 1;
    while (sampleSize <= std::numeric_limits<int>::max() / 2 &&
            sourceWidth / (sampleSize * 2) >= requestedWidth &&
            sourceHeight / (sampleSize * 2) >= requestedHeight) {
        sampleSize *= 2;
    }
    const int sampledWidth = std::max(1, sourceWidth / sampleSize);
    const int sampledHeight = std::max(1, sourceHeight / sampleSize);
    const bool forceExactOutput =
            contentScaleMode == kCropScale || contentScaleMode == kFillBoundsScale;
    int outputWidth = forceExactOutput
            ? targetWidth
            : std::min(sampledWidth, CeilToBucket(targetWidth, decodeSizeBucket));
    int outputHeight = forceExactOutput
            ? targetHeight
            : std::min(sampledHeight, CeilToBucket(targetHeight, decodeSizeBucket));
    outputWidth = std::max(1, std::min(outputWidth, std::numeric_limits<int>::max()));
    outputHeight = std::max(1, std::min(outputHeight, std::numeric_limits<int>::max()));
    if (outputWidth >= sourceWidth && outputHeight >= sourceHeight) {
        outputWidth = sourceWidth;
        outputHeight = sourceHeight;
    }

    result->width = outputWidth;
    result->height = outputHeight;
    result->crop = contentScaleMode == kCropScale;
    result->subset = SkIRect::MakeWH(sourceWidth, sourceHeight);
    if (result->crop) {
        const double sourceRatio = static_cast<double>(sourceWidth) / sourceHeight;
        const double targetRatio = static_cast<double>(outputWidth) / outputHeight;
        if (sourceRatio > targetRatio) {
            const double cropWidth = sourceHeight * targetRatio;
            const int left = std::max(0, static_cast<int>((sourceWidth - cropWidth) / 2.0));
            const int right = std::min(sourceWidth, static_cast<int>(std::ceil(left + cropWidth)));
            result->subset = SkIRect::MakeLTRB(left, 0, std::max(left + 1, right), sourceHeight);
        } else {
            const double cropHeight = sourceWidth / targetRatio;
            const int top = std::max(0, static_cast<int>((sourceHeight - cropHeight) / 2.0));
            const int bottom = std::min(sourceHeight, static_cast<int>(std::ceil(top + cropHeight)));
            result->subset = SkIRect::MakeLTRB(0, top, sourceWidth, std::max(top + 1, bottom));
        }
    }
    return true;
}

sk_sp<SkData> MakeRawFileData(void* nativeResourceManager, const char* rawFilePath) {
    if (nativeResourceManager == nullptr || rawFilePath == nullptr || rawFilePath[0] == '\0') {
        return nullptr;
    }
    auto* manager = static_cast<NativeResourceManager*>(nativeResourceManager);
    RawFile* rawFile = OH_ResourceManager_OpenRawFile(manager, rawFilePath);
    if (rawFile == nullptr) return nullptr;

    RawFileDescriptor descriptor = {};
    if (OH_ResourceManager_GetRawFileDescriptorData(rawFile, &descriptor) &&
            descriptor.fd >= 0 && descriptor.start >= 0 && descriptor.length > 0) {
        sk_sp<SkData> hapData = SkData::MakeFromFD(descriptor.fd);
        const size_t start = static_cast<size_t>(descriptor.start);
        const size_t length = static_cast<size_t>(descriptor.length);
        const bool validRange = hapData != nullptr && start <= hapData->size() &&
                length <= hapData->size() - start;
        OH_ResourceManager_ReleaseRawFileDescriptorData(&descriptor);
        OH_ResourceManager_CloseRawFile(rawFile);
        if (validRange) {
            return SkData::MakeSubset(hapData.get(), start, length);
        }
        return nullptr;
    }

    const long rawFileSize = OH_ResourceManager_GetRawFileSize(rawFile);
    if (rawFileSize <= 0) {
        OH_ResourceManager_CloseRawFile(rawFile);
        return nullptr;
    }
    const size_t dataLength = static_cast<size_t>(rawFileSize);
    void* data = std::malloc(dataLength);
    if (data == nullptr) {
        OH_ResourceManager_CloseRawFile(rawFile);
        return nullptr;
    }
    const int bytesRead = OH_ResourceManager_ReadRawFile(rawFile, data, dataLength);
    OH_ResourceManager_CloseRawFile(rawFile);
    if (bytesRead <= 0 || static_cast<size_t>(bytesRead) != dataLength) {
        std::free(data);
        return nullptr;
    }
    return SkData::MakeWithProc(data, dataLength, ReleaseMallocData, nullptr);
}

sk_sp<SkData> MakeMediaData(void* nativeResourceManager, KInt mediaResId) {
    if (nativeResourceManager == nullptr || mediaResId <= 0) return nullptr;
    uint8_t* data = nullptr;
    uint64_t dataLength = 0;
    const auto error = OH_ResourceManager_GetMediaData(
            static_cast<NativeResourceManager*>(nativeResourceManager),
            static_cast<uint32_t>(mediaResId),
            &data,
            &dataLength,
            0);
    if (error != SUCCESS || data == nullptr || dataLength == 0 ||
            dataLength > std::numeric_limits<size_t>::max()) {
        std::free(data);
        return nullptr;
    }
    return SkData::MakeWithProc(
            data,
            static_cast<size_t>(dataLength),
            ReleaseMallocData,
            nullptr);
}

KNativePointer DecodeResourceImage(sk_sp<SkData> encodedData,
                                   KInt targetWidth,
                                   KInt targetHeight,
                                   KInt contentScaleMode,
                                   KInt decodeSizeBucket) {
    if (encodedData == nullptr) return 0;
    if (targetWidth == 0 && targetHeight == 0) {
        sk_sp<SkImage> image = SkImages::DeferredFromEncodedData(encodedData);
        return reinterpret_cast<KNativePointer>(image.release());
    }
    if (targetWidth <= 0 || targetHeight <= 0) return 0;
    std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(std::move(encodedData));
    if (codec == nullptr) return 0;

    const SkImageInfo sourceInfo = codec->getInfo();
    TargetDecodePlan plan = {};
    if (!BuildTargetDecodePlan(
            sourceInfo,
            targetWidth,
            targetHeight,
            contentScaleMode,
            decodeSizeBucket,
            &plan)) {
        return 0;
    }

    SkCodec::Options options;
    if (plan.crop) options.fSubset = &plan.subset;
    const SkImageInfo targetInfo = sourceInfo.makeWH(plan.width, plan.height);
    auto decoded = codec->getImage(targetInfo, plan.crop ? &options : nullptr);
    sk_sp<SkImage> image = std::move(std::get<0>(decoded));
    if (std::get<1>(decoded) != SkCodec::kSuccess || image == nullptr ||
            image->width() != plan.width || image->height() != plan.height) {
        return 0;
    }
    return reinterpret_cast<KNativePointer>(image.release());
}
}  // namespace

SKIKO_EXPORT KNativePointer org_jetbrains_skia_Image__1nMakeFromEncodedOhosTargetSize(
        KByte* encodedArray,
        KInt encodedLength,
        KInt desiredWidth,
        KInt desiredHeight,
        KInt cropX,
        KInt cropY,
        KInt cropWidth,
        KInt cropHeight) {
    if (encodedArray == nullptr || encodedLength <= 0 || desiredWidth <= 0 || desiredHeight <= 0) {
        return 0;
    }

    sk_sp<SkData> encodedData = SkData::MakeWithCopy(encodedArray, encodedLength);
    if (encodedData == nullptr) {
        return 0;
    }
    std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(std::move(encodedData));
    if (codec == nullptr) {
        return 0;
    }
    const SkImageInfo sourceInfo = codec->getInfo();
    const bool hasCrop = cropWidth > 0 && cropHeight > 0;
    const bool hasPartialCrop = cropX != 0 || cropY != 0 || cropWidth != 0 || cropHeight != 0;
    if (hasPartialCrop && !hasCrop) {
        return 0;
    }

    SkIRect subset;
    SkCodec::Options options;
    if (hasCrop) {
        if (cropX < 0 || cropY < 0 || cropX >= sourceInfo.width() || cropY >= sourceInfo.height() ||
                cropWidth > sourceInfo.width() - cropX || cropHeight > sourceInfo.height() - cropY) {
            return 0;
        }
        subset = SkIRect::MakeXYWH(cropX, cropY, cropWidth, cropHeight);
        options.fSubset = &subset;
    }

    const SkImageInfo targetInfo = sourceInfo.makeWH(desiredWidth, desiredHeight);
    auto decoded = codec->getImage(targetInfo, hasCrop ? &options : nullptr);
    sk_sp<SkImage> image = std::move(std::get<0>(decoded));
    if (std::get<1>(decoded) != SkCodec::kSuccess || image == nullptr ||
            image->width() != desiredWidth || image->height() != desiredHeight) {
        return 0;
    }

    return reinterpret_cast<KNativePointer>(image.release());
}

SKIKO_EXPORT KNativePointer org_jetbrains_skia_Image__1nMakeFromRawFileOhos(
        void* nativeResourceManager,
        const char* rawFilePath,
        KInt targetWidth,
        KInt targetHeight,
        KInt contentScaleMode,
        KInt decodeSizeBucket) {
    return DecodeResourceImage(
            MakeRawFileData(nativeResourceManager, rawFilePath),
            targetWidth,
            targetHeight,
            contentScaleMode,
            decodeSizeBucket);
}

SKIKO_EXPORT KNativePointer org_jetbrains_skia_Image__1nMakeFromMediaOhos(
        void* nativeResourceManager,
        KInt mediaResId,
        KInt targetWidth,
        KInt targetHeight,
        KInt contentScaleMode,
        KInt decodeSizeBucket) {
    return DecodeResourceImage(
            MakeMediaData(nativeResourceManager, mediaResId),
            targetWidth,
            targetHeight,
            contentScaleMode,
            decodeSizeBucket);
}
