"""Climate data and mass-balance computations"""
# Built ins
import logging
import os
import datetime
import warnings
# External libs
import numpy as np
import pandas as pd
import xarray as xr
from scipy import stats
import netCDF4
import salem
from scipy import optimize as optimization
# Locals
from oggm import cfg
from oggm import utils
from oggm.core import centerlines
from oggm import entity_task, global_task

# Module logger
log = logging.getLogger(__name__)


@global_task
def process_histalp_nonparallel(gdirs, fpath=None):
    """This is the way OGGM used to do it (deprecated).

    It requires an input file with a specific format, and uses lazy
    optimisation (computing time dependant gradients can be slow)
    """

    # Did the user specify a specific climate data file?
    if fpath is None:
        if 'climate_file' in cfg.PATHS:
            fpath = cfg.PATHS['climate_file']

    if not os.path.exists(fpath):
        raise IOError('Custom climate file not found')

    log.info('process_histalp_nonparallel')

    # read the file and data entirely (faster than many I/O)
    with netCDF4.Dataset(fpath, mode='r') as nc:
        lon = nc.variables['lon'][:]
        lat = nc.variables['lat'][:]

        # Time
        time = nc.variables['time']
        time = netCDF4.num2date(time[:], time.units)
        ny, r = divmod(len(time), 12)
        if r != 0:
            raise ValueError('Climate data should be N full years exclusively')
        y0, y1 = time[0].year, time[-1].year

        # Units
        assert nc.variables['hgt'].units == 'm'
        assert nc.variables['temp'].units == 'degC'
        assert nc.variables['prcp'].units == 'kg m-2'

    # Gradient defaults
    use_grad = cfg.PARAMS['temp_use_local_gradient']
    def_grad = cfg.PARAMS['temp_default_gradient']
    g_minmax = cfg.PARAMS['temp_local_gradient_bounds']

    for gdir in gdirs:
        ilon = np.argmin(np.abs(lon - gdir.cenlon))
        ilat = np.argmin(np.abs(lat - gdir.cenlat))
        ref_pix_lon = lon[ilon]
        ref_pix_lat = lat[ilat]
        iprcp, itemp, igrad, ihgt = utils.joblib_read_climate(fpath, ilon,
                                                              ilat, def_grad,
                                                              g_minmax,
                                                              use_grad)
        gdir.write_monthly_climate_file(time, iprcp, itemp, igrad, ihgt,
                                        ref_pix_lon, ref_pix_lat)
        # metadata
        out = {'climate_source': fpath,
               'hydro_yr_0': y0+1, 'hydro_yr_1': y1}
        gdir.write_pickle(out, 'climate_info')


@entity_task(log, writes=['climate_monthly'])
def process_custom_climate_data(gdir):
    """Processes and writes the climate data from a user-defined climate file.

    The input file must have a specific format (see
    oggm-sample-data/test-files/histalp_merged_hef.nc for an example).

    Uses caching for faster retrieval.

    This is the way OGGM does it for the Alps (HISTALP).
    """

    if not (('climate_file' in cfg.PATHS) and
            os.path.exists(cfg.PATHS['climate_file'])):
        raise IOError('Custom climate file not found')

    # read the file
    fpath = cfg.PATHS['climate_file']
    nc_ts = salem.GeoNetcdf(fpath)

    # set temporal subset for the ts data (hydro years)
    sm = cfg.PARAMS['hydro_month_' + gdir.hemisphere]
    em = sm - 1 if (sm > 1) else 12
    yrs = nc_ts.time.year
    y0, y1 = yrs[0], yrs[-1]
    nc_ts.set_period(t0='{}-{:02d}-01'.format(y0, sm),
                     t1='{}-{:02d}-01'.format(y1, em))
    time = nc_ts.time
    ny, r = divmod(len(time), 12)
    if r != 0:
        raise ValueError('Climate data should be N full years exclusively')

    # Units
    assert nc_ts._nc.variables['hgt'].units.lower() in ['m', 'meters', 'meter',
                                                        'metres', 'metre']
    assert nc_ts._nc.variables['temp'].units.lower() in ['degc', 'degrees',
                                                         'degree', 'c']
    assert nc_ts._nc.variables['prcp'].units.lower() in ['kg m-2', 'l m-2',
                                                         'mm', 'millimeters',
                                                         'millimeter']

    # geoloc
    lon = nc_ts._nc.variables['lon'][:]
    lat = nc_ts._nc.variables['lat'][:]

    # Gradient defaults
    use_grad = cfg.PARAMS['temp_use_local_gradient']
    def_grad = cfg.PARAMS['temp_default_gradient']
    g_minmax = cfg.PARAMS['temp_local_gradient_bounds']

    ilon = np.argmin(np.abs(lon - gdir.cenlon))
    ilat = np.argmin(np.abs(lat - gdir.cenlat))
    ref_pix_lon = lon[ilon]
    ref_pix_lat = lat[ilat]

    # read the data
    temp = nc_ts.get_vardata('temp')
    prcp = nc_ts.get_vardata('prcp')
    hgt = nc_ts.get_vardata('hgt')
    igrad = np.zeros(len(time)) + def_grad
    ttemp = temp[:, ilat-1:ilat+2, ilon-1:ilon+2]
    itemp = ttemp[:, 1, 1]
    thgt = hgt[ilat-1:ilat+2, ilon-1:ilon+2]
    ihgt = thgt[1, 1]
    thgt = thgt.flatten()
    iprcp = prcp[:, ilat, ilon]
    nc_ts.close()

    # Now the gradient
    if use_grad:
        for t, loct in enumerate(ttemp):
            slope, _, _, p_val, _ = stats.linregress(thgt,
                                                     loct.flatten())
            igrad[t] = slope if (p_val < 0.01) else def_grad
        # dont exagerate too much
        igrad = np.clip(igrad, g_minmax[0], g_minmax[1])

    gdir.write_monthly_climate_file(time, iprcp, itemp, igrad, ihgt,
                                    ref_pix_lon, ref_pix_lat)
    # metadata
    out = {'climate_source': fpath, 'hydro_yr_0': y0+1, 'hydro_yr_1': y1}
    gdir.write_pickle(out, 'climate_info')


