""" "Module for interacting with deck.gl in Python."""

# *******************************************************************************#
# This module contains extra features of the geemap package.                     #
# The geemap community will maintain the extra features.                         #
# *******************************************************************************#

import os
from typing import Optional

import box
import ee

from .common import *
from . import coreutils
from . import examples
from .geemap import basemaps
from .osm import *

try:
    import pydeck as pdk

except ImportError:
    raise ImportError(
        "pydeck needs to be installed to use this module. "
        "Use 'pip install pydeck' to install the package. "
        "See https://deckgl.readthedocs.io/en/latest/installation.html "
        "for more details."
    )


class Layer(pdk.Layer):
    """Configures a deck.gl layer for rendering on a map.

    Parameters passed here will be specific to the particular deck.gl layer that you are
    choosing to use. Please see the deck.gl Layer catalog
    (https://deck.gl/docs/api-reference/layers) to determine the particular parameters
    of your layer. You are highly encouraged to look at the examples in the pydeck
    documentation.
    """

    # TODO: Rename `type` and `id` as they are buildins.
    def __init__(
        self,
        type: str,
        data: str | None = None,
        id=None,
        use_binary_transport: bool | None = None,
        **kwargs,
    ):
        """Initialize a Layer object.

        Args:
            type: Type of layer to render, e.g., HexagonLayer. See deck.gl Layer
                catalog (https://deck.gl/docs/api-reference/layers)
            data: Unique name for layer.
            id (str | dict | pandas.DataFrame, optional): Either a URL of data to load
                in or an array of data.
            use_binary_transport: Boolean indicating binary data.
        """
        super().__init__(type, data, id, use_binary_transport, **kwargs)


