from __future__ import division

hatches = { 'S': None, 'O': 'O', 'A': 'xx', 'F': '--', 'M': '*' }
hcycle = { 'S': 'O', 'O': 'A', 'A': 'F', 'F': 'M', 'M': 'S' }

def overlaps(order, apert):
    a = o = []
    if isinstance(apert, tuple): a = apert
    elif isinstance(apert, dict): a = apert['edges']
    if isinstance(order, tuple): o = order
    elif isinstance(order, dict): o = order['edges']
    return int(
        # left edge overlaps order by at least 1 pix
        (o[0] <= a[0] and a[0] < o[1]) or
        # right edge overlaps order by at least 1 pix
        (o[0] < a[1] and a[1] <= o[1]) or
        # aperture completely encompasses order
        (a[0] <= o[0] and o[1] <= a[1])
    )

def contains(span, pt):
    w = []
    if isinstance(span, tuple): w = span
    elif isinstance(span, dict): w = span['edges']
    return int(w[0] <= pt and pt <= w[1])

# import the needed modules
try:
    import re
    import math
    import reflex
    import pipeline_product
    import pipeline_display
    import reflex_plot_widgets as plot_widgets
    from matplotlib import gridspec, image, widgets, patches, pyplot
    from collections import OrderedDict
    #from pprint import pprint
    import_sucess = True

# NOTE for developers:
# -If you want to modify the current script to cope
#  with different parameters, this is the function to modify:
#  setInteractiveParameters()
# -If you want to modify the current script to read different data from
#  the input FITS, this is the function to modify:
#  readFitsData()                  (from class DataPlotterManager)
# -If you want to modify the current script to modify the plots (using the same
#  data),  this is the function to modify:
#  plotProductsGraphics()          (from class DataPlotterManager)
# -If you want to modify the text that appears in the "Help" button,
#  this is the function to modify:
#  setWindowHelp()
# -If you want to modify the tittle of the window, modify this function:
#  setWindowTitle()
# -Do not modify anything else apart from these methods.