@entity_task(log, writes=['cesm_data'])
def process_cesm_data(gdir, filesuffix='', fpath_temp=None, fpath_precc=None,
                      fpath_precl=None):
    """Processes and writes the climate data for this glacier.

    This function is made for interpolating the Community
    Earth System Model Last Millenial Ensemble (CESM-LME) climate simulations,
    from Otto-Bliesner et al. (2016), to the high-resolution CL2 climatologies
    (provided with OGGM) and writes everything to a NetCDF file.

    Parameters
    ----------
    filesuffix : str
        append a suffix to the filename (useful for ensemble experiments).
    fpath_temp : str
        path to the temp file (default: cfg.PATHS['gcm_temp_file'])
    fpath_precc : str
        path to the precc file (default: cfg.PATHS['gcm_precc_file'])
    fpath_precl : str
        path to the precl file (default: cfg.PATHS['gcm_precl_file'])
    """

    # GCM temperature and precipitation data
    if fpath_temp is None:
        if not ('gcm_temp_file' in cfg.PATHS):
            raise ValueError("Need to set cfg.PATHS['gcm_temp_file']")
        fpath_temp = cfg.PATHS['gcm_temp_file']
    if fpath_precc is None:
        if not ('gcm_precc_file' in cfg.PATHS):
            raise ValueError("Need to set cfg.PATHS['gcm_precc_file']")
        fpath_precc = cfg.PATHS['gcm_precc_file']
    if fpath_precl is None:
        if not ('gcm_precl_file' in cfg.PATHS):
            raise ValueError("Need to set cfg.PATHS['gcm_precl_file']")
        fpath_precl = cfg.PATHS['gcm_precl_file']

    # read the files
    with warnings.catch_warnings():
        # Long time series are currently a pain pandas
        warnings.filterwarnings("ignore", message='Unable to decode time axis')
        tempds = xr.open_dataset(fpath_temp)
    precpcds = xr.open_dataset(fpath_precc, decode_times=False)
    preclpds = xr.open_dataset(fpath_precl, decode_times=False)

    # select for location
    lon = gdir.cenlon
    lat = gdir.cenlat

    # CESM files are in 0-360
    if lon <= 0:
        lon += 360

    # take the closest
    # TODO: consider GCM interpolation?
    temp = tempds.TREFHT.sel(lat=lat, lon=lon, method='nearest')
    precp = precpcds.PRECC.sel(lat=lat, lon=lon, method='nearest') + \
            preclpds.PRECL.sel(lat=lat, lon=lon, method='nearest')

    # from normal years to hydrological years
    sm = cfg.PARAMS['hydro_month_' + gdir.hemisphere]
    em = sm - 1 if (sm > 1) else 12
    # TODO: we don't check if the files actually start in January but we should
    precp = precp[sm-1:sm-13].load()
    temp = temp[sm-1:sm-13].load()
    y0 = int(temp.time.values[0].strftime('%Y'))
    y1 = int(temp.time.values[-1].strftime('%Y'))
    time = pd.period_range('{}-{:02d}'.format(y0, sm),
                           '{}-{:02d}'.format(y1, em), freq='M')
    temp['time'] = time
    precp['time'] = time
    # Workaround for https://github.com/pydata/xarray/issues/1565
    temp['month'] = ('time', time.month)
    precp['month'] = ('time', time.month)
    temp['year'] = ('time', time.year)
    precp['year'] = ('time', time.year)
    ny, r = divmod(len(time), 12)
    assert r == 0

    # Convert m s-1 to mm mth-1
    ndays = np.tile(np.roll(cfg.DAYS_IN_MONTH, 13-sm), y1 - y0)
    precp = precp * ndays * (60 * 60 * 24 * 1000)

    # compute monthly anomalies
    # of temp
    ts_tmp_avg = temp.sel(time=(temp.year >= 1961) & (temp.year <= 1990))
    ts_tmp_avg = ts_tmp_avg.groupby(ts_tmp_avg.month).mean(dim='time')
    ts_tmp = temp.groupby(temp.month) - ts_tmp_avg
    # of precip
    ts_pre_avg = precp.isel(time=(precp.year >= 1961) & (precp.year <= 1990))
    ts_pre_avg = ts_pre_avg.groupby(ts_pre_avg.month).mean(dim='time')
    ts_pre = precp.groupby(precp.month) - ts_pre_avg

    # Get CRU to apply the anomaly to
    fpath = gdir.get_filepath('climate_monthly')
    ds_cru = xr.open_dataset(fpath)

    # Here we assume the gradient is a monthly average
    ts_grad = np.tile(ds_cru.grad[0:12], ny)

    # Add climate anomaly to CRU clim
    dscru = ds_cru.sel(time=slice('1961', '1990'))
    # for temp
    loc_tmp = dscru.temp.groupby('time.month').mean()
    ts_tmp = ts_tmp.groupby(ts_tmp.month) + loc_tmp
    # for prcp
    loc_pre = dscru.prcp.groupby('time.month').mean()
    ts_pre = ts_pre.groupby(ts_pre.month) + loc_pre

    # load dates in right format to save
    dsindex = salem.GeoNetcdf(fpath_temp, monthbegin=True)
    time1 = dsindex.variables['time']
    time2 = time1[sm-1:sm-13] - ndays  # to hydrological years
    time2 = netCDF4.num2date(time2, time1.units, calendar='noleap')

    assert np.all(np.isfinite(ts_pre.values))
    assert np.all(np.isfinite(ts_tmp.values))
    assert np.all(np.isfinite(ts_grad))

    # back to -180 - 180
    loc_lon = precp.lon if precp.lon <= 180 else precp.lon - 360

    gdir.write_monthly_climate_file(time2, ts_pre.values, ts_tmp.values,
                                    ts_grad, float(dscru.ref_hgt),
                                    loc_lon, precp.lat.values,
                                    time_unit=time1.units,
                                    file_name='cesm_data',
                                    filesuffix=filesuffix)

    dsindex._nc.close()
    tempds.close()
    precpcds.close()
    preclpds.close()
    ds_cru.close()


