#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Jul 30th 2025

@author: Marcel Weschke
@email: marcel.weschke@directbox.de
"""
# %% Load libraries
import os
import base64
import io
import datetime
from math import radians, sin, cos, sqrt, asin

import dash
from dash import dcc, html, Input, Output, Dash
import dash_bootstrap_components as dbc

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from scipy.interpolate import interp1d
import gpxpy


# === Helper Functions ===
def list_gpx_files():
    folder = './gpx_files'
    #return [{'label': f, 'value': os.path.join(folder, f)} for f in os.listdir(folder) if f.endswith('.gpx')]
    files = [f for f in os.listdir(folder) if f.endswith('.gpx')]

    # Extract date from the start of the filename and sort descending
    def extract_date(filename):
        try:
            return datetime.datetime.strptime(filename[:10], '%Y-%m-%d')
        except ValueError:
            return datetime.datetime.min  # Put files without a valid date at the end

    files.sort(key=extract_date, reverse=True)

    return [{'label': f, 'value': os.path.join(folder, f)} for f in files]



def haversine(lon1, lat1, lon2, lat2):
    R = 6371
    dlon = radians(lon2 - lon1)
    dlat = radians(lat2 - lat1)
    a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
    return 2 * R * asin(sqrt(a))


def process_gpx(file_path):
    with open(file_path, 'r') as gpx_file:
        gpx = gpxpy.parse(gpx_file)

    points = gpx.tracks[0].segments[0].points
    df = pd.DataFrame([{
        'lat': p.latitude,
        'lon': p.longitude,
        'elev': p.elevation,
        'time': p.time
    } for p in points])

    # Basic cleanup
    df['time'] = pd.to_datetime(df['time'])
    df['time_loc'] = df['time'].dt.tz_localize(None)
    df['time_diff'] = df['time'] - df['time'][0]
    df['time_diff_sec'] = df['time_diff'].dt.total_seconds()
    df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0])
    # Cumulative distance (km)
    distances = [0]
    for i in range(1, len(df)):
        d = haversine(df.loc[i-1, 'lon'], df.loc[i-1, 'lat'], df.loc[i, 'lon'], df.loc[i, 'lat'])
        distances.append(distances[-1] + d)
    df['cum_dist_km'] = distances
    # Elevation and elevation change
    df['elev'] = df['elev'].bfill()
    df['delta_elev'] = df['elev'].diff().fillna(0)
    df['rel_elev'] = df['elev'] - df['elev'].iloc[0]
    # Velocity (used in pace and speed)
    df['vel_kmps'] = np.gradient(df['cum_dist_km'], df['time_diff_sec'])
    # Speed calculation (km/h) via distance and time diffs
    df['delta_t'] = df['time'].diff().dt.total_seconds()
    df['delta_d'] = df['cum_dist_km'].diff()
    df['speed_kmh'] = (df['delta_d'] / df['delta_t']) * 3600
    df['speed_kmh'] = df['speed_kmh'].replace([np.inf, -np.inf], np.nan)
    # Smoothed speed (Gaussian rolling)
    df['speed_kmh_smooth'] = df['speed_kmh'].rolling(window=10, win_type="gaussian", center=True).mean(std=2)

    return df


# =============================================================================
# INFO BANNER
# =============================================================================
def create_info_banner(df):
    # Total distance in km
    total_distance_km = df['cum_dist_km'].iloc[-1]

    # Total time as timedelta
    total_seconds = df['time_diff_sec'].iloc[-1]
    hours, remainder = divmod(int(total_seconds), 3600)
    minutes, seconds = divmod(remainder, 60)
    formatted_total_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}"

    # Average pace (min/km)
    if total_distance_km > 0:
        pace_sec_per_km = total_seconds / total_distance_km
        pace_min = int(pace_sec_per_km // 60)
        pace_sec = int(pace_sec_per_km % 60)
        formatted_pace = f"{pace_min}:{pace_sec:02d} min/km"
    else:
        formatted_pace = "N/A"

    # Build the info banner layout
    info_banner = html.Div([
        html.Div([
            html.H4("Total Distance", style={'margin-bottom': '5px'}),
            html.H2(f"{total_distance_km:.2f} km")
        ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),

        html.Div([
            html.H4("Total Time", style={'margin-bottom': '5px'}),
            html.H2(formatted_total_time)
        ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),

        html.Div([
            html.H4("Average Pace", style={'margin-bottom': '5px'}),
            html.H2(formatted_pace)
        ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),
    ], style={
        'display': 'flex',
        'justifyContent': 'space-around',
        'backgroundColor': '#1e1e1e',
        'color': 'white',
        'padding': '20px',
        'marginBottom': '5px',
        'borderRadius': '10px',
        'width': '100%',
        #'maxWidth': '1200px',
        'margin': 'auto'
    })

    return info_banner
  

# =============================================================================
# START OF THE PLOTS
# =============================================================================
def create_map_plot(df):
    # fig = px.line_map(
    #     df,
    #     lat='lat',
    #     lon='lon',
    #     hover_name='time',
    #     hover_data={
    #         'cum_dist_km': ':.2f',
    #         'duration_hms': True,
    #         'lat': False,
    #         'lon': False,
    #         'time': False
    #     },
    #     labels={
    #         'cum_dist_km': 'Distance (km) ',
    #         'duration_hms': 'Elapsed Time '
    #     },
    #     zoom=13,
    #     height=800
    # )
    fig = px.line_map(
    df,
    lat='lat',
    lon='lon',
    zoom=13,
    height=800
    )
    
    fig.update_traces(
        hovertemplate=(
            #"Time: %{customdata[0]}<br>" +
            "Distance (km): %{customdata[1]:.2f}<br>" +
            "Elapsed Time: %{customdata[2]}<extra></extra>"
        ),
        customdata=df[['time', 'cum_dist_km', 'duration_hms']]
    )
    # Define map style and the line ontop
    fig.update_layout(map_style="open-street-map")
    # The built-in plotly.js styles are: carto-darkmatter, carto-positron, open-street-map, stamen-terrain, stamen-toner, stamen-watercolor, white-bg
    # The built-in Mapbox styles are: basic, streets, outdoors, light, dark, satellite, satellite-streets
    fig.update_traces(line=dict(color="#f54269", width=3))

    # Start / Stop marker
    start = df.iloc[0]
    end = df.iloc[-1]
    fig.add_trace(go.Scattermap(
        lat=[start['lat']], lon=[start['lon']], mode='markers+text',
        marker=dict(size=12, color='#fca062'), text=['Start'], name='Start', textposition='bottom left'
    ))
    fig.add_trace(go.Scattermap(
        lat=[end['lat']], lon=[end['lon']], mode='markers+text',
        marker=dict(size=12, color='#b9fc62'), text=['Stop'], name='Stop', textposition='bottom left'
    ))
    fig.update_layout(paper_bgcolor='#1e1e1e', font=dict(color='white'))
    fig.update_layout(
    legend=dict(
        orientation='h',         # horizontal layout
        yanchor='top',
        y=-0.01,                  # move legend below the map
        xanchor='center',
        x=0.5,
        font=dict(color='white')
    )
)
    return fig




######################
# NEUE VERSION:
def create_elevation_plot(df, smooth_points=500):
    # Originale Daten
    x = df['time']
    y = df['rel_elev']

    # Einfache Glättung: nur Y-Werte glätten, X-Werte beibehalten
    if len(y) >= 4:  # Genug Punkte für cubic interpolation
        y_numeric = y.to_numpy()

        # Nur gültige Y-Punkte für Interpolation
        mask = ~np.isnan(y_numeric)

        if np.sum(mask) >= 4:  # Genug gültige Punkte
            # Index-basierte Interpolation für Y-Werte
            valid_indices = np.where(mask)[0]
            valid_y = y_numeric[mask]

            # Interpolation über die Indizes
            f = interp1d(valid_indices, valid_y, kind='cubic',
                        bounds_error=False, fill_value='extrapolate')

            # Neue Y-Werte für alle ursprünglichen X-Positionen
            all_indices = np.arange(len(y))
            y_smooth = f(all_indices)

            # Originale X-Werte beibehalten
            x_smooth = x
        else:
            # Fallback: originale Daten
            x_smooth, y_smooth = x, y
    else:
        # Zu wenige Punkte: originale Daten verwenden
        x_smooth, y_smooth = x, y

    fig = go.Figure()

    # Fläche unter der Kurve (mit geglätteten Daten)
    fig.add_trace(go.Scatter(
        x=x_smooth, y=y_smooth,
        mode='lines',
        line=dict(color='#1CAF50'),      # Fill between color!
        fill='tozeroy',
        #fillcolor='rgba(226, 241, 248)',
        hoverinfo='skip',
        showlegend=False
    ))

    # Hauptlinie (geglättet)
    fig.add_trace(go.Scatter(
        x=x_smooth, y=y_smooth,
        mode='lines',
        line=dict(color='#084C20', width=2), # Line color!
        name='Elevation',
        showlegend=False
    ))

    # SUPDER IDEE, ABER GEHT NICHT WEGE NEUEN smoothed POINTS! GEHT NUR BEI X
    #fig.update_traces(
    #    hovertemplate=(
    #        #"Time: %{customdata[0]}<br>" +
    #        "Distance (km): %{customdata[0]:.2f}<br>" +
    #        "Elevation: %{customdata[1]}<extra></extra>" +
    #        "Elapsed Time: %{customdata[2]}<extra></extra>"
    #    ),
    #    customdata=df[['cum_dist_km','elev', 'time']]
    #

    # Layout im Dark Theme
    fig.update_layout(
        title=dict(text='Höhenprofil relativ zum Startwert', font=dict(size=16, color='white')),
        xaxis_title='Zeit',
        yaxis_title='Höhe relativ zum Start (m)',
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=50, b=40),
        height=400
    )

    return fig



def create_deviation_plot(df):  #Distanz-Zeit-Diagramm
    # Compute mean velocity in km/s
    vel_kmps_mean = df['cum_dist_km'].iloc[-1] / df['time_diff_sec'].iloc[-1]
    # Expected cumulative distance assuming constant mean velocity
    df['cum_dist_km_qmean'] = df['time_diff_sec'] * vel_kmps_mean
    # Deviation from mean velocity distance
    df['del_dist_km_qmean'] = df['cum_dist_km'] - df['cum_dist_km_qmean']
    # Plot the deviation
    fig = px.line(
        df,
        x='time_loc',
        y='del_dist_km_qmean',
        labels={
            'time_loc': 'Zeit',
            'del_dist_km_qmean': 'Δ Strecke (km)'
        },
        template='plotly_dark',
    )
    fig.update_layout(
        title=dict(text='Abweichung von integriertem Durchschnittstempo', font=dict(size=16)),
        yaxis_title='Abweichung (km)',
        xaxis_title='Zeit',
        height=400,
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white', size=14),
        margin=dict(l=40, r=40, t=50, b=40)
    )
    # Add horizontal reference line at y=0
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=0,
        y1=0,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )
    return fig


def create_speed_plot(df):
    mask = df['speed_kmh_smooth'].isna()
    mean_speed_kmh = df['speed_kmh'].mean()
    fig = go.Figure()
    fig.add_trace(go.Scatter(
        x=df['time'][~mask],
        y=df['speed_kmh_smooth'][~mask],
        mode='lines',
        name='Geglättete Geschwindigkeit',
        line=dict(color='royalblue')
    ))
    fig.update_layout(
        title=dict(text=f'Tempo über die Zeit (geglättet) - Durchschnittstempo: {mean_speed_kmh:.2f} km/h', font=dict(size=16)),
        xaxis=dict(title='Zeit', tickformat='%H:%M', type='date'),
        yaxis=dict(title='Geschwindigkeit (km/h)', rangemode='tozero'),
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=40, b=40)
    )
    # Add horizontal reference line at y=mean_speed_kmh
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=mean_speed_kmh,
        y1=mean_speed_kmh,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )
    return fig



def create_pace_bars_plot(df):
    # Ensure time column is datetime
    if not pd.api.types.is_datetime64_any_dtype(df['time']):
        df['time'] = pd.to_datetime(df['time'], errors='coerce')

    # Assign km segments
    df['km'] = df['cum_dist_km'].astype(int)

    # Time in seconds from start
    df['time_sec'] = (df['time'] - df['time'].iloc[0]).dt.total_seconds()

    # Step 3: Compute pace manually per km group
    df['km_start'] = np.nan
    df['segment_len'] = np.nan
    df['pace_min_per_km'] = np.nan

    for km_val, group in df.groupby('km'):
        dist_start = group['cum_dist_km'].iloc[0]
        dist_end = group['cum_dist_km'].iloc[-1]
        segment_len = dist_end - dist_start

        time_start = group['time_sec'].iloc[0]
        time_end = group['time_sec'].iloc[-1]
        elapsed_time_sec = time_end - time_start

        if segment_len > 0:
            pace_min_per_km = (elapsed_time_sec / 60) / segment_len
        else:
            pace_min_per_km = np.nan

        df.loc[group.index, 'km_start'] = km_val
        df.loc[group.index, 'segment_len'] = segment_len
        df.loc[group.index, 'pace_min_per_km'] = pace_min_per_km

    # Clean types
    df['km_start'] = df['km_start'].astype(int)
    df['segment_len'] = df['segment_len'].astype(float)
    df['pace_min_per_km'] = pd.to_numeric(df['pace_min_per_km'], errors='coerce')

    # Step 4: Create Plotly bar chart
    fig = go.Figure()
    fig.add_trace(go.Bar(
        x=df['km_start'],
        y=df['pace_min_per_km'],
        width=df['segment_len'],
        text=[f"{v:.1f} min/km" if pd.notnull(v) else "" for v in df['pace_min_per_km']],
        #textposition='outside',
        textposition='inside',
        marker_color='#125595',
        name='Pace pro km',
        offset=0
    ))




    # #########
    # Add horizontal reference line - X-Werte für gesamte Breite
    # Calculate average pace
    total_distance_km = df['cum_dist_km'].iloc[-1]
    total_seconds = df['time_diff_sec'].iloc[-1]

    # Average pace (min/km) - KORRIGIERT
    if total_distance_km > 0:
        pace_sec_per_km = total_seconds / total_distance_km
        pace_min_per_km = pace_sec_per_km / 60  # Konvertiere zu Minuten pro km
    else:
        pace_min_per_km = 0

    fig.add_shape(
        type='line',
        x0=0,  # Start bei 0
        x1=total_distance_km,  # Ende bei maximaler Distanz
        y0=pace_min_per_km,
        y1=pace_min_per_km,
        line=dict(color='gray', width=1, dash='dash'),
    )

    ## Optional: Text-Annotation für die durchschnittliche Pace
    #fig.add_annotation(
    #    x=total_distance_km * 0.8,  # Position bei 80% der Distanz
    #    y=pace_min_per_km,
    #    text=f"Ø {pace_min_per_km:.1f} min/km",
    #    showarrow=True,
    #    arrowhead=2,
    #    arrowcolor="gray",
    #    bgcolor="rgba(255,0,0,0.1)",
    #    bordercolor="gray",
    #    font=dict(color="white")
    #)




    fig.update_layout(
        title=dict(text='Tempo (min/km) je Kilometer', font=dict(size=16)),
        xaxis_title='Distanz (km)',
        yaxis_title='Minuten pro km',
        barmode='overlay',
        bargap=0,
        bargroupgap=0,
        xaxis=dict(
            type='linear',
            range=[0, df['cum_dist_km'].iloc[-1]],
            tickmode='linear',
            dtick=1,
            showgrid=True
        ),
        template='plotly_dark',
        height=400,
        margin=dict(l=40, r=40, t=30, b=40),
        plot_bgcolor='#111111',
        paper_bgcolor='#1e1e1e',
        font=dict(color='white')
    )
    return fig





# === App Setup ===
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SLATE])
app.title = "GPX Dashboard"

app.layout = html.Div([
    html.H1("Running Dashboard", style={'textAlign': 'center'}),
    dcc.Store(id='stored-df'),

    html.Div([
        html.Label("GPX-Datei wählen:", style={'color': 'white'}),
        dcc.Dropdown(
            id='gpx-file-dropdown',
            options=list_gpx_files(),
            value=list_gpx_files()[0]['value'],
            clearable=False,
            style={'width': '300px', 'color': 'black'}
        )
    ], style={'padding': '20px', 'backgroundColor': '#1e1e1e'}),

    html.Div(id='info-banner'),
    dcc.Graph(id='fig-map'),
    dcc.Graph(id='fig-elevation'),
    dcc.Graph(id='fig_deviation'),
    dcc.Graph(id='fig_speed'),
    dcc.Graph(id='fig_pace_bars')
])


# === Callbacks ===
# Callback 1: Load GPX File and Store as JSON
@app.callback(
    Output('stored-df', 'data'),
    Input('gpx-file-dropdown', 'value')
)
def load_gpx_data(path):
    df = process_gpx(path)
    return df.to_json(date_format='iso', orient='split')

# Callback 2: Update All Plots
@app.callback(
    Output('info-banner', 'children'),
    Output('fig-map', 'figure'),
    Output('fig-elevation', 'figure'),
    Output('fig_deviation', 'figure'),
    Output('fig_speed', 'figure'),
    Output('fig_pace_bars', 'figure'),
    Input('stored-df', 'data')
)
def update_all_plots(json_data):
    df = pd.read_json(io.StringIO(json_data), orient='split')

    info = create_info_banner(df)
    fig_map = create_map_plot(df)
    fig_elev = create_elevation_plot(df)
    fig_dev = create_deviation_plot(df)
    fig_speed = create_speed_plot(df)
    fig_pace = create_pace_bars_plot(df)

    return info, fig_map, fig_elev, fig_dev, fig_speed, fig_pace


# === Run Server ===
if __name__ == '__main__':
    app.run(debug=True, port=8051)

    
# NOTE:
#    Zusammenhang zwischen Pace und Geschwindigkeit
#     - Pace = Minuten pro Kilometer (z. B. 5:40/km)
#     - Geschwindigkeit = Kilometer pro Stunde (z. B. 10.71 km/h)
# 
