# -*- coding: utf-8 -*-

# PyNX - Python tools for Nano-structures Crystallography
#   (c) 2023-present : ESRF-European Synchrotron Radiation Facility
#       authors:
#         Vincent Favre-Nicolin, favre@esrf.fr

import argparse


class RawTextArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter,
                                           argparse.ArgumentDefaultsHelpFormatter):
    pass


# # Experimental so not visible:
#     crop=2: this keyword allows to crop the observed intensity array by a factor 2,
#         effectively dividing the object resolution by th same factor. Different values
#         can also be done for each axis using e.g. crop=122 (no crop along z, 2 along x&y)
#
#     algorithm setp:
#         crop2bin: this step, which must be combined with crop, allows to switch
#             from a cropped intensity array (low resolution object) to a binned
#             observed intensity (object at full resolution, but less oversampling)


class ActionROI(argparse.Action):
    """Argparse Action to check --roi is correctly used"""

    def __call__(self, parser_, namespace, values, option_string=None):
        if len(values) == 1:
            if values[0] not in ['auto', 'full', 'all']:
                raise argparse.ArgumentError(
                    self, "--roi: need either 'auto' (default), 'full'"
                          " or 4/6 separate numbers with the xmin xmax ymin ymax [zmin zmax]")
            else:
                setattr(namespace, self.dest, values[0])
        elif len(values) in [4, 6]:
            try:
                vv = [int(v) for v in values]
            except ValueError:
                raise argparse.ArgumentError(
                    self, f"--roi f{' '.join(values)}: error "
                          f"converting coordinates to integers")
            nx, ny = vv[1] - vv[0], vv[3] - vv[2]
            if nx <= 0 or ny <= 0 or (nx % 2 != 0) or (ny % 2 != 0):
                raise argparse.ArgumentError(
                    self, f"--roi f{' '.join(values)}: need an even "
                          f"and positive size along all dimensions (nx={nx}, ny={ny})")
            if len(vv) == 6:
                nz = vv[5] - vv[4]
                if nz <= 0 or (nz % 2 != 0):
                    raise argparse.ArgumentError(
                        self, f"--roi f{' '.join(values)}: need an even "
                              f"and positive size along all dimensions "
                              f"(nx={nx}, ny={ny}, nz={nz})")
            setattr(namespace, self.dest, vv)
        else:
            raise argparse.ArgumentError(
                self, "--roi: need either 'auto' (default), 'full' "
                      "or 4 separate numbers with the xmin xmax ymin ymax [zmin zmax]")


class ActionSupportPostExpand(argparse.Action):
    """Argparse Action to parse --support_post_expand"""

    def __call__(self, parser_, namespace, values, option_string=None):
        if len(values) == 1:
            try:
                vv = [int(v) for v in values[0].split(',')]
            except ValueError:
                raise argparse.ArgumentError(self, f"--support_post_expand="
                                                   f"f{values[0]}: error "
                                                   f"converting values to integers")
        else:
            try:
                vv = [int(v) for v in values]
            except ValueError:
                raise argparse.ArgumentError(self, f"--support_post_expand "
                                                   f"f{' '.join(values)}: error "
                                                   f"converting values to integers")
        setattr(namespace, self.dest, vv)


