# Copyright (C) 2019-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""
HERE inspector utilities.
"""
from dataclasses import dataclass, field
from math import asin, cos, log2, sin, sqrt
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union

import geopandas as gpd
import pandas as pd
from geojson import factory, utils
from here.inspector.styles import Color, Theme
from shapely.geometry.base import BaseGeometry


@dataclass
class Options:
    """Global options for any inspector."""

    #: styles are assigned automatically picked from this list of colors, when not specified.
    default_colors: List[Color] = field(default_factory=lambda: list(Color))

    #: the default inspector theme
    default_theme: Theme = Theme.LIGHT_MAP

    #: API key for the HERE platform to use HERE-provided background maps
    api_key: Optional[str] = None

    #: Name of the backend class to use for visualisation
    inspector_class: Optional[Type] = None


options = Options()
Tiles = Union[pd.Series, Iterable[int]]
Features = Union[
    gpd.GeoSeries,  # geometries with ids (index)
    gpd.GeoDataFrame,  # geometries with ids (index) and attributes
    BaseGeometry,  # single geometries
    Iterable[BaseGeometry],  # geometries
    Iterable[Tuple[BaseGeometry, Dict[str, Any]]],  # geometries with ids and attributes
    Dict[str, Any],  # one single GeoJSON FeatureCollection
    Iterable[Dict[str, Any]],  # collection of GeoJSON Feature
]


Bounds = Tuple[Tuple[float, float], Tuple[float, float]]


def bounds_obj(geojson_obj: dict) -> Bounds:
    """Calculate the bounds of given GeoJSON object.

    :param geojson_obj: The GeoJSON object (dict).
    :return: Bounds of the GeoJSON object in this format:
        ``((south, west), (north, east))``.
    :raises ValueError: in case the GeoJSON object has no coordinates
    """
    coords = list(utils.coords(geojson_obj))
    if not coords:
        raise ValueError("No coordinates to calculate the bounds")
    # pos can be (lon, lat) or (lon, lat, alt)
    south = min(pos[1] for pos in coords)
    north = max(pos[1] for pos in coords)
    west = min(pos[0] for pos in coords)
    east = max(pos[0] for pos in coords)
    bounds = ((south, west), (north, east))
    return bounds


def bounds_gdf(gdf: gpd.GeoDataFrame) -> Bounds:
    """Calculate the bounds of one GeoDataFrame.

    :param gdf: The GeoDataFrame.
    :return: Bounds of the GeoDataFrame object in this format:
        ``((south, west), (north, east))``.
    :raises ValueError: in case GeoDataFrame is empty
    """
    if gdf.empty:
        raise ValueError("GeoDataFrame is empty.")
    tb = gdf.geometry.total_bounds
    minx = tb[0]
    miny = tb[1]
    maxx = tb[2]
    maxy = tb[3]
    return (miny, minx), (maxy, maxx)


def merge_bounds(bounds: List[Bounds]) -> Bounds:
    """
    Merge multiple bounds together into a single one that covers them all.

    :param bounds: the list of bounds to merge
    :return: overall bounds, ``None`` if the input list is empty
    """
    south = min(b[0][0] for b in bounds)
    west = min(b[0][1] for b in bounds)
    north = max(b[1][0] for b in bounds)
    east = max(b[1][1] for b in bounds)
    return (south, west), (north, east)


def center_bounds(bounds: Bounds) -> Tuple[float, float]:
    """The center of the bound area, for map display, latitude and longitude"""
    return (bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2


def zoom_bounds(bounds: Bounds, min_zoom: float) -> float:
    """The approximate zoom level to visualize the bound area, for map display"""
    sx = abs(bounds[0][1] - bounds[1][1])
    sy = abs(bounds[0][0] - bounds[1][0])
    # TODO: these should depend on the viewport, so far zoom level is approximate
    if sx != 0 and sy != 0:
        zx = log2(360 / sx)
        zy = log2(180 / sy)
        avg = (min(zx, zy) + max(zx, zy)) / 2
        zoom_level = max(avg, min_zoom) if bounds else min_zoom
        return zoom_level
    else:
        # haversine formula
        a = sin(sx / 2) ** 2 + cos(bounds[1][1]) * cos(bounds[0][1]) * sin(sy / 2) ** 2
        c = 2 * asin(sqrt(a))
        distance = 6371 * c
        # equator length = 40000000
        zoom_level = int(256 * distance / 40000000 * 2) ^ 4
        return zoom_level


def is_geojson(d: dict) -> bool:
    """Check if the dictionary contains a valid GeoJSON object"""
    if "type" in d:
        try:
            # Check if the type is supported by the GeoJSON factory
            getattr(factory, d["type"])
            return True
        except AttributeError:
            return False
    else:
        return False
