#' @title Create a leaflet map of the so-ii territory with several layers
#' 
#' @description
#' This function generates an interactive Leaflet map displaying multiple
#' layers.It uses spatial data from `sf` objects of type polygon, multipolygon, 
#' linestring and multilinestring. The function adds a base map layer using 
#' tile provider and then overlays the layers.
#' 
#' @param base_layer list of named sf objects, where the sf objects are the
#' base layers to add to the map and the names the labels of the layers
#' @param base_color list of named data.frames, where the color vectors
#' contain all colors for all of the geometries of the corresponding sf object
#' in the `base_layer` parameter. Names should be the same as in `base_layer`
#' @param overlay_layer list of named sf objects, where the sf objects are the
#' overlay layers to add to the map and the names the labels of the layers
#' @param base_junction list of vectors containing the column names with which 
#' each object in `base_layer` can be joined to the corresponding object in 
#' `base_color` in a merge operation. It must be the same length as 
#' `base_layer`. Names should be the same as in `overlay_layer`. If the layer 
#' class is "sfc", the junction can be omitted. Defaults to `NULL`.
#' @param overlay_color list of named color vectors, where the color
#' vectors contain all colors for all of the geometries of the corresponding sf
#' object in the `overlay_layer` parameter. Names should be the same as in
#' `overlay_layer`
#' @param overlay_junction list of vectors containing the column names with 
#' which each object in `overlay_layer` can be joined to the corresponding
#' object in `overlay_color` in a merge operation. It must be the same length
#' as `overlay_layer`. Names should be the same as in `overlay_layer`. If the
#' layer class is "sfc", the junction can be omitted. Defaults to `NULL`.
#' @param map_bg named list, containing the background map provider URL and the
#' attribution text, as issued from the function `set_provider()`
#' @param bg_opacity numeric, background map opacity; values should be between 0
#' and 1. Defaults to 0.2
#' @param point_marker character string specifying the type of marker to use for
#' point geometries. Valid options are "marker", "circle", and "popup".  
#' Defaults to "marker".
#' @param show_popup Logical. It indicates whether to display popups for
#' features in the layer. Defaults to `TRUE`.
#' 
#' @return A Leaflet map object (invisibly). The map is displayed in the viewer
#' or within an R Markdown or quarto document.
#' 
#' @details
#' The function internally calls the function `add_leaflet_layer()`, enabling it
#' to differentiate between polygon, polyline, and point geometries within the
#' `layer` object. The layer's legend is also added to the map, displaying the
#' unique fill colors and corresponding labels from the layer's attributes (only
#' for non-point geometries).
#' 
#' The `layer` (sf object) provided to this function is expected to include the 
#' following attributes in its associated `data.frame`:
#' - color: A character or numeric vector specifying the color of the feature
#' (used for polygons and polylines).
#' - opacity: A numeric value between 0 and 1 indicating the opacity of the
#' feature (used for polygons and polylines).
#' - fill: A character or numeric vector specifying the fill color of the
#' feature (used for polygons).
#' - label: A character vector providing a label or description for each feature
#' This label is used as the legend label for polygon and line geometries.
#' - popup: If show_popup = TRUE, a character vector containing HTML-formatted
#' text
#' 
#' @family interactive mapping
#' 
#' @export 
#' 
#' @examples
#' 
#' \dontrun{
#' # Example 1
#' map_leaflet_multilayer(
#'     base_layer = list(
#'         perimeter = so.ii::so_ii_limit
#'     ),
#'     base_color = list(
#'         perimeter = data.frame(
#'             id = "perimeter",
#'             label = "so-ii perimeter",
#'             popup = "so-ii",
#'             color = "white",
#'             fill = "transparent",
#'             opacity = 1
#'         )
#'     ),
#'     overlay_layer = list(
#'         clc = so.ii::so_ii_clc["color"]
#'     ),
#'     overlay_color = list(
#'         clc = data.frame(
#'             id = so.ii::clc_color[["color"]],
#'             label = so.ii::clc_color[["label_fr"]],
#'             popup = so.ii::clc_color[["label_fr"]],
#'             color = so.ii::clc_color[["color"]], 
#'             fill = so.ii::clc_color[["color"]],
#'             opacity = 1
#'         )
#'     ),
#'     overlay_junction = list(
#'         clc = c("color", "id")
#'     ),
#'     map_bg = so.ii::set_provider("ign_3d"),
#'     bg_opacity = 0.6
#' )
#' 
#' # Example 2
#' map_leaflet_multilayer(
#'     base_layer = list(
#'         perimeter = so.ii::so_ii_limit
#'     ),
#'     base_color = list(
#'         perimeter = data.frame(
#'             id = "perimeter",
#'             label = "so-ii perimeter",
#'             popup = "so-ii",
#'             color = "white",
#'             fill = "transparent",
#'             opacity = 1
#'         )
#'     ),
#'     overlay_layer = list(
#'         clc = so.ii::so_ii_clc["color"],
#'         hydro = so.ii::so_ii_hydro["type"]
#'     ),
#'     overlay_color = list(
#'         clc = data.frame(
#'             id = so.ii::clc_color[["color"]],
#'             label = so.ii::clc_color[["label_fr"]],
#'             popup = so.ii::clc_color[["label_fr"]],
#'             color = so.ii::clc_color[["color"]], 
#'             fill = so.ii::clc_color[["color"]],
#'             opacity = 1
#'         ),
#'         hydro = data.frame(
#'             id = unique(so.ii::so_ii_hydro[["type"]]),
#'             label = unique(so.ii::so_ii_hydro[["type"]]),
#'             popup = unique(so.ii::so_ii_hydro[["type"]]),
#'             color = scales::alpha(c("blue", "red", "dodgerblue"), 0.5), 
#'             fill = scales::alpha(c("blue", "red", "dodgerblue"), 0.5),
#'             opacity = 0.5
#'         )
#'     ),
#'     overlay_junction = list(
#'         clc = c("color", "id"),
#'         hydro = c("type", "id")
#'     ),
#'     map_bg = so.ii::set_provider("ign_3d"),
#'     bg_opacity = 0.6,
#'     show_popup = FALSE
#' )
#' 
#' # Example3
#' library(sf)
#' random_points = data.frame(
#'     label = c("point1", "point2", "point3"),
#'     long = c(3.821011, 3.870760, 3.896732),
#'     lat = c(43.55250, 43.65147, 43.61877)
#' )
#' 
#' random_points = sf::st_as_sf(
#'     x = random_points,
#'     coords = c("long","lat"),
#'     crs = sf::st_crs(so_ii_hydro)
#' )
#' map_leaflet_multilayer(
#'     base_layer = list(
#'         perimeter = so.ii::so_ii_limit
#'     ),
#'     base_color = list(
#'         perimeter = data.frame(
#'             id = "perimeter",
#'             label = "perimeter",
#'             color = "white",
#'             fill = "transparent",
#'             opacity = 1
#'         )
#'     ),
#'     overlay_layer = list(
#'         hydro = so.ii::so_ii_hydro,
#'         random_points = random_points
#'     ),
#'     overlay_color = list(
#'         hydro = data.frame(
#'             id = unique(so.ii::so_ii_hydro[["type"]]),
#'             label = unique(so.ii::so_ii_hydro[["type"]]),
#'             color = scales::alpha(c("blue", "red", "dodgerblue"), 0.5), 
#'             fill = scales::alpha(c("blue", "red", "dodgerblue"), 0.5),
#'             opacity = 0.5
#'         ),
#'         random_points = data.frame(
#'             id = unique(random_points[["label"]]),
#'             label = unique(random_points[["label"]]),
#'             color = NA, 
#'             fill = NA,
#'             opacity = 0.5,
#'             popup = sprintf("this is random %s", random_points[["label"]])
#'         )
#'     ),
#'     overlay_junction = list(
#'         hydro = c("type", "id"),
#'         random_points = "label"
#'     ),
#'     map_bg = so.ii::set_provider("ign_ortho"),
#'     bg_opacity = 0.6
#' )
#' }