def make_parser(prog: str, description: str, epilog: str, default_params: dict):
    p = default_params
    parser = argparse.ArgumentParser(prog=prog,
                                     description=description,
                                     formatter_class=RawTextArgumentDefaultsHelpFormatter,
                                     epilog=epilog,
                                     conflict_handler='resolve')
    # ===================== Input parameters ==================
    grp = parser.add_argument_group("Input parameters")

    grp.add_argument('--data', type=str, default=None, required=True,
                     help="path to the data file, e.g. /some/dir/to/data."
                          "Recognized formats include .npy, .npz (if several arrays are included iobs, "
                          "should be named 'data' or 'iobs'), .tif or .tiff "
                          "(assumes a multiframe tiff image), or .cxi (hdf5). "
                          "An hdf5 file can be supplied with a path, e.g.:\n"
                          "   '--data path/to/data.h5:entry/path/data'\n"
                          "Multiple files can be processed by combining with scan, e.g.:\n\n"
                          "* --data my_data%%04d.cxi scan=23,24,25\n"
                          "* --data my_data%%s.cxi scan=room_temperature")

    grp.add_argument('--detector_distance', '--detectordistance', '--distance',
                     type=float, default=p['detector_distance'],
                     help='Detector distance in meters.')

    grp.add_argument("--pixel_size_detector", action='store', type=float,
                     default=p['pixel_size_detector'],
                     help="Detector pixel size (m)")

    grp.add_argument('--wavelength', type=float, default=None,
                     help='Experiment wavelength in meters.')

    # TODO: deprecate this argument, replaced by --roi auto
    grp.add_argument(
        '--auto_center_resize', '--auto-crop', '--autocrop',
        action='store_true', default=p['auto_center_resize'],
        dest='auto_center_resize',
        help='Automatically center and crop the input data')

    # grp.add_argument('--nrj', type=float, default=None,
    #                  help='X-ray energy in keV')
    grp.add_argument(
        '--roi', type=str, default=p['roi'], nargs='+', action=ActionROI,
        dest='roi_user',
        help='Region-of-interest to be used for the reconstruction. This requires '
             '4 (2D) or 6 (3D) values for xmin xmax ymin ymax [zmin zmax]. '
             'The area is taken with python conventions, i.e. pixels with indices xmin<= x < xmax and '
             'ymin<= y < ymax.'
             'The final shape must also have a suitable integer number '
             'for GPU FFT, i.e. it must be a multiple of 2 and the largest number in'
             'its prime factor decomposition must be less or equal to the largest value'
             'acceptable by vkFFT for a radix transform(<=13).'
             'If n does not fulfill these constraints,'
             'it will be reduced using the largest possible integer smaller than n.'
             'This option supersedes "--max_size" unless roi="auto".\n'
             'If this option is not used, the entire data array is used.'
             'Other possible values:\n'
             '- "auto": automatically selects the roi from the center of mass '
             '          and the maximum possible size. [default]')

    grp.add_argument(
        '--flatfield', '--flat_field', action='store', type=str,
        default=p['flatfield'],
        help="filename with a flatfield array by which the observed intensity "
             "will be multiplied. In order to preserve Poisson statistics, the "
             "average correction should be close to 1.\n"
             "Accepted file formats: .npy, .npz, .edf, .mat, .tif, .h5, .cxi.\n"
             "The first available array will be used if multiple are present.\n"
             "For an hdf5 file (h5 or cxi), the path can be supplied e.g. using: "
             "   '--flatfield path/to/flatfield.h5:entry/path/flat'\n"
             "if the hdf5 path is not given,"
             "the first array with a correct shape and with 'flat' in the path is used.\n"
             "If the flatfield is 2D and the observed intensity 3D, the flatfield is repeated"
             "along the depth (first/slowest axis).\n"
             "Note that if no flatfield is given and '--mask maxipix1' or 'maxipix6',"
             "a correction is automatically to the gap pixels (see --mask parameter)."
             "[default=None, no flatfield correction]")

    # TODO: handle properly multiple mask, comma-separated
    grp.add_argument(
        '--mask', action='store', type=str, nargs='+',
        default=p['mask'],  # choices=['zero', 'negative', 'maxipix', 'maxipix1', 'maxipix6'],
        help="Optional mask for the diffraction data. Choices:\n\n"
             "* ``--mask zero``: all pixels with iobs <= 0 will be masked\n"
             "* ``--mask negative``, all pixels with iobs < 0 will be masked\n"
             "* ``--mask maxipix``, the maxipix gaps will be masked\n"
             "* ``--mask maxipix1``, the maxipix gaps will be masked, "
             " except for the large pixel one ach side of the gap,"
             " for which the intensity will be divided by 3\n"
             "* ``--mask maxipix6``, the maxipix gaps will be linearly interpolated "
             "between the gap large pixels.\n\n"
             "NaN-valued pixels in the observed intensity are always masked.\n\n"
             "A filename can also be given (accepted formats: .npy, .npz, "
             ".edf, .mat, .tif, .h5, .cxi):\n"
             "(the first available array will be used if multiple are present) "
             "file. For an hdf5 file (.h5 or .cxi),"
             "the path can be supplied e.g. using data=path/to/mask.h5:entry/path/mask "
             "- if the hdf5 path is not given, the first array with a correct "
             "shape and with 'mask' in the path is used."
             "When an imported array defines the mask, this follows the CXI convention: "
             "pixels = 0 are valid, > 0 are masked. If the mask is 2D "
             "and the data 3D, the mask is repeated for all frames along the first dimension (depth)."
             "Several masks can be combined, separated by a comma, e.g. '--mask maxipix dead.npz'")

    grp.add_argument(
        '--free_pixel_mask', action='store', type=str,
        help="pixel mask for free log-likelihood calculation. By default the free pixel mask is "
             "randomly initialised, but this can be used to load a mask from a file for "
             "consistency between runs. It can be loaded from a .npy, .edf, .tif, .npz or "
             ".mat file - if .npz or .mat are used, the first array matching the iobs shape is used."
             "A .cxi filename can also be used as input, in which case it is assumed that this"
             "is the result of a previous optimisation, and that the mask can be loaded from"
             "'/entry_last/image_1/process_1/configuration/free_pixel_mask'")

    grp.add_argument('--iobs_saturation', type=float,
                     default=p['iobs_saturation'],
                     help="saturation value for the observed intensity. "
                          "Any pixel above this intensity will be masked")

    grp.add_argument(
        '--mask_interp', '--maskinterp', type=int, default=p['mask_interp'],
        nargs=2,
        help="interpolate masked pixels from surrounding pixels, using an inverse distance "
             "weighting. The first number N indicates that the pixels used for interpolation range "
             "from i-N to i+N for pixel i around all dimensions. The second number n that the weight "
             "is equal to 1/d**n for pixels with at a distance n. "
             "The interpolated values iobs_m are stored in memory as -1e19*(iobs_m+1) so that the "
             "algorithm knows these are not true observations, and are applied with a large "
             "confidence interval. See --confidence_interval_factor_mask")

    grp.add_argument(
        '--confidence_interval_factor_mask_min', type=float,
        default=p['confidence_interval_factor_mask_min'],
        help="When masked pixels are interpolated using mask_interp, the calculated "
             "values iobs_m are not observed and the amplitude projection is done for "
             "those with a confidence interval, equal to: iobs_m*confidence_interval_factor_mask "
             "Two values (min/max) must be given, normally around 1")

    grp.add_argument(
        '--confidence_interval_factor_mask_max', type=float,
        default=p['confidence_interval_factor_mask_max'],
        help="When masked pixels are interpolated using mask_interp, the calculated "
             "values iobs_m are not observed and the amplitude projection is done for "
             "those with a confidence interval, equal to: iobs_m*confidence_interval_factor_mask "
             "Two values (min/max) must be given, normally around 1")

    grp.add_argument(
        '--rebin', '--binning', type=int, nargs='+',
        default=p['rebin'],
        help="the experimental data can be rebinned (i.e. a group of n x n (x n) pixels is "
             "replaced by a single one whose intensity is equal to the sum of all the pixels). "
             "Both iobs and mask (if any) will be binned, but the support (if any) should "
             "correspond to the new size. The supplied pixel_size_detector should correspond "
             "to the original size. The rebin factor can also be supplied as one value per "
             "dimension, e.g. '--rebin 4 1 2'.\n"
             "Finally, instead of summing pixels with the given rebin size, it is possible "
             "to select a single sub-pixel by skipping instead of binning: "
             "e.g. using '--rebin 4 1 2 0 0 1', the extracted pixel will use "
             "array slicing data[0::4,0::1,1::2].")

    grp.add_argument(
        '--max_size', '--maxsize', type=int, default=p['max_size'],
        help="maximum size for the array used for analysis, along all dimensions. The data "
             "will be cropped to this value after binning and centering")

    grp = parser.add_argument_group("Display parameters")

    grp.add_argument('--verbose', type=int, default=p['verbose'],
                     help="print evolution of llk (and display plot if 'liveplot' is set) every N cycle")

    grp.add_argument('--live_plot', '--liveplot', action='store_true',
                     default=p['live_plot'], help='liveplot during optimisation')

    grp.add_argument('--plot_axis', '--plotaxis', type=int,
                     default=p['plot_axis'], choices=[0, 1, 2],
                     help='for 3D data, this option allows to choose the axis'
                          'of the 2D cut used for live_plot')

    grp.add_argument('--fig_num', '--fignum', type=int,
                     default=p['fig_num'], choices=[0, 1, 2],
                     help='Figure number for live plotting')

    # ===================== Compute parameters ==================
    grp = parser.add_argument_group("Compute parameters")

    grp.add_argument('--gpu', action='store', type=str, default=None,
                     help='string matching GPU name (or part of it, case-insensitive)')

    grp.add_argument('--mpi', action='store', type=str, default=p['mpi'],
                     choices=['scan', 'run'],
                     help="when launching the script using mpiexec, this tells the script to "
                          "either distribute the list of scans to different processes "
                          "(mpi=scan), or (mpi=run) split the runs to the "
                          "different processes. If nb_run_keep is used, "
                          "the results are merged before selecting the best results.")

    # ===================== Run parameters ==================
    grp = parser.add_argument_group("Run parameters")

    grp.add_argument('--nb_run', '--nbrun', type=int, default=p['nb_run'],
                     help="number of times to run the optimization")

    grp.add_argument('--nb_run_keep', '--nbrunkeep', type=int, default=p['nb_run_keep'],
                     help="number of best run results to keep, according to likelihood statistics. "
                          "This is only useful associated with --nb_run [default: keep all run results]")

    grp.add_argument('--nb_run_keep_max_obj2_out', type=float,
                     default=p['nb_run_keep_max_obj2_out'],
                     help="when performing multiple runs with nb_run_keep, if the fraction "
                          "of the object square modulus outside the object support is larger "
                          "than this value, the solution will be rejected regardless "
                          "of the free log-likelihood")

    # ===================== Output parameters ==================
    grp = parser.add_argument_group("Output parameters")

    grp.add_argument('--save', action='store', type=str, default=p['save'],
                     choices=['all', 'final', 'none'],
                     help="Option to save either only the final result (the default), "
                          "after each algorithm step (comma-separated), or never.\n"
                          "Note that the name of the save file including the LLK ank LLK_free "
                          "values will be decided by the first step and not the final one"
                          "when using '--save all'")

    grp.add_argument('--save_plot', '--saveplot', action='store_true',
                     help="Use this option to save an image of the object- this will "
                          "show three perpendicular cuts for 3D objects.")

    grp.add_argument('--data2cxi', action='store', default=False, const=True,
                     nargs='?', choices=['crop'],
                     help="Option to save the raw data in CXI format (http://cxidb.org/cxi.html), "
                          "with all the required information for a CDI experiment "
                          "if '--data2cxi crop' is used, the data will be saved "
                          "after centering and cropping (default is to save "
                          "the raw data).")

    grp.add_argument('--output_format', action='store', type=str, default=p['output_format'],
                     choices=['cxi', 'npz', 'none'],
                     help="output format for the final object and support. "
                          "'none' can be selected for testing and simulation")

    grp.add_argument('--note', action='store', type=str, default=None,
                     help="Optional text which will be saved as a note in the"
                          "output CXI file, e.g.:\n"
                          "  --note 'This dataset was measured... "
                          "Citation: Journal of coherent imaging (2018), 729...'")

    grp.add_argument('--instrument', action='store', type=str, default=p['instrument'],
                     help="Name of the beamline/instrument used for data collection")

    grp.add_argument('--sample_name', '--sample', action='store', type=str,
                     default=p['sample_name'],
                     help="Sample name")

    grp.add_argument(
        '--crop_output', type=int,
        default=p['crop_output'],
        help="if >0 (default:4), the output data will be cropped around the final "
             "support plus 'crop_output' pixels. If 0, no cropping is performed.")

    # TODO: user_config* - maybe as positional arguments ?
    # user_config*=*: this can be used to store a custom configuration parameter which will be ignored by the
    #                 algorithm, but will be stored among configuration parameters in the CXI file (data and output).
    #                 e.g.: user_config_temperature=268K  user_config_comment="Vibrations during measurement" etc...

    # ===================== Object & support parameters ==================
    grp = parser.add_argument_group("Initial object and support parameters")

    grp.add_argument(
        '--object', action='store', type=str,
        default=p['object'],
        help="starting object. Import object from .npy, .npz, .mat (the first available array "
             "will  be used if multiple are present), CXI or hdf5 modes file. \n"
             "It is also possible to supply the random range for both amplitude and phase, using:\n\n"
             "* ``--object support,0.9,1,0.5``: this will use random values over the initial support, "
             "  with random amplitudes between 0.9 and 1 and phases with a 0.5 radians range\n"
             "* ``--object obj.npy,0.9,1,0.5``: same but the random values will be multiplied by "
             "      the loaded object.\n\n"
             "By default the object will be defined as random values inside the support area.")

    grp.add_argument(
        '--support', action='store', type=str,
        default=p['support'],
        help="starting support. Different options are available:\n\n"
             "* ``--support sup.cxi``: import support from .npy, .npz, .edf, .mat (the first "
             "available array will be used if multiple are present) or CXI/hdf5 file. Pixels > 0 "
             "are in the support, 0 outside. If the support shape is different than the Iobs "
             "array, it will be cropped or padded accordingly. "
             "If a CXI filename is given, the support will be searched in entry_last/image_1/mask"
             "or entry_1/image_1/mask, e.g. to load support from a previous result file. "
             "For CXI and h5 files, the hdf5 path can also be given: "
             "``--support path/to/data.h5:entry_1/image_1/data``\n"
             "* ``--support auto``: support will be estimated using auto-correlation\n"
             "* ``--support circle``: or ``square``, the support will be initialised to a "
             "circle (sphere in 3d), or a square (cube).\n"
             "* ``--support object``: the support will be initialised from the object "
             "using the given support threshold and smoothing parameters - this requires "
             "that the object itself is loaded from a file. The applied threshold is always "
             "relative to the max in this case.\n"
             "* ``--support object,0.1,1.0``: same as above, but 0.1 will be used as the relative "
             "threshold and 1.0 the smoothing width just for this initialisation.\n"
    )

    grp.add_argument(
        '--support_size', '--supportsize', type=int,
        default=p['support_size'], nargs='+',
        help="size (radius or half-size) for the initial support, to be used in "
             "combination with 'support_type'. The size is given in pixel units. "
             "Alternatively one value can be given for each dimension, i.e. "
             " '--support_size 20 40' for 2D data, and '--support_size 20 40 60' for 3D data. "
             "This will result in an initial support which is a rectangle/parallelepiped"
             "or ellipsis/ellipsoid.\n")

    grp.add_argument(
        '--support_formula', '--supportformula', action='store', type=str,
        default=p['support_formula'],
        help="formula to compute the support. This should be an equation using ix,iy,iz "
             "and ir (radius) pixel coordinates"
             "(all centered on the array) which can be evaluated to produce an "
             "initial support (1/True inside, 0/False outside). "
             "Mathematical functions should use the np. prefix (np.sqrt, ..).\n"
             "Examples:\n\n"
             "* ``--support_formula 'ir<100'``: sphere or circle of radius 100 pixels\n"
             "* ``--support_formula '(np.sqrt(ix**2+iy**2)<50)*(np.abs(iz)<100)'``: "
             "cylinder of radius 50 and height 200.\n"
    )

    grp.add_argument(
        '--support_autocorrelation_threshold', type=float,
        default=p['support_autocorrelation_threshold'], nargs='+',
        help="if no support is given, it will be estimated "
             "from the intensity auto-correlation, with this relative  threshold. "
             "A range can also be given, e.g. '--support_autocorrelation_threshold 0.09 0.11' "
             "and the actual threshold will be randomly chosen between the min and max.")

    grp.add_argument(
        '--support_threshold', type=float,
        default=p['support_threshold'], nargs='+',
        help="Threshold for the support update. Alternatively two values can be given, and the threshold "
             "will be randomly chosen in the interval given by two values: '--support_threshold 0.20 0.28'\n"
             "This is mostly useful in combination with --nb_run")

    grp.add_argument(
        '--support_threshold_method', type=str,
        default=p['support_threshold_method'], choices=['max', 'rms', 'average'],
        help="method used to determine the absolute threshold for the "
             "support update. Either:'max' or 'average' or 'rms' values, "
             "taken over the support area/volume, after smoothing.\n"
             "Note that 'rms' and 'average' use the modulus or root-mean-square value normalised "
             "over the support area, so when the support shrinks, the threshold tends to increase. "
             "In contrast, the 'max' value tends to diminish as the optimisation "
             "progresses. rms is varying more slowly than average and is thus more stable."
             "In practice, using rms or average with a too low threshold can lead to the divergence "
             "of the support (too large), if the initial support is too large.")

    grp.add_argument(
        '--support_only_shrink', action='store_true',
        default=p['support_only_shrink'],
        help="Force the support to only shrink when updated.")

    grp.add_argument(
        '--support_smooth_width_begin', type=float,
        default=p['support_smooth_width_begin'],
        help="during support update, the object amplitude is convoluted by a "
             "gaussian with a size "
             "(sigma) exponentially decreasing from support_smooth_width_begin "
             "to support_smooth_width_end from the first to the last RAAR or "
             "HIO cycle.")

    grp.add_argument(
        '--support_smooth_width_end', type=float,
        default=p['support_smooth_width_end'],
        help="during support update, the object amplitude is convoluted by a "
             "gaussian with a size "
             "(sigma) exponentially decreasing from support_smooth_width_begin "
             "to support_smooth_width_end from the first to the last RAAR or "
             "HIO cycle.")

    grp.add_argument(
        '--support_smooth_width_relax_n', type=float,
        default=p['support_smooth_width_relax_n'],
        help="Number of cycles over which the support smooth width will "
             "exponentially decrease from support_smooth_width_begin to "
             "support_smooth_width_end, and then stay constant. "
             "This is ignored if nb_hio, nb_raar, nb_er are used, "
             "and the number of cycles used "
             "is then the total number of HIO+RAAR cycles")

    grp.add_argument(
        '--support_post_expand', type=str, nargs='+',
        default=p['support_post_expand'], action=ActionSupportPostExpand,
        help="after the support has been updated using a threshold,  it can be shrunk "
             "or expanded by a few pixels, either one or multiple times, e.g. in order"
             "to 'clean' the support:\n\n"
             "* ``--support_post_expand=1`` will expand the support by 1 pixel\n"
             "* ``--support_post_expand=-1`` will shrink the support by 1 pixel\n"
             "* ``--support_post_expand=-1,1`` will shrink and then expand the support "
             "by 1 pixel\n"
             "* ``--support_post_expand=-2,3`` will shrink and then expand the support "
             "by 2 and 3 pixels\n"
             "* ``--support_post_expand=2,-4,2`` will expand/shrink/expand the support "
             "by 2, 4 and 2 pixels\n\n"
             "Note that while it is possible to supply positive values separated by space, "
             "e.g. ``--support_post_expand 2``, for negative values it is necessary to "
             "use ``--support_post_expand=-1`` or ``--support_post_expand=-2,1`` "
             "otherwise the negative number will be mis-interpreted as a command-line "
             "option.")

    grp.add_argument(
        '--support_update_border_n', type=int,
        default=p['support_update_border_n'],
        help="Value to restrict the support update around the previous one: "
             "the only pixels affected by the support updated must lie within "
             "+/- N pixels around the outer border of the support.")

    grp.add_argument(
        '--support_update_period', type=int,
        default=p['support_update_period'],
        help="Update support every N cycle. If 0, it is never updated.")

    grp.add_argument(
        '--support_fraction_min', type=int,
        default=p['support_fraction_min'],
        help="if the fraction of points inside the support falls below "
             "this value, the run is aborted, and a few attempts will be made "
             "to restart the run by dividing "
             "the support_threshold by support_threshold_auto_tune_factor.")

    grp.add_argument(
        '--support_fraction_max', type=int,
        default=p['support_fraction_max'],
        help="If the fraction of points inside the support becomes larger than "
             "this value, the run is aborted, and a few tries will be made "
             "to restart the run by multiplying the "
             "support_threshold by support_threshold_auto_tune_factor.")

    grp.add_argument(
        '--support_threshold_auto_tune_factor', type=float,
        default=p['support_threshold_auto_tune_factor'],
        help="the factor by which the support threshold will be "
             "changed if the support diverges.")

    grp.add_argument(
        '--zero_mask', '--zeromask', action='store', type=str,
        default=p['zero_mask'],
        help="by default masked pixels are free and keep the calculated intensities "
             "during HIO, RAAR, ER and CF cycles."
             "Setting this flag will force all masked pixels to zero intensity. "
             "While not recommended, this may be more stable with a large "
             "number of masked pixels and low intensity diffraction data. "
             "If a value is supplied the following options can be used: \n\n"
             "* ``--zero_mask 0``: masked pixels are free and keep the calculated complex amplitudes\n"
             "* ``--zero_mask 1``: masked pixels are set to zero \n"
             "* ``--zero_mask auto``: this is only meaningful when using a "
             "'standard' algorithm below. The masked pixels will "
             "be set to zero during the first 60%% of the HIO/RAAR cycles,"
             "and will be free during the last 40%% and ER ones.")

    # ===================== Algorithm general parameters ==================
    grp = parser.add_argument_group("Algorithm: general parameters")

    grp.add_argument(
        '--positivity', action='store_true',
        default=p['positivity'],
        help="Use this option to bias the algorithms towards a real, positive "
             "object. Object is still complex-valued, but random start will begin with real "
             "values..")

    grp.add_argument(
        '--beta', type=float,
        default=p['beta'],
        help="Beta value for the HIO/RAAR algorithm")

    grp.add_argument('--gps_inertia', type=float,
                     default=p['gps_inertia'],
                     help="Inertia parameter for the GPS algorithm")

    grp.add_argument('--gps_t', type=float,
                     default=p['gps_t'],
                     help="T parameter for the GPS algorithm")

    grp.add_argument('--gps_s', type=float,
                     default=p['gps_s'],
                     help="S parameter for the GPS algorithm")

    grp.add_argument('--gps_sigma_f', type=float,
                     default=p['gps_sigma_f'],
                     help="Fourier sigma parameter for the GPS algorithm")

    grp.add_argument('--gps_sigma_o', type=float,
                     default=p['gps_sigma_o'],
                     help="Object sigma parameter for the GPS algorithm")

    # ===================== Algorithm standard parameters ==================
    grp = parser.add_argument_group("Algorithm: standard run parameters- either use this or --algorithm")

    grp.add_argument(
        '--nb_raar', type=int,
        default=p['nb_raar'],
        help="number of relaxed averaged alternating reflections cycles, which the "
             "algorithm will use first. During RAAR and HIO, the support is updated regularly")

    grp.add_argument(
        '--nb_hio', type=int,
        default=p['nb_hio'],
        help="number of hybrid input/output cycles, which the algorithm will use after RAAR. "
             "During RAAR and HIO, the support is updated regularly")

    grp.add_argument(
        '--nb_er', type=int,
        default=p['nb_er'],
        help="number of error reduction cycles, performed after HIO/RAAR, without support update")

    grp.add_argument(
        '--nb_ml', type=int,
        default=p['nb_ml'],
        help="number of maximum-likelihood conjugate gradient to perform after ER")

    grp.add_argument(
        '--detwin', action='store', nargs='?', const=True,
        default=p['detwin'], type=int,
        help="Using this option, 10 cycles will be performed at 25%% of the total "
             "number of RAAR or HIO cycles, with a support cut in half to bias "
             "towards one twin image")

    grp.add_argument(
        '--psf', type=str,
        default=p['psf'],
        help="this will trigger the activation of a point-spread function "
             "kernel to model partial coherence (and detector psf), after 66%% of the total number "
             "of HIO and RAAR have been reached. \n"
             "The following options are possible for the psf: \n\n"
             "* ``--psf pseudo-voigt,1,0.05,10``: use a pseudo-Voigt profile "
             "with FWHM 1 pixel and eta=0.05,  i.e. 5%% Lorentzian and 95%% "
             "Gaussian, then update every 10 cycles (using 0 for the last "
             "number prevents the PSF update) \n"
             "* ``--psf gaussian,1.5,10``: start with a Gaussian of FWHM 1.5 pixel, "
             "and update it every 10 cycles \n"
             "* ``--psf lorentzian,0.6,10``: start with a Lorentzian of FWHM 0.6 pixel, "
             "and update it every 10 cycles \n"
             "* ``--psf path/to/file.npz``: load the PSF array from an npz file. "
             "The array loaded should either "
             "be named 'psf' or 'PSF', or the first found array will be loaded."
             "The PSF must be centred in the array, and will be resized (so it can be cropped). "
             "The PSF will be updated every 5 cycles\n"
             "* ``--psf /path/to/file.cxi``: load the PSF from a previous result "
             "CXI file. The PSF will be updated every 5 cycles \n"
             "* ``--psf /path/to/file.cxi,10``: same as before, update the PSF every 10 cycles\n"
             "* ``--psf /path/to/file.cxi,0``: same as before, do not update the PSF\n\n"
             "Recommended values:\n\n"
             "* for highly coherent datasets: no PSF\n"
             "* with some partial coherence: ``--psf pseudo-voigt,1,0.05,20``\n"
             "You can try different widths to start (from 0.25 to 2), and change "
             "the eta mixing parameter.")

    grp.add_argument(
        '--psf_filter', type=str,
        default=p['psf_filter'], choices=['hann', 'tukey', 'none'],
        help="the PSF update filter to avoid divergence. [EXPERIMENTAL, don't use]")

    # ===================== Custom algorithm parameters ==================
    grp = parser.add_argument_group("Algorithm: custom operator chain")

    grp.add_argument(
        '--algorithm', '--algo', type=str, default=p['algorithm'],
        help="Custom algorithm, e.g. ``--algorithm 'ER**50,(Sup*ER**5*HIO**50)**10``: "
             "give a specific sequence of algorithms and/or "
             "parameters to be used for the optimisation (note: this string is case-insensitive).\n"
             "Important: \n"
             "1) when supplied from the command line, there should be NO SPACE in the expression !"
             " And if there are parenthesis in the expression, quotes are required around the "
             "algorithm string\n"
             "2) the string and operators are applied from right to left\n\n"
             "Valid changes of individual parameters include (see detailed description above):\n\n"
             "* ``positivity=0`` or 1\n"
             "* ``support_only_shrink=0`` or 1\n"
             "* ``beta=0.7``\n"
             "* ``live_plot=0`` (no display) or an integer number N to trigger plotting every N cycle\n"
             "* ``support_update_period=0`` (no update) or a positive integer number\n"
             "* ``support_smooth_width_begin=2.0``\n"
             "* ``support_smooth_width_end=0.5``\n"
             "* ``support_smooth_width_relax_n=500``\n"
             "* ``support_threshold=0.25``\n"
             "* ``support_threshold_mult=0.7``  (multiply the threshold by 0.7)\n"
             "* ``support_threshold_method=max`` or average or rms\n"
             "* ``support_post_expand=-1#2`` (in this case the commas are replaced by '#' for parsing)\n"
             "* ``zero_mask=0`` or 1\n"
             "* ``psf=5``: will update the PSF every 5 cycles (and init PSF if necessary with the default"
             " pseudo-Voigt of FWHM 1 pixels and eta=0.1)\n"
             "* ``psf_init=gaussian@1.5``: initialise the PSF with a Gaussian of FWHM 1.5 pixels\n"
             "* ``psf_init=lorentzian@0.5``: initialise the PSF with a Lorentzian of FWHM 0.5 pixels\n"
             "* ``psf_init=pseudo-voigt@1@0.1``: initialise the PSF with a pseudo-Voigt of FWHM 1 pixels"
             " and eta=0.1\n"
             "  Note that using psf_init automatically triggers updating the PSF every 5 cycles, "
             "unless it has already been set using 'psf=...'\n"
             "* ``verbose=20``\n"
             "* ``fig_num=1``: change the figure number for plotting \n\n"
             "Valid basic operators include:\n\n"
             "* ``ER``: Error Reduction\n"
             "* ``HIO``: Hybrid Input/Output\n"
             "* ``RAAR``: Relaxed Averaged Alternating Reflections\n"
             "* ``DetwinHIO``: HIO with a half-support (along first dimension)\n"
             "* ``DetwinHIO1``: HIO with a half-support (along second dimension)\n"
             "* ``DetwinHIO2``: HIO with a half-support (along third dimension)\n"
             "* ``DetwinRAAR``: RAAR with a half-support (along first dimension)\n"
             "* ``DetwinRAAR1``: RAAR with a half-support (along second dimension)\n"
             "* ``DetwinRAAR2``: RAAR with a half-support (along third dimension)\n"
             "* ``CF``: Charge Flipping\n"
             "* ``ML``: Maximum Likelihood conjugate gradient (incompatible with partial coherence PSF)\n"
             "* ``FAP``: FourierApplyAmplitude- Fourier to detector space, apply observed amplitudes, "
             "and back to object space.\n"
             "* ``Sup`` or ``SupportUpdate``: update the support according to the support_* parameters\n"
             "* ``ShowCDI``: display of the object and calculated/observed intensity. This can be used "
             "to trigger this plot at specific steps, instead of regularly using "
             "live_plot=N. This is thus best used using live_plot=0\n\n"
             "**Examples of algorithm string**, where steps are separated with commas (and NO SPACE!),"
             "and are applied from right to left. Operations in a given step will be applied"
             "mathematically, also from right to left, and ``**N`` means repeating N times (N cycles) "
             "the  operation on the left of the exponent:\n\n"
             "* ``--algorithm HIO`` : single HIO cycle\n"
             "* ``--algorithm ER**100`` : 100 cycles of ER\n"
             "* ``--algorithm ER**50*HIO**100`` : 100 cycles of HIO, followed by 50 cycles of ER\n"
             "* ``--algorithm ER**50,HIO**100`` : 100 cycles of HIO, followed by 50 cycles of ER. "
             "The result is the same as the previous example, the difference between using '*' "
             "and ',' when switching from HIO to ER is mostly cosmetic as the process will "
             "separate the two algorithmic steps explicitly when using a ',' , which "
             "can be slightly slower. "
             "Moreover, when using  '--save all', the different steps will be saved as "
             "different entries in the CXI file.\n"
             "* ``--algorithm 'ER**50,(Sup*ER**5*HIO**50)**10'``: "
             "10 times [50 HIO + 5 ER + Support update], followed by 50 ER\n"
             "* ``--algorithm 'ER**50,verbose=1,(Sup*ER**5*HIO**50)**10,verbose=100,HIO**100'``: "
             "change the periodicity of verbose output\n"
             "* ``--algorithm 'ER**50,(Sup*ER**5*HIO**50)**10,support_post_expand=1,"
             "(Sup*ER**5*HIO**50)**10,support_post_expand=-1#2,HIO**100'``: "
             "same but change the post-expand (wrap) method\n"
             "* ``--algorithm 'ER**50,(Sup*ER**5*HIO**50)**5,psf=5,(Sup*ER**5*HIO**50)**10,HIO**100'``: "
             "activate partial correlation after a first series of algorithms\n"
             "* ``--algorithm 'ER**50,(Sup*HIO**50)**4,psf=5,(Sup*HIO**50)**8'``:"
             "typical algorithm steps with partial coherence\n"
             "* ``--algorithm 'ER**50,(Sup*HIO**50)**4,(Sup*HIO**50)**4,positivity=0,"
             "(Sup*HIO**50)**8,positivity=1'``: "
             "same as previous but starting with positivity constraint, removed at the end.")

    return parser
