"""
This module contains a set of common lightweight wrappers to perform simple and non-customised DeltaDeltaCt analyses.
This is designed for lightweight users that do not wish to employ specialised pipelines.
The ``qpcr.Pipes`` allow you to "shortcut" some steps for you as they handle object setup for the main ``qpcr`` classes.
They will perform file-reading, Delta-Ct computation, and normalisation, as well as visualization and saving for you.

Which pipeline to use?
--------------

Depending on which pipeline you choose you will get different levels of customizability. 
In the basic tutorials we worked with the ``BasicPlus`` pipeline which allows for file-reading, filtering, Delta-Ct computation, and normalisation, as well as visualisation and file-saving for the results.
It is the "big brother" of the most simple pipeline called ``Basic`` which only performs file-reading, Delta-Ct computation, and normalisation, as well as results saving.

The most "free" pipeline that works with files is the ``Blueprint`` pipeline that allows a user-specified *Reader*, *Analyser*, and *Normaliser*. 

However, these above pipelines were all designed with **multiple regular datafiles** as input in mind! However, as you may have *irregular* or *multi assay files* you may not find these pipelines to be usable for you.
This is no problem, however, since the ``qpcr.DataReader`` allows for very swift file-reading into ``qpcr.Assay`` objects from many different filetypes. These can then directly be submitted to the ``ddCt`` pipeline. 
This skips file reading but only performs filtering, Delta-Ct computation, normalisation, and visualisation + file saving. Here is a "pipeline example" using ``ddCt``:

.. code-block:: python

    import qpcr
    from qpcr.Pipes import ddCt

    myfile = "some_irregular_multi_assay_file.xlsx"

    # read the data externally
    assays, normalisers = qpcr.read_multi_assay( myfile )

    # now setup the pipeline
    pipe = ddCt()

    # add our data and run
    pipe.link( assays = assays, normalisers = normalisers )
    pipe.run()

    # now get the results to inspect 
    results = pipe.results()



Note
-----
A Word on Plotters 
Please, note that pipelines fully support mixing "static" and "interactive" Plotters, 
but static figures will not stay open if interactive plotters are called to plot after them! 
Because `qpcr.Filters` are always called to plot *before* any other `qpcr.Plotters`, this will mainly
affect visualising the `qpcr.Plotters.ReplicateBoxplots` generated as Filter-Summaries.
"""

import logging
import qpcr
import qpcr._auxiliary.warnings as aw
import qpcr._auxiliary as aux
import qpcr.Plotters as Plotters
import qpcr.Filters as Filters
import qpcr.Readers as Readers
import os

logger = aux.default_logger()


