from __future__ import absolute_import, division, print_function

"""
Author      : Lyubimov, A.Y.
Created     : 04/14/2014
Last Changed: 11/25/2019
Description : IOTA GUI Threads and PostEvents
"""

import os
import wx
import shutil
from threading import Thread

from libtbx.easy_mp import parallel_map
from libtbx import easy_pickle as ep
from libtbx import easy_run

from dxtbx.model.experiment_list import ExperimentListFactory
import multiprocessing

import subprocess
import sys
import signal

from xfel.clustering.cluster import Cluster
from cctbx import crystal
from cctbx.uctbx import unit_cell
from cctbx.sgtbx import lattice_symmetry

from iota.components.iota_utils import InputFinder, Capturing, IOTATermination
from iota.components.iota_analysis import Analyzer

ginp = InputFinder()

# for testing
import time

assert time

# -------------------------------- Processing -------------------------------- #

# Set up events for finishing one cycle and for finishing all cycles
tp_EVT_ALLDONE = wx.NewEventType()
EVT_ALLDONE = wx.PyEventBinder(tp_EVT_ALLDONE, 1)

tp_EVT_IMGDONE = wx.NewEventType()
EVT_IMGDONE = wx.PyEventBinder(tp_EVT_IMGDONE, 1)

tp_EVT_OBJDONE = wx.NewEventType()
EVT_OBJDONE = wx.PyEventBinder(tp_EVT_OBJDONE, 1)


class ImageFinderAllDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, input_list=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.input_list = input_list

    def GetValue(self):
        return self.input_list


class ObjectFinderAllDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, obj_list=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.obj_list = obj_list

    def GetValue(self):
        return self.obj_list


class AllDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, img_objects=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.image_objects = img_objects

    def GetValue(self):
        return self.image_objects


class JobSubmitThread(Thread):
    """Thread for easy_run submissions so that they don't block GUI."""

    def __init__(self, parent, params, out_type="gui_silent"):
        Thread.__init__(self)
        self.parent = parent
        self.params = params
        self.job_id = None

        # Enforce silent run if on mp queue (unless need debug statements)
        if self.params.mp.method != "multiprocessing" and out_type != "gui_debug":
            self.out_type = "gui_silent"
        else:
            self.out_type = out_type

    def submit(self):
        run_path = self.parent.info.int_base
        iota_cmd = "iota.run --run_path {} -o {}".format(run_path, self.out_type)
        if self.params.mp.submit_command:
            command = self.params.mp.submit_command.replace("<iota_command>", iota_cmd)
        else:
            command = iota_cmd

        if command is not None:
            print(command)
            self.job = CustomRun(command=str(command), join_stdout_stderr=True)
            self.job.run()
            self.job.show_stdout()
            if self.job_id is not None:
                print("JOB NAME = ", self.job_id)
            return
        else:
            print("IOTA ERROR: COMMAND NOT ISSUED!")
            return

    def abort(self):
        if self.params.mp.kill_command:
            CustomRun(command=self.params.mp.kill_command)
        else:
            try:
                self.job.kill_thread()
            except Exception as e:
                print("IOTA JOB ERROR: Cannot kill job thread! {}".format(e))

    def run(self):
        return self.submit()


class ObjectReader:
    def __init__(self):
        pass

    def update_info(self, info):
        # # Determine chunk of image objects to check for existing
        # if self.n_proc:
        #   chunk = self.n_proc
        # else:
        #   chunk = 25
        # if len(self.info.unread_files) < chunk:
        #   chunk = len(self.info.unread_files)
        #
        # # Create filelist of written image objects
        # filelist = []
        # for fp in self.info.unread_files[:chunk]:
        #   if os.path.isfile(fp):
        #     filelist.append(fp)
        #     self.info.unread_files.pop(fp)
        #
        # # Perform stat extraction
        # if filelist:
        #   from iota.components.iota_analysis import Analyzer
        #   analyzer = Analyzer(info=self.info)
        #   stats_OK = analyzer.get_results(filelist=filelist)
        #   if stats_OK:
        #     self.info = analyzer.info
        #     self.obs = analyzer.obs

        finished_objects = info.get_finished_objects_from_file()
        if finished_objects:
            analyzer = Analyzer(info=info, gui_mode=True)
            stats_OK = analyzer.run_get_results(finished_objects=finished_objects)
            if stats_OK:
                return analyzer.info
        return None

    def run(self, info):
        return self.update_info(info)


