#!/usr/bin/env python
# encoding: utf-8

"""
A set of utilities to use spike in NMR or FTMS within jupyter


First version MAD june 2017
Intermediate version MAD october 2019
Improvements MAD spring - summer - automn  2021 - winter 2022
"""

"""
The code contains several parts

first a bunch of general small gadgets used to improve a jupyter notebook (hidecode, jsalert, injectcss, etc...)

Then some NMR specific tools

The most important is the interactive tools (hence the name):

Show1D
    from which inherits
Show1Dplus
baseline1D
Phaser
NMRintegrate
NMRpeakpicker

all tools are not at the same level on completion, this is an evoluating code...

Naming Conventions are used
For interactive tools (phaser, show, baseline)
draw() creates static matplotlib "artists"
    should probably called once at creation 
    eventually is things change a lot
disp() 
    takes care of redisplaying after some modifications (x or y limits, eventually xdata / ydata ...)
    should note create new artists

"""


import asyncio
from spike.plugins import Peaks
from . import __path__
from spike.plugins.NMR.Integrate import Integrals, Integralitem
from .ipyfilechooser import FileChooser as FileChooser_code
from ..File.BrukerNMR import Import_1D
from ..NMR import NMRData
from .. import NMR
from .. import version
from numpy.core.overrides import array_function_dispatch
import numpy as np
from IPython.display import display, HTML, Javascript, Markdown, Image
import ipywidgets as widgets
from ipywidgets import fixed, Layout, HBox, VBox, Label, Output, Button, Tab
from matplotlib.widgets import MultiCursor
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import matplotlib as mpl
from datetime import datetime
import re
import warnings
import time
import math as m
import glob
import unittest
import sys
import os
import os.path as op
from pathlib import Path
if version.revision < "538":
    warnmsg = """
There is version missmatch between the core program and the interactive tools
You may experiment some difficulties or halting with this notebook
"""
    warnings.warn(warnmsg)


try:
    import spike.plugins.bcorr as bcorr
except ModuleNotFoundError:
    print('Baseline correction plugins not installed !')

__version__ = "1.3.0"

# REACTIVE modify callback behaviour
# True is good for inline mode / False is better for notebook mode
REACTIVE = True
HEAVY = False
# Activate_Wheel: scale with wheel control in the graphic cells
Activate_Wheel = True
# reverse_scroll : if True, reverses direction of mouse scroll
reverse_scroll = True

# default color set
Colors = ('black', 'red', 'blue', 'green', 'orange',
          'blueviolet', 'crimson', 'turquoise', 'indigo',
          'magenta', 'gold', 'pink', 'purple', 'salmon', 'darkblue', 'sienna')
Cursor = dict(linestyle='--', color='darkblue',
              linewidth=0.8)   # used for running cursors
Contrast = dict(color="crimson")  # used for contrasting spectra
##############################
# First General Utilities
##############################


def initialize(verb=1):
    "initialize notebook interface for Spike"
    if verb > 0:
        print("\nInteractive module version,", __version__)
    if verb > 1:
        hidecode()
        hidedoc()
    else:
        hidecode(message="")
        hidedoc(message="")
    initCSS()
    Logo()


def initCSS():
    "adapt some CSS"
    display(HTML('''
<style>
hr         {height: 2px; border: 0;border-top: 1px solid #ccc;margin: 1em 0;padding: 0; }
.container {width:100% !important; }
</style>
    '''))


def hidecode(initial='show', message="<i>useful to show/print a clean screen when processing is finished</i>"):
    """
    this func adds a button to hide/show the code on a jupyter page
    initial is either 'show' or 'hide'
    inspired from: https://stackoverflow.com/questions/27934885/how-to-hide-code-from-cells-in-ipython-notebook-visualized-with-nbviewer/28073228#28073228
    """
    if initial == 'show':
        init = 'false'
    elif initial == 'hide':
        init = 'true'
    display(HTML('''
<script>
code_show=%s; 
function code_toggle()
    { if (code_show)
        {   $('div.input').hide();
            $('div.prompt').hide();
            $('#but').val("show python code");
        } else {
            $('div.input').show();
            $('div.prompt').show();
            $('#but').val("hide python code");
        }
    code_show = !code_show
    } 
$(document).ready(code_toggle);
</script>
<form action="javascript:code_toggle()">
<input id="but" type="submit" style="border:1px solid black; background-color:#DDD">
%s
</form>''' % (init, message)))


def hidedoc(initial='show', message="<i>useful to show/print a clean screen when processing is finished</i>"):
    """
    this func adds a button to hide/show the doc on a jupyter page
    initial is either 'show' or 'hide'
    inspired from: https://stackoverflow.com/questions/27934885/how-to-hide-code-from-cells-in-ipython-notebook-visualized-with-nbviewer/28073228#28073228
    """
    if initial == 'show':
        init = 'false'
    elif initial == 'hide':
        init = 'true'
    display(HTML('''
<script>
doc_show=%s; 
function doc_toggle()
    { if (doc_show)
        { $('div.text_cell').hide(); $('#butdoc').val("show documentation");
        } else { $('div.text_cell').show(); $('#butdoc').val("hide documentation");
    }
    doc_show = !doc_show } 
$(document).ready(doc_toggle);
</script>
<form action="javascript:doc_toggle()">
<input id="butdoc" type="submit" style="border:1px solid black; background-color:#DDD">
%s
</form>''' % (init, message)))


def UserLogofile():
    "return the address on disk of the User Logo file - located in $HOME/Spike/Logo.png"
    ufile = Path.home()/'.config'/'Spike'/'Logo.png'
    if ufile.exists():
        return ufile
    else:
        return Logofile()


def Logofile():
    "return the address on disk of the Logo file"
    return op.join(__path__[0], 'Logo.png')


def Logo(width=150):
    "display the Logo"
    display(Image(filename=Logofile(), width=width))


def jsalert(msg="Alert text"):
    "send a javascript alert"
    # use \\n if you want to add several lines
    display(Javascript("alert('%s')" % msg))


def _jsalert(msg="Alert text", title='Alert'):
    display(Javascript("""
    require(
        ["base/js/dialog"], 
        function(dialog) {
            dialog.modal({
                title: '%s',
                body: '%s',
                buttons: {
                    'Ok': {}
                }
            });
        }
    );
    """ % (title, msg)))


def space(width="80px"):
    "defines a layout of width, width should be a string in CSS syntax"
    return widgets.Layout(width=width)


def spacer(width="80px"):
    "defines a spacer to be used in widgets.Box - width should be a string in CSS syntax"
    return widgets.HTML("&nbsp;", layout=space(width))


class _FileChooser:
    """a simple file chooser for Jupyter - obsolete - not used any more"""

    def __init__(self, base=None, filetype=['fid', 'ser'], mode='r', show=True):
        if base is None:
            self.curdir = "/"
        else:
            self.curdir = base
        self.filetype = filetype
        self.mode = mode
        self.wfile = widgets.Text(layout=space(
            '70%'), description='File to load')
        self.ldir = widgets.Label(value="Chosen dir:  "+self.curdir)
        self.wdir = widgets.Select(
            options=self.dirlist(),
            description='Choose Dir',
            layout=space('70%'))
        if mode == 'r':
            self.wchooser = widgets.Select(
                options=self.filelist(),
                description='Choose File',
                layout=Layout(width='70%'))
            self.wchooser.observe(self.wob)
            self.wfile.disabled = True   # not mofiable in read mode
        elif mode == "w":
            self.wfile.description = 'File to create'
            self.wfile.disabled = False
            self.wchooser = widgets.Select(
                options=self.filelist(),
                description='Files',
                layout=space('70%'))
        else:
            raise Exception('Only "r" and  "w" modes supported')
        self.wsetdir = Button(description='❯', layout=space('20%'),
                              button_style='success',  # 'success', 'info', 'warning', 'danger' or ''
                              tooltip='descend in directory')
        self.wup = Button(description='❮', layout=space('20%'),
                          button_style='success',  # 'success', 'info', 'warning', 'danger' or ''
                          tooltip='up to previous directory')
        self.wsetdir.on_click(self.setdir)
        self.wup.on_click(self.updir)
        if show:
            self.show()

    def filelist(self):
        fl = []
        if self.mode == 'r':
            if type(self.filetype) is str:
                fl = glob.glob(op.join(self.curdir, self.filetype))
            elif type(self.filetype) in (tuple, list):
                for f in self.filetype:
                    fl += glob.glob(op.join(self.curdir, f))
            else:
                raise Exception(
                    'TypeError, filetype should be either a string or a list')
        else:   # 'w'
            fl = [f for f in glob.glob(
                op.join(self.curdir, '*')) if op.isfile(f)]
            self.wfile.value = op.join(self.curdir, self.filetype)
        if fl == []:
            fl = [" "]
        return fl

    def dirlist(self):
        base = self.curdir
        return [d for d in glob.glob(op.join(base, '*')) if (op.isdir(d) and not d.endswith('__pycache__'))]

    def wob(self, e):
        self.wfile.value = self.wchooser.value

    def updir(self, e):
        self.curdir = op.dirname(self.curdir)
        self.ldir.value = "Chosen dir:  "+self.curdir
        self.wdir.options = self.dirlist()
        self.wchooser.options = self.filelist()

    def setdir(self, e):
        self.curdir = self.wdir.value
        self.ldir.value = "Chosen dir:  "+self.curdir
        self.wdir.options = self.dirlist()
        self.wchooser.options = self.filelist()

    def show(self):
        display(VBox(
            [self.ldir,
             HBox([self.wdir, VBox([self.wup, self.wsetdir])]),
                self.wchooser,
                self.wfile
             ])
        )
        return self

    @property
    def file(self):
        "the chosen complete filename"
        return self.wfile.value

    @property
    def dirname(self):
        "the final dirname containing the chosen file"
        if op.isdir(self.wfile.value):
            return op.basename(self.wfile.value)
        else:
            return op.basename(op.dirname(self.wfile.value))

    @property
    def nmrname(self):
        "the final dirname containing the chosen file for TopSpin files"
        if op.isdir(self.wfile.value):
            return op.join(
                op.basename(op.dirname(self.wfile.value)), self.dirname)
        else:
            return op.join(
                op.basename(op.dirname(op.dirname(self.wfile.value))), self.dirname)

    @property
    def basename(self):
        "the basename of the chosen file"
        return op.basename(self.wfile.value)


