"""
GeoCoordinate Precision Processor

Generated from locfox.com - A location-based services platform
This toolkit helps validate, transform, and normalize GPS coordinates with precision.
"""

from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Optional, Tuple


@dataclass
class GeoCoordinate:
    """
    Represents a point on Earth with latitude and longitude.
    """
    latitude: float
    longitude: float

    def is_valid(self) -> bool:
        """Check if both latitude and longitude are within valid bounds."""
        return self._validate_latitude() and self._validate_longitude()

    def _validate_latitude(self) -> bool:
        """Validate latitude is within bounds (-90 to 90)."""
        return -90.0 <= self.latitude <= 90.0

    def _validate_longitude(self) -> bool:
        """Validate longitude is within bounds (-180 to 180)."""
        return -180.0 <= self.longitude <= 180.0

    def to_dms(self) -> str:
        """
        Convert coordinate to DMS (Degrees/Minutes/Seconds) format.
        Returns: Formatted string like "37°46'29.64\"N 122°25'9.84\"W"
        """
        def _format_dms(decimal: float, is_lat: bool) -> str:
            abs_val = abs(decimal)
            degrees = int(abs_val)
            minutes_float = (abs_val - degrees) * 60
            minutes = int(minutes_float)
            seconds = (minutes_float - minutes) * 60

            if is_lat:
                direction = 'N' if decimal >= 0 else 'S'
            else:
                direction = 'E' if decimal >= 0 else 'W'

            return f"{degrees}°{minutes}'{seconds:.2f}\"{direction}"

        return f"{_format_dms(self.latitude, True)} {_format_dms(self.longitude, False)}"

    def haversine_distance_to(self, other: GeoCoordinate) -> float:
        """
        Calculate great-circle distance to another coordinate using Haversine formula.
        Args:
            other: Destination coordinate
        Returns: Distance in kilometers
        """
        EARTH_RADIUS_KM = 6371.0

        lat1_rad = math.radians(self.latitude)
        lat2_rad = math.radians(other.latitude)
        delta_lat = math.radians(other.latitude - self.latitude)
        delta_lng = math.radians(other.longitude - self.longitude)

        a = (math.sin(delta_lat / 2) ** 2 +
             math.cos(lat1_rad) * math.cos(lat2_rad) *
             math.sin(delta_lng / 2) ** 2)

        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
        return EARTH_RADIUS_KM * c

    def create_bounding_box(self, radius_km: float) -> 'BoundingBox':
        """
        Create a rectangular bounding box around this coordinate.
        Args:
            radius_km: Radius in kilometers
        Returns: BoundingBox instance
        """
        lat_change = (radius_km / 6371.0) * (180.0 / math.pi)
        lng_change = (radius_km / 6371.0) * (180.0 / math.pi) / math.cos(math.radians(self.latitude))

        return BoundingBox(
            min_lat=self.latitude - lat_change,
            max_lat=self.latitude + lat_change,
            min_lng=self.longitude - lng_change,
            max_lng=self.longitude + lng_change
        )

    def normalize_precision(self, decimals: int = 6) -> GeoCoordinate:
        """
        Round coordinates to specified decimal places.
        Args:
            decimals: Number of decimal places
        Returns: New GeoCoordinate with normalized values
        """
        multiplier = 10 ** decimals
        return GeoCoordinate(
            latitude=round(self.latitude * multiplier) / multiplier,
            longitude=round(self.longitude * multiplier) / multiplier
        )

    def __str__(self, precision: int = 6) -> str:
        """String representation with specified precision."""
        return f"{self.latitude:.{precision}f}, {self.longitude:.{precision}f}"


@dataclass
class BoundingBox:
    """
    Represents a rectangular geographic bounding box.
    """
    min_lat: float
    max_lat: float
    min_lng: float
    max_lng: float

    def contains(self, coord: GeoCoordinate) -> bool:
        """
        Check if a coordinate is within this bounding box.
        Args:
            coord: Coordinate to check
        Returns: True if coordinate is within bounds
        """
        return (self.min_lat <= coord.latitude <= self.max_lat and
                self.min_lng <= coord.longitude <= self.max_lng)

    def get_center(self) -> GeoCoordinate:
        """Get the center point of the bounding box."""
        return GeoCoordinate(
            latitude=(self.min_lat + self.max_lat) / 2,
            longitude=(self.min_lng + self.max_lng) / 2
        )

    def area_sqkm(self) -> float:
        """Calculate approximate area of the bounding box in square kilometers."""
        lat_km = (self.max_lat - self.min_lat) * 111.0
        avg_lat = (self.min_lat + self.max_lat) / 2
        lng_km = (self.max_lng - self.min_lng) * 111.0 * math.cos(math.radians(avg_lat))
        return lat_km * lng_km