class ObjectReaderThread(Thread):
    """Thread for reading processed objects and making all calculations for
    plotting in main GUI thread."""

    def __init__(self, parent, info=None):
        Thread.__init__(self, name="object_reader")
        self.parent = parent
        self.info = info
        self.obj_worker = ObjectReader()

    def run(self):
        info = self.obj_worker.run(self.info)
        if info:
            info.export_json()
            evt = ObjectFinderAllDone(tp_EVT_OBJDONE, -1, obj_list=info)
            wx.PostEvent(self.parent, evt)


class ImageFinderThread(Thread):
    """Worker thread generated to poll filesystem on timer.

    Will check to see if any new images have been found. Put on a thread
    to run in background
    """

    def __init__(
        self,
        parent,
        input,
        input_list,
        min_back=None,
        last_file=None,
        back_to_thread=False,
    ):
        Thread.__init__(self)
        self.parent = parent
        self.input = input
        self.min_back = min_back
        self.last_file = last_file
        self.back_to_thread = back_to_thread

        # Generate comparable input list
        self.input_list = []
        for item in input_list:
            if isinstance(item, list) or isinstance(item, tuple):
                self.input_list.append((item[1], item[2]))
            else:
                self.input_list.append(item)

    def run(self):
        # Poll filesystem and determine which files are new (if any)

        ext_file_list, _ = ginp.make_input_list(
            self.input,
            filter_results=True,
            filter_type="image",
            min_back=self.min_back,
            last=self.last_file,
            expand_multiple=True,
        )

        new_input_list = list(set(ext_file_list) - set(self.input_list))
        new_input_list = sorted(new_input_list)

        if self.back_to_thread:
            wx.CallAfter(self.parent.onImageFinderDone, new_input_list)
        else:
            evt = ImageFinderAllDone(tp_EVT_IMGDONE, -1, input_list=new_input_list)
            wx.PostEvent(self.parent, evt)


class ObjectFinderThread(Thread):
    """Worker thread that polls filesystem on timer for image objects.

    Will collect and extract info on images processed so far
    """

    def __init__(self, parent, object_folder, last_object=None, new_fin_base=None):
        Thread.__init__(self)
        self.parent = parent
        self.object_folder = object_folder
        self.new_fin_base = new_fin_base
        self.last_object = last_object

    def run(self):
        if self.last_object is not None:
            last = self.last_object.obj_file
        else:
            last = None
        object_files, _ = ginp.get_input_from_folder(
            self.object_folder, ext_only="int", last=last
        )
        new_objects = [self.read_object_file(i) for i in object_files]
        new_finished_objects = [i for i in new_objects if i is not None]

        evt = ObjectFinderAllDone(tp_EVT_OBJDONE, -1, obj_list=new_finished_objects)
        wx.PostEvent(self.parent, evt)

    def read_object_file(self, filepath):
        try:
            object = ep.load(filepath)
            return object
        except EOFError as e:
            print("OBJECT_IMPORT_ERROR: ", e)
            return None


class ImageViewerThread(Thread):
    """Worker thread that will move the image viewer launch away from the GUI
    and hopefully will prevent the image selection dialog freezing on MacOS."""

    def __init__(
        self, parent, file_string=None, viewer="dials.image_viewer", img_type=None
    ):
        Thread.__init__(self)
        self.parent = parent
        self.file_string = file_string
        self.viewer = viewer
        self.img_type = img_type
        self.options = (
            "show_ctr_mass=False "
            "show_max_pix=False "
            "show_all_pix=False "
            "show_predictions=False "
            "show_basis_vectors=False"
        )

    def run(self):
        command = "{} {} {}".format(self.viewer, self.file_string, self.options)
        easy_run.fully_buffered(command)


# ---------------------------------- PRIME ----------------------------------- #

tp_EVT_PRIMEDONE = wx.NewEventType()
EVT_PRIMEDONE = wx.PyEventBinder(tp_EVT_PRIMEDONE, 1)


class PRIMEAllDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, info=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.info = info

    def GetValue(self):
        return self.info


