# gpx.py
#
# author: deng
# date: 20251127

import gpxpy
import gpxpy.gpx
from geopy.distance import geodesic


class GPXProcessor:
    """Handle GPX file parsing and route analysis."""

    def __init__(self, gpx_file):
        """Initialize GPX processor with uploaded file.

        Args:
            gpx_file: File-like object containing GPX data
        """
        self.gpx_file = gpx_file
        self.gpx = None
        self.points = []
        self.elevations = []
        self.distances = []  # unit: km

    def validate_and_parse(self):
        """Validate and parse the GPX file.

        Returns:
            bool: True if valid GPX file, False otherwise

        Raises:
            Exception: If file cannot be parsed
        """
        try:
            gpx_data = self.gpx_file.read()
            if isinstance(gpx_data, bytes):
                gpx_data = gpx_data.decode('utf-8')

            self.gpx = gpxpy.parse(gpx_data)

            # Extract points from all tracks and segments
            for track in self.gpx.tracks:
                for segment in track.segments:
                    for point in segment.points:
                        self.points.append((point.latitude, point.longitude))
                        self.elevations.append(point.elevation if point.elevation else 0)

            return len(self.points) > 0

        except Exception as e:
            raise Exception(f'GPX 檔案解析失敗: {str(e)}')

    def calculate_distance(self):
        """Calculate total distance of the route.

        Returns:
            float: Total distance in kilometers
        """
        if len(self.points) < 2:
            return 0.0

        total_distance = 0.0
        self.distances = [0.0]  # Start at 0 km

        for i in range(1, len(self.points)):
            segment_distance = geodesic(self.points[i - 1], self.points[i]).kilometers
            total_distance += segment_distance
            self.distances.append(total_distance)

        return total_distance

    def calculate_elevation_gain_loss(self, threshold=3.0):
        """Calculate total elevation gain and loss with noise filtering.

        GPS altitude data is notoriously inaccurate. Small fluctuations (noise) can
        accumulate to create inflated elevation gain/loss values. This method uses
        a threshold to filter out GPS noise.

        Args:
            threshold: Minimum elevation change in meters to be counted (default: 3.0)
                      This filters out GPS noise while preserving real elevation changes.

        Returns:
            tuple: (elevation_gain, elevation_loss) in meters
        """
        if len(self.elevations) < 2:
            return 0.0, 0.0

        gain = 0.0
        loss = 0.0

        # Use cumulative approach to avoid missing real elevation changes
        # Track cumulative change since last significant threshold crossing
        cumulative_change = 0.0

        for i in range(1, len(self.elevations)):
            diff = self.elevations[i] - self.elevations[i - 1]
            cumulative_change += diff

            # Only count if cumulative change exceeds threshold
            if cumulative_change >= threshold:
                gain += cumulative_change
                cumulative_change = 0.0
            elif cumulative_change <= -threshold:
                loss += abs(cumulative_change)
                cumulative_change = 0.0

        return gain, loss

    def get_min_max_elevation(self):
        """Get minimum and maximum elevation.

        Returns:
            tuple: (min_elevation, max_elevation) in meters
        """
        if not self.elevations:
            return 0.0, 0.0

        return min(self.elevations), max(self.elevations)

    def get_start_end_points(self):
        """Get start and end point coordinates.

        Returns:
            tuple: ((start_lat, start_lon), (end_lat, end_lon))
        """
        if len(self.points) < 2:
            return None, None

        return self.points[0], self.points[-1]

    def get_all_points(self):
        """Get all route points.

        Returns:
            list: List of (latitude, longitude) tuples
        """
        return self.points

    def get_elevation_profile_data(self):
        """Get data for elevation profile visualization.

        Returns:
            tuple: (distances, elevations) where distances is cumulative km
        """
        return self.distances, self.elevations

    def get_gradients(self):
        """Calculate gradient (slope) for each segment.

        Returns:
            list: List of gradients in percentage
        """
        if len(self.points) < 2:
            return []

        gradients = []
        for i in range(1, len(self.points)):
            distance_m = geodesic(self.points[i - 1], self.points[i]).meters
            elevation_diff = self.elevations[i] - self.elevations[i - 1]

            if distance_m > 0:
                gradient = (elevation_diff / distance_m) * 100
            else:
                gradient = 0

            gradients.append(gradient)

        return gradients

    def calculate_naismith_time(self):
        """Calculate estimated hiking time using Naismith's Rule.

        Naismith's Rule:
        - Base time: 1 hour per 5 km of horizontal distance
        - Add time: 1 hour per 600 meters of ascent

        Returns:
            int: Estimated time in minutes
        """
        if len(self.points) < 2:
            return 0

        # Get total distance and elevation gain
        total_distance = self.calculate_distance() if not self.distances else self.distances[-1]
        elevation_gain, _ = self.calculate_elevation_gain_loss()

        # Naismith's Rule calculation
        # 1 hour per 5 km = 0.2 hours per km
        time_for_distance = total_distance * 0.2

        # 1 hour per 600 meters of ascent
        time_for_ascent = elevation_gain / 600.0

        total_time = int((time_for_distance + time_for_ascent) * 60)

        return total_time
