r"""
Transfering cold features

CommandLine:

    DATA_DVC_DPATH=$(geowatch_dvc --tags=phase2_data --hardware="auto")
    EXPT_DVC_DPATH=$(geowatch_dvc --tags=phase2_expt --hardware="auto")
    python -m geowatch.tasks.cold.transfer_features \
        --coco_fpath="$DATA_DVC_DPATH/Drop6/imgonly_KR_R001_cold-HTR.kwcoco.zip" \
        --combine_fpath="$DATA_DVC_DPATH/Drop6-MeanYear10GSD-V2/imgonly-KR_R001.kwcoco.zip" \
        --new_coco_fpath="$DATA_DVC_DPATH/Drop6-MeanYear10GSD-V2/imganns-KR_R001_uconn_cold.kwcoco.zip"

"""
import os
import ubelt as ub
import kwcoco
import kwimage
from os.path import join
from kwutil import util_time
import scriptconfig as scfg

try:
    from line_profiler import profile
except ImportError:
    profile = ub.identity


class TransferCocoConfig(scfg.DataConfig):
    """
    Transfer channels to one kwcoco file to the nearest image in the future.
    """

    src_kwcoco = scfg.Value(None, position=1, help=ub.paragraph(
        '''
        a path to a file to input kwcoco file (to predict on)
        '''), alias=['coco_fpath'])
    dst_kwcoco = scfg.Value(None, help=ub.paragraph(
        '''
        a path to a file to combined input kwcoco file (to merge with)
        '''), alias=['combine_fpath'])
    new_coco_fpath = scfg.Value(None, help='file path for modified output coco json')
    channels_to_transfer = scfg.Value(None, help='COLD channels for transfer')

    io_workers = scfg.Value(0, help='number of workers for copy-asset jobs')

    copy_assets = scfg.Value(False, help='if True copy the assests to the new bundle directory')

    respect_sensors = scfg.Value(
        True, help='if True only transfer features to images that share a sensor')

    # TODO: propogate strategy?
    max_propogate = scfg.Value(1, help='maximum number of future images a src image can transfer onto.')

    allow_affine_approx = scfg.Value(False, isflag=True, help=ub.paragraph(
        '''
        if True allow a transfer between different CRSs by approximating an affine transform. This should only be used for quick-and-dirty analysis, and never in production
        '''))