@entity_task(log, writes=['climate_monthly'])
def process_cru_data(gdir):
    """Processes and writes the climate data for this glacier.

    Interpolates the CRU TS data to the high-resolution CL2 climatologies
    (provided with OGGM) and writes everything to a NetCDF file.
    """

    # read the climatology
    clfile = utils.get_cru_cl_file()
    ncclim = salem.GeoNetcdf(clfile)
    # and the TS data
    nc_ts_tmp = salem.GeoNetcdf(utils.get_cru_file('tmp'), monthbegin=True)
    nc_ts_pre = salem.GeoNetcdf(utils.get_cru_file('pre'), monthbegin=True)

    # set temporal subset for the ts data (hydro years)
    sm = cfg.PARAMS['hydro_month_' + gdir.hemisphere]
    em = sm - 1 if (sm > 1) else 12
    yrs = nc_ts_pre.time.year
    y0, y1 = yrs[0], yrs[-1]
    nc_ts_tmp.set_period(t0='{}-{:02d}-01'.format(y0, sm),
                         t1='{}-{:02d}-01'.format(y1, em))
    nc_ts_pre.set_period(t0='{}-{:02d}-01'.format(y0, sm),
                         t1='{}-{:02d}-01'.format(y1, em))
    time = nc_ts_pre.time
    ny, r = divmod(len(time), 12)
    assert r == 0

    # gradient default params
    use_grad = cfg.PARAMS['temp_use_local_gradient']
    def_grad = cfg.PARAMS['temp_default_gradient']
    g_minmax = cfg.PARAMS['temp_local_gradient_bounds']

    lon = gdir.cenlon
    lat = gdir.cenlat

    # This is guaranteed to work because I prepared the file (I hope)
    ncclim.set_subset(corners=((lon, lat), (lon, lat)), margin=1)

    # get climatology data
    loc_hgt = ncclim.get_vardata('elev')
    loc_tmp = ncclim.get_vardata('temp')
    loc_pre = ncclim.get_vardata('prcp')
    loc_lon = ncclim.get_vardata('lon')
    loc_lat = ncclim.get_vardata('lat')

    # see if the center is ok
    if not np.isfinite(loc_hgt[1, 1]):
        # take another candidate where finite
        isok = np.isfinite(loc_hgt)

        # wait: some areas are entirely NaNs, make the subset larger
        _margin = 1
        while not np.any(isok):
            _margin += 1
            ncclim.set_subset(corners=((lon, lat), (lon, lat)), margin=_margin)
            loc_hgt = ncclim.get_vardata('elev')
            isok = np.isfinite(loc_hgt)
        if _margin > 1:
            log.debug('(%s) I had to look up for far climate pixels: %s',
                      gdir.rgi_id, _margin)

        # Take the first candidate (doesn't matter which)
        lon, lat = ncclim.grid.ll_coordinates
        lon = lon[isok][0]
        lat = lat[isok][0]
        # Resubset
        ncclim.set_subset()
        ncclim.set_subset(corners=((lon, lat), (lon, lat)), margin=1)
        loc_hgt = ncclim.get_vardata('elev')
        loc_tmp = ncclim.get_vardata('temp')
        loc_pre = ncclim.get_vardata('prcp')
        loc_lon = ncclim.get_vardata('lon')
        loc_lat = ncclim.get_vardata('lat')

    assert np.isfinite(loc_hgt[1, 1])
    isok = np.isfinite(loc_hgt)
    hgt_f = loc_hgt[isok].flatten()
    assert len(hgt_f) > 0.
    ts_grad = np.zeros(12) + def_grad
    if use_grad and len(hgt_f) >= 5:
        for i in range(12):
            loc_tmp_mth = loc_tmp[i, ...][isok].flatten()
            slope, _, _, p_val, _ = stats.linregress(hgt_f, loc_tmp_mth)
            ts_grad[i] = slope if (p_val < 0.01) else def_grad
    # ... but dont exaggerate too much
    ts_grad = np.clip(ts_grad, g_minmax[0], g_minmax[1])
    # convert to timeserie and hydroyears
    ts_grad = ts_grad.tolist()
    ts_grad = ts_grad[9:] + ts_grad[0:9]
    ts_grad = np.asarray(ts_grad * ny)

    # maybe this will throw out of bounds warnings
    nc_ts_tmp.set_subset(corners=((lon, lat), (lon, lat)), margin=1)
    nc_ts_pre.set_subset(corners=((lon, lat), (lon, lat)), margin=1)

    # compute monthly anomalies
    # of temp
    ts_tmp = nc_ts_tmp.get_vardata('tmp', as_xarray=True)
    ts_tmp_avg = ts_tmp.sel(time=slice('1961-01-01', '1990-12-01'))
    ts_tmp_avg = ts_tmp_avg.groupby('time.month').mean(dim='time')
    ts_tmp = ts_tmp.groupby('time.month') - ts_tmp_avg
    # of precip
    ts_pre = nc_ts_pre.get_vardata('pre', as_xarray=True)
    ts_pre_avg = ts_pre.sel(time=slice('1961-01-01', '1990-12-01'))
    ts_pre_avg = ts_pre_avg.groupby('time.month').mean(dim='time')
    ts_pre = ts_pre.groupby('time.month') - ts_pre_avg

    # interpolate to HR grid
    if np.any(~np.isfinite(ts_tmp[:, 1, 1])):
        # Extreme case, middle pix is not valid
        # take any valid pix from the 3*3 (and hope there's one)
        found_it = False
        for idi in range(2):
            for idj in range(2):
                if np.all(np.isfinite(ts_tmp[:, idj, idi])):
                    ts_tmp[:, 1, 1] = ts_tmp[:, idj, idi]
                    ts_pre[:, 1, 1] = ts_pre[:, idj, idi]
                    found_it = True
        if not found_it:
            msg = '({}) there is no climate data'.format(gdir.rgi_id)
            raise RuntimeError(msg)
    elif np.any(~np.isfinite(ts_tmp)):
        # maybe the side is nan, but we can do nearest
        ts_tmp = ncclim.grid.map_gridded_data(ts_tmp.values, nc_ts_tmp.grid,
                                              interp='nearest')
        ts_pre = ncclim.grid.map_gridded_data(ts_pre.values, nc_ts_pre.grid,
                                              interp='nearest')
    else:
        # We can do bilinear
        ts_tmp = ncclim.grid.map_gridded_data(ts_tmp.values, nc_ts_tmp.grid,
                                              interp='linear')
        ts_pre = ncclim.grid.map_gridded_data(ts_pre.values, nc_ts_pre.grid,
                                              interp='linear')

    # take the center pixel and add it to the CRU CL clim
    # for temp
    loc_tmp = xr.DataArray(loc_tmp[:, 1, 1], dims=['month'],
                           coords={'month': ts_tmp_avg.month})
    ts_tmp = xr.DataArray(ts_tmp[:, 1, 1], dims=['time'],
                          coords={'time': time})
    ts_tmp = ts_tmp.groupby('time.month') + loc_tmp
    # for prcp
    loc_pre = xr.DataArray(loc_pre[:, 1, 1], dims=['month'],
                           coords={'month': ts_pre_avg.month})
    ts_pre = xr.DataArray(ts_pre[:, 1, 1], dims=['time'],
                          coords={'time': time})
    ts_pre = ts_pre.groupby('time.month') + loc_pre

    # done
    loc_hgt = loc_hgt[1, 1]
    loc_lon = loc_lon[1]
    loc_lat = loc_lat[1]
    assert np.isfinite(loc_hgt)
    assert np.all(np.isfinite(ts_pre.values))
    assert np.all(np.isfinite(ts_tmp.values))
    assert np.all(np.isfinite(ts_grad))
    gdir.write_monthly_climate_file(time, ts_pre.values, ts_tmp.values,
                                    ts_grad, loc_hgt, loc_lon, loc_lat)
    ncclim._nc.close()
    nc_ts_tmp._nc.close()
    nc_ts_pre._nc.close()
    # metadata
    out = {'climate_source': 'CRU data', 'hydro_yr_0': y0+1, 'hydro_yr_1': y1}
    gdir.write_pickle(out, 'climate_info')


