#For PyCharm
from geosoft import gxpy

gxc = gxpy.gx.GXpy()  # Initialize Geosoft context first

# -----------------------------
# New/Updated Imports for Optimization
# -----------------------------
import geosoft.gxpy.gdb as gxdb
import geopandas as gpd
from shapely.geometry import Point, LineString
import pandas as pd
import numpy as np
from itertools import combinations
import math
import os
import time

# Suppress GeoPandas/Shapely warnings
import warnings

warnings.filterwarnings("ignore", category=UserWarning)


# -----------------------------
# Helper functions (Kept from original)
# -----------------------------
def flatten(arr):
    """Flatten nested lists/arrays from Geosoft VV channels"""
    out = []
    for item in arr:
        if item is None:
            continue
        if isinstance(item, (list, np.ndarray)):
            out.extend(flatten(item))
        else:
            try:
                out.append(float(item))
            except:
                pass
    return np.array(out, dtype=float)


def are_parallel(line1, line2, tolerance_degrees=30.0):
    """Checks if two LineString objects are approximately parallel."""
    coords1 = line1.coords
    coords2 = line2.coords

    if len(coords1) < 2 or len(coords2) < 2: return False

    dx1 = coords1[-1][0] - coords1[0][0]
    dy1 = coords1[-1][1] - coords1[0][1]
    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

    angle1 = math.atan2(dy1, dx1)
    angle2 = math.atan2(dy2, dx2)
    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


def clean_points(pts):
    """Removes NaN points and sequential duplicate points."""
    cleaned = []
    last = None
    for p in pts:
        if not (math.isnan(p.x) or math.isnan(p.y)):
            if last is None or (p.x != last.x or p.y != last.y):
                cleaned.append(p)
                last = p
    return cleaned


# -----------------------------
# User parameters
# -----------------------------
t_start = time.time()
gdb_path = r"D:\P110-2025\Database\P304-2024_Split_A1.gdb"
check_column = "Signal"
alt_column = "Alt"
buffer_distance = 0.5
parallel_tolerance_deg = 10.0

print(f"Opening GDB: {gdb_path}")

# -----------------------------
# Read GDB lines/channels (No change needed here)
# -----------------------------
records = []
with gxdb.Geosoft_gdb.open(gdb_path) as gdb:
    lines = gdb.list_lines()
    print(f"Found {len(lines)} lines.")
    for idx, line in enumerate(lines, start=1):
        gdb.current_line = line
        chs = gdb.list_channels()
        if not all(c in chs for c in ["X", "Y", check_column, alt_column]):
            print(f"[{idx}/{len(lines)}] Skipped line {line} (missing channels)")
            continue
        X = flatten(gdb.read_channel(line, "X"))
        Y = flatten(gdb.read_channel(line, "Y"))
        S = flatten(gdb.read_channel(line, check_column))
        Alt = flatten(gdb.read_channel(line, alt_column))
        n = min(len(X), len(Y), len(S), len(Alt))
        for i in range(n):
            records.append({
                "row_id": len(records),  # Add unique row_id for stable merge later
                "Line": line,
                "X": X[i],
                "Y": Y[i],
                check_column: S[i],
                alt_column: Alt[i]
            })

df = pd.DataFrame(records)

# -----------------------------
# BUILD GEODATAFRAMES (The Optimization Base)
# -----------------------------

# 1. Create GeoDataFrame of Points
geometry_points = [Point(x, y) for x, y in zip(df["X"], df["Y"])]
gdf_points = gpd.GeoDataFrame(
    df.assign(geometry=geometry_points),
    geometry='geometry',
    crs="EPSG:4326"  # Using a dummy CRS
)

# 2. Build Line Geometries
line_geoms = {}
for line, group in gdf_points.groupby("Line"):
    pts = clean_points(list(group.sort_values("row_id").geometry))
    if len(pts) >= 2:
        try:
            ls = LineString(pts)
            if ls.is_valid:
                line_geoms[line] = ls
        except:
            continue

# 3. Create GeoDataFrame of Lines (for spatial indexing)
gdf_lines = gpd.GeoDataFrame(
    {'Line': line_geoms.keys(), 'geometry': line_geoms.values()},
    geometry='geometry',
    crs=gdf_points.crs
).reset_index(drop=True)

# -----------------------------
# OPTIMIZED INTERSECTION DETECTION
# -----------------------------
intersection_data = []
sindex = gdf_lines.sindex  # Build R-tree index