@profile
def transfer_features_main(cmdline=1, **kwargs):
    """
    Args:
        cmdline (int, optional): _description_. Defaults to 1.

    Ignore:
        python -m geowatch.tasks.cold.transfer_features --help
        TEST_COLD=1 xdoctest -m geowatch.tasks.cold.transfer_features transfer_features_main

     Example:
        >>> # xdoctest: +REQUIRES(env:TEST_COLD)
        >>> from geowatch.tasks.cold.transfer_features import transfer_features_main
        >>> from geowatch.tasks.cold.transfer_features import *
        >>> kwargs= dict(
        >>>   coco_fpath = ub.Path('/gpfs/scratchfs1/zhz18039/jws18003/new-repos/smart_data_dvc2/Drop6/imgonly_KR_R001_cold-V2.kwcoco.zip'),
        >>>   combine_fpath = ub.Path('/gpfs/scratchfs1/zhz18039/jws18003/new-repos/smart_data_dvc2/Drop6-MeanYear10GSD-V2/imgonly-KR_R001.kwcoco.zip'),
        >>>   new_coco_fpath = ub.Path('/gpfs/scratchfs1/zhz18039/jws18003/new-repos/smart_data_dvc2/Drop6-MeanYear10GSD-V2/imganns-KR_R001_uconn_cold_test.kwcoco.zip'),
        >>>   #workermode = 'process',
        >>> )
        >>> cmdline=0
        >>> transfer_features_main(cmdline, **kwargs)

    Example:
        >>> from geowatch.tasks.cold.transfer_features import transfer_features_main
        >>> from geowatch.tasks.cold.transfer_features import *
        >>> import geowatch
        >>> dset1 = geowatch.coerce_kwcoco('geowatch-msi', geodata=True, heatmap=True, dates=True)
        >>> dset2 = dset1.copy()
        >>> # Remove saliency assets from dset2
        >>> for img in dset2.images().coco_images:
        >>>     asset = img.find_asset_obj('salient')
        >>>     img['auxiliary'].remove(asset)
        >>> dset2_orig = dset2.copy()
        >>> # Transfer the saliency from dset1 onto dset2
        >>> kwargs = TransferCocoConfig(**{
        >>>     'src_kwcoco': dset1,
        >>>     'dst_kwcoco': dset2,
        >>>     'new_coco_fpath': 'return',
        >>>     'channels_to_transfer': 'salient',
        >>>     'copy_assets': False,
        >>> })
        >>> cmdline = False
        >>> new_dset = transfer_features_main(cmdline, **kwargs)
        >>> assert new_dset is dset2, 'modifies combine_fpath inplace'
        >>> from geowatch.utils import kwcoco_extensions
        >>> stats1 = kwcoco_extensions.coco_channel_stats(dset1)
        >>> stats2 = kwcoco_extensions.coco_channel_stats(dset2)
        >>> stats2_orig = kwcoco_extensions.coco_channel_stats(dset2_orig)
        >>> assert stats2['single_chan_hist']['salient'] == stats1['single_chan_hist']['salient'], 'all assets should transfer'
        >>> assert stats2['single_chan_hist']['salient'] == dset2.n_images, 'all dst images should get saliency here'
        >>> assert stats2_orig['single_chan_hist']['salient'] == 0
    """
    #NOTE: This script doesn't consider timestamp = True
    config = TransferCocoConfig.cli(cmdline=cmdline, data=kwargs, strict=True)
    import rich
    rich.print('config = {}'.format(ub.urepr(config, nl=1)))

    from geowatch.cli.reproject_annotations import keyframe_interpolate
    from geowatch.utils import process_context
    from kwutil import util_json

    resolved_config = config.to_dict()
    resolved_config = util_json.ensure_json_serializable(resolved_config)
    proc_context = process_context.ProcessContext(
        name='geowatch.tasks.cold.transfer_features',
        type='process',
        config=resolved_config,
    )
    proc_context.start()

    # Assign variables
    default_channels = [
        'blue_COLD_cv', 'green_COLD_cv', 'red_COLD_cv', 'nir_COLD_cv',
        'swir16_COLD_cv', 'swir22_COLD_cv', 'blue_COLD_a0', 'green_COLD_a0',
        'red_COLD_a0', 'nir_COLD_a0', 'swir16_COLD_a0', 'swir22_COLD_a0',
        'blue_COLD_a1', 'green_COLD_a1', 'red_COLD_a1', 'nir_COLD_a1',
        'swir16_COLD_a1', 'swir22_COLD_a1', 'blue_COLD_b1', 'green_COLD_b1',
        'red_COLD_b1', 'nir_COLD_b1', 'swir16_COLD_b1', 'swir22_COLD_b1',
        'blue_COLD_c1', 'green_COLD_c1', 'red_COLD_c1', 'nir_COLD_c1',
        'swir16_COLD_c1', 'swir22_COLD_c1', 'blue_COLD_rmse',
        'green_COLD_rmse', 'red_COLD_rmse', 'nir_COLD_rmse',
        'swir16_COLD_rmse', 'swir22_COLD_rmse'
    ]

    do_return = (config['new_coco_fpath'] == 'return')
    if not do_return:
        new_coco_fpath = ub.Path(config['new_coco_fpath'])

    if config['channels_to_transfer'] is None:
        channels_to_transfer = kwcoco.FusedChannelSpec.coerce(default_channels)
    else:
        channels_to_transfer = kwcoco.FusedChannelSpec.coerce(config['channels_to_transfer'])

    print('Loading source kwcoco file')
    src = kwcoco.CocoDataset.coerce(config['src_kwcoco'])

    print('Loading destination kwcoco file')
    dst = kwcoco.CocoDataset.coerce(config['dst_kwcoco'])

    print(f'src={src}')
    print(f'dst={dst}')

    if not do_return:
        _update_coco_fpath(dst, new_coco_fpath)

    src_video_names = src.videos().lookup('name')

    dst_vidnames = sorted(set(dst.index.name_to_video))
    src_vidnames = sorted(set(src.index.name_to_video))
    if src_vidnames != dst_vidnames:
        # If video names do not agree, we need to check for overlaps
        from geowatch.utils import kwcoco_extensions
        from kwgis.utils import util_gis
        src_vid_gdf = kwcoco_extensions.covered_video_geo_regions(src)
        dst_vid_gdf = kwcoco_extensions.covered_video_geo_regions(dst)
        dst_to_src_idxs = util_gis.geopandas_pairwise_overlaps(dst_vid_gdf, src_vid_gdf)
        # For each site, chose a single video assign to Note: this only works
        # well when the dst are smaller than the src, which is the case it is
        # being written for. If larger dst are needed the logic will need to
        # change.
        vidname_pairs = []
        for dst_idx, src_idxs in dst_to_src_idxs.items():
            if len(src_idxs) == 0:
                continue
            elif len(src_idxs) == 1:
                idx = 0
            else:
                qshape = dst_vid_gdf.iloc[dst_idx]['geometry']
                candidates = src_vid_gdf.iloc[src_idxs]
                overlaps = []
                for dshape in candidates['geometry']:
                    iarea = qshape.intersection(dshape).area
                    uarea = qshape.area
                    iofa = iarea / uarea
                    overlaps.append(iofa)
                idx = ub.argmax(overlaps)

            dst_vidname = dst_vid_gdf.iloc[dst_idx].video_name
            src_vidname = src_vid_gdf.iloc[src_idxs[idx]].video_name
            vidname_pairs.append((src_vidname, dst_vidname))
    else:
        vidname_pairs = list(zip(src_video_names, dst_vidnames))
        ...

    # We will build a list containing all of the assignments to make
    assignments = []
    for src_vidname, dst_vidname in vidname_pairs:
        # For each corresponding video
        src_video = src.index.name_to_video[src_vidname]
        dst_video = dst.index.name_to_video[dst_vidname]
        # Look at each sequence of images
        all_src_images = src.images(video_id=src_video['id'])
        dst_images = dst.images(video_id=dst_video['id'])

        # Filter out the source images missing the channels we want to transfer
        keep_flags = [
            coco_img.channels.intersection(channels_to_transfer).numel() > 0
            for coco_img in all_src_images.coco_images
        ]
        src_images = all_src_images.compress(keep_flags)

        # Build a list of columns used as group-ids which will be used to
        # ensure src images only transfer to dst images with the same group-id.
        src_groupers = []
        dst_groupers = []

        if config.respect_sensors:
            # Use sensors in group-ids, so we only transfer between similar
            # sensors
            src_sensors = src_images.lookup('sensor_coarse')
            dst_sensors = dst_images.lookup('sensor_coarse')
            src_groupers.append(src_sensors)
            dst_groupers.append(dst_sensors)

        if src_groupers:
            src_groupids = zip(*src_groupers)
            dst_groupids = zip(*dst_groupers)
            groupid_to_src_gids = ub.group_items(src_images, src_groupids)
            groupid_to_dst_gids = ub.group_items(dst_images, dst_groupids)
        else:
            # No groupers were given, so any src can transfer to any dst.
            groupid_to_src_gids = {'__nogroup__': src_images}
            groupid_to_dst_gids = {'__nogroup__': dst_images}

        for groupid in groupid_to_src_gids.keys():
            src_image_ids = groupid_to_src_gids[groupid]
            dst_image_ids = groupid_to_dst_gids[groupid]

            src_subimgs = src.images(src_image_ids)
            dst_subimgs = dst.images(dst_image_ids)

            # Find the timestamps for each image in both sequences
            src_timestamps = [util_time.coerce_datetime(d) for d in src_subimgs.lookup('date_captured')]
            dst_timestamps = [util_time.coerce_datetime(d) for d in dst_subimgs.lookup('date_captured')]
            assert sorted(src_timestamps) == src_timestamps, 'data should be in order'
            assert sorted(dst_timestamps) == dst_timestamps, 'data should be in order'

            # We will use logic in reproject annotations to assign images with COLD
            # features to the nearest image in the future. In this case the
            # keyframes are the images with COLD features and the target sequence
            # are the images we are transfering onto.
            target_times = dst_timestamps

            key_infos = [{'time': d, 'applies': 'future'} for d in src_timestamps]
            assigned_indexes = keyframe_interpolate(target_times, key_infos)
            # The above function returns a list for each key-frame with the
            # assigned indexes but we are only going to assign it to one of them,
            # so choose the earliest one in each group. The minimum index will be
            # the earliest because dates are sorted.
            for src_idx, dst_idxs in enumerate(assigned_indexes):
                if dst_idxs:
                    dst_idxs = sorted(dst_idxs)
                    chosen_dst_idxs = dst_idxs[:config.max_propogate]
                    for dst_idx in chosen_dst_idxs:
                        assignments.append({
                            'src_image_id': src_image_ids[src_idx],
                            'dst_image_id': dst_image_ids[dst_idx],
                            'src_vidname': src_vidname,
                            'dst_vidname': dst_vidname,
                        })

    print(f'Found {len(assignments)} image-to-image assignments to transfer')
    assets_to_transfer = []

    # Now we have image assignments, find the assets to transfer
    vidname_to_assignments = ub.group_items(assignments, lambda x: (x['src_vidname'], x['dst_vidname']))
    for vidnames, vid_assignments in vidname_to_assignments.items():
        src_vidname, dst_vidname = vidnames
        src_video = src.index.name_to_video[src_vidname]
        dst_video = dst.index.name_to_video[dst_vidname]

        if 0:
            # OVERSIMPLIFIED LOGIC
            # In most cases the source and destination videos will have the exact same
            # width / height, but in some cases they may differ if one is downsampled.
            # Just to be safe, assume the videos align and compute a scale factor
            # transform to account for this case (its just the identity if width/height
            # are the same).
            fx = dst_video['width'] / src_video['width']
            fy = dst_video['height'] / src_video['height']
            warp_dstvid_from_srcvid = kwimage.Affine.scale((fx, fy))
        else:
            # Better logic
            dst_crs = dst_video['wld_crs_info']
            src_crs = src_video['wld_crs_info']

            warp_src_from_wld = kwimage.Affine.coerce(src_video['warp_wld_to_vid'])
            warp_dst_from_wld = kwimage.Affine.coerce(dst_video['warp_wld_to_vid'])
            warp_wld_from_src = warp_src_from_wld.inv()

            warp_dstwld_from_srcwld = None

            if dst_crs != src_crs:
                msg = ub.codeblock(
                    f'''
                    Expected the same CRS but got:
                    dst_crs={dst_crs}
                    src_crs={src_crs}
                    ''')
                ALLOW_AFFINE_APPROXIMATE = config.allow_affine_approx
                if not ALLOW_AFFINE_APPROXIMATE:
                    raise AssertionError(msg)
                else:
                    rich.print('[yellow]WARNING: ' + msg)
                    rich.print('[yellow]WARNING: Estimating an approximate affine transform')
                    # We can construct a non-affine transform between the two CRS
                    # values, but because we are only writing metadata and not
                    # rewriting the pixel values, we cant use it. (kwcoco metadata
                    # only allows for affine transforms).
                    import pyproj
                    import numpy as np
                    # crs1 = pyproj.CRS.from_epsg(32639)
                    # crs2 = pyproj.CRS.from_epsg(32638)
                    crs1 = pyproj.CRS.from_authority(*src_crs['auth'])
                    crs2 = pyproj.CRS.from_authority(*dst_crs['auth'])
                    crs_tf = pyproj.Transformer.from_crs(crs_from=crs1, crs_to=crs2)

                    src_pxl_valid_region = kwimage.MultiPolygon.coerce(src_video['valid_region'])
                    src_wld_valid_region = src_pxl_valid_region.warp(warp_wld_from_src)
                    src_pts = np.concatenate([p.data['exterior'].data for p in src_wld_valid_region.data], axis=0)

                    # We now try be far too clever and estimate an affine
                    # approximation that gets does a good job in the region of
                    # the source image.
                    import numpy as np
                    xx1 = src_pts.T[0]
                    yy1 = src_pts.T[1]
                    xx2, yy2 = crs_tf.transform(xx1, yy1)
                    pts1 = np.stack([xx1, yy1], axis=1)
                    pts2 = np.stack([xx2, yy2], axis=1)
                    approx = kwimage.Affine.fit(pts1, pts2)
                    pts2_hat = kwimage.Points(xy=pts1).warp(approx)
                    error = np.abs(pts2_hat.xy - pts2)
                    max_error = error.max(axis=0)
                    ave_error = error.mean(axis=0)
                    print(f'ave_error={ave_error}')
                    print(f'max_error={max_error}')
                    warp_dstwld_from_srcwld = approx

            if warp_dstwld_from_srcwld is not None:
                # Using the hack
                warp_dstvid_from_srcvid = warp_dst_from_wld @ warp_dstwld_from_srcwld @ warp_wld_from_src
            else:
                warp_dstvid_from_srcvid = warp_dst_from_wld @ warp_wld_from_src

        for assignment in vid_assignments:
            src_image_id = assignment['src_image_id']
            dst_image_id = assignment['dst_image_id']
            src_coco_img = src.coco_image(src_image_id)
            dst_coco_img = dst.coco_image(dst_image_id)

            # Compute the alignment between assigned images.
            warp_srcvid_from_srcimg = src_coco_img.warp_vid_from_img
            warp_dstimg_from_dstvid = dst_coco_img.warp_img_from_vid
            warp_dstimg_from_srcimg = (
                warp_dstimg_from_dstvid @
                warp_dstvid_from_srcvid @
                warp_srcvid_from_srcimg
            )

            # For each asset in the src, check if we want to transfer it.
            img_assets_to_transfer = []
            for asset in src_coco_img.iter_asset_objs():
                asset_channels = kwcoco.FusedChannelSpec.coerce(asset['channels'])
                if asset_channels.intersection(channels_to_transfer):
                    img_assets_to_transfer.append(asset)

            for old_asset in img_assets_to_transfer:
                new_asset = old_asset.copy()
                # Ensure the filename points to the correct place (just use the
                # absolute path for simplicity for now)

                # Handle the case where the transform is not identity
                warp_srcimg_from_aux = kwimage.Affine.coerce(old_asset['warp_aux_to_img'])
                warp_dstimg_from_aux = warp_dstimg_from_srcimg @ warp_srcimg_from_aux
                new_asset['warp_aux_to_img'] = warp_dstimg_from_aux.concise()
                assets_to_transfer.append((dst_coco_img, old_asset, new_asset))

    print(f'Found {len(assets_to_transfer)} assets to tranfer')

    # Update the destination kwcoco file and prepare any copy jobs that need to
    # be done.
    src_bundle_dpath = src.bundle_dpath
    dst_bundle_dpath = dst.bundle_dpath
    copy_assets = config.copy_assets
    copy_tasks = []
    for dst_coco_img, src_asset, new_asset in assets_to_transfer:
        new_fname, copy_task = _make_new_asset_fname(
            src_asset, src_bundle_dpath, dst_bundle_dpath, copy_assets)
        if copy_task:
            copy_tasks.append(copy_task)

        new_asset['file_name'] = new_fname
        new_asset['image_id'] = dst_coco_img['id']
        dst_coco_img.add_asset(**new_asset)

    if not copy_assets:
        assert len(copy_tasks) == 0
        print('Copy assets is False. Only transfering reference to assets.')
    else:
        print(f'Found {len(copy_tasks)} assets to copy')

    rich.print(f'Dest Bundle: [link={dst.bundle_dpath}]{dst.bundle_dpath}[/link]')
    if copy_tasks:
        from kwutil import copy_manager
        copyman = copy_manager.CopyManager(workers=config.io_workers)
        for task in copy_tasks:
            copyman.submit(src=task['src'], dst=task['dst'],
                           overwrite=True)
        copyman.run(desc='Copy Assets')

    obj = proc_context.stop()
    dst.dataset['info'].append(obj)

    dst._ensure_json_serializable()
    if not do_return:
        print('Writing new coco file')
        dst.dump()
        print(f'Wrote transfered features to: {dst.fpath}')
    else:
        return dst