def mb_climate_on_height(gdir, heights, prcp_fac,
                         time_range=None, year_range=None):
    """Mass-balance climate of the glacier at a specific height

    Reads the glacier's monthly climate data file and computes the
    temperature "energies" (temp above 0) and solid precipitation at the
    required height.

    Parameters:
    -----------
    gdir: the glacier directory
    heights: a 1D array of the heights (in meter) where you want the data
    prcp_fac: the correction factor for precipitation
    time_range (optional): default is to read all data but with this you
    can provide a [datetime, datetime] bounds (inclusive).
    year_range (optional): maybe more useful than the time bounds above.
    Provide a [y0, y1] year range to get the data for specific (hydrological)
    years only

    Returns:
    --------
    (time, tempformelt, prcpsol)::
        - time: array of shape (nt,)
        - tempformelt:  array of shape (len(heights), nt)
        - prcpsol:  array of shape (len(heights), nt)
    """

    if year_range is not None:
        sm = cfg.PARAMS['hydro_month_' + gdir.hemisphere]
        em = sm - 1 if (sm > 1) else 12
        t0 = datetime.datetime(year_range[0]-1, sm, 1)
        t1 = datetime.datetime(year_range[1], em, 1)
        return mb_climate_on_height(gdir, heights, prcp_fac,
                                    time_range=[t0, t1])

    # Parameters
    temp_all_solid = cfg.PARAMS['temp_all_solid']
    temp_all_liq = cfg.PARAMS['temp_all_liq']
    temp_melt = cfg.PARAMS['temp_melt']

    # Read file
    with netCDF4.Dataset(gdir.get_filepath('climate_monthly'), mode='r') as nc:
        # time
        time = nc.variables['time']
        time = netCDF4.num2date(time[:], time.units)
        if time_range is not None:
            p0 = np.where(time == time_range[0])[0]
            try:
                p0 = p0[0]
            except IndexError:
                raise RuntimeError('time_range[0] not found in file')
            p1 = np.where(time == time_range[1])[0]
            try:
                p1 = p1[0]
            except IndexError:
                raise RuntimeError('time_range[1] not found in file')
        else:
            p0 = 0
            p1 = len(time)-1

        time = time[p0:p1+1]

        # Read timeseries
        itemp = nc.variables['temp'][p0:p1+1]
        iprcp = nc.variables['prcp'][p0:p1+1]
        igrad = nc.variables['grad'][p0:p1+1]
        ref_hgt = nc.ref_hgt

    # Correct precipitation
    iprcp *= prcp_fac

    # For each height pixel:
    # Compute temp and tempformelt (temperature above melting threshold)
    npix = len(heights)
    grad_temp = np.atleast_2d(igrad).repeat(npix, 0)
    grad_temp *= (heights.repeat(len(time)).reshape(grad_temp.shape) - ref_hgt)
    temp2d = np.atleast_2d(itemp).repeat(npix, 0) + grad_temp
    temp2dformelt = temp2d - temp_melt
    temp2dformelt = np.clip(temp2dformelt, 0, temp2dformelt.max())
    # Compute solid precipitation from total precipitation
    prcpsol = np.atleast_2d(iprcp).repeat(npix, 0)
    fac = 1 - (temp2d - temp_all_solid) / (temp_all_liq - temp_all_solid)
    fac = np.clip(fac, 0, 1)
    prcpsol = prcpsol * fac

    return time, temp2dformelt, prcpsol


def mb_yearly_climate_on_height(gdir, heights, prcp_fac,
                                year_range=None, flatten=False):
    """Yearly mass-balance climate of the glacier at a specific height

    The precipitation time series are not corrected!

    Parameters:
    -----------
    gdir: the glacier directory
    heights: a 1D array of the heights (in meter) where you want the data
    prcp_fac: the correction factor for precipitation
    year_range (optional): a [y0, y1] year range to get the data for specific (
    hydrological) years only
    flatten: for some applications (glacier average MB) it's ok to flatten  the
    data (average over height) prior to annual summing.

    Returns:
    --------
    (years, tempformelt, prcpsol)::
        - years: array of shape (ny,)
        - tempformelt:  array of shape (len(heights), ny) (or ny if flatten
        is set)
        - prcpsol:  array of shape (len(heights), ny) (or ny if flatten
        is set)
    """

    time, temp, prcp = mb_climate_on_height(gdir, heights, prcp_fac,
                                            year_range=year_range)

    ny, r = divmod(len(time), 12)
    if r != 0:
        raise ValueError('Climate data should be N full years exclusively')
    # Last year gives the tone of the hydro year
    years = np.arange(time[-1].year-ny+1, time[-1].year+1, 1)

    if flatten:
        # Spatial average
        temp_yr = np.zeros(len(years))
        prcp_yr = np.zeros(len(years))
        temp = np.mean(temp, axis=0)
        prcp = np.mean(prcp, axis=0)
        for i, y in enumerate(years):
            temp_yr[i] = np.sum(temp[i*12:(i+1)*12])
            prcp_yr[i] = np.sum(prcp[i*12:(i+1)*12])
    else:
        # Annual prcp and temp for each point (no spatial average)
        temp_yr = np.zeros((len(heights), len(years)))
        prcp_yr = np.zeros((len(heights), len(years)))
        for i, y in enumerate(years):
            temp_yr[:, i] = np.sum(temp[:, i*12:(i+1)*12], axis=1)
            prcp_yr[:, i] = np.sum(prcp[:, i*12:(i+1)*12], axis=1)

    return years, temp_yr, prcp_yr


def mb_yearly_climate_on_glacier(gdir, prcp_fac, year_range=None):
    """Yearly mass-balance climate at all glacier heights,
    multiplied with the flowlines widths. (all in pix coords.)

    The precipitation time series are not corrected!

    Parameters:
    -----------
    gdir : the glacier directory
    prcp_fac: the correction factor for precipitation
    year_range (optional): a [y0, y1] year range to get the data for specific
    (hydrological) years only

    Returns:
    --------
    (years, tempformelt, prcpsol)::
        - years: array of shape (ny,)
        - tempformelt:  array of shape (len(heights), ny)
        - prcpsol:  array of shape (len(heights), ny) (not corrected!)
    """

    flowlines = gdir.read_pickle('inversion_flowlines')

    heights = np.array([])
    widths = np.array([])
    for fl in flowlines:
        heights = np.append(heights, fl.surface_h)
        widths = np.append(widths, fl.widths)

    years, temp, prcp = mb_yearly_climate_on_height(gdir, heights, prcp_fac,
                                                    year_range=year_range,
                                                    flatten=False)

    temp = np.average(temp, axis=0, weights=widths)
    prcp = np.average(prcp, axis=0, weights=widths)

    return years, temp, prcp


