import logging
import numpy as np
import os
from .matrix import Matrix, PersistentMatrix

#
L = logging.getLogger(__name__)
#


class GeoMatrix(Matrix):
    """
    Matrix, specific for `GeoAnalyzer`.
    `bbox` is the dictionary with `max_lat`, `min_lat`, `max_lon` and `min_lon`
    for corner gps-coordinates.
    `GeoMatrix` is 2d projection of real-world coordinates on a plane.

    """

    ConfigDefaults = {
        "max_lat": 71.26,  # Europe endpoints
        "min_lat": 23.33,
        "min_lon": -10.10,
        "max_lon": 40.6,
    }

    def __init__(
        self, app, dtype="float_", bbox=None, resolution=5, id=None, config=None
    ):
        if bbox is None:
            bbox = {
                "min_lat": float(self.ConfigDefaults["min_lat"]),
                "max_lat": float(self.ConfigDefaults["max_lat"]),
                "min_lon": float(self.ConfigDefaults["min_lon"]),
                "max_lon": float(self.ConfigDefaults["max_lon"]),
            }

        self.Bbox = bbox
        self.Resolution = resolution
        self._update_matrix_dimensions()

        super().__init__(app, dtype=dtype, id=id, config=config)

    def zeros(self):
        self.Array = np.zeros([self.MapHeight, self.MapWidth], dtype=self.DType)

    def is_in_boundaries(self, lat, lon):
        """
        Check, if coordinates are within the bbox coordinates.
        """
        if (lat >= self.Bbox["max_lat"]) or (lat <= self.Bbox["min_lat"]):
            return False

        if (lon >= self.Bbox["max_lon"]) or (lon <= self.Bbox["min_lon"]):
            return False

        return True

    def _update_matrix_dimensions(self):
        """
        Calculation of MapHeight and MapWidth.
        """
        self.SizeWidth = self.get_gps_distance(
            self.Bbox["min_lat"],
            self.Bbox["min_lon"],
            self.Bbox["min_lat"],
            self.Bbox["max_lon"],
        )
        self.SizeHeight = self.get_gps_distance(
            self.Bbox["min_lat"],
            self.Bbox["min_lon"],
            self.Bbox["max_lat"],
            self.Bbox["min_lon"],
        )
        self.MapHeight = int(np.ceil(self.SizeHeight / self.Resolution))
        self.MapWidth = int(np.ceil(self.SizeWidth / self.Resolution))

        # Correction
        self.SizeWidth = int(self.MapWidth * self.Resolution)
        self.SizeHeight = int(self.MapHeight * self.Resolution)

    def degrees_to_radians(self, degrees):
        return degrees * np.pi / 180

    def get_gps_distance(self, lat1, lon1, lat2, lon2):
        """
        Calculation of distance between 2 gps-coordinates in km.
        """
        R = 6371
        dLat = self.degrees_to_radians(lat2 - lat1)
        dLon = self.degrees_to_radians(lon2 - lon1)
        lat1 = self.degrees_to_radians(lat1)
        lat2 = self.degrees_to_radians(lat2)
        a = np.sin(dLat / 2) * np.sin(dLat / 2) + np.sin(dLon / 2) * np.sin(
            dLon / 2
        ) * np.cos(lat1) * np.cos(lat2)
        c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
        return R * c

    def project_equirectangular(self, lat, lon):
        """
        Converts latitude and longitude into row and column indexes.
        """
        column = (lon - self.Bbox["min_lon"]) * (
            (self.MapWidth - 1) / (self.Bbox["max_lon"] - self.Bbox["min_lon"])
        )
        row = ((lat * (-1)) + self.Bbox["max_lat"]) * (
            (self.MapHeight - 1) / (self.Bbox["max_lat"] - self.Bbox["min_lat"])
        )
        return int(row), int(column)

    def inverse_equirectangular(self, row, column):
        """
        Converts row and column into latitude and longitude.
        """
        row += 0.5
        column += 0.5

        lat = -(
            (row / (self.MapHeight - 1) * (self.Bbox["max_lat"] - self.Bbox["min_lat"]))
            - self.Bbox["max_lat"]
        )
        lon = (
            column * (self.Bbox["max_lon"] - self.Bbox["min_lon"]) / (self.MapWidth - 1)
            + self.Bbox["min_lon"]
        )

        return lat, lon


