import gc
import os
import numpy as np
import sys
import xarray as xr
import rasterio
import spectral

from brdf_model_M02 import M02
from brdf_model_M02SeaDAS import M02SeaDAS
from brdf_model_L11 import L11
from brdf_model_O25 import O25
from brdf_utils import ADF_OCP, squeeze_trivial_dims


UNC_LUT_CACHE = {}

"""
主 BRDF 校正模块
输入为 xarray 数据集
所需的光谱维度为 "bands"，其他维度自由
输入数据集中必填的字段：
Rw: 方向性海洋反射率
sza: 太阳天顶角
vza: 观测天顶角
raa: 相对方位角（当太阳和观测在同一侧时，raa=0）
输入数据集中可选的字段：
Rw_unc: Rw 的不确定度（若缺失，设为零）
输出数据集中的字段：
nrrs: 完全归一化的遥感反射率
rho_ex_w: nrrs * PI
omega_b: bb/(a+bb)
eta_b: bbw/bb
C_brdf: BRDF 校正因子
brdf_unc: C_brdf 的不确定度
nrrs_unc: nrrs 的不确定度

使用和适配 brdf_hypercp 模块需保留的信息：
Brdf_hypercp 是 EUMETSAT 研究项目的一部分
"BRDF correction of S3 OLCI water reflectance products"（S3 OLCI 水体反射率产品的 BRDF 校正），
合同号：RB_EUM-CO-21-4600002626-JIG。
研究团队成员：Davide D'Alimonte (davide.dalimonte@aequora.org)，
Tamito Kajiyama (tamito.kajiyama@aequora.org)，
Jaime Pitarch (jaime.pitarchportero@artov.ismar.cnr.it)，
Vittorio Brando (vittorio.brando@cnr.it)，
Marco Talone (talone@icm.csic.es) 以及
Constant Mazeran (constant.mazeran@solvo.fr)。
BRDF 查找表中的相对方位角遵循 OLCI 约定。详见 https://www.eumetsat.int/media/50720，图 6。
"""


# ---------------------------------------------------------------------------
# File I/O helpers
# ---------------------------------------------------------------------------

def _resolve_envi_header(filepath):
    """Return the ENVI header path accepted by spectral."""
    if filepath.lower().endswith(".hdr"):
        return filepath

    hdr_path = filepath + ".hdr"
    if os.path.exists(hdr_path):
        return hdr_path

    base, _ext = os.path.splitext(filepath)
    hdr_path = base + ".hdr"
    if os.path.exists(hdr_path):
        return hdr_path

    return filepath


def read_bsq(filepath, wavelengths=None, dtype=np.float32):
    """Read an ENVI BSQ hyperspectral image.

    Parameters
    ----------
    filepath : str
        Path to the ENVI header (.hdr) or image file.
    wavelengths : array-like, optional
        Band centre wavelengths in nm.  When *None*, wavelengths are read
        from the ENVI header if available.

    Returns
    -------
    data : ndarray, shape (bands, rows, cols), float64
    wavelengths : ndarray, shape (bands,)
    img : spectral image object (metadata / transform access)
    """
    img = spectral.open_image(_resolve_envi_header(filepath))
    data = np.asarray(img.open_memmap(interleave="source"), dtype=dtype)
    # data = np.transpose(data, (2, 0, 1)) # -> (bands, rows, cols)

    if wavelengths is None:
        if hasattr(img, 'bands') and img.bands.centers is not None:
            wavelengths = np.array(img.bands.centers, dtype=np.float32)
        else:
            n_bands = data.shape[0]
            wavelengths = np.arange(1, n_bands + 1, dtype=np.float32)

    return data, np.asarray(wavelengths, dtype=dtype), img