@entity_task(log, writes=['mu_candidates'])
def mu_candidates(gdir, prcp_sf=None):
    """Computes the mu candidates.

    For each 31 year-period centered on the year of interest, mu is is the
    temperature sensitivity necessary for the glacier with its current shape
    to be in equilibrium with its climate.

    For glaciers with MB data only!

    Parameters
    ----------
    gdir : oggm.GlacierDirectory
    prcp_sf : float (optional)
        force to a certain prcp scaling factor
    """

    mu_hp = int(cfg.PARAMS['mu_star_halfperiod'])

    # Only get the years were we consider looking for tstar
    y0, y1 = cfg.PARAMS['tstar_search_window']
    ci = gdir.read_pickle('climate_info')
    y0 = y0 or ci['hydro_yr_0']
    y1 = y1 or ci['hydro_yr_1']

    years, temp_yr, prcp_yr = mb_yearly_climate_on_glacier(gdir, 1.,
                                                           year_range=[y0, y1])

    # Be sure we have no marine terminating glacier
    assert gdir.terminus_type == 'Land-terminating'

    # prcp scaling factor - one or more?
    if cfg.PARAMS['prcp_scaling_factor'] == 'stddev_perglacier':
        sf = np.arange(0.5, 5.01, 0.05)
    elif cfg.PARAMS['prcp_scaling_factor'] == 'stddev':
        sf = [float(prcp_sf)]
    else:
        sf = np.asarray([cfg.PARAMS['prcp_scaling_factor']])

    # Compute mu for each 31-yr climatological period and each prcp factor
    ny = len(years)
    nsf = len(sf)
    mu_yr_clim = np.zeros((ny, nsf)) * np.NaN
    for j, fac in enumerate(sf):
        for i, y in enumerate(years):
            # Ignore begin and end
            if ((i-mu_hp) < 0) or ((i+mu_hp) >= ny):
                continue
            t_avg = np.mean(temp_yr[i-mu_hp:i+mu_hp+1])
            if t_avg > 1e-3:  # if too cold no melt possible
                prcp_ts = prcp_yr[i-mu_hp:i+mu_hp+1] * fac
                mu_yr_clim[i, j] = np.mean(prcp_ts) / t_avg

    # Check that we found a least one mustar
    if np.sum(np.isfinite(mu_yr_clim)) < 1:
        raise RuntimeError('({}) no mustar candidates found.'
                           .format(gdir.rgi_id))

    # Write
    df = pd.DataFrame(data=mu_yr_clim, index=years, columns=sf)
    gdir.write_pickle(df, 'mu_candidates')


def t_star_from_refmb(gdir, mbdf):
    """Computes the t* for the glacier, given a series of MB measurements.

    Could be multiprocessed but its probably not necessary.

    Parameters
    ----------
    gdirs: the list of oggm.GlacierDirectory objects where to write the data.
    mbdf: a pd.Series containing the observed MB data indexed by year

    Returns
    -------
    A dict: {t_star:[], bias:[], bias_std:[], prcp_fac:float}
    """

    # Only divide 0, we believe the original RGI entities to be the ref...
    years, temp_yr_ts, prcp_yr_ts = mb_yearly_climate_on_glacier(gdir, 1.)

    # which years to look at
    selind = np.searchsorted(years, mbdf.index)
    temp_yr_ts = temp_yr_ts[selind]
    prcp_yr_ts = prcp_yr_ts[selind]
    temp_yr = np.mean(temp_yr_ts)
    prcp_yr = np.mean(prcp_yr_ts)

    # Average oberved mass-balance
    ref_mb = np.mean(mbdf)
    ref_mb_std = np.std(mbdf)

    # Average mass-balance per mu and fac
    mu_yr_clim_df = gdir.read_pickle('mu_candidates')

    odf = pd.DataFrame(index=mu_yr_clim_df.columns)
    out = dict()
    for prcp_fac in mu_yr_clim_df:

        mu_yr_clim = mu_yr_clim_df[prcp_fac]
        nmu = len(mu_yr_clim)
        mb_per_mu = prcp_yr * prcp_fac - mu_yr_clim * temp_yr
        mbts_per_mu = np.atleast_2d(prcp_yr_ts * prcp_fac).repeat(nmu, 0) - \
                      np.atleast_2d(mu_yr_clim).T * \
                      np.atleast_2d(temp_yr_ts).repeat(nmu, 0)
        std_per_mu = mb_per_mu*0 + np.std(mbts_per_mu, axis=1)

        # Diff to reference
        diff = (mb_per_mu - ref_mb).dropna()
        diff_std = (std_per_mu - ref_mb_std).dropna()
        signchange = utils.signchange(diff)

        # If sign changes save them
        # TODO: ideas to do this better:
        #  - apply a smooth (take noise into account)
        #  - take longer stable periods into account
        # these stuffs could be proven better (or not) with cross-val
        pchange = np.where(signchange == 1)[0]
        years = diff.index
        diff = diff.values
        diff_std = diff_std.values
        if len(pchange) > 0:
            t_stars = []
            bias = []
            std_bias = []
            for p in pchange:
                # Check the side with the smallest bias
                ide = p-1 if np.abs(diff[p-1]) < np.abs(diff[p]) else p
                if years[ide] not in t_stars:
                    t_stars.append(years[ide])
                    bias.append(diff[ide])
                    std_bias.append(diff_std[ide])
        else:
            amin = np.argmin(np.abs(diff))
            t_stars = [years[amin]]
            bias = [diff[amin]]
            std_bias = [diff_std[amin]]

        # (prcp_fac, t_stars, bias, std_bias)
        odf.loc[prcp_fac, 'avg_bias'] = np.mean(bias)
        odf.loc[prcp_fac, 'avg_std_bias'] = np.mean(std_bias)
        odf.loc[prcp_fac, 'n_tstar'] = len(std_bias)
        out[prcp_fac] = {'t_star': t_stars, 'bias': bias, 'std_bias': std_bias,
                         'prcp_fac': prcp_fac}

    # write
    gdir.write_pickle(odf, 'prcp_fac_optim')

    # we take the closest result and see later if it needs cleverer handling
    amin = odf.avg_std_bias.abs().idxmin()  # this gives back an index!
    return out[amin]


def calving_mb(gdir):
    """Calving mass-loss in specific MB equivalent.

    This is necessary to compute mu star.

    TODO: currently this is hardcoded for Columbia, but we should come-up with
    somthing better!
    """

    if not gdir.is_tidewater:
        return 0.

    # Ok. Just take the caving rate from cfg and change its units
    # Original units: km3 a-1, to change to mm a-1 (units of specific MB)
    return gdir.inversion_calving_rate * 1e9 * cfg.RHO / gdir.rgi_area_m2


