#ifndef TILEPLUGIN_HPP
#define TILEPLUGIN_HPP

#include "engine/plugins/plugin_base.hpp"
#include "osrm/json_container.hpp"

#include <protozero/varint.hpp>
#include <protozero/pbf_writer.hpp>

#include <string>
#include <vector>
#include <utility>

#include <cmath>
#include <cstdint>

/*
 * This plugin generates Mapbox Vector tiles that show the internal
 * routing geometry and speed values on all road segments.
 * You can use this along with a vector-tile viewer, like Mapbox GL,
 * to display maps that show the exact road network that
 * OSRM is routing.  This is very useful for debugging routing
 * errors
 */
namespace osrm
{
namespace engine
{
namespace plugins
{

// from mapnik/well_known_srs.hpp
const constexpr double EARTH_RADIUS = 6378137.0;
const constexpr double EARTH_DIAMETER = EARTH_RADIUS * 2.0;
const constexpr double EARTH_CIRCUMFERENCE = EARTH_DIAMETER * M_PI;
const constexpr double MAXEXTENT = EARTH_CIRCUMFERENCE / 2.0;
const constexpr double M_PI_by2 = M_PI / 2.0;
const constexpr double D2R = M_PI / 180.0;
const constexpr double R2D = 180.0 / M_PI;
const constexpr double M_PIby360 = M_PI / 360.0;
const constexpr double MAXEXTENTby180 = MAXEXTENT / 180.0;
const double MAX_LATITUDE = R2D * (2.0 * std::atan(std::exp(180.0 * D2R)) - M_PI_by2);
// ^ math functions are not constexpr since they have side-effects (setting errno) :(

// from mapnik-vector-tile
namespace detail_pbf
{

inline unsigned encode_length(const unsigned len) { return (len << 3u) | 2u; }
}

// Converts a regular WSG84 lon/lat pair into
// a mercator coordinate
inline void lonlat2merc(double &x, double &y)
{
    if (x > 180)
        x = 180;
    else if (x < -180)
        x = -180;
    if (y > MAX_LATITUDE)
        y = MAX_LATITUDE;
    else if (y < -MAX_LATITUDE)
        y = -MAX_LATITUDE;
    x = x * MAXEXTENTby180;
    y = std::log(std::tan((90 + y) * M_PIby360)) * R2D;
    y = y * MAXEXTENTby180;
}

// This is the global default tile size for all Mapbox Vector Tiles
const constexpr double tile_size_ = 256.0;

//
inline void from_pixels(const double shift, double &x, double &y)
{
    const double b = shift / 2.0;
    x = (x - b) / (shift / 360.0);
    const double g = (y - b) / -(shift / (2 * M_PI));
    y = R2D * (2.0 * std::atan(std::exp(g)) - M_PI_by2);
}

// Converts a WMS tile coordinate (z,x,y) into a mercator bounding box
inline void xyz2mercator(
    const int x, const int y, const int z, double &minx, double &miny, double &maxx, double &maxy)
{
    minx = x * tile_size_;
    miny = (y + 1.0) * tile_size_;
    maxx = (x + 1.0) * tile_size_;
    maxy = y * tile_size_;
    const double shift = std::pow(2.0, z) * tile_size_;
    from_pixels(shift, minx, miny);
    from_pixels(shift, maxx, maxy);
    lonlat2merc(minx, miny);
    lonlat2merc(maxx, maxy);
}

// Converts a WMS tile coordinate (z,x,y) into a wsg84 bounding box
inline void xyz2wsg84(
    const int x, const int y, const int z, double &minx, double &miny, double &maxx, double &maxy)
{
    minx = x * tile_size_;
    miny = (y + 1.0) * tile_size_;
    maxx = (x + 1.0) * tile_size_;
    maxy = y * tile_size_;
    const double shift = std::pow(2.0, z) * tile_size_;
    from_pixels(shift, minx, miny);
    from_pixels(shift, maxx, maxy);
}

// emulates mapbox::box2d, just a simple container for
// a box
struct bbox final
{
    bbox(const double _minx, const double _miny, const double _maxx, const double _maxy)
        : minx(_minx), miny(_miny), maxx(_maxx), maxy(_maxy)
    {
    }