class PRIMEThread(Thread):
    """Thread for running all PRIME calculations; will prepare info for
    plotting in main GUI thread."""

    def __init__(self, parent, info, params, best_pg=None, best_uc=None):
        Thread.__init__(self, name="live_prime")
        self.parent = parent
        self.params = params
        self.info = info
        self.best_pg = best_pg
        self.best_uc = best_uc

    def prepare_PRIME_input(self):
        """Prepare the list of integrated pickles as well as pertinent
        parameters; create a PRIME input file."""

        # Check if any pg/uc information is available; pg/uc info from UI
        # supercedes pg/uc from Cluster

        if self.best_pg is None:
            self.best_pg = self.info.best_pg

        if self.best_uc is None:
            self.best_uc = self.info.best_uc
        else:
            uc_params = str(self.best_uc).rsplit()
            self.best_uc = [float(i) for i in uc_params]

        # Only run PRIME if both pg and uc are provided
        if self.best_pg and self.best_uc:
            from iota.components.iota_analysis import Analyzer

            # Create a file with list of integrated pickles (overwrite old file)
            int_pickles_file = os.path.join(self.info.int_base, "int_pickles.lst")
            with open(int_pickles_file, "w") as ff:
                ff.write("\n".join(self.info.categories["integrated"][0]))

            # Create PRIME input file
            analyzer = Analyzer(info=self.info, params=self.params)
            analyzer.prime_data_path = int_pickles_file
            analyzer.best_pg = self.best_pg
            analyzer.best_uc = self.best_uc

            prime_phil = analyzer.make_prime_input(
                filename="live_prime.phil", run_zero=True
            )
            self.pparams = prime_phil.extract()

            # Modify specific options based in IOTA settings
            # Queue options
            if self.params.mp.method == "lsf" and self.params.mp.queue is not None:
                self.pparams.queue.mode = "bsub"
                self.pparams.queue.qname = self.params.mp.queue

            # Number of processors (automatically, 1/2 of IOTA procs)
            self.pparams.n_processors = int(self.params.mp.n_processors / 2)

            # Generate command args
            cmd_args_list = [
                "n_postref_cycle=0",
                "queue.mode={}".format(self.pparams.queue.mode),
                "queue.qname={}".format(self.pparams.queue.qname),
                "n_processors={}".format(self.pparams.n_processors),
            ]
            if self.pparams.queue.mode == "bsub":
                cmd_args_list.append("timeout_seconds=120")
            cmd_args = " ".join(cmd_args_list)

            return cmd_args
        else:
            return None

    def get_prime_stats(self):
        stats_folder = os.path.join(self.pparams.run_no, "stats")
        prime_info = None
        if os.path.isdir(stats_folder):
            stat_files = [
                os.path.join(stats_folder, i)
                for i in os.listdir(stats_folder)
                if i.endswith("stat")
            ]
            if stat_files:
                assert len(stat_files) == 1
                stat_file = stat_files[0]
                if os.path.isfile(stat_file):
                    prime_info = ep.load(stat_file)
                    live_prime_info_file = os.path.join(
                        self.info.int_base, "life_prime_info.pickle"
                    )
                    shutil.copyfile(stat_file, live_prime_info_file)

        # Convert space_group_info object to str, to make compatible with JSON
        if prime_info and "space_group_info" in prime_info:
            for i in prime_info["space_group_info"]:
                sg = str(i).split("(")[0].rstrip(" ")
                idx = prime_info["space_group_info"].index(i)
                prime_info["space_group_info"][idx] = sg

        return prime_info

    def abort(self):
        # TODO: put in an LSF kill command
        if hasattr(self, "job"):
            try:
                self.job.kill_thread()
            except Exception as e:
                print("PRIME THREAD ERROR: Cannot terminate thread! {}".format(e))

    def run(self):
        # Generate PRIME input
        cmd_args = self.prepare_PRIME_input()

        # Run PRIME
        if cmd_args:
            # remove previous run to avoid conflict
            prime_dir = os.path.join(self.info.int_base, "prime/000")
            if os.path.isdir(prime_dir):
                shutil.rmtree(prime_dir)

            # Launch PRIME
            prime_file = os.path.join(self.info.int_base, "live_prime.phil")
            cmd = "prime.run {} {}".format(prime_file, cmd_args)

            try:
                # easy_run.fully_buffered(cmd, join_stdout_stderr=True).show_stdout()
                self.job = CustomRun(command=cmd, join_stdout_stderr=True)
                self.job.run()
                prime_info = self.get_prime_stats()
            except Exception as e:
                print("LIVE PRIME ERROR: ", e)
                prime_info = None

        else:
            prime_info = None

        # Send signal to UI
        evt = PRIMEAllDone(tp_EVT_PRIMEDONE, -1, info=prime_info)
        wx.PostEvent(self.parent, evt)


# ------------------------------ IMAGE TRACKING ------------------------------ #