@entity_task(log, writes=['local_mustar'])
def local_mustar(gdir, tstar=None, bias=None, prcp_fac=None,
                 minimum_mustar=0.):
    """Compute the local mustar from interpolated tstars.

    Parameters
    ----------
    gdir : oggm.GlacierDirectory
    tstar: int
        the year where the glacier should be equilibrium
    bias: int
        the associated reference bias
    prcp_fac: int
        the associated precipitation factor
    minimum_mustar: float
        if mustar goes below this threshold, clip it to that value.
        If you want this to happen with `minimum_mustar=0.` you will have
        to set `cfg.PARAMS['allow_negative_mustar']=True` first.
    """

    assert bias is not None
    assert prcp_fac is not None
    assert tstar is not None

    # Climate period
    mu_hp = int(cfg.PARAMS['mu_star_halfperiod'])
    yr = [tstar-mu_hp, tstar+mu_hp]

    # Do we have a calving glacier?
    cmb = calving_mb(gdir)

    log.info('(%s) local mu* for t*=%d', gdir.rgi_id, tstar)

    # Get the corresponding mu
    years, temp_yr, prcp_yr = mb_yearly_climate_on_glacier(gdir, prcp_fac,
                                                           year_range=yr)
    assert len(years) == (2 * mu_hp + 1)

    # mustar is taking calving into account (units of specific MB)
    mustar = (np.mean(prcp_yr) - cmb) / np.mean(temp_yr)
    if not np.isfinite(mustar):
        raise RuntimeError('{} has a non finite mu'.format(gdir.rgi_id))
    if not cfg.PARAMS['allow_negative_mustar']:
        if mustar < 0:
            raise RuntimeError('{} has a negative mu'.format(gdir.rgi_id))
    # For the calving param it might be useful to clip the mu
    mustar = np.clip(mustar, minimum_mustar, np.max(mustar))

    # Scalars in a small dataframe for later
    df = pd.DataFrame()
    df['rgi_id'] = [gdir.rgi_id]
    df['t_star'] = [tstar]
    df['mu_star'] = [mustar]
    df['prcp_fac'] = [prcp_fac]
    df['bias'] = [bias]
    df.to_csv(gdir.get_filepath('local_mustar'), index=False)


@entity_task(log, writes=['inversion_flowlines'])
def apparent_mb(gdir):
    """Compute the apparent mb from the calibrated mustar.

    Parameters
    ----------
    """

    # Calibrated data
    df = pd.read_csv(gdir.get_filepath('local_mustar')).iloc[0]
    tstar = df['t_star']
    prcp_fac = df['prcp_fac']
    mu_star = df['mu_star']
    bias = df['bias']

    # Climate period
    mu_hp = int(cfg.PARAMS['mu_star_halfperiod'])
    yr = [tstar-mu_hp, tstar+mu_hp]

    # Do we have a calving glacier?
    cmb = calving_mb(gdir)

    # For each flowline compute the apparent MB
    fls = gdir.read_pickle('inversion_flowlines')

    # Reset flux
    for fl in fls:
        fl.flux = np.zeros(len(fl.surface_h))

    # Flowlines in order to be sure
    for fl in fls:
        y, t, p = mb_yearly_climate_on_height(gdir, fl.surface_h, prcp_fac,
                                              year_range=yr,
                                              flatten=False)
        fl.set_apparent_mb(np.mean(p, axis=1) - mu_star*np.mean(t, axis=1))

    # Sometimes, low lying tributaries have a non-physically consistent
    # Mass-balance. We should remove these, and start all over again until
    # all tributaries are consistent
    do_filter = [fl.flux_needed_correction for fl in fls]
    if cfg.PARAMS['filter_for_neg_flux'] and np.any(do_filter):
        assert not do_filter[-1]  # This should not happen
        # Keep only the good lines
        heads = [fl.orig_head for fl in fls if not fl.flux_needed_correction]
        centerlines.compute_centerlines(gdir, heads=heads, reset=True)
        centerlines.initialize_flowlines(gdir, reset=True)
        if gdir.has_file('downstream_line'):
            centerlines.compute_downstream_line(gdir, reset=True)
            centerlines.compute_downstream_bedshape(gdir, reset=True)
        centerlines.catchment_area(gdir, reset=True)
        centerlines.catchment_intersections(gdir, reset=True)
        centerlines.catchment_width_geom(gdir, reset=True)
        centerlines.catchment_width_correction(gdir, reset=True)
        local_mustar(gdir, tstar=tstar, bias=bias, prcp_fac=prcp_fac,
                     reset=True)
        # Ok, re-call ourselves
        return apparent_mb(gdir, reset=True)

    # Check and write
    aflux = fls[-1].flux[-1] * 1e-9 / cfg.RHO * gdir.grid.dx**2
    # If not marine and a bit far from zero, warning
    if cmb == 0 and not np.allclose(fls[-1].flux[-1], 0., atol=0.01):
        log.warning('(%s) flux should be zero, but is: '
                    '%.4f km3 ice yr-1', gdir.rgi_id, aflux)
    # If not marine and quite far from zero, error
    if cmb == 0 and not np.allclose(fls[-1].flux[-1], 0., atol=1):
        msg = ('({}) flux should be zero, but is: {:.4f} km3 ice yr-1'
               .format(gdir.rgi_id, aflux))
        raise RuntimeError(msg)
    gdir.write_pickle(fls, 'inversion_flowlines')