class Pipeline:
    """
    This is the basic template class for qpcr Pipelines.
    It contains a set of basic preliminary methods
    that ensure that elementary required inputs are provided.

    Note
    ----
    The simplest implementation of this `Pipeline` template is the `Basic` pipeline.
    """

    __slots__ = ['_Normalisers', '_Assays', '_save_to', '_df', '_stats_df', '_Results', '_replicates', '_names', '_softlink']

    def __init__(self):
        # super().__init__()
        self._Normalisers = []
        self._Assays = []
        self._save_to = None
        self._df = None
        self._stats_df = None
        self._Results = qpcr.Results()
        self._replicates = None
        self._names = None
        self._softlink = True

    def assays(self):
        """
        Returns
        -------
        assays : list
            The linked `qpcr.Assay` objects for assays-of-interest.
        """
        assays = self._Assays
        return assays

    def normalisers(self):
        """
        Returns
        -------
        normalisers : list
            The linked `qpcr.Assay` objects for normaliser-assays.
        """
        normalisers = self._Normalisers
        return normalisers

    def replicates(self, replicates: (int or tuple)):
        """
        Set the replicates specifics to use for grouping.

        Parameters
        ----------
        replicates : int or tuple
            Can be an `integer` (equal group sizes, e.g. `3` for triplicates),
            or a `tuple` (uneven group sizes, e.g. `(3,2,3)` if the second group is only a duplicate).
        """
        self._replicates = replicates

    def names(self, names: (list or dict)):
        """
        Set names for replicates groups.

        Parameters
        ----------
        names : list or dict
            Either a `list` (new names without repetitions) or `dict` (key = old name, value = new name) specifying new group names.
            Group names only need to be specified once, and are applied to all replicate entries.
        """
        self._names = names

    def run(self, **kwargs):
        """
        Will run the pipeline provided that at least minimal inputs have been provided (i.e. Assays and Normalisers, as well as replicate specifics have been provided).
        This is a wrapper, the actual functional core is defined in the method `self._run()`.
        To implement your own pipeline, make sure to define your own `_run()`, redefine `run()`
        as you require.

        Parameters
        ----------
        **kwargs
            Any additional keyword arguments that will be passed to the actual `_run()` method.
        """

        # vet if there are at least one normaliser and assay present
        if self._Normalisers == [] or self._Assays == []:
            e = aw.PipeError("no_data")
            logger.critical(e)
            raise e

        self._run(**kwargs)

    def save_to(self, directory: str):
        """
        Set the location where to save result files

        Parameters
        ----------
        directory : str
            A directory to save the results to.
        """
        self._save_to = directory
        # if the directory does not yet exist, we make it
        if not os.path.exists(self._save_to):
            os.mkdir(self._save_to)

    def get(self, kind="stats"):
        """
        Returns
        -------
        data
            A pandas dataframe either in replicate version `kind="df"`
            or in stats version `kind="stats"` (default). Alternatively,
            a qpcr.Results object can be returned using `kind="obj"`.
        """
        if kind == "stats":
            return self._stats_df
        elif kind == "df":
            df = self._df
            if "assay" in df.columns:
                df = df.drop(columns=["assay"])
            return df
        elif kind == "obj":
            return self._Results

    def results(self):
        """
        Returns
        -------
        Results : qpcr.Results
            The `qpcr.Results` object storing the pipelines data.
            Note, this is equivalent to `get( kind = "obj" )`.
        """
        return self._Results

    def link(self, assays: (list or str) = None, normalisers: (list or str) = None):
        """
        Links new assays-of-interest / sample assays and/or normaliser assays
        to the pipline, either replacing old ones or keeping them,
        depending on `softlink()` settings.

        Parameters
        ----------
        assays : list or str
            A `list` of filepaths to raw datafiles of assays-of-interest, or a directory (`str`) where these are stored.
        normalisers : list or str
            A `list` of filepaths to raw datafiles of normaliser assays, or a directory (`str`) where these are stored.
        """
        if self._softlink:
            self.prune()
        self.add_assays(assays)
        self.add_normalisers(normalisers)

    def prune(self, assays=True, results=True, normalisers=True):
        """
        Will clear assays, results, and/or normalisers

        Parameters
        ----------
        assays : bool
            Will clear any sample assays in the pipeline if True (default).

        results : bool
            Will clear any computed results in the pipline if True (default).

        normalisers : bool
            Will clear any normalisers in the pipline if True (default).
        """
        if assays:
            self._Assays = []
        if normalisers:
            self._Normalisers = []
        if results:
            self._df = None
            self._stats_df = None
            self._Results = qpcr.Results()

    def add_normalisers(self, normalisers):
        """
        Adds normalisers (filepaths) (keeping any already present)

        Parameters
        ----------
        normalisers : list or str
            A `list` of filepaths to raw datafiles of normaliser assays, or a directory (`str`) where these are stored.
        """
        normalisers = self._from_directory(normalisers)
        self._Normalisers.extend(normalisers)

    def add_assays(self, assays):
        """
        Adds assays-of-interest / sample assays (filepaths) (keeping any already present)

        Parameters
        ----------
        assays : list or str
            A `list` of filepaths to raw datafiles of sample assays, or a directory (`str`) where these are stored.
        """
        assays = self._from_directory(assays)
        self._Assays.extend(assays)

    def softlink(self, bool=None):
        """
        If `softlink = True`, then `link_assays()` will
        prune any previous assays. Otherwise, it will
        add new ones and keep old ones.

        Parameters
        ----------
        bool
            Set to False to disable `softlinking` (default is True).
        """
        if bool is None:
            return self._softlink
        else:
            self._softlink = bool

    def _from_directory(self, files):
        """
        Checks if a directory was provided for assays / normalisers and returns a list of all contained files if so.
        Otherwise it just returns the list of files.
        This is used for add_assays and add_normalisers
        """
        if isinstance(files, str):

            # if inputs are a directory
            if os.path.isdir(files):

                # get files from the directory
                datafiles = os.listdir(files)

                # if no files are found, raise error
                if len(datafiles) == 0:
                    e = aw.PipeError("no_data_input", file=files)
                    logger.critical(e)
                    SystemExit(e)

                # combine paths with parent directory
                datafiles = [os.path.join(files, n) for n in datafiles]
                return datafiles

            # else check if inputs are a single file
            # then just put it into a list to be valid input for .extend()
            elif os.path.isfile(files):

                return [files]

            else:
                e = aw.PipeError("no_data_input", file=files)
                logger.critical(e)
                SystemExit(e)

        # else check if we got a list or tuple of files
        elif isinstance(files, (list or tuple)):
            return files

        # raise error for anything else...
        else:
            e = aw.PipeError("no_data_input", file=files)
            logger.critical(e)
            SystemExit(e)

    def _run(self, **kwargs):
        """
        This is the actual function that each custom Pipeline has to define...
        """
        print("self._run() is the actual function that each custom Pipeline has to define...")


