#pragma once

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

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/device_attrs.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/url_context.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/txn_id.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/sql_chemistry/include/gateway_access.h>

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/point.h>

#include <maps/libs/enum_io/include/enum_io_fwd.h>
#include <maps/libs/json/include/value.h>

#include <contrib/libs/eigen/Eigen/Geometry>

#include <optional>
#include <utility>
#include <string>
#include <string_view>
#include <vector>

namespace maps::mrc::db::eye {

enum class FrameSource { Mrc, Panorama };

DECLARE_ENUM_IO(FrameSource);

class DeviceAttrs;

class Device {

public:
    Device(const DeviceAttrs& attrs);

    TId id() const { return id_; }

    TId txnId() const { return txnId_; }

    DeviceAttrs attrs() const;

private:
    friend class sql_chemistry::GatewayAccess<Device>;
    friend class TxnIdAccess<Device>;

    Device() {};

    Device& setTxnId(TId txnId) {
        txnId_ = txnId;
        return *this;
    }

    template <typename T>
    static auto introspect(T& t)
    {
        return std::tie(t.id_, t.txnId_, t.attrs_);
    }

    TId id_;
    TId txnId_;
    json::Value attrs_{json::null};

public:
    auto introspect() const { return introspect(*this); }
};

using Devices = std::vector<Device>;
class UrlContext;

class Frame {

public:
    Frame(
            TId deviceId,
            const common::ImageOrientation& orientation,
            const UrlContext& urlContext,
            const common::Size& originalSize,
            chrono::TimePoint time);

    TId id() const { return id_; }

    TId txnId() const { return txnId_; }

    TId deviceId() const { return deviceId_; }

    bool deleted() const { return deleted_; }

    common::Size originalSize() const
    {
        return {static_cast<size_t>(width_), static_cast<size_t>(height_)};
    }

    common::Size size() const
    {
        return common::transformByImageOrientation(originalSize(), orientation());
    }

    common::ImageOrientation orientation() const {
        return common::ImageOrientation::fromExif(orientation_);
    }

    UrlContext urlContext() const;

    chrono::TimePoint time() const { return time_; }

    Frame& setDeleted(bool flag)
    {
        deleted_ = flag;
        return *this;
    }

    Frame& setOrientation(const common::ImageOrientation& orientation)
    {
        orientation_ = static_cast<int>(orientation);
        return *this;
    }

    Frame& setOriginalSize(const common::Size& size)
    {
        width_ = size.width;
        height_ = size.height;

        return *this;
    }

    Frame& setUrlContext(const UrlContext& urlContext);

private:
    friend class sql_chemistry::GatewayAccess<Frame>;
    friend class TxnIdAccess<Frame>;

    Frame() {};

    Frame& setTxnId(TId txnId) {
        txnId_ = txnId;
        return * this;
    }

    template <typename T>
    static auto introspect(T& t) {
        return std::tie(
            t.id_,
            t.txnId_,
            t.deviceId_,
            t.deleted_,
            t.orientation_,
            t.urlContext_,
            t.width_,
            t.height_,
            t.time_
        );
    }

    TId id_;
    TId txnId_;
    TId deviceId_;
    bool deleted_;
    int16_t orientation_;
    json::Value urlContext_{json::null};
    int16_t width_;
    int16_t height_;
    chrono::TimePoint time_;

public:
    auto introspect() const { return introspect(*this); }
};

using Frames = std::vector<Frame>;

class FrameLocation {

public:
    FrameLocation(
            TId frameId,
            const geolib3::Point2& mercatorPos,
            const Eigen::Quaterniond& rotation)
        : frameId_(frameId)
        , txnId_(0)
    {
        setMercatorPos(mercatorPos);
        setRotation(rotation);
    }

    FrameLocation(
            TId frameId,
            const geolib3::Point2& mercatorPos,
            const Eigen::Quaterniond& rotation,
            const Eigen::Vector3d& move)
        : FrameLocation(frameId, mercatorPos, rotation)
    {
        setMove(move);
    }

    TId frameId() const { return frameId_; }

    TId txnId() const { return txnId_; }

    const geolib3::Point2& mercatorPos() const { return mercatorPos_; }

    geolib3::Point2 geodeticPos() const { return geolib3::convertMercatorToGeodetic(mercatorPos()); }

    bool hasMove() const { return move_.has_value(); }

    Eigen::Vector3d move() const
    {
        REQUIRE(hasMove(), "Frame location has no move!");
        return Eigen::Vector3d(move_->data());
    }

    Eigen::Quaterniond rotation() const {
        return Eigen::Quaterniond{rotation_.data()};
    }

    FrameLocation& setMercatorPos(const geolib3::Point2& pos)
    {
        mercatorPos_ = pos;
        return * this;
    }

    FrameLocation& setGeodeticPos(const geolib3::Point2 pos)
    {
        return setMercatorPos(geolib3::convertGeodeticToMercator(pos));
    }

    FrameLocation& setRotation(const Eigen::Quaterniond& rotation)
    {
        rotation_ = {rotation.x(), rotation.y(), rotation.z(), rotation.w()};
        return *this;
    }

    FrameLocation& setMove(const Eigen::Vector3d& move)
    {
        move_ = {move.x(), move.y(), move.z()};
        return * this;
    }

private:
    friend class sql_chemistry::GatewayAccess<FrameLocation>;
    friend class TxnIdAccess<FrameLocation>;

    FrameLocation() {};

    FrameLocation& setTxnId(TId txnId) {
        txnId_ = txnId;
        return * this;
    }

    template <typename T>
    static auto introspect(T& t) {
        return std::tie(t.frameId_, t.txnId_, t.mercatorPos_, t.rotation_, t.move_);
    }

    TId frameId_;
    TId txnId_;
    geolib3::Point2 mercatorPos_;
    std::vector<double> rotation_;
    std::optional<std::vector<double>> move_;

public:
    auto introspect() const { return introspect(*this); }
};

using FrameLocations = std::vector<FrameLocation>;

class FramePrivacy {

public:
    FramePrivacy(TId frameId, FeaturePrivacy type): frameId_(frameId), txnId_(0), type_(type) {}

    TId frameId() const { return frameId_; }

    TId txnId() const { return txnId_; }

    FeaturePrivacy type() const { return type_; };

    FramePrivacy& setType(FeaturePrivacy type)
    {
        type_ = type;
        return * this;
    }

private:
    friend class sql_chemistry::GatewayAccess<FramePrivacy>;
    friend class TxnIdAccess<FramePrivacy>;

    FramePrivacy() {};

    FramePrivacy& setTxnId(TId txnId) {
        txnId_ = txnId;
        return * this;
    }

    template <typename T>
    static auto introspect(T& t) {
        return std::tie(t.frameId_, t.txnId_, t.type_);
    }

    TId frameId_;
    TId txnId_;
    FeaturePrivacy type_;

public:
    auto introspect() const { return introspect(*this); }
};

using FramePrivacies = std::vector<FramePrivacy>;

} // namespace maps::mrc::db::eye