def read_bip_angles(filepath, dtype=np.float32):
    """Read an ENVI BIP angle file and extract the five geometry bands.

    Band layout (1-indexed):
        band  7  -> SZA  Solar Zenith Angle
        band  8  -> SAA  Solar Azimuth Angle
        band  9  -> VZA  Sensor Zenith Angle
        band 10  -> VAA  Sensor Azimuth Angle
        band 11  -> RAA  Relative Azimuth Angle

    Parameters
    ----------
    filepath : str
        Path to the ENVI BIP file (header or image file).

    Returns
    -------
    dict with keys 'sza', 'saa', 'vza', 'vaa', 'raa'.
    Each value is a 2-D ndarray of shape (rows, cols), float64.
    """
    img = spectral.open_image(_resolve_envi_header(filepath))
    data = np.asarray(img.open_memmap(interleave="source"), dtype=dtype)

    n_bands = data.shape[2]
    if n_bands < 11:
        raise ValueError(
            f"Angle file has only {n_bands} bands; at least 11 are required "
            "(bands 7–11 = SZA, SAA, VZA, VAA, RAA)."
        )

    return {
        'sza': data[:, :, 6].copy(),
        'saa': data[:, :, 7].copy(),
        'vza': data[:, :, 8].copy(),
        'vaa': data[:, :, 9].copy(),
        'raa': data[:, :, 10].copy(),
    }


def read_water_mask(filepath):
    """Read a single-band GeoTIFF water mask.

    Parameters
    ----------
    filepath : str
        Path to the GeoTIFF file.

    Returns
    -------
    mask : ndarray, shape (rows, cols), int8
        Pixels with value 1 indicate water; all other values are non-water.
    """
    with rasterio.open(filepath) as src:
        mask = src.read(1).astype(np.int8)
    return mask


def save_brdf_result(
    ds_out,
    output_file,
    source_file=None,
    output_var="Rw_brdf",
    output_format="ENVI",
    dtype=np.float32,
):
    """Save the BRDF result to ENVI or NetCDF."""
    output_format = output_format.upper()

    if output_var not in ds_out:
        raise KeyError(f"Output variable '{output_var}' not found in result dataset.")

    if output_format == "NETCDF":
        if not output_file.lower().endswith(".nc"):
            output_file = output_file + ".nc"
        ds_out.to_netcdf(output_file)
        return output_file

    if output_format != "ENVI":
        raise ValueError("output_format must be 'ENVI' or 'NETCDF'.")

    cube = np.asarray(ds_out[output_var].values, dtype=dtype)
    cube = np.transpose(cube, (1, 2, 0))

    if output_file.lower().endswith(".hdr"):
        hdr_path = output_file
    else:
        hdr_path = output_file + ".hdr"

    metadata = {
        "interleave": "bsq",
        "description": f"BRDF corrected result: {output_var}",
        "bands": cube.shape[2],
        "lines": cube.shape[0],
        "samples": cube.shape[1],
    }
    if "bands" in ds_out.coords:
        metadata["wavelength"] = [str(v) for v in ds_out.coords["bands"].values.tolist()]

    if source_file is not None:
        metadata["source_file"] = source_file

    spectral.envi.save_image(
        hdr_path,
        cube,
        dtype=dtype,
        interleave="bsq",
        metadata=metadata,
        force=True,
    )
    return hdr_path


# ---------------------------------------------------------------------------
# Top-level pipeline
# ---------------------------------------------------------------------------