# the following set of utilities is to display a nice summary of a SPIKE/Bruker dataset
# first parse parameters in dictionary derived from acqus or procs
# for the program to recognize arrays, the arrayname should be given below ( | separated, no space)
arraynames = 'D|P|PL|PCPD|SP'


def readplist(paramtoadd, paramdict):
    "parse lists from acqus files - only that ones defined in arraynames"
    m = (re.match('(%s)([0-9]+)' % (arraynames,), paramtoadd))
    if m:    # arrays are special !
        i = int(m.group(2))
        pp = paramdict['$'+m.group(1)]
        val = pp[i]
    else:
        val = paramdict['$%s' % paramtoadd]
    return val


ParamList = ['PULPROG', 'SFO1', 'NS', 'TE', 'TD', 'RG', 'SW', 'O1', 'D1', 'P1']


def param_line(acqus, paramlist=None, output='string'):
    """
    given a acqus dictionary, and a list of parameters to extract, 
    this functions return either a plain text or a HTML table depending on output value
    either "string" or "HTML" )
    """
    head = []
    lines = []
    if paramlist == None:
        paramlist = ParamList

    for k in paramlist:
        val = str(readplist(k, acqus))
        if val.startswith('<'):
            val = val[1:-1]
        lines.append(val)
        if k.startswith("$"):
            head.append(k[1:])
        else:
            head.append(k)
    if output == "string":
        res = "\t".join(head) + "\n" + "\t".join(lines)
    elif output == "HTML":
        Head = '<table class="dataframe" border="1"><thead><tr>'
        Head += '<th style="text-align: center;">' + \
            '</th><th style="text-align: center;">'.join(head) + '</th>'
        Head += '</tr></thead>'
        Lines = '<tbody><tr><td>' + \
            '</td><td>'.join(lines) + '</tr></tbody></table>'
        res = HTML(Head + Lines)
    return res


def param_table(data, output='string'):
    """ return the complete parameter table held into data
    output is either "string" or "HTML
    """
    lines = []
    acqus = data.params['acqu']
    if output == 'string':
        for k in acqus.keys():
            lines.append("%s : %s" % (k, acqus[k]))
        res = "\n".join(lines)
    if output == 'HTML':
        st = '<table class="dataframe" border="1"><thead><tr><th><b>Parameter</b></th><th style="text-align: left;">value</th></tr>'
        st += '</thead><tbody>\n'
        for k in acqus.keys():
            val = str(acqus[k]).replace("'", "") \
                .replace("&", '&amp;') \
                .replace("[", '') \
                .replace("]", '') \
                .replace("<", '&lsaquo;') \
                .replace(">", '&rsaquo;') \
                .replace('"', '&quot;')
            lines.append(
                '<tr><td>%s</td><td style="text-align: left;">%s</td></tr>' % (k, val))
        st += ('\n'.join(lines) + '\n</tbody></table>\n')
        res = st
    return res


def summary(data, param=True, output='string'):
    """ produces a summary of the data set
    if param is True, a short list of 1D parameters is given, using the globally defined Paramlist
    output is either "string" or "HTML"
    """
    if output == 'string':
        res = data.params['acqu']['title']
        if param:
            res += r"\n" + param_line(data.params['acqu'], output='string')
    elif output == 'HTML':
        res = HTML('<b>' + data.params['acqu']['title'] + '</b>')
        if param:
            res2 = param_line(data.params['acqu'], output='HTML')
            res = HTML(res.data + res2.data)
    return res


def popup_param_table(data):
    css = """<style>
    table { border-collapse: collapse; border: 3px solid #eee; }
    table tr th:first-child { background-color: #eeeeee; color: #333; font-weight: bold }
    table thead th { background-color: #eee; color: #000; }
    tr, th, td { border: 1px solid #ccc; border-width: 1px 0 0 1px; border-collapse: collapse;
    padding: 3px; font-family: monospace; font-size: 10px }</style>
    """
    s = '<script type="text/Javascript">'
    s += 'var win = window.open("", "Parameters", "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=780, height=200, top="+(screen.height-400)+", left="+(screen.width-840));'
    s += 'win.document.body.innerHTML = \''
    s += (param_table(data, output='HTML') + css).replace("\n", '\\') + '\';'
    s += '</script>'
    return(HTML(s+css))
# ²


class Timer:
    def __init__(self, timeout, callback):
        self._timeout = timeout
        self._callback = callback

    async def _job(self):
        await asyncio.sleep(self._timeout)
        self._callback()

    def start(self):
        self._task = asyncio.ensure_future(self._job())

    def cancel(self):
        self._task.cancel()


def debounce(wait):
    """ Decorator that will postpone a function's
        execution until after `wait` seconds
        have elapsed since the last time it was invoked. """
    def decorator(fn):
        timer = None

        def debounced(*args, **kwargs):
            nonlocal timer

            def call_it():
                fn(*args, **kwargs)
            if timer is not None:
                timer.cancel()
            timer = Timer(wait, call_it)
            timer.start()
        return debounced
    return decorator

##############################
# Then 1D NMR Tools
##############################


"""
There is a hierarchic structure of the tools
All these tools are Jupyter widgets which can be mixed with other regulat ipywidgets
- Show1D creates the basic environement
    it sets 2 functions called back by all actions
    - draw() which build the whole picture
    - disp() which refresh only the bounding box (in x and y), and eventually modify simple objects
    - Reset which redraw and reset to default display parameter
    - on-done() which freezes the current display as a static 
- Phaser1D, NMRPeaker1D, NMRIntegrate, baseline1D  are surcharging Show1d, 
    with new methods, and eventually surcharging draw() and disp()
- Show1Dplus() which presents all the options

"""


class Show1D(HBox):
    """
    An interactive display, 1D NMR
        Show1D(spectrum)
    """

    def __init__(self, data, title=None, figsize=None, show=True, create_children=True, yratio=21.0):
        """
        data : to be displayed
        title : text for window
        figsize : size in inches (x,y)
        yratio : inverse minimum neg display extension, 1=centered, 21:all positive
        """
        super().__init__()
        self.yratio0 = yratio            #  
        self.yratio = self.yratio0
        self.data = data
        self.drspectrum = None     # will store matplotlib artist
        try:
            (_, self.noise) = data.robust_stats()    # noise will be handy !
        except AttributeError:
            self.noise = 0
        self.title = title
        # graphic set-up details
        self.yaxis_visible = False
        self.ax_spines = False
        self.canvas_header_visible = False
        if figsize is None:
            figsize = mpl.rcParams['figure.figsize']  # (10,5)
        if reverse_scroll:
            self.reverse_scroll = -1
        else:
            self.reverse_scroll = 1
        self.blay = space('80px')       # default layout for buttons
        self.blank = widgets.HTML("&nbsp;", layout=self.blay)
        self.done = Button(description="Done", button_style='success', layout=self.blay,
                           tooltip="Stop interactivity and store figure")
        self.done.on_click(self.on_done)
        self.reset = Button(description="Reset", button_style='success', layout=self.blay,
                            tooltip="Reset to default display")
        self.savepdf = widgets.Button(description="Save figure", button_style='', layout=self.blay,
                                      tooltip='Save the current figure as pdf')

        def onsave(e):
            name = self.fig.get_axes()[0].get_title()
            datedext = datetime.strftime(
                datetime.now(), " %y-%m-%d_%H:%M:%S.pdf")
            name = name.replace('/', '_') + datedext
            if name.startswith('.'):
                name = 'Figure'+name
            self.fig.savefig(name)
            print('figure saved as: ', name)
        self.savepdf.on_click(onsave)
        self.reset.on_click(self.on_reset)
        self.scale = widgets.FloatSlider(description='scale:', value=1.0, min=0.1, max=200, step=0.1,
                                         layout=Layout(width='80px', height=str(0.8*2.54*figsize[1])+'cm'), continuous_update=REACTIVE,
                                         orientation='vertical')
        for widg in (self.scale,):
            widg.observe(self.obdisp)
        if create_children:
            plt.ioff()
            fi, ax = plt.subplots(figsize=figsize)
            fi.set_tight_layout(True)
            plt.ion()
            self.ax = ax
            # def update_box(axes):
            #     self.xb = self.ax.get_xbound()
            #     self.ax.set_ybound(self.yb0/self.scale.value)
            # self.ax.add_callback(self.update_box)
            self.fig = fi
