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

#include <type_traits>

namespace maps::mrc::common {

using RotationIntType = std::underlying_type_t<Rotation>;

namespace {

const Exiv2::ExifKey& exifImageOrientation()
{
    static const Exiv2::ExifKey EXIF_IMAGE_ORIENTATION("Exif.Image.Orientation");
    return EXIF_IMAGE_ORIENTATION;
}

template<typename TByteSequenceOne, typename TByteSequenceTwo>
void copyExifDataWithoutImageOrientation(
        const TByteSequenceOne& imageFrom,
        TByteSequenceTwo& imageTo) try
{
    auto exifData = getExifData(imageFrom);
    const auto it = exifData.findKey(exifImageOrientation());
    if (it != exifData.end()) {
        exifData.erase(it);
    }
    setExifData(imageTo, exifData);
} catch (const Exiv2::AnyError&) {
    // no EXIF
} catch (const maps::Exception&) {
    // no EXIF
}

/**
 * @see https://www.impulseadventure.com/photo/exif-orientation.html
 *      0th Row      0th Column
 *   1  top          left side
 *   2  top          right side
 *   3  bottom       right side
 *   4  bottom       left side
 *   5  left side    top
 *   6  right side   top
 *   7  right side   bottom
 *   8  left side    bottom
 */

static const std::vector<ImageOrientation> IMAGE_ORIENTATIONS {
    {false, Rotation::CW_0},   // 1
    {true, Rotation::CW_0},    // 2
    {false, Rotation::CW_180}, // 3
    {true, Rotation::CW_180},  // 4
    {true, Rotation::CW_90},   // 5
    {false, Rotation::CW_90},  // 6
    {true, Rotation::CW_270},  // 7
    {false, Rotation::CW_270}, // 8
};

/*
 * Stackoverflow driven development:
 * https://stackoverflow.com/questions/16265673/rotate-image-by-90-180-or-270-degrees
 */

ImageBox flipHorizontal(const ImageBox& box, const Size& size)
{
    return ImageBox {
        size.width - box.maxX(), box.minY(),
        size.width - box.minX(), box.maxY(),
    };
}

cv::Mat flipHorizontal(const cv::Mat& mat)
{
    cv::Mat result;
    cv::flip(mat, result, 1);
    return result;
}

ImageBox rotate90cw(const ImageBox& box, const Size& size)
{
    return ImageBox {
        size.height - box.maxY(), box.minX(),
        size.height - box.minY(), box.maxX()
    };
}

cv::Mat rotate90cw(const cv::Mat& mat)
{
    cv::Mat result;
    cv::flip(mat.t(), result, 1);
    return result;
}

ImageBox rotate180cw(const ImageBox& box, const Size& size)
{
    return ImageBox {
        size.width - box.maxX(), size.height - box.maxY(),
        size.width - box.minX(), size.height - box.minY()
    };
}

cv::Mat rotate180cw(const cv::Mat& mat)
{
    cv::Mat result;
    cv::flip(mat, result, -1);
    return result;
}

ImageBox rotate270cw(const ImageBox& box, const Size& size)
{
    return ImageBox {
        box.minY(), size.width - box.maxX(),
        box.maxY(), size.width - box.minX()
    };
}

cv::Mat rotate270cw(const cv::Mat& mat)
{
    cv::Mat result;
    cv::flip(mat.t(), result, 0);
    return result;
}

inline ImageBox rotate90ccw(const ImageBox& box, const Size& size)
{
    return rotate270cw(box, size);
}

inline ImageBox rotate180ccw(const ImageBox& box, const Size& size)
{
    return rotate180cw(box, size);
}

inline ImageBox rotate270ccw(const ImageBox& box, const Size& size)
{
    return rotate90cw(box, size);
}

void requireBoxInImage(const ImageBox& box, const Size& imageSize)
{
    REQUIRE(
        box.maxX() <= imageSize.width && box.maxY() <= imageSize.height,
        "Box is out of image "  << box <<  ", image size = " << imageSize
    );
}

} // namespace

std::ostream& operator<<(std::ostream& out, Rotation rotation)
{
    return out << "CW_" << static_cast<std::underlying_type<Rotation>::type>(rotation);
}

std::istream& operator>>(std::istream& in, Rotation& rotation)
{
    RotationIntType value;
    in >> value;
    rotation = Rotation{value};
    return in;
}

Rotation operator+(Rotation lhs, Rotation rhs)
{
    constexpr RotationIntType FULL_TURN{360};

    RotationIntType result = static_cast<RotationIntType>(lhs)
        + static_cast<RotationIntType>(rhs);
    if (FULL_TURN <= result) {
       result -= FULL_TURN;
    }

    return Rotation{result};
}

ImageOrientation ImageOrientation::fromExif(int exifOrientation)
{
    REQUIRE(
        1 <= exifOrientation && exifOrientation <= 8,
        "Invalid exif orientation " << exifOrientation
    );

    return IMAGE_ORIENTATIONS.at(exifOrientation - 1);
}

ImageOrientation::operator int16_t() const
{
    const auto it = std::find(
        IMAGE_ORIENTATIONS.begin(), IMAGE_ORIENTATIONS.end(),
        *this
    );

    ASSERT(it != IMAGE_ORIENTATIONS.end());

    return it - IMAGE_ORIENTATIONS.begin() + 1;
}

ImageOrientation::operator int32_t() const
{
    return static_cast<int16_t>(*this);
}

template<typename TByteSequence>
std::optional<ImageOrientation> parseImageOrientationFromExif(const TByteSequence& encodedImage) try {
    auto exifData = getExifData(encodedImage);
    auto it = exifData.findKey(exifImageOrientation());

    if (it != exifData.end()) {
        return ImageOrientation::fromExif(static_cast<int>(it->toLong()));
    }

    return std::nullopt;
} catch (const Exiv2::AnyError&) {
    return std::nullopt;
} catch (const maps::Exception&) {
    return std::nullopt;
}

template std::optional<ImageOrientation> parseImageOrientationFromExif<std::string>(const std::string&);
template std::optional<ImageOrientation> parseImageOrientationFromExif<Bytes>(const Bytes&);

template<typename TByteSequence>
std::optional<std::string> parseModelFromExif(const TByteSequence& encodedImage) try
{
    static const Exiv2::ExifKey EXIF_MODEL("Exif.Image.Model");

    auto exifData = getExifData(encodedImage);
    auto it = exifData.findKey(EXIF_MODEL);

    if (it != exifData.end()) {
        return it->toString();
    }

    return std::nullopt;
} catch (const Exiv2::AnyError&) {
    return std::nullopt;
} catch (const maps::Exception&) {
    return std::nullopt;
}

template std::optional<std::string> parseModelFromExif<std::string>(const std::string&);
template std::optional<std::string> parseModelFromExif<Bytes>(const Bytes&);

Size transformByImageOrientation(const Size& size, const ImageOrientation& orientation)
{
    switch (orientation.rotation()) {
        case Rotation::CW_0:
        case Rotation::CW_180:
            return size;
        case Rotation::CW_90:
        case Rotation::CW_270:
            return Size{size.height, size.width};
    };
}

Size revertByImageOrientation(const Size& size, const ImageOrientation& orientation)
{
    switch (orientation.rotation()) {
        case Rotation::CW_0:
        case Rotation::CW_180:
            return size;
        case Rotation::CW_90:
        case Rotation::CW_270:
            return Size{size.height, size.width};
    };
}

cv::Mat transformByImageOrientation(
        const cv::Mat& image,
        const ImageOrientation& orientation)
{
    cv::Mat result;

    switch (orientation.rotation()) {
        case Rotation::CW_0:
            result = image;
            break;
        case Rotation::CW_90:
            result = rotate90cw(image);
            break;
        case Rotation::CW_180:
            result = rotate180cw(image);
            break;
        case Rotation::CW_270:
            result = rotate270cw(image);
            break;
    }

    return orientation.horizontalFlip() ? flipHorizontal(result) : result;
}

Bytes transformByImageOrientation(
        const Bytes& encodedImage,
        const ImageOrientation& orientation)
{
    Bytes result;

    if (orientation.isNormal()) {
        result = encodedImage;
    } else {
        cv::Mat image = decodeImage(encodedImage);
        image = transformByImageOrientation(image, orientation);
        result = encodeImage(image);
    }

    copyExifDataWithoutImageOrientation(encodedImage, result);

    return result;
}

Blob transformByImageOrientation(
        const Blob& encodedImage,
        const ImageOrientation& orientation)
{
    Blob result;

    if (orientation.isNormal()) {
        result = encodedImage;
    } else {
        cv::Mat image = decodeImage(encodedImage);
        image = transformByImageOrientation(image, orientation);
        result = toBlob(encodeImage(image));
    }

    copyExifDataWithoutImageOrientation(encodedImage, result);

    return result;
}

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

    switch (orientation.rotation()) {
        case Rotation::CW_0:
            break;
        case Rotation::CW_90:
            box = rotate90cw(box, originalImageSize);
            break;
        case Rotation::CW_180:
            box = rotate180cw(box, originalImageSize);
            break;
        case Rotation::CW_270:
            box = rotate270cw(box, originalImageSize);
            break;
    };

    const Size size = transformByImageOrientation(originalImageSize, orientation);
    return orientation.horizontalFlip() ? flipHorizontal(box, size) : box;
}

ImageBox revertByImageOrientation(
        ImageBox box,
        const Size& originalImageSize,
        const ImageOrientation& orientation)
{
    const Size size = transformByImageOrientation(originalImageSize, orientation);
    requireBoxInImage(box, size);

    if (orientation.horizontalFlip()) {
        box = flipHorizontal(box, size);
    }

    switch (orientation.rotation()) {
        case Rotation::CW_0:
            return box;
        case Rotation::CW_90:
            return rotate90ccw(box, size);
        case Rotation::CW_180:
            return rotate180ccw(box, size);
        case Rotation::CW_270:
            return rotate270ccw(box, size);
    };
}

} // namesapce maps::mrc::common