# FIXME:
# Redesign the thing:
# A function init() that will be called with fitsFiles and sof (the information to be conveyed to the dataplotmanager
# and then the functions setInteractiveParameters readFitsData plotProductsGraphics setWindowHelp and setWindowHelp
# Maybe figure can also be passed to the init

    class NonInteractiveText():
        def __init__(self, axes):
            self.ax = axes
            self.ax.yaxis.set_major_locator(pyplot.NullLocator())
            self.ax.xaxis.set_major_locator(pyplot.NullLocator())
            self.old = None
        def setText(self, x, y, text, **kwargs):
            if self.old: self.old.remove()
            kwargs['transform'] = self.ax.transAxes
            kwargs['clip_on'] = True
            if 'color' not in kwargs: kwargs['color'] = 'red'
            self.old = self.ax.text(x, y, text, **kwargs)


    class NonInteractiveLegend():
        def __init__(self, axes, descript, hatch):
            self.ax = axes
            axes.spines['top'].set_visible(False)
            self.ax.yaxis.set_major_locator(pyplot.NullLocator())
            self.ax.xaxis.set_major_locator(pyplot.NullLocator())
            self.text = self.ax.text(0.5, 0.5, descript,
                    horizontalalignment='center', verticalalignment='center',
                    transform=self.ax.transAxes)
            self.rect = patches.Rectangle((0, 0), 1, 1, linewidth=0,
                    facecolor='none', hatch=hatches[hatch])
            self.ax.add_patch(self.rect)
            self.text.set_visible(False)


    class MyInteractiveSlider(plot_widgets.CallableWidget):
        def __init__(self, axes, callback, valmin, valmax, valinit, label="",
                fmt='%g', step=None):
            plot_widgets.CallableWidget.__init__(self)
            self.ax = axes
            self.slider = widgets.Slider(axes, label, valmin, valmax, valinit,
                                         dragging=True, valfmt=fmt)
            self.slider.on_changed(self.__callback)
            self.usercallback = callback

        def __callback(self, slider_pos):
            new_params = self.usercallback(slider_pos)
            self.postCallback(new_params)

        def resetMax(self, maxw):
            maxw = max(1, maxw)
            self.slider.valmax = maxw
            if maxw < self.slider.val: self.slider.set_val(maxw)
            self.ax.set_xlim(None, maxw)
            self.ax.figure.canvas.draw_idle()


    class InteractiveTextBox(plot_widgets.CallableWidget):
        def __init__(self, axes, callback, label, initial=None, title=None):
            plot_widgets.CallableWidget.__init__(self)
            self.widget = widgets.TextBox(axes, label, initial=initial)
            if title is not None:
                axes.set_title(title, fontsize=12, fontweight='semibold')
            self.widget.on_text_change(self.__callback) # or on_submit()
            self.usercallback = callback

        def __callback(self, label):
            new_params = self.usercallback(label)
            self.postCallback(new_params)


    class InteractiveButtons(plot_widgets.CallableWidget):
        def __init__(self, axes, callback, label, title = None):
            plot_widgets.CallableWidget.__init__(self)
            self.widget = widgets.Button(axes, label)
            if title is not None:
                axes.set_title(title, fontsize=12, fontweight='semibold')
            self.widget.on_clicked(self.__callback)
            self.usercallback = callback

        def __callback(self, label):
            new_params = self.usercallback(label)
            self.postCallback(new_params)


    # This class deals with the specific details of data reading and final
    # plotting.
    class DataPlotterManager:
        # This function will read all the columns, images and whatever is needed
        # from the products. The variables , self.plot_x, self.plot_y, etc...
        # are used later in function plotProductsGraphics().
        # Add/delete these variables as you need (only that plotProductsGraphics()
        # has to use the same names).
        # You can also create some additional variables (like statistics) after
        # reading the files.
        # If you use a control variable (self.xxx_found), you can modify
        # later on the layout of the plotting window based on the presence of
        # given input files.
        # sof contains all the set of frames
        # TODO: Add in_sop here, because it is needed to plot some of the
        # parameters
        
        def __init__(self):
            self.cmap = 'binary'
            self.layer = 0
            self.apwidth = 8
            self.del_mode = False
            self.tag_mode = False
            self.resize_mode = False
            self.redraw_orders = False
            self.axesPlot = False
            self.apfile = None
            self.activeOrd = None
            self.aps = []
            self.ords = []
            self.orderArts = []


        def readFitsData(self, fitsFiles):
            # Control variable to check if the interesting files where at the
            # input
            self.found_specim = False

            # Read all the products
            frames = dict()
            for frame in fitsFiles:
                if frame == '': continue
                category = frame.category
                frames[category] = frame

            for catg in frames:
                if not re.search('SPC_(OBS|PHOT)_.*TAB', catg): continue

                self.found_specim = True
                self.pp_specim = pipeline_product.PipelineProduct(frames[catg])
                self.pp_specim.readImage(fits_extension='IMG_COMBINED')
                self.imy = self.pp_specim.image.shape[0]
                self.imx = self.pp_specim.image.shape[1]
                self.ords = self.generateOrders()  # this must come before...
                self.aps = self.generateApertures()  # ...this
                self.maxw = self.imx if not self.ords else max(
                        [o['edges'][1] - o['edges'][0] for o in self.ords])

                #self.pp_specim.read2DLinearWCS(fits_extension=0)

                break


        def generateApertures(self):
            if self.apfile:
                file = open(self.apfile, 'r')
                lines = file.readlines()
            else:
                # try getting the aperture definitions from the header
                lines = self.pp_specim.all_hdu[0].header
                lines = [lines[kw] for kw in list(lines)
                         if kw.startswith('ESO DRS APDEF')]
                if not lines: return []

            bins = [0 for _ in self.ords]
            all_aps = []
            line_aps = []

            for line in lines:
                # first the main object aperture
                toks = line.split()
                meth = toks[0] if toks[0] == 'O' else toks[3]
                l = float(toks[1]) - 0.5
                r = float(toks[2]) + 0.5
                pt = (l + r) / 2
                wid = r - l
                ap = dict(method=meth, edges=(l,r), width=wid, center=pt)
                line_aps.append(ap)

                # now the sky apertures for non-optimal methods
                if toks[0] != 'O':
                    nskys = (len(toks) - 4) // 2
                    for i in range(nskys):
                        l = float(toks[i*2 + 4]) - 0.5
                        r = float(toks[i*2 + 5]) + 0.5
                        pt = (l + r) / 2
                        wid = r - l
                        ap = dict(method='S', edges=(l,r), width=wid, center=pt)
                        line_aps.append(ap)

                # handle layering
                new = line_aps[0]  # any will do
                overlap = [overlaps(o, new) for o in self.ords]
                which = overlap.index(1)
                for ap in line_aps: ap['layer'] = bins[which]
                bins[which] += 1

                all_aps.extend(line_aps)
                line_aps = []

            return all_aps


        def generateOrders(self):
            # make a dict containing only the APGUI keys
            gui = self.pp_specim.all_hdu[0].header
            gui = OrderedDict((kw, gui[kw]) for kw in list(gui)
                              if kw.startswith('ESO DRS APGUI'))

            # build list of successfully extracted orders
            extracts = {}
            nextns = (len(self.pp_specim.all_hdu) - 2) // 3
            for extn in range(nextns):
                target = extn*3 + 1 + 2
                hdr = self.pp_specim.all_hdu[target].header
                offset = re.split('[:_]', hdr['EXTNAME'])[1]
                target = extn*3 + 1 + 0
                hdr = self.pp_specim.all_hdu[target].header
                coeff = [hdr[kw] for kw in list(hdr)
                         if kw.startswith('ESO QC XCDISPX')]  # or PHDISPX
                extracts[int(offset)] = coeff

            # convert APGUI keys to a list of orders
            first = True
            orders = []
            l = c = r = ident = status = None
            for kw in list(gui):
                if 'OFFS' in kw:
                    ident = gui[kw]
                    status = ident in list(extracts)
                    coeffs = extracts[ident] if status else []
                if 'CPIX' in kw:
                    c = gui[kw]
                if 'LPIX' in kw:
                    l = gui[kw] - 0.5 + self.rl
                if 'RPIX' in kw:
                    r = gui[kw] + 0.5 - self.rr
                    new = dict(id=ident, center=c, edges=(l,r), present=status,
                            coeffs=coeffs)
                    orders.append(new)
                    if first:
                        self.activeOrd = new
                        first = False

            if not orders:
                # handle non-echelle case
                new = dict(id=0, center=self.imx/2, edges=(1-0.5,self.imx+0.5),
                           present=True, coeffs=extracts[0])
                orders.append(new)
                self.activeOrd = new

            return orders


        # This function creates all the subplots. It is responsible for the
        # plotting layouts.  There can be different layouts, depending on the
        # availability of data.  Note that subplot(I,J,K) means the Kth plot
        # in a IxJ grid.  Note also that the last one is actually a box with
        # text, no graphs.
        def addSubplots(self, figure):
            if self.found_specim:
                gs = gridspec.GridSpec(70, 90)  # y, x

                # left hand column
                self.ax_specim   = figure.add_subplot(gs[0:60,:60])
                self.ax_apwidth  = figure.add_subplot(gs[69,5:60])

                # right hand column, top
                self.ax_dec_rl   = figure.add_subplot(gs[0:3,70:73])
                self.ax_rl       = figure.add_subplot(gs[0:3,73:77])
                self.ax_inc_rl   = figure.add_subplot(gs[0:3,77:80])
                self.ax_inc_rr   = figure.add_subplot(gs[0:3,80:83])
                self.ax_rr       = figure.add_subplot(gs[0:3,83:87])
                self.ax_dec_rr   = figure.add_subplot(gs[0:3,87:90])
                self.widget_rl = NonInteractiveText(self.ax_rl)
                self.widget_rr = NonInteractiveText(self.ax_rr)

                self.ax_mode     = figure.add_subplot(gs[3:13,70:90])

                ax1              = figure.add_subplot(gs[13:19,70:75])
                ax2              = figure.add_subplot(gs[13:19,75:80])
                ax3              = figure.add_subplot(gs[13:19,80:85])
                ax4              = figure.add_subplot(gs[13:19,85:90])
                widgets = []
                widgets.append(NonInteractiveLegend(ax1, 'Opt', 'O'))
                widgets.append(NonInteractiveLegend(ax2, 'Sum\n+\nAvg', 'A'))
                widgets.append(NonInteractiveLegend(ax3, 'Sum\n+\nFit', 'F'))
                widgets.append(NonInteractiveLegend(ax4, 'Sum\n+\nMed', 'M'))
                self.legends = widgets

                self.ax_layer    = figure.add_subplot(gs[19:49,70:90])
                self.ax_cmap     = figure.add_subplot(gs[49:59,70:90])

                # right hand column, bottom
                self.ax_error    = figure.add_subplot(gs[61:70,70:90])

                self.widget_text = NonInteractiveText(self.ax_error)
                self.drawOrders()

                figure.canvas.mpl_connect('axes_enter_event', self.legendEnter)
                figure.canvas.mpl_connect('axes_leave_event', self.legendLeave)

            else:
                self.ax_nodata = figure.add_subplot(1, 1, 1)


        def initRejects(self):
            self.widget_rl.setText(0.5, 0.5, str(self.rl), color='blue',
                                   ha='center', va='center')
            self.widget_rr.setText(0.5, 0.5, str(self.rr), color='blue',
                                    ha='center', va='center')
            self.widget_slider.resetMax(self.maxw - self.rl - self.rr)


        def setRejects(self):
            self.initRejects()
            self.redraw_orders = True
            self.plotProductsGraphics()


        def rlDecrement(self, event):
            self.rl -= 1
            self.setRejects()


        def rlIncrement(self, event):
            self.rl += 1
            self.setRejects()


        def rrDecrement(self, event):
            self.rr -= 1
            self.setRejects()


        def rrIncrement(self, event):
            self.rr += 1
            self.setRejects()


        def legendEnter(self, event):
            try:
                legend = next(l for l in self.legends if l.ax == event.inaxes)
                legend.rect.set_visible(False)
                legend.text.set_visible(True)
                event.inaxes.figure.canvas.draw_idle()
            except StopIteration:
                return


        def legendLeave(self, event):
            try:
                legend = next(l for l in self.legends if l.ax == event.inaxes)
                legend.rect.set_visible(True)
                legend.text.set_visible(False)
                event.inaxes.figure.canvas.draw_idle()
            except StopIteration:
                return


        # This is the function that makes the plots.
        # Add new plots or delete them using the given scheme.
        # The data has been already stored in self.plot_x, self.plot_xdif, etc ...
        # It is mandatory to add a tooltip variable to each subplot.
        # One might be tempted to merge addSubplots() and plotProductsGraphics().
        # There is a reason not to do it: addSubplots() is called only once at
        # startup, while plotProductsGraphics() is called always there is a
        # resize.
        def plotProductsGraphics(self):
            if self.found_specim:
                #print('narts: %d' % len(self.ax_specim.get_children()))
                #pprint(self.ax_specim.get_children())

                if self.redraw_orders:
                    self.ords = self.generateOrders()
                    self.drawOrders()

                self.drawApertures()

                imgdisp = pipeline_display.ImageDisplay()
                #imgdisp.setYLinearWCSAxis(self.pp_specim.crval1,
                #                          self.pp_specim.cdelt1,
                #                          self.pp_specim.crpix1)
                #imgdisp.setXLinearWCSAxis(self.pp_specim.crval2,
                #                          self.pp_specim.cdelt2,
                #                          self.pp_specim.crpix2)
                imgdisp.setAspect('equal')
                imgdisp.setCmap(self.cmap)

                if not self.axesPlot:
                    self.ax_specim.xaxis.set_label_position('bottom')
                    self.ax_specim.yaxis.set_label_position('left')
                    self.ax_specim.set_xlabel('Order Offset', style='italic')
                    self.ax_specim.set_ylabel('Wavelength (microns)', style='italic')
                    self.ax_specim.tick_params(which='both', left=True,
                            right=False, bottom=True, top=False, labelleft=True,
                            labelright=False, labelbottom=True, labeltop=False)

                    self.ax_specim.xaxis.set_major_locator(pyplot.FixedLocator(
                        [o['center'] for o in self.ords]))
                    self.ax_specim.yaxis.set_major_formatter(pyplot.FuncFormatter(
                        self.format_func))

                    self.axesPlot = True

                # this should always run regardless of self.axesPlot
                self.ax_specim.xaxis.set_major_formatter(pyplot.FixedFormatter(
                    ['{}{}'.format(o['id'], '*' if o is self.activeOrd else '')
                     for o in self.ords]))

                imgdisp.display(self.ax_specim, '', '', self.pp_specim.image)
                self.ax_specim.grid(False)

                # display() above calls imshow() which creates a new AxesImage
                # artist, so remove the old one(s)
                aximgs = [child for child in self.ax_specim.get_children()
                          if isinstance(child, image.AxesImage)]
                aximgs.pop()
                for axim in aximgs: axim.remove()

                self.setCheck(None)

            else:
                # Data not found info
                self.ax_nodata.set_axis_off()
                self.text_nodata = """Data not found (should be one of:
SPC_PHOT_TAB, SPC_PHOT_HRG_TAB,
SPC_OBS_LMR_TAB, SPC_OBS_HRG_TAB)

Apertures and/or parameters
previously set by the user may
have caused the visir_old_spc_obs
recipe to fail.
"""
                self.ax_nodata.text(
                    0.1, 0.6, self.text_nodata, color='#11557c',
                    fontsize=18, ha='left', va='center', alpha=1.0)
                #self.ax_nodata.tooltip = 'Line prediction not found in the products'


        def format_func(self, value, _):
            if not self.activeOrd: return '?'
            ncoeffs = len(self.activeOrd['coeffs'])
            if ncoeffs < 1: return '?'
            accrue = self.activeOrd['coeffs'][0]
            if ncoeffs < 2: return "%.3f" % (accrue * 1e6)
            for i in range(ncoeffs - 1):
                accrue += self.activeOrd['coeffs'][i+1] * pow(value, i+1)
            return "%.3f" % (accrue * 1e6)


        def plotWidgets(self):
            if not self.found_specim: return []

            widgets = []

            widgets.append(plot_widgets.InteractiveRadioButtons(
                self.ax_mode, self.toggleMode,
                ('Add/Shift', 'Resize/Shift', 'Delete', 'Set Method'), 0))

            widgets.append(plot_widgets.InteractiveRadioButtons(
                self.ax_cmap, self.setColorMap,
                ('Light', 'Contrast'), 0))
            
            widgets.append(plot_widgets.InteractiveRadioButtons(
                self.ax_layer, self.setLayer,
                tuple('Layer ' + str(layer) for layer in range(10)), 0))

            widgets.append(InteractiveButtons(
                self.ax_dec_rl, self.rlDecrement, '-'))
            widgets.append(InteractiveButtons(
                self.ax_inc_rl, self.rlIncrement, '+'))
            widgets.append(InteractiveButtons(
                self.ax_inc_rr, self.rrIncrement, '+'))
            widgets.append(InteractiveButtons(
                self.ax_dec_rr, self.rrDecrement, '-'))

            widgets.append(plot_widgets.InteractiveClickableSubplot(
                self.ax_specim, self.setPoint))
            
            self.widget_slider = MyInteractiveSlider(
                self.ax_apwidth, self.setApWidth, 1, self.maxw, 8, "Width",
                fmt="%d", step=1.0)
            widgets.append(self.widget_slider)

            self.initRejects()
            return widgets
            

        # This function specifies which are the parameters that should be presented
        # in the window to be edited.
        # Note that the parameter has to be also in the in_sop port (otherwise it
        # won't appear in the window)
        # The descriptions are used to show a tooltip. They should match one to one
        # with the parameter list
        # Note also that parameters have to be prefixed by the 'recipe name:'
        def setInteractiveParameters(self):
            paramList = list()
            # using our own parameter classes instead of the reflex ones, at
            # least for rl & rr, in order to support callbacks (needed to change
            # the order widths displayed onscreen the moment the user changes
            # the rl and/or rr param values)

            #paramList.append(reflex.RecipeParameter("visir_old_spc_obs", "rl",
            #    group="reject", description="Reject cols from LHS"))
            #paramList.append(reflex.RecipeParameter("visir_old_spc_obs", "rr",
            #    group="reject", description="Reject cols from RHS"))

            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "apfile", group="params", description="WARNING: Changing this "
                "path to point to your own apfile overrides any edits you may "
                "have made in the left-hand panel (they are effectively "
                "discarded).")) 
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "bkgcorrect", group="params", description="Subtract the median "
                "from the spectral column before extracting the wavelength. "
                "This is required when the skylines do not correctly cancel due "
                "to gratting oscillations.")) 
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "destripe_iterations", group="params", description="Max number "
                "of destriping iterations (0 to disable destriping). Horizontal "
                "destriping is done first and if no horizontal striping is "
                "detected, vertical destriping is performed."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "destripe_morpho", group="params", description="Destripe with "
                "morphological cleaning."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "fixcombi", group="params", description="Perform the distortion "
                "correction on the combined image, and not on each of the "
                "jittered images. This will reduce excution time and degrade "
                "the quality of the combined image."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "gain", group="params", description="Detector gain."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "hori_arc", group="params", description="Distortion correction: "
                "LMR Detector horizontal curvature (pixel). Increased by a "
                "factor 1.5 in HR A-side. Reduced by a factor 2 in HR B-side."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "ox_kernel", group="params", description="Size of square "
                "smoothing kernel, in pixels, to apply to science frame before "
                "optimal extraction (ignored during aperture extraction). A "
                "median filter is used."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "ox_niters", group="params", description="Number of optimal "
                "extraction iterations to perform."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "ox_sigma", group="params", description="Sigma to use for "
                "clipping in optimal extraction."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "ox_smooth", group="params", description="Width of smoothing "
                "window to use along spectral dimension during optimal "
                "extraction. A median filter is used."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "rej", group="params", description="Each resulting pixel is the "
                "average of the corresponding (interpolated) pixel value in "
                "each jittered image. A positive value, n1, for the first of "
                "the two integers specifies that for each pixel the smallest n1 "
                "pixel values shall be ignored in the averaging. Similarly, a "
                "positive value, n2, for the second of the two integers "
                "specifies that for each pixel the largest n2 pixel values "
                "shall be ignored in the averaging."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "respcal", group="params", description="An optional path to a "
                "FITS file containing a 1-D fringe model to be divided into the "
                "1-D extracted spectra in order to remove the fringes."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "ro_noise", group="params", description="Readout noise of the "
                "detector."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "slit_skew", group="params", description="Distortion "
                "correction: Skew of slit (degrees) (clockwise)."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "spectrum_skew", group="params", description="Distortion "
                "correction: LMR Skew of spectrum (degrees) "
                "(counter-clockwise). Not used in High Resolution."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "vert_arc", group="params", description="Distortion correction: "
                "LR Detector vertical curvature (pixel). Reduced by a factor 4 "
                "in MR. Not used in HR A-side. Increased by a factor 115/52 in "
                "HR B-side."))
            paramList.append(reflex.RecipeParameter("visir_old_spc_obs",
                "xcorr", group="params", description="If user-defined refining "
                "of offsets is enabled a cross-correlation of the images is "
                "performed. In order to speed up this process, this "
                "cross-correlation is performed only on smaller rectangles "
                "around the anchor points. The first two parameters is the "
                "half-size of this rectangle in pixels. The second pair is the "
                "maximum shift in x and y (pixels) evaluated by the "
                "cross-correlation on the rectangle. Used only if user-defined "
                "refining of offsets is enabled."))

            return paramList


        # plot cutouts/orders, colored according to extraction status
        def drawApertures(self):
            for ap in self.aps:
                if ap['layer'] == self.layer:
                    if 'art' in ap: continue  # absence of art flags redraw

                    ap['art'] = self.ax_specim.axvspan(
                        ap['edges'][0], ap['edges'][1],
                        alpha=0.3, hatch=hatches[ap['method']])

                elif 'art' in ap:
                    ap['art'].remove(); del ap['art']


        # plot cutouts/orders, colored according to extraction status
        def drawOrders(self):
            for art in self.orderArts: art.remove()
            ndot = []; pdot = []; nline = []; pline = []; ndash = []; pdash = []
            for o in self.ords:
                line = pline if o['present'] else nline
                dash = pdash if o['present'] else ndash
                dot = pdot if o['present'] else ndot
                dot.append(o['center'])
                line.append(o['edges'][0])
                line.append(o['edges'][1])
                if self.rl: dash.append(o['edges'][0] + self.rl)
                if self.rr: dash.append(o['edges'][1] - self.rr)

            #nstyle = {'alpha': 0.5, 'color': 'red'}
            #pstyle = {'alpha': 0.5, 'color': 'green'}
            newArts = []
            if nline: newArts.append(self.ax_specim.vlines(nline, 0, self.imy,
                alpha=0.5, color='red'))#**nstyle))
            if pline: newArts.append(self.ax_specim.vlines(pline, 0, self.imy,
                alpha=0.5, color='green'))#**pstyle))
            if ndash: newArts.append(self.ax_specim.vlines(ndash, 0, self.imy,
                linestyle='dashed', alpha=0.5, color='red'))#**nstyle))
            if pdash: newArts.append(self.ax_specim.vlines(pdash, 0, self.imy,
                linestyle='dashed', alpha=0.5, color='green'))#**pstyle))
            if ndot: newArts.append(self.ax_specim.vlines(ndot, 0, self.imy,
                linestyle='dotted', alpha=0.5, color='red'))#**nstyle))
            if pdot: newArts.append(self.ax_specim.vlines(pdot, 0, self.imy,
                linestyle='dotted', alpha=0.5, color='green'))#**pstyle))
            self.orderArts = newArts
            self.redraw_orders = False

 
        def setCurrentParameterHelper(self, helper) :
            self.getParam = helper


        def setWindowHelp(self):
            help_text = """
This is an interactive window to allow customisation of the extraction apertures.
"""
            return help_text

        def setWindowTitle(self):
            self.rl = self.getParam('rl')
            self.rr = self.getParam('rr')
            self.apfile = self.getParam('apfile')
            if self.apfile == 'NONE': self.apfile = None
            title = 'Aperture Editor'
            return title
          

        def toggleMode(self, label):
            if not self.found_specim: return
            self.del_mode = (label == 'Delete')
            self.resize_mode = (label == 'Resize/Shift')
            self.tag_mode = (label == 'Set Method')


        def setColorMap(self, label):
            if not self.found_specim: return
            mapping = dict(Light='binary', Contrast='gist_ncar')
            self.cmap = mapping[label]
            self.plotProductsGraphics()


        def setLayer(self, label):
            if not self.found_specim: return
            self.layer = int(label.split()[-1])
            self.plotProductsGraphics()


        def setCheck(self, _):
            if not self.found_specim: return
            result = self.scanForErrors()
            if result:
                self.widget_text.setText(0, 0, result)
            else:
                self.widget_text.setText(0, 0, "No errors", color='green')
            self.ax_error.figure.canvas.draw_idle()


        def setPoint(self, point):
            if not self.found_specim: return
            #assert point.inaxes is self.ax_specim
            #print('Point data', point.xdata, point.ydata)
            #print('Point ', point.x, point.y)
            #print(point.inaxes)
            #print(point.inaxes.figure.gca())
            pt = point.xdata

            for order in self.ords:
                if contains(order, pt):
                    self.activeOrd = order
                    break

            if self.tag_mode:
                # tag the most recent aperture containing the point
                for ap in reversed(self.aps):
                    if ap['layer'] != self.layer: continue
                    if not contains(ap, pt): continue

                    ap['method'] = hcycle[ap['method']]

                    # flag a redraw
                    ap['art'].remove(); del ap['art']
                    break

            elif self.del_mode:
                # delete the most recent aperture containing the point
                for apert in reversed(self.aps):
                    if apert['layer'] != self.layer: continue
                    if not contains(apert, pt): continue

                    self.aps[:] = [ap for ap in self.aps if ap is not apert]

                    # flag a redraw
                    apert['art'].remove()
                    break

            elif self.resize_mode:
                # resize the most recent aperture containing the point
                for ap in reversed(self.aps):
                    if ap['layer'] != self.layer: continue
                    if not contains(ap, pt): continue

                    # use ap['center'] if shifting in resize mode is undesired
                    dest = pt#ap['center']
                    l = math.ceil(dest - self.apwidth / 2.) - 0.5
                    r = math.floor(dest + self.apwidth / 2.) + 0.5
                    win = (l, r)

                    # only proceed if new width overlaps a single order
                    overlap = sum([overlaps(o, win) for o in self.ords])
                    if overlap != 1: return

                    ap['edges'] = win
                    ap['width'] = self.apwidth

                    # flag a redraw
                    ap['art'].remove(); del ap['art']
                    break

            else:
                # shift an existing aperture...
                shift = False
                for ap in reversed(self.aps):
                    if ap['layer'] != self.layer: continue
                    if not contains(ap, pt): continue

                    shift = True
                    l = math.ceil(pt - ap['width'] / 2.) - 0.5
                    r = math.floor(pt + ap['width'] / 2.) + 0.5
                    win = (l, r)

                    # only proceed if it still overlaps only a single order
                    overlap = sum([overlaps(o, win) for o in self.ords])
                    if overlap != 1: return

                    ap['edges'] = win
                    ap['center'] = pt

                    # flag a redraw
                    ap['art'].remove(); del ap['art']
                    break

                # ...or add a new one
                if not shift:
                    # determine window around clicked point
                    l = math.ceil(pt - self.apwidth / 2.) - 0.5
                    r = math.floor(pt + self.apwidth / 2.) + 0.5
                    win = (l, r)

                    # only proceed if our window overlaps a single order
                    overlap = sum([overlaps(o, win) for o in self.ords])
                    if overlap != 1: return

                    ap = dict(center=pt, edges=win, layer=self.layer,
                            method='S', width=self.apwidth)
                    self.aps.append(ap)

            self.plotProductsGraphics()


        def setApWidth(self, value):
            if not self.found_specim: return
            self.apwidth = int(value)


        def scanForErrors(self):
            rv = ''
            for layer in reversed(range(10)):
                for order in reversed(self.ords):
                    locn = 'LAY[{}]/ORD[{}{}]: '.format(
                        layer, '+' if order['id']>0 else '', order['id'])
                    aps = [a for a in self.aps if a['layer'] == layer and
                           overlaps(order, a)]
                    nsky = len([ap for ap in aps if ap['method'] == 'S'])
                    nopt = len([ap for ap in aps if ap['method'] == 'O'])
                    navg = len([ap for ap in aps if ap['method'] == 'A'])
                    nlin = len([ap for ap in aps if ap['method'] == 'F'])
                    nmed = len([ap for ap in aps if ap['method'] == 'M'])

                    # check for certain specific errors
                    if nopt+navg+nlin+nmed<1 and nsky>0:
                        # check they are not all sky
                        rv += locn + '\n   need a method\n'
                    elif nopt+navg+nlin+nmed>1:
                        # check there is a single method ap
                        rv += locn + '\n   too many meths\n'
                    elif nopt==1 and nsky+navg+nlin+nmed>0:
                        # check if optimal, there are no other aps
                        rv += locn + '\n   opt not alone\n'
                    elif navg+nlin+nmed==1 and nsky<1:
                        # check if AFM, there is at least one sky
                        rv += locn + '\n   need sky ap(s)\n'

                    # check for overlapping aps
                    bail = False
                    for ap1 in aps:
                        if bail: break
                        for ap2 in aps:
                            if bail: break
                            if ap1 is ap2: continue
                            if overlaps(ap1, ap2):
                                rv += locn + '\n   aps overlap\n'
                                bail = True

                    # check for aps extending beyond order boundaries
                    bail = False
                    for ap in aps:
                        lwithin = contains(order, ap['edges'][0])
                        rwithin = contains(order, ap['edges'][1])
                        if not lwithin or not rwithin:
                            rv += locn + '\n   unbound ap\n'
                            bail = True

            return rv.rstrip()


        def writeAperturesToFile(self, apfile):
            if not self.aps or self.scanForErrors() != '':
                return 'NONE'

            original_stdout = sys.stdout
            with open(apfile, 'w') as f:
                sys.stdout = f

                def takeFirst(elem): return elem['edges'][0]
                self.aps.sort(key=takeFirst)

                for order in self.ords:
                    for layer in range(10):
                        items = []
                        aps = [a for a in self.aps if a['layer'] == layer and
                               overlaps(order, a)]
                        if not aps: continue

                        sky = [ap for ap in aps if ap['method'] == 'S']
                        opt = [ap for ap in aps if ap['method'] == 'O']
                        avg = [ap for ap in aps if ap['method'] == 'A']
                        lin = [ap for ap in aps if ap['method'] == 'F']
                        med = [ap for ap in aps if ap['method'] == 'M']

                        items.append('O' if opt else 'A')
                        for ary in [opt, avg, lin, med]:
                            if not ary: continue
                            items.append(int(ary[0]['edges'][0] + 0.5))
                            items.append(int(ary[0]['edges'][1] - 0.5))
                        for ary in [avg, lin, med]:
                            if not ary: continue
                            items.append(ary[0]['method'])
                        for ap in sky:
                            items.append(int(ap['edges'][0] + 0.5))
                            items.append(int(ap['edges'][1] - 0.5))

                        print(' '.join(map(str, items)))#*items)

            sys.stdout = original_stdout
            return apfile