#            self.xb = self.ax.get_xbound()
            self.on_reset()
            self.children = [
                VBox([self.reset, self.scale, self.savepdf, self.done]), self.fig.canvas]
            if show:
                self.draw()

    # call backs
    def on_done(self, b):
        self.close()
        display(self.fig)   # shows spectrum

    def on_reset(self, b=None):
        self.scale.value = 1.0
        try:
            self.xb0 = np.array([self.data.axis1.itoc(0), self.data.axis1.itoc(self.data.size1)])  # full box in x
            self.xb = self.xb0                                                      # current x box
            self.yratio = self.yratio0            # inverse minimum neg display extension 
            self.ymax = self.data.absmax*1.1      # highest position
            self.yb0 = np.array( [-self.ymax/self.yratio, self.ymax] )              # full box in y
            self.disp()
        except AttributeError:   # if self.data not defined
            pass
    def ob(self, event):
        "observe events and redraw"
        if event['name'] == 'value':
            self.draw()

    def obdisp(self, event):
        "observe events and display"
        if event['name'] == 'value':
            self.disp()

    @debounce(0.005)
    def scale_up(self, step):
        # 1.1892 is 4th root of 2.0
        self.scale.value *= 1.1892**(self.reverse_scroll*step)

    def set_on_redraw(self):
        "set-up mouse scroll control and click"
        @debounce(0.005)
        def on_draw(event):
            self.xb = self.ax.get_xbound()      # follow x and y box
            yb = self.ax.get_ybound()
            #  [-ymax/(yratio*scale), ymax/scale = [yb0, yb1]
            #  ymax/scale = yb1   =>  scale = ymax/yb1
            # -ymax/(yratio*scale) = yb0    =>   -ymax/(scale*yb0) = yratio
            ratio = -yb[1]/yb[0]
            if abs(ratio-self.yratio)/ratio > 0.1:      # more than 10% variation
                self.scale.value = self.ymax/yb[1]
                self.yratio = -self.ymax/(yb[0]*self.scale.value)
                self.yb0 = np.array( [-self.ymax/self.yratio, self.ymax] ) 
        self.cids_disp = self.fig.canvas.mpl_connect('draw_event', on_draw)
        def on_scroll(event):
            self.scale_up(event.step)
        if Activate_Wheel:
            self.fig.canvas.capture_scroll = True
            self.cids_scroll = self.fig.canvas.mpl_connect('scroll_event', on_scroll)

    def draw(self):
        "builds and display the picture"
        self.ax.clear()
        self.data.display(new_fig=False, figure=self.ax, title=self.title)
        self.drspectrum = self.data.disp_artist
        self.ax.set_xbound(self.xb)
        self.ax.set_ybound(self.yb0/self.scale.value)
        self.fig.canvas.header_visible = False
        for s in ["left", "top", "right"]:
            self.ax.spines[s].set_visible(False)
        self.ax.yaxis.set_visible(False)
        visible_ticks = {"top": False,   "right": False}
        self.ax.tick_params(axis="x", which="both", **visible_ticks)
        self.set_on_redraw()

    def disp(self):
        # scale = self.ax.get_ybound()
        # self.scale.value = self.yb0/scale
        # self.xb = self.ax.get_xbound()
        self.ax.set_ybound(self.yb0/self.scale.value)
        self.ax.set_xbound(self.xb)


class baseline1D(Show1D):
    """
    An interactive baseline for 1D NMR

        baseline1D(spectrum, ...)

    """

    def __init__(self, data, figsize=None, show=True, create_children=True, yratio=10):
        try:
            self.BslPoints = data.BslPoints    # recover from previous correction
        except:
            self.BslPoints = []   # initialize with empty list

        super().__init__(data, figsize=figsize,
                        show=False, create_children=create_children,yratio=yratio)
        self.data.real()
        a, b = self.itoc3(0.0), self.itoc3(self.data.size1)   # spectral borders
        self.select = widgets.FloatSlider(description='Manually:',
                                          min=min(a, b), max=max(a, b),
                                          value=self.itoc3(0.5*self.data.size1), continuous_update=REACTIVE,
                                          layout=space('40%'),
                                          tooltip='position of the selected point in %s' % (data.unit[0]))
        self.smooth = widgets.IntSlider(description='smooth:', min=0, max=20,  layout=space('40%'),
                                        tooltip='apply a local smoothing for pivot points',
                                        value=1, readout=True, continuous_update=REACTIVE)
        self.toshow = widgets.Dropdown(
            options=['baseline', 'corrected', 'points'],  description='Display:')

        self.bsl_points = self.BslPoints
        self.baseline = np.zeros_like(self.data.buffer)
        self.cancel = widgets.Button(description="Exit", button_style='warning', layout=self.blay,
                                     tooltip='Exit without corrections')
        self.auto = widgets.Button(description="Auto", button_style='success', layout=self.blay,
                                   tooltip='Automatic set of points')
        self.set = widgets.Button(description="Add", button_style='success', layout=self.blay,
                                  tooltip='Add a baseline point at the selector position')
        self.unset = widgets.Button(description="Rem", button_style='warning', layout=self.blay,
                                    tooltip='Remove the baseline point closest to the selector')
        self.nbautopoints = widgets.IntText(value=8, layout=space('4em'))
        if create_children:
            orig = self.children
            self.children = [VBox([
                HBox([Label('Baseline points'),
                      self.nbautopoints,
                      Label("points"),
                      self.auto,
                      self.blank,
                      self.select,
                      self.set, self.unset]),
                HBox([self.cancel, self.toshow, self.smooth]),
                HBox(orig)])]
        # add interaction
        # done button is activated by base class
        self.cancel.on_click(self.on_cancel)
        self.auto.on_click(self.on_auto)
        self.set.on_click(self.on_set)
        self.unset.on_click(self.on_unset)
        for w in [self.select, self.smooth, self.toshow]:
            w.observe(self.ob)

        def on_press(event):
            if event.button == 3:       # right-click
                v = event.xdata
                self.select.value = round(v, 3)
                self.disp()
        self.cids_press = self.fig.canvas.mpl_connect('button_press_event', on_press)
        # finalize
        if show:
            self.draw()

    def disconnect_press(self):
        "should be called before close()"
        self.fig.canvas.mpl_disconnect(self.cids_press)

    def itoc3(self, value):
        return round(self.data.axis1.itoc(value), 3)

    def on_cancel(self, e):
        self.disconnect_press()
        self.close()
        print('no baseline correction')

    def on_done(self, e):
        if self.bsl_points == []:
            jsalert(
                'Please define control points before applying baseline correction')
            return
        printlist = [round(i, 3) for i in sorted(self.bsl_points)]
        print("Applied correction:\ndata.bcorr(xpoints={0}, nsmooth={1})".format(
            printlist, self.smooth.value))
        sp = self.data     # short name for convenience
        sp.set_buffer(sp.get_buffer() - self.correction())
        self.drspectrum[0].set_xdata( sp.axis1.itoc( sp.axis1.points_axis() ) )    # I got once a shorten x display axis !!!  so restore here
        self.drspectrum[0].set_ydata(sp.get_buffer())
        self.clear()   # remove additional drawings
        self.drpivot.set_visible(True)   # but pivot points
        # self.ax.clear()
        # self.data.display(new_fig=False, figure=self.ax, title=self.title)
        self.drpivot.set_xdata(self.bsl_points)
        y = np.zeros(len(self.bsl_points))
        self.drpivot.set_ydata(y)
        # store for latter use
        self.data.BslPoints = self.bsl_points
        self.disconnect_press()
        self.close()
        display(self.fig)   # shows spectrum

    def on_auto(self, e):
        "automatically set baseline points"
        self.bsl_points = [self.data.axis1.itoc(x) for x in bcorr.autopoints(
            self.data, Npoints=self.nbautopoints.value)]
        self.disp()

    def on_set(self, e):
        "add baseline points at selector"
        self.bsl_points.append(self.select.value)
        self.disp()

    def on_unset(self, e):
        "remove baseline points closest from selector"
        if len(self.bsl_points) == 0:
            return
        here = self.select.value   # find
        distclose = np.inf
        pclose = np.NaN
        for i, p in enumerate(self.bsl_points):
            if abs(p-here) < distclose:
                pclose = p
                ipclose = i
                distclose = abs(p-here)
        self.bsl_points.remove(pclose)  # remove and clean display
        self.disp()

    def correction(self):
        "returns the correction to apply as a numpy array"
        ibsl_points = self.data.axis1.ptoi(
            np.array(self.bsl_points)).astype(int)
        x = np.arange(self.data.size1)
        yy = self.data.get_buffer()
        if len(self.bsl_points) == 0:
            return 0.0
        elif len(self.bsl_points) == 1:
            value = self.data[ibsl_points[0]] * np.ones(self.data.size1)
        elif len(self.bsl_points) < 4:
            corr = bcorr._linear_interpolate(
                yy, ibsl_points, nsmooth=self.smooth.value)
            value = corr(x)
        else:
            corr = bcorr._spline_interpolate(
                yy, ibsl_points, nsmooth=self.smooth.value)
            value = corr(x)
        return value

    def corrected(self):
        value = self.data.copy()
        value.set_buffer(value.get_buffer() - self.correction())
        return value

    def clear(self):
        "remove artists created by draw()"