tp_EVT_SPFDONE = wx.NewEventType()
EVT_SPFDONE = wx.PyEventBinder(tp_EVT_SPFDONE, 1)

tp_EVT_SPFALLDONE = wx.NewEventType()
EVT_SPFALLDONE = wx.PyEventBinder(tp_EVT_SPFALLDONE, 1)

tp_EVT_SLICEDONE = wx.NewEventType()
EVT_SLICEDONE = wx.PyEventBinder(tp_EVT_SLICEDONE)

tp_EVT_SPFTERM = wx.NewEventType()
EVT_SPFTERM = wx.PyEventBinder(tp_EVT_SPFTERM)


class SpotFinderAllDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, info=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.info = info

    def GetValue(self):
        return self.info


class SpotFinderOneDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, info=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.info = info

    def GetValue(self):
        return self.info


class SpotFinderTerminated(wx.PyCommandEvent):
    """Send event when spotfinder terminated."""

    def __init__(self, etype, eid):
        wx.PyCommandEvent.__init__(self, etype, eid)

    def GetValue(self):
        return None


class SpotFinderDIALSThread:
    def __init__(
        self, parent, processor, term_file, run_indexing=False, run_integration=False
    ):
        self.meta_parent = parent.parent
        self.processor = processor
        self.term_file = term_file
        self.run_indexing = run_indexing
        self.run_integration = run_integration

    def run(self, idx, img):
        if self.meta_parent.terminated:
            raise IOTATermination("IOTA_TRACKER: SPF Termination signal received!")
        else:
            with Capturing() as junk_output:
                fail = False
                sg = None
                uc = None
                try:
                    experiments = ExperimentListFactory.from_filenames([img])[0]
                    observed = self.processor.find_spots(experiments=experiments)
                except Exception:
                    fail = True
                    observed = []
                    pass

                # TODO: Indexing / lattice determination very slow (how to speed up?)
                if self.run_indexing:
                    if not fail:
                        try:
                            experiments, indexed = self.processor.index(
                                experiments=experiments, reflections=observed
                            )
                        except Exception:
                            fail = True
                            pass

                    if not fail:
                        try:
                            solution = self.processor.refine_bravais_settings(
                                reflections=indexed, experiments=experiments
                            )

                            # Only reindex if higher-symmetry solution found
                            if solution is not None:
                                experiments, indexed = self.processor.reindex(
                                    reflections=indexed,
                                    experiments=experiments,
                                    solution=solution,
                                )
                            lat = experiments[0].crystal.get_space_group().info()
                            sg = str(lat).replace(" ", "")
                        except Exception:
                            fail = True
                            pass

                    if not fail:
                        unit_cell = experiments[0].crystal.get_unit_cell().parameters()
                        uc = " ".join(["{:.4f}".format(i) for i in unit_cell])

                    if self.run_integration:
                        if not fail:
                            try:
                                # Run refinement
                                experiments, indexed = self.processor.refine(
                                    experiments=experiments, centroids=indexed
                                )
                            except Exception:
                                fail = True
                                pass

                        if not fail:
                            try:
                                print(experiments)
                                print(indexed)
                                integrated = self.processor.integrate(
                                    experiments=experiments, indexed=indexed
                                )
                            except Exception:
                                pass

            return [idx, int(len(observed)), img, sg, uc]


