"""
Functions for working with the Wetlands Insight Tool (WIT)
"""

# Import required packages

# Force GeoPandas to use Shapely instead of PyGEOS
# In a future release, GeoPandas will switch to using Shapely by default.
import os
os.environ['USE_PYGEOS'] = '0'

import warnings
import numpy as np
import pandas as pd
import geopandas as gpd
import seaborn as sns
import xarray as xr
import matplotlib.pyplot as plt
from skimage import exposure
import matplotlib.animation as animation
import matplotlib.patheffects as PathEffects
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from dask.distributed import progress

import datacube
from datacube.utils import masking
from datacube.utils import geometry

from deafrica_tools.bandindices import calculate_indices
from deafrica_tools.datahandling import load_ard, wofs_fuser
from deafrica_tools.spatial import xr_rasterize
from deafrica_tools.classification import HiddenPrints


def WIT_drill(
    gdf,
    time,
    min_gooddata=0.85,
    TCW_threshold=-0.035,
    resample_frequency=None,
    export_csv=None,
    dask_chunks=None,
    verbose=False,
    verbose_progress=False,
):
    """
    The Wetlands Insight Tool run onver an extent covered by a polygon.
    This function loads FC, WOfS, and Landsat data, and calculates tasseled
    cap wetness, in order to determine the dominant land cover class
    within a polygon at each satellite observation.

    The output is a pandas dataframe containing a timeseries of the relative
    fractions of each class at each time-step. This forms the input to produce
    a stacked line-plot.

    Last modified: Oct 2021

    Parameters
    ----------
    gdf : geopandas.GeoDataFrame
        The dataframe must only contain a single row,
        containing the polygon you wish to interrograte.
    time : tuple
        a tuple containing the time range over which to run the WIT.
        e.g. ('2015-01' , '2019-12')
    min_gooddata : Float, optional
        A number between 0 and 1 (e.g 0.8) indicating the minimum percentage
        of good quality pixels required for a satellite observation to be loaded
        and therefore included in the WIT plot. This number should, at a minimum,
        be set to 0.80 to limit biases in the result if not resampling the time-series.
        If resampling the data using the parameter `resample_frequency`, then
        setting this number to 0 (or a low float number) is acceptable.
    TCW_threshold : Int, optional
        The tasseled cap wetness threshold, beyond which a pixel will be
        considered 'wet'. Defaults to -0.035.
    resample_frequency : str 
        Option for resampling time-series of input datasets. This option is useful
        for either smoothing the WIT plot, or because the area of analysis is larger
        than a scene width and therefore requires composites. Options include any
        str accepted by `xarray.resample(time=)`. The resampling method used is .max()
    export_csv : str, optional
        To save the returned pandas dataframe as a .csv file, pass a
        a location string (e.g. 'output/results.csv')
    dask_chunks : dict, optional
        To lazily load the datasets using dask, pass a dictionary containing
        the dimensions over which to chunk e.g. {'time':-1, 'x':250, 'y':250}.
    verbose: bool, optional
        If true, print statements are putput detailing the progress of the tool.
    verbose_progress: bool, optional
        For use with Dask progress bar

    Returns
    -------
    df : Pandas.Dataframe
        A pandas dataframe containing the timeseries of relative fractions
        of each land cover class (WOfs, FC, TCW)

    """
    # add geom to dc query dict
    if isinstance(gdf, datacube.utils.geometry._base.Geometry):
        gdf = gpd.GeoDataFrame({'col1':['name'],'geometry':gdf.geom}, crs=gdf.crs)
    geom = geometry.Geometry(geom=gdf.iloc[0].geometry, crs=gdf.crs)
    query = {"geopolygon": geom, "time": time}

    # Create a datacube instance
    dc = datacube.Datacube(app="wetlands insight tool")

    # load landsat 5,7,8 data
    warnings.filterwarnings("ignore")

    if verbose_progress:
        print("Loading Landsat data")
    ds_ls = load_ard(
        dc=dc,
        products=["ls8_sr", "ls7_sr", "ls5_sr"],
        output_crs="epsg:6933",
        min_gooddata=min_gooddata,
        mask_filters=(['opening', 3], ['dilation', 3]),
        measurements=["red", "green", "blue", "nir", "swir_1", "swir_2"],
        dask_chunks=dask_chunks,
        group_by="solar_day",
        resolution=(-30, 30),
        verbose=verbose,
        **query,
    )
    
    # create polygon mask
    mask = xr_rasterize(gdf.iloc[[0]], ds_ls)
    ds_ls = ds_ls.where(mask)
    
    # calculate tasselled cap wetness within masked AOI
    if verbose:
        print("calculating tasseled cap wetness index ")
    
    with HiddenPrints(): #suppres the prints from this func
        tcw = calculate_indices(
            ds_ls, index=["TCW"], normalise=False, satellite_mission="ls", drop=True
        )
    
    if resample_frequency is not None:
        if verbose:
            print('Resampling TCW to '+ resample_frequency)
        tcw = tcw.resample(time=resample_frequency).max()
    
    tcw = tcw.TCW >= TCW_threshold
    tcw = tcw.where(mask, 0)
    tcw = tcw.persist() 

    if verbose:
        print("Loading WOfS layers ")

    wofls = dc.load(
        product="wofs_ls",
        like=ds_ls,
        fuse_func=wofs_fuser,
        dask_chunks=dask_chunks,
        collection_category="T1",
    )

    # boolean of wet/dry
    wofls_wet = masking.make_mask(wofls.water, wet=True)
    
    if resample_frequency is not None:
        if verbose:
            print('Resampling WOfS to '+ resample_frequency)
        wofls_wet = wofls_wet.resample(time=resample_frequency).max()
    
    # mask sure wofs matches other datasets
    wofls_wet = wofls_wet.where(wofls_wet.time == tcw.time)
    
    # apply the polygon mask
    wofls_wet = wofls_wet.where(mask)

    # load Fractional cover
    if verbose:
        print("Loading fractional Cover")

    # load fractional cover
    fc_ds = dc.load(
        product="fc_ls",
        time=time,
        dask_chunks=dask_chunks,
        like=ds_ls,
        measurements=["pv", "npv", "bs"],
        collection_category="T1",
    )
    
    # use wofls mask to cloud mask FC
    clear_and_dry = masking.make_mask(wofls, dry=True).water
    fc_ds = fc_ds.where(clear_and_dry)
    
    if resample_frequency is not None:
        if verbose:
            print('Resampling FC to '+ resample_frequency)
        fc_ds = fc_ds.resample(time=resample_frequency).max()
    
    # mask sure fc matches other datasets
    fc_ds = fc_ds.where(fc_ds.time == tcw.time)

    # mask with polygon
    fc_ds = fc_ds.where(mask)

    # mask with TC wetness
    fc_ds_noTCW = fc_ds.where(tcw == False)

    if verbose:
        print("Generating classification")

    # Cast the dataset to a dataarray
    fc_ds_noTCW = fc_ds_noTCW.to_array(dim="variable", name="fc_ds_noTCW")

    # turn FC array into integer only as nanargmax doesn't
    # seem to handle floats the way we want it to
    fc_int = fc_ds_noTCW.astype("int8")

    # use nanargmax to get the index of the maximum value
    BSPVNPV = fc_int.argmax(dim="variable")
    
    #int dytype remocves NaNs so we need to create mask again
    FC_mask = np.isfinite(fc_ds_noTCW).all(dim="variable")
    BSPVNPV = BSPVNPV.where(FC_mask)

    # Restack the Fractional cover dataset all together
    # CAUTION:ARGMAX DEPENDS ON ORDER OF VARIABALES IN
    # DATASET. NEED TO ADJUST BELOW DEPENDING ON ORDER OF FC VARIABLES
    
    FC_dominant = xr.Dataset(
        {
            "bs": (BSPVNPV == 2).where(FC_mask),
            "pv": (BSPVNPV == 0).where(FC_mask),
            "npv": (BSPVNPV == 1).where(FC_mask),
        }
    )

    # pixel counts
    pixels = mask.sum(dim=["x", "y"])
    

    if verbose_progress:
        print("Computing wetness")
    tcw_pixel_count = tcw.sum(dim=["x", "y"]).compute()
    
    if verbose_progress:
        print("Computing green veg, dry veg, and bare soil")
    FC_count = FC_dominant.sum(dim=["x", "y"]).compute()
    
    if verbose_progress:
        print("Computing open water")
    wofs_pixels = wofls_wet.sum(dim=["x", "y"]).compute()

    # count percentages
    wofs_area_percent = (wofs_pixels / pixels) * 100
    tcw_area_percent = (tcw_pixel_count / pixels) * 100
    tcw_less_wofs = tcw_area_percent - wofs_area_percent  # wet not wofs

    # Fractional cover pixel count method
    # Get number of FC pixels, divide by total number of pixels per polygon
    # Work out the number of nodata pixels in the data
    BS_percent = (FC_count.bs / pixels) * 100
    PV_percent = (FC_count.pv / pixels) * 100
    NPV_percent = (FC_count.npv / pixels) * 100
    NoData_count = ((
        100 - wofs_area_percent - tcw_less_wofs - PV_percent - NPV_percent - BS_percent
    ) / 100) * pixels

    # re-do percentages but now handling any no-data pixels within polygon
    BS_percent = (FC_count.bs / (pixels - NoData_count)) * 100
    PV_percent = (FC_count.pv / (pixels - NoData_count)) * 100
    NPV_percent = (FC_count.npv / (pixels - NoData_count)) * 100
    wofs_area_percent = (wofs_pixels / (pixels - NoData_count)) * 100
    tcw_area_percent = (tcw_pixel_count / (pixels - NoData_count)) * 100
    tcw_less_wofs = tcw_area_percent - wofs_area_percent
    
    # Sometimes when we resample datastes, WOfS extent can be
    # greater than the wetness extent, thus make negative values == zero
    tcw_less_wofs = tcw_less_wofs.where(tcw_less_wofs>=0, 0) 

    # start setup of dataframe by adding only one dataset
    df = pd.DataFrame(
        data=wofs_area_percent.data,
        index=wofs_area_percent.time.values,
        columns=["wofs_area_percent"],
    )

    # add data into pandas dataframe for export
    df["wet_percent"] = tcw_less_wofs.data
    df["green_veg_percent"] = PV_percent.data
    df["dry_veg_percent"] = NPV_percent.data
    df["bare_soil_percent"] = BS_percent.data

    # round numbers
    df = df.round(2)
    
    # save the csv of the output data used to create the stacked plot for the polygon drill
    if export_csv:
        if verbose:
            print("exporting csv: " + export_csv)
        df.to_csv(export_csv, index_label="Datetime")

    return df