def run_brdf_correction(
    hyperspectral_file,
    angle_file,
    mask_file,
    wavelengths=None,
    brdf_model='L11',
    chunk_size=4096,
):
    """BRDF correction pipeline for water pixels in a hyperspectral scene.

    Reads the three input files, identifies water pixels via the mask,
    runs ``brdf_prototype`` only on those pixels, and returns a full-scene
    xarray Dataset where water pixels carry corrected values and non-water
    pixels retain the original Rw (BRDF-derived fields are NaN outside
    the water mask).

    Parameters
    ----------
    hyperspectral_file : str
        Path to the ENVI BSQ hyperspectral image (.hdr sidecar required).
    angle_file : str
        Path to the ENVI BIP angle file.  Bands 7–11 must be
        SZA, SAA, VZA, VAA, RAA respectively.
    mask_file : str
        Path to the GeoTIFF water mask.  Pixels == 1 are treated as water.
    wavelengths : array-like, optional
        Band centre wavelengths in nm.  Overrides the header value when given.
    brdf_model : str
        BRDF model identifier: ``'L11'`` (default), ``'M02'``,
        ``'M02SeaDAS'``, or ``'O25'``.
    chunk_size : int, optional
        Number of water pixels processed per batch (default 4096).
        Smaller values reduce peak memory at the cost of more loop
        iterations.

    Returns
    -------
    xr.Dataset
        Full-scene dataset with dimensions ``(bands, y, x)``.
        Variables:

        * ``Rw``          – original directional water reflectance
        * ``nrrs``        – normalised remote-sensing reflectance (water only)
        * ``rho_ex_w``    – nrrs × π (water only)
        * ``C_brdf``      – BRDF correction factor (water only)
        * ``brdf_unc``    – uncertainty of C_brdf (water only)
        * ``nrrs_unc``    – uncertainty of nrrs (water only)
        * ``C_brdf_fail`` – flag: True where correction failed (water only)
        * ``water_mask``  – the input mask (1 = water)
        * ``sza``, ``saa``, ``vza``, ``vaa``, ``raa`` – viewing geometry
    """
    # ------------------------------------------------------------------
    # 1. Read all inputs
    # ------------------------------------------------------------------
    rw_data, wvl, _img_meta = read_bsq(hyperspectral_file, wavelengths, dtype=np.float32)
    angles = read_bip_angles(angle_file, dtype=np.float32)
    water_mask = read_water_mask(mask_file)

    n_bands, n_rows, n_cols = rw_data.shape

    # Sanity-check spatial dimensions
    if angles['sza'].shape != (n_rows, n_cols):
        raise ValueError(
            f"Angle file spatial shape {angles['sza'].shape} does not match "
            f"hyperspectral image shape ({n_rows}, {n_cols})."
        )
    if water_mask.shape != (n_rows, n_cols):
        raise ValueError(
            f"Mask shape {water_mask.shape} does not match "
            f"hyperspectral image shape ({n_rows}, {n_cols})."
        )

    # ------------------------------------------------------------------
    # 2. Identify water pixels
    # ------------------------------------------------------------------
    water_rows, water_cols = np.where(water_mask == 1)
    n_water = water_rows.size
    if n_water == 0:
        raise RuntimeError("No water pixels found in the mask (no pixels == 1).")

    # ------------------------------------------------------------------
    # 3. Pre-allocate full-scene output arrays
    #    Water pixels will be filled in chunks; non-water pixels stay NaN.
    # ------------------------------------------------------------------
    shape3 = (n_bands, n_rows, n_cols)
    shape2 = (n_rows, n_cols)

    nrrs_full        = np.full(shape3, np.nan, dtype=np.float32)
    rho_ex_w_full    = np.full(shape3, np.nan, dtype=np.float32)
    C_brdf_full      = np.full(shape3, np.nan, dtype=np.float32)
    brdf_unc_full    = np.full(shape3, np.nan, dtype=np.float32)
    nrrs_unc_full    = np.full(shape3, np.nan, dtype=np.float32)
    C_brdf_fail_full = np.zeros(shape2, dtype=bool)
    rw_brdf_full     = rw_data.copy()

    # ------------------------------------------------------------------
    # 4–6. Chunked BRDF correction
    #      Process water pixels in batches of `chunk_size` to bound
    #      peak memory.  Each batch builds a minimal xarray Dataset,
    #      calls brdf_prototype, writes results back to the full-scene
    #      arrays, then releases the batch objects.
    # ------------------------------------------------------------------
    n_chunks = int(np.ceil(n_water / chunk_size))
    for chunk_idx in range(n_chunks):
        lo = chunk_idx * chunk_size
        hi = min(lo + chunk_size, n_water)

        rows_c = water_rows[lo:hi]
        cols_c = water_cols[lo:hi]

        # Extract batch vectors
        rw_c  = rw_data[:, rows_c, cols_c].T          # (chunk, bands)
        sza_c = angles['sza'][rows_c, cols_c]          # (chunk,)
        vza_c = angles['vza'][rows_c, cols_c]
        raa_c = angles['raa'][rows_c, cols_c]

        pixel_idx_c = np.arange(hi - lo)
        ds_chunk = xr.Dataset(
            {
                'Rw':  xr.DataArray(rw_c,  dims=('n', 'bands'),
                                    coords={'n': pixel_idx_c, 'bands': wvl}),
                'sza': xr.DataArray(sza_c, dims='n',
                                    coords={'n': pixel_idx_c}),
                'vza': xr.DataArray(vza_c, dims='n',
                                    coords={'n': pixel_idx_c}),
                'raa': xr.DataArray(raa_c, dims='n',
                                    coords={'n': pixel_idx_c}),
            }
        )

        ds_corr = brdf_prototype(ds_chunk, brdf_model=brdf_model)

        # Write results back; ds_corr arrays have dims (n, bands) → transpose
        nrrs_full[:, rows_c, cols_c]        = ds_corr['nrrs'].values.T
        rho_ex_w_full[:, rows_c, cols_c]    = ds_corr['rho_ex_w'].values.T
        C_brdf_full[:, rows_c, cols_c]      = ds_corr['C_brdf'].values.T
        brdf_unc_full[:, rows_c, cols_c]    = ds_corr['brdf_unc'].values.T
        nrrs_unc_full[:, rows_c, cols_c]    = ds_corr['nrrs_unc'].values.T
        C_brdf_fail_full[rows_c, cols_c]    = ds_corr['C_brdf_fail'].values
        rw_brdf_full[:, rows_c, cols_c]     = ds_corr['rho_ex_w'].values.T

        # Release batch objects so GC can reclaim memory
        del rw_c, sza_c, vza_c, raa_c, pixel_idx_c, ds_chunk, ds_corr
        gc.collect()

    # ------------------------------------------------------------------
    # 7. Assemble full-scene output Dataset
    # ------------------------------------------------------------------
    full_coords = {'bands': wvl, 'y': np.arange(n_rows), 'x': np.arange(n_cols)}
    dims3 = ('bands', 'y', 'x')
    dims2 = ('y', 'x')

    ds_out = xr.Dataset(
        {
            'Rw':          xr.DataArray(rw_data,          dims=dims3, coords=full_coords),
            'Rw_brdf':     xr.DataArray(rw_brdf_full,     dims=dims3, coords=full_coords),
            'nrrs':        xr.DataArray(nrrs_full,        dims=dims3, coords=full_coords),
            'rho_ex_w':    xr.DataArray(rho_ex_w_full,    dims=dims3, coords=full_coords),
            'C_brdf':      xr.DataArray(C_brdf_full,      dims=dims3, coords=full_coords),
            'brdf_unc':    xr.DataArray(brdf_unc_full,    dims=dims3, coords=full_coords),
            'nrrs_unc':    xr.DataArray(nrrs_unc_full,    dims=dims3, coords=full_coords),
            'C_brdf_fail': xr.DataArray(C_brdf_fail_full, dims=dims2),
            'water_mask':  xr.DataArray(water_mask,       dims=dims2),
            'sza':         xr.DataArray(angles['sza'],    dims=dims2),
            'saa':         xr.DataArray(angles['saa'],    dims=dims2),
            'vza':         xr.DataArray(angles['vza'],    dims=dims2),
            'vaa':         xr.DataArray(angles['vaa'],    dims=dims2),
            'raa':         xr.DataArray(angles['raa'],    dims=dims2),
        }
    )

    return ds_out


