#! /opt/local/bin/python
# -*- coding: utf-8 -*-
import argparse
# PyNX - Python tools for Nano-structures Crystallography
#   (c) 2016-present : ESRF-European Synchrotron Radiation Facility
#       authors:
#         Vincent Favre-Nicolin, favre@esrf.fr
#   (C) 2020-2024 - Synchrotron SOLEIL
#       authors:
#         Nicolas Mille, nicolas.mille@synchrotron-soleil.fr
#         Picca Frederic-Emmanuel , picca@synchrotron-soleil.fr

import os
import time

import numexpr as ne

import numpy as np

from h5py import File
from PIL import Image

from .runner import PtychoRunner, PtychoRunnerScan, PtychoRunnerException, \
    default_params as params0
from ...utils import phase

helptext_epilog = " Example:\n" \
                  "     pynx-ptycho-hermes --folder path/to/my/data/ --maxsize 1300 --threshold 10 " \
                  " --algorithm=AP**150,nbprobe=1,probe=1 --probediam 61e-9)\n\n" \
                  " The distance (SI units - in meters) between the " \
                  "detector and the sample can be guessed thanks to " \
                  "the annulus size.\n"\
                  " A calibration file is here :\n" \
                  "     /home/experiences/hermes/com-hermes/DATA/Sample2detector_Calculator.ods"

# The beamline specific default parameters
params_beamline = {
    # specific
    'adaptq': 'soft',
    'camcenter': None,
    'dark_as_optim': False,
    'logfile': "log_reconstruction_"
               f"{time.gmtime().tm_mon:.2f}_{time.gmtime().tm_year}.txt",
    'nrjdigits' : 3,
    'onlynrj': None,
    'real_coordinates': False,
    'ruchepath': None,
    'savefolder': None,
    'savellk': False,
    'savetiff': 'full',
    'scantype': None,  # choice: stack,image determine via filenames.
    'threshold': 30,
}

params_generic_overwrite = {
    # overwrite generic parameters
    'algorithm': 'AP**150,nbprobe=1,probe=1',
    'detector_orientation': "1,0,0",
    'instrument': 'Hermes@Soleil',
    'liveplot': True,
    'maxsize': 1000,
    'obj_inertia': 0.1,
    'pixelsize': 11e-6,
    'probe': 'disc,61e-9',
    'probe_inertia': 0.01,
    'saveplot': True,
    'verbose': 10,
}

default_params = params0.copy()
default_params.update(params_beamline)
default_params.update(params_generic_overwrite)

H5PATH_SAMPLE_X = '/entry1/camera/sample_x'
H5PATH_SAMPLE_Y = '/entry1/camera/sample_y'
H5PATH_ENERGY = '/entry1/camera/energy'
H5PATH_SCAN_DATA = '/entry/scan_data/'
H5PATH_SSD_DISTANCE = '/entry1/scanArgs/SDDistance'


def wavelength(energy):
    """Compute the wavelength from the nrj.

    :param energy: X-ray energy in eV
    :return: wavelength in Angstroems
    """
    return 1239.84 * 1e-9 / energy


def calc_roi(start_roi, length, start_energy, this_energy):
    """Function to calculate the new ROI.

    in order to adapt Q space the software way
    """
    cam_pixel_size = 11.6 * 1e-6  # camera pixel size in µm TODO use
    # the params for this
    roi1 = cam_pixel_size * start_roi / 2
    theta1 = np.arctan(roi1 / length) * 180 / np.pi
    ll1 = wavelength(start_energy)
    ll2 = wavelength(this_energy)
    return round(length * np.tan(np.arcsin((ll2 / ll1) * np.sin(theta1 * np.pi / 180)))
                 * 1e+6 / 11.6 * 2)