#        self.drspectrum[0].set_visible(False)
        self.selector.set_visible(False)
        self.drhoriz.set_visible(False)
        self.drbaseline.set_visible(False)
        self.drpivot.set_visible(False)

    def draw(self):
        "used to create the view with additional artists"
        super().draw()
        # baseline
        self.drbaseline = self.ax.plot(
            self.data.axis1.unit_axis(), self.baseline, **Contrast)[0]
        # corrected spectrum
        corrected = self.data.get_buffer()-self.correction()
        # yoffset is used to make fig clearer
        yoffset = 0.2*self.ax.get_ybound()[1]
        self.drcorrected = self.ax.plot(
            self.data.axis1.unit_axis(), corrected+yoffset, alpha=0.5, **Contrast)
        # horizontal line at yoffset
        self.drhoriz = self.ax.axhline(yoffset, **Cursor)
        # selector
        ppos = self.select.value
        self.selector = self.ax.axvline(ppos, **Cursor)
        # pivots
        if len(self.bsl_points) > 0:
            y = bcorr.get_ypoints(self.data.get_buffer(),
                                  self.data.axis1.ptoi(
                                      np.array(self.bsl_points)),
                                  nsmooth=self.smooth.value)
            self.drpivot = self.ax.plot(
                self.bsl_points, y, marker='o', linestyle="None", **Contrast)[0]  # , markersize=12)
        else:
            self.drpivot = self.ax.plot(
                [], [], marker='o', linestyle="None", **Contrast)[0]  # , markersize=12)
        self.disp()

    def disp(self):
        "used to refresh view"
        # graphic objects: (drspectrum) drbaseline drpivot drcorrected drhoriz selector
        super().disp()
        if len(self.bsl_points) > 0:
            if self.toshow.value == 'baseline':
                self.drcorrected[0].set_visible(False)
                self.drhoriz.set_ydata(0.0)
                self.drhoriz.set_visible(True)
                self.drbaseline.set_ydata(self.correction())
                self.drbaseline.set_visible(True)
            elif self.toshow.value == 'corrected':
                self.drbaseline.set_visible(False)
                yoffset = 0.2*self.ax.get_ybound()[1]
                self.drcorrected[0].set_ydata(
                    self.data.get_buffer() - self.correction() + yoffset)
                self.drhoriz.set_ydata(yoffset)
                self.drhoriz.set_visible(True)
                self.drcorrected[0].set_visible(True)
            elif self.toshow.value == 'points':
                self.drbaseline.set_visible(False)
                self.drcorrected[0].set_visible(False)
                self.drhoriz.set_visible(False)
        else:
            self.drbaseline.set_visible(False)
            self.drcorrected[0].set_visible(False)
            self.drhoriz.set_visible(False)

        # selector
        ppos = self.select.value
        self.selector.set_xdata([ppos, ppos])
        # pivots
        # pivot points
        y = bcorr.get_ypoints(self.data.get_buffer(),
                              self.data.axis1.ptoi(np.array(self.bsl_points)),
                              nsmooth=self.smooth.value)
        #self.drpivot = self.ax.scatter(self.bsl_points, y,  c='r', marker='o')
        # self.drpivot = self.ax.plot(self.bsl_points, y, marker='o',linestyle="None", **Contrast)[0]#, markersize=12)
        self.drpivot.set_xdata(self.bsl_points)
        self.drpivot.set_ydata(y)
        # set zoom


class SpforSuper():
    "a holder for one spectrum to SuperImpose"

    def __init__(self, i, filename='None', axref=False, ref=None):
        """
        filename is the dataset to show  (None or Self possible)
        axref is the axes of the caller, where to display
        ref is a dataset in case filename is Self 
        """
        self.axref = axref
        self.ref = ref
        self.drartist = None
        self.filename = widgets.Text(
            value=filename, layout=Layout(width='30em'))
        self.filename.observe(self.nactivate)
        j = i % len(Colors)
        self.color = widgets.Dropdown(
            options=["steelblue"]+list(Colors), value=Colors[j], layout=space('90px'))
        self.direct = widgets.Dropdown(
            options=['up', 'down', 'off'], value='off', layout=space('60px'))
        self.direct.observe(self.activate)
        self.zmleft = widgets.FloatText(
            value=1000, step=0.1, layout=space('70px'))
        self.zmright = widgets.FloatText(
            value=-1000, step=0.1, layout=space('70px'))
        self.label = widgets.Checkbox(
            value=False, indent=False, layout=space('40px'))
        self.scale = widgets.FloatText(value=1.0, step=0.1, layout=space(
            '70px'), tooltip="relative intensity scale")
        self.stretch = widgets.FloatText(
            value=1.0, step=0.1, layout=space('70px'), tooltip="stretching along x")
        self.xoffset = widgets.FloatText(value=0.0, step=0.1, layout=space(
            '60px'), tooltip="x offset in spec unit")
        self.yoffset = widgets.FloatText(
            value=0.0, step=1, layout=space('60px'), tooltip="y offset in %")
        self.splw = widgets.FloatText(
            value=1.0, step=0.1, layout=space('50px'))
        # for w in (self.color, self.direct, self.zmleft, self.zmright, self.label, self.scale, self.offset, self.splw):
        #     w.observe(self.ob)
        # control bar
        self.me = HBox([self.B(str(i)),
                        self.filename,
                        self.scale,
                        self.stretch,
                        self.direct,
                        self.xoffset,
                        self.yoffset,
                        self.zmleft,
                        self.zmright,
                        self.splw,
                        self.color,
                        self.label,
                        ])
        self. header = HBox([self.B('Id', '40px'),
                             self.B('file name', '5em'),
                             self.I('or "Self" for spectrum excerpts', '26em'),
                             self.B('scale', '60px'),
                             self.B('stretching', '60px'),
                             self.B('direction', '60px'),
                             self.B('xoffset', '50px'),
                             self.B('yoffset(%)', '50px'),
                             self.B(' ', '50px'), self.B('Zoom', '90px'),
                             self.B('Linewidth', '80px'),
                             self.B('color', '70px'),
                             self.B('label')
                             ])
        self.nactivate(None)

    def ob(self, event):
        "observe events and display"
        if event['name'] == 'value':
            self.draw()

    def injectobserve(self, method):
        "used to to observe form outer object for draw"
        for w in (self.color,  self.zmleft, self.zmright, self.label, self.splw, self.direct,
                  self.scale, self.stretch, self.xoffset, self.yoffset):
            w.observe(method)

    def nactivate(self, e):
        "called to check name validity, when name is entered"
        fileexists = (self.filename.value == 'Self' or
                      (self.filename.value not in ('None', '') and Path(self.filename.value).exists()))
        if fileexists:
            self.direct.value = 'up'  # will call self.activate()
            self.direct.disabled = False
        else:
            self.direct.value = 'off'  # will call self.activate()
            self.direct.disabled = True

    def activate(self, e):
        "called when state changes"
        for w in (self.color, self.zmleft, self.zmright, self.label, self.scale, self.stretch,
                  self.xoffset, self.yoffset, self.splw):
            if self.direct.value == 'off':
                w.disabled = True
            else:
                w.disabled = False

    def B(self, text, size='auto'):
        "bold HTML widget"
        return widgets.HTML(value="<b>%s</b>" % text, layout=widgets.Layout(width=size))

    def I(self, text, size='auto'):
        "Italic HTML widget"
        return widgets.HTML(value="<i>%s</i>" % text, layout=widgets.Layout(width=size))

    def draw(self, unit='ppm'):
        if self.filename.value == 'None' or self.filename.value == '' or self.direct.value == 'off':
            return  # nothing to do
        # else
        if self.drartist is not None:   # remove previous ones
            del(self.drartist)
        # label
        if self.label.value:
            m = self.mult
            if not (abs(m) == 1 or m == 0):
                lb = "%s (x%.1f)" % (self.nmrname,m)
            else:
                lb = self.nmrname
        else:
            lb = None
        # load
        if self.filename.value == 'Self':
            d = self.ref.copy().set_unit(unit)
        else:
            try:
                d = NMRData(name=self.filename.value).set_unit(unit)
            except:
                self.direct.value = 'off'
                jsalert('%s : cannot open File' % self.filename.value)
                return
        # draw
        zml = min(self.zmleft.value, d.axis1.itoc(0))
        zmr = max(self.zmright.value, d.axis1.itoc(d.size1))
        d.display(
            scale=1,
            figure=self.axref,
            zoom=(zml, zmr),
            color=self.color.value,
            linewidth=self.splw.value,
            label=lb)
        self.drartist = d.disp_artist[0]
        self.ydata = self.drartist.get_ydata()   # store spectral data
        self.xdata = self.drartist.get_xdata()
        # adapt to scale and direction
        self.drartist.set_ydata(self.move_ydata())
        self.drartist.set_xdata(self.move_xdata())
        # text
        # will be changed by disp()
        self.text = self.axref.text(
            zmr, self.ydata[-1], ' ', color=self.color.value)
        self.disp()

    def move_xdata(self):
        "adapt self.xdata to  offset and stretching, then  return the value"
        # get starting value
        start = self.xdata[0]
        # affine transform
        return self.xdata[0] + self.stretch.value*(self.xdata - start) + self.xoffset.value

    def move_ydata(self):
        "adapt self.ydata to direct, scale, and offset and return the value"
        # add offset
        # relative to screen in spectral unit
        off = self.axref.get_ybound()[1] * self.yoffset.value/100
        # and multiply
        return self.mult*self.ydata + off

    def disp(self):
        " refresh "
        if self.filename.value == 'None' or self.direct.value == 'off':
            return  # nothing to do
        yd = self.move_ydata()
        xd = self.move_xdata()
        self.drartist.set_ydata(yd)
        self.drartist.set_xdata(xd)
        self.text.set_text(" ")     # empty text
        if not self.label.value:    # unless... there is a scale != 1, and no label
            m = self.mult
            if not (abs(m) == 1 or m == 0):
                tt = "x%.1f" % (m,)
                self.text.set_text(tt)
                self.text.set_y(yd[-1])
                self.text.set_x(xd[-1])

    @property
    def mult(self):
        if self.direct.value == 'up':
            mult = self.scale.value
        elif self.direct.value == 'down':
            mult = -self.scale.value
        else:
            mult = 1
        return mult

    @property
    def nmrname(self):
        fnm = self.filename.value
        if fnm not in ('None', 'Self'):
            return op.join(op.basename(op.dirname(fnm)), op.basename(fnm))
        elif fnm == 'Self':
            return 'excerpt'
        else:
            return None

    def simpledisplay(self):
        "a simplified widget "
        lb = widgets.Label(self.nmrname)
        return HBox([lb, self.scale])