def animated_timeseries_WIT(
    ds,
    df,
    output_path,
    width_pixels=1000,
    interval=200,
    bands=["red", "green", "blue"],
    percentile_stretch=(0.02, 0.98),
    image_proc_func=None,
    title=False,
    show_date=True,
    annotation_kwargs={},
    onebandplot_cbar=True,
    onebandplot_kwargs={},
    shapefile_path=None,
    shapefile_kwargs={},
    pandasplot_kwargs={},
    time_dim="time",
    x_dim="x",
    y_dim="y",
):

    ###############
    # Setup steps #
    ###############

    # Test if all dimensions exist in dataset
    if time_dim in ds and x_dim in ds and y_dim in ds:

        # Test if there is one or three bands, and that all exist in both datasets:
        if ((len(bands) == 3) | (len(bands) == 1)) & all(
            [(b in ds.data_vars) for b in bands]
        ):

            # Import xarrays as lists of three band numpy arrays
            imagelist, vmin, vmax = _ds_to_arrraylist(
                ds,
                bands=bands,
                time_dim=time_dim,
                x_dim=x_dim,
                y_dim=y_dim,
                percentile_stretch=percentile_stretch,
                image_proc_func=image_proc_func,
            )

            # Get time, x and y dimensions of dataset and calculate width vs height of plot
            timesteps = len(ds[time_dim])
            width = len(ds[x_dim])
            height = len(ds[y_dim])
            width_ratio = float(width) / float(height)
            height = 10.0 / width_ratio

            # If title is supplied as a string, multiply out to a list with one string per timestep.
            # Otherwise, use supplied list for plot titles.
            if isinstance(title, str) or isinstance(title, bool):
                title_list = [title] * timesteps
            else:
                title_list = title

            # Set up annotation parameters that plt.imshow plotting for single band array images.
            # The nested dict structure sets default values which can be overwritten/customised by the
            # manually specified `onebandplot_kwargs`
            onebandplot_kwargs = dict(
                {
                    "cmap": "Greys",
                    "interpolation": "bilinear",
                    "vmin": vmin,
                    "vmax": vmax,
                    "tick_colour": "black",
                    "tick_fontsize": 11,
                },
                **onebandplot_kwargs,
            )

            # Use pop to remove the two special tick kwargs from the onebandplot_kwargs dict, and save individually
            onebandplot_tick_colour = onebandplot_kwargs.pop("tick_colour")
            onebandplot_tick_fontsize = onebandplot_kwargs.pop("tick_fontsize")

            # Set up annotation parameters that control font etc. The nested dict structure sets default
            # values which can be overwritten/customised by the manually specified `annotation_kwargs`
            annotation_kwargs = dict(
                {
                    "xy": (1, 1),
                    "xycoords": "axes fraction",
                    "xytext": (-5, -5),
                    "textcoords": "offset points",
                    "horizontalalignment": "right",
                    "verticalalignment": "top",
                    "fontsize": 15,
                    "color": "white",
                    "path_effects": [
                        PathEffects.withStroke(linewidth=3, foreground="black")
                    ],
                },
                **annotation_kwargs,
            )

            # Define default plotting parameters for the overlaying shapefile(s). The nested dict structure sets
            # default values which can be overwritten/customised by the manually specified `shapefile_kwargs`
            shapefile_kwargs = dict(
                {"linewidth": 2, "edgecolor": "black", "facecolor": "#00000000"},
                **shapefile_kwargs,
            )

            # Define default plotting parameters for the right-hand line plot. The nested dict structure sets
            # default values which can be overwritten/customised by the manually specified `pandasplot_kwargs`
            pandasplot_kwargs = dict({}, **pandasplot_kwargs)

            ###################
            # Initialise plot #
            ###################

            # Set up figure
            fig, (ax1, ax2) = plt.subplots(
                ncols=2, gridspec_kw={"width_ratios": [1, 2]}
            )
            fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0.2, hspace=0)
            fig.set_size_inches(10.0, height * 0.5, forward=True)
            ax1.axis("off")
            ax2.margins(x=0.01)
            ax2.xaxis.label.set_visible(False)

            # Initialise axesimage objects to be updated during animation, setting extent from dims
            extents = [
                float(ds[x_dim].min()),
                float(ds[x_dim].max()),
                float(ds[y_dim].min()),
                float(ds[y_dim].max()),
            ]
            im = ax1.imshow(imagelist[0], extent=extents, **onebandplot_kwargs)

            # Initialise right panel and set y axis limits
            # set up color palette
            pal = [
                sns.xkcd_rgb["cobalt blue"],
                sns.xkcd_rgb["neon blue"],
                sns.xkcd_rgb["grass"],
                sns.xkcd_rgb["beige"],
                sns.xkcd_rgb["brown"],
            ]

            # make a stacked area plot
            ax2.stackplot(
                df.index,
                df.wofs_area_percent,
                df.wet_percent,
                df.green_veg_percent,
                df.dry_veg_percent,
                df.bare_soil_percent,
                labels=["open water", "wet", "green veg", "dry veg", "bare soil"],
                colors=pal,
                alpha=0.6,
                **pandasplot_kwargs,
            )

            ax2.legend(loc="lower left", framealpha=0.6)

            df1 = pd.DataFrame(
                {
                    "wofs_area_percent": df.wofs_area_percent,
                    "wet_percent": df.wofs_area_percent + df.wet_percent,
                    "green_veg_percent": df.wofs_area_percent
                    + df.wet_percent
                    + df.green_veg_percent,
                    "dry_veg_percent": df.wofs_area_percent
                    + df.wet_percent
                    + df.green_veg_percent
                    + df.dry_veg_percent,
                    "bare_soil_percent": df.dry_veg_percent
                    + df.green_veg_percent
                    + df.wofs_area_percent
                    + df.wet_percent
                    + df.bare_soil_percent,
                }
            )
            df1 = df1.set_index(df.index)

            line_test = df1.plot(
                ax=ax2, legend=False, color="black", **pandasplot_kwargs
            )

            # set axis limits to the min and max
            ax2.set(xlim=(df.index[0], df.index[-1]), ylim=(0, 100))

            # add a legend and a tight plot box

            ax2.set_title("Fractional Cover, Wetness, and Water")

            # Initialise annotation objects to be updated during animation
            t = ax1.annotate("", **annotation_kwargs)

            #########################
            # Add optional overlays #
            #########################

            # Optionally add shapefile overlay(s) from either string path or list of string paths
            if isinstance(shapefile_path, str):

                shapefile = gpd.read_file(shapefile_path)
                shapefile.plot(**shapefile_kwargs, ax=ax1)

            elif isinstance(shapefile_path, list):

                # Iterate through list of string paths
                for shapefile in shapefile_path:

                    shapefile = gpd.read_file(shapefile)
                    shapefile.plot(**shapefile_kwargs, ax=ax1)

            # After adding shapefile, fix extents of plot
            ax1.set_xlim(extents[0], extents[1])
            ax1.set_ylim(extents[2], extents[3])

            # Optionally add colourbar for one band images
            if (len(bands) == 1) & onebandplot_cbar:
                _add_colourbar(
                    ax1,
                    im,
                    tick_fontsize=onebandplot_tick_fontsize,
                    tick_colour=onebandplot_tick_colour,
                    vmin=onebandplot_kwargs["vmin"],
                    vmax=onebandplot_kwargs["vmax"],
                )

            ########################################
            # Create function to update each frame #
            ########################################

            # Function to update figure

            def update_figure(frame_i):

                ####################
                # Plot image panel #
                ####################

                # If possible, extract dates from time dimension
                try:

                    # Get human-readable date info (e.g. "16 May 1990")
                    ts = ds[time_dim][{time_dim: frame_i}].dt
                    year = ts.year.item()
                    month = ts.month.item()
                    day = ts.day.item()
                    date_string = "{} {} {}".format(
                        day, calendar.month_abbr[month], year
                    )

                except:

                    date_string = ds[time_dim][{time_dim: frame_i}].values.item()

                # Create annotation string based on title and date specifications:
                title = title_list[frame_i]
                if title and show_date:
                    title_date = "{}\n{}".format(date_string, title)
                elif title and not show_date:
                    title_date = "{}".format(title)
                elif show_date and not title:
                    title_date = "{}".format(date_string)
                else:
                    title_date = ""

                # Update left panel with annotation and image
                im.set_array(imagelist[frame_i])
                t.set_text(title_date)

                ########################
                # Plot linegraph panel #
                ########################

                # Create list of artists to return
                artist_list = [im, t]

                # Update right panel with temporal line subset, adding each new line into artist_list
                for i, line in enumerate(line_test.lines):

                    # Clip line data to current time, and get x and y values
                    y = df1[
                        df1.index
                        <= datetime(year=year, month=month, day=day, hour=23, minute=59)
                    ].iloc[:, i]
                    x = df1[
                        df1.index
                        <= datetime(year=year, month=month, day=day, hour=23, minute=59)
                    ].index

                    # Plot lines after stripping NaNs (this produces continuous, unbroken lines)
                    line.set_data(x[y.notnull()], y[y.notnull()])
                    artist_list.extend([line])

                # Return the artists set
                return artist_list

            # Nicely space subplots
            fig.tight_layout()

            ##############################
            # Generate and run animation #
            ##############################

            # Generate animation
            ani = animation.FuncAnimation(
                fig=fig,
                func=update_figure,
                frames=timesteps,
                interval=interval,
                blit=True,
            )

            # Export as either MP4 or GIF
            if output_path[-3:] == "mp4":
                print("    Exporting animation to {}".format(output_path))
                ani.save(output_path, dpi=width_pixels / 10.0)

            elif output_path[-3:] == "wmv":
                print("    Exporting animation to {}".format(output_path))
                ani.save(
                    output_path,
                    dpi=width_pixels / 10.0,
                    writer=animation.FFMpegFileWriter(
                        fps=1000 / interval, bitrate=4000, codec="wmv2"
                    ),
                )

            elif output_path[-3:] == "gif":
                print("    Exporting animation to {}".format(output_path))
                ani.save(output_path, dpi=width_pixels / 10.0, writer="imagemagick")

            else:
                print("    Output file type must be either .mp4, .wmv or .gif")

        else:
            print(
                "Please select either one or three bands that all exist in the input dataset"
            )

    else:
        print(
            "At least one x, y or time dimension does not exist in the input dataset. Please use the `time_dim`,"
            "`x_dim` or `y_dim` parameters to override the default dimension names used for plotting"
        )