map_leaflet_multilayer = function(
    base_layer, 
    base_color,
    base_junction = NULL,
    overlay_layer,
    overlay_color,
    overlay_junction = NULL,
    map_bg = set_provider("ign_ortho"),
    bg_opacity = 0.2, 
    point_marker = "marker",
    show_popup = TRUE
) {
    map = leaflet::leaflet() |>
        leaflet::addTiles(
            urlTemplate = map_bg[["provider"]],
            attribution = map_bg[["attribution"]],
            options = leaflet::tileOptions(opacity = bg_opacity)
        ) 

    for(i in seq_along(base_layer)) {
        layer = add_visual_param(
            base_layer[[i]], 
            base_color[[i]], 
            base_junction[[i]]
        )
        map = add_leaflet_layer(map, layer, names(base_layer)[i], point_marker) 
    }

    for(i in seq_along(overlay_layer)) {
        layer = add_visual_param(
            overlay_layer[[i]], 
            overlay_color[[i]], 
            overlay_junction[[i]]
        )
        map = add_leaflet_layer(
            map, 
            layer, 
            names(overlay_layer)[i], 
            point_marker,
            show_popup
        ) 
    }

    map = leaflet::addLayersControl(
        map,
        baseGroups = names(base_layer),
        overlayGroups = names(overlay_layer),
        options = leaflet::layersControlOptions(collapsed = FALSE)
    )

    return(map)
}