class PtychoRunnerScanHermes(PtychoRunnerScan):
    """Deal with Hermes Scans."""

    def load_scan(self):
        """Supersed of load_scan function from runner.

        Update x, y positions of the camera images as well as the
        number of images imgn

        When the scan type is an energy stack:
        ---> self.scan refer to the nrj number
             (from 01 to NN, NN being the total number of energies)
        When the scan type are separated files:
        ---> self.scan refer to the scan number XX,
             given by the Image_YYYYMMDD_XX.hdf5 file
        """
        if self.params['verbose']:
            print(f'\nProcessing the {self.scan} step'
                  f" from the scantype: '{self.params['scantype']}'\n")
        h5n = os.path.join(self.params['folder'], self.params['filedescr'])
        h5n = h5n % self.scan if self.params['scantype'] == 'image' else h5n

        x = y = imgn = None

        with File(h5n, 'r') as h5:
            # Building coodinates depending if sample_x and sample_y
            # contains the full coordinates or the square points
            dset_x = h5[H5PATH_SAMPLE_X]
            dset_y = h5[H5PATH_SAMPLE_Y]
            n_x = len(dset_x)
            n_y = len(dset_y)

            if self.params["real_coordinates"]:
                # extract x dataset without extra copy
                x = np.empty(n_x, dtype=np.float32)
                dset_x.read_direct(x)

                # extract y dataset without extra copy
                y = np.empty(n_y, dtype=np.float32)
                dset_y.read_direct(y)
            else:
                x = np.empty(n_x * n_y, dtype=np.float32)
                x_tmp = np.empty(n_x, dtype=np.float32)
                dset_x.read_direct(x_tmp)

                y = np.empty(n_x * n_y, dtype=np.float32)
                y_tmp = np.empty(n_y, dtype=np.float32)
                dset_y.read_direct(y_tmp)

                # Here two options for the scan direction : switch True/False
                if True:
                    # Option 1 : The scan is in the type Y stays
                    # constant, scan X, step Y, rescan X, etc.
                    for j in range(n_y):
                        for i in range(n_x):
                            x[i + j * n_x] = x_tmp[i]
                            y[i + j * n_x] = y_tmp[j]
                else:
                    # Option 2 : The scan is in the type X stay
                    # constant, scan Y, step X, rescan Y, etc.
                    for i in range(n_x):
                        for j in range(n_y):
                            x[j + i * n_y] = x_tmp[i]
                            y[j + i * n_y] = y_tmp[j]

        # set values in meter
        x *= 1e-6
        y *= 1e-6

        # Imgn variable
        imgn = np.arange(len(x), dtype=int)

        if self.params['moduloframe'] is not None:
            n1, n2 = self.params['moduloframe']
            idx = np.where(imgn % n1 == n2)[0]

            x = x.take(idx)
            y = y.take(idx)
            imgn = imgn.take(idx)

        if self.params['maxframe'] is not None:
            maxframe = self.params['maxframe']
            if len(imgn) > maxframe:
                if self.params['verbose']:
                    print(f'MAXFRAME: only using first {maxframe} frames')

                x = x[:maxframe]
                y = y[:maxframe]
                imgn = imgn[:maxframe]

        self.x = x
        self.y = y
        self.imgn = imgn

    def load_data(self):
        """
        Supersed of load_data function from runner
        Update the raw_data variable with all the images

        Also update the energy variable here

        When the type of scan is energy stack:
        ---> self.scan refer to the nrj number
             (from 01 to NN, NN being the total number of energies)
        When the type of scan is separate files:
        ---> self.scan refer to the scan number XX,
             given by the Image_YYYYMMDD_XX.hdf5 file
        """

        # nxs data file
        h5n_sample = os.path.join(self.params["folder"],
                                  self.params["filesample"] % self.scan)
        with File(h5n_sample, 'r') as h5_sample:

            # Load the sample data from the nxs
            raw_data = None
            sample_data_entry = h5_sample.get(H5PATH_SCAN_DATA)
            # Get the name of the image data (e.g. SI107_20191201_image)
            # It is the first entry of the H5PATH_SCAN_DATA group of nxs file.
            # Need to be modified if it is no longer the case
            sample_data_imgname = list(sample_data_entry.keys())[0]
            # Loading only the images corresponding to the imgn variable
            # (in order to take into account moduloframe and maxframe
            # params) and broadcasting to float32.
            # Purpose of separating into two case is to avoid making a
            # copy if the full data are used (a copy is mandatory for the
            # sliced data, so it will be longer to load sliced data than
            # the full ones ... )
            raw_data_dset = sample_data_entry.get(sample_data_imgname)
            if self.imgn.shape[0] == raw_data_dset.shape[0]:
                raw_data = np.array(raw_data_dset, dtype=np.float32, copy=False)
            else:
                raw_data = np.array(raw_data_dset[self.imgn, :, :], dtype=np.float32, copy=False)
            self.raw_data = raw_data

        # load all other parameters
        h5n = os.path.join(self.params["folder"], self.params["filedescr"])
        h5n_dark = os.path.join(self.params["folder"], self.params["filedark"])

        # TODO: this shoudl rather use directly self.params['saveprefix']
        saveprefix = os.path.join(self.params['savefolder']
                                  if self.params['savefolder']
                                  else self.params['folder'],
                                  'reconstructed')
        if self.params['scantype'] == 'stack':
            prefix = 'stack_' + self.params['date'] + '_' + self.params['scannbr']
            saveprefix = os.path.join(saveprefix,
                                      prefix + '_reconstructed',
                                      prefix + '_nrj%03d_run%02d')
        elif self.params['scantype'] == 'image':
            h5n = h5n % self.scan
            h5n_dark = h5n_dark % self.scan
            saveprefix = os.path.join(saveprefix,
                                      'image_' + self.params["date"] + '_%03d_reconstructed_run%02d')
        self.params['saveprefix'] = saveprefix

        with File(h5n, 'r') as hd5_scaninfo:
            nrj = None
            nrjstart = None
            nrj_dset = hd5_scaninfo.get(H5PATH_ENERGY)
            if self.params["scantype"] == "stack":
                sss = "njr"
                # Retrieve the energy in keV: For an energy stack,
                # the data are saved as
                # stack_date_scannbr_nrjXX_000001.nxs with XX
                # being identified as the self.scan param XX goes
                # from 01 to NN, NN being the number of
                # energies. But a list start from 0 !  So we
                # deincrement the self.scan parameter to find the
                # right energy
                nrj = nrj_dset[self.scan - 1]
                nrjstart = nrj_dset[0]
            else:
                sss = "image"
                nrj = nrj_dset[()]
                nrjstart = 0.0  # useless for image

            self.params['nrj'] = nrj * 1e-3
            self.params['startnrj'] = nrjstart * 1e-3

            print('##############################################')
            print(f"Start loading data for {sss} number {self.scan} ;"
                  f" energy = {nrj} eV")

            with File(h5n_dark, 'r') as hd5_dark:
                # Load the same way the dark from nxs
                dark_data_entry = hd5_dark.get(H5PATH_SCAN_DATA)
                dark_data_imgname = list(dark_data_entry.keys())[0]
                # Avoiding the first dark image (a generally not good one)
                dark_data = dark_data_entry[dark_data_imgname][1:, :, :]
                dark_average = np.mean(dark_data, axis=0, dtype=np.float32)

                print('##############################################')
                print(f"Start Preprocessing for {sss} {self.scan}")

                if self.params["dark_as_optim"] is False:
                    print("dark subtraction and applying threshold")
                    # Subtract dark and apply threshold: parallel with numexpr version
                    ne_expression = "where(data - dark > thr, data - dark, 0)"
                    ne_localdict = {"data": self.raw_data,
                                    "dark": dark_average,
                                    "thr": self.params["threshold"]}
                    ne.evaluate(ne_expression, local_dict=ne_localdict, out=self.raw_data)

                    # Bring dark_subtract to 0 in order not to subtract twice
                    self.params["dark_subtract"] = 0

                else:
                    # Load the dark in order to use it in the reconstruction
                    print("dark loading...")
                    self.dark = dark_average
                    self.params["dark_subtract"] = 1

                    if np.any(self.raw_data < 0):
                        raise PtychoRunnerException("some raw data below zero ??!!")

            # Adapt the maxsize or the detector distance if it is an
            # energy stack depending if detector distance has been moved
            # (hard way: the detector distance changes) or not (software
            # way: maxsize changes)
            if (self.params["scantype"] == "stack"
                    and self.scan != self.params["scan"][0]):
                if self.params["adaptq"] == 'soft':
                    self.params["maxsize"] = calc_roi(self.params["maxsizeini"],
                                                      self.params["detectordistance"],
                                                      self.params["startnrj"] * 1e3,
                                                      self.params["nrj"] * 1e3)
                elif self.params["adaptq"] == 'hard':
                    try:
                        self.params["detectordistance"] = \
                            hd5_scaninfo.get(H5PATH_SSD_DISTANCE)[self.scan - 1] * 1e-6
                    except Exception:  # TODO use the right exception
                        print("You wanted to use hard adaptq but the updated sample-detector distances \
                        are not found in the hdf5 descriptor file, reconstruction will continue \
                        without any q adaptation")

            # Check if a camera center has been given, and define the according camera roi in that case
            if self.params['camcenter'] is not None:
                xmin = self.params['camcenter'][0] - self.params['maxsize'] // 2
                xmax = self.params['camcenter'][0] + self.params['maxsize'] // 2
                ymin = self.params['camcenter'][1] - self.params['maxsize'] // 2
                ymax = self.params['camcenter'][1] + self.params['maxsize'] // 2
                self.params['roi'] = f"{xmin},{xmax},{ymin},{ymax}"

            # Apply the load_data_post_process function
            # It initialize the dark, the mask and the flatfield variables
            # If none are given, initialize at 0 for dark and mask and at 1 for flatfield
            self.load_data_post_process()

            print("Preprocessing over")
            print('##############################################')
            print(f"Reconstruction of {sss} {self.scan} will start with the following parameters :")
            self.print_params_hermes()
            print('##############################################')

    def save(self, run, stepnum=None, algostring=None):
        """Overwriting of the save function

        Purpose:
        - updating the reconstruction log file
        - saving llk value if user params says so

        The original function is called via super()
        Only a part is added at the end

        Save the result of the optimization, and (if
        self.params['saveplot'] is True) the corresponding plot.

        This is an internal function.

        :param run:  the run number (integer)
        :param stepnum: the step number in the set of algorithm steps
        :param algostring: the string corresponding to all the algorithms ran
        :return:
        """
        super().save(run, stepnum, algostring)
        self.update_logfile(run)

        # Save LLKs values in dedicated text file
        if self.params['savellk']:
            self.save_llk(run)

    def save_plot(self, run, stepnum=None, algostring=None,
                  display_plot=False):
        """
        Overwriting of the save_plot function

        Purpose: saving also the reconstruction data as four tif files:
        - two for the object (amplitude and phase)
        - two for the probe (amplitude and phase)

        The original function is called via super()
        Only a part is added at the end to do the tif files

        Save the plot to a png file.

        :param run:  the run number (integer)
        :param stepnum: the step number in the set of algorithm steps
        :param algostring: the string corresponding to all the algorithms ran
        :param display_plot: if True, the saved plot will also be displayed
        :return:
        """
        super().save_plot(run, stepnum, algostring, display_plot)

        if self.params["savetiff"] not in [None, "No"]:
            # Get the obj, probe and scanned area for the probe and
            # object (copy-pasted from parent function)
            if 'split' in self.params['mpi']:
                self.p.stitch(sync=True)
                obj = self.p.mpi_obj
                scan_area_obj = self.p.get_mpi_scan_area_obj()
                if not self.mpi_master:
                    return
            else:
                obj = self.p.get_obj()
                scan_area_obj = self.p.get_scan_area_obj()
            scan_area_probe = self.p.get_scan_area_probe()

            if ((self.p.data.near_field
                 or not self.params['remove_obj_phase_ramp'])):
                obj = obj[0]
                probe = self.p.get_probe()[0]
            else:
                obj = phase.minimize_grad_phase(obj[0],
                                                center_phase=0,
                                                global_min=False,
                                                mask=~scan_area_obj,
                                                rebin_f=2)[0]
                probe = phase.minimize_grad_phase(self.p.get_probe()[0],
                                                  center_phase=0,
                                                  global_min=False,
                                                  mask=~scan_area_probe,
                                                  rebin_f=2)[0]

            # Get the amplitude and the phase of object
            if self.params["savetiff"] == "full":
                obj_amp = Image.fromarray(np.abs(obj))
                obj_phase = Image.fromarray(np.angle(obj))
                probe_amp = Image.fromarray(np.abs(probe))
                probe_phase = Image.fromarray(np.angle(probe))
            elif self.params["savetiff"] == "crop":
                # Get the indices of the object and probe where they
                # are actually reconstructed
                xmin_obj, ymin_obj = np.argwhere(scan_area_obj).min(axis=0)
                xmax_obj, ymax_obj = np.argwhere(scan_area_obj).max(axis=0)
                xmin_probe, ymin_probe = \
                    np.argwhere(scan_area_probe).min(axis=0)
                xmax_probe, ymax_probe = \
                    np.argwhere(scan_area_probe).max(axis=0)

                # This part is needed because sometimes (why ???),
                # scan_area_obj and scan_area_probe give a strange
                # size of the object (especially a non square
                # reconstructed image araise from a square scan ...)

                # object AMPLITUDE as Image cropped with the right size
                obj_amp = Image.fromarray(
                    np.abs(obj[xmin_obj:xmax_obj,
                           ymin_obj:ymax_obj]))
                # Object PHASE as Image cropped with the right size
                obj_phase = Image.fromarray(
                    np.angle(obj[xmin_obj:xmax_obj,
                             ymin_obj:ymax_obj]))
                # Probe AMPLITUDE as Image cropped with the right size
                probe_amp = Image.fromarray(
                    np.abs(probe[xmin_probe:xmax_probe,
                           ymin_probe:ymax_probe]))
                # Probe PHASE as Image cropped with the right size
                probe_phase = Image.fromarray(
                    np.angle(probe[xmin_probe:xmax_probe,
                             ymin_probe:ymax_probe]))

            # Save the images
            prefix = self.params["saveprefix"] % (self.scan, run)
            obj_amp.save(prefix + '_Object_Amplitude.tif')
            obj_phase.save(prefix + '_Object_Phase.tif')
            probe_amp.save(prefix + '_Probe_Amplitude.tif')
            probe_phase.save(prefix + '_Probe_Phase.tif')

    def update_logfile(self, run):
        """Logfile updating function"""
        with open(self.params['logfile'], mode='a') as logfile:
            content = ["############################################\n"]
            content.append("date of reconstruction: " + time.asctime() + "\n")
            # TODO remove the hadcoded 11
            content.append("file reconstructed: "
                           + self.params["filesample"][:-11] % self.scan
                           + "\n")
            content.append("Run number: " + str(run) + "\n")

            for key, value in self.params.items():
                if key in ['algorithm', 'maxsize', 'threshold',
                           'detectordistance', 'rebin', 'defocus',
                           'probe', 'scantype']:
                    content.append(str(key) + " = " + str(value) + "\n")

            content.append(f"LLK={self.p.llk_poisson / self.p.nb_obs:.3f}\n")

            savepath = self.params["saveprefix"] % (self.scan, run)
            content.append("Saved in: " + savepath + ".cxi\n")
            content.append("END\n\n")

            logfile.writelines(content)
            if self.params['verbose']:
                print()
                print('Updated log file: ' + self.params['logfile'])
                print()

    def save_llk(self, run):
        """Save into a file the llk parameters"""
        all_llk = np.array([[k]
                            for k, v in self.p.history['llk_poisson'].items()])
        headerllk = "cycle\t"
        for whichllk in ['llk_poisson', 'llk_gaussian', 'llk_euclidian']:
            headerllk += whichllk + '\t'
            thisllk = np.array([[v]
                                for k, v in self.p.history[whichllk].items()])
            all_llk = np.concatenate((all_llk, thisllk), axis=1)
        llkfilename = self.params["saveprefix"] % (self.scan, run) \
                      + '_everyllk.txt'
        if self.params['verbose']:
            print("\nSaving all llk values in " + llkfilename + "\n")
        np.savetxt(llkfilename, all_llk, delimiter='\t', header=headerllk)

    def print_params_hermes(self):
        """Display the Hermes specific parameters"""
        for k in params_beamline.keys():
            print(k + ' : ' + str(self.params[k]))