class Basic(Pipeline):
    """
    Performs simple standardized DeltaDeltaCt analysis
    based on two lists of files, one for normaliser assays and one for sample assays.
    This makes use of standard settings for `qpcr.Analyser` and `qpcr.Normaliser`
    which cannot be customized!
    For customization check out the `Blueprint` pipeline or generate your own.
    """

    def __init__(self):
        super().__init__()

    def _run(self, **kwargs):
        """
        The automated standard DeltaDeltaCt pipeline.
        Using default settings of `qpcr.Analyser` and `qpcr.Normaliser`
        """
        reader = qpcr.DataReader()
        analyser = qpcr.Analyser()
        normaliser = qpcr.Normaliser()

        # add replicate and names info to kwargs
        kwargs = dict(kwargs, replicates=self._replicates, names=self._names)

        # analyse normalisers
        normalisers = [reader.read(i, **kwargs) for i in self._Normalisers]
        normalisers = [analyser.pipe(i) for i in normalisers]

        # analyse sample assays
        assays = [reader.read(i, **kwargs) for i in self._Assays]
        assays = [analyser.pipe(i) for i in assays]

        # now normalise
        normaliser.link(assays=assays, normalisers=normalisers)
        normaliser.normalise()

        # get and store results
        results = normaliser.get()
        self._Results = results
        self._df = results.get()
        self._stats_df = results.stats()

        if self._save_to is not None:
            results.save(self._save_to)


class BasicPlus(Basic):
    """
    The same as the Basic Pipeline, but has the option to integrate Plotters and Filters.
    """

    def __init__(self):
        super().__init__()
        self._Plotters = []
        self._Figures = []
        self._Filters = []

    def prune(self, assays=True, results=True, normalisers=True, figures=True, plotters=False, filters=False):
        """
        Will clear the pipeline.

        Parameters
        ----------
        assays : bool
            Will clear any sample assays in the pipeline if True (default).

        results : bool
            Will clear any computed results in the pipline if True (default).

        normalisers : bool
            Will clear any normalisers in the pipline if True (default).
        figures : bool
            Will clear any figures in the pipline if True (default).
        plotters : bool
            Will clear any Plotters from the pipeline if True (default False).
        filters : bool
            Will clear any Filters in the pipline if True (default False).
        """
        super().prune(assays=assays, results=results, normalisers=normalisers)
        if figures:
            self._Figures = []
        if filters:
            self._Filters = []
        if plotters:
            self._Plotters = []

    def add_plotters(self, *Plotters: object):
        """
        Adds already specified qpcr.Plotter objects to the Pipeline.

        Parameters
        ----------
        *Plotters
            Any number of qpcr.Plotters.Plotter objects.
        """
        self._Plotters.extend(Plotters)

    def add_filters(self, *Filters: object):
        """
        Adds already specified qpcr.Filter instances to the Pipeline.

        Parameters
        ----------
        *Filters
            Any number of `qpcr.Filters.Filter` objects.
        """
        self._Filters.extend(Filters)

    def Figures(self):
        """
        Returns
        -------
        list
            A list of all Figures generated by the pipeline's Plotters.
        """
        return self._Figures

    def _run(self, **kwargs):
        """
        The automated standard DeltaDeltaCt pipeline.
        Also produces applies and Plotters to produce figures...
        """

        # simply set up the Blueprint pipeline
        # and run with default settings
        pipeline = Blueprint()

        pipeline.replicates(self._replicates)
        pipeline.names(self._names)

        if self._save_to is not None:
            pipeline.save_to(self._save_to)

        pipeline.add_assays(self._Assays)
        pipeline.add_normalisers(self._Normalisers)

        pipeline.add_plotters(*self._Plotters)
        pipeline.add_filters(*self._Filters)

        pipeline.run()

        # and store the results
        self._Results = pipeline.get(kind="obj")
        self._df = pipeline.get(kind="df")
        self._stats_df = pipeline.get(kind="stats")


