import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from itertools import combinations
from scipy.spatial import cKDTree
import math
import time
import os
import warnings

# Ignore RuntimeWarnings that can sometimes occur in pandas/numpy operations
warnings.filterwarnings("ignore", category=RuntimeWarning)


# -----------------------------
# Helper functions
# -----------------------------

def are_parallel(coords1, coords2, tolerance_degrees=30.0):
    """
    Checks if two line segments (defined by their start and end coordinates)
    are approximately parallel based on the angle between them.

    Args:
        coords1 (np.array): Coordinates for the first line.
        coords2 (np.array): Coordinates for the second line.
        tolerance_degrees (float): Maximum angle difference (in degrees)
                                   to consider lines parallel.
    Returns:
        bool: True if parallel, False otherwise.
    """
    if len(coords1) < 2 or len(coords2) < 2: return False

    # Calculate vector components for line 1
    dx1 = coords1[-1][0] - coords1[0][0]
    dy1 = coords1[-1][1] - coords1[0][1]

    # Calculate vector components for line 2
    dx2 = coords2[-1][0] - coords2[0][0]
    dy2 = coords2[-1][1] - coords2[0][1]

    if (dx1 == 0 and dy1 == 0) or (dx2 == 0 and dy2 == 0): return False

    # Calculate angles in radians
    angle1 = math.atan2(dy1, dx1)
    angle2 = math.atan2(dy2, dx2)

    # Calculate the smaller angle between the two lines (0 to pi/2)
    diff_rad = abs(angle1 - angle2) % math.pi
    if diff_rad > math.pi / 2:
        diff_rad = math.pi - diff_rad
    diff_deg = math.degrees(diff_rad)

    return diff_deg <= tolerance_degrees


# -----------------------------
# Main Processing Function
# -----------------------------