def _update_coco_fpath(self, new_fpath):
    # New method for more robustly updating the file path and bundle
    # directory, still a WIP
    if new_fpath is None:
        # Bundle directory is clobbered, so we should make everything
        # absolute
        self.reroot(absolute=True)
    else:
        old_fpath = self.fpath
        if old_fpath is not None:
            old_fpath_ = ub.Path(old_fpath)
            new_fpath_ = ub.Path(new_fpath)

            same_bundle = (
                (old_fpath_.parent == new_fpath_.parent) or
                (old_fpath_.resolve() == new_fpath_.resolve())
            )
            if not same_bundle:
                # The bundle directory has changed, so we need to reroot
                new_root = new_fpath_.parent
                self.reroot(new_root)

        self._fpath = new_fpath
        self._infer_dirs()


def _make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, copy_assets):
    """
    Find a new path for the transfered asset relative to the destination bundle.

    Args:
        src_asset (Dict): The asset dictionary we are transfering from src to dst
        src_bundle_dpath (str | PathLike): source bundle
        dst_bundle_dpath (str | PathLike): dest bundle
        copy_assets (bool): if True, build a new fname to copy to if possible

    Returns:
        Tuple[str, Dict | None]:
            the new fname and a dictionary containing the copy task
            if we need to perform one.

    TODO:
        port to kwcoco proper and move most of these tests to a unit test

    Example:
        >>> from geowatch.tasks.cold.transfer_features import _make_new_asset_fname

        >>> # Case: asset is absolute and inside src bundle
        >>> src_asset = {'file_name': '/my/src/rel/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('/my/src/rel/img.tif', None)
        ('rel/img.tif', {'src': '/my/src/rel/img.tif', 'dst': '/my/dst/rel/img.tif'})

        >>> # Case: asset is absolute and inside dst bundle
        >>> src_asset = {'file_name': '/my/dst/rel/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('rel/img.tif', None)
        ('rel/img.tif', None)

        >>> # Case: asset is absolute and inside neither bundle
        >>> src_asset = {'file_name': '/abs/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('/abs/img.tif', None)
        ('/abs/img.tif', None)

        >>> # Case: asset is relative and inside src bundle
        >>> src_asset = {'file_name': 'rel/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('/my/src/rel/img.tif', None)
        ('rel/img.tif', {'src': '/my/src/rel/img.tif', 'dst': '/my/dst/rel/img.tif'})

        >>> # Case: asset is relative and inside src bundle but with ..
        >>> src_asset = {'file_name': '../src/rel/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('/my/src/../src/rel/img.tif', None)
        ('rel/img.tif', {'src': '/my/src/../src/rel/img.tif', 'dst': '/my/dst/rel/img.tif'})

        >>> # Case: asset is relative and inside dst bundle
        >>> src_asset = {'file_name': '../dst/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('img.tif', None)
        ('img.tif', None)

        >>> # Case: asset is relative and inside neither bundle
        >>> src_asset = {'file_name': '../neither/img.tif'}
        >>> src_bundle_dpath = '/my/src'
        >>> dst_bundle_dpath = '/my/dst'
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, False))
        >>> print(_make_new_asset_fname(src_asset, src_bundle_dpath, dst_bundle_dpath, True))
        ('/my/src/../neither/img.tif', None)
        ('/my/neither/img.tif', None)
    """
    import warnings
    old_asset_fpath = join(src_bundle_dpath, src_asset['file_name'])
    dst_rel_old_asset_fpath = os.path.relpath(old_asset_fpath, dst_bundle_dpath)
    old_points_outside_dst_bundle = ('..' in ub.Path(dst_rel_old_asset_fpath).parts)

    copy_task = None

    if copy_assets:
        src_rel_old_asset_fpath = os.path.relpath(old_asset_fpath, src_bundle_dpath)
        old_points_outside_src_bundle = ('..' in ub.Path(src_rel_old_asset_fpath).parts)
        if not old_points_outside_dst_bundle:
            warnings.warn('An asset to copy already points inside of the dst bundle, not copying')
            new_fname = dst_rel_old_asset_fpath
        elif old_points_outside_src_bundle:
            # Dont want to deal with the corner case of figuring out a
            # good internal-to-dst-bundle location for an asset that
            # doesnt have a defined internal-to-src-bundle location.
            warnings.warn('An asset to copy points outside of the src bundle, not copying')
            new_fname = os.path.normpath(old_asset_fpath)
        else:
            # This case is safer to copy
            new_fname = src_rel_old_asset_fpath
            copy_task = {
                'src': old_asset_fpath,
                'dst': join(dst_bundle_dpath, new_fname),
            }
    else:
        if old_points_outside_dst_bundle:
            # The asset is outside the dst bundle, so we need to point at
            # the absolute path to it.
            new_fname = old_asset_fpath
        else:
            # Not copying, but safe to use relative paths because the asset
            # is already inside the destination bundle.
            new_fname = dst_rel_old_asset_fpath

    return new_fname, copy_task


