#pragma once

#include <mapbox/geojson.hpp>
#include <mapbox/geojson/rapidjson.hpp>

#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>

namespace mapbox {
namespace geojson {

using error    = std::runtime_error;
using prop_map = std::unordered_map<std::string, value>;

template <typename T>
T convert(const rapidjson_value &json);

template <>
point convert<point>(const rapidjson_value &json) {
    if (json.Size() < 2)
        throw error("coordinates array must have at least 2 numbers");

    return point{ json[0].GetDouble(), json[1].GetDouble() };
}

template <typename Cont>
Cont convert(const rapidjson_value &json) {
    Cont points;
    auto size = json.Size();
    points.reserve(size);

    for (auto &element : json.GetArray()) {
        points.push_back(convert<typename Cont::value_type>(element));
    }
    return points;
}

template <>
geometry convert<geometry>(const rapidjson_value &json) {
    if (!json.IsObject())
        throw error("Geometry must be an object");

    const auto &json_end = json.MemberEnd();

    const auto &type_itr = json.FindMember("type");
    if (type_itr == json_end)
        throw error("Geometry must have a type property");

    const auto &type = type_itr->value;

    if (type == "GeometryCollection") {
        const auto &geometries_itr = json.FindMember("geometries");
        if (geometries_itr == json_end)
            throw error("GeometryCollection must have a geometries property");

        const auto &json_geometries = geometries_itr->value;

        if (!json_geometries.IsArray())
            throw error("GeometryCollection geometries property must be an array");

        return geometry{ convert<geometry_collection>(json_geometries) };
    }

    const auto &coords_itr = json.FindMember("coordinates");

    if (coords_itr == json_end)
        throw error(std::string(type.GetString()) + " geometry must have a coordinates property");

    const auto &json_coords = coords_itr->value;
    if (!json_coords.IsArray())
        throw error("coordinates property must be an array");

    if (type == "Point")
        return geometry{ convert<point>(json_coords) };
    if (type == "MultiPoint")
        return geometry{ convert<multi_point>(json_coords) };
    if (type == "LineString")
        return geometry{ convert<line_string>(json_coords) };
    if (type == "MultiLineString")
        return geometry{ convert<multi_line_string>(json_coords) };
    if (type == "Polygon")
        return geometry{ convert<polygon>(json_coords) };
    if (type == "MultiPolygon")
        return geometry{ convert<multi_polygon>(json_coords) };

    throw error(std::string(type.GetString()) + " not yet implemented");
}

template <>
value convert<value>(const rapidjson_value &json);

template <>
prop_map convert(const rapidjson_value &json) {
    if (!json.IsObject())
        throw error("properties must be an object");

    prop_map result;
    for (auto &member : json.GetObject()) {
        result.emplace(std::string(member.name.GetString(), member.name.GetStringLength()),
                       convert<value>(member.value));
    }
    return result;
}

template <>
value convert<value>(const rapidjson_value &json) {
    switch (json.GetType()) {
    case rapidjson::kNullType:
        return ::mapbox::geometry::null_value_t{};
    case rapidjson::kFalseType:
        return false;
    case rapidjson::kTrueType:
        return true;
    case rapidjson::kObjectType:
        return convert<prop_map>(json);
    case rapidjson::kArrayType:
        return convert<std::vector<value>>(json);
    case rapidjson::kStringType:
        return std::string(json.GetString(), json.GetStringLength());
    default:
        assert(json.GetType() == rapidjson::kNumberType);
        if (json.IsUint64())
            return std::uint64_t(json.GetUint64());
        if (json.IsInt64())
            return std::int64_t(json.GetInt64());
        return json.GetDouble();
    }
}

template <>
identifier convert<identifier>(const rapidjson_value &json) {
    switch (json.GetType()) {
    case rapidjson::kStringType:
        return std::string(json.GetString(), json.GetStringLength());
    case rapidjson::kNumberType:
        if (json.IsUint64())
            return std::uint64_t(json.GetUint64());
        if (json.IsInt64())
            return std::int64_t(json.GetInt64());
        return json.GetDouble();
    default:
        throw error("Feature id must be a string or number");
    }
}

template <>
feature convert<feature>(const rapidjson_value &json) {
    if (!json.IsObject())
        throw error("Feature must be an object");

    auto const &json_end = json.MemberEnd();
    auto const &type_itr = json.FindMember("type");

    if (type_itr == json_end)
        throw error("Feature must have a type property");
    if (type_itr->value != "Feature")
        throw error("Feature type must be Feature");

    auto const &geom_itr = json.FindMember("geometry");

    if (geom_itr == json_end)
        throw error("Feature must have a geometry property");

#if !defined(__GNUC__) || __GNUC__ >= 5
    feature result{ convert<geometry>(geom_itr->value) };
#else
    feature result;
    result.geometry = convert<geometry>(geom_itr->value);
#endif

    auto const &id_itr = json.FindMember("id");
    if (id_itr != json_end) {
        result.id = convert<identifier>(id_itr->value);
    }

    auto const &prop_itr = json.FindMember("properties");

    if (prop_itr == json_end)
        throw error("Feature must have a properties property");

    const auto &json_props = prop_itr->value;
    if (!json_props.IsNull()) {
        result.properties = convert<prop_map>(json_props);
    }

    return result;
}

template <>
geojson convert<geojson>(const rapidjson_value &json) {
    if (!json.IsObject())
        throw error("GeoJSON must be an object");

    const auto &type_itr = json.FindMember("type");
    const auto &json_end = json.MemberEnd();

    if (type_itr == json_end)
        throw error("GeoJSON must have a type property");

    const auto &type = type_itr->value;

    if (type == "FeatureCollection") {
        const auto &features_itr = json.FindMember("features");
        if (features_itr == json_end)
            throw error("FeatureCollection must have features property");

        const auto &json_features = features_itr->value;

        if (!json_features.IsArray())
            throw error("FeatureCollection features property must be an array");

        feature_collection collection;

        const auto &size = json_features.Size();
        collection.reserve(size);

        for (auto &feature_obj : json_features.GetArray()) {
            collection.push_back(convert<feature>(feature_obj));
        }

        return geojson{ collection };
    }

    if (type == "Feature")
        return geojson{ convert<feature>(json) };

    return geojson{ convert<geometry>(json) };
}

template <class T>
T parse(const std::string &json) {
    rapidjson_document d;
    d.Parse(json.c_str());
    return convert<T>(d);
}

template <>
geometry parse<geometry>(const std::string &);
template <>
feature parse<feature>(const std::string &);
template <>
feature_collection parse<feature_collection>(const std::string &);

geojson parse(const std::string &json) {
    return parse<geojson>(json);
}

geojson convert(const rapidjson_value &json) {
    return convert<geojson>(json);
}

template <>
rapidjson_value convert<geometry>(const geometry&, rapidjson_allocator&);

template <>
rapidjson_value convert<feature>(const feature&, rapidjson_allocator&);

template <>
rapidjson_value convert<feature_collection>(const feature_collection&, rapidjson_allocator&);

struct to_type {
public:
    const char * operator()(const point&) {
        return "Point";
    }