class SpotFinderMosflmThread:
    def __init__(self, parent, term_file):
        self.meta_parent = parent.parent
        self.term_file = term_file

    def run(self, idx, img):
        if os.path.isfile(self.term_file):
            raise IOTATermination("IOTA_TRACKER: Termination signal received!")
        else:
            # First, parse filepath to create Mosflm template
            directory = os.path.dirname(img)
            filepath = os.path.basename(img).split(".")
            fname = filepath[0]
            extension = filepath[1]
            if "_" in fname:
                suffix = fname.split("_")[-1]
            elif "-" in fname:
                suffix = fname.split("-")[-1]
            elif "." in fname:
                suffix = fname.split(".")[-1]
            else:
                suffix = fname
            img_number = int("".join(n if n.isdigit() else "" for n in suffix))
            prefix = fname.replace(suffix, "")
            n_suffix = "".join("#" if c.isdigit() else c for c in suffix)
            template = "{}{}.{}".format(prefix, n_suffix, extension)

            # Create autoindex.com w/ Mosflm script
            # Write to temporary file and change permissions to run
            autoindex = [
                "#! /bin/tcsh -fe",
                "ipmosflm << eof-ipmosflm".format(fname),
                "NEWMATRIX {0}.mat".format(fname),
                "DIRECTORY {}".format(directory),
                "TEMPLATE {}".format(template),
                "AUTOINDEX DPS THRESH 0.1 IMAGE {} PHI 0 0.01".format(img_number),
                "GO",
                "eof-ipmosflm",
            ]
            autoindex_string = "\n".join(autoindex)
            autoindex_filename = "autoindex_{}.com".format(idx)

            with open(autoindex_filename, "w") as af:
                af.write(autoindex_string)
            os.chmod(autoindex_filename, 0o755)

            # Run Mosflm autoindexing
            command = "./{}".format(autoindex_filename)
            out = easy_run.fully_buffered(command, join_stdout_stderr=True)

            # Scrub text output
            final_spots = [
                l for l in out.stdout_lines if "spots written for image" in l
            ]
            final_cell_line = [l for l in out.stdout_lines if "Final cell" in l]
            final_sg_line = [l for l in out.stdout_lines if "space group" in l]

            if final_spots:
                spots = final_spots[0].rsplit()[0]
            else:
                spots = 0
            if final_cell_line:
                cell = (
                    final_cell_line[0]
                    .replace("Final cell (after refinement) is", "")
                    .rsplit()
                )
            else:
                cell = None
            if final_sg_line:
                sg = final_sg_line[0].rsplit()[6]
            else:
                sg = None

            # Temp file cleanup
            try:
                os.remove("{}.mat".format(fname))
            except Exception:
                pass
            try:
                os.remove("{}.spt".format(prefix[:-1]))
            except Exception:
                pass
            try:
                os.remove("SUMMARY")
            except Exception:
                pass
            try:
                os.remove(autoindex_filename)
            except Exception:
                pass

            return [idx, spots, img, sg, cell]


class SpotFinderThread(Thread):
    """Basic spotfinder (with defaults) that could be used to rapidly analyze
    images as they are collected."""

    def __init__(
        self,
        parent,
        data_list=None,
        term_file=None,
        proc_params=None,
        backend="dials",
        n_proc=0,
        run_indexing=False,
        run_integration=False,
    ):
        Thread.__init__(self)
        self.parent = parent
        self.data_list = data_list
        self.term_file = term_file
        self.terminated = False
        self.backend = backend
        self.run_indexing = run_indexing
        self.run_integration = run_integration
        if n_proc > 0:
            self.n_proc = n_proc
        else:
            self.n_proc = multiprocessing.cpu_count() - 2

        if self.backend == "dials":
            # Modify default DIALS parameters
            # These parameters will be set no matter what
            proc_params.output.experiments_filename = None
            proc_params.output.indexed_filename = None
            proc_params.output.strong_filename = None
            proc_params.output.refined_experiments_filename = None
            proc_params.output.integrated_filename = None
            proc_params.output.integrated_experiments_filename = None
            proc_params.output.profile_filename = None
            proc_params.output.integration_pickle = None

            from iota.components.iota_processing import IOTAImageProcessor

            self.processor = IOTAImageProcessor(phil=proc_params)

    def run(self):
        try:
            parallel_map(
                iterable=self.data_list,
                func=self.spf_wrapper,
                callback=self.callback,
                processes=self.n_proc,
            )
        except IOTATermination as e:
            self.terminated = True
            print(e)

        # Signal that this batch is finished
        try:
            if self.terminated:
                print("RUN TERMINATED!")
                evt = SpotFinderTerminated(tp_EVT_SPFTERM, -1)
                wx.PostEvent(self.parent, evt)

            wx.CallAfter(self.parent.onSpfAllDone, self.data_list)

            # info = self.data_list
            # evt = SpotFinderAllDone(tp_EVT_SPFALLDONE, -1, info=info)
            # wx.PostEvent(self.parent, evt)
            return
        except TypeError as e:
            print(e)
            return

    def spf_wrapper(self, img):
        try:
            if os.path.isfile(img):
                if self.backend == "dials":
                    spf_worker = SpotFinderDIALSThread(
                        self,
                        processor=self.processor,
                        term_file=self.term_file,
                        run_indexing=self.run_indexing,
                        run_integration=self.run_integration,
                    )
                    result = spf_worker.run(idx=int(self.data_list.index(img)), img=img)
                elif self.backend == "mosflm":
                    spf_worker = SpotFinderMosflmThread(self, self.term_file)
                    result = spf_worker.run(idx=int(self.data_list.index(img)), img=img)
                else:
                    result = [int(self.data_list.index(img)), 0, img, None, None]
                return result
            else:
                return [int(self.data_list.index(img)), 0, img, None, None]
        except IOTATermination as e:
            raise e

    def callback(self, info):
        try:
            wx.CallAfter(self.parent.onSpfOneDone, info)
            # evt = SpotFinderOneDone(tp_EVT_SPFDONE, -1, info=info)
            # wx.PostEvent(self.parent.parent, evt)
        except TypeError:
            pass

    # def terminate_thread(self):
    #   raise IOTATermination('IOTA_TRACKER: SPF THREAD Terminated!')