    double width() const { return maxx - minx; }
    double height() const { return maxy - miny; }

    const double minx;
    const double miny;
    const double maxx;
    const double maxy;
};

// Simple container class for WSG84 coordinates
struct point_type_d final
{
    point_type_d(double _x, double _y) : x(_x), y(_y) {}

    const double x;
    const double y;
};

// Simple container for integer coordinates (i.e. pixel coords)
struct point_type_i final
{
    point_type_i(std::int64_t _x, std::int64_t _y) : x(_x), y(_y) {}

    const std::int64_t x;
    const std::int64_t y;
};

using line_type = std::vector<point_type_i>;
using line_typed = std::vector<point_type_d>;

// from mapnik-vector-tile
// Encodes a linestring using protobuf zigzag encoding
inline bool encode_linestring(line_type line,
                              protozero::packed_field_uint32 &geometry,
                              std::int32_t &start_x,
                              std::int32_t &start_y)
{
    const std::size_t line_size = line.size();
    // line_size -= detail_pbf::repeated_point_count(line);
    if (line_size < 2)
    {
        return false;
    }

    const unsigned line_to_length = static_cast<const unsigned>(line_size) - 1;

    auto pt = line.begin();
    geometry.add_element(9); // move_to | (1 << 3)
    geometry.add_element(protozero::encode_zigzag32(pt->x - start_x));
    geometry.add_element(protozero::encode_zigzag32(pt->y - start_y));
    start_x = pt->x;
    start_y = pt->y;
    geometry.add_element(detail_pbf::encode_length(line_to_length));
    for (++pt; pt != line.end(); ++pt)
    {
        const std::int32_t dx = pt->x - start_x;
        const std::int32_t dy = pt->y - start_y;
        /*if (dx == 0 && dy == 0)
        {
            continue;
        }*/
        geometry.add_element(protozero::encode_zigzag32(dx));
        geometry.add_element(protozero::encode_zigzag32(dy));
        start_x = pt->x;
        start_y = pt->y;
    }
    return true;
}

template <class DataFacadeT> class TilePlugin final : public BasePlugin
{
  public:
    explicit TilePlugin(DataFacadeT *facade) : facade(facade), descriptor_string("tile") {}

    const std::string GetDescriptor() const override final { return descriptor_string; }

    Status HandleRequest(const RouteParameters &route_parameters,
                         util::json::Object &json_result) override final
    {

        // Vector tiles are 4096 virtual pixels on each side
        const double tile_extent = 4096.0;
        double min_lon, min_lat, max_lon, max_lat;

        // Convert the z,x,y mercator tile coordinates into WSG84 lon/lat values
        xyz2wsg84(route_parameters.x, route_parameters.y, route_parameters.z, min_lon, min_lat,
                  max_lon, max_lat);

        FixedPointCoordinate southwest{static_cast<std::int32_t>(min_lat * COORDINATE_PRECISION),
                                       static_cast<std::int32_t>(min_lon * COORDINATE_PRECISION)};
        FixedPointCoordinate northeast{static_cast<std::int32_t>(max_lat * COORDINATE_PRECISION),
                                       static_cast<std::int32_t>(max_lon * COORDINATE_PRECISION)};

        // Fetch all the segments that are in our bounding box.
        // This hits the OSRM StaticRTree
        const auto edges = facade->GetEdgesInBox(southwest, northeast);

        // TODO: extract speed values for compressed and uncompressed geometries

        // Convert tile coordinates into mercator coordinates
        xyz2mercator(route_parameters.x, route_parameters.y, route_parameters.z, min_lon, min_lat,
                     max_lon, max_lat);
        const bbox tile_bbox{min_lon, min_lat, max_lon, max_lat};

        // Protobuf serialized blocks when objects go out of scope, hence
        // the extra scoping below.
        std::string buffer;
        protozero::pbf_writer tile_writer(buffer);
        {
            // Add a layer object to the PBF stream.  3=='layer' from the vector tile spec (2.1)
            protozero::pbf_writer layer_writer(tile_writer, 3);
            // TODO: don't write a layer if there are no features
            // Field 15 is the "version field, and it's a uint32
            layer_writer.add_uint32(15, 2); // version
            // Field 1 is the "layer name" field, it's a string
            layer_writer.add_string(1, "speeds"); // name
            // Field 5 is the tile extent.  It's a uint32 and should be set to 4096
            // for normal vector tiles.
            layer_writer.add_uint32(5, 4096); // extent

            // Begin the layer features block
            {
                // Each feature gets a unique id, starting at 1
                unsigned id = 1;
                for (const auto &edge : edges)
                {
                    // Get coordinates for start/end nodes of segmet (NodeIDs u and v)
                    const auto a = facade->GetCoordinateOfNode(edge.u);
                    const auto b = facade->GetCoordinateOfNode(edge.v);
                    // Calculate the length in meters, using the same calculation used to set the
                    // weight, so we can back-calculate the speed value that was set.
                    const double length = osrm::util::coordinate_calculation::greatCircleDistance(
                        a.lat, a.lon, b.lat, b.lon);

                    int forward_weight = 0;
                    int reverse_weight = 0;

                    if (edge.forward_packed_geometry_id != SPECIAL_EDGEID) {
                        std::vector<EdgeWeight> forward_weight_vector;
                        facade->GetUncompressedWeights(edge.forward_packed_geometry_id,
                                                          forward_weight_vector);
                        forward_weight = forward_weight_vector[edge.fwd_segment_position];
                    }

                    if (edge.reverse_packed_geometry_id != SPECIAL_EDGEID) {
                        std::vector<EdgeWeight> reverse_weight_vector;
                        facade->GetUncompressedWeights(edge.reverse_packed_geometry_id,
                                                          reverse_weight_vector);

                        BOOST_ASSERT(edge.fwd_segment_position < reverse_weight_vector.size());

                        reverse_weight = reverse_weight_vector[reverse_weight_vector.size() -
                                                               edge.fwd_segment_position - 1];
                    }

                    // If this is a valid forward edge, go ahead and add it to the tile
                    if (forward_weight != 0 &&
                        edge.forward_edge_based_node_id != SPECIAL_NODEID)
                    {
                        std::int32_t start_x = 0;
                        std::int32_t start_y = 0;

                        line_typed geo_line;
                        geo_line.emplace_back(a.lon / COORDINATE_PRECISION,
                                              a.lat / COORDINATE_PRECISION);
                        geo_line.emplace_back(b.lon / COORDINATE_PRECISION,
                                              b.lat / COORDINATE_PRECISION);

                        // Calculate the speed for this line
                        std::uint32_t speed = static_cast<std::uint32_t>(
                            round(length / forward_weight * 10 * 3.6));

                        line_type tile_line;
                        for (auto const &pt : geo_line)
                        {
                            double px_merc = pt.x;
                            double py_merc = pt.y;
                            lonlat2merc(px_merc, py_merc);
                            // convert lon/lat to tile coordinates
                            const auto px = std::round(((px_merc - tile_bbox.minx) * tile_extent /
                                                        16.0 / tile_bbox.width()) *
                                                       tile_extent / 256.0);
                            const auto py = std::round(((tile_bbox.maxy - py_merc) * tile_extent /
                                                        16.0 / tile_bbox.height()) *
                                                       tile_extent / 256.0);
                            tile_line.emplace_back(px, py);
                        }

                        // Here, we save the two attributes for our feature: the speed and the
                        // is_small
                        // boolean.  We onl serve up speeds from 0-139, so all we do is save the
                        // first
                        protozero::pbf_writer feature_writer(layer_writer, 2);
                        // Field 3 is the "geometry type" field.  Value 2 is "line"
                        feature_writer.add_enum(3, 2); // geometry type
                        // Field 1 for the feature is the "id" field.
                        feature_writer.add_uint64(1, id++); // id
                        {
                            // When adding attributes to a feature, we have to write
                            // pairs of numbers.  The first value is the index in the
                            // keys array (written later), and the second value is the
                            // index into the "values" array (also written later).  We're
                            // not writing the actual speed or bool value here, we're saving
                            // an index into the "values" array.  This means many features
                            // can share the same value data, leading to smaller tiles.
                            protozero::packed_field_uint32 field(feature_writer, 2);

                            field.add_element(0); // "speed" tag key offset
                            field.add_element(
                                std::min(speed, 127u)); // save the speed value, capped at 127
                            field.add_element(1);       // "is_small" tag key offset
                            field.add_element(edge.component.is_tiny ? 0 : 1); // is_small feature
                        }
                        {
                            // Encode the geometry for the feature
                            protozero::packed_field_uint32 geometry(feature_writer, 4);
                            encode_linestring(tile_line, geometry, start_x, start_y);
                        }
                    }

                    // Repeat the above for the coordinates reversed and using the `reverse`
                    // properties
                    if (reverse_weight != 0 &&
                        edge.reverse_edge_based_node_id != SPECIAL_NODEID)
                    {
                        std::int32_t start_x = 0;
                        std::int32_t start_y = 0;

                        line_typed geo_line;
                        geo_line.emplace_back(b.lon / COORDINATE_PRECISION,
                                              b.lat / COORDINATE_PRECISION);
                        geo_line.emplace_back(a.lon / COORDINATE_PRECISION,
                                              a.lat / COORDINATE_PRECISION);

                        const auto speed = static_cast<const std::uint32_t>(
                            round(length / reverse_weight * 10 * 3.6));

                        line_type tile_line;
                        for (auto const &pt : geo_line)
                        {
                            double px_merc = pt.x;
                            double py_merc = pt.y;
                            lonlat2merc(px_merc, py_merc);
                            // convert to integer tile coordinat
                            const auto px = std::round(((px_merc - tile_bbox.minx) * tile_extent /
                                                        16.0 / tile_bbox.width()) *
                                                       tile_extent / 256.0);
                            const auto py = std::round(((tile_bbox.maxy - py_merc) * tile_extent /
                                                        16.0 / tile_bbox.height()) *
                                                       tile_extent / 256.0);
                            tile_line.emplace_back(px, py);
                        }

                        protozero::pbf_writer feature_writer(layer_writer, 2);
                        feature_writer.add_enum(3, 2);      // geometry type
                        feature_writer.add_uint64(1, id++); // id
                        {
                            protozero::packed_field_uint32 field(feature_writer, 2);
                            field.add_element(0); // "speed" tag key offset
                            field.add_element(
                                std::min(speed, 127u)); // save the speed value, capped at 127
                            field.add_element(1);       // "is_small" tag key offset
                            field.add_element(edge.component.is_tiny ? 0 : 1); // is_small feature
                        }
                        {
                            protozero::packed_field_uint32 geometry(feature_writer, 4);
                            encode_linestring(tile_line, geometry, start_x, start_y);
                        }
                    }
                }
            }

            // Field id 3 is the "keys" attribute
            // We need two "key" fields, these are referred to with 0 and 1 (their array indexes)
            // earlier
            layer_writer.add_string(3, "speed");
            layer_writer.add_string(3, "is_small");

            // Now, we write out the possible speed value arrays and possible is_tiny
            // values.  Field type 4 is the "values" field.  It's a variable type field,
            // so requires a two-step write (create the field, then write its value)
            for (std::size_t i = 0; i < 128; i++)
            {
                {
                    // Writing field type 4 == variant type
                    protozero::pbf_writer values_writer(layer_writer, 4);
                    // Attribute value 5 == uin64 type
                    values_writer.add_uint64(5, i);
                }
            }
            {
                protozero::pbf_writer values_writer(layer_writer, 4);
                // Attribute value 7 == bool type
                values_writer.add_bool(7, true);
            }
            {
                protozero::pbf_writer values_writer(layer_writer, 4);
                // Attribute value 7 == bool type
                values_writer.add_bool(7, false);
            }
        }

        // Encode the PBF result as a special Buffer object on the response.
        // This will allow downstream consumers to handle this type differently
        // to the String type.
        json_result.values["pbf"] = osrm::util::json::Buffer(buffer);

        return Status::Ok;
    }

  private:
    DataFacadeT *const facade;
    const std::string descriptor_string;
};
}
}
}

#endif /* TILEPLUGIN_HPP */