class Blueprint(BasicPlus):
    """
    Performs simple Delta-Delta-Ct analysis based on the same workflow as the `BasicPlus` pipeline, but allows full costumization of Reader, Analyser, and Normaliser objects.
    Optionally, `Reader`, `qpcr.Analyser`, and `qpcr.Normaliser` may be set up externally and linked into the pipeline. Any non-linked processing classes will be set up using defaults.
    """

    def __init__(self):
        super().__init__()
        self._Reader = None
        self._Analyser = None
        self._Normaliser = None

    def prune(self, assays=True, results=True, normalisers=True, figures=True, plotters=False, filters=False, cores=False, reader=False, analyser=False, normaliser=False):
        """
        Will clear the pipeline.

        Parameters
        ----------
        assays : bool
            Will clear any sample assays in the pipeline if True (default).

        results : bool
            Will clear any computed results in the pipline if True (default).

        normalisers : bool
            Will clear any normalisers in the pipline if True (default).
        figures : bool
            Will clear any figures in the pipline if True (default).
        plotters : bool
            Will clear any Plotters from the pipeline if True (default False).
        filters : bool
            Will clear any Filters in the pipline if True (default False).
        cores : bool
            Will clear Reader, Analyser, and Normaliser if True (default False).
        reader : bool
            Will only clear the Reader if True (default False).
        analyser : bool
            Will only clear the Analyser if True (default False).
        normaliser : bool
            Will only clear the Normaliser if True (default False).
            Note, that clearing `results` will also clear the `Normaliser`'s results,
            but keep the Normaliser itself!
        """
        super().prune(assays=assays, results=results, normalisers=normalisers, figures=figures, plotters=plotters, filters=filters)

        try:
            self._Normaliser.prune(assays=assays, normalisers=normalisers, results=results)
        except Exception as e:
            logger.debug(e)

        if cores:
            self._Reader = None
            self._Analyser = None
            self._Normaliser = None
        else:
            if reader:
                self._Reader = None
            if analyser:
                self._Analyser = None
            if normaliser:
                self._Normaliser = None

    def Reader(self, Reader=None):
        """
        Links one of the `qpcr.Readers` to the pipeline.

        Parameters
        ----------
        Reader
            A `qpcr.Readers` object
        """
        if Reader is not None:
            self._Reader = Reader
        return self._Reader

    def Analyser(self, Analyser: qpcr.Analyser = None):
        """
        Links a `qpcr.Analyser` object to the pipeline.

        Parameters
        ----------
        Analyser : qpcr.Analyser
            A `qpcr.Analyser` object
        """
        if Analyser is not None:
            self._Analyser = Analyser
        return self._Analyser

    def Normaliser(self, Normaliser: qpcr.Normaliser = None):
        """
        Links a `qpcr.Normaliser` object to the pipeline.

        Parameters
        ----------
        Normaliser : qpcr.Normaliser
            A `qpcr.Normaliser` object
        """
        if Normaliser is not None:
            self._Normaliser = Normaliser
        return self._Normaliser

    def _setup_cores(self):
        """
        Sets Reader, Analyser, and Normaliser to defaults, if no external ones were provided...
        """
        if self.Reader() is None:
            self.Reader(qpcr.DataReader())
        if self.Analyser() is None:
            self.Analyser(qpcr.Analyser())
        if self.Normaliser() is None:
            self.Normaliser(qpcr.Normaliser())

    def _run(self, **kwargs):
        """
        The automated standard DeltaDeltaCt pipeline.
        Also produces applies and Plotters to produce figures...
        """
        # setup Analyser, and Normaliser (if none were provided)
        self._setup_cores()

        reader = self.Reader()
        analyser = self.Analyser()
        normaliser = self.Normaliser()

        # check if we have plotters or filters
        have_plotters = len(self._Plotters) != 0
        have_filters = len(self._Filters) != 0

        # add replicate and names info to kwargs
        kwargs = dict(kwargs, replicates=self._replicates, names=self._names)

        # analyse normalisers
        normalisers = [reader.read(i, **kwargs) for i in self._Normalisers]

        if have_filters:
            for filter in self._Filters:
                normalisers = [filter.pipe(i) for i in normalisers]

        normalisers = [analyser.pipe(i) for i in normalisers]

        # analyse sample assays
        assays = [reader.read(i, **kwargs) for i in self._Assays]

        if have_filters:
            for filter in self._Filters:
                assays = [filter.pipe(i) for i in assays]

        assays = [analyser.pipe(i) for i in assays]

        # now normalise
        normaliser.link(assays=assays, normalisers=normalisers)
        normaliser.normalise()

        # get and store results
        results = normaliser.get()
        self._Results = results
        self._df = results.get()
        self._stats_df = results.stats()

        if self._save_to is not None:
            results.save(self._save_to)

        # plot filtering report
        if have_filters:
            for filter in self._Filters:
                if self._save_to is not None or filter.report() is not None:

                    # add report location if none was specified...
                    if filter.report() is None:
                        filter.report(self._save_to)

                # save filter summary fig
                fig = filter.plot()
                self._Figures.append(fig)

        # plot results
        if have_plotters:
            for plotter in self._Plotters:

                # generate plotter fig and save
                plotter.link(self._Results)
                fig = plotter.plot()
                self._Figures.append(fig)

                if self._save_to is not None:
                    filename = self._make_figure_filename(plotter)
                    plotter.save(filename)

    def _make_figure_filename(self, plotter):
        """
        Increments a filename with a numeric counter...
        """
        num = 1
        suffix = plotter.suffix()
        while True:
            filename = os.path.join(self._save_to, f"{plotter.id()}_{num}.{suffix}")
            if not os.path.exists(filename):
                break
            num += 1
        return filename