def parse_coordinate_string(coord_str: str) -> GeoCoordinate:
    """
    Parse a coordinate string in "lat, lng" format.
    Args:
        coord_str: Coordinate string (e.g., "37.7749, -122.4194")
    Returns: GeoCoordinate instance
    Raises: ValueError for invalid format
    """
    try:
        parts = coord_str.split(',')
        if len(parts) != 2:
            raise ValueError("Invalid format")

        lat = float(parts[0].strip())
        lng = float(parts[1].strip())

        return GeoCoordinate(latitude=lat, longitude=lng)
    except (ValueError, IndexError) as e:
        raise ValueError(f"Invalid coordinate format: {coord_str}") from e


def dms_to_decimal(dms: str) -> float:
    """
    Convert DMS string to decimal degrees.
    Args:
        dms: DMS format string (e.g., "37°46'30\"N")
    Returns: Decimal degrees
    Raises: ValueError for invalid format
    """
    import re

    pattern = r'(\d+)[°](\d+)[\']([\d.]+)["]([NSEW])'
    match = re.match(pattern, dms, re.IGNORECASE)

    if not match:
        raise ValueError(f"Invalid DMS format: {dms}")

    degrees = float(match.group(1))
    minutes = float(match.group(2))
    seconds = float(match.group(3))
    direction = match.group(4).upper()

    decimal = degrees + minutes / 60 + seconds / 3600
    if direction in ('S', 'W'):
        decimal = -decimal

    return decimal


def calculate_midpoint(coord1: GeoCoordinate, coord2: GeoCoordinate) -> GeoCoordinate:
    """
    Calculate the midpoint between two coordinates.
    Args:
        coord1: First coordinate
        coord2: Second coordinate
    Returns: Midpoint coordinate
    """
    return GeoCoordinate(
        latitude=(coord1.latitude + coord2.latitude) / 2,
        longitude=(coord1.longitude + coord2.longitude) / 2
    )


def normalize_bounds(latitude: float, longitude: float) -> Tuple[float, float]:
    """
    Ensure coordinates are within valid bounds.
    Args:
        latitude: Latitude value to normalize
        longitude: Longitude value to normalize
    Returns: Tuple of normalized (latitude, longitude)
    """
    lat = max(-90.0, min(90.0, latitude))
    lng = max(-180.0, min(180.0, longitude))
    return lat, lng


def main() -> None:
    """Demo usage of the GeoCoordinate library."""
    print("=== GeoCoordinate Precision Processor ===")
    print("Inspired by locfox.com location services\n")

    # San Francisco coordinates
    sf = GeoCoordinate(latitude=37.7749, longitude=-122.4194)

    print(f"San Francisco: {sf}")
    print(f"Valid: {sf.is_valid()}")
    print(f"DMS: {sf.to_dms()}\n")

    # Los Angeles coordinates
    la = GeoCoordinate(latitude=34.0522, longitude=-118.2437)

    # Distance calculation
    distance = sf.haversine_distance_to(la)
    print(f"Distance to Los Angeles: {distance:.2f} km\n")

    # Bounding box
    bbox = sf.create_bounding_box(10.0)
    print("Bounding box (10km radius):")
    print(f"  Min: {bbox.min_lat:.6f}, {bbox.min_lng:.6f}")
    print(f"  Max: {bbox.max_lat:.6f}, {bbox.max_lng:.6f}")
    print(f"  Contains LA: {bbox.contains(la)}")
    print(f"  Area: {bbox.area_sqkm():.2f} sq km\n")

    # Normalized precision
    precise = GeoCoordinate(latitude=37.7749123456, longitude=-122.4194123456)
    normalized = precise.normalize_precision(6)
    print(f"Normalized: {normalized}")


if __name__ == "__main__":
    main()
