from glob import glob
import inspect
import itertools
import os

import numpy as np

from dipy.testing.decorators import warning_for_keywords
from dipy.workflows.base import get_args_default


def common_start(sa, sb):
    """Return the longest common substring from the beginning of sa and sb."""

    def _iter():
        for a, b in zip(sa, sb):
            if a == b:
                yield a
            else:
                return

    return "".join(_iter())


def slash_to_under(dir_str):
    return "".join(dir_str.replace("/", "_"))


@warning_for_keywords()
def connect_output_paths(
    inputs, out_dir, out_files, *, output_strategy="absolute", mix_names=True
):
    """Generate a list of output files paths based on input files and
    output strategies.

    Parameters
    ----------
    inputs : array
        List of input paths.
    out_dir : string
        The output directory.
    out_files : array
        List of output files.
    output_strategy : string, optional
        Which strategy to use to generate the output paths.
            'append': Add out_dir to the path of the input.
            'prepend': Add the input path directory tree to out_dir.
            'absolute': Put directly in out_dir.
    mix_names : bool, optional
        Whether or not prepend a string composed of a mix of the input
        names to the final output name.

    Returns
    -------
        A list of output file paths.

    """
    outputs = []
    if isinstance(inputs, str):
        inputs = [inputs]
    if isinstance(out_files, str):
        out_files = [out_files]

    sizes_of_inputs = [len(inp) for inp in inputs]

    max_size = np.max(sizes_of_inputs)
    min_size = np.min(sizes_of_inputs)
    if min_size > 1 and min_size != max_size:
        raise ImportError("Size of input issue")

    elif min_size == 1:
        for i, sz in enumerate(sizes_of_inputs):
            if sz == min_size:
                inputs[i] = max_size * inputs[i]

    if mix_names:
        mixing_prefixes = concatenate_inputs(inputs)
    else:
        mixing_prefixes = [""] * len(inputs[0])

    for mix_pref, inp in zip(mixing_prefixes, inputs[0]):
        inp_dirname = os.path.dirname(inp)
        if output_strategy == "prepend":
            if os.path.isabs(out_dir):
                dname = out_dir + inp_dirname
            if not os.path.isabs(out_dir):
                dname = os.path.join(os.getcwd(), out_dir + inp_dirname)

        elif output_strategy == "append":
            dname = os.path.join(inp_dirname, out_dir)

        else:
            dname = out_dir

        updated_out_files = []
        for out_file in out_files:
            updated_out_files.append(os.path.join(dname, mix_pref + out_file))

        outputs.append(updated_out_files)

    return inputs, outputs


def concatenate_inputs(multi_inputs):
    """Concatenate list of inputs."""
    mixing_names = []
    for inps in zip(*multi_inputs):
        mixing_name = ""
        for inp in inps:
            mixing_name += basename_without_extension(inp) + "_"

        mixing_names.append(mixing_name + "_")
    return mixing_names


def basename_without_extension(fname):
    base = os.path.basename(fname)
    result = base.split(".")[0]
    if result[-4:] == ".nii":
        result = result.split(".")[0]

    return result


@warning_for_keywords()
def io_iterator(
    inputs,
    out_dir,
    fnames,
    *,
    output_strategy="absolute",
    mix_names=False,
    out_keys=None,
):
    """Create an IOIterator from the parameters.

    Parameters
    ----------
    inputs : array
        List of input files.
    out_dir : string
        Output directory.
    fnames : array
        File names of all outputs to be created.
    output_strategy : string, optional
        Controls the behavior of the IOIterator for output paths.
    mix_names : bool, optional
        Whether or not to append a mix of input names at the beginning.
    out_keys : list, optional
        Output parameter names.

    Returns
    -------
        Properly instantiated IOIterator object.

    """
    io_it = IOIterator(output_strategy=output_strategy, mix_names=mix_names)
    io_it.set_inputs(*inputs)
    io_it.set_out_dir(out_dir)
    io_it.set_out_fnames(*fnames)
    io_it.create_outputs()
    if out_keys:
        io_it.set_output_keys(*out_keys)

    return io_it


