#pragma once

#include <maps/libs/introspection/include/comparison.h>
#include <maps/libs/introspection/include/stream_output.h>

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/image_box.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/types.h>

#include <exiv2.hpp>
#include <opencv2/opencv.hpp>

#include <cstdint>
#include <optional>
#include <ostream>
#include <string>
#include <tuple>
#include <type_traits>

namespace maps::mrc::common {

enum class Rotation: std::uint16_t {
    CW_0 = 0,
    CW_90 = 90,
    CW_180 = 180,
    CW_270 = 270,
    CCW_0 = CW_0,
    CCW_90 = CW_270,
    CCW_180 = CW_180,
    CCW_270 = CW_90,
};

std::ostream& operator<<(std::ostream& out, Rotation rotation);

std::istream& operator>>(std::istream& in, Rotation& rotation);

Rotation operator+(Rotation lhs, Rotation rhs);

// Transformation that is needed to restore true image orientation.
// The rotation is applied first, then flipping.
class ImageOrientation {

public:
    ImageOrientation()
        : horizontalFlip_(false)
        , rotation_(Rotation::CW_0)
    {}

    ImageOrientation(Rotation rotation)
        : horizontalFlip_(false)
        , rotation_(rotation)
    {}

    ImageOrientation(bool horizontalFlip, Rotation rotation)
        : horizontalFlip_(horizontalFlip)
        , rotation_(rotation)
    {}

    static ImageOrientation fromExif(int exifOrientation);

    explicit operator int32_t() const;

    explicit operator int16_t() const;

    // Need flip to restore orientation
    bool horizontalFlip() const { return horizontalFlip_; }

    // Need rotate to restore orientation
    Rotation rotation() const { return rotation_; }

    bool isNormal() const
    {
        return !horizontalFlip_ && rotation_ == Rotation::CW_0;
    }

    auto introspect() const
    {
        return std::tie(horizontalFlip_, rotation_);
    }

private:
    bool horizontalFlip_;
    Rotation rotation_;
};

using introspection::operator==;
using introspection::operator!=;
using introspection::operator<<;
using introspection::operator<;

template<typename TByteSequence>
std::optional<ImageOrientation> parseImageOrientationFromExif(const TByteSequence& encodedImage);

template<typename TByteSequence>
std::optional<std::string> parseModelFromExif(const TByteSequence& encodedImage);

Size transformByImageOrientation(
        const Size& imageSize,
        const ImageOrientation& orientation);

Size revertByImageOrientation(
        const Size& imageSize,
        const ImageOrientation& orientation);

ImageBox transformByImageOrientation(
        ImageBox box,
        const Size& originalImageSize,
        const ImageOrientation& orientation);

ImageBox revertByImageOrientation(
        ImageBox box,
        const Size& originalImageSize,
        const ImageOrientation& orientation);

// Return original image or new transformed version
cv::Mat transformByImageOrientation(
        const cv::Mat& image,
        const ImageOrientation& orientation);

// Return original image or new transformed version.
Blob transformByImageOrientation(
        const Blob& encodedImage,
        const ImageOrientation& orientation);

// Return original image or new transformed version.
Bytes transformByImageOrientation(
        const Bytes& encodedImage,
        const ImageOrientation& orientation);

template <class TBytes>
Exiv2::ExifData getExifData(const TBytes& imageData)
{
    const auto image = Exiv2::ImageFactory::open(
        reinterpret_cast<const Exiv2::byte*>(imageData.data()),
        imageData.size());

    image->readMetadata();
    return image->exifData();
}

template <class TBytes>
void setExifData(TBytes& imageData, const Exiv2::ExifData& exifData)
{
    auto image = Exiv2::ImageFactory::open(
        reinterpret_cast<const Exiv2::byte*>(imageData.data()),
        imageData.size());

    image->setExifData(exifData);
    image->writeMetadata();

    auto len = image->io().size();
    image->io().seek(0, Exiv2::BasicIo::beg);

    auto buf = image->io().read(len);
    imageData.assign(buf.pData_, buf.pData_ + len);
}

} // namespace maps::mrc::common