    const char * operator()(const line_string&) {
        return "LineString";
    }

    const char * operator()(const polygon&) {
        return "Polygon";
    }

    const char * operator()(const multi_point&) {
        return "MultiPoint";
    }

    const char * operator()(const multi_line_string&) {
        return "MultiLineString";
    }

    const char * operator()(const multi_polygon&) {
        return "MultiPolygon";
    }

    const char * operator()(const geometry_collection&) {
        return "GeometryCollection";
    }
};

struct to_coordinates_or_geometries {
    rapidjson_allocator& allocator;

    // Handles line_string, polygon, multi_point, multi_line_string, multi_polygon, and geometry_collection.
    template <class E>
    rapidjson_value operator()(const std::vector<E>& vector) {
        rapidjson_value result;
        result.SetArray();
        for (std::size_t i = 0; i < vector.size(); ++i) {
            result.PushBack(operator()(vector[i]), allocator);
        }
        return result;
    }

    rapidjson_value operator()(const point& element) {
        rapidjson_value result;
        result.SetArray();
        result.PushBack(element.x, allocator);
        result.PushBack(element.y, allocator);
        return result;
    }

    rapidjson_value operator()(const geometry& element) {
        return convert(element, allocator);
    }
};

struct to_value {
    rapidjson_allocator& allocator;