@warning_for_keywords()
def _io_iterator(frame, fnc, *, output_strategy="absolute", mix_names=False):
    """Create an IOIterator using introspection.

    Parameters
    ----------
    frame : frameobject
        Contains the info about the current local variables values.
    fnc : function
        The function to inspect
    output_strategy : string, optional
        Controls the behavior of the IOIterator for output paths.
    mix_names : bool, optional
        Whether or not to append a mix of input names at the beginning.

    Returns
    -------
        Properly instantiated IOIterator object.

    """

    # Create a new object that does not contain the ``self`` dict item
    def _selfless_dict(_values):
        return {key: val for key, val in _values.items() if key != "self"}

    args, _, _, values = inspect.getargvalues(frame)
    args.remove("self")
    # Create a new object that does not contain the ``self`` dict item from the
    # provided copy of the local symbol table returned by ``getargvalues``.
    # Avoids attempting to remove it from the object returned by
    # ``getargvalues``.
    values = _selfless_dict(values)

    spargs, defaults = get_args_default(fnc)

    len_args = len(spargs)
    len_defaults = len(defaults)
    split_at = len_args - len_defaults

    inputs = []
    outputs = []
    out_dir = ""

    # inputs
    for arv in args[:split_at]:
        inputs.append(values[arv])

    # defaults
    out_keys = []
    for arv in args[split_at:]:
        if arv == "out_dir":
            out_dir = values[arv]
        elif "out_" in arv:
            out_keys.append(arv)
            outputs.append(values[arv])

    return io_iterator(
        inputs,
        out_dir,
        outputs,
        output_strategy=output_strategy,
        mix_names=mix_names,
        out_keys=out_keys,
    )


class IOIterator:
    """Create output filenames that work nicely with multiple input files from
    multiple directories (processing multiple subjects with one command)

    Use information from input files, out_dir and out_fnames to generate
    correct outputs which can come from long lists of multiple or single
    inputs.
    """

    @warning_for_keywords()
    def __init__(self, *, output_strategy="absolute", mix_names=False):
        self.output_strategy = output_strategy
        self.mix_names = mix_names
        self.inputs = []
        self.out_keys = None

    def set_inputs(self, *args):
        self.file_existence_check(args)
        self.input_args = list(args)
        for inp in self.input_args:
            if isinstance(inp, str):
                self.inputs.append(sorted(glob(inp)))
            if isinstance(inp, list) and all(isinstance(s, str) for s in inp):
                nested = [sorted(glob(i)) for i in inp if isinstance(i, str)]
                self.inputs.append(list(itertools.chain.from_iterable(nested)))

    def set_out_dir(self, out_dir):
        self.out_dir = out_dir

    def set_out_fnames(self, *args):
        self.out_fnames = list(args)

    def set_output_keys(self, *args):
        self.out_keys = list(args)

    def create_outputs(self):
        if len(self.inputs) >= 1:
            self.updated_inputs, self.outputs = connect_output_paths(
                self.inputs,
                self.out_dir,
                self.out_fnames,
                output_strategy=self.output_strategy,
                mix_names=self.mix_names,
            )

            self.create_directories()

        else:
            raise ImportError("No inputs")

    def create_directories(self):
        for outputs in self.outputs:
            for output in outputs:
                directory = os.path.dirname(output)
                if not (directory == "" or os.path.exists(directory)):
                    os.makedirs(directory)

    def __iter__(self):
        ins = np.array(self.inputs).T
        out = np.array(self.outputs)
        IO = np.concatenate([ins, out], axis=1)
        for i_o in IO:
            if len(i_o) == 1:
                yield str(*i_o)
            else:
                yield i_o

    def file_existence_check(self, args):
        input_args = []
        for fname in args:
            if isinstance(fname, str):
                input_args.append(fname)
            # unpack variable string
            if isinstance(fname, list) and all(isinstance(s, str) for s in fname):
                input_args += fname
        for path in input_args:
            if len(glob(path)) == 0:
                raise OSError(f"File not found: {path}")