class Show1Dplus(Show1D):
    def __init__(self, data, base='/DATA', N=9, **kw):
        show = kw.get('show', True)
        kw['show'] = False
        super().__init__(data, **kw)
        self.NbMaxPeaks = Peaks.NbMaxDisplayPeaks
        # spectrum control widgets
        self.sptitle = widgets.Text(description='Title',
                                    value=self.title, layout=space('40%'))
        self.spcolor = widgets.Dropdown(description='Color',
                                        options=["steelblue"]+list(Colors), value='steelblue',
                                        layout=space('24%'))
        self.splw = widgets.FloatText(description='Linewidth',
                                      value=1.0, step=0.1,
                                      layout=space('24%'))
        self.showlogo = widgets.Checkbox(description="Logo",
                                         value=True, layout=widgets.Layout(left='200px'))
        self.axlogo = self.fig.add_axes([.92, .84, .08, .16], visible=True)
        self.axlogo.imshow(plt.imread(UserLogofile(), 'rb'))
        self.axlogo.set_axis_off()

        def switchlogo(e):
            if self.showlogo.value:
                self.axlogo.set_visible(True)
            else:
                self.axlogo.set_visible(False)
        self.showlogo.observe(switchlogo)
        SP = VBox([HBox([self.sptitle, self.showlogo]),
                   HBox([self.spcolor, self.splw])])
        # integral control widgets
        self.integ = widgets.Checkbox(description='Show',
                                      value=False)
        self.scaleint = widgets.FloatSlider(description='Scale',
                                            value=0.5, min=0.1, max=10, step=0.05,
                                            layout=Layout(width='40%'),
                                            continuous_update=REACTIVE)
        self.offset = widgets.FloatSlider(description='Offset',
                                          value=0.4, min=0.0, max=1.0, step=0.01,
                                          layout=Layout(width='45%'),
                                          continuous_update=REACTIVE)
        self.intlw = widgets.FloatText(description='Linewidth',
                                       value=1.5, step=0.1,
                                       layout=Layout(width='24%'))
        self.intcolor = widgets.Dropdown(description='Color',
                                         options=Colors, value='red',
                                         layout=Layout(width='45%'))
        self.intlabelcolor = widgets.Dropdown(description='Label',
                                              options=Colors, value='crimson',
                                              layout=Layout(width='40%'))
        self.labelx = widgets.FloatText(description='Label X pos',
                                        value=1, step=0.1,
                                        layout=Layout(width='24%'))
        self.labely = widgets.FloatText(description='Y pos',
                                        value=-1, step=0.1,
                                        layout=Layout(width='24%'))
        self.labelfont = widgets.BoundedFloatText(description='Size',
                                                  value=8, mini=1, maxi=128, step=1,
                                                  layout=Layout(width='24%'))
        self.labelrot = widgets.BoundedFloatText(description='Angle',
                                                 value=0, mini=-90, maxi=90, step=1,
                                                 layout=Layout(width='24%'))
        INT = VBox([HBox([widgets.HTML('<b>Integrals</b>'), self.integ]),
                    HBox([self.scaleint, self.offset, self.intlw]),
                    HBox([self.intcolor, self.intlabelcolor]),
                    HBox([self.labelx, self.labely, self.labelfont, self.labelrot])
                    ], layout=Layout(width='60%'))
        # peaks control widgets
        self.peaks = widgets.Checkbox(description='Show',value=False, tooltip="show/hide peaks on the spectrum")
        markers = ['None', 'x', 'X', 'd', 'D', 'v', '^', '<', '>', '|']
        self.marker = widgets.Dropdown(description='Marker',
                                       options=markers, value="x",
                                       layout=Layout(width='20%'))
        self.pkcolor = widgets.Dropdown(description='Color',
                                        options=Colors, value='darkblue',
                                        layout=Layout(width='40%'))
        self.pkvalues = widgets.Checkbox(description='Values',
                                         value=False, layout=Layout(width='30%'))
        self.pkrotation = widgets.BoundedFloatText(description='Angle',
                                                   value=45, mini=-90, maxi=90, step=1,
                                                   layout=Layout(width='20%'))
        self.pkfont = widgets.BoundedFloatText(description='Size',
                                               value=8, mini=1, maxi=128, step=1,
                                               layout=Layout(width='20%'))
        PK = VBox([HBox([widgets.HTML('<b>Peaks</b>'), self.peaks]),
                   HBox([self.marker, self.pkcolor]),
                   HBox([self.pkvalues, self.pkfont, self.pkrotation])
                   ], layout=Layout(width='60%'))
        # Superposition control widgets
        # (base=base, filetype="*.gs1", mode='r', show=False)
        self.Chooser = FileChooser_code(path='/DATA/', filename='.gs1')
        self.bsel = widgets.Button(description='Copy', layout=self.blay,
                                   button_style='info',  # 'success', 'info', 'warning', 'danger' or ''
                                   tooltip='copy selected data-set to entry below')
        self.to = widgets.IntText(
            value=1, min=1, max=N, layout=Layout(width='10%'))
        self.bsel.on_click(self.copy)
        self.DataList = [SpforSuper(
            i+1, axref=self.ax, ref=self.data) for i in range(N)]
        for s in self.DataList:
            s.injectobserve(self.ob)
        self.DataList[0].color.value = 'red'

        for widg in (self.sptitle, self.spcolor, self.splw,
                     self.peaks, self.integ, self.scaleint, self.offset, self.intlw,
                     self.intcolor, self.intlabelcolor, self.labelx, self.labely, self.labelfont, self.labelrot,
                     self.pkvalues, self.marker, self.pkcolor, self.pkrotation, self.pkfont):
            widg.observe(self.ob)

        orig = self.children
        controls = [SP]
        try:
            self.data.peaks
            controls.append(PK)
        except:
            pass
        try:
            self.data.integrals
            controls.append(INT)
        except:
            pass

        self.tabs = Tab()

        self.tabs.children = [
            VBox([VBox(controls),
                  HBox(orig),
                  ]),
            VBox([Label("Choose spectra to superimpose - first Select, then Copy to the numbered slot"),
                  HBox([self.Chooser, self.bsel, self.to]),
                  self.DataList[0].header] +
                 [sp.me for sp in self.DataList]
                 )
        ]

        self.tabs.set_title(0, 'Spectrum')
        self.tabs.set_title(1, 'Superimpose')

        self.children = [self.tabs]
        if show:
            self.draw()

    def copy(self, event):
        if self.to.value < 1 or self.to.value > len(self.DataList):
            jsalert('Destination is out of range !')
        else:
            entry = self.DataList[self.to.value-1]
            entry.filename.value = self.Chooser.selected
            entry.direct.value = 'up'
            for w in entry.me.children:
                w.observe(self.ob)
            self.to.value = min(self.to.value, len(self.DataList)) + 1

    def on_done(self, e):
        self.close()
        self.disp()
        display(self.fig)

    def draw(self, zoom=False):
        "refresh display - if zoom is True, display only in xbound"
        super().draw()
        self.xb = self.ax.get_xbound()
        if zoom:
            zoom = self.xb
        else:
            zoom = None
        # self.data.display(new_fig=False, figure=self.ax, color=self.spcolor.value,
        #                     title=self.sptitle.value, linewidth=self.splw.value, zoom=zoom)
        self.drspectrum[0].set_color(self.spcolor.value)
        self.drspectrum[0].set_linewidth(self.splw.value)
        self.ax.set_title(self.sptitle.value)
        if self.integ.value:
            if hasattr(self.data,'integrals') and self.data.integrals != []:
                if self.labely.value == -1:
                    labely = None
                else:
                    labely = self.labely.value
                self.data.display_integral(label=True, integscale=self.scaleint.value,
                                           integoff=self.offset.value,
                                           labelxposition=self.labelx.value,
                                           labelyposition=labely,
                                           curvedict={
                                               'color': self.intcolor.value, 'linewidth': self.intlw.value},
                                           labeldict={
                                               'color': self.intlabelcolor.value, 'fontsize': self.labelfont.value, 'rotation': self.labelrot.value},
                                           figure=self.ax, zoom=zoom)
            else:
                #                print('no or wrong integrals (have you clicked on "Done" in the Integration tool ?)')
                pass
        if self.peaks.value:
            if hasattr(self.data,'peaks') and self.data.peaks != []:
                self.data.display_peaks(peak_label=self.pkvalues.value,
                                        color=self.pkcolor.value,
                                        markersize=self.pkfont.value,
                                        NbMaxPeaks=self.NbMaxPeaks,
                                        markerdict={
                                            'marker': self.marker.value},
                                        labeldict={'rotation': self.pkrotation.value,
                                                   'fontsize': self.pkfont.value},
                                        figure=self.ax, scale=1.0, zoom=zoom)
            else:
                #                print('no or wrong peaklist (have you clicked on "Done" in the Peak-Picker tool ?)')
                pass
        # superimposition
        for s in self.DataList:
            s.draw()

        self.ax.set_xbound(self.xb)

    # def disp(self, zoom=False):
    #     self.xb = self.ax.get_xbound()
    #     if zoom:
    #         zoom = self.xb
    #     else:
    #         zoom = None
        super().disp()
        for s in self.DataList:
            s.disp()