class InterceptorFileThread(Thread):
    def __init__(self, parent, results_file, reorder=False):
        Thread.__init__(self)
        self.parent = parent
        self.results_file = results_file
        self.reorder = reorder

        self.bookmark = 0
        self.msg = ""
        self.spotfinding_info = []
        self.cluster_info = None

        self.prc_timer = wx.Timer()
        self.cls_timer = wx.Timer()

        # Bindings
        self.prc_timer.Bind(wx.EVT_TIMER, self.onProcTimer)
        self.cls_timer.Bind(wx.EVT_TIMER, self.onClusterTimer)

    def run(self):
        # self.timer.Start(1000)
        pass

    def onProcTimer(self, e):
        if os.path.isfile(self.results_file):
            with open(self.results_file, "r") as rf:
                rf.seek(self.bookmark)
                split_info = [i.replace("\n", "").split(" ") for i in rf.readlines()]
                self.bookmark = rf.tell()

            if self.reorder:
                idx_offset = len(self.spotfinding_info)
                new_info = [
                    [
                        split_info.index(i) + idx_offset,
                        int(i[1]),
                        i[2],
                        i[3],
                        tuple(i[4:10]),
                    ]
                    if len(i) > 5
                    else [
                        split_info.index(i) + idx_offset,
                        int(i[1]),
                        i[2],
                        util.makenone(i[3]),
                        util.makenone(i[4]),
                    ]
                    for i in split_info
                ]
            else:
                new_info = [
                    [int(i[0]), int(i[1]), i[2], i[3], tuple(i[4:10])]
                    if len(i) > 5
                    else [
                        int(i[0]),
                        int(i[1]),
                        i[2],
                        util.makenone(i[3]),
                        util.makenone(i[4]),
                    ]
                    for i in split_info
                ]

            if len(new_info) > 0:
                self.spotfinding_info.extend(new_info)

                if len(self.spotfinding_info) > 0:
                    self.msg = "Tracking new images in {} ...".format(self.results_file)
            else:
                self.msg = "Waiting for new images in {} ...".format(self.results_file)

        else:
            self.msg = "Waiting for new run to initiate..."

        info = [self.msg, self.spotfinding_info, self.cluster_info]
        evt = SpotFinderOneDone(tp_EVT_SPFDONE, -1, info=info)
        wx.PostEvent(self.parent, evt)

    def onClusterTimer(self, e):
        input = []
        for item in self.spotfinding_info:
            if item[4] is not None:
                try:
                    if type(item[4]) in (tuple, list):
                        uc = item[4]
                    else:
                        uc = item[4].rsplit()
                    info_line = [float(i) for i in uc]
                    info_line.append(item[3])
                    input.append(info_line)
                except ValueError as e:
                    print("CLUSTER ERROR: ", e)
                    pass

        if len(input) > 0:
            self.running_clustering = True
            cluster_thread = ClusterWorkThread(self)
            self.cluster_info = cluster_thread.run(iterable=input)

    def terminate_thread(self):
        raise IOTATermination("IOTA_TRACKER: Termination signal received!")