except ImportError:
    import_sucess = False
    print("Error importing modules")

# This is the 'main' function
if __name__ == '__main__':

    # import reflex modules
    import reflex_interactive_app
    import sys

    # Create interactive application
    app = reflex_interactive_app.PipelineInteractiveApp(
        enable_init_sop=True)

    # Check if import failed or not
    if import_sucess == False:
        app.setEnableGUI('false')

    # Open the interactive window if enabled
    if app.isGUIEnabled():
        # Get the specific functions for this window
        dataPlotManager = DataPlotterManager()
        app.setPlotManager(dataPlotManager)
        app.showGUI()

        # FIXME: user-supplied apfile, rl, & rr values passed downstream even
        # if user opts to discard edits by clicking Continue.  The downstream
        # recipe doesn't use these params but it *does* save them to product
        # files so could be misleading.
        if hasattr(app, 'user_edited_param'):
            originals = app.inputs.in_sop
            useredits = app.user_edited_param

            aforig = next(p for p in originals if p.displayName == 'apfile')
            afedit = next(p for p in useredits if p.displayName == 'apfile')
            if aforig.value == afedit.value:
                # user has not supplied own apfile: generate from ApEditor
                apfile = app.inputs.products_dir + '/aperty'
                afedit.value = dataPlotManager.writeAperturesToFile(apfile)

            rledit = next(p for p in useredits if p.displayName == 'rl')
            rledit.value = dataPlotManager.rl
            rredit = next(p for p in useredits if p.displayName == 'rr')
            rredit.value = dataPlotManager.rr

    else:
        app.passProductsThrough()

    # Print outputs. This is parsed by the Reflex python actor to get the results
    # Do not remove
    app.print_outputs()

    sys.exit()