class Phaser1D(Show1D):
    """
    An interactive phaser in 1D NMR

        Phaser1D(spectrum, ...)

    """

    def __init__(self, data, figsize=None, title=None, maxfirstorder=360, show=True, warning=True, create_children=True, yratio=10):
        data.check1D()
        if data.itype == 0:
            if warning:
                jsalert('Data is Real - the Imaginary part is being reconstructed')
            data.real2cpx()
        # we'll work on a copy of the data
        super().__init__(data, figsize=figsize, title=title,
                         show=False, create_children=create_children, yratio=yratio)
        self.ydata = data.get_buffer()   # store (complex) buffer

        # change done button and create an Apply one
        self.done.description = 'Apply'
        self.p0 = widgets.FloatSlider(description='P0:', min=-200, max=200, step=0.1,
                                      layout=Layout(width='100%'), continuous_update=REACTIVE)
        self.p1 = widgets.FloatSlider(description='P1:', min=-maxfirstorder, max=maxfirstorder, step=1.0,
                                      layout=Layout(width='100%'), continuous_update=REACTIVE)
        # self.pivot = widgets.FloatSlider(description='pivot:',
        #                 min=0.0, max=self.data.size1,
        #                 step=1, layout=Layout(width='80%'),
        #                 value=0.5*self.data.size1, readout=False, continuous_update=REACTIVE)
        pivl = self.data.axis1.itoc(self.data.size1)
        pivr = self.data.axis1.itoc(0)
        self.pivot = widgets.BoundedFloatText(description='Pivot',
                                              value=round(self.data.axis1.itoc(
                                                  0.5*self.data.size1), 2),
                                              min=min(pivr, pivl),
                                              max=max(pivr, pivl),
                                              format='%.3f',
                                              step=0.1, layout=Layout(width='20%'))
        self.cancel = widgets.Button(
            description="Exit", button_style='warning', tooltip='Exit without corrections')
        # draw HBox
        if create_children:
            orig = self.children
            self.children = [VBox([
                HBox([self.cancel, self.pivot, widgets.HTML(
                    '<i>set with right-click on spectrum</i>')]),
                self.p0,
                self.p1,
                HBox(orig)])]
        # add interaction
        self.cancel.on_click(self.on_cancel)
        self.done.on_click(self.on_Apply)
        for w in [self.p0, self.p1]:
            w.observe(self.ob)
        self.pivot.observe(self.on_movepivot)
        # add click event on spectral window

        def on_press(event):
            if event.button == 3:       # right-click
                v = event.xdata
                self.pivot.value = round(v, 4)
                self.disp()
        self.cids_press = self.fig.canvas.mpl_connect('button_press_event', on_press)
        # finalize
        self.lp0, self.lp1 = self.ppivot()
        if show:
            self.draw()

    def disconnect_press(self):
        "should be called before close()"
        self.fig.canvas.mpl_disconnect(self.cids_press)

    def on_cancel(self, b):
        self.disconnect_press()
        self.close()
        print("no applied phase correction")

    def on_Apply(self, b):
        self.drpivot.set_visible(False)
        lp0, lp1 = self.ppivot()  # get centered values
        self.data.phase(lp0, lp1)
        self.disconnect_press()
        self.close()
        # display(self.fig)   # would show twice - no idea why ...
        print("Applied: data.phase(%.1f,  %.1f)" % (lp0, lp1))

    def ppivot(self):
        "converts from pivot values to centered ones"
        pp = 1.0-(self.data.axis1.ctoi(self.pivot.value)/self.data.size1)
        return (self.p0.value + (pp-0.5)*self.p1.value, self.p1.value)

    def ctopivot(self, p0, p1):
        "convert from centered to pivot values"
        pp = 1.0-(self.data.axis1.ctoi(self.pivot.value)/self.data.size1)
        p0p = p0 - (pp-0.5)*p1
        while p0p > 180:
            p0p -= 360
        while p0p < -180:
            p0p += 360
        return p0p, p1

    def on_movepivot(self, event):
        if event['name'] == 'value':
            if self.p1.value != 0:   # in that case values do not change
                self.p0.value, self.p1.value = self.ctopivot(
                    self.lp0, self.lp1)
            else:
                self.disp()

    @debounce(1.0)
    def rescale_p1(self):
        "shifts by +/- 180°  if P1 hits the border"
        if self.p1.value == self.p1.max:
            self.p1.max += 180
            self.p1.min += 180
        if self.p1.value == self.p1.min:
            self.p1.max -= 180
            self.p1.min -= 180

    def ob(self, event):
        "observe changes and start phasing"
        if event['name'] == 'value':
            self.rescale_p1()
            self.phase_n_disp()

    def draw(self):
        "copied from super() as it does not display the spectrum !"
#         self.ax.clear()
#         self.drspectrum = self.ax.plot(self.data.axis1.unit_axis()[::2] , self.ydata.real, lw=1 )[0]
        super().draw()
        self.phase()
        ppos = self.pivot.value
        self.drpivot = self.ax.axvline(ppos, **Cursor)

    def phase(self):
        self.lp0, self.lp1 = self.ppivot()         # get centered values
        size = len(self.ydata)
        # compute correction in e
        if self.lp0 == 0:  # we can optimize a little
            e = np.exp(1J*m.radians(self.lp0)) * \
                np.ones(size, dtype=complex)   # e = exp(j ph0)
        else:
            le = m.radians(self.lp0) + (m.radians(self.lp1)) * \
                np.linspace(-0.5, 0.5, size)
            e = np.cos(le) + 1J*np.sin(le)
        # then apply
        y = (e*self.ydata).real
        self.drspectrum[0].set_ydata(y)   # multiply and keep only real values

    def phase_n_disp(self):
        "apply phase and disp"
        self.phase()
        self.disp()

    def disp(self):
        "update display and display pivot"
        super().disp()
        ppos = self.pivot.value
        self.drpivot.set_xdata([ppos, ppos])
        self.drpivot.set_ydata(self.ax.get_ybound())