class InterceptorThread(Thread):
    """Thread for the full Interceptor image processing process; will also
    house the processing timer, which will update the UI front end and initiate
    plotting."""

    def __init__(
        self,
        parent,
        data_folder=None,
        term_file=None,
        proc_params=None,
        backend="dials",
        n_proc=0,
        min_back=None,
        run_indexing=False,
        run_integration=False,
    ):
        Thread.__init__(self)
        self.parent = parent
        self.data_folder = data_folder
        self.term_file = term_file
        self.terminated = False
        self.backend = backend
        self.run_indexing = run_indexing
        self.run_integration = run_integration
        self.min_back = min_back
        self.submit_new_images = True

        self.spotfinding_info = []
        self.cluster_info = None
        self.msg = None
        self.done_list = []
        self.data_list = []
        self.new_data = []

        self.spf_thread = None

        if n_proc > 0:
            self.n_proc = n_proc
        else:
            self.n_proc = multiprocessing.cpu_count() - 2

        if self.backend == "dials":
            # Modify default DIALS parameters
            # These parameters will be set no matter what
            proc_params.output.experiments_filename = None
            proc_params.output.indexed_filename = None
            proc_params.output.strong_filename = None
            proc_params.output.refined_experiments_filename = None
            proc_params.output.integrated_filename = None
            proc_params.output.integrated_experiments_filename = None
            proc_params.output.profile_filename = None
            proc_params.output.integration_pickle = None

            self.proc_params = proc_params

            # from iota.components.iota_dials import IOTADialsProcessor
            # self.processor = IOTADialsProcessor(params=proc_params)

        self.prc_timer = wx.Timer()
        self.cls_timer = wx.Timer()

        # Bindings
        self.prc_timer.Bind(wx.EVT_TIMER, self.onProcTimer)
        self.cls_timer.Bind(wx.EVT_TIMER, self.onClusterTimer)

    def run(self):
        pass

    def onProcTimer(self, e):
        """Main timer (1 sec) will send data to UI, find new images, and submit
        new processing run."""

        # Send current data to UI
        info = [self.msg, self.spotfinding_info, self.cluster_info]
        evt = SpotFinderOneDone(tp_EVT_SPFDONE, -1, info=info)
        wx.PostEvent(self.parent, evt)

        # Find new images
        if self.data_list:
            last_file = self.data_list[-1]
        else:
            last_file = None
        self.find_new_images(last_file=last_file, min_back=self.min_back)

        if self.spf_thread is not None:
            if not self.spf_thread.isAlive():
                self.submit_new_images = True

        # Submit new images (if found)
        if self.submit_new_images and len(self.new_data) > 0:
            self.submit_new_images = False
            self.run_processing()

    def onClusterTimer(self, e):
        input = []
        if len(self.spotfinding_info) > 0:
            for item in self.spotfinding_info:
                if item[4] is not None:
                    try:
                        if type(item[4]) in (tuple, list):
                            uc = item[4]
                        else:
                            uc = item[4].rsplit()
                        info_line = [float(i) for i in uc]
                        info_line.append(item[3])
                        input.append(info_line)
                    except ValueError as e:
                        print("CLUSTER ERROR: ", e)
                        pass

            if len(input) > 0:
                self.running_clustering = True
                cluster_thread = ClusterWorkThread(self)
                self.cluster_info = cluster_thread.run(iterable=input)

    def find_new_images(self, min_back=None, last_file=None):
        found_files, _ = ginp.make_input_list(
            [self.data_folder],
            filter_results=True,
            filter_type="image",
            last=last_file,
            min_back=min_back,
            expand_multiple=True,
        )

        # Sometimes duplicate files are found anyway; clean that up
        found_files = list(set(found_files) - set(self.data_list))

        # Add new files to the data list & clean up
        self.new_data.extend(found_files)
        self.new_data = sorted(self.new_data, key=lambda i: i)
        self.data_list.extend(self.new_data)

    def run_processing(self):
        self.spf_thread = SpotFinderThread(
            self,
            data_list=self.new_data,
            term_file=self.term_file,
            proc_params=self.proc_params,
            backend=self.backend,
            n_proc=self.n_proc,
            run_indexing=self.run_indexing,
            run_integration=self.run_integration,
        )
        self.new_data = []
        self.spf_thread.start()

    def onSpfOneDone(self, info):
        info[0] = int(info[0]) + len(self.done_list)
        self.spotfinding_info.append(info)

    def onSpfAllDone(self, done_list):
        self.done_list.extend(done_list)

    def terminate_thread(self):
        self.terminated = True


# ------------------------------ UC CLUSTERING ------------------------------- #

tp_EVT_CLUSTERDONE = wx.NewEventType()
EVT_CLUSTERDONE = wx.PyEventBinder(tp_EVT_CLUSTERDONE, 1)


class ClusteringDone(wx.PyCommandEvent):
    """Send event when finished all cycles."""

    def __init__(self, etype, eid, info=None):
        wx.PyCommandEvent.__init__(self, etype, eid)
        self.info = info

    def GetValue(self):
        return self.info