def process_brdf_files(
    hyperspectral_file,
    angle_file,
    mask_file,
    output_file,
    wavelengths=None,
    brdf_model="L11",
    output_var="Rw_brdf",
    output_format="ENVI",
    chunk_size=4096,
):
    """Run BRDF correction from three input files and save the result."""
    ds_out = run_brdf_correction(
        hyperspectral_file=hyperspectral_file,
        angle_file=angle_file,
        mask_file=mask_file,
        wavelengths=wavelengths,
        brdf_model=brdf_model,
        chunk_size=chunk_size,
    )
    saved_path = save_brdf_result(
        ds_out=ds_out,
        output_file=output_file,
        source_file=hyperspectral_file,
        output_var=output_var,
        output_format=output_format,
    )
    return ds_out, saved_path


# ---------------------------------------------------------------------------
# Core BRDF correction (unchanged from original)
# ---------------------------------------------------------------------------

def brdf_prototype(ds, adf=None, brdf_model='L11'):
    # 测试 BRDF 模型在 GUI 中不支持：强制覆盖
    # brdf_model = 'M02SeaDAS'

    # 压缩单一维度（例如，投放次数、提取次数等）以避免插值问题
    ds, squeezedDims = squeeze_trivial_dims(ds)

    # Initialise model
    if brdf_model == 'M02':
        BRDF_model = M02(bands=ds.bands, aot=ds.aot, wind=ds.wind, adf=None)  # Don't use brdf_py.ADF context
    elif brdf_model == 'M02SeaDAS':
        BRDF_model = M02SeaDAS(bands=ds.bands, adf=None)  # Don't use brdf_py.ADF context
    elif brdf_model == 'L11':
        BRDF_model = L11(bands=ds.bands, adf=None)  # Don't use brdf_py.ADF context
    elif brdf_model == 'O25':
        BRDF_model = O25(bands=ds.bands, adf=None)  # Don't use brdf_py.ADF context
    else:
        print("BRDF model %s not supported" % brdf_model)
        sys.exit(1)

    # Init pixel
    BRDF_model.init_pixels(ds['sza'], ds['vza'], ds['raa'])

    # Compute IOP and normalize by iterating
    ds['nrrs'] = ds['Rw'] / np.pi

    ds['convergeFlag'] = (0 * ds['sza']).astype(bool)
    ds['C_brdf'] = 0 * ds['nrrs'] + 1

    for iter_brdf in range(int(BRDF_model.niter)):

        # M02: Initialise chl_iter
        if brdf_model in ['M02', 'M02SeaDAS'] and (iter_brdf == 0):
            chl_iter = {}
            ds['log10_chl'] = 0 * ds['sza'] + float(np.log10(BRDF_model.OC4MEchl0))
            chl_iter[-1]    = 0 * ds['sza'] + float(BRDF_model.OC4MEchl0)

        ds = BRDF_model.backward(ds, iter_brdf)

        # M02: Check convergence (dummy for M02SeaDAS for the moment... epsilon set to 0)
        if brdf_model in ['M02', 'M02SeaDAS']:
            chl_iter[iter_brdf] = 10 ** ds['log10_chl']
            #  Check if convergence is reached |chl_old-chl_new| < epsilon * chl_new
            ds['convergeFlag'] = (ds['convergeFlag']) | (
                (np.abs(chl_iter[iter_brdf - 1] - chl_iter[iter_brdf]) < float(BRDF_model.OC4MEepsilon) * chl_iter[
                    iter_brdf]))

        # Apply forward model in both geometries
        # forward_mod = BRDF_model.forward(ds).transpose('n', 'bands')
        # forward_mod0 = BRDF_model.forward(ds, normalized=True).transpose('n', 'bands')
        forward_mod = BRDF_model.forward(ds)
        forward_mod0 = BRDF_model.forward(ds, normalized=True)

        ratio = forward_mod0 / forward_mod

        # Drop remnant coordinates to avoid ambiguities in the update of the BRDF factor.
        for coord in ratio.coords:
            if coord not in ds['C_brdf'].coords:
                ratio = ratio.drop(coord)

        # Normalize reflectance
        ds['C_brdf'] = xr.where(ds['convergeFlag'], ds['C_brdf'], ratio)
        ds['nrrs'] = ds['Rw'] / np.pi * ds['C_brdf']

    # Flag BRDF where NaN and set to 1 (no correction applied).
    ds['C_brdf_fail'] = np.isnan(ds['C_brdf'])
    ds['C_brdf'] = xr.where(ds['C_brdf_fail'], 1, ds['C_brdf'])
    ds['nrrs'] = xr.where(ds['C_brdf_fail'], ds['Rw'] / np.pi, ds['nrrs'])

    # If QAA_fail is raised, raise C_brdf_fail (but still apply C_brdf).
    if 'QAA_fail' in ds:
        ds['C_brdf_fail'] = (ds['C_brdf_fail']) | (ds['QAA_fail'])

    # Compute uncertainty
    ds = brdf_uncertainty(ds)

    # Compute flag
    ds['flags_level2'] = ds['Rw'] * 0  # TODO

    # Convert to reflectance unit
    ds['rho_ex_w'] = ds['nrrs'] * np.pi

    # Expand squeezed trivial dimensions
    for dim,d0 in squeezedDims.items():
        ds = ds.expand_dims(dim,axis=d0)

    return ds