class AvProc1D:
    "Detailed 1D NMR Processing"

    def __init__(self, filename=""):
        print('WARNING this tool is not functional/tested yet')
        self.wfile = widgets.Text(
            description='File to process', layout=Layout(width='80%'), value=filename)
        self.wapod = widgets.Dropdown(
            options=['None', 'apod_sin (sine bell)', 'apod_em (Exponential)',
                     'apod_gm (Gaussian)', 'gaussenh (Gaussian Enhacement)', 'kaiser'],
            value='apod_sin (sine bell)',
            description='Apodisation')
        self.wpapod_Hz = widgets.FloatText(
            value=1.0,
            min=0,  # max exponent of base
            max=30,  # min exponent of base
            description='Width in Hz',
            layout=Layout(width='15%'),
            disabled=True)
        self.wpapod_enh = widgets.FloatText(
            value=2.0,
            min=0.0,  # max exponent of base
            max=5.0,  # min exponent of base
            description='strength',
            layout=Layout(width='15%'),
            step=1,
            disabled=True)
        self.wpapod_sin = widgets.FloatText(
            value=0.0,
            min=0,  # max exponent of base
            max=0.5,  # min exponent of base
            description='bell shape',
            layout=Layout(width='15%'),
            step=0.01,
            tooltip='value is the maximum of the bell, 0 is pure cosine, 0.5 is pure sine',
            disabled=False)
        self.wzf = widgets.Dropdown(
            options=[0, 1, 2, 4, 8],
            value=1,
            description='Zero-Filling')
        self.wphase0 = widgets.FloatText(
            value=0, description='Phase : P0', layout=Layout(width='20%'), disabled=True)
        self.wphase1 = widgets.FloatText(
            value=0, description='P1', layout=Layout(width='20%'), disabled=True)
        self.wapmin = widgets.Checkbox(
            value=True, description='AutoPhasing', tooltip='Perform AutoPhasing')
        self.wapmin.observe(self.apmin_select)
        self.wbcorr = widgets.Checkbox(
            value=False, description='Baseline Correction', tooltip='Perform Baseline Correction')
        self.wapod.observe(self.apod_select)
        self.bapod = widgets.Button(description='Show effect on FID')
        self.bapod.on_click(self.show_apod)
        self.bdoit = widgets.Button(description='Process')
        self.bdoit.on_click(self.process)
        self.show()
        fi, ax = plt.subplots()
        self.ax = ax
        if os.path.exists(filename):
            self.load()
            # self.data.set_unit('sec')
            self.display()

    def apod_select(self, e):
        test = self.wapod.value.split()[0]
        self.wpapod_sin.disabled = True
        self.wpapod_Hz.disabled = True
        self.wpapod_enh.disabled = True
        if test == "apod_sin":
            self.wpapod_sin.disabled = False
        if test in ('apod_em', 'apod_gm', 'gaussenh'):
            self.wpapod_Hz.disabled = False
        if test == 'gaussenh':
            self.wpapod_enh.disabled = False

    def apmin_select(self, e):
        for w in self.wphase0, self.wphase1:
            w.disabled = self.wapmin.value

    def load(self):
        self.data = Import_1D(self.wfile.value)

    def apod(self):
        func = self.wapod.value.split()[0]
        todo = None
        if func == 'apod_sin':
            todo = 'self.data.apod_sin(%f)' % (self.wpapod_sin.value,)
        elif func in ('apod_em', 'apod_gm'):
            todo = 'self.data.%s(%f)' % (func, self.wpapod_Hz.value)
        elif func == 'gaussenh':
            todo = 'self.data.gaussenh(%f,enhancement=%f)' % (
                self.wpapod_Hz.value, self.wpapod_enh.value)
        if todo is not None:
            eval(todo)
        return self.data

    def show_apod(self, e):
        self.load()
        self.apod()
        self.display()

    def process(self, e):
        self.load()
        self.apod().zf(self.wzf.value).ft_sim().bk_corr().set_unit('ppm')
        if self.wapmin.value:
            self.data.apmin()
            self.wphase0.value = round(self.data.axis1.P0, 1)
            self.wphase1.value = self.data.axis1.P1
        else:
            self.data.phase(self.wphase0.value, self.wphase1.value)
        self.display()

    def display(self):
        self.ax.clear()
        self.data.display(figure=self.ax)

    def show(self):
        display(
            VBox([self.wfile,
                  HBox([self.wapod, self.wpapod_sin, self.wpapod_Hz,
                        self.wpapod_enh, self.bapod]),
                  self.wzf,
                  HBox([self.wapmin, self.wphase0, self.wphase1]),
                  #                self.wbcorr,
                  self.bdoit]))


class NMRPeaker1D(Show1D):
    """
    a peak-picker for NMR experiments
    """
    # self.peaks : the defined peaklist, copyied in and out of data
    # self.temppk : the last computed pklist

    def __init__(self, data, figsize=None, show=True):
        self.data = data.real()
        try:
            if self.data.peaks is None:  # might happen
                self.peaks = Peaks.Peak1DList(
                    source=self.data)   # empty peak list
            else:
                self.peaks = self.data.peaks
        except AttributeError:
            self.peaks = Peaks.Peak1DList(source=self.data)   # empty peak list
        self.temppk = Peaks.Peak1DList(source=self.data)      # empty peak list
        self.thresh = widgets.FloatLogSlider(value=20.0,
                                             min=-1, max=2.0, base=10, step=0.01, layout=Layout(width='30%'),
                                             continuous_update=True, readout=True, readout_format='.2f')
        try:
            self.thresh.value = 100*self.data.peaks.threshold / \
                self.data.absmax  # if already peak picked
        except:
            self.thresh.value = 20.0
        self.thresh.observe(self.pickpeak)
        self.peak_mode = widgets.Dropdown(
            options=['marker', 'bar'], value='marker', description='show as')
        self.peak_mode.observe(self.ob)
        self.out = Output(layout={'border': '1px solid red'})
#        self.done = widgets.Button(description="Done", button_style='success')
#        self.done.on_click(self.on_done)
        self.badd = widgets.Button(
            description="Add", button_style='success', layout=space('80px'))
        self.badd.on_click(self.on_add)
        self.brem = widgets.Button(
            description="Rem", button_style='warning', layout=space('80px'))
        self.brem.on_click(self.on_rem)
        self.cancel = widgets.Button(
            description="Exit", button_style='warning', layout=space('80px'), tooltip='Exit without corrections')
        self.cancel.on_click(self.on_cancel)
        self.selval = widgets.FloatText(
            value=0.0, description='', layout=Layout(width='10%'), step=0.001, disabled=True)
        self.newval = widgets.FloatText(
            value=0.0, description='calibration', layout=Layout(width='20%'), step=0.001, disabled=True)
        self.setcalib = widgets.Button(description="Set", layout=Layout(width='10%'),
                                       button_style='success', tooltip='Set spectrum calibration')
        self.setcalib.on_click(self.on_setcalib)
        super().__init__(data, figsize=figsize, show=False)

        def on_press(event):
            if event.button == 3:       # right-click
                v = event.xdata
                # store position in index (peaks are internally in index)
                iv = self.data.axis1.ptoi(v)
                distclose = np.inf     # search closest peak

                pclose = 0.0
                for p in self.data.peaks:
                    if abs(p.pos-iv) < distclose:
                        pclose = p.pos
                        distclose = abs(p.pos-iv)
                self.selval.value = self.data.axis1.itop(pclose)  # back to ppm
                for w in (self.selval, self.newval):
                    w.disabled = False
        self.cids_press = self.fig.canvas.mpl_connect('button_press_event', on_press)
        # redefine Box
        orig = self.children
        self.tabs = Tab()
        self.tabs.children = [
            VBox([
                HBox([Label('threshold - % largest signal'),
                      self.thresh, self.badd, self.brem, self.peak_mode]),
                HBox(
                    [VBox([self.blank, self.reset, self.scale, self.done]), self.fig.canvas])
            ]),
            VBox([
                HBox([Label('Select a peak with mouse (or value) and set calibrated values',layout=Layout(width="40%")),
                      Label('selection (in ppm)'),self.selval, self.newval, self.setcalib]),
                HBox(
                    [VBox([self.blank, self.reset, self.scale, self.done]), self.fig.canvas])
            ]),
            self.out]
        self.tabs.set_title(0, 'Peak Picker')
        self.tabs.set_title(1, 'calibration')
        self.tabs.set_title(2, 'Peak Table')

        self.children = [VBox([HBox([self.cancel]), self.tabs])]

        self.pp()
        self.draw()

    def on_add(self, b):
        self.peaks.pkadd(self.temppk)
        self.peaks = Peaks.peak_aggreg(self.peaks, distance=1.0)
        self.peaks.source = self.data
        self.temppk = Peaks.Peak1DList(source=self.data)
        self.draw()

    def on_rem(self, b):
        (up, down) = self.ax.get_xbound()
        iup = self.data.axis1.ptoi(up)
        idown = self.data.axis1.ptoi(down)
        iup, idown = (max(iup, idown), min(iup, idown))
        to_rem = []
        for pk in self.peaks:
            if pk.pos < iup and pk.pos > idown:
                to_rem.append(pk)
        for pk in to_rem:
            self.peaks.remove(pk)
        self.draw()

    def on_cancel(self, b):
        self.close()
        del self.data.peaks
        print("no Peak-Picking done")

    def on_reset(self, b=None):
        self.thresh.value = 20.0
        super().on_reset()

    def on_done(self, b):
        self.temppk = Peaks.Peak1DList()  # clear temp peaks
        self.close()
        # new figure
        self.draw()
        # and display
        display(self.fig)
        display(self.out)
        self.data.peaks = self.peaks  # and copy

    def on_setcalib(self, e):
        off = self.selval.value-self.newval.value
        # off is in ppm, axis1.offset is in Hz
        self.data.axis1.offset -= off*self.data.axis1.frequency
        self.selval.value = self.newval.value