class PersistentGeoMatrix(PersistentMatrix):
    """
    Matrix, specific for `GeoAnalyzer`.
    `bbox` is the dictionary with `max_lat`, `min_lat`, `max_lon` and `min_lon`
    for corner gps-coordinates.
    `GeoMatrix` is 2d projection of real-world coordinates on a plane with pointers
    to `IdsToMembers`, where objects can be kept.

    """

    ConfigDefaults = {
        "max_lat": 71.26,  # Europe endpoints
        "min_lat": 23.33,
        "min_lon": -10.10,
        "max_lon": 40.6,
    }

    def __init__(
        self, app, dtype="float_", bbox=None, resolution=5, id=None, config=None
    ):
        if bbox is None:
            bbox = {
                "min_lat": float(self.ConfigDefaults["min_lat"]),
                "max_lat": float(self.ConfigDefaults["max_lat"]),
                "min_lon": float(self.ConfigDefaults["min_lon"]),
                "max_lon": float(self.ConfigDefaults["max_lon"]),
            }

        self.Bbox = bbox
        self.Resolution = resolution
        self._update_matrix_dimensions()

        super().__init__(app, dtype=dtype, id=id, config=config)

    def zeros(self):
        self.create_path()
        if os.path.exists(self.ArrayPath):
            self.Array = np.memmap(self.ArrayPath, dtype=self.DType, mode="readwrite")
            self.Array = self.Array.reshape(self.reshape(self.Array.shape))
        else:
            array = np.zeros([self.MapHeight, self.MapWidth], dtype=self.DType)
            self.Array = np.memmap(
                self.ArrayPath, dtype=self.DType, mode="w+", shape=array.shape
            )

    def reshape(self, shape):
        return [self.MapHeight, self.MapWidth]

    def is_in_boundaries(self, lat, lon):
        """
        Check, if coordinates are within the bbox coordinates.
        """
        if (lat >= self.Bbox["max_lat"]) or (lat <= self.Bbox["min_lat"]):
            return False

        if (lon >= self.Bbox["max_lon"]) or (lon <= self.Bbox["min_lon"]):
            return False

        return True

    def _update_matrix_dimensions(self):
        """
        Calculation of MapHeight and MapWidth.
        """
        self.SizeWidth = self.get_gps_distance(
            self.Bbox["min_lat"],
            self.Bbox["min_lon"],
            self.Bbox["min_lat"],
            self.Bbox["max_lon"],
        )
        self.SizeHeight = self.get_gps_distance(
            self.Bbox["min_lat"],
            self.Bbox["min_lon"],
            self.Bbox["max_lat"],
            self.Bbox["min_lon"],
        )
        self.MapHeight = int(np.ceil(self.SizeHeight / self.Resolution))
        self.MapWidth = int(np.ceil(self.SizeWidth / self.Resolution))

        # Correction
        self.SizeWidth = int(self.MapWidth * self.Resolution)
        self.SizeHeight = int(self.MapHeight * self.Resolution)

    def degrees_to_radians(self, degrees):
        return degrees * np.pi / 180

    def get_gps_distance(self, lat1, lon1, lat2, lon2):
        """
        Calculation of distance between 2 gps-coordinates in km.
        """
        R = 6371
        dLat = self.degrees_to_radians(lat2 - lat1)
        dLon = self.degrees_to_radians(lon2 - lon1)
        lat1 = self.degrees_to_radians(lat1)
        lat2 = self.degrees_to_radians(lat2)
        a = np.sin(dLat / 2) * np.sin(dLat / 2) + np.sin(dLon / 2) * np.sin(
            dLon / 2
        ) * np.cos(lat1) * np.cos(lat2)
        c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
        return R * c

    def project_equirectangular(self, lat, lon):
        """
        Converts latitude and longitude into row and column indexes.
        """
        column = (lon - self.Bbox["min_lon"]) * (
            (self.MapWidth - 1) / (self.Bbox["max_lon"] - self.Bbox["min_lon"])
        )
        row = ((lat * (-1)) + self.Bbox["max_lat"]) * (
            (self.MapHeight - 1) / (self.Bbox["max_lat"] - self.Bbox["min_lat"])
        )
        return int(row), int(column)

    def inverse_equirectangular(self, row, column):
        """
        Converts row and column into latitude and longitude.
        """
        row += 0.5
        column += 0.5

        lat = -(
            (row / (self.MapHeight - 1) * (self.Bbox["max_lat"] - self.Bbox["min_lat"]))
            - self.Bbox["max_lat"]
        )
        lon = (
            column * (self.Bbox["max_lon"] - self.Bbox["min_lon"]) / (self.MapWidth - 1)
            + self.Bbox["min_lon"]
        )

        return lat, lon