#' Add Visual Parameters to a Spatial Layer
#'
#' This function enhances a spatial layer (either `sf` or `sfc` object) with 
#' visual parameters, typically colors or other attributes, using a merge 
#' operation (`sf`) or direct attribute addition (`sfc`).
#'
#' @param layer spatial object, either an `sf` object or an `sfc` object. This
#' is the layer to which visual parameters will be added.
#' @param visual_param `data.frame` containing the visual parameters to add.
#' This should have a common identifier with the `layer`object for merging.
#' @param junction character vector specifying the column name(s) to use for
#' merging the `layer` and `visual_param` objects. For `sf` objects, this
#' defines the join key. If missing for `sf` objects, an error is thrown.
#'
#' @return spatial object (`sf`) with the visual parameters added as
#' new attributes.
#'
#' @details
#' For `sf` objects, the function merges the `layer` and `visual_param`
#' data frames based on the specified `junction` column.  If a single `junction`
#' is provided, a standard merge is performed. If two `junction` values are
#' provided, the merge is done using `by.x` and `by.y`.  If more than two
#' junction values are provided, the function stops with an error.
#' For `sfc` objects, the visual parameters are directly added as new attributes
#' to the `sfc` object.
#' 
#' @family interactive mapping
#'
#' @export
#' 
#' @examples
#' 
#' enhanced_sf = add_visual_param(
#'   layer = so_ii_hydro,
#'   visual_param = data.frame(
#'      id = unique(so.ii::so_ii_hydro[["type"]]),
#'      color = scales::alpha(c("blue", "red", "dodgerblue"), 0.5), 
#'      fill = scales::alpha(c("blue", "red", "dodgerblue"), 0.5),
#'      opacity = 0.5
#'   ),
#'   junction = c("type", "id")
#' )
#' 
#' enhanced_sf = add_visual_param(
#'   layer = so_ii_hydro,
#'   visual_param = data.frame(
#'      type = unique(so.ii::so_ii_hydro[["type"]]),
#'      color = scales::alpha(c("blue", "red", "dodgerblue"), 0.5), 
#'      fill = scales::alpha(c("blue", "red", "dodgerblue"), 0.5),
#'      opacity = 0.8
#'   ),
#'   junction = "type"
#' )
#' 
#' # For sfc objects
#' enhanced_sfc = add_visual_param(
#'   layer = so_ii_limit,
#'   visual_param = data.frame(
#'      id = "perimeter",
#'      label = "perimeter",
#'      color = "white",
#'      fill = "transparent",
#'      opacity = 1
#'   )
#' )
#'

