Generate daily ETa collection from ETo and ETf collections

    Parameters
    ----------
    target_coll : ee.ImageCollection
        Source images will be interpolated to each target image time_start.
        Target images should have a daily time step.  This will typically be
        the reference ET (ETr) collection.
    source_coll : ee.ImageCollection
        Images that will be interpolated to the target image collection.
        This will typically be the fraction of reference ET (ETrF) collection.
    interp_days : int, optional
        Number of days before and after each image date to include in the
        interpolation (the default is 32).
    interp_method : {'linear'}, optional
        Interpolation method (the default is 'linear').

    Returns
    -------
    ee.ImageCollection() of daily interpolated images

    Raises
    ------
    ValueError
        If `interp_method` is not a supported method.

def daily(target_coll, source_coll, interp_days=32, interp_method='linear'):
    """Generate daily ETa collection from ETo and ETf collections

    Parameters
    ----------
    target_coll : ee.ImageCollection
        Source images will be interpolated to each target image time_start.
        Target images should have a daily time step.  This will typically be
        the reference ET (ETr) collection.
    source_coll : ee.ImageCollection
        Images that will be interpolated to the target image collection.
        This will typically be the fraction of reference ET (ETrF) collection.
    interp_days : int, optional
        Number of days before and after each image date to include in the
        interpolation (the default is 32).
    interp_method : {'linear'}, optional
        Interpolation method (the default is 'linear').

    Returns
    -------
    ee.ImageCollection() of daily interpolated images

    Raises
    ------
    ValueError
        If `interp_method` is not a supported method.

    """

    # # DEADBEEF - This module is assuming that the time band is already in
    # #   the source collection.
    # # Uncomment the following to add a time band here instead.
    # def add_utc0_time_band(image):
    #     date_0utc = utils.date_0utc(ee.Date(image.get('system:time_start')))
    #     return image.addBands([
    #         image.select([0]).double().multiply(0).add(date_0utc.millis())\
    #             .rename(['time'])])
    # source_coll = ee.ImageCollection(source_coll.map(add_utc0_time_band))

    if interp_method.lower() == 'linear':
        def _linear(image):
            """Linearly interpolate source images to target image time_start(s)

            Parameters
            ----------
            image : ee.Image.
                The first band in the image will be used as the "target" image
                and will be returned with the output image.

            Returns
            -------
            ee.Image of interpolated values with band name 'src'

            Notes
            -----
            The source collection images must have a time band.
            This function is intended to be mapped over an image collection and
                can only take one input parameter.

            """
            target_image = ee.Image(image).select(0).double()
            target_date = ee.Date(image.get('system:time_start'))

            # All filtering will be done based on 0 UTC dates
            utc0_date = utils.date_0utc(target_date)
            # utc0_time = target_date.update(hour=0, minute=0, second=0)\
            #     .millis().divide(1000).floor().multiply(1000)
            time_image = ee.Image.constant(utc0_date.millis()).double()

            # Build nodata images/masks that can be placed at the front/back of
            #   of the qm image collections in case the collections are empty.
            bands = source_coll.first().bandNames()
            prev_qm_mask = ee.Image.constant(ee.List.repeat(1, bands.length()))\
                .double().rename(bands).updateMask(0)\
                .set({
                    'system:time_start': utc0_date.advance(
                        -interp_days - 1, 'day').millis()})
            next_qm_mask = ee.Image.constant(ee.List.repeat(1, bands.length()))\
                .double().rename(bands).updateMask(0)\
                .set({
                    'system:time_start': utc0_date.advance(
                        interp_days + 2, 'day').millis()})

            # Build separate collections for before and after the target date
            prev_qm_coll = source_coll.filterDate(
                    utc0_date.advance(-interp_days, 'day'), utc0_date)\
                .merge(ee.ImageCollection(prev_qm_mask))
            next_qm_coll = source_coll.filterDate(
                    utc0_date, utc0_date.advance(interp_days + 1, 'day'))\
                .merge(ee.ImageCollection(next_qm_mask))

            # Flatten the previous/next collections to single images
            # The closest image in time should be on "top"
            # CGM - Is the previous collection already sorted?
            # prev_qm_image = prev_qm_coll.mosaic()
            prev_qm_image = prev_qm_coll.sort('system:time_start', True).mosaic()
            next_qm_image = next_qm_coll.sort('system:time_start', False).mosaic()

            # DEADBEEF - It might be easier to interpolate all bands instead of
            #   separating the value and time bands
            # prev_value_image = ee.Image(prev_qm_image).double()
            # next_value_image = ee.Image(next_qm_image).double()

            # Interpolate all bands except the "time" band
            prev_bands = prev_qm_image.bandNames()\
                .filter(ee.Filter.notEquals('item', 'time'))
            next_bands = next_qm_image.bandNames() \
                .filter(ee.Filter.notEquals('item', 'time'))
            prev_value_image = ee.Image(prev_qm_image.select(prev_bands)).double()
            next_value_image = ee.Image(next_qm_image.select(next_bands)).double()
            prev_time_image = ee.Image(prev_qm_image.select('time')).double()
            next_time_image = ee.Image(next_qm_image.select('time')).double()

            # Fill masked values with values from the opposite image
            # Something like this is needed to ensure there are always two
            #   values to interpolate between
            # For data gaps, this will cause a flat line instead of a ramp
            prev_time_mosaic = ee.Image(ee.ImageCollection.fromImages([
                next_time_image, prev_time_image]).mosaic())
            next_time_mosaic = ee.Image(ee.ImageCollection.fromImages([
                prev_time_image, next_time_image]).mosaic())
            prev_value_mosaic = ee.Image(ee.ImageCollection.fromImages([
                next_value_image, prev_value_image]).mosaic())
            next_value_mosaic = ee.Image(ee.ImageCollection.fromImages([
                prev_value_image, next_value_image]).mosaic())

            # Calculate time ratio of the current image between other cloud free images
            time_ratio_image = time_image.subtract(prev_time_mosaic) \
                .divide(next_time_mosaic.subtract(prev_time_mosaic))

            # Interpolate values to the current image time
            interp_value_image = next_value_mosaic.subtract(prev_value_mosaic) \
                .multiply(time_ratio_image).add(prev_value_mosaic)

            # CGM
            # Should/can the target image be mapped to the interpolated image?
            # Is there a clean way of computing ET here?
            return interp_value_image \
                .addBands(target_image) \
                .set({
                    'system:index': image.get('system:index'),
                    'system:time_start': image.get('system:time_start'),
                    # 'system:time_start': utc0_time,
            })

        interp_coll = ee.ImageCollection(target_coll.map(_linear))
    # elif interp_method.lower() == 'nearest':
    #     interp_coll = ee.ImageCollection(target_coll.map(_nearest))
    else:
        raise ValueError('invalid interpolation method: {}'.format(interp_method))

    return interp_coll