class _Qupid_Blueprint(Blueprint):
    """
    This is the implementation of the Plueprint pipeline for the Qupid webapp.
    It redefines the file-reading functions with methods compatible with the UploadedFile objects from streamlit.
    """

    def __init__(self):
        super().__init__()

    def _setup_cores(self):
        """
        Sets Reader, Analyser, and Normaliser to defaults, if no external ones were provided...
        """
        if self.Reader() is None:
            self.Reader(qpcr._Qupid_SampleReader())
        if self.Analyser() is None:
            self.Analyser(qpcr.Analyser())
        if self.Normaliser() is None:
            self.Normaliser(qpcr.Normaliser())


# We push this one behind in the release...
# Since we now have the ddCt pipeline we should be able to slim down most of the pipelines here anyway...

# class MultiAssay(Blueprint):
#     """
#     Performs Delta-Delta-Ct based on data from a single multi-assay datafile.
#     Datasets within this datafile must be decorated to identify them as assays-of-interest or normalisers.
#     Check out the documentation of `qpcr.Parsers` for more information on decorators.

#     Note
#     -------
#     This class relies on `qpcr.Parsers` and decorated assays to get its input data.
#     If your file does not offer this kind of architecture, choose another pipeline.

#     """
#     def __init__(self):
#         super().__init__()
#         self._src = None

#     def link(self, filename : str, **kwargs):
#         """
#         Reads a datafile in csv or excel format containing
#         multiple decorated datasets and extracts sample and normalisers assays.

#         Parameters
#         ----------
#         filename : str
#             A filepath to a raw data file.
#         **kwargs
#             Any additional keyword arguments that shall be passed to the `qpcr.MultiReader`'s `pipe` method.
#         """
#         self._src = filename