#        self.pp()
        self.peaks.pos2label()
        self.temppk.pos2label()
        self.draw()

    def pkprint(self, event):
        self.out.clear_output(wait=True)
        with self.out:
            if len(self.temppk) > 0:
                display(HTML("<p style=color:red> Transient peak list </p>"))
                self.data.peaks = self.temppk
                display(HTML(self.data.pk2pandas().to_html()))
            if len(self.peaks) > 0:
                display(HTML("<p style=color:blue> Defined peak list </p>"))
                self.data.peaks = self.peaks
                display(HTML(self.data.pk2pandas().to_html()))
            else:
                display(HTML("<p style=color:blue> Defined peak list Empty</p>"))

    def _pkprint(self, event):
        self.out.clear_output(wait=True)
        with self.out:
            print(self.pklist())

    def pklist(self):
        "creates peaklist for printing or exporting"
        text = ["ppm\tInt.(%)\twidth (Hz)"]
        data = self.data
        intmax = max(data.peaks.intens)/100
        for pk in data.peaks:
            ppm = data.axis1.itop(pk.pos)
            width = 2*pk.width*data.axis1.specwidth/data.size1
            l = "%.3f\t%.1f\t%.2f" % (ppm, pk.intens/intmax, width)
            text.append(l)
        return "\n".join(text)

    def ob(self, event):
        if event['name'] == 'value':
            self.disp()

    def pickpeak(self, event):
        "interactive wrapper to pp"
        if event['name'] == 'value':
            self.disp()
            self.pp()

    @debounce(1.0)
    def pp(self):
        "do the peak-picking calling pp().centroid() within the current zoom"
        th = self.data.absmax*self.thresh.value/100
        zm = self.ax.get_xbound()
        self.data.set_unit('ppm').peakpick(
            threshold=th, verbose=False, zoom=zm).centroid()
        self.temppk = self.data.peaks
        self.data.peaks = None
        self.draw()
        self.ax.annotate('%d peaks detected' % len(
            self.data.peaks), (0.05, 0.95), xycoords='figure fraction')
        self.pkprint({'name': 'value'})

    def draw(self):
        "interactive wrapper to peakpick"
        super().draw()
        self.xb = self.ax.get_xbound()
        x = [self.data.axis1.itoc(z) for z in (0, self.data.size1)]
        y = [self.data.absmax*self.thresh.value/100]*2
        self.threshline = self.ax.plot(x, y, ':r')[0]
        if True:  # try:
            self.temppk.display(peak_label=False, peak_mode=self.peak_mode.value,
                                f=self.data.axis1.itoc, figure=self.ax, color='red')
            self.peaks.display(peak_label=False, peak_mode=self.peak_mode.value,
                               f=self.data.axis1.itoc, color='blue', figure=self.ax)
        else:  # except:
            rrr("problem")
        self.temppk.display(
            peak_label=True, peak_mode=self.peak_mode.value, color='red', figure=self.ax)
        self.ax.set_xbound(self.xb)
        # self.ax.set_ylim(ymax=self.data.absmax/self.scale.value)
        # send pseudo event to display peak table
        self.pkprint({'name': 'value'})
        self.disp()

    def disp(self):
        y = [self.data.absmax*self.thresh.value/100]*2
        self.threshline.set_ydata(y)
        super().disp()


class NMRIntegrate(Show1D):
    "an integrator for NMR experiments"

    def __init__(self, data, figsize=None, show=True):
        try:
            self.Integ = data.integrals
        except:
            # initialize with empty list
            self.Integ = Integrals(data, compute=False)
        try:
            self.peaks_reserved = data.peaks
        except AttributeError:
            #print('no peaks')
            self.peaks_reserved = None
        self.idisp = 0
        self.idraw = 0
        super().__init__(data, figsize=figsize, show=show)
        self.thresh = widgets.FloatLogSlider(description='sensitivity', value=10.0,
                                             min=-1, max=2.0, base=10, step=0.01, layout=Layout(width='30%'),
                                             continuous_update=HEAVY, readout=True, readout_format='.1f',
                                             tooltip='sensitivity to weak signals')
        self.bias = widgets.FloatSlider(
            description='bias', layout=Layout(width='20%'),
            value=0.0, min=-10.0, max=10.0, step=0.1,
            continuous_update=HEAVY, readout=True, readout_format='.1f')
        self.sep = widgets.FloatSlider(
            description='separation', layout=Layout(width='30%'),
            value=3.0, min=0.0, max=20.0, step=0.1,
            continuous_update=HEAVY, readout=True, readout_format='.1f')
        self.wings = widgets.FloatSlider(
            description='extension', layout=Layout(width='30%'),
            value=5.0, min=0.5, max=20.0, step=0.1,
            continuous_update=HEAVY, readout=True, readout_format='.1f')
        for w in (self.bias, self.sep, self.wings):
            w.observe(self.integrate)
        self.thresh.observe(self.peak_and_integrate)
        self.cancel = widgets.Button(
            description="Exit", button_style='warning', layout=self.blay, tooltip='Exit without corrections')
        self.cancel.on_click(self.on_cancel)

        self.badd = widgets.Button(description="Add", layout=self.blay,
                                   button_style='success', tooltip='Add an entry from current zoom')
        self.badd.on_click(self.on_add)
        self.brem = widgets.Button(description="Rem", layout=self.blay,
                                   button_style='warning', tooltip='Remove all entries in current zoom')
        self.brem.on_click(self.on_rem)
        self.bauto = widgets.Button(description="Compute", layout=self.blay,
                                    button_style='success', tooltip='Automatic definition of integrals')
        self.bauto.on_click(self.peak_and_integrate)

        self.entry = widgets.IntText(value=0, description='Entry', min=0, layout=Layout(width='25%'),
                                     tooltip='Index of the calibrating integral')
        self.value = widgets.FloatText(value=100, description='Value', layout=Layout(width='25%'),
                                       tooltip='Value of the calibrating integral')
        self.set = widgets.Button(
            description="Set", button_style='success', layout=Layout(width='10%'))
        self.set.on_click(self.set_value)
        self.out = Output(layout={'border': '1px solid red'})
        # redefine children
        orig = self.children
        self.tabs = Tab()
        self.tabs.children = [
            VBox([
                HBox([widgets.HTML("Use buttons to add and remove integrals in the current zoom window&nbsp;&nbsp;"),
                      self.badd, self.brem]),
                HBox(orig),
            ]),
            VBox([HBox([widgets.HTML("Define integral shapes using the sliders below (erases the current integrals)&nbsp;&nbsp;"), self.bauto]),
                  HBox([self.thresh,  self.sep, self.wings]),
                  HBox(orig),
                  ]),
            VBox([HBox([Label('Choose an integral for calibration'),
                        self.entry, self.value, self.set]),
                  self.out])
        ]
        self.tabs.set_title(0, 'Manual integration')
        self.tabs.set_title(1, 'Automatic')
        self.tabs.set_title(2, 'Integral Table & Calibration')
        self.children = [VBox([self.cancel, self.tabs])]
        self.draw()
        self.print(None)

    def on_cancel(self, b):
        self.close()
        print("No integration")

    def on_done(self, b):
        self.close()
        self.draw()
        display(self.fig)
        display(self.out)
        self.data.integrals = self.Integ        # copy integrals
        if self.peaks_reserved:                # restore peaks
            self.data.peaks = self.peaks_reserved
        else:
            try:
                del(self.data.peaks)
            except AttributeError:    # if no peaks
                pass

    def on_add(self, b):
        start, end = self.ax.get_xbound()
        self.Integ.append(Integralitem(self.data.axis1.ptoi(
            start), self.data.axis1.ptoi(end), [], 0.0))
        self.Integ.zonestocurves()
        self.draw()
        self.print(None)

    def on_rem(self, b):
        start, end = self.ax.get_xbound()
        start, end = self.data.axis1.ptoi(start), self.data.axis1.ptoi(end)
        # -1,+1 needed sometimes...
        start, end = (min(start, end)-1, max(start, end)+1)
        to_rem = []
        for ii in self.Integ:
            if ii.start > start and ii.end < end:
                to_rem.append(ii)
        for ii in to_rem:
            self.Integ.remove(ii)
        self.draw()
        self.print(None)

    def set_value(self, b):
        self.Integ.recalibrate(self.entry.value, self.value.value)
        self.draw()
        self.print(None)

    def print(self, event):
        self.out.clear_output()
        self.Integ.sort(key=lambda x: x.start)   # sort integrals
        with self.out:
            display(HTML(self.Integ.to_pandas().to_html()))

    def peak_and_integrate(self, event):
        self.data.pp(threshold=self.data.absmax*self.thresh.value/100,
                     verbose=False, zoom=self.ax.get_xbound()).centroid()
        if len(self.data.peaks) > 0:
            self.int()

    def integrate(self, event):
        #"integrate from event"
        # if event['name']=='value':
        self.int()

    def int(self):
        "do the automatic integration from peaks and parameters"
        self.on_rem(None)
        try:
            calib = self.Integ.calibration
        except:
            calib = None
        self.data.set_unit('ppm')
        try:
            self.data.peaks
        except:
            self.data.pp(threshold=self.data.absmax*self.thresh.value/100,
                         verbose=False, zoom=self.ax.get_xbound()).centroid()
        # appending to the list, self and Integrals are equivalent to list -
        self.Integ += Integrals(self.data, separation=self.sep.value, wings=self.wings.value,
                                bias=self.data.absmax*self.bias.value/100)
        self.Integ.calibrate(calibration=calib)
        self.print(None)
        self.draw()

    def ob(self, event):
        if event['name'] == 'value':
            self.draw()

    def disp(self):
        self.ax.set_ybound(self.yb0/self.scale.value)
        self.idisp += 1

    def draw(self):
        "refresh display from event - if zoom is True, display only in xbound"
        # self.yb = self.ax.get_ybound()
        super().draw()
        self.idraw += 1
        self.Integ.display(label=True, figure=self.ax,
                           labelyposition=None, regions=False, zoom=None)
        self.ax.set_xbound(self.xb)
        # self.ax.set_ybound(self.yb)


# if __name__ == '__main__':
#    unittest.main()
