# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# Copyright The NiPreps Developers <nipreps@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# We support and encourage derived works from this project, please read
# about our expectations at
#
#     https://www.nipreps.org/community/licensing/
#
"""
Resampling workflows
++++++++++++++++++++

.. autofunction:: init_bold_surf_wf
.. autofunction:: init_bold_fsLR_resampling_wf
.. autofunction:: init_bold_grayords_wf
.. autofunction:: init_goodvoxels_bold_mask_wf

"""

from __future__ import annotations

import typing as ty

from nipype import Function
from nipype.interfaces import freesurfer as fs
from nipype.interfaces import fsl
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe
from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms
from niworkflows.interfaces.freesurfer import MedialNaNs
from niworkflows.interfaces.workbench import MetricDilate, MetricMask, MetricResample

from nibabies import config
from nibabies.config import DEFAULT_DISMISS_ENTITIES, DEFAULT_MEMORY_MIN_GB
from nibabies.interfaces.bids import BIDSURI
from nibabies.workflows.bold.outputs import prepare_timing_parameters


def init_bold_surf_wf(
    *,
    mem_gb: float,
    surface_spaces: list[str],
    medial_surface_nan: bool,
    metadata: dict,
    output_dir: str,
    name: str = 'bold_surf_wf',
):
    """
    Sample functional images to FreeSurfer surfaces.

    For each vertex, the cortical ribbon is sampled at six points (spaced 20% of thickness apart)
    and averaged.

    Outputs are in GIFTI format.

    Workflow Graph
        .. workflow::
            :graph2use: colored
            :simple_form: yes

            from nibabies.workflows.bold import init_bold_surf_wf
            wf = init_bold_surf_wf(mem_gb=0.1,
                                   surface_spaces=["fsnative", "fsaverage5"],
                                   medial_surface_nan=False,
                                   metadata={},
                                   output_dir='.',
                                   )

    Parameters
    ----------
    surface_spaces : :obj:`list`
        List of FreeSurfer surface-spaces (either ``fsaverage{3,4,5,6,}`` or ``fsnative``)
        the functional images are to be resampled to.
        For ``fsnative``, images will be resampled to the individual subject's
        native surface.
    medial_surface_nan : :obj:`bool`
        Replace medial wall values with NaNs on functional GIFTI files

    Inputs
    ------
    source_file
        Original BOLD series
    sources
        List of files used to create the output files.
    bold_anat
        Motion-corrected BOLD series in T1 space
    subjects_dir
        FreeSurfer SUBJECTS_DIR
    subject_id
        FreeSurfer subject ID
    fsnative2anat_xfm
        ITK-style affine matrix translating from FreeSurfer-conformed subject space to anatomical

    Outputs
    -------
    surfaces
        BOLD series, resampled to FreeSurfer surfaces

    """
    from nipype.interfaces.io import FreeSurferSource
    from niworkflows.engine.workflows import LiterateWorkflow as Workflow
    from niworkflows.interfaces.nitransforms import ConcatenateXFMs
    from niworkflows.interfaces.surf import GiftiSetAnatomicalStructure

    from nibabies.interfaces import DerivativesDataSink

    timing_parameters = prepare_timing_parameters(metadata)

    workflow = Workflow(name=name)
    workflow.__desc__ = """\
The BOLD time-series were resampled onto the following surfaces
(FreeSurfer reconstruction nomenclature):
{out_spaces}.
""".format(out_spaces=', '.join([f'*{s}*' for s in surface_spaces]))

    inputnode = pe.Node(
        niu.IdentityInterface(
            fields=[
                'source_file',
                'sources',
                'bold_anat',
                'subject_id',
                'subjects_dir',
                'fsnative2anat_xfm',
            ]
        ),
        name='inputnode',
    )
    itersource = pe.Node(niu.IdentityInterface(fields=['target']), name='itersource')
    itersource.iterables = [('target', surface_spaces)]

    surfs_sources = pe.Node(
        BIDSURI(
            numinputs=1,
            dataset_links=config.execution.dataset_links,
            out_dir=output_dir,
        ),
        name='surfs_sources',
    )

    get_fsnative = pe.Node(FreeSurferSource(), name='get_fsnative', run_without_submitting=True)

    def select_target(subject_id, space):
        """Get the target subject ID, given a source subject ID and a target space."""
        return subject_id if space == 'fsnative' else space

    targets = pe.Node(
        niu.Function(function=select_target),
        name='targets',
        run_without_submitting=True,
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    itk2lta = pe.Node(
        ConcatenateXFMs(out_fmt='fs', inverse=True), name='itk2lta', run_without_submitting=True
    )
    sampler = pe.MapNode(
        fs.SampleToSurface(
            interp_method='trilinear',
            out_type='gii',
            override_reg_subj=True,
            sampling_method='average',
            sampling_range=(0, 1, 0.2),
            sampling_units='frac',
        ),
        iterfield=['hemi'],
        name='sampler',
        mem_gb=mem_gb * 3,
    )
    sampler.inputs.hemi = ['lh', 'rh']

    update_metadata = pe.MapNode(
        GiftiSetAnatomicalStructure(),
        iterfield=['in_file'],
        name='update_metadata',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    ds_bold_surfs = pe.MapNode(
        DerivativesDataSink(
            base_directory=output_dir,
            extension='.func.gii',
            dismiss_entities=DEFAULT_DISMISS_ENTITIES,
            TaskName=metadata.get('TaskName'),
            **timing_parameters,
        ),
        iterfield=['in_file', 'hemi'],
        name='ds_bold_surfs',
        run_without_submitting=True,
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )
    ds_bold_surfs.inputs.hemi = ['L', 'R']

    workflow.connect([
        (inputnode, get_fsnative, [
            ('subject_id', 'subject_id'),
            ('subjects_dir', 'subjects_dir')
        ]),
        (inputnode, targets, [('subject_id', 'subject_id')]),
        (inputnode, itk2lta, [
            ('bold_anat', 'moving'),
            ('fsnative2anat_xfm', 'in_xfms'),
        ]),
        # Use brain > T1 to ensure all recon methods have it
        (get_fsnative, itk2lta, [('brain', 'reference')]),
        (inputnode, sampler, [
            ('subjects_dir', 'subjects_dir'),
            ('subject_id', 'subject_id'),
            ('bold_anat', 'source_file'),
        ]),
        (itersource, targets, [('target', 'space')]),
        (itk2lta, sampler, [('out_inv', 'reg_file')]),
        (targets, sampler, [('out', 'target_subject')]),
        (inputnode, ds_bold_surfs, [('source_file', 'source_file')]),
        (inputnode, surfs_sources, [('sources', 'in1')]),
        (surfs_sources, ds_bold_surfs, [('out', 'Sources')]),
        (itersource, ds_bold_surfs, [('target', 'space')]),
        (update_metadata, ds_bold_surfs, [('out_file', 'in_file')]),
    ])  # fmt:skip

    # Refine if medial vertices should be NaNs
    medial_nans = pe.MapNode(
        MedialNaNs(), iterfield=['in_file'], name='medial_nans', mem_gb=DEFAULT_MEMORY_MIN_GB
    )

    if medial_surface_nan:
        workflow.connect([
            (inputnode, medial_nans, [('subjects_dir', 'subjects_dir')]),
            (sampler, medial_nans, [('out_file', 'in_file')]),
            (medial_nans, update_metadata, [('out_file', 'in_file')]),
        ])  # fmt:skip
    else:
        workflow.connect([(sampler, update_metadata, [('out_file', 'in_file')])])

    return workflow


def init_goodvoxels_bold_mask_wf(mem_gb: float, name: str = 'goodvoxels_bold_mask_wf'):
    """Calculate a mask of a BOLD series excluding high variance voxels.

    Workflow Graph
        .. workflow::
            :graph2use: colored
            :simple_form: yes

            from nibabies.workflows.bold.resampling import init_goodvoxels_bold_mask_wf
            wf = init_goodvoxels_bold_mask_wf(mem_gb=0.1)

    Parameters
    ----------
    mem_gb : :obj:`float`
        Size of BOLD file in GB
    name : :obj:`str`
        Name of workflow (default: ``goodvoxels_bold_mask_wf``)

    Inputs
    ------
    anat_ribbon
        Cortical ribbon in anatomical space
    bold_file
        Motion-corrected BOLD series in anatomical space

    Outputs
    -------
    masked_bold
        BOLD series after masking outlier voxels with locally high COV
    goodvoxels_ribbon
        Cortical ribbon mask excluding voxels with locally high COV
    """
    workflow = pe.Workflow(name=name)

    inputnode = pe.Node(
        niu.IdentityInterface(
            fields=[
                'anat_ribbon',
                'bold_file',
            ]
        ),
        name='inputnode',
    )
    outputnode = pe.Node(
        niu.IdentityInterface(
            fields=[
                'goodvoxels_mask',
                'goodvoxels_ribbon',
            ]
        ),
        name='outputnode',
    )
    ribbon_boldsrc_xfm = pe.Node(
        ApplyTransforms(interpolation='MultiLabel', transforms='identity'),
        name='ribbon_boldsrc_xfm',
        mem_gb=mem_gb,
    )

    stdev_volume = pe.Node(
        fsl.maths.StdImage(dimension='T'),
        name='stdev_volume',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    mean_volume = pe.Node(
        fsl.maths.MeanImage(dimension='T'),
        name='mean_volume',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_volume = pe.Node(
        fsl.maths.BinaryMaths(operation='div'),
        name='cov_volume',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_ribbon = pe.Node(
        fsl.ApplyMask(),
        name='cov_ribbon',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_ribbon_mean = pe.Node(
        fsl.ImageStats(op_string='-M'),
        name='cov_ribbon_mean',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_ribbon_std = pe.Node(
        fsl.ImageStats(op_string='-S'),
        name='cov_ribbon_std',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_ribbon_norm = pe.Node(
        fsl.maths.BinaryMaths(operation='div'),
        name='cov_ribbon_norm',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    smooth_norm = pe.Node(
        fsl.maths.MathsCommand(args='-bin -s 5'),
        name='smooth_norm',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    merge_smooth_norm = pe.Node(
        niu.Merge(1),
        name='merge_smooth_norm',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
        run_without_submitting=True,
    )

    cov_ribbon_norm_smooth = pe.Node(
        fsl.maths.MultiImageMaths(op_string='-s 5 -div %s -dilD'),
        name='cov_ribbon_norm_smooth',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_norm = pe.Node(
        fsl.maths.BinaryMaths(operation='div'),
        name='cov_norm',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_norm_modulate = pe.Node(
        fsl.maths.BinaryMaths(operation='div'),
        name='cov_norm_modulate',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    cov_norm_modulate_ribbon = pe.Node(
        fsl.ApplyMask(),
        name='cov_norm_modulate_ribbon',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    def _calc_upper_thr(in_stats):
        return in_stats[0] + (in_stats[1] * 0.5)

    upper_thr_val = pe.Node(
        Function(
            input_names=['in_stats'], output_names=['upper_thresh'], function=_calc_upper_thr
        ),
        name='upper_thr_val',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    def _calc_lower_thr(in_stats):
        return in_stats[1] - (in_stats[0] * 0.5)

    lower_thr_val = pe.Node(
        Function(
            input_names=['in_stats'], output_names=['lower_thresh'], function=_calc_lower_thr
        ),
        name='lower_thr_val',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    mod_ribbon_mean = pe.Node(
        fsl.ImageStats(op_string='-M'),
        name='mod_ribbon_mean',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    mod_ribbon_std = pe.Node(
        fsl.ImageStats(op_string='-S'),
        name='mod_ribbon_std',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    merge_mod_ribbon_stats = pe.Node(
        niu.Merge(2),
        name='merge_mod_ribbon_stats',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
        run_without_submitting=True,
    )

    bin_mean_volume = pe.Node(
        fsl.maths.UnaryMaths(operation='bin'),
        name='bin_mean_volume',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    merge_goodvoxels_operands = pe.Node(
        niu.Merge(2),
        name='merge_goodvoxels_operands',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
        run_without_submitting=True,
    )

    goodvoxels_thr = pe.Node(
        fsl.maths.Threshold(),
        name='goodvoxels_thr',
        mem_gb=mem_gb,
    )

    goodvoxels_mask = pe.Node(
        fsl.maths.MultiImageMaths(op_string='-bin -sub %s -mul -1'),
        name='goodvoxels_mask',
        mem_gb=mem_gb,
    )

    # make HCP-style "goodvoxels" mask in anat space for filtering outlier voxels
    # in bold timeseries, based on modulated normalized covariance
    workflow.connect([
        (inputnode, ribbon_boldsrc_xfm, [('anat_ribbon', 'input_image')]),
        (inputnode, stdev_volume, [('bold_file', 'in_file')]),
        (inputnode, mean_volume, [('bold_file', 'in_file')]),
        (mean_volume, ribbon_boldsrc_xfm, [('out_file', 'reference_image')]),
        (stdev_volume, cov_volume, [('out_file', 'in_file')]),
        (mean_volume, cov_volume, [('out_file', 'operand_file')]),
        (cov_volume, cov_ribbon, [('out_file', 'in_file')]),
        (ribbon_boldsrc_xfm, cov_ribbon, [('output_image', 'mask_file')]),
        (cov_ribbon, cov_ribbon_mean, [('out_file', 'in_file')]),
        (cov_ribbon, cov_ribbon_std, [('out_file', 'in_file')]),
        (cov_ribbon, cov_ribbon_norm, [('out_file', 'in_file')]),
        (cov_ribbon_mean, cov_ribbon_norm, [('out_stat', 'operand_value')]),
        (cov_ribbon_norm, smooth_norm, [('out_file', 'in_file')]),
        (smooth_norm, merge_smooth_norm, [('out_file', 'in1')]),
        (cov_ribbon_norm, cov_ribbon_norm_smooth, [('out_file', 'in_file')]),
        (merge_smooth_norm, cov_ribbon_norm_smooth, [('out', 'operand_files')]),
        (cov_ribbon_mean, cov_norm, [('out_stat', 'operand_value')]),
        (cov_volume, cov_norm, [('out_file', 'in_file')]),
        (cov_norm, cov_norm_modulate, [('out_file', 'in_file')]),
        (cov_ribbon_norm_smooth, cov_norm_modulate, [('out_file', 'operand_file')]),
        (cov_norm_modulate, cov_norm_modulate_ribbon, [('out_file', 'in_file')]),
        (ribbon_boldsrc_xfm, cov_norm_modulate_ribbon, [('output_image', 'mask_file')]),
        (cov_norm_modulate_ribbon, mod_ribbon_mean, [('out_file', 'in_file')]),
        (cov_norm_modulate_ribbon, mod_ribbon_std, [('out_file', 'in_file')]),
        (mod_ribbon_mean, merge_mod_ribbon_stats, [('out_stat', 'in1')]),
        (mod_ribbon_std, merge_mod_ribbon_stats, [('out_stat', 'in2')]),
        (merge_mod_ribbon_stats, upper_thr_val, [('out', 'in_stats')]),
        (merge_mod_ribbon_stats, lower_thr_val, [('out', 'in_stats')]),
        (mean_volume, bin_mean_volume, [('out_file', 'in_file')]),
        (upper_thr_val, goodvoxels_thr, [('upper_thresh', 'thresh')]),
        (cov_norm_modulate, goodvoxels_thr, [('out_file', 'in_file')]),
        (bin_mean_volume, merge_goodvoxels_operands, [('out_file', 'in1')]),
        (goodvoxels_thr, goodvoxels_mask, [('out_file', 'in_file')]),
        (merge_goodvoxels_operands, goodvoxels_mask, [('out', 'operand_files')]),
    ])  # fmt:skip

    goodvoxels_ribbon_mask = pe.Node(
        fsl.ApplyMask(),
        name_source=['in_file'],
        keep_extension=True,
        name='goodvoxels_ribbon_mask',
        mem_gb=DEFAULT_MEMORY_MIN_GB,
    )

    # apply goodvoxels ribbon mask to bold
    workflow.connect([
        (goodvoxels_mask, goodvoxels_ribbon_mask, [('out_file', 'in_file')]),
        (ribbon_boldsrc_xfm, goodvoxels_ribbon_mask, [('output_image', 'mask_file')]),
        (goodvoxels_mask, outputnode, [('out_file', 'goodvoxels_mask')]),
        (goodvoxels_ribbon_mask, outputnode, [('out_file', 'goodvoxels_ribbon')]),
    ])  # fmt:skip

    return workflow


def init_bold_fsLR_resampling_wf(
    grayord_density: ty.Literal['91k', '170k'],
    omp_nthreads: int,
    mem_gb: float,
    name: str = 'bold_fsLR_resampling_wf',
):
    """Resample BOLD time series to fsLR surface.

    This workflow is derived heavily from three scripts within the DCAN-HCP pipelines scripts

    Line numbers correspond to the locations of the code in the original scripts, found at:
    https://github.com/DCAN-Labs/DCAN-HCP/tree/9291324/

    Workflow Graph
        .. workflow::
            :graph2use: colored
            :simple_form: yes

            from nibabies.workflows.bold.resampling import init_bold_fsLR_resampling_wf
            wf = init_bold_fsLR_resampling_wf(
                grayord_density='91k',
                omp_nthreads=1,
                mem_gb=1,
            )

    Parameters
    ----------
    grayord_density : :class:`str`
        Either ``"91k"`` or ``"170k"``, representing the total *grayordinates*.
    omp_nthreads : :class:`int`
        Maximum number of threads an individual process may use
    mem_gb : :class:`float`
        Size of BOLD file in GB
    name : :class:`str`
        Name of workflow (default: ``bold_fsLR_resampling_wf``)

    Inputs
    ------
    bold_file : :class:`str`
        Path to BOLD file resampled into anatomical space
    white : :class:`list` of :class:`str`
        Path to left and right hemisphere white matter GIFTI surfaces.
    pial : :class:`list` of :class:`str`
        Path to left and right hemisphere pial GIFTI surfaces.
    midthickness : :class:`list` of :class:`str`
        Path to left and right hemisphere midthickness GIFTI surfaces.
    midthickness_fsLR : :class:`list` of :class:`str`
        Path to left and right hemisphere midthickness GIFTI surfaces in fsLR space.
    sphere_reg_fsLR : :class:`list` of :class:`str`
        Path to left and right hemisphere sphere.reg GIFTI surfaces, mapping from subject to fsLR
    cortex_mask : :class:`list` of :class:`str`
        Path to left and right hemisphere cortical masks.
    volume_roi : :class:`str` or Undefined
        Pre-calculated goodvoxels mask. Not required.

    Outputs
    -------
    bold_fsLR : :class:`list` of :class:`str`
        Path to BOLD series resampled as functional GIFTI files in fsLR space

    """
    import smriprep
    import templateflow.api as tf
    from niworkflows.engine.workflows import LiterateWorkflow as Workflow
    from niworkflows.interfaces.utility import KeySelect
    from niworkflows.interfaces.workbench import VolumeToSurfaceMapping

    fslr_density = '32k' if grayord_density == '91k' else '59k'

    workflow = Workflow(name=name)

    workflow.__desc__ = """\
The BOLD time-series were resampled onto the left/right-symmetric template
"fsLR" using the Connectome Workbench [@hcppipelines].
"""

    inputnode = pe.Node(
        niu.IdentityInterface(
            fields=[
                'bold_file',
                'white',
                'pial',
                'midthickness',
                'midthickness_fsLR',
                'sphere_reg_fsLR',
                'cortex_mask',
                'volume_roi',
            ]
        ),
        name='inputnode',
    )

    hemisource = pe.Node(
        niu.IdentityInterface(fields=['hemi']),
        name='hemisource',
        iterables=[('hemi', ['L', 'R'])],
    )

    joinnode = pe.JoinNode(
        niu.IdentityInterface(fields=['bold_fsLR']),
        name='joinnode',
        joinsource='hemisource',
    )

    outputnode = pe.Node(
        niu.IdentityInterface(fields=['bold_fsLR']),
        name='outputnode',
    )

    # select white, midthickness and pial surfaces based on hemi
    select_surfaces = pe.Node(
        KeySelect(
            fields=[
                'white',
                'pial',
                'midthickness',
                'midthickness_fsLR',
                'sphere_reg_fsLR',
                'template_sphere',
                'cortex_mask',
                'template_roi',
            ],
            keys=['L', 'R'],
        ),
        name='select_surfaces',
        run_without_submitting=True,
    )
    select_surfaces.inputs.template_sphere = [
        str(sphere)
        for sphere in tf.get(
            template='fsLR',
            density=fslr_density,
            suffix='sphere',
            space=None,
            extension='.surf.gii',
        )
    ]
    atlases = smriprep.load_data('atlases')
    select_surfaces.inputs.template_roi = [
        str(atlases / f'L.atlasroi.{fslr_density}_fs_LR.shape.gii'),
        str(atlases / f'R.atlasroi.{fslr_density}_fs_LR.shape.gii'),
    ]

    # RibbonVolumeToSurfaceMapping.sh
    # Line 85 thru ...
    volume_to_surface = pe.Node(
        VolumeToSurfaceMapping(method='ribbon-constrained'),
        name='volume_to_surface',
        mem_gb=mem_gb * 3,
        n_procs=omp_nthreads,
    )
    metric_dilate = pe.Node(
        MetricDilate(distance=10, nearest=True),
        name='metric_dilate',
        mem_gb=1,
        n_procs=omp_nthreads,
    )
    mask_native = pe.Node(MetricMask(), name='mask_native')
    resample_to_fsLR = pe.Node(
        MetricResample(method='ADAP_BARY_AREA', area_surfs=True),
        name='resample_to_fsLR',
        mem_gb=1,
        n_procs=omp_nthreads,
    )
    # ... line 89
    mask_fsLR = pe.Node(MetricMask(), name='mask_fsLR')

    workflow.connect([
        (inputnode, select_surfaces, [
            ('white', 'white'),
            ('pial', 'pial'),
            ('midthickness', 'midthickness'),
            ('midthickness_fsLR', 'midthickness_fsLR'),
            ('sphere_reg_fsLR', 'sphere_reg_fsLR'),
            ('cortex_mask', 'cortex_mask'),
        ]),
        (hemisource, select_surfaces, [('hemi', 'key')]),
        # Resample BOLD to native surface, dilate and mask
        (inputnode, volume_to_surface, [
            ('bold_file', 'volume_file'),
            ('volume_roi', 'volume_roi'),
        ]),
        (select_surfaces, volume_to_surface, [
            ('midthickness', 'surface_file'),
            ('white', 'inner_surface'),
            ('pial', 'outer_surface'),
        ]),
        (select_surfaces, metric_dilate, [('midthickness', 'surf_file')]),
        (select_surfaces, mask_native, [('cortex_mask', 'mask')]),
        (volume_to_surface, metric_dilate, [('out_file', 'in_file')]),
        (metric_dilate, mask_native, [('out_file', 'in_file')]),
        # Resample BOLD to fsLR and mask
        (select_surfaces, resample_to_fsLR, [
            ('sphere_reg_fsLR', 'current_sphere'),
            ('template_sphere', 'new_sphere'),
            ('midthickness', 'current_area'),
            ('midthickness_fsLR', 'new_area'),
            ('cortex_mask', 'roi_metric'),
        ]),
        (mask_native, resample_to_fsLR, [('out_file', 'in_file')]),
        (select_surfaces, mask_fsLR, [('template_roi', 'mask')]),
        (resample_to_fsLR, mask_fsLR, [('out_file', 'in_file')]),
        # Output
        (mask_fsLR, joinnode, [('out_file', 'bold_fsLR')]),
        (joinnode, outputnode, [('bold_fsLR', 'bold_fsLR')]),
    ])  # fmt:skip

    return workflow


def init_bold_grayords_wf(
    grayord_density: ty.Literal['91k', '170k'],
    repetition_time: float,
    name: str = 'bold_grayords_wf',
):
    """
    Sample Grayordinates files onto the fsLR atlas.

    Outputs are in CIFTI2 format.

    Workflow Graph
        .. workflow::
            :graph2use: colored
            :simple_form: yes

            from nibabies.workflows.bold.resampling import init_bold_grayords_wf
            wf = init_bold_grayords_wf(grayord_density='91k')

    Parameters
    ----------
    grayord_density : :obj:`str`
        Either `91k` or `170k`, representing the total of vertices or *grayordinates*.
    mem_gb : :obj:`float`
        Size of BOLD file in GB
    name : :obj:`str`
        Unique name for the subworkflow (default: ``'bold_grayords_wf'``)

    Inputs
    ------
    bold_std : :obj:`str`
        The subcortical structures in MNI152NLin6Asym space.
    bold_labels : :obj:`str`
        Volume file containing all subcortical labels
    bold_fsLR : :obj:`list`
        List of BOLD files resampled on the fsaverage (ico7) surfaces

    Outputs
    -------
    dtseries : :obj:`str`
        BOLD CIFTI dense timeseries.
    dtseries_metadata : :obj:`str`
        BIDS metadata file corresponding to ``dtseries``.

    """
    import templateflow.api as tf
    from niworkflows.engine.workflows import LiterateWorkflow as Workflow

    from nibabies.interfaces.nibabel import ReorientImage
    from nibabies.interfaces.workbench import CiftiCreateDenseTimeseries

    workflow = Workflow(name=name)
    workflow.__desc__ = f"""\
*Grayordinates* files [@hcppipelines] containing {grayord_density} samples were also
generated using the highest-resolution ``fsaverage`` as intermediate standardized
surface space.
"""

    fslr_density = '32k' if grayord_density == '91k' else '59k'

    inputnode = pe.Node(
        niu.IdentityInterface(fields=['bold_std', 'bold_labels', 'bold_fsLR']),
        name='inputnode',
    )

    outputnode = pe.Node(
        niu.IdentityInterface(fields=['dtseries', 'dtseries_metadata']),
        name='outputnode',
    )

    split_surfaces = pe.Node(
        niu.Function(function=_split_surfaces, output_names=['left_surface', 'right_surface']),
        name='split_surfaces',
    )

    reorient_data = pe.Node(ReorientImage(target_orientation='LAS'), name='reorient_data')
    reorient_labels = reorient_data.clone(name='reorient_labels')

    gen_cifti = pe.Node(CiftiCreateDenseTimeseries(timestep=repetition_time), name='gen_cifti')
    gen_cifti.inputs.roi_left = tf.get(
        'fsLR',
        density=fslr_density,
        hemi='L',
        desc='nomedialwall',
        suffix='dparc',
    )
    gen_cifti.inputs.roi_right = tf.get(
        'fsLR',
        density=fslr_density,
        hemi='R',
        desc='nomedialwall',
        suffix='dparc',
    )
    gen_cifti_metadata = pe.Node(
        niu.Function(function=_gen_metadata, output_names=['out_metadata']),
        name='gen_cifti_metadata',
    )
    gen_cifti_metadata.inputs.grayord_density = grayord_density

    workflow.connect([
        (inputnode, reorient_data, [('bold_std', 'in_file')]),
        (inputnode, reorient_labels, [('bold_labels', 'in_file')]),
        (reorient_data, gen_cifti, [('out_file', 'volume_data')]),
        (reorient_labels, gen_cifti, [('out_file', 'volume_structure_labels')]),
        (inputnode, split_surfaces, [('bold_fsLR', 'in_surfaces')]),
        (split_surfaces, gen_cifti, [
            ('left_surface', 'left_metric'),
            ('right_surface', 'right_metric')]),
        (gen_cifti, outputnode, [('out_file', 'dtseries')]),
        (gen_cifti_metadata, outputnode, [('out_metadata', 'dtseries_metadata')]),
    ])  # fmt:skip
    return workflow


def _gen_metadata(grayord_density):
    import json
    from pathlib import Path

    from niworkflows.interfaces.cifti import _prepare_cifti

    _, _, metadata = _prepare_cifti(grayord_density)
    metadata_file = Path('bold.dtseries.json').absolute()
    metadata_file.write_text(json.dumps(metadata, indent=2))
    return str(metadata_file)


def _split_surfaces(in_surfaces: list) -> tuple[str, str]:
    """Split surfaces into selectable hemispheres."""
    return in_surfaces[0], in_surfaces[1]