class PtychoRunnerHermes(PtychoRunner):
    """Class to process Hermes scans."""

    def __init__(self, argv, params, *args, **kwargs):
        super().__init__(argv, default_params if params is None else params)
        self.PtychoRunnerScan = PtychoRunnerScanHermes
        self.redefine_scanparam()

    @classmethod
    def make_parser(cls, default_par, description=None, script_name="pynx-ptycho-hermes", epilog=None):
        if epilog is None:
            epilog = helptext_epilog
        if description is None:
            description = "Script to perform a ptychography analysis on data from Hermes@Soleil"
        p = default_par

        parser = super().make_parser(default_par, script_name, description, epilog)
        grp = parser.add_argument_group("Hermes parameters")

        class ActionFolder(argparse.Action):
            """Argparse Action to check the folder exists"""

            def __call__(self, parser_, namespace, value, option_string=None):
                if not os.path.isdir(value):
                    raise argparse.ArgumentError(
                        self, f"--folder {value}: folder does not exist or is not a directory")
                setattr(namespace, self.dest, value)

        grp.add_argument('--folder', type=str, required=True, action=ActionFolder,
                         help="The path where the data are located (data.nxs, dark.nxs, "
                              "descr.hdf5 must all be there !)")

        grp.add_argument(
            '--threshold', type=int, default=p['threshold'],
            help='threshold, for the high pass filter. It correspond to the thermal '
                 'noise of the camera, after dark subtraction.')

        class ActionCamCenter(argparse.Action):
            """Argparse Action to check the folder exists"""

            def __call__(self, parser_, namespace, value, option_string=None):
                mes = f"--camcenter {' '.join(value)}: need two integers for " \
                      f"the the camera centre position, e.g. '--camcenter 1002 1036"
                try:
                    if len(value) == 1 and ',' in value[0]:
                        # Probably --camcenter=200,400
                        v = [int(s) for s in value[0].split(',')]
                    elif len(value) == 2:
                        v = [int(s) for s in value]
                    else:
                        raise argparse.ArgumentError(self, mes)
                except ValueError:
                    raise argparse.ArgumentError(self, mes)

                setattr(namespace, self.dest, v)

        grp.add_argument(
            '--camcenter', default=p['camcenter'], nargs='+',
            action=ActionCamCenter,
            help='The center of the camera, if the auto finding of PyNX do not '
                 'find the proper center of diffraction You should respect the '
                 'convention ``--camcenter x0 y0`` with x0 and y0 INTEGERS: the '
                 'coordinates x and y of the center in pixel coordinates. Do not '
                 'call this parameter to let PyNX find the center of diffraction.')

        grp.add_argument('--adaptq', type=str, default=p['adaptq'],
                         help="the way to adapt q:\n"
                              "Only used for energy stack (so if type=stack).\n"
                              "Defines the way to adapt the data to have a constant q space "
                              "in the reconstructions of the whole stack.\n"
                              "Takes only three values:\n\n"
                              "* 'soft': adapt the q space changing the 'maxsize' PyNX parameter. "
                              "  ⚠️ do it if you didn't make detector distance to move "
                              "during the stack\n"
                              "* 'hard': ⛔ NOT IMPLEMENTED YET ⛔. "
                              "adapt the q space changing the 'detector distance' parameter "
                              "according to the control program way to move it "
                              "⚠️ do it if you actually did make detector distance to move "
                              " during the stack\n"
                              "* ``none``: do not adaptq. "
                              "⚠️ do it only if you don't want to adapt q space\n\n"
                              "The default is ``soft``: it changes automatically the 'maxsize' parameter "
                              "taking into account the starting energy and the detector distance "
                              "to get the same q range and thus the same nbr of pixel in all the "
                              "reconstruction of the stack")

        # VFN: would be easier to use directly '--probe disc,60e-6' instead
        # of introducing a new 'probediam' parameter
        class ActionProbeDiam(argparse.Action):
            """Argparse Action to convert the 'probediam' entry to a string for 'probe' """

            def __call__(self, parser_, namespace, value, option_string=None):
                diam = float(value)
                setattr(namespace, self.dest, f"disc,{diam}")

        grp.add_argument(
            '--probediam', '--probe', dest='probe', action=ActionProbeDiam, required=True,
            help="Diameter of the probe in meters, at the focus "
                 "(it is 1.22 x FZP outerZone width).\n"
                 "⚠️ don't take into account the defocus here ⚠️")

        grp.add_argument('--nrjdigits', type=int, dest='nrjdigits',
                         default=p['nrjdigits'],
                         help=argparse.SUPPRESS)

        grp.add_argument(
            '--onlynrj', type=int, default=None, nargs='+',
            help="nrj numbers (indices in the stack) you want to reconstruct within a stack."
                 "If you want the whole stack, don't use this parameter.")

        # VFN: I'm not fond of this argument - it's simply the opposite of the
        # default argument "--dark_subtract". Also, I don't think subtracting the
        # background by default is a good idea. Though in this case the process is
        # different due to the 'filter' used with a threshold.
        grp.add_argument(
            '--dark_as_optim', action='store_true',
            help="Call this flag to use the dark as background for reconstruction "
                 "instead of subtracting it to each diffraction patter before the "
                 "reconstruction.\n\n"
                 "⚠️⚠️ If you use this, you may need to optimise the background"
                 " in your algorithm (background=1) ⚠️⚠️")

        grp.add_argument(
            '--ruchepath', type=str, default=None,
            help="e.g. /nfs/ruche-hermes/hermes-soleil/com-hermes/PTYCHO/some/path:\n"
                 "Path where only the Amplitude.tif will be saved. Useful to have "
                 "directly access to it for XMCD on another computer")

        # VFN: TODO: Not implemented. Have a look at --save_prefix which is quite flexible and
        #            should probably be used instead..
        grp.add_argument('--savefolder', type=str, default=None, help=argparse.SUPPRESS)

        grp.add_argument('--savetiff', default='full',
                         choices=['crop', 'full', 'No'],
                         help="Option to save the reconstruction as tiff:\n\n"
                              "* ``crop``: only the scanned area\n"
                              "* ``full``: full reconstruction, with parts outside the scan area\n"
                              "* ``No``: don't save the tiff file")

        grp.add_argument('--savellk', action='store_true',
                         help="If you want to save the llk values in a separate text file, "
                              "which will be named 'everyllk.txt'")
        # Undocumented option
        grp.add_argument('--real_coordinates', action='store_true',
                         help=argparse.SUPPRESS)

        # VFN: the following parameters are automatically determined. Not sure they
        # should really be in self.params (they could be regular class members), but
        # we keep them here for now as they are needed

        # VFN: This should probably be available as an option in the main parser
        grp.add_argument('--logfile', action='store', type=str, default=p['logfile'],
                         help=argparse.SUPPRESS)
        grp.add_argument('--scantype', action='store', type=str, default=p['scantype'],
                         help=argparse.SUPPRESS)

        return parser

    def redefine_scanparam(self):
        """
        Computing the filenames and checking the files are there.

        """
        # Store the initial detector distance and maxsize in case
        # adaptq is used
        self.params['detectordistanceini'] = self.params['detectordistance']
        self.params['maxsizeini'] = self.params['maxsize']

        # Here we retrieve the id XX for the data, the dark and the descr
        alldatafiles = [f for f in os.listdir(self.params["folder"])
                        if '.nxs' in f and 'dark' not in f and 'temp' not in f]
        alldarkfiles = [f for f in os.listdir(self.params["folder"])
                        if '.nxs' in f and 'dark' in f and 'temp' not in f]
        alldescrfiles = [f for f in os.listdir(self.params["folder"])
                         if '.hdf5' in f]

        dataids = [int(f.split('_')[2]) for f in alldatafiles]
        darkids = [int(f.split('_')[2]) for f in alldarkfiles]
        descrids = [int(f.split('.')[0].split('_')[2]) for f in alldescrfiles]

        scanfound = []
        scannames = []
        scantypes = []
        scandates = []

        if self.params["scan"] is not None:
            # First case: scan nbr are given by the user: Looking for
            # the files which have the same id as the user parameter
            # 'scan' and where there is a data.nxs, a dark.nxs and a
            # descr.hdf5 files
            for descrfile, descrid in zip(alldescrfiles, descrids):
                if ((descrid in self.params['scan']
                     and descrid in darkids
                     and descrid in dataids)):
                    scanfound.append(descrid)
                    basename = descrfile.split('.')[0]
                    scannames.append(basename)
                    scantypes.append(basename.split('_')[0].lower())
                    scandates.append(basename.split('_')[1].lower())

            scannotfound = [i for i in self.params['scan'] if i not in scanfound]

            if scannotfound != []:
                sss = ",".join(f"{scannbr}" for scannbr in scannotfound)
                raise PtychoRunnerException(
                    f"Scans number {sss} not found in folder:"
                    f" {self.params['folder']}\nIt could be because the scan"
                    " nbr is wrong or because one of the file"
                    " (data.nxs, dark.nxs or descr.hdf5) is missing")
        else:
            # Second case: no scan nbr given by the user:
            # Looking for all the files in the folder
            # where there are a data.nxs, a darks.nxs and a descr.hdf5 files

            for descrfile, descrid in zip(alldescrfiles, descrids):
                if descrid in darkids and descrid in dataids:
                    scanfound.append(descrid)
                    basename = descrfile.split('.')[0]
                    scannames.append(basename)
                    scantypes.append(basename.split('_')[0].lower())
                    scandates.append(basename.split('_')[1].lower())

            if not scanfound:
                raise PtychoRunnerException(
                    f"No scan found in folder: {self.params['folder']}\n"
                    "It could be because one of the file"
                    " (data.nxs, dark.nxs or descr.hdf5) is missing")

        # Now all the scan to be reconstructed have been found

        # Check the scan type and associate it to the 'scantype' parameter
        if 'image' in scantypes and 'stack' not in scantypes:
            self.params["scantype"] = 'image'
        elif 'stack' in scantypes and 'image' not in scantypes:
            self.params["scantype"] = 'stack'
        elif 'image' in scantypes and 'stack' in scantypes:
            raise PtychoRunnerException(
                f"You asked for reconstruction of stacks and images in your"
                f" folder: {self.params['folder']}, but we cannot reconstruct"
                " like this for now. Use batch on different folder or express"
                " a scan number with scan=XXX")
        else:
            raise PtychoRunnerException("Uknown file type")

        if self.params["scantype"] == 'stack':
            if len(scanfound) > 1:
                sss = ",".join(f"{scanname}" for scanname in scannames)
                raise PtychoRunnerException(
                    "You asked for several stacks to be reconstructed on the"
                    " same batch line, or there is several hdf5 descriptor"
                    f" file in you folder: {sss}. Several scans cannot be"
                    " reconstructed on the same batch line, use batch with"
                    " several lines and scan=XXX to do so. Check also if there"
                    " are several hdf5 with the same scan nbr in your folder")

            scannbr = f"{scanfound[0]:03d}"
            self.params["scannbr"] = scannbr

        if len(set(scandates)) != 1:
            raise PtychoRunnerException(
                "Ok this is a tricky error! You asked for reconstructions of"
                " files which come from different dates. Sorry it cannot be"
                " done. Use batch file and scan=XXX to do so")

        date = set(scandates).pop()
        self.params["date"] = date

        # Redefine the 'scan' parameter to fit with PyNX way to do
        if self.params["scantype"] == 'image':
            # If only images, the scan parameter is the same as the user give,
            # well formated in string of int separated by commas
            scanfound.sort()
            scan = [scanid for scanid in scanfound]
        else:
            # If it is a stack, the scan parameter will be the nrjs
            allnrjs = [f
                       for f in alldatafiles
                       if "stack_" + date + "_" + scannbr in f]
            allnrjs.sort()

            # and take into account the 'onlynrj' parameter
            if self.params["onlynrj"] is None:
                # TODO rename usednrj and add an 's' this is a list.
                usednrj = allnrjs
            else:
                usednrj = []
                # Get the nrjs as integer for proper formatting
                try:
                    onlynrj = [int(n)
                               for n in self.params["onlynrj"].split(',')]
                except ValueError as exc:
                    raise PtychoRunnerException(
                        "onlynrj parameter wrongly formated: use"
                        " onlynrj=XX,XX,XX,etc. with XX the nrj number in the"
                        " stack, not the nrj in eV!") from exc

                # Goes through every file of the stack and get the
                # filename if the nrj is one of the user defined nrj
                for fname in allnrjs:
                    for nrj in onlynrj:
                        if f"_nrj{nrj:.3d}_" in fname:
                            if self.params['verbose']:
                                print(f"nrj {nrj:.3d} will be reconstructed")
                            usednrj.append(fname)

                if len(onlynrj) != len(usednrj):
                    raise PtychoRunnerException(
                        "You have asked for nrjs that are not in the stack."
                        " Check again the 'onlynrj' parameter")

                # Format the scan parameter to be the different nrjs
            scan = [int(filenrj.split('_')[3][3:]) for filenrj in usednrj]

        # Complete the 'scan' parameter removing the last comma
        self.params["scan"] = scan

        # Format the filenames so they can be retrieve with 'filename'
        # % self.scan in the 'load_data' and 'load_scan' functions of
        # PtychoHermesRunnerScan class TODO check if we can use
        # f-strings
        if self.params["scantype"] == "stack":
            # 1st %s: YYYYMMDD ; 2nd %s: XXX
            prefix = f'stack_{date}_{scannbr}'
            digits = self.params['nrjdigits']
            self.params["filesample"] = f'{prefix}_nrj%.{digits}d_000001.nxs'
            self.params["filedark"] = f'{prefix}_dark_000001.nxs'
            self.params["filedescr"] = f'{prefix.capitalize()}.hdf5'
        elif self.params["scantype"] == "image":
            # 1st %s: YYYYMMDD
            prefix = f'image_{date}'
            self.params["filesample"] = f'{prefix}_%.3d_000001.nxs'
            self.params["filedark"] = f'{prefix}_%.3d_dark_000001.nxs'
            self.params["filedescr"] = f'{prefix.capitalize()}_%.3d.hdf5'


def make_parser_sphinx():
    """Returns the argparse for sphinx documentation"""
    return PtychoRunnerHermes.make_parser(default_params)