#         # setup Reader, Analyser, and Normaliser (if none were provided)
#         self._setup_cores(**kwargs)
#         reader = self.Reader()

#         # read the multi-assay datafile
#         self._Assays, self._Normalisers = reader.pipe(self._src, **kwargs)


#     def _run(self, **kwargs):
#         """
#         The main workflow
#         """
#         # setup Reader, Analyser, and Normaliser (if none were provided)
#         self._setup_cores()

#         analyser = self.Analyser()
#         normaliser = self.Normaliser()

#         normalisers = []
#         samples = []

#         # analyse normalisers:
#         for norm in self._Normalisers:
#             for filter in self._Filters:
#                 norm = filter.pipe(norm)
#             norm = analyser.pipe(norm)
#             normalisers.append(norm)
#         normaliser.link(normalisers = normalisers)

#         # analyse sample assays
#         for sample in self._Assays:
#             for filter in self._Filters:
#                 sample = filter.pipe(sample)
#             sample = analyser.pipe(sample)
#             samples.append(sample)
#         normaliser.link(assays = samples)

#         normaliser.normalise()
#         results = normaliser.get()

#         self._Results = results
#         self._df = results.get()
#         self._stats_df = results.stats()

#         if self._save_to is not None:
#             results.save(self._save_to)

#         # plot filtering report
#         for filter in self._Filters:
#             if self._save_to is not None or filter.report() is not None:
#                 # add report location if none was specified...
#                 if filter.report() is None:
#                     filter.report(self._save_to)

#             figs = filter.plot()
#             self._Figures.extend(figs)

#         # plot results
#         for plotter in self._Plotters:
#             plotter.link(self._Results)
#             fig = plotter.plot()
#             self._Figures.append(fig)

#             if self._save_to is not None:
#                 filename = self._make_figure_filename(plotter)
#                 plotter.save(filename)

#     def _setup_cores(self, **kwargs):
#         """
#         Sets Reader, Analyser, and Normaliser to defaults, if no external ones were provided...
#         """
#         # check if a sheet_name was specified in case of multi-sheet files...
#         use_multi_sheet = "sheet_name" not in kwargs
#         if self.Reader() is None:
#             if self._is_multisheet() and use_multi_sheet:
#                 self.Reader(Readers.MultiSheetReader())
#             else:
#                 self.Reader(Readers.MultiReader())
#         if self.Analyser() is None:
#             self.Analyser(qpcr.Analyser())
#         if self.Normaliser() is None:
#             self.Normaliser(qpcr.Normaliser())

#     def _is_multisheet(self):
#         """
#         Checks if a provided excel file contains multiple sheets
#         """
#         verdict = False
#         if self._src.endswith("xlsx"):
#             data = pd.read_excel(self._src, sheet_name = None)
#             verdict = len(data.keys()) > 1
#         return verdict

#     # add_assays is disabled
#     def add_assays(self):
#         print("To provide a data input file use link()!\nIf you wish to supply separate files for assays-of-interest and normalisers, checkout another Pipeline as this one only works with a single Mlit-Assay file!")

#     # add_normalisers is disabled
#     def add_assays(self):
#         print("To provide a data input file use link()!\nIf you wish to supply separate files for assays-of-interest and normalisers, checkout another Pipeline as this one only works with a single Mlit-Assay file!")


