#pragma once

#include <mbgl/math/clamp.hpp>
#include <mbgl/math/wrap.hpp>
#include <mbgl/util/constants.hpp>

#include <mapbox/geometry/point.hpp>
#include <mapbox/geometry/point_arithmetic.hpp>
#include <mapbox/geometry/line_string.hpp>
#include <mapbox/geometry/box.hpp>

#include <cmath>
#include <stdexcept>

namespace mbgl {

class CanonicalTileID;
class UnwrappedTileID;

using ScreenCoordinate = mapbox::geometry::point<double>;
using ScreenLineString = mapbox::geometry::line_string<double>;
using ScreenBox        = mapbox::geometry::box<double>;

class LatLng {
private:
    double lat;
    double lon;

public:
    enum WrapMode : bool { Unwrapped, Wrapped };

    LatLng(double lat_ = 0, double lon_ = 0, WrapMode mode = Unwrapped)
        : lat(lat_), lon(lon_) {
        if (std::isnan(lat)) {
            throw std::domain_error("latitude must not be NaN");
        }
        if (std::isnan(lon)) {
            throw std::domain_error("longitude must not be NaN");
        }
        if (std::abs(lat) > 90.0) {
            throw std::domain_error("latitude must be between -90 and 90");
        }
        if (!std::isfinite(lon)) {
            throw std::domain_error("longitude must not be infinite");
        }
        if (mode == Wrapped) {
            wrap();
        }
    }

    double latitude() const { return lat; }
    double longitude() const { return lon; }

    LatLng wrapped() const { return { lat, lon, Wrapped }; }

    void wrap() {
        lon = util::wrap(lon, -util::LONGITUDE_MAX, util::LONGITUDE_MAX);
    }

    // If the distance from start to end longitudes is between half and full
    // world, unwrap the start longitude to ensure the shortest path is taken.
    void unwrapForShortestPath(const LatLng& end) {
        const double delta = std::abs(end.lon - lon);
        if (delta < util::LONGITUDE_MAX || delta > util::DEGREES_MAX) return;
        if (lon > 0 && end.lon < 0) lon -= util::DEGREES_MAX;
        else if (lon < 0 && end.lon > 0) lon += util::DEGREES_MAX;
    }

    // Constructs a LatLng object with the top left position of the specified tile.
    LatLng(const CanonicalTileID& id);
    LatLng(const UnwrappedTileID& id);

    friend bool operator==(const LatLng& a, const LatLng& b) {
        return a.lat == b.lat && a.lon == b.lon;
    }

    friend bool operator!=(const LatLng& a, const LatLng& b) {
        return !(a == b);
    }
};

class LatLngBounds {
public:
    // Return a bounds covering the entire (unwrapped) world.
    static LatLngBounds world() {
        return LatLngBounds({-90, -180}, {90, 180});
    }

    // Return the bounds consisting of the single point.
    static LatLngBounds singleton(const LatLng& a) {
        return LatLngBounds(a, a);
    }

    // Return the convex hull of two points; the smallest bounds that contains both.
    static LatLngBounds hull(const LatLng& a, const LatLng& b) {
        LatLngBounds bounds(a, a);
        bounds.extend(b);
        return bounds;
    }

    // Return a bounds that may serve as the identity element for the extend operation.
    static LatLngBounds empty() {
        LatLngBounds bounds = world();
        std::swap(bounds.sw, bounds.ne);
        return bounds;
    }

    // Constructs a LatLngBounds object with the tile's exact boundaries.
    LatLngBounds(const CanonicalTileID&);

    bool valid() const {
        return (sw.latitude() <= ne.latitude()) && (sw.longitude() <= ne.longitude());
    }

    double south() const { return sw.latitude(); }
    double west()  const { return sw.longitude(); }
    double north() const { return ne.latitude(); }
    double east()  const { return ne.longitude(); }

    LatLng southwest() const { return sw; }
    LatLng northeast() const { return ne; }
    LatLng southeast() const { return LatLng(south(), east()); }
    LatLng northwest() const { return LatLng(north(), west()); }

    LatLng center() const {
        return LatLng((sw.latitude() + ne.latitude()) / 2,
                      (sw.longitude() + ne.longitude()) / 2);
    }

