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

from typing import Optional

import plotly.graph_objects as go


class ElevationRenderer:
    """Render elevation profiles with gradient-based coloring."""

    def __init__(self) -> None:
        """Initialize elevation profile renderer."""
        self.color = {
            'steep_ascent': 'rgb(139, 69, 19)',
            'gentle_ascent': 'rgb(210, 180, 140)',
            'steep_descent': 'rgb(34, 139, 34)',
            'gentle_descent': 'rgb(154, 205, 50)',
        }

        self.slope = {
            'steep_ascent': [15, float('inf')],
            'gentle_ascent': [0, 15],
            'steep_descent': [float('-inf'), -20],
            'gentle_descent': [-20, 0],
        }

        self.label = {
            'steep_ascent': '陡上坡 (>= 15%)',
            'gentle_ascent': '緩上坡 (< 15%)',
            'steep_descent': '陡下坡 (<= -20%)',
            'gentle_descent': '緩下坡 (> -20%)',
        }

    def _get_gradient_category(self, gradient: float) -> str:
        """Determine gradient category based on slope thresholds.

        Args:
            gradient: Gradient value in percentage

        Returns:
            str: Category key ('steep_ascent', 'gentle_ascent', 'steep_descent', 'gentle_descent')
        """
        if gradient >= self.slope['steep_ascent'][0]:
            return 'steep_ascent'
        elif gradient >= self.slope['gentle_ascent'][0]:
            return 'gentle_ascent'
        elif gradient <= self.slope['steep_descent'][1]:
            return 'steep_descent'
        else:
            return 'gentle_descent'

    def _downsample_data(
        self, distances: list[float], elevations: list[float], gradients: list[float], max_points: int = 800
    ) -> tuple[list[float], list[float], list[float]]:
        """Downsample data if there are too many points.

        Args:
            distances: List of distances
            elevations: List of elevations
            gradients: List of gradients
            max_points: Maximum number of points to keep

        Returns:
            tuple: (downsampled_distances, downsampled_elevations, downsampled_gradients)
        """
        if len(distances) <= max_points:
            return distances, elevations, gradients

        # Calculate step size for uniform downsampling
        step = len(distances) / max_points

        # Use floating point indexing for smoother sampling
        indices = []
        for i in range(max_points):
            idx = int(i * step)
            if idx not in indices and idx < len(distances):
                indices.append(idx)

        # Always include first and last points
        if 0 not in indices:
            indices.insert(0, 0)
        if (len(distances) - 1) not in indices:
            indices.append(len(distances) - 1)

        downsampled_distances = [distances[i] for i in indices]
        downsampled_elevations = [elevations[i] for i in indices]

        # Recalculate gradients for downsampled data
        downsampled_gradients = []
        for i in range(1, len(indices)):
            idx_prev = indices[i - 1]
            idx_curr = indices[i]
            if idx_prev < len(gradients) and idx_curr <= len(gradients):
                # Use average gradient of the skipped segments
                avg_gradient = sum(gradients[idx_prev:idx_curr]) / max(1, idx_curr - idx_prev)
                downsampled_gradients.append(avg_gradient)

        return downsampled_distances, downsampled_elevations, downsampled_gradients

    def create_elevation_profile(self, distances: list[float], elevations: list[float], gradients: list[float]) -> Optional[go.Figure]:
        """Create an interactive elevation profile chart.

        Args:
            distances: List of cumulative distances in km
            elevations: List of elevations in meters
            gradients: List of gradients (slopes) in percentage

        Returns:
            plotly.graph_objects.Figure: Interactive elevation chart
        """
        if not distances or not elevations or len(distances) != len(elevations):
            return None

        # Downsample if too many points
        distances, elevations, gradients = self._downsample_data(distances, elevations, gradients)

        fig = go.Figure()

        # Add gradient list for first point (use 0 as placeholder)
        gradients_extended = [0] + gradients

        # Group consecutive segments with similar color into single traces
        # This dramatically reduces the number of traces
        i = 0
        while i < len(distances) - 1:
            gradient = gradients_extended[i + 1]

            # Determine color based on gradient category
            category = self._get_gradient_category(gradient)
            color = self.color[category]
            is_ascending = gradient > 0

            # Collect consecutive points with similar direction (ascending/descending)
            segment_x = [distances[i]]
            segment_y = [elevations[i]]
            segment_gradients = []

            j = i
            while j < len(distances) - 1:
                next_gradient = gradients_extended[j + 1]
                # Check if same direction (both positive or both negative/zero)
                if (next_gradient > 0) == is_ascending:
                    segment_x.append(distances[j + 1])
                    segment_y.append(elevations[j + 1])
                    segment_gradients.append(next_gradient)
                    j += 1
                else:
                    break

            # Add segment as a single trace with fill
            avg_gradient = sum(segment_gradients) / len(segment_gradients) if segment_gradients else gradient

            # Create fill color with 50% opacity (rgba format)
            # Extract RGB values from the color string
            rgb_values = color.replace('rgb(', '').replace(')', '').split(',')
            r, g, b = int(rgb_values[0]), int(rgb_values[1]), int(rgb_values[2])
            fill_color = f'rgba({r},{g},{b},0.5)'

            fig.add_trace(
                go.Scatter(
                    x=segment_x,
                    y=segment_y,
                    mode='lines',
                    line=dict(color=color, width=3),
                    fill='tozeroy',  # Fill to y=0
                    fillcolor=fill_color,  # Semi-transparent fill
                    hovertemplate=f'距離: %{{x:.2f}} km<br>海拔: %{{y:.0f}} m<br>平均坡度: {avg_gradient:.1f}%<extra></extra>',
                    showlegend=False,
                )
            )

            i = j

        # Update layout
        fig.update_layout(
            title='海拔剖面圖',
            xaxis_title='距離 (公里)',
            yaxis_title='海拔 (公尺)',
            hovermode='closest',
            template='plotly_white',
            height=400,
            xaxis=dict(showgrid=True, gridcolor='lightgray'),
            yaxis=dict(showgrid=True, gridcolor='lightgray'),
        )

        return fig

    def calculate_gradient_distribution(self, distances: list[float], gradients: list[float]) -> Optional[dict[str, float]]:
        """Calculate distance distribution across different gradient categories.

        Args:
            distances: List of cumulative distances in km
            gradients: List of gradients in percentage

        Returns:
            dict: Dictionary with distances for each category
        """
        if len(distances) < 2 or len(gradients) < 1:
            return None

        steep_ascent_km = 0.0  # >= 15%
        gentle_ascent_km = 0.0  # 0% to 15%
        steep_descent_km = 0.0  # <= -20%
        gentle_descent_km = 0.0  # -20% to 0%

        for i in range(len(gradients)):
            if i + 1 < len(distances):
                segment_distance = distances[i + 1] - distances[i]
                gradient = gradients[i]
                category = self._get_gradient_category(gradient)

                if category == 'steep_ascent':
                    steep_ascent_km += segment_distance
                elif category == 'gentle_ascent':
                    gentle_ascent_km += segment_distance
                elif category == 'steep_descent':
                    steep_descent_km += segment_distance
                else:  # gentle_descent
                    gentle_descent_km += segment_distance

        return {
            'steep_ascent': steep_ascent_km,
            'gentle_ascent': gentle_ascent_km,
            'steep_descent': steep_descent_km,
            'gentle_descent': gentle_descent_km,
        }

    def create_gradient_pie_chart(self, distances: list[float], gradients: list[float]) -> Optional[go.Figure]:
        """Create a pie chart showing gradient distribution.

        Args:
            distances: List of cumulative distances in km
            gradients: List of gradients in percentage

        Returns:
            plotly.graph_objects.Figure: Pie chart figure
        """
        stats = self.calculate_gradient_distribution(distances, gradients)

        if not stats:
            return None

        # Create pie chart
        fig = go.Figure(
            data=[
                go.Pie(
                    labels=list(self.label.values()),
                    values=list(stats.values()),
                    marker=dict(colors=list(self.color.values())),
                    textinfo='label+percent',
                    textposition='inside',
                    hovertemplate='%{value:.2f} km<extra></extra>',
                    showlegend=False,  # Don't show legend as requested
                )
            ]
        )

        fig.update_layout(
            title='',
            height=400,
            margin=dict(l=20, r=20, t=40, b=20),
        )

        return fig

    def add_gradient_legend(self, fig: go.Figure) -> go.Figure:
        """Add a custom legend explaining the color scheme.

        Args:
            fig: Plotly figure object

        Returns:
            plotly.graph_objects.Figure: Updated figure with legend
        """
        # Add dummy traces for legend
        fig.add_trace(
            go.Scatter(
                x=[None],
                y=[None],
                mode='lines',
                line=dict(color=self.color['steep_ascent'], width=3),
                name=self.label['steep_ascent'],
            )
        )

        fig.add_trace(
            go.Scatter(
                x=[None],
                y=[None],
                mode='lines',
                line=dict(color=self.color['gentle_ascent'], width=3),
                name=self.label['gentle_ascent'],
            )
        )

        fig.add_trace(
            go.Scatter(
                x=[None],
                y=[None],
                mode='lines',
                line=dict(color=self.color['steep_descent'], width=3),
                name=self.label['steep_descent'],
            )
        )

        fig.add_trace(
            go.Scatter(
                x=[None],
                y=[None],
                mode='lines',
                line=dict(color=self.color['gentle_descent'], width=3),
                name=self.label['gentle_descent'],
            )
        )

        fig.update_layout(showlegend=True, legend=dict(x=0.02, y=0.98))

        return fig