    rapidjson_value operator()(null_value_t) {
        rapidjson_value result;
        result.SetNull();
        return result;
    }

    rapidjson_value operator()(bool t) {
        rapidjson_value result;
        result.SetBool(t);
        return result;
    }

    rapidjson_value operator()(int64_t t) {
        rapidjson_value result;
        result.SetInt64(t);
        return result;
    }

    rapidjson_value operator()(uint64_t t) {
        rapidjson_value result;
        result.SetUint64(t);
        return result;
    }

    rapidjson_value operator()(double t) {
        rapidjson_value result;
        result.SetDouble(t);
        return result;
    }

    rapidjson_value operator()(const std::string& t) {
        rapidjson_value result;
        result.SetString(t.data(), rapidjson::SizeType(t.size()), allocator);
        return result;
    }

    rapidjson_value operator()(const std::vector<value>& array) {
        rapidjson_value result;
        result.SetArray();
        for (const auto& item : array) {
            result.PushBack(value::visit(item, *this), allocator);
        }
        return result;
    }

    rapidjson_value operator()(const std::unordered_map<std::string, value>& map) {
        rapidjson_value result;
        result.SetObject();
        for (const auto& property : map) {
            result.AddMember(
                rapidjson::GenericStringRef<char> {
                    property.first.data(),
                    rapidjson::SizeType(property.first.size())
                },
                value::visit(property.second, *this),
                allocator);
        }
        return result;
    }
};

template <>
rapidjson_value convert<geometry>(const geometry& element, rapidjson_allocator& allocator) {
    rapidjson_value result(rapidjson::kObjectType);

    result.AddMember(
        "type",
        rapidjson::GenericStringRef<char> { geometry::visit(element, to_type()) },
        allocator);

    result.AddMember(
        rapidjson::GenericStringRef<char> { element.is<geometry_collection>() ? "geometries" : "coordinates" },
        geometry::visit(element, to_coordinates_or_geometries { allocator }),
        allocator);

    return result;
}

template <>
rapidjson_value convert<feature>(const feature& element, rapidjson_allocator& allocator) {
    rapidjson_value result(rapidjson::kObjectType);
    result.AddMember("type", "Feature", allocator);

    if (element.id) {
        result.AddMember("id", identifier::visit(*element.id, to_value { allocator }), allocator);
    }

    result.AddMember("geometry", convert(element.geometry, allocator), allocator);
    result.AddMember("properties", to_value { allocator }(element.properties), allocator);

    return result;
}

template <>
rapidjson_value convert<feature_collection>(const feature_collection& collection, rapidjson_allocator& allocator) {
    rapidjson_value result(rapidjson::kObjectType);
    result.AddMember("type", "FeatureCollection", allocator);

    rapidjson_value features(rapidjson::kArrayType);
    for (const auto& element : collection) {
        features.PushBack(convert(element, allocator), allocator);
    }
    result.AddMember("features", features, allocator);

    return result;
}

rapidjson_value convert(const geojson& element, rapidjson_allocator& allocator) {
    return geojson::visit(element, [&] (const auto& alternative) {
        return convert(alternative, allocator);
    });
}

template <class T>
std::string stringify(const T& t) {
    rapidjson_allocator allocator;
    rapidjson::StringBuffer buffer;
    rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
    convert(t, allocator).Accept(writer);
    return buffer.GetString();
}

std::string stringify(const geojson& element) {
    return geojson::visit(element, [] (const auto& alternative) {
        return stringify(alternative);
    });
}

} // namespace geojson
} // namespace mapbox
