import pandas as pd
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import numpy as np
import pathlib
from typing import Sequence
import random
from aiRNN import preprocessors

class BaseDataset(Dataset):
    def __init__(
        self,
        filepaths,
        n_input,
        n_output,
        n_windows_per_file,
        step=1,
        feature_columns=("lat", "lon", "alt", "ias"),
        context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
        time_columns=("timestamp",),
        target_columns=("lat", "lon", "alt"),
        device="cpu",
    ):
        super().__init__()
        self.filepaths = [pathlib.Path(fp) for fp in filepaths]
        self.n_input = n_input
        self.n_output = n_output
        self.window_size = n_input + n_output
        self.device = device
        self.n_windows_per_file = n_windows_per_file
        self.step = step

        # Which columns to use
        sample = pd.read_csv(self.filepaths[0], nrows=5)
        sample = self._modify_df(sample)
        self.feature_cols = list(feature_columns)
        self.context_cols = list(context_columns)
        self.time_cols = list(time_columns)
        self.target_cols = list(target_columns)

        # Total samples = windows_per_file * number_of_files
        self.total_windows = n_windows_per_file * len(self.filepaths)
        for col_list in [self.feature_cols, self.context_cols, self.time_cols, self.target_cols]:
            assert all(
                col in sample.columns for col in col_list
            ), "Some specified columns not found in data."

    def __len__(self):
        return self.total_windows
    
    def _get_start_end_indices(self, idx, n_rows):
        # Max valid start index so window stays inside the file
        max_start = n_rows - self.window_size * self.step
        if max_start < 0:
            raise ValueError("File too small for one window.")

        # Evenly spaced index:
        # raw_start = (n_rows / n_windows_per_file) * local_idx
        # mapped into integer space
        raw_start = (n_rows / self.n_windows_per_file) * idx
        start_idx = int(raw_start // self.step) * self.step

        # Clamp to safe zone
        if start_idx > max_start:
            start_idx = max_start

        end_idx = start_idx + self.window_size * self.step
        return start_idx, end_idx

    def _modify_df(self, df):
        """Hook for subclasses to modify dataframe before slicing windows."""
        lat_norm, lon_norm, alt_norm = preprocessors.norm_coords(
            df["lat"].values, df["lon"].values, df["alt"].values
        )
        df.loc[:, "lat"] = lat_norm
        df.loc[:, "lon"] = lon_norm
        df.loc[:, "alt"] = alt_norm
        df.loc[:, "ias"] = preprocessors.norm_ias(df["ias"].values)
        df.loc[:, "dt"] = df["timestamp"].diff()
        df.loc[:, "dt"] = preprocessors.fillna_with_mean(df["dt"], allow_nan_mean=False)
        df.loc[:, "timestamp"] = preprocessors.norm_time(df["timestamp"].values)
        for col in ["lat", "lon", "alt", "ias"]:
            if df[col].isnull().all() and col != "ias":
                raise ValueError(f"All values in column {col} are NaN.")
            df.loc[:, col] = preprocessors.fillna_with_mean(df[col], allow_nan_mean=(col == "ias"))

        mapped_df = preprocessors.map_categories(df, preprocessors.category_mappings)
        max_values = list(preprocessors.category_max_limits.values())
        X_a_1 = F.one_hot(torch.tensor(mapped_df["R_1_IDX"].values), max_values[0]).float()
        X_a_2 = F.one_hot(torch.tensor(mapped_df["R_2_IDX"].values), max_values[1]).float()
        X_t = F.one_hot(torch.tensor(mapped_df["T_IDX"].values), max_values[2]).float()
        df.loc[:, [f"type_encoding_{i}" for i in range(4)]] = preprocessors.encode_features(
            X_a_1, X_a_2, X_t
        )
        last_row = df.iloc[-1][["lat", "lon", "alt", "ias", "timestamp"]]

        df.loc[:, "last_lat"] = last_row["lat"]
        df.loc[:, "last_lon"] = last_row["lon"]
        df.loc[:, "last_alt"] = last_row["alt"]
        df.loc[:, "last_ias"] = last_row["ias"]
        df.loc[:, "last_timestamp"] = last_row["timestamp"]

        return df


class EvenlySpacedDataset(BaseDataset):
    def __init__(
        self,
        filepaths,
        n_input,
        n_output,
        n_windows_per_file,
        step=1,
        feature_columns=("lat", "lon", "alt", "ias"),
        context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
        time_columns=("timestamp",),
        target_columns=("lat", "lon", "alt"),
        device="cpu",
    ):
        super().__init__(
            filepaths=filepaths,
            n_input=n_input,
            n_output=n_output,
            feature_columns=feature_columns,
            context_columns=context_columns,
            time_columns=time_columns,
            target_columns=target_columns,
            device=device,
            step=step,
            n_windows_per_file=n_windows_per_file,
        )

        # Preload all data into memory
        self.data = []
        for fp in self.filepaths:
            df = pd.read_csv(fp, dtype={"icao": str, "r": str, "t": str, "timestamp": float, "lat": float, "lon": float, "alt": float, "ias": float})
            n_rows = len(df)
            # Extract windows
            for w in range(n_windows_per_file):
                try:
                    start_idx, end_idx = self._get_start_end_indices(w, n_rows)
                    window = df.iloc[start_idx:end_idx:self.step]
                    window = self._modify_df(window)
                    self.data.append(window)
                except ValueError:
                    self.total_windows -= 1
                    continue

    def __len__(self):
        return self.total_windows

    def __getitem__(self, idx):
        window = self.data[idx]

        X_feat = window.iloc[: self.n_input][self.feature_cols].to_numpy(np.float32)
        if len(self.context_cols) > 0:
            X_context = window.iloc[0][self.context_cols].to_numpy(np.float32)
        else:
            X_context = None
        X_time = window.iloc[: self.window_size][self.time_cols].to_numpy(np.float32)
        Y_out = window.iloc[self.n_input :][self.target_cols].to_numpy(np.float32)
        return (
            torch.tensor(X_feat, device=self.device),
            torch.tensor(X_time, device=self.device),
            torch.tensor(Y_out, device=self.device),
            torch.tensor(X_context, device=self.device) if X_context is not None else None,
        )

class EvenlySpacedStreamingDataset(BaseDataset):
    def __init__(
        self,
        filepaths,
        n_input,
        n_output,
        n_windows_per_file,
        step=1,
        feature_columns=("lat", "lon", "alt", "ias"),
        context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
        time_columns=("timestamp",),
        target_columns=("lat", "lon", "alt"),
        device="cpu",
    ):
        super().__init__(
            filepaths=filepaths,
            n_input=n_input,
            n_output=n_output,
            feature_columns=feature_columns,
            context_columns=context_columns,
            time_columns=time_columns,
            target_columns=target_columns,
            device=device,
            step=step,
            n_windows_per_file=n_windows_per_file,
        )
        
        # Precompute row counts for each file
        self.row_counts = [self._count_rows(fp) for fp in self.filepaths]


    def _count_rows(self, fp):
        with open(fp, "r") as f:
            return sum(1 for _ in f) - 1  # minus header

    def __len__(self):
        return self.total_windows

    def __getitem__(self, idx):
        # Figure out which file we belong to
        file_idx = idx // self.n_windows_per_file
        local_idx = idx % self.n_windows_per_file
        fp = self.filepaths[file_idx]

        n_rows = self.row_counts[file_idx]

        start_idx, end_idx = self._get_start_end_indices(local_idx, n_rows)

        # Read only the required rows
        skip = list(set(range(1, start_idx + 1)))
        df = pd.read_csv(fp, skiprows=skip, nrows=self.window_size * self.step, dtype={"icao": str, "r": str, "t": str, "timestamp": float, "lat": float, "lon": float, "alt": float, "ias": float}).iloc[::self.step]
        df = self._modify_df(df)

        # Slice into input / decoder-input / targets
        X_feat = df.iloc[: self.n_input][self.feature_cols].to_numpy(np.float32)
        if len(self.context_cols) > 0:
            X_context = df.iloc[: self.n_input][self.context_cols].to_numpy(np.float32)
        else:
            X_context = None
        X_time = df.iloc[: self.window_size][self.time_cols].to_numpy(np.float32)
        Y_out = df.iloc[self.n_input :][self.target_cols].to_numpy(np.float32)
        return (
            torch.tensor(X_feat, device=self.device),
            torch.tensor(X_time, device=self.device),
            torch.tensor(Y_out, device=self.device),
            torch.tensor(X_context, device=self.device) if X_context is not None else None,
        )

def get_datasets(
    files: Sequence[str],
    n_input: int,
    n_output: int,
    n_windows_per_file: int,
    step: int = 1,
    feature_columns=("lat", "lon", "alt", "ias"),
    context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
    time_columns=("timestamp",),
    target_columns=("lat", "lon", "alt"),
    device="cpu",
    test_split: float = 0.2,
    val_split: float = 0.1,
    seed: int = 42,
    streaming: bool = False,
) -> tuple[Dataset, Dataset, Dataset]:
    """Utility to create train/val/test datasets from file list."""
    random.seed(seed)
    files = list(files)
    random.shuffle(files)

    n_total = len(files)
    n_test = int(n_total * test_split)
    n_val = int(n_total * val_split)

    test_files = files[:n_test]
    val_files = files[n_test : n_test + n_val]
    train_files = files[n_test + n_val :]

    if not streaming:
        ds = EvenlySpacedDataset
    else:
        ds = EvenlySpacedStreamingDataset

    train_dataset = ds(
        train_files,
        n_input=n_input,
        n_output=n_output,
        n_windows_per_file=n_windows_per_file,
        step=step,
        feature_columns=feature_columns,
        context_columns=context_columns,
        time_columns=time_columns,
        target_columns=target_columns,
        device=device,
    )
    val_dataset = ds(
        val_files,
        n_input=n_input,
        n_output=n_output,
        n_windows_per_file=n_windows_per_file,
        step=step,
        feature_columns=feature_columns,
        context_columns=context_columns,
        time_columns=time_columns,
        target_columns=target_columns,
        device=device,
    )
    test_dataset = ds(
        test_files,
        n_input=n_input,
        n_output=n_output,
        n_windows_per_file=n_windows_per_file,
        step=step,
        feature_columns=feature_columns,
        context_columns=context_columns,
        time_columns=time_columns,
        target_columns=target_columns,
        device=device,
    )

    return train_dataset, val_dataset, test_dataset
    
