#include <mbgl/style/conversion/source.hpp>
#include <mbgl/style/conversion/coordinate.hpp>
#include <mbgl/style/conversion/geojson.hpp>
#include <mbgl/style/conversion/geojson_options.hpp>
#include <mbgl/style/conversion/tileset.hpp>
#include <mbgl/style/conversion_impl.hpp>
#include <mbgl/style/sources/geojson_source.hpp>
#include <mbgl/style/sources/raster_source.hpp>
#include <mbgl/style/sources/raster_dem_source.hpp>
#include <mbgl/style/sources/vector_source.hpp>
#include <mbgl/style/sources/image_source.hpp>
#include <mbgl/util/geo.hpp>

namespace mbgl {
namespace style {
namespace conversion {

// A tile source can either specify a URL to TileJSON, or inline TileJSON.
static std::optional<variant<std::string, Tileset>> convertURLOrTileset(const Convertible& value, Error& error) {
    auto urlVal = objectMember(value, "url");
    if (!urlVal) {
        std::optional<Tileset> tileset = convert<Tileset>(value, error);
        if (!tileset) {
            return std::nullopt;
        }
        return {*tileset};
    }

    std::optional<std::string> url = toString(*urlVal);
    if (!url) {
        error.message = "source url must be a string";
        return std::nullopt;
    }

    return {*url};
}

static std::optional<std::unique_ptr<Source>> convertRasterSource(const std::string& id,
                                                                  const Convertible& value,
                                                                  Error& error) {
    std::optional<variant<std::string, Tileset>> urlOrTileset = convertURLOrTileset(value, error);
    if (!urlOrTileset) {
        return std::nullopt;
    }

    uint16_t tileSize = util::tileSize_I;
    auto tileSizeValue = objectMember(value, "tileSize");
    if (tileSizeValue) {
        std::optional<float> size = toNumber(*tileSizeValue);
        if (!size || *size < 0 || *size > std::numeric_limits<uint16_t>::max()) {
            error.message = "invalid tileSize";
            return std::nullopt;
        }
        tileSize = static_cast<uint16_t>(*size);
    }

    return {std::make_unique<RasterSource>(id, std::move(*urlOrTileset), tileSize)};
}

static std::optional<std::unique_ptr<Source>> convertRasterDEMSource(const std::string& id,
                                                                     const Convertible& value,
                                                                     Error& error) {
    std::optional<variant<std::string, Tileset>> urlOrTileset = convertURLOrTileset(value, error);
    if (!urlOrTileset) {
        return std::nullopt;
    }

    uint16_t tileSize = util::tileSize_I;
    auto tileSizeValue = objectMember(value, "tileSize");
    if (tileSizeValue) {
        std::optional<float> size = toNumber(*tileSizeValue);
        if (!size || *size < 0 || *size > std::numeric_limits<uint16_t>::max()) {
            error.message = "invalid tileSize";
            return std::nullopt;
        }
        tileSize = static_cast<uint16_t>(*size);
    }

    return {std::make_unique<RasterDEMSource>(id, std::move(*urlOrTileset), tileSize)};
}

static std::optional<std::unique_ptr<Source>> convertVectorSource(const std::string& id,
                                                                  const Convertible& value,
                                                                  Error& error) {
    std::optional<variant<std::string, Tileset>> urlOrTileset = convertURLOrTileset(value, error);
    if (!urlOrTileset) {
        return std::nullopt;
    }
    auto maxzoomValue = objectMember(value, "maxzoom");
    std::optional<float> maxzoom;
    if (maxzoomValue) {
        maxzoom = toNumber(*maxzoomValue);
        if (!maxzoom || *maxzoom < 0 || *maxzoom > std::numeric_limits<uint8_t>::max()) {
            error.message = "invalid maxzoom";
            return std::nullopt;
        }
    }
    auto minzoomValue = objectMember(value, "minzoom");
    std::optional<float> minzoom;
    if (minzoomValue) {
        minzoom = toNumber(*minzoomValue);
        if (!minzoom || *minzoom < 0 || *minzoom > std::numeric_limits<uint8_t>::max()) {
            error.message = "invalid minzoom";
            return std::nullopt;
        }
    }
    return {std::make_unique<VectorSource>(id, std::move(*urlOrTileset), std::move(maxzoom), std::move(minzoom))};
}

static std::optional<std::unique_ptr<Source>> convertGeoJSONSource(const std::string& id,
                                                                   const Convertible& value,
                                                                   Error& error) {
    auto dataValue = objectMember(value, "data");
    if (!dataValue) {
        error.message = "GeoJSON source must have a data value";
        return std::nullopt;
    }

    Immutable<GeoJSONOptions> options = GeoJSONOptions::defaultOptions();
    if (std::optional<GeoJSONOptions> converted = convert<GeoJSONOptions>(value, error)) {
        options = makeMutable<GeoJSONOptions>(std::move(*converted));
    }

    auto result = std::make_unique<GeoJSONSource>(id, std::move(options));

    if (isObject(*dataValue)) {
        auto geoJSON = convert<GeoJSON>(*dataValue, error);
        if (!geoJSON) {
            return std::nullopt;
        }
        result->setGeoJSON(*geoJSON);
    } else if (toString(*dataValue)) {
        result->setURL(*toString(*dataValue));
    } else {
        error.message = "GeoJSON data must be a URL or an object";
        return std::nullopt;
    }

    return {std::move(result)};
}

static std::optional<std::unique_ptr<Source>> convertImageSource(const std::string& id,
                                                                 const Convertible& value,
                                                                 Error& error) {
    auto urlValue = objectMember(value, "url");
    if (!urlValue) {
        error.message = "Image source must have a url value";
        return std::nullopt;
    }

    auto urlString = toString(*urlValue);
    if (!urlString) {
        error.message = "Image url must be a URL string";
        return std::nullopt;
    }

    auto coordinatesValue = objectMember(value, "coordinates");
    if (!coordinatesValue) {
        error.message = "Image source must have a coordinates values";
        return std::nullopt;
    }

    if (!isArray(*coordinatesValue) || arrayLength(*coordinatesValue) != 4) {
        error.message = "Image coordinates must be an array of four longitude latitude pairs";
        return std::nullopt;
    }

    std::array<LatLng, 4> coordinates;
    for (std::size_t i = 0; i < 4; i++) {
        auto latLng = conversion::convert<LatLng>(arrayMember(*coordinatesValue, i), error);
        if (!latLng) {
            return std::nullopt;
        }
        coordinates[i] = *latLng;
    }
    auto result = std::make_unique<ImageSource>(id, coordinates);
    result->setURL(*urlString);

    return {std::move(result)};
}

std::optional<std::unique_ptr<Source>> Converter<std::unique_ptr<Source>>::operator()(const Convertible& value,
                                                                                      Error& error,
                                                                                      const std::string& id) const {
    if (!isObject(value)) {
        error.message = "source must be an object";
        return std::nullopt;
    }

    auto typeValue = objectMember(value, "type");
    if (!typeValue) {
        error.message = "source must have a type";
        return std::nullopt;
    }

    std::optional<std::string> type = toString(*typeValue);
    if (!type) {
        error.message = "source type must be a string";
        return std::nullopt;
    }
    const std::string& tname = type.value();
    if (tname == "raster") {
        return convertRasterSource(id, value, error);
    } else if (tname == "raster-dem") {
        return convertRasterDEMSource(id, value, error);
    } else if (tname == "vector") {
        return convertVectorSource(id, value, error);
    } else if (tname == "geojson") {
        return convertGeoJSONSource(id, value, error);
    } else if (tname == "image") {
        return convertImageSource(id, value, error);
    } else {
        error.message = "invalid source type";
        return std::nullopt;
    }
}

} // namespace conversion
} // namespace style
} // namespace mbgl