def mark_intersections_kdtree(input_csv_path, radius=1.0, parallel_tolerance_deg=15.0, output_path=None):
    """
    Identifies intersection points between survey lines using a cKDTree spatial
    search, marks them, and calculates min/max mean signal and altitude flags.

    Args:
        input_csv_path (str): Path to the input CSV file.
        radius (float): Search radius (in coordinate units) for intersection.
        parallel_tolerance_deg (float): Angle threshold to skip parallel lines.
        output_path (str, optional): Directory path where the final CSV should be saved.
                                     If None, the file is saved in the current working directory.
    """
    start_time = time.time()
    print(f"Starting KD-Tree intersection marking for {os.path.basename(input_csv_path)}...")

    # Define column names for mean calculation
    SIGNAL_COL = "Signal"
    ALT_COL = "Alt"

    # 1. Load and prepare data
    df = pd.read_csv(input_csv_path)
    df = df.dropna(subset=["X", "Y"]).reset_index(drop=True)
    df['row_id'] = df.index.astype(int)  # Ensure row_id is contiguous after dropping NaNs

    # Check for required columns
    if SIGNAL_COL not in df.columns or ALT_COL not in df.columns:
        raise ValueError(f"Input CSV must contain '{SIGNAL_COL}' and '{ALT_COL}' columns for mean calculation.")

    line_ids = df["Line"].unique().tolist()
    print(f"\n{len(line_ids)} unique lines detected.")

    line_coords = {}
    line_info = {}  # Stores row_id, indexed by the line order

    # Pre-process coordinates and row info by line
    for line in line_ids:
        group = df[df["Line"] == line].sort_values(by='row_id')  # Ensure order is consistent
        line_coords[line] = group[["X", "Y"]].to_numpy()
        # Store just the row_id for easy lookup
        line_info[line] = group["row_id"].to_numpy()

        # 2. KD-Tree Search for Intersections and Mapping
    # Stores tuples of (row_id, unique_intersection_key, line_id)
    intersection_map = []
    total_match_pairs = 0

    print("Computing line intersections using cKDTree...")
    for line1, line2 in combinations(line_ids, 2):

        A_coords = line_coords[line1]
        B_coords = line_coords[line2]
        A_ids = line_info[line1]
        B_ids = line_info[line2]

        # Skip parallel lines early
        if are_parallel(A_coords, B_coords, tolerance_degrees=parallel_tolerance_deg):
            continue

        # Build tree on the second line's points
        tree = cKDTree(B_coords)
        # Query: find all points in A within radius 'r' of any point in B
        matches = tree.query_ball_point(A_coords, r=radius)

        n_matches = sum(len(js) for js in matches)
        if n_matches == 0:
            continue

        total_match_pairs += n_matches

        # Extract and store row IDs and the intersection key
        intersection_key = '_'.join(sorted([line1, line2]))

        for i, js in enumerate(matches):
            if js:
                # Point from Line 1 (A)
                intersection_map.append((A_ids[i], intersection_key, line1))

                # Points from Line 2 (B) that match A[i]
                for j in js:
                    intersection_map.append((B_ids[j], intersection_key, line2))

    # Define final flag column names
    # CHANGED: Lower signal (MIN) is 1
    FLAG_SIG = "is_minimum_signal_line"
    # CHANGED: Higher altitude (MAX) is 1
    FLAG_ALT = "is_maximum_alt_line"

    if not intersection_map:
        print("\nNo intersecting points found. Skipping mean calculation and flagging.")
        df["is_intersection"] = 0
        df[FLAG_SIG] = 0
        df[FLAG_ALT] = 0

    else:
        # 3. Create Intersection Map DataFrame and Merge Data
        # df_map has columns: row_id, key (intersection_key), Line
        df_map = pd.DataFrame(intersection_map, columns=['row_id', 'key', 'Line']).drop_duplicates()

        print(f"\nFound {total_match_pairs} point-pair matches in total.")
        print(f"Marking {df_map['row_id'].nunique()} unique survey points as intersecting.")

        # Merge signal and alt data from the main df
        df_map = df_map.merge(df[['row_id', SIGNAL_COL, ALT_COL]], on='row_id', how='left')

        # 4. Calculate Means and Min/Max Flags

        # Calculate means per line per intersection key
        line_means = df_map.groupby(['key', 'Line']).agg(
            mean_signal=(SIGNAL_COL, 'mean'),
            mean_alt=(ALT_COL, 'mean')
        ).reset_index()

        # Determine line with MINIMUM mean Signal for each key (User requested: lower signal = 1)
        line_means["min_signal"] = line_means.groupby("key")["mean_signal"].transform("min")
        line_means[FLAG_SIG] = (line_means["mean_signal"] == line_means["min_signal"]).astype(int)

        # Determine line with MAXIMUM mean Alt for each key (User requested: higher altitude = 1)
        line_means["max_alt"] = line_means.groupby("key")["mean_alt"].transform("max")
        line_means[FLAG_ALT] = (line_means["mean_alt"] == line_means["max_alt"]).astype(int)

        # 5. Determine the final point-level flags based on intersection results

        # Merge line_means (intersection results) back into df_map (point-to-key mapping)
        # This assigns the per-intersection flag back to the individual points.
        df_flagged_points = df_map.merge(line_means, on=['key', 'Line'], how='left')

        # Get the row_ids that should have FLAG_SIG = 1 and FLAG_ALT = 1
        # is_minimum_signal_line (lower signal)
        sig_row_ids = df_flagged_points[df_flagged_points[FLAG_SIG] == 1]['row_id'].unique()
        # is_maximum_alt_line (higher altitude)
        alt_row_ids = df_flagged_points[df_flagged_points[FLAG_ALT] == 1]['row_id'].unique()

        # 6. Mark the original DataFrame
        FLAG_COLUMN_INT = "is_intersection"

        # Initialize the flags
        df[FLAG_COLUMN_INT] = 0
        df[FLAG_SIG] = 0
        df[FLAG_ALT] = 0

        # Mark all points that belong to ANY intersection
        intersection_row_ids = df_map['row_id'].unique()
        df.loc[df['row_id'].isin(intersection_row_ids), FLAG_COLUMN_INT] = 1

        # Mark the signal flags (only points in sig_row_ids get 1, others remain 0)
        df.loc[df['row_id'].isin(sig_row_ids), FLAG_SIG] = 1

        # Mark the altitude flags (only points in alt_row_ids get 1, others remain 0)
        df.loc[df['row_id'].isin(alt_row_ids), FLAG_ALT] = 1

    # 7. Save Output
    base = os.path.splitext(os.path.basename(input_csv_path))[0]
    # UPDATED FILENAME to reflect MINSIG and MAXALT
    filename = f"{base}_marked_kdtree_{radius}m_MEAN_FLAGS_MINSIG_MAXALT.csv"

    # Determine output path: use specified path or save to current directory
    if output_path:
        # Ensure the output directory exists before saving
        os.makedirs(output_path, exist_ok=True)
        outpath = os.path.join(output_path, filename)
    else:
        outpath = filename  # Saves to current working directory

    # Save the full dataframe with the new intersection columns
    df.to_csv(outpath, index=False)

    end_time = time.time()
    elapsed = (end_time - start_time) / 60.0

    print(f"\n✔ Process complete. Saved: {outpath}")
    print(f"Total time taken: {elapsed:.2f} minutes.")

    return df


# -----------------------------
# Script Execution
# -----------------------------
if __name__ == "__main__":
    # --- Configuration ---
    # NOTE: The radius here is the maximum distance between points on two lines
    # to be considered part of an intersection zone.
    RADIUS_M = 0.5  # Set your desired radius here
    PARALLEL_TOLERANCE_DEG = 10.0  # Keep lines crossing at less than 10 degrees (or whatever you prefer)

    # --- Input Path ---
    #input_csv_path = "MAG_Channel_Turning_Processed_Data_Split_A0.csv"
    input_csv_path = r"C:\Users\NivedithaPoolla\Documents\P110-2025\P304-2024_Processed_Data_Split_A0.csv"

    # --- Output Path ---
    # Setting the user-requested output folder
    OUTPUT_FOLDER = r"C:\Users\NivedithaPoolla\Documents\P110-2025"

    try:
        df_marked = mark_intersections_kdtree(
            input_csv_path=input_csv_path,
            radius=RADIUS_M,
            parallel_tolerance_deg=PARALLEL_TOLERANCE_DEG,
            output_path=OUTPUT_FOLDER
        )
    except FileNotFoundError:
        print(f"\n--- ERROR ---")
        print(f"File not found at path: {input_csv_path}")
        print("Please ensure the CSV file is in the correct directory.")
    except Exception as e:
        print(f"\nAn unexpected error occurred during processing: {e}")