add_visual_param = function(layer, visual_param, junction = NULL) {

    if(isTRUE(methods::is(layer, "sf"))){
        if(missing(junction)){
            stop("Cannot process request. Need to provide 'junction'")
        }
        if(length(junction) == 1){
            result = merge(layer, visual_param, by = junction)
        }
        if(length(junction) == 2){
            result = merge(layer, visual_param, by.x = junction[1], by.y = junction[2])
        }
        if(length(junction) > 2){
            stop(
                "Cannot make merge between 'layer' and 'color'. 
                Incorrect dimension of 'junction'. Nothing is done"
            )
        }
    }

    if(isTRUE(methods::is(layer, "sfc"))){
        result = sf::st_sf(layer, visual_param)
    }

    return(result)
}

#' Add a Spatial Layer to a Leaflet Map
#'
#' This function adds a spatial layer (e.g., polygons or polylines, or points)
#' to an existing Leaflet map, customizing its appearance with colors, weights,
#' opacity, and fill properties. It dynamically handles different geometry types
#' within the layer.
#'
#' @param map leaflet map object (`leaflet` object) to which the layer will be
#' added.
#' @param layer `sf` object representing the spatial layer to add to the map.
#' This object should contain attributes for color, opacity, and fill.
#' @param group character string representing the group name for the layer.
#' This is used for the layers control in the Leaflet map, allowing users to
#' toggle the layer's visibility.
#' @param point_marker character string specifying the type of marker to use for
#' point geometries. Valid options are "marker", "circle", and "popup".  
#' Defaults to "marker".
#' @param show_popup Logical. It indicates whether to display popups for
#' features in the layer. Defaults to `TRUE`.
#'
#' @return leaflet map object (`leaflet` object) with the added layer.
#'
#' @details
#' The function differentiates between polygon, polyline, and point geometries
#' within the `layer` object. 
#' Polygons are added using `leaflet::addPolygons`, polylines are added using
#' `leaflet::addPolylines`, and points are added using either
#' `leaflet::addMarkers`, `leaflet::addCircleMarkers`, or `leaflet::addPopups`,
#' based on the `point_marker` parameter.
#' The function also adds the layer's  legend to the map, displaying the unique
#' fill colors and corresponding labels from the layer's attributes (only for
#' non-point geometries).
#' 
#' The `layer` (sf object) provided to this function is expected to include the 
#' following attributes in its associated `data.frame`:
#' - color: A character or numeric vector specifying the color of the feature
#' (used for polygons and polylines).
#' - opacity: A numeric value between 0 and 1 indicating the opacity of the
#' feature (used for polygons and polylines).
#' - fill: A character or numeric vector specifying the fill color of the
#' feature (used for polygons).
#' - label: A character vector providing a label or description for each feature
#' This label is used as the legend label for polygon and line geometries.
#' - popup: If show_popup = TRUE, a character vector containing HTML-formatted
#' text
#'
#' @family interactive mapping
#' 
#' @export 
#' 
#' @examples
#' 
#' \dontrun{
#' # Example usage:
#' map_bg = set_provider("ign_3d")
#' map = leaflet::leaflet() |>
#'  leaflet::addTiles(
#'      urlTemplate = map_bg[["provider"]],
#'      attribution = map_bg[["attribution"]],
#'      options = leaflet::tileOptions(opacity = 0.2)
#' )
#' map
#' 
#' map = add_leaflet_layer(
#'     map = map,
#'     layer = add_visual_param(
#'         layer = so_ii_limit,
#'         visual_param = data.frame(
#'             id = "perimeter",
#'             label = "perimeter",
#'             popup = "so-ii perimeter",
#'             color = "white",
#'             fill = "transparent",
#'             opacity = 1
#'         )
#'     ),
#'     group = "perimeter"
#' )
#' map
#' 
#' map = add_leaflet_layer(
#'     map = map,
#'     layer = add_visual_param(
#'         layer = so_ii_hydro,
#'         visual_param = data.frame(
#'             id = unique(so.ii::so_ii_hydro[["type"]]),
#'             label = unique(so.ii::so_ii_hydro[["type"]]),
#'             popup = unique(so.ii::so_ii_hydro[["type"]]),
#'             color = scales::alpha(c("blue", "red", "dodgerblue"), 0.5), 
#'             fill = scales::alpha(c("blue", "red", "dodgerblue"), 0.5),
#'             opacity = 0.5
#'         ),
#'         junction = c("type", "id")
#'     ),
#'     group = "perimeter"
#' )
#' map
#' 
#' random_points = data.frame(
#'     label = c("point1", "point2", "point3"),
#'     long = c(3.821011, 3.870760, 3.896732),
#'     lat = c(43.55250, 43.65147, 43.61877)
#' )
#' 
#' random_points = sf::st_as_sf(
#'     x = random_points,
#'     coords = c("long","lat"),
#'     crs = sf::st_crs(so_ii_hydro)
#' )
#' 
#' map = add_leaflet_layer(
#'     map = map,
#'     layer = add_visual_param(
#'         layer = random_points,
#'         visual_param = data.frame(
#'             id = unique(random_points[["label"]]),
#'             label = unique(random_points[["label"]]),
#'             color = NA, 
#'             fill = NA,
#'             opacity = 0.5,
#'             popup = sprintf("this is random %s", random_points[["label"]])
#'         ),
#'         junction = "label"
#'     ),
#'     group = "random_points"
#' )
#' map
#'}