@entity_task(log, writes=['inversion_flowlines', 'linear_mb_params'])
def apparent_mb_from_linear_mb(gdir, mb_gradient=3.):
    """Compute apparent mb from a linear mass-balance assumption (for testing).

    This is for testing currently, but could be used as alternative method
    for the inversion quite easily.

    Parameters
    ----------
    gdir : oggm.GlacierDirectory
    """

    # Do we have a calving glacier?
    cmb = calving_mb(gdir)

    # Get the height and widths along the fls
    h, w = gdir.get_inversion_flowline_hw()

    # Now find the ELA till the integrated mb is zero
    from oggm.core.massbalance import LinearMassBalance
    def to_minimize(ela_h):
        mbmod = LinearMassBalance(ela_h[0], grad=mb_gradient)
        smb = mbmod.get_specific_mb(h, w)
        return (smb - cmb)**2

    ela_h = optimization.minimize(to_minimize, [0.], bounds=((0, 10000), ))
    ela_h = ela_h['x'][0]
    mbmod = LinearMassBalance(ela_h, grad=mb_gradient)

    # For each flowline compute the apparent MB
    fls = gdir.read_pickle('inversion_flowlines')

    # Reset flux
    for fl in fls:
        fl.flux = np.zeros(len(fl.surface_h))

    # Flowlines in order to be sure
    for fl in fls:
        mbz = mbmod.get_annual_mb(fl.surface_h) * cfg.SEC_IN_YEAR * cfg.RHO
        fl.set_apparent_mb(mbz)

    # Check and write
    aflux = fls[-1].flux[-1] * 1e-9 / cfg.RHO * gdir.grid.dx**2
    # If not marine and a bit far from zero, warning
    if cmb == 0 and not np.allclose(fls[-1].flux[-1], 0., atol=0.01):
        log.warning('(%s) flux should be zero, but is: '
                    '%.4f km3 ice yr-1', gdir.rgi_id, aflux)
    # If not marine and quite far from zero, error
    if cmb == 0 and not np.allclose(fls[-1].flux[-1], 0., atol=1):
        msg = ('({}) flux should be zero, but is: {:.4f} km3 ice yr-1'
               .format(gdir.rgi_id, aflux))
        raise RuntimeError(msg)
    gdir.write_pickle(fls, 'inversion_flowlines')
    gdir.write_pickle({'ela_h': ela_h, 'grad': mb_gradient},
                      'linear_mb_params')


def _get_optimal_scaling_factor(ref_gdirs):
    """Get the precipitation scaling factor that minimizes the std dev error.
    """

    from scipy import optimize as optimization

    def to_optimize(sf):
        abs_std = []
        for gdir in ref_gdirs:
            # all possible mus
            mu_candidates(gdir, prcp_sf=sf, reset=True)
            # list of mus compatibles with refmb
            mbdf = gdir.get_ref_mb_data()['ANNUAL_BALANCE']
            res = t_star_from_refmb(gdir, mbdf)
            abs_std.append(np.mean(res['std_bias']))
        return np.mean(abs_std)**2

    with utils.DisableLogger():
        opti = optimization.minimize(to_optimize, [1.],
                                     bounds=((0.01, 10),),
                                     tol=1)

    fac = opti['x'][0]
    log.info('Optimal prcp factor: {:.2f}'.format(fac))
    return fac


@global_task
def compute_ref_t_stars(gdirs):
    """ Detects the best t* for the reference glaciers.

    Parameters
    ----------
    gdirs: list of oggm.GlacierDirectory objects
    """

    if not cfg.PARAMS['run_mb_calibration']:
        raise RuntimeError('Are you sure you want to calibrate the reference '
                           't*? There is a pre-calibrated version available. '
                           'If you know what you are doing and still want to '
                           'calibrate, set the `run_mb_calibration` parameter '
                           'to `True`.')

    log.info('Compute the reference t* and mu* for WGMS glaciers')

    # Reference glaciers only if in the list and period is good
    ref_gdirs = utils.get_ref_mb_glaciers(gdirs)

    prcp_sf = None
    if cfg.PARAMS['prcp_scaling_factor'] == 'stddev':
        prcp_sf = _get_optimal_scaling_factor(ref_gdirs)

    # Loop
    only_one = []  # start to store the glaciers with just one t*
    per_glacier = dict()
    for gdir in ref_gdirs:
        # all possible mus
        mu_candidates(gdir, prcp_sf=prcp_sf, reset=True)
        # list of mus compatibles with refmb
        mbdf = gdir.get_ref_mb_data()['ANNUAL_BALANCE']
        res = t_star_from_refmb(gdir, mbdf)

        # if we have just one candidate this is good
        if len(res['t_star']) == 1:
            only_one.append(gdir.rgi_id)
        # this might be more than one, we'll have to select them later
        per_glacier[gdir.rgi_id] = (gdir, res['t_star'], res['bias'],
                                    res['prcp_fac'])

    # At least one of the glaciers should have a single t*, otherwise we don't
    # know how to start
    if len(only_one) == 0:
        # TODO: hardcoded stuff here, for the test workflow
        for v in ['4', '5', '6']:
            rid = 'RGI{}0-11.00897'.format(v)
            if rid in per_glacier:
                only_one.append(rid)
                gdir, t_star, res_bias, prcp_fac = per_glacier[rid]
                per_glacier[rid] = (gdir, [t_star[-1]], [res_bias[-1]],
                                    prcp_fac)
    if len(only_one) == 0:
        raise RuntimeError('We need at least one glacier with one '
                           'tstar only.')

    log.info('%d out of %d have only one possible t*. Start from here',
             len(only_one), len(ref_gdirs))

    # Ok. now loop over the nearest glaciers until all have a unique t*
    while True:
        ids_left = [id for id in per_glacier.keys() if id not in only_one]
        if len(ids_left) == 0:
            break

        # Compute the summed distance to all glaciers with one t*
        distances = []
        for id in ids_left:
            gdir = per_glacier[id][0]
            lon, lat = gdir.cenlon, gdir.cenlat
            ldis = 0.
            for id_o in only_one:
                ogdir = per_glacier[id_o][0]
                ldis += utils.haversine(lon, lat, ogdir.cenlon, ogdir.cenlat)
            distances.append(ldis)

        # Take the shortest and choose the best t*
        pg = per_glacier[ids_left[np.argmin(distances)]]
        gdir, t_star, res_bias, prcp_fac = pg
        distances = []
        for tt in t_star:
            ldis = 0.
            for id_o in only_one:
                _, ot_star, _, _ = per_glacier[id_o]
                ldis += np.abs(tt - ot_star)
            distances.append(ldis)
        amin = np.argmin(distances)
        per_glacier[gdir.rgi_id] = (gdir, [t_star[amin]], [res_bias[amin]],
                                    prcp_fac)
        only_one.append(gdir.rgi_id)

    # Write out the data
    rgis_ids, t_stars, prcp_facs,  biases, lons, lats, n_mb = ([], [], [], [],
                                                               [], [], [])
    for id, (gdir, t_star, res_bias, prcp_fac) in per_glacier.items():
        rgis_ids.append(id)
        t_stars.append(t_star[0])
        prcp_facs.append(prcp_fac)
        biases.append(res_bias[0])
        lats.append(gdir.cenlat)
        lons.append(gdir.cenlon)
        n_mb.append(len(gdir.get_ref_mb_data()))
    df = pd.DataFrame(index=rgis_ids)
    df['lon'] = lons
    df['lat'] = lats
    df['n_mb_years'] = n_mb
    df['tstar'] = t_stars
    df['prcp_fac'] = prcp_facs
    df['bias'] = biases
    file = os.path.join(cfg.PATHS['working_dir'], 'ref_tstars.csv')
    df.sort_index().to_csv(file)


