"""A higher level API to provide a single function to the external merge sort
"""
from collections.abc import Iterator
from merge_sort import chunks, merge
import os
# import pprint as pp

_HEADER_KWARG = 'header'
"""The keyword argument name for the header value (`str`)
"""
_DELIMITER_KWARG = 'delimiter'
"""The keyword argument name for the csv delimiter value (`str`)
"""
_LINETERM_KWARG = 'lineterminator'
"""The keyword argument name for the csv lineterminator value (`str`)
"""
_CSV_KWARGS = 'csv_kwargs'
"""The keyword argument name for the csv keyword arguments (`str`)
"""


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def sorted(initer, key=None, reverse=False, chunksize=100000, tmpdir=None):
    """Perform an external merge sort on an iterator.

    Parameters
    ----------
    initer : `iterator`
        An iterator that provides, strings, lists or dicts to use in the sort.
        If the object is not an iterator an attempt will be made to make it
        into an iterator. An iterator is required as the first row is used to
        determine the chunker used for the sort. For list like objects will a
        csv based chunker will be used, for dicts a csv dict based chunker will
        be used. The keys of the dictionary will be used as a header row for the
        file, so every key will be converted to a string.
    key : `function`, optional, default: `NoneType`
        A function to provide the sort key. Please keep in mind that this will
        be used for both chunking the input and merging the intermediate files.
        When merging the data types of any list or dictionary values will be
        strings, so your function should perform appropriate casts for data
        types even if the input rows already have those data types.
    reverse : `bool`, optional, default: `False`
        Sort in reverse.
    chunksize : `int`, optional, default: `100000`
        The number of rows to hold in memory at anyone time during the chunking
        to intermediate files.
    tmpdir : `str`, optional, default: `NoneType`
        The temporary directory to store the intermediate chunk files.

    Yields
    ------
    sorted_rows : `str` or `list` or `dict`
        The output rows will match the data type input rows. Although the
        values within a list or dictionary will all be strings even if the
        input had differently typed values. Also, keep in mind that unless the
        input was list or dict (i.e. using csv) then the yielded rows will have
        newlines.

    Raises
    ------
    TypeError
        If the first row of the input is not a `str`, `int`, `float`, `list`
        or `dict`.
    """
    # Check if the input is truly an iterator
    if not isinstance(initer, Iterator):
        try:
            initer = iter(initer)
        except TypeError as e:
            raise TypeError(
                "not an iterator and can't be made into one"
            ) from e

    csv_delimiter = "\t"
    first_row = next(initer)

    # Sort out the chunker/merger required for the sort process
    chunker = None
    merger = None
    chunk_kwargs = dict(key=key, chunksize=chunksize, reverse=reverse)
    merge_kwargs = dict(key=key, tmpdir=tmpdir, reverse=reverse)
    proc_func = _dummy
    if isinstance(first_row, (str, int, float)):
        chunker = chunks.ExtendedChunks
        merger = merge.IterativeHeapqMerge
        chunk_kwargs[_HEADER_KWARG] = None
        merge_kwargs[_HEADER_KWARG] = False
        proc_func = _newline
    elif isinstance(first_row, (list, tuple)):
        chunker = chunks.CsvExtendedChunks
        merger = merge.CsvIterativeHeapqMerge
        chunk_kwargs[_HEADER_KWARG] = None
        chunk_kwargs[_DELIMITER_KWARG] = csv_delimiter
        chunk_kwargs[_LINETERM_KWARG] = os.linesep
        merge_kwargs[_HEADER_KWARG] = False
        merge_kwargs[_CSV_KWARGS] = dict(delimiter=csv_delimiter,
                                         lineterminator=os.linesep)
    elif isinstance(first_row, dict):
        chunker = chunks.CsvDictExtendedChunks
        merger = merge.CsvDictIterativeHeapqMerge
        chunk_kwargs[_HEADER_KWARG] = [str(i) for i in first_row.keys()]
        chunk_kwargs[_DELIMITER_KWARG] = csv_delimiter
        chunk_kwargs[_LINETERM_KWARG] = os.linesep
        merge_kwargs[_HEADER_KWARG] = True
        merge_kwargs[_CSV_KWARGS] = dict(delimiter=csv_delimiter,
                                         lineterminator=os.linesep)
    else:
        raise TypeError(f"can't handle input type: {type(first_row)}")

    with chunker(tmpdir, **chunk_kwargs) as c:
        c.add_row(proc_func(first_row))
        for i in initer:
            c.add_row(proc_func(i))

    with merger(c.chunk_file_list, **merge_kwargs) as m:
        for i in m:
            yield i


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _dummy(x):
    """A dummy passthrough function.

    Parameters
    ----------
    x : `Any`
        Value to passthrough.

    Returns
    -------
    x : `Any`
        The given value.
    """
    return x


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _newline(x):
    """Convert a simple type to a string and add a newline if it does not
    already exist.

    Parameters
    ----------
    x : `str` or `int` or `float`
        The value to convert.

    Returns
    -------
    str_x : `str`
        The converted value.
    """
    x = str(x)
    if x.endswith("\n"):
        return x
    return x + "\n"