def brdf_uncertainty(ds, adf=None):
    ''' Compute uncertainty of BRDF factor and propagate to nrrs '''

    # Read LUT
    if adf is None:
        adf = ADF_OCP

    unc_lut_path = adf % 'UNC'
    if unc_lut_path not in UNC_LUT_CACHE:
        UNC_LUT_CACHE[unc_lut_path] = xr.open_dataset(unc_lut_path, engine='netcdf4')
    LUT = UNC_LUT_CACHE[unc_lut_path]

    # Interpolate relative uncertainty
    unc = LUT['unc'].interp(lambda_unc=ds.bands, theta_s_unc=ds.sza, theta_v_unc=ds.vza,
                            delta_phi_unc=ds.raa)

    # Compute absolute uncertainty of factor
    ds['brdf_unc'] = unc * ds['C_brdf']

    # Flag BRDF_unc where NaN and set to 0
    ds['brdf_unc_fail'] = np.isnan(ds['brdf_unc'])
    ds['brdf_unc'] = xr.where(ds['brdf_unc_fail'], 0, ds['brdf_unc'])

    # Propagate to nrrs
    Rwex_unc2 = ds['brdf_unc'] * ds['brdf_unc'] * ds['Rw'] * ds['Rw']
    if 'Rw_unc' in ds:
        Rwex_unc2 += ds['C_brdf'] * ds['C_brdf'] * ds['Rw_unc'] * ds['Rw_unc']
    ds['nrrs_unc'] = np.sqrt(Rwex_unc2)/np.pi

    return ds