# Define function to convert xarray dataset to list of one or three band numpy arrays


def _ds_to_arrraylist(
    ds, bands, time_dim, x_dim, y_dim, percentile_stretch, image_proc_func=None
):
    """
    Converts an xarray dataset to a list of numpy arrays for plt.imshow plotting
    """

    # Compute percents
    p_low, p_high = ds[bands].to_array().quantile(percentile_stretch).values

    array_list = []
    for i, timestep in enumerate(ds[time_dim]):

        # Select single timestep from the data array
        ds_i = ds[{time_dim: i}]

        # Get shape of array
        x = len(ds[x_dim])
        y = len(ds[y_dim])

        if len(bands) == 1:

            # Create new one band array
            img_toshow = exposure.rescale_intensity(
                ds_i[bands[0]].values, in_range=(p_low, p_high), out_range="image"
            )

        else:

            # Create new three band array
            rawimg = np.zeros((y, x, 3), dtype=np.float32)

            # Add xarray bands into three dimensional numpy array
            for band, colour in enumerate(bands):

                rawimg[:, :, band] = ds_i[colour].values

            # Stretch contrast using percentile values
            img_toshow = exposure.rescale_intensity(
                rawimg, in_range=(p_low, p_high), out_range=(0, 1.0)
            )

            # Optionally image processing
            if image_proc_func:

                img_toshow = image_proc_func(img_toshow).clip(0, 1)

        array_list.append(img_toshow)

    return array_list, p_low, p_high


def _add_colourbar(
    ax, im, vmin, vmax, cmap="Greys", tick_fontsize=15, tick_colour="black"
):
    """
    Add a nicely formatted colourbar to an animation panel
    """

    # Add colourbar
    axins2 = inset_axes(ax, width="97%", height="4%", loc=8, borderpad=1)
    plt.gcf().colorbar(
        im, cax=axins2, orientation="horizontal", ticks=np.linspace(vmin, vmax, 3)
    )
    axins2.xaxis.set_ticks_position("top")
    axins2.tick_params(axis="x", colors=tick_colour, labelsize=tick_fontsize)

    # Justify left and right labels to edge of plot
    axins2.get_xticklabels()[0].set_horizontalalignment("left")
    axins2.get_xticklabels()[-1].set_horizontalalignment("right")
    labels = [item.get_text() for item in axins2.get_xticklabels()]
    labels[0] = "  " + labels[0]
    labels[-1] = labels[-1] + "  "


if __name__ == "__main__":
    # print that we are running the testing
    print("Testing..")
    # import doctest to test our module for documentation
    import doctest

    doctest.testmod()
    print("Testing done")