add_leaflet_layer = function(
    map, 
    layer, 
    group, 
    point_marker = "marker",
    show_popup = TRUE
)
{
    geometry_type = as.character(sf::st_geometry_type(layer))
    # add layer to map, minding geometry type
    selection = grepl("POLYGON", geometry_type)
    if(sum(selection) > 0) {
        if(isTRUE(show_popup)) {
            popup = htmltools::htmlEscape(layer[["popup"]][selection])
        } else {
            popup = NULL
        } 
        map = leaflet::addPolygons(
            map,
            data = layer[selection,],
            color = layer[["color"]][selection], 
            weight = 2,
            smoothFactor = 0.5,
            opacity = layer[["opacity"]],
            fillOpacity = layer[["opacity"]],
            fillColor = layer[["fill"]][selection],
            popup = popup,
            group = group
        )
    }
    selection = grepl("LINE", geometry_type)
    if(sum(selection) > 0) {
        if(isTRUE(show_popup)) {
            popup = htmltools::htmlEscape(layer[["popup"]][selection])
        } else {
            popup = NULL
        }
        map = leaflet::addPolylines(
            map,
            data = layer[selection,],
            color = layer[["color"]][selection], 
            weight = 2,
            smoothFactor = 0.5,
            opacity = layer[["opacity"]],
            popup = popup,
            group = group
        )
    }
    selection = grepl("POINT", geometry_type)
    if(sum(selection) > 0) {
        if(isTRUE(show_popup)) {
            popup = htmltools::htmlEscape(layer[["popup"]][selection])
        } else {
            popup = NULL
        }
        map = switch(
            EXPR = point_marker,
            "marker" = leaflet::addMarkers(
                map, 
                data = layer[selection, ], 
                popup = popup,
                group = group
            ),
            "circle" = leaflet::addCircleMarkers(
                map, 
                data = layer[selection, ], 
                popup = popup,
                group = group
            ),
            "popup" = leaflet::addPopups(
                map, 
                data = layer[selection, ], 
                popup = popup,
                group = group
            ),
            stop(sprintf("No function available for %s. You should use 'marker', 'circle' or 'popup'", point_marker))
        )
    }

    #  add legend
    selection = grepl("POINT", geometry_type)
    legend_par = unique(
        sf::st_drop_geometry(
            layer[!selection, c("fill","label")]
        )
    )
    map = leaflet::addLegend(
        map,
        "bottomright",
        colors = legend_par[["fill"]],
        labels = legend_par[["label"]],
        opacity = 1,
        group = group
    )
    return(map)
}