def main():
    import argparse

    parser = argparse.ArgumentParser(description="Water-region BRDF correction for hyperspectral data.")
    parser.add_argument("hyperspectral_file", help="Input ENVI BSQ hyperspectral file or .hdr path.")
    parser.add_argument("angle_file", help="Input ENVI BIP angle file or .hdr path.")
    parser.add_argument("mask_file", help="Input water mask GeoTIFF; pixels == 1 are corrected.")
    parser.add_argument("output_file", help="Output path prefix. ENVI writes .hdr/.img, NetCDF writes .nc.")
    parser.add_argument("--brdf-model", default="L11", choices=["L11", "M02", "M02SeaDAS", "O25"])
    parser.add_argument("--output-var", default="Rw_brdf", choices=["Rw_brdf", "rho_ex_w", "nrrs", "C_brdf"])
    parser.add_argument("--output-format", default="ENVI", choices=["ENVI", "NETCDF"])
    parser.add_argument("--chunk-size", type=int, default=4096,
                        help="Number of water pixels per processing batch (default: 4096).")
    args = parser.parse_args()

    _ds_out, saved_path = process_brdf_files(
        hyperspectral_file=args.hyperspectral_file,
        angle_file=args.angle_file,
        mask_file=args.mask_file,
        output_file=args.output_file,
        brdf_model=args.brdf_model,
        output_var=args.output_var,
        output_format=args.output_format,
        chunk_size=args.chunk_size,
    )
    print(f"BRDF correction finished. Saved to: {saved_path}")


if __name__ == "__main__":
    main()