class ddCt(Blueprint):
    """
    Performs only Delta-Delta-Ct and requires `qpcr.Assay` objects as inputs.
    Hence, this pipeline does NOT read any files!

    It follows the default workflow of the `BasicPlus`pipeline and is based on the `BluePrint`
    pipeline to allow customisation.

    Note
    ----
    As the pipeline inherits from the `Blueprint` pipeline it does have a `Reader` method (which won't do anything though!).
    """

    def __init__(self):
        super().__init__()

    def _setup_cores(self):
        """
        Sets Analyser, and Normaliser to defaults, if no external ones were provided...
        """
        if self.Analyser() is None:
            self.Analyser(qpcr.Analyser())
        if self.Normaliser() is None:
            self.Normaliser(qpcr.Normaliser())

    def _run(self, **kwargs):
        # setup Analyser, and Normaliser (if none were provided)
        self._setup_cores()

        analyser = self.Analyser()
        normaliser = self.Normaliser()

        # check if we have plotters or filters
        have_plotters = len(self._Plotters) != 0
        have_filters = len(self._Filters) != 0

        # analyse normalisers
        normalisers = [i for i in self._Normalisers]

        if have_filters:
            for filter in self._Filters:
                normalisers = [filter.pipe(i) for i in normalisers]

        normalisers = [analyser.pipe(i) for i in normalisers]

        # analyse sample assays
        assays = [i for i in self._Assays]

        if have_filters:
            for filter in self._Filters:
                assays = [filter.pipe(i) for i in assays]

        assays = [analyser.pipe(i) for i in assays]

        # now normalise
        normaliser.link(assays=assays, normalisers=normalisers)
        normaliser.normalise()

        # get and store results
        results = normaliser.get()
        self._Results = results
        self._df = results.get()
        self._stats_df = results.stats()

        if self._save_to is not None:
            results.save(self._save_to)

        # plot filtering report
        if have_filters:
            for filter in self._Filters:
                if self._save_to is not None or filter.report() is not None:

                    # add report location if none was specified...
                    if filter.report() is None:
                        filter.report(self._save_to)

                # save filter summary fig
                fig = filter.plot()
                self._Figures.append(fig)

        # plot results
        if have_plotters:
            for plotter in self._Plotters:

                # generate plotter fig and save
                plotter.link(self._Results)
                fig = plotter.plot()
                self._Figures.append(fig)

                if self._save_to is not None:
                    filename = self._make_figure_filename(plotter)
                    plotter.save(filename)


if __name__ == "__main__":

    norm_files = ["./Examples/Example Data/28S.csv", "./Examples/Example Data/actin.csv"]
    sample_files = ["./Examples/Example Data/HNRNPL_nmd.csv", "./Examples/Example Data/HNRNPL_prot.csv"]

    norm_folder = "./Example Data 3/normalisers/"
    sample_folder = "./Example Data 3/samples/"

    groupnames = ["wt-", "wt+", "ko-", "ko+"]

    analysis = Blueprint()

    # grouped_analyser = qpcr.Analyser()
    # grouped_analyser.anchor("first")
    # analysis.Analyser(grouped_analyser)

    # analysis.save_to("Example Data 2")
    # analysis.add_assays(sample_files) # alternative: link() for iteratively linking new assays...
    # analysis.add_normalisers(norm_files)

    analysis.add_assays(sample_folder)
    analysis.add_normalisers(norm_folder)

    # print("No Reps specified, all inferred!")
    analysis.replicates(6)
    analysis.names(groupnames)

    iqr_filter = Filters.RangeFilter()
    iqr_filter.plotmode("interactive")

    iqr_filter.report("Example Data 4")
    analysis.add_filters(iqr_filter)

    preview = Plotters.PreviewResults(mode="static")
    analysis.add_plotters(preview)

    # now that pipeline is ready, we can run!
    analysis.run()

    # now we can get results!
    results = analysis.get(kind="df")
    print(results)
    # norm_folder = "Example Data 3/normalisers"
    # exp_folder = "Example Data 3/experimental/samples"
    # ctr_folder = "Example Data 3/experimental/samples"

    # groupnames = ["wt-", "wt+", "ko-", "ko+"]

    # analysis = BasicPlus(index = "Example Data 2/index.csv")
    # analysis.save_to("Example Data ")

    # print(analysis._index)
    # # analysis.set_experimental(assays = exp_folder, normalisers = norm_folder)
    # # analysis.set_control(assays = ctr_folder, normalisers = norm_folder)

    # # analysis.replicates(6)
    # # analysis.names(groupnames)

    # iqr_filter = Filters.RangeFilter()
    # # iqr_filter.report("Example Data 2")
    # analysis.add_filters(iqr_filter)

    # # preview = Plotters.PreviewResults(mode = "interactive")
    # # analysis.add_plotters(preview)

    # # # now that pipeline is ready, we can run!
    # # analysis.run()

    # # # now we can get results!
    # # results = analysis.get(kind="df")
    # # print(results)

    # # exit(0)
