"""Common functions used across modules.
"""
import csv

_CSV_KWARGS = [
    'delimiter',
    'doublequote',
    'escapechar',
    'lineterminator',
    'quotechar',
    'quoting',
    'skipinitialspace',
    'strict',
    'dialect',
    'restval',
    'extrasaction'
]
"""The possible csv keyword arguments (`list of `str`)
"""


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class _CsvWrapper(object):
    """A wrapper around csv to provide a basic file-like interface.

    Parameters
    ----------
    file : `File`
        A file object to wrap in a csv reader/writer.
    mode : `str`
        The csv mode, either ``r`` (reader) or ``w`` (writer).
    **csv_kwargs
        Keyword arguments that will be passed through to csv. They should be
        appropriate for (dict) readers/writers.
    """

    READ_MODE = 'r'
    """The read mode indicator (`str`)
    """
    WRITE_MODE = 'w'
    """The write mode indicator (`str`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, file, mode="r", **csv_kwargs):
        self._file = None
        self._csv = None
        self._csv_kwargs = csv_kwargs

        self.file = file

        if mode not in [self.WRITE_MODE, self.READ_MODE]:
            raise ValueError(
                f"mode must be read ({self.READ_MODE}) or write "
                f"({self.WRITE_MODE})"
            )
        self._mode = mode

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __enter__(self):
        """Entry into the context manager.
        """
        return self

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __exit__(self, *args):
        """Exit from the context manager.
        """
        return self

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __iter__(self):
        """The iterator.
        """
        return self

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __next__(self):
        """Get the next row from the csv reader.
        """
        try:
            return next(self._csv)
        except TypeError as e:
            if self._csv is not None:
                raise
            else:
                raise IOError("did you forget to open") from e

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def open(self):
        """Initialise the csv file.

        Returns
        -------
        self
            The wrapper object.
        """
        return self

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def close(self):
        """Close the csv wrapper file object.
        """
        self._file.close()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def name(self):
        """Get the file name (`str`).
        """
        return self._file.name

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def csv_kwargs(self):
        """Get the csv keyword arguments (`dict`).
        """
        return self._csv_kwargs

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def mode(self):
        """Get the opening mode (`str`).
        """
        return self._mode

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def csv(self):
        """Get the csv reader/writer object (`csv.reader` or `csv.writer` or
        `csv.DictReader` or `csv.DictWriter` or `NoneType`).
        """
        return self._csv

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @csv.setter
    def csv(self, csv):
        """Set the csv reader/writer object, can only be set once (`csv.reader`
        or `csv.writer` or `csv.DictReader` or `csv.DictWriter` or `NoneType`).
        """
        if self._csv is not None:
            raise ValueError("the csv object can only be set once")
        self._csv = csv

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def file(self):
        """Get the file object being wrapped (`File`).
        """
        return self._file

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @file.setter
    def file(self, file):
        """Set the file object to be wrapped, can only be set once (`File`).
        """
        if self._file is not None:
            raise ValueError("the file object can only be set once")
        self._file = file

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def read(self):
        """Read the entire file (`list` of (`list` or `dict`)).
        """
        return [i for i in self._csv]

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def readline(self):
        """Read a single line from the file (`list` or `dict`).
        """
        return next(self._csv)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def write(self, row):
        """Write a single row to the file (`list` or `dict`).
        """
        return self._csv.writerow(row)


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class CsvWrapper(_CsvWrapper):
    """A wrapper around a file to provide a basic file-like interface to a
    ``csv.reader``/``csv.writer``

    Parameters
    ----------
    file : `File`
        A file object to wrap in a csv reader/writer.
    mode : `str`
        The csv mode, either ``r`` (reader) or ``w`` (writer).
    **csv_kwargs
        Keyword arguments that will be passed through to csv. They should be
        appropriate for (dict) readers/writers.
    """
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def open(self):
        """Initialise the ``csv.reader`` or ``csv.writer``.

        Returns
        -------
        self
            The wrapper object.
        """
        if self.mode == self.READ_MODE:
            self.csv = csv.reader(self.file, **self.csv_kwargs)
        else:
            self.csv = csv.writer(self.file, **self.csv_kwargs)
        return self


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class CsvDictWrapper(_CsvWrapper):
    """A wrapper around a file to provide a basic file-like interface to a
    ``csv.DictReader``/``csv.DictWriter``

    Parameters
    ----------
    file : `File`
        A file object to wrap in a csv reader/writer.
    mode : `str`
        The csv mode, either ``r`` (reader) or ``w`` (writer).
    fieldnames : `list` of `str`
        The field names that will be passed to a ``csv.DictWriter`` if mode is
        ``w``.
    **csv_kwargs
        Keyword arguments that will be passed through to csv. They should be
        appropriate for (dict) readers/writers.
    """
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, *args, fieldnames=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._fieldnames = fieldnames

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def open(self):
        """Initialise the ``csv.DictReader`` or ``csv.DictWriter``.

        Returns
        -------
        self
            The wrapper object.
        """
        if self.mode == self.READ_MODE:
            self.csv = csv.DictReader(self.file, **self.csv_kwargs)
        else:
            self.csv = csv.DictWriter(self.file, self._fieldnames,
                                      **self.csv_kwargs)
        return self


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def extract_csv_kwargs(kwargs):
    """Extract the CSV keyword arguments from a dictionary.

    Parameters
    ----------
    kwargs : `dict`
        A dictionary of keyword arguments potentially containing some keyword
        arguments for the csv constructor.

    Returns
    -------
    csv_kwargs : `dict`
        Any csv keyword arguments that have been extracted. This will be empty
        if none are found.
    """
    csv_kwargs = dict()
    for i in _CSV_KWARGS:
        try:
            csv_kwargs[i] = kwargs.pop(i)
        except KeyError:
            pass
    return csv_kwargs