@global_task
def distribute_t_stars(gdirs, ref_df=None, minimum_mustar=0.):
    """After the computation of the reference tstars, apply
    the interpolation to each individual glacier.

    Parameters
    ----------
    gdirs : []
        list of oggm.GlacierDirectory objects
    ref_df : pd.Dataframe
        replace the default calibration list
    minimum_mustar: float
        if mustar goes below this threshold, clip it to that value.
        If you want this to happen with `minimum_mustar=0.` you will have
        to set `cfg.PARAMS['allow_negative_mustar']=True` first.
    """

    log.info('Distribute t* and mu*')

    if ref_df is None:
        fp = os.path.join(cfg.PATHS['working_dir'], 'ref_tstars.csv')
        if not cfg.PARAMS['run_mb_calibration']:
            # Make some checks and use the default one
            if (('climate_file' in cfg.PATHS) and
                    os.path.exists(cfg.PATHS['climate_file'])):
                raise RuntimeError('If you are using a custom climate file '
                                   'you should run your own MB calibration.')
            fn = 'oggm_ref_tstars_rgi{}_cru4.csv'.format(gdirs[0].rgi_version)
            fp = utils.get_demo_file(fn)
        ref_df = pd.read_csv(fp)

    for gdir in gdirs:

        # Compute the distance to each glacier
        distances = utils.haversine(gdir.cenlon, gdir.cenlat,
                                    ref_df.lon, ref_df.lat)

        # Take the 10 closests
        aso = np.argsort(distances)[0:9]
        amin = ref_df.iloc[aso]
        distances = distances[aso]**2

        # If really close no need to divide, else weighted average
        if distances.iloc[0] <= 0.1:
            tstar = amin.tstar.iloc[0]
            prcp_fac = amin.prcp_fac.iloc[0]
            bias = amin.bias.iloc[0]
        else:
            tstar = int(np.average(amin.tstar, weights=1./distances))
            prcp_fac = np.average(amin.prcp_fac, weights=1./distances)
            bias = np.average(amin.bias, weights=1./distances)

        # Go
        local_mustar(gdir, tstar=tstar, bias=bias, prcp_fac=prcp_fac,
                     reset=True, minimum_mustar=minimum_mustar)


@global_task
def crossval_t_stars(gdirs):
    """Cross-validate the interpolation of tstar to each individual glacier.

    This is a thorough check (redoes many calculations) because it recomputes
    the chosen tstars at each time. You should use quick_crossval_t_stars
    instead.

    Parameters
    ----------
    gdirs: list of oggm.GlacierDirectory objects
    """

    log.info('Cross-validate the t* and mu* determination')

    full_ref_df = pd.read_csv(os.path.join(cfg.PATHS['working_dir'],
                                           'ref_tstars.csv'), index_col=0)

    rgdirs = utils.get_ref_mb_glaciers(gdirs)
    n = len(full_ref_df)
    for i, rid in enumerate(full_ref_df.index):

        log.info('Cross-validation iteration {} of {}'.format(i+1, n))

        # the glacier to look at
        gdir = [g for g in rgdirs if g.rgi_id == rid][0]

        # the reference glaciers
        ref_gdirs = [g for g in rgdirs if g.rgi_id != rid]

        # redo the computations
        with utils.DisableLogger():
            compute_ref_t_stars(ref_gdirs)
            distribute_t_stars([gdir])

        # store
        rdf = pd.read_csv(gdir.get_filepath('local_mustar'))
        full_ref_df.loc[rid, 'cv_tstar'] = int(rdf['t_star'].values[0])
        full_ref_df.loc[rid, 'cv_mustar'] = rdf['mu_star'].values[0]
        full_ref_df.loc[rid, 'cv_prcp_fac'] = rdf['prcp_fac'].values[0]
        full_ref_df.loc[rid, 'cv_bias'] = rdf['bias'].values[0]

    # write
    file = os.path.join(cfg.PATHS['working_dir'], 'crossval_tstars.csv')
    full_ref_df.to_csv(file)


@global_task
def quick_crossval_t_stars(gdirs):
    """Cross-validate the interpolation of tstar to each individual glacier.

    This version does NOT recompute the precipitation scaling factor at each
    round (this quite OK to do so)

    Parameters
    ----------
    gdirs: list of oggm.GlacierDirectory objects
    """

    log.info('Cross-validate the t* and mu* determination')

    rgdirs = utils.get_ref_mb_glaciers(gdirs)

    # This might be redundant but we redo the calc here
    with utils.DisableLogger():
        compute_ref_t_stars(rgdirs)
    full_ref_df = pd.read_csv(os.path.join(cfg.PATHS['working_dir'],
                                           'ref_tstars.csv'), index_col=0)
    with utils.DisableLogger():
        distribute_t_stars(rgdirs)

    n = len(full_ref_df)
    for i, rid in enumerate(full_ref_df.index):

        # log.info('Cross-validation iteration {} of {}'.format(i+1, n))

        # the glacier to look at
        gdir = [g for g in rgdirs if g.rgi_id == rid][0]

        # the reference glaciers
        tmp_ref_df = full_ref_df.loc[full_ref_df.index != rid]

        # before the cross-val we can get the info about "real" mustar
        rdf = pd.read_csv(gdir.get_filepath('local_mustar'))
        full_ref_df.loc[rid, 'mustar'] = rdf['mu_star'].values[0]

        # redo the computations
        with utils.DisableLogger():
            distribute_t_stars([gdir], ref_df=tmp_ref_df)

        # store
        rdf = pd.read_csv(gdir.get_filepath('local_mustar'))
        full_ref_df.loc[rid, 'cv_tstar'] = int(rdf['t_star'].values[0])
        full_ref_df.loc[rid, 'cv_mustar'] = rdf['mu_star'].values[0]
        full_ref_df.loc[rid, 'cv_prcp_fac'] = rdf['prcp_fac'].values[0]
        full_ref_df.loc[rid, 'cv_bias'] = rdf['bias'].values[0]

    # Reproduce Ben's figure
    for i, rid in enumerate(full_ref_df.index):
        # the glacier to look at
        gdir = full_ref_df.loc[full_ref_df.index == rid]
        # the reference glaciers
        tmp_ref_df = full_ref_df.loc[full_ref_df.index != rid]

        # Compute the distance
        distances = utils.haversine(gdir.lon.values[0], gdir.lat.values[0],
                                    tmp_ref_df.lon, tmp_ref_df.lat)

        # Take the 10 closests
        aso = np.argsort(distances)[0:9]
        amin = tmp_ref_df.iloc[aso]
        distances = distances[aso] ** 2
        interp = np.average(amin.mustar, weights=1. / distances)
        full_ref_df.loc[rid, 'interp_mustar'] = interp

    # write
    file = os.path.join(cfg.PATHS['working_dir'], 'crossval_tstars.csv')
    full_ref_df.to_csv(file)