def _test_cases():
    """
    Embedding test cases as doctests for now

    Example:
        >>> # Test case: transfer from temporal-lores to temporal-hires
        >>> from geowatch.tasks.cold.transfer_features import transfer_features_main
        >>> from geowatch.tasks.cold.transfer_features import *
        >>> import geowatch
        >>> dset1 = geowatch.coerce_kwcoco('geowatch-msi', geodata=True, heatmap=True, dates=True)
        >>> dset2 = dset1.copy()
        >>> # Remove half of the images from dset1
        >>> dset1.remove_images(list(dset1.images())[::2])
        >>> # Remove saliency assets from dset2
        >>> for img in dset2.images().coco_images:
        >>>     asset = img.find_asset_obj('salient')
        >>>     img['auxiliary'].remove(asset)
        >>> dset2_orig = dset2.copy()
        >>> # Transfer the saliency from dset1 onto dset2
        >>> kwargs = TransferCocoConfig(**{
        >>>     'src_kwcoco': dset1,
        >>>     'dst_kwcoco': dset2,
        >>>     'new_coco_fpath': 'return',
        >>>     'channels_to_transfer': 'salient',
        >>>     'copy_assets': False,
        >>> })
        >>> cmdline = False
        >>> new_dset = transfer_features_main(cmdline, **kwargs)
        >>> assert new_dset is dset2, 'modifies combine_fpath inplace'
        >>> from geowatch.utils import kwcoco_extensions
        >>> stats1 = kwcoco_extensions.coco_channel_stats(dset1)
        >>> stats2 = kwcoco_extensions.coco_channel_stats(dset2)
        >>> stats2_orig = kwcoco_extensions.coco_channel_stats(dset2_orig)
        >>> assert stats2['single_chan_hist']['salient'] == stats1['single_chan_hist']['salient'], 'all assets should transfer'
        >>> assert stats2['single_chan_hist']['salient'] == dset2.n_images // 2, 'only some dst images get saliency'
        >>> assert stats2_orig['single_chan_hist']['salient'] == 0

    Example:
        >>> # Test case: transfer from temporal-lores to temporal-hires multiple transfer
        >>> from geowatch.tasks.cold.transfer_features import transfer_features_main
        >>> from geowatch.tasks.cold.transfer_features import *
        >>> import geowatch
        >>> dset1 = geowatch.coerce_kwcoco('geowatch-msi', geodata=True, heatmap=True, dates=True)
        >>> dset2 = dset1.copy()
        >>> # Remove half of the images from dset1
        >>> dset1.remove_images(list(dset1.images())[::2])
        >>> # Remove saliency assets from dset2
        >>> for img in dset2.images().coco_images:
        >>>     asset = img.find_asset_obj('salient')
        >>>     img['auxiliary'].remove(asset)
        >>> dset2_orig = dset2.copy()
        >>> # Transfer the saliency from dset1 onto dset2
        >>> kwargs = TransferCocoConfig(**{
        >>>     'src_kwcoco': dset1,
        >>>     'dst_kwcoco': dset2,
        >>>     'new_coco_fpath': 'return',
        >>>     'respect_sensors': False,
        >>>     'max_propogate': None,
        >>>     'channels_to_transfer': 'salient',
        >>>     'copy_assets': False,
        >>> })
        >>> cmdline = False
        >>> new_dset = transfer_features_main(cmdline, **kwargs)
        >>> assert new_dset is dset2, 'modifies combine_fpath inplace'
        >>> from geowatch.utils import kwcoco_extensions
        >>> stats1 = kwcoco_extensions.coco_channel_stats(dset1)
        >>> stats2 = kwcoco_extensions.coco_channel_stats(dset2)
        >>> stats2_orig = kwcoco_extensions.coco_channel_stats(dset2_orig)
        >>> assert stats2['single_chan_hist']['salient'] > stats1['single_chan_hist']['salient'], 'some assets should transfer multiple times'
        >>> assert stats2_orig['single_chan_hist']['salient'] == 0

    Example:
        >>> # Test case: transfer from temporal-hires to temporal-lores
        >>> from geowatch.tasks.cold.transfer_features import transfer_features_main
        >>> from geowatch.tasks.cold.transfer_features import *
        >>> import geowatch
        >>> dset1 = geowatch.coerce_kwcoco('geowatch-msi', geodata=True, heatmap=True, dates=True)
        >>> dset2 = dset1.copy()
        >>> # Remove half of the images from dset2
        >>> dset2.remove_images(list(dset1.images())[::2])
        >>> # Remove saliency assets from dset2
        >>> for img in dset2.images().coco_images:
        >>>     asset = img.find_asset_obj('salient')
        >>>     img['auxiliary'].remove(asset)
        >>> dset2_orig = dset2.copy()
        >>> # Transfer the saliency from dset1 onto dset2
        >>> kwargs = TransferCocoConfig(**{
        >>>     'src_kwcoco': dset1,
        >>>     'dst_kwcoco': dset2,
        >>>     'new_coco_fpath': 'return',
        >>>     'channels_to_transfer': 'salient',
        >>>     'copy_assets': False,
        >>> })
        >>> cmdline = False
        >>> new_dset = transfer_features_main(cmdline, **kwargs)
        >>> assert new_dset is dset2, 'modifies combine_fpath inplace'
        >>> from geowatch.utils import kwcoco_extensions
        >>> stats1 = kwcoco_extensions.coco_channel_stats(dset1)
        >>> stats2 = kwcoco_extensions.coco_channel_stats(dset2)
        >>> stats2_orig = kwcoco_extensions.coco_channel_stats(dset2_orig)
        >>> assert stats2['single_chan_hist']['salient'] < stats1['single_chan_hist']['salient'], 'more saliency in dset1 than dset2'
        >>> assert stats2['single_chan_hist']['salient'] == dset2.n_images, 'all dst images get saliency'
        >>> assert stats2_orig['single_chan_hist']['salient'] == 0
    """


if __name__ == '__main__':
    transfer_features_main()