class Map(pdk.Deck):
    """The Map class inherits pydeck.Deck."""

    def __init__(
        self,
        center: tuple[float, float] = (20, 0),
        zoom: float = 1.2,
        height: int = 800,
        width: int | None = None,
        **kwargs,
    ):
        """Initialize a Map object.

        Args:
            center: Center of the map in the format of (lat, lon).
            zoom: The map zoom level.
            height: The map height. Note that the height has no effect in Jupyter
                notebook. Only works for streamlit.
            width: The map width. Note that the height has no effect in Jupyter
                notebook. Only works for streamlit.
        """
        # Authenticates Earth Engine and initializes an Earth Engine session.
        if "ee_initialize" not in kwargs.keys():
            kwargs["ee_initialize"] = True

        if kwargs["ee_initialize"]:
            coreutils.ee_initialize()

        kwargs.pop("ee_initialize")

        if "initial_view_state" not in kwargs:
            kwargs["initial_view_state"] = pdk.ViewState(
                latitude=center[0],
                longitude=center[1],
                zoom=zoom,
                height=height,
                width=width,
            )

        if "map_style" not in kwargs:
            kwargs["map_style"] = "light"

        super().__init__(**kwargs)

    def add_layer(self, layer, layer_name: str | None = None, **kwargs) -> None:
        """Add a layer to the map.

        Args:
            layer (pydeck.Layer): A pydeck Layer object.
            layer_name: Sets the layer 'id' for the pydeck Layer object.
            **kwargs: Additional keyword arguments for the pydeck Layer object.
        """

        try:
            if isinstance(layer, str) and layer.startswith(("http://", "https://")):
                pdk.settings.custom_libraries = [
                    {
                        "libraryName": "MyTileLayerLibrary",
                        "resourceUri": "https://cdn.jsdelivr.net/gh/giswqs/pydeck_myTileLayer@master/dist/bundle.js",
                    }
                ]
                layer = pdk.Layer("MyTileLayer", layer, id=layer_name, **kwargs)

            self.layers.append(layer)
        except Exception as e:
            raise Exception(e)

    def add_ee_layer(
        self,
        ee_object,
        # TODO(schwehr): Do not use a mutable object as the default.
        vis_params: dict[Any, Any] = {},
        name: str | None = None,
        **kwargs,
    ):
        """Adds a given EE object to the map as a layer.

        Args:
            ee_object (Collection|Feature|Image|MapId): The object to add to the map.
            vis_params: The visualization parameters. Defaults to {}.
            name: The name of the layer. Defaults to 'Layer N'.
            **kwargs: Additional keyword arguments for the pydeck Layer object.
        """
        image = None

        if vis_params is None:
            vis_params = {}

        if name is None:
            layer_count = len(self.layers)
            name = "Layer " + str(layer_count + 1)

        if (
            not isinstance(ee_object, ee.Image)
            and not isinstance(ee_object, ee.ImageCollection)
            and not isinstance(ee_object, ee.FeatureCollection)
            and not isinstance(ee_object, ee.Feature)
            and not isinstance(ee_object, ee.Geometry)
        ):
            err_str = (
                "\n\nThe image argument in 'addLayer' function must be an instance of "
                "one of ee.Image, ee.Geometry, ee.Feature or ee.FeatureCollection."
            )
            raise AttributeError(err_str)

        if isinstance(ee_object, (ee.Geometry, ee.Feature, ee.FeatureCollection)):
            features = ee.FeatureCollection(ee_object)

            width = 2

            if "width" in vis_params:
                width = vis_params["width"]

            color = "000000"

            if "color" in vis_params:
                color = vis_params["color"]

            image_fill = features.style(**{"fillColor": color}).updateMask(
                ee.Image.constant(0.5)
            )
            image_outline = features.style(
                **{"color": color, "fillColor": "00000000", "width": width}
            )

            image = image_fill.blend(image_outline)
        elif isinstance(ee_object, ee.Image):
            image = ee_object
        elif isinstance(ee_object, ee.ImageCollection):
            image = ee_object.mosaic()

        if "palette" in vis_params:
            if isinstance(vis_params["palette"], tuple):
                vis_params["palette"] = list(vis_params["palette"])
            if isinstance(vis_params["palette"], box.Box):
                try:
                    vis_params["palette"] = vis_params["palette"]["default"]
                except Exception as e:
                    print("The provided palette is invalid.")
                    raise Exception(e)
            elif isinstance(vis_params["palette"], str):
                vis_params["palette"] = coreutils.check_cmap(vis_params["palette"])
            elif not isinstance(vis_params["palette"], list):
                raise ValueError(
                    "The palette must be a list of colors or a string or a Box object."
                )

        map_id_dict = ee.Image(image).getMapId(vis_params)
        url = map_id_dict["tile_fetcher"].url_format
        self.add_layer(url, layer_name=name, **kwargs)

    addLayer = add_ee_layer

    def add_basemap(self, basemap: str = "HYBRID") -> None:
        """Adds a basemap to the map.

        Args:
            basemap: Can be one of string from pydeck_basemaps.
        """
        import xyzservices

        try:
            if isinstance(basemap, xyzservices.TileProvider):
                name = basemap.name
                url = basemap.build_url()
                self.add_layer(url, name)

            elif basemap in basemaps:
                pdk.settings.custom_libraries = [
                    {
                        "libraryName": "MyTileLayerLibrary",
                        "resourceUri": "https://cdn.jsdelivr.net/gh/giswqs/pydeck_myTileLayer@master/dist/bundle.js",
                    }
                ]
                layer = pdk.Layer("MyTileLayer", basemaps[basemap].url, basemap)

                self.add_layer(layer)

            else:
                print(
                    "Basemap can only be one of the following:\n  {}".format(
                        "\n  ".join(basemaps.keys())
                    )
                )

        except Exception:
            raise ValueError(
                "Basemap can only be one of the following:\n  {}".format(
                    "\n  ".join(basemaps.keys())
                )
            )

    def add_gdf(
        self,
        gdf,
        layer_name: str | None = None,
        random_color_column: str | None = None,
        **kwargs,
    ) -> None:
        """Adds a GeoPandas GeoDataFrame to the map.

        Args:
            gdf (GeoPandas.GeoDataFrame): The GeoPandas GeoDataFrame to add to the map.
            layer_name: The layer name to be used.
            random_color_column: Column name to use for random color.

        Raises:
            TypeError: gdf must be a GeoPandas GeoDataFrame.
        """
        try:
            import geopandas as gpd

            if not isinstance(gdf, gpd.GeoDataFrame):
                raise TypeError("gdf must be a GeoPandas GeoDataFrame.")

            if layer_name is None:
                layer_name = "layer_" + coreutils.random_string(3)

            if "pickable" not in kwargs:
                kwargs["pickable"] = True
            if "opacity" not in kwargs:
                kwargs["opacity"] = 0.5
            if "stroked" not in kwargs:
                kwargs["stroked"] = True
            if "filled" not in kwargs:
                kwargs["filled"] = True
            if "extruded" not in kwargs:
                kwargs["extruded"] = False
            if "wireframe" not in kwargs:
                kwargs["wireframe"] = True
            if "get_line_color" not in kwargs:
                kwargs["get_line_color"] = [0, 0, 0]
            if "get_line_width" not in kwargs:
                kwargs["get_line_width"] = 2
            if "line_width_min_pixels" not in kwargs:
                kwargs["line_width_min_pixels"] = 1

            if random_color_column is not None:
                if random_color_column not in gdf.columns.values.tolist():
                    raise ValueError(
                        "The random_color_column provided does not exist in the vector file."
                    )
                color_lookup = pdk.data_utils.assign_random_colors(
                    gdf[random_color_column]
                )
                gdf["color"] = gdf.apply(
                    lambda row: color_lookup.get(row[random_color_column]), axis=1
                )
                kwargs["get_fill_color"] = "color"

            layer = pdk.Layer(
                "GeoJsonLayer",
                gdf,
                id=layer_name,
                **kwargs,
            )
            self.add_layer(layer)

        except Exception as e:
            raise Exception(e)

    def add_vector(
        self,
        filename: str,
        layer_name: str | None = None,
        random_color_column: str | None = None,
        **kwargs,
    ) -> None:
        """Adds a vector file to the map.

        Args:
            filename: The input file path to the vector dataset.
            layer_name: The layer name to be used.
            random_color_column: Column name to use for random color.

        Raises:
            FileNotFoundError: The provided vector file could not be found.
        """

        try:
            import geopandas as gpd

            if not filename.startswith(("http://", "https://")):
                filename = os.path.abspath(filename)
                if filename.endswith(".zip"):
                    filename = "zip://" + filename

            gdf = gpd.read_file(filename)

            self.add_gdf(gdf, layer_name, random_color_column, **kwargs)

        except Exception as e:
            raise Exception(e)

    def add_geojson(
        self,
        filename: str,
        layer_name: str | None = None,
        random_color_column: str | None = None,
        **kwargs,
    ) -> None:
        """Adds a GeoJSON file to the map.

        Args:
            filename: The input file path to the vector dataset.
            layer_name: The layer name to be used.
            random_color_column: Column name to use for random color.

        Raises:
            FileNotFoundError: The provided vector file could not be found.
        """
        self.add_vector(filename, layer_name, random_color_column, **kwargs)

    def add_shp(
        self,
        filename: str,
        layer_name: str | None = None,
        random_color_column: str | None = None,
        **kwargs,
    ) -> None:
        """Adds a shapefile to the map.

        Args:
            filename: The input file path to the vector dataset.
            layer_name: The layer name to be used.
            random_color_column: Column name to use for random color.

        Raises:
            FileNotFoundError: The provided vector file could not be found.
        """
        self.add_vector(filename, layer_name, random_color_column, **kwargs)

    def add_kml(
        self,
        filename: str,
        layer_name: str | None = None,
        random_color_column: str | None = None,
        **kwargs,
    ) -> None:
        """Adds a KML file to the map.

        Args:
            filename: The input file path to the vector dataset.
            layer_name): The layer name to be used.
            random_color_column: Column name to use for random color.

        Raises:
            FileNotFoundError: The provided vector file could not be found.
        """
        self.add_vector(filename, layer_name, random_color_column, **kwargs)