class ClusterWorkThread:
    def __init__(self, parent):
        self.parent = parent
        self.abort = False

    def run(self, iterable):

        # with Capturing() as junk_output:
        errors = []
        try:
            ucs = Cluster.from_iterable(iterable=iterable)
            clusters, _ = ucs.ab_cluster(
                5000, log=False, write_file_lists=False, schnell=True, doplot=False
            )
        except Exception as e:
            print("IOTA ERROR (CLUSTERING): ", e)
            clusters = []
            errors.append(str(e))

        info = []
        if clusters:
            for cluster in clusters:
                uc_init = unit_cell(cluster.medians)
                symmetry = crystal.symmetry(unit_cell=uc_init, space_group_symbol="P1")
                groups = lattice_symmetry.metric_subgroups(
                    input_symmetry=symmetry, max_delta=3
                )
                top_group = groups.result_groups[0]
                best_sg = str(groups.lattice_group_info()).split("(")[0]
                best_uc = top_group["best_subsym"].unit_cell().parameters()
                uc_no_stdev = (
                    "{:<6.2f} {:<6.2f} {:<6.2f} "
                    "{:<6.2f} {:<6.2f} {:<6.2f} "
                    "".format(
                        best_uc[0],
                        best_uc[1],
                        best_uc[2],
                        best_uc[3],
                        best_uc[4],
                        best_uc[5],
                    )
                )
                cluster_info = {
                    "number": len(cluster.members),
                    "pg": str(best_sg),
                    "uc": uc_no_stdev,
                }
                info.append(cluster_info)

        return info, errors


class ClusterThread(Thread):
    """Basic spotfinder (with defaults) that could be used to rapidly analyze
    images as they are collected."""

    def __init__(self, parent, iterable):
        Thread.__init__(self, name="live_cluster")
        self.parent = parent
        self.iterable = iterable
        self.clustering = ClusterWorkThread(self)

    def abort(self):
        self.clustering.abort = True

    def run(self):
        clusters, errors = self.clustering.run(iterable=self.iterable)

        if clusters:
            clusters = sorted(clusters, key=lambda i: i["number"], reverse=True)
        evt = SpotFinderOneDone(tp_EVT_CLUSTERDONE, -1, info=[clusters, errors])
        wx.PostEvent(self.parent, evt)


# ------------------------------- SUBPROCESS --------------------------------- #


class CustomRun(easy_run.fully_buffered_base):
    """A subclass from easy_run with a "kill switch" for easy process
    termination from UI; took out timeout, since that won't be used, and
    doesn't work on all systems, anyway.

    Tested on Mac OS X 10.13.6 and CentOS 6 so far.
    """

    def __init__(
        self,
        command,
        timeout=None,
        stdin_lines=None,
        join_stdout_stderr=False,
        stdout_splitlines=True,
        bufsize=-1,
    ):
        self.command = command
        self.join_stdout_stderr = join_stdout_stderr
        self.timeout = timeout
        self.stdin_lines = stdin_lines
        self.stdout_splitlines = stdout_splitlines
        self.bufsize = bufsize
        self.thread = None

    def target(self, process, lines, result):
        o, e = process.communicate(input=lines)
        result[0] = o
        result[1] = e

    def kill_thread(self):
        os.killpg(os.getpgid(self.p.pid), signal.SIGTERM)

    def run(self):
        if not isinstance(self.command, str):
            self.command = subprocess.list2cmdline(self.command)
        if sys.platform == "darwin":  # bypass SIP on OS X 10.11
            self.command = (
                "DYLD_LIBRARY_PATH=%s exec " % os.environ.get("DYLD_LIBRARY_PATH", "")
            ) + self.command
        if self.stdin_lines is not None:
            if not isinstance(self.stdin_lines, str):
                self.stdin_lines = os.linesep.join(self.stdin_lines)
                if len(self.stdin_lines) != 0:
                    self.stdin_lines += os.linesep
        if self.join_stdout_stderr:
            stderr = subprocess.STDOUT
        else:
            stderr = subprocess.PIPE

        self.p = subprocess.Popen(
            args=self.command,
            shell=True,
            bufsize=self.bufsize,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=stderr,
            universal_newlines=True,
            close_fds=(sys.platform != "win32"),
            preexec_fn=os.setsid if sys.platform != "win32" else None,
        )
        o, e = self.p.communicate(input=self.stdin_lines)
        if self.stdout_splitlines:
            self.stdout_buffer = None
            self.stdout_lines = o.splitlines()
        else:
            self.stdout_buffer = o
            self.stdout_lines = None
        if self.join_stdout_stderr:
            self.stderr_lines = []
        else:
            self.stderr_lines = e.splitlines()
        self.return_code = self.p.returncode
