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

# BCDI: tools for pre(post)-processing Bragg coherent X-ray diffraction imaging data
#   (c) 07/2017-06/2019 : CNRS UMR 7344 IM2NP
#   (c) 07/2019-present : DESY PHOTON SCIENCE
#       authors:
#         Clément Atlan, c.atlan@outlook.com
#         Jerome Carnis, carnis_jerome@yahoo.fr

"""Parsing of command-line arguments and config files."""

from argparse import ArgumentParser
import os
import pathlib
from typing import Any, ByteString, Dict, Union
import yaml

from bcdi.utils.parameters import valid_param
import bcdi.utils.validation as valid


def add_cli_parameters(argument_parser: ArgumentParser) -> ArgumentParser:
    """
    Add generic parameters to the argument parser.

    :param argument_parser: an instance of argparse.ArgumentParser
    :return: the updated instance
    """
    argument_parser.add_argument(
        "--align_q",
        type=str,
        help="If orthogonalized, do not align q",
    )

    argument_parser.add_argument(
        "-bckg", "--background_file", type=str, help="optional background_file"
    )

    argument_parser.add_argument(
        "-bl",
        "--beamline",
        type=str,
        help="beamline where the measurement was made",
    )

    argument_parser.add_argument(
        "-bin" "--phasing_binning",
        type=str,
        help="binning factor applied during phasing",
    )

    argument_parser.add_argument(
        "-c",
        "--config_file",
        type=str,
        help="path to the config file",
    )

    argument_parser.add_argument("--debug", type=str, help="debugging option")

    argument_parser.add_argument(
        "-det", "--detector", type=str, help="detector used during measurement"
    )

    argument_parser.add_argument("-en", "--energy", type=float, help="beam energy")

    argument_parser.add_argument(
        "-med", "--median_filter", type=str, help="apply a filtering method"
    )

    argument_parser.add_argument(
        "--fix_voxel",
        type=str,
        help="the voxel size used for interpolation",
    )

    argument_parser.add_argument(
        "-flip",
        "--flip_reconstruction",
        type=str,
        help="choose to flip or not the reconstruction",
    )

    argument_parser.add_argument("--grazing_angle", type=float, help="incidence angle")

    argument_parser.add_argument(
        "--inplane_angle", type=float, help="detector in-plane angle"
    )

    argument_parser.add_argument(
        "--interact",
        type=str,
        help="choose if interaction mode is enabled",
    )

    argument_parser.add_argument(
        "-iso",
        "--isosurface_strain",
        type=float,
        help="the isosurface threshold used for postprocessing",
    )

    argument_parser.add_argument(
        "--outofplane_angle", type=float, help="detector out-of-plane angle"
    )

    argument_parser.add_argument(
        "--phasing_binning",
        type=str,
        help="binning factor applied during phasing",
    )

    argument_parser.add_argument(
        "--rocking_angle",
        type=str,
        choices=["inplane", "outofplane"],
        help="rocking angle",
    )

    argument_parser.add_argument(
        "--root_folder",
        type=str,
        help="path to the directory where all scans are stored",
    )

    argument_parser.add_argument("--sample_name", type=str, help="name of the sample")

    argument_parser.add_argument(
        "--save_dir", type=str, help="directory path where to save"
    )

    argument_parser.add_argument(
        "-s", "--scan", type=int, help="number of the scan to process"
    )

    argument_parser.add_argument(
        "--scans", type=int, help="list of the scans to process"
    )

    argument_parser.add_argument(
        "--sdd",
        type=float,
        help="sample to detector distance",
    )

    argument_parser.add_argument(
        "--specfile_name", type=str, help="path to '.spec' file"
    )

    argument_parser.add_argument(
        "--template_imagefile",
        type=str,
        help="template of the data image files",
    )

    argument_parser.add_argument(
        "--tilt_angle", type=float, help="angle step used during scan"
    )

    return argument_parser


class ConfigParser:
    """
    Base class for parsing arguments.

    If provided, command line arguments will override the parameters from the config
    file. Some validation is also realized in the class.

    :param file_path: path of the configuration file that contains
    the arguments, str.
    :param command_line_args: an optional dictionary of command-line arguments
    """

    def __init__(
        self, file_path: str, command_line_args: Union[Dict[str, Any], None] = None
    ) -> None:
        self.file_path = file_path
        self.command_line_args = command_line_args
        self.raw_config = self._open_file()
        self.arguments: Union[Dict, None] = None

    @property
    def command_line_args(self):
        """Return an optional dictionary of command line parameters."""
        return self._command_line_args

    @command_line_args.setter
    def command_line_args(self, value):
        valid.valid_container(
            value, container_types=dict, allow_none=True, name="command_line_args"
        )
        self._command_line_args = value

    @property
    def file_path(self):
        """Path of the configuration file."""
        return self._file_path

    @file_path.setter
    def file_path(self, value):
        valid.valid_container(
            value, container_types=str, min_length=1, name="file_path"
        )
        if pathlib.Path(value).suffix != ".yml":
            raise ValueError("Expecting a YAML config file")
        if not os.path.isfile(value):
            raise ValueError(f"The file {value} does not exist")
        self._file_path = value

    @staticmethod
    def _check_args(dic: Dict[str, Any]) -> Dict[str, Any]:
        """Apply some validation on each parameter."""
        checked_keys = []
        for key, value in dic.items():
            value, is_valid = valid_param(key, value)
            if is_valid:
                dic[key] = value
                checked_keys.append(key)
            else:
                print(
                    f"'{key}' is an unexpected key, " "its value won't be considered."
                )
        return {key: dic[key] for key in checked_keys}

    @staticmethod
    def filter_dict(dic: Dict, filter_value: Any = None) -> Dict:
        """
        Filter out key where the value is None.

        :param dic: a dictionary
        :param filter_value: value to be filtered out
        :return: a dictionary with only keys where the value is not None
        """
        return {k: v for k, v in dic.items() if v is not filter_value}

    def load_arguments(self) -> Dict:
        """Parse the byte string, eventually override defaults and check parameters."""
        args = yaml.load(self.raw_config, Loader=yaml.SafeLoader)

        # keep only command line arguments which are defined (not None)
        valid_cla = self.filter_dict(self.command_line_args)

        # override the default config by the valid command line arguments
        args.update(valid_cla)

        # validate the parameters
        self.arguments = self._check_args(args)
        return self.arguments

    def _open_file(self) -> ByteString:
        """Open the file and return it."""
        with open(self.file_path, "rb") as f:
            raw_config = f.read()
        return raw_config