print(f"\nComputing line intersections using Spatial Index on {len(gdf_lines)} lines...")
# Iterate over lines and query the index for potential neighbors (bounding box overlap)
for i in range(len(gdf_lines)):
    line_i = gdf_lines.loc[i]
    possible_matches_index = list(sindex.intersection(line_i.geometry.bounds))

    for j in possible_matches_index:
        # Check each pair once (j > i) and skip self-check
        if i < j:
            line_j = gdf_lines.loc[j]

            # Check parallel lines
            if are_parallel(line_i.geometry, line_j.geometry, tolerance_degrees=parallel_tolerance_deg):
                continue

            # Check for intersection
            if line_i.geometry.intersects(line_j.geometry):
                inter = line_i.geometry.intersection(line_j.geometry)
                if not inter.is_empty:
                    buf = inter.buffer(buffer_distance)
                    intersection_data.append({
                        'line_id1': line_i['Line'],
                        'line_id2': line_j['Line'],
                        'intersecting_lines_pair': f"{line_i['Line']}_{line_j['Line']}",
                        'geometry': buf
                    })

if not intersection_data:
    print("No line intersections found. Skipping mark/writeback.")
    exit()  # Exit early if no work is needed

# GeoDataFrame of all intersection buffers
gdf_buffers = gpd.GeoDataFrame(
    intersection_data,
    geometry='geometry',
    crs=gdf_points.crs
)
print(f"Identified {len(gdf_buffers)} unique intersection buffers.")

# -----------------------------
# OPTIMIZED POINT MARKING (SPATIAL JOIN)
# -----------------------------

# Vectorized Spatial Join: find all points contained within any buffer
joined_points = gpd.sjoin(
    gdf_points,
    gdf_buffers,
    how="inner",
    predicate="within"  # Check if the point is within the buffer polygon
)

# Get the unique original row_ids and the corresponding intersecting_lines_pair
intersecting_rows = (
    joined_points[['row_id', 'intersecting_lines_pair']]
    .drop_duplicates(subset=['row_id'])
    .set_index('row_id')
    .rename(columns={'intersecting_lines_pair': 'intersecting_lines'})
)

# Merge results back to the original DataFrame
df['intersection'] = 0
df['intersecting_lines'] = None
df.loc[intersecting_rows.index, 'intersection'] = 1
df.loc[intersecting_rows.index, 'intersecting_lines'] = intersecting_rows['intersecting_lines'].values

# -----------------------------
# Highest mean Signal (No change needed, using optimized df)
# -----------------------------
print("Calculating mean signal and altitude...")
sig_means = df.groupby(["intersecting_lines", "Line"])[check_column].mean().reset_index()
sig_means["min_sig"] = sig_means.groupby("intersecting_lines")[check_column].transform("min")
sig_means["is_highest_mean_Signal"] = (sig_means[check_column] == sig_means["min_sig"]).astype(int)

df = df.merge(
    sig_means[["intersecting_lines", "Line", "is_highest_mean_Signal"]],
    on=["intersecting_lines", "Line"],
    how="left"
).fillna({"is_highest_mean_Signal": 0})

# -----------------------------
# Lowest mean Alt (No change needed, using optimized df)
# -----------------------------
alt_means = df.groupby(["intersecting_lines", "Line"])[alt_column].mean().reset_index()
alt_means["max_alt"] = alt_means.groupby("intersecting_lines")[alt_column].transform("max")
alt_means["is_lowest_mean_Alt"] = (alt_means[alt_column] == alt_means["max_alt"]).astype(int)

df = df.merge(
    alt_means[["intersecting_lines", "Line", "is_lowest_mean_Alt"]],
    on=["intersecting_lines", "Line"],
    how="left"
).fillna({"is_lowest_mean_Alt": 0})

# -----------------------------
# Write back to GDB (Final Correction)
# -----------------------------
t_start_write = time.time()
print("\nWriting channels back to GDB...")

# Define only the final NUMERIC channels to write back
NUMERIC_CHANNELS_TO_WRITE = [
    "intersection",
    "is_highest_mean_Signal",
    "is_lowest_mean_Alt"
]

# --- FIX APPLIED HERE: Removed the 'mode' argument ---
with gxdb.Geosoft_gdb.open(gdb_path) as gdb:
    # 1. DELETE AND CREATE ALL NUMERIC CHANNELS ONCE
    for ch in NUMERIC_CHANNELS_TO_WRITE:
        if ch in gdb.list_channels():
            # This requires write access
            gdb.delete_channel(ch)

        # This requires write access and ensures the channel exists
        gdb.new_channel(ch, dtype=np.float32)

        # 2. WRITE DATA LINE BY LINE
    for line in gdb.list_lines():
        gdb.current_line = line
        chunk = df[df["Line"] == line]
        if chunk.empty: continue

        # Write data for all three numeric columns
        gdb.write_channel(line, "intersection", chunk["intersection"].astype(float).tolist())
        gdb.write_channel(line, "is_highest_mean_Signal", chunk["is_highest_mean_Signal"].astype(float).tolist())
        gdb.write_channel(line, "is_lowest_mean_Alt", chunk["is_lowest_mean_Alt"].astype(float).tolist())

t_end_write = time.time()
print("\n✔ Updated GDB successfully!")
print(f"Channels written: {', '.join(NUMERIC_CHANNELS_TO_WRITE)}")
print(f"Writeback time: {t_end_write - t_start_write:.2f} seconds.")