    LatLng constrain(const LatLng& p) const {
        if (contains(p)) {
            return p;
        }
        return LatLng {
            util::clamp(p.latitude(), sw.latitude(), ne.latitude()),
            util::clamp(p.longitude(), sw.longitude(), ne.longitude())
        };
    }

    void extend(const LatLng& point) {
        sw = LatLng(std::min(point.latitude(), sw.latitude()),
                    std::min(point.longitude(), sw.longitude()));
        ne = LatLng(std::max(point.latitude(), ne.latitude()),
                    std::max(point.longitude(), ne.longitude()));
    }

    void extend(const LatLngBounds& bounds) {
        extend(bounds.sw);
        extend(bounds.ne);
    }

    bool isEmpty() const {
        return sw.latitude() > ne.latitude() ||
               sw.longitude() > ne.longitude();
    }

    bool contains(const LatLng& point) const {
        return (point.latitude()  >= sw.latitude()  &&
                point.latitude()  <= ne.latitude()  &&
                point.longitude() >= sw.longitude() &&
                point.longitude() <= ne.longitude());
    }

    bool contains(const LatLngBounds& area) const {
        return (area.ne.latitude()  <= ne.latitude()  &&
                area.sw.latitude()  >= sw.latitude()  &&
                area.ne.longitude() <= ne.longitude() &&
                area.sw.longitude() >= sw.longitude());
    }

    bool intersects(const LatLngBounds area) const {
        return (area.ne.latitude()  > sw.latitude()  &&
                area.sw.latitude()  < ne.latitude()  &&
                area.ne.longitude() > sw.longitude() &&
                area.sw.longitude() < ne.longitude());
    }

private:
    LatLng sw;
    LatLng ne;

    LatLngBounds(LatLng sw_, LatLng ne_)
        : sw(std::move(sw_)), ne(std::move(ne_)) {}

    friend bool operator==(const LatLngBounds& a, const LatLngBounds& b) {
        return a.sw == b.sw && a.ne == b.ne;
    }

    friend bool operator!=(const LatLngBounds& a, const LatLngBounds& b) {
        return !(a == b);
    }
};

// Determines the orientation of the map.
enum class NorthOrientation : uint8_t {
    Upwards, // Default
    Rightwards,
    Downwards,
    Leftwards,
};

/// The distance on each side between a rectangle and a rectangle within.
class EdgeInsets {
private:
    double _top;    // Number of pixels inset from the top edge.
    double _left;   // Number of pixels inset from the left edge.
    double _bottom; // Number of pixels inset from the bottom edge.
    double _right;  // Number of pixels inset from the right edge.

public:
    EdgeInsets(double t_ = 0, double l_ = 0, double b_ = 0, double r_ = 0)
        : _top(t_), _left(l_), _bottom(b_), _right(r_) {
        if (std::isnan(_top)) {
            throw std::domain_error("top must not be NaN");
        }
        if (std::isnan(_left)) {
            throw std::domain_error("left must not be NaN");
        }
        if (std::isnan(_bottom)) {
            throw std::domain_error("bottom must not be NaN");
        }
        if (std::isnan(_right)) {
            throw std::domain_error("right must not be NaN");
        }
    }

    double top() const { return _top; }
    double left() const { return _left; }
    double bottom() const { return _bottom; }
    double right() const { return _right; }

    bool isFlush() const {
        return _top == 0 && _left == 0 && _bottom == 0 && _right == 0;
    }

    void operator+=(const EdgeInsets& o) {
        _top += o._top;
        _left += o._left;
        _bottom += o._bottom;
        _right += o._right;
    }

    EdgeInsets operator+(const EdgeInsets& o) const {
        return {
            _top + o._top, _left + o._left, _bottom + o._bottom, _right + o._right,
        };
    }

    ScreenCoordinate getCenter(uint16_t width, uint16_t height) const;

    friend bool operator==(const EdgeInsets& a, const EdgeInsets& b) {
        return a._top == b._top && a._left == b._left && a._bottom == b._bottom && a._right == b._right;
    }

    friend bool operator!=(const EdgeInsets& a, const EdgeInsets& b) {
        return !(a == b);
    }
};

} // namespace mbgl
