#!python
import os
import re
import sys
import datetime
import argparse

import numpy as np
import astropy.io.fits as fits
from astropy.table import Table

import matplotlib
matplotlib.use('TkAgg')
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as tck

from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,
                                               NavigationToolbar2Tk)
from matplotlib.backends.backend_agg import FigureCanvasAgg

from gamse.pipelines.common import load_config
from gamse.echelle.wlcalib import (identlinetype, CalibFigure,
        reference_self_wavelength,
        save_ident, load_ident, get_linelist,
        is_identified, auto_line_fitting_filter, find_local_peak,
        guess_wavelength, get_wavelength, fit_wavelength)


if sys.version_info[0] < 3:
    import Tkinter as tk
    import ttk
else:
    import tkinter as tk
    import tkinter.ttk as ttk


class CustomToolbar(NavigationToolbar2Tk):
    """Class for customized matplotlib toolbar.

    Args:
        canvas (:class:`matplotlib.backends.backend_agg.FigureCanvasAgg`):
            Canvas object used in :class:`CalibWindow`.
        master (Tkinter widget): Parent widget.
    """

    def __init__(self, canvas, master):
        """Constructor of :class:`CustomToolbar`.
        """
        self.toolitems = (
            ('Home', 'Reset original view', 'home', 'home'),
            ('Back', 'Back to previous view', 'back', 'back'),
            ('Forward', 'Forward to next view', 'forward', 'forward'),
            ('Pan', 'Pan axes with left mouse, zoom with right', 'move','pan'),
            ('Zoom', 'Zoom to rectangle', 'zoom_to_rect','zoom'),
            ('Subplots', 'Configure subplots', 'subplots','configure_subplots'),
            ('Save', 'Save the figure', 'filesave', 'save_figure'),
        )
        NavigationToolbar2Tk.__init__(self, canvas, master)

    def set_message(self, msg):
        """Remove the coordinate displayed in the toolbar.
        """
        pass

class PlotFrame(tk.Frame):
    """The frame for plotting spectrum in the :class:`CalibWindow`.

    Args:
        master (Tkinter widget): Parent widget.
        width (int): Width of frame.
        height (int): Height of frame.
        dpi (int): DPI of figure.
        identlist (dict): Dict of identified lines.
        linelist (list): List of wavelength standards.
    """
    def __init__(self, master, width, height, dpi, identlist, linelist):
        """Constructor of :class:`PlotFrame`.
        """

        tk.Frame.__init__(self, master, width=width, height=height)

        self.fig = CalibFigure(width  = width,
                               height = height,
                               dpi    = dpi,
                               )
        self.ax1 = self.fig._ax1
        self.ax2 = self.fig._ax2
        self.ax3 = self.fig._ax3

        self.canvas = FigureCanvasTkAgg(self.fig, master=self)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(side=tk.TOP)
        self.canvas.mpl_connect('button_press_event', master.on_click)
        self.canvas.mpl_connect('draw_event', master.on_draw)

        aperture = master.param['aperture']
        self.ax1._text.set_text('Aperture {}'.format(aperture))

        # add toolbar
        self.toolbar = CustomToolbar(self.canvas, master=self)
        self.toolbar.update()
        self.toolbar.pack(side=tk.LEFT)

        self.pack()

class InfoFrame(tk.Frame):
    """The frame for buttons and tables on the right side of the
    :class:`CalibWindow`.

    Args:
        master (Tkinter widget): Parent widget.
        width (int): Width of the frame.
        height (int): Height of the frame.
        linelist (list): List of wavelength standards.
        identlist (dict): Dict of identified lines.
    """

    def __init__(self, master, width, height, linelist, identlist):
        """Constuctor of :class:`InfoFrame`.
        """

        self.master = master

        title = master.param['title']

        tk.Frame.__init__(self, master, width=width, height=height)

        self.fname_label = tk.Label(master = self,
                                    width  = width,
                                    font   = ('Arial', 14),
                                    text   = title,
                                    )
        self.order_label = tk.Label(master = self,
                                    width  = width,
                                    font   = ('Arial', 10),
                                    text   = '',
                                    )
        self.fname_label.pack(side=tk.TOP,pady=(30,5))
        self.order_label.pack(side=tk.TOP,pady=(5,10))

        button_width = 13

        self.switch_frame = tk.Frame(master=self, width=width, height=30)
        self.prev_button = tk.Button(master  = self.switch_frame,
                                     text    = '◀',
                                     width   = button_width,
                                     font    = ('Arial',10),
                                     command = master.prev_aperture,
                                     )
        self.next_button = tk.Button(master  = self.switch_frame,
                                     text    = '▶',
                                     width   = button_width,
                                     font    = ('Arial',10),
                                     command = master.next_aperture,
                                     )
        self.prev_button.pack(side=tk.LEFT)
        self.next_button.pack(side=tk.RIGHT)
        self.switch_frame.pack(side=tk.TOP, pady=5, padx=10, fill=tk.X)

        # line table area
        self.line_frame = LineTable(master    = self,
                                    width     = width-30,
                                    height    = 400, #900,
                                    identlist = identlist,
                                    linelist  = linelist)
        self.line_frame.pack(side=tk.TOP, padx=10, pady=5)


        # batch operation buttons
        button_width2 = 13
        self.batch_frame = tk.Frame(master=self, width=width, height=30)
        self.recenter_button = tk.Button(master  = self.batch_frame,
                                         text    = 'recenter',
                                         width   = button_width2,
                                         font    = ('Arial',10),
                                         command = master.recenter,
                                         )
        self.clearall_button = tk.Button(master  = self.batch_frame,
                                         text    = 'clear all',
                                         width   = button_width2,
                                         font    = ('Arial',10),
                                         command = master.clearall,
                                         )
        self.recenter_button.pack(side=tk.LEFT)
        self.clearall_button.pack(side=tk.RIGHT)
        self.batch_frame.pack(side=tk.TOP, pady=5, padx=10, fill=tk.X)


        # fit buttons
        self.auto_button = tk.Button(master=self, text='Auto Identify',
                            font = ('Arial', 10), width=25,
                            command = master.auto_identify)
        self.fit_button = tk.Button(master=self, text='Fit',
                            font = ('Arial', 10), width=25,
                            command = master.fit)
        self.switch_button = tk.Button(master=self, text='Plot',
                            font = ('Arial', 10), width=25,
                            command = master.switch)
        # set status
        self.auto_button.config(state=tk.DISABLED)
        self.fit_button.config(state=tk.DISABLED)
        self.switch_button.config(state=tk.DISABLED)

        # Now pack from bottom to top
        self.switch_button.pack(side=tk.BOTTOM, pady=(5,30))
        self.fit_button.pack(side=tk.BOTTOM, pady=5)
        self.auto_button.pack(side=tk.BOTTOM, pady=5)

        self.fitpara_frame = FitparaFrame(master=self, width=width-20, height=35)
        self.fitpara_frame.pack(side=tk.BOTTOM, padx=10, pady=5, fill=tk.X)

        self.pack()

    def update_nav_buttons(self):
        """Update the navigation buttons.
        """
        mode = self.master.param['mode']
        if mode == 'ident':
            aperture = self.master.param['aperture']

            if aperture == self.master.spec['aperture'].min():
                state = tk.DISABLED
            else:
                state = tk.NORMAL
            self.prev_button.config(state=state)

            if aperture == self.master.spec['aperture'].max():
                state = tk.DISABLED
            else:
                state = tk.NORMAL
            self.next_button.config(state=state)
        elif mode == 'fit':
            self.prev_button.config(state=tk.DISABLED)
            self.next_button.config(state=tk.DISABLED)
        else:
            pass

    def update_aperture_label(self):
        """Update the order information to be displayed on the top.
        """
        mode     = self.master.param['mode']
        aperture = self.master.param['aperture']
        k        = self.master.param['k']
        offset   = self.master.param['offset']

        if mode == 'ident':
            if None in (k, offset):
                order = '?'
            else:
                order = str(k*aperture + offset)
            text = 'Order {} (Aperture {})'.format(order, aperture)
            self.order_label.config(text=text)
        elif mode == 'fit':
            self.order_label.config(text='')
        else:
            pass


class LineTable(tk.Frame):
    """A table for the input spectral lines embedded in the :class:`InfoFrame`.

    Args:
        master (Tkinter widget): Parent widget.
        width (int): Width of line table.
        height (int): Height of line table.
        identlist (dict): Dict of identified lines.
        linelist (list): List of wavelength standards.
    """
    def __init__(self, master, width, height, identlist, linelist):
        """Constructor of :class:`LineTable`.
        """
        self.master = master

        font = ('Arial', 10)

        tk.Frame.__init__(self, master=master, width=width, height=height)

        self.tool_frame = tk.Frame(master=self, width=width, height=40)
        self.tool_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5))

        self.search_text = tk.StringVar()
        self.search_entry = tk.Entry(master=self.tool_frame, width=10,
                                     font=font, textvariable=self.search_text)
        self.search_entry.pack(side=tk.LEFT, fil=tk.Y, padx=0)

        # create 3 buttons
        self.clr_button = tk.Button(master=self.tool_frame, text='Clear',
                                    font=font, width=5,
                                    command=self.on_clear_search)
        self.add_button = tk.Button(master=self.tool_frame, text='Add',
                                    font=font, width=5,
                                    command=master.master.on_add_ident)
        self.del_button = tk.Button(master=self.tool_frame, text='Del',
                                    font=font, width=5,
                                    command=master.master.on_delete_ident)

        # put 3 buttons
        self.del_button.pack(side=tk.RIGHT, padx=(5,0))
        self.add_button.pack(side=tk.RIGHT, padx=(5,0))
        self.clr_button.pack(side=tk.RIGHT, padx=(5,0))

        # update status of 3 buttons
        self.clr_button.config(state=tk.DISABLED)
        self.add_button.config(state=tk.DISABLED)
        self.del_button.config(state=tk.DISABLED)

        # create line table
        self.data_frame = tk.Frame(master=self, width=width)
        self.data_frame.pack(side=tk.TOP, fill=tk.X, pady=5)

        # create line tree
        self.line_tree = ttk.Treeview(master  = self.data_frame,
                                      columns    = ('wl', 'species', 'source'),
                                      show       = 'headings',
                                      style      = 'Treeview',
                                      height     = 13, # 22,
                                      selectmode ='browse')
        self.line_tree.bind('<Button-1>', self.on_click_item)

        self.scrollbar = tk.Scrollbar(master = self.data_frame,
                                      orient = tk.VERTICAL,
                                      width  = 20)

        self.line_tree.column('wl',      width=150)
        self.line_tree.column('species', width=120)
        self.line_tree.column('source',  width=width-150-120-20)
        self.line_tree.heading('wl',      text=u'\u03bb in air (\xc5)')
        self.line_tree.heading('species', text='Species')
        self.line_tree.heading('source',  text='Source')
        self.line_tree.config(yscrollcommand=self.scrollbar.set)

        style = ttk.Style()
        style.configure('Treeview', rowheight=30)
        style.configure('Treeview.Heading', font=('Arial', 10))

        self.scrollbar.config(command=self.line_tree.yview)

        self.item_lst = []
        for row in linelist:
            wl      = row['wave_air']
            element = row['element']
            ion     = row['ion']
            source  = row['source']
            if ion is np.ma.masked:
                ion = ''
            species = '{} {}'.format(element, ion).strip()
            iid = self.line_tree.insert('', tk.END,
                    values=(wl, species, source), tags='normal')
            self.item_lst.append((iid,  wl))
        self.line_tree.tag_configure('normal', font=('Arial', 10))

        self.line_tree.pack(side=tk.LEFT, fill=tk.Y, expand=True)
        self.scrollbar.pack(side=tk.LEFT, fill=tk.Y)
        self.data_frame.pack(side=tk.TOP, fill=tk.Y)
        self.pack()

    def on_clear_search(self):
        """Clear the search bar.
        """
        # clear the search bar
        self.search_text.set('')

        # clear the identified line
        self.master.master.param['center']    = None
        self.master.master.param['amplitude'] = None
        self.master.master.param['fwhm']      = None

        # de-select the line table
        sel_items = self.line_tree.selection()
        self.line_tree.selection_remove(sel_items)
        #self.line_tree.selection_clear()
        # doesn't work?

        # update the status of 3 button
        self.clr_button.config(state=tk.DISABLED)
        self.add_button.config(state=tk.DISABLED)
        self.del_button.config(state=tk.DISABLED)

        # replot
        self.master.master.plot_aperture()

        self.master.master.plot_linefit(None)

        self.master.master.plot_frame.canvas.draw()

        # set focus to canvas
        self.master.master.plot_frame.canvas.get_tk_widget().focus()

    def on_click_item(self, event):
        """Event response function for clicking lines.
        """

        identlist = self.master.master.identlist
        aperture = self.master.master.param['aperture']

        # find the clicked item
        item = self.line_tree.identify_row(event.y)
        values = self.line_tree.item(item, 'values')
        # put the wavelength into the search bar
        self.search_text.set(values[0])
        # update status
        self.clr_button.config(state=tk.NORMAL)


        # find if the clicked line is in ident list.
        # if yes, replot the figure with idented line with blue color, and set
        # the delete button to normal. Otherwise, replot the figure with black,
        # and disable the delete button.

        if aperture in identlist:

            list1 = identlist[aperture]

            wl_diff = np.abs(list1['wavelength'] - float(values[0]))
            mindiff = wl_diff.min()
            argmin  = wl_diff.argmin()
            if mindiff < 1e-3:
                # the selected line is in identlist of this aperture
                xpos = list1[argmin]['pixel']
                for line, text in self.master.master.ident_objects:
                    if abs(line.get_xdata()[0] - xpos)<1e-3:
                        plt.setp(line, color='b')
                        plt.setp(text, color='b')
                    else:
                        plt.setp(line, color='k')
                        plt.setp(text, color='k')
                # update the status of del button
                self.del_button.config(state=tk.NORMAL)
            else:
                # the selected line is not in identlist of this aperture
                for line, text in self.master.master.ident_objects:
                    plt.setp(line, color='k')
                    plt.setp(text, color='k')
                # update the status of del button
                self.del_button.config(state=tk.DISABLED)

            self.master.master.plot_frame.canvas.draw()

        else:
            # if the current aperture is not in identlist, do nothing
            pass

class FitparaFrame(tk.Frame):
    """Frame for the fitting parameters embedded in the :class:`InfoFrame`.

    Args:
        master (Tkinter widget): Parent widget.
        width (int): Width of frame.
        height (int): Height of frame.
    """
    def __init__(self, master, width, height):
        """Constructor of :class:`FitparaFrame`.
        """

        self.master = master

        font = ('Arial', 10)

        tk.Frame.__init__(self, master, width=width, height=height)

        # the first row
        self.row1_frame = tk.Frame(master=self, width=width)

        self.xorder_label = tk.Label(master = self.row1_frame,
                                     text   = 'X ord =',
                                     font   = font)

        # spinbox for adjusting xorder
        self.xorder_str = tk.StringVar()
        self.xorder_str.set(master.master.param['xorder'])
        self.xorder_box = tk.Spinbox(master = self.row1_frame,
                                     from_        = 1,
                                     to_          = 10,
                                     font         = font,
                                     width        = 2,
                                     textvariable = self.xorder_str,
                                     command      = self.on_change_xorder)

        self.yorder_label = tk.Label(master = self.row1_frame,
                                     text   = 'Y ord =',
                                     font   = font)
        # spinbox for adjusting yorder
        self.yorder_str = tk.StringVar()
        self.yorder_str.set(master.master.param['yorder'])
        self.yorder_box = tk.Spinbox(master       = self.row1_frame,
                                     from_        = 1,
                                     to_          = 10,
                                     font         = font,
                                     width        = 2,
                                     textvariable = self.yorder_str,
                                     command      = self.on_change_yorder)

        self.maxiter_label  = tk.Label(master = self.row1_frame,
                                       text   = 'N =',
                                       font   = font)
        self.maxiter_str = tk.StringVar()
        self.maxiter_str.set(master.master.param['maxiter'])
        self.maxiter_box = tk.Spinbox(master       = self.row1_frame,
                                      from_        = 1,
                                      to_          = 20,
                                      font         = font,
                                      width        = 2,
                                      textvariable = self.maxiter_str,
                                      command      = self.on_change_maxiter)

        self.xorder_label.pack(side=tk.LEFT)
        self.xorder_box.pack(side=tk.LEFT)
        self.yorder_label.pack(side=tk.LEFT, padx=(10,0))
        self.yorder_box.pack(side=tk.LEFT)
        self.maxiter_label.pack(side=tk.LEFT, padx=(10,0))
        self.maxiter_box.pack(side=tk.LEFT)

        self.row1_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,2))

        # the second row
        self.row2_frame = tk.Frame(master=self, width=width)

        self.clip_label = tk.Label(master = self.row2_frame,
                                   text   = 'Clipping =',
                                   font   = font,
                                   width  = width,
                                   anchor = tk.W)
        self.clip_scale = tk.Scale(master     = self.row2_frame,
                                   from_      = 1.0,
                                   to         = 5.0,
                                   orient     = tk.HORIZONTAL,
                                   resolution = 0.1,
                                   command    = self.on_change_clipping)
        self.clip_scale.set(master.master.param['clipping'])

        self.clip_label.pack(side=tk.TOP)
        self.clip_scale.pack(side=tk.TOP, fill=tk.X)
        self.row2_frame.pack(side=tk.TOP, fill=tk.X)

        self.pack()

    def on_change_xorder(self):
        """Response function of changing order of polynomial along x-axis.
        """
        self.master.master.param['xorder'] = int(self.xorder_box.get())

    def on_change_yorder(self):
        """Response function of changing order of polynomial along y-axis.
        """
        self.master.master.param['yorder'] = int(self.yorder_box.get())

    def on_change_maxiter(self):
        """Response function of changing maximum number of iteration.
        """
        self.master.master.param['maxiter'] = int(self.maxiter_box.get())

    def on_change_clipping(self, value):
        """Response function of changing clipping value.
        """
        self.master.master.param['clipping'] = float(value)

class CalibWindow(tk.Frame):
    """Frame of the wavelength calibration window.

    Args:
        master (:class:`tk.TK`): Tkinter root window.
        width (int): Width of window.
        height (int): Height of window.
        dpi (int): DPI of figure.
        spec (:class:`numpy.dtype`): Spectra data.
        figfilename (str): Filename of the output wavelength calibration
            figure.
        title (str): A string to display as the title of calib figure.
        identlist (dict): Identification line list.
        linelist (list): List of wavelength standards (wavelength, species).
        fitfuncanme (str): Name of fitting function of each line.
        window_size (int): Size of the window in pixel to search for line
            peaks.
        xorder (int): Degree of polynomial along X direction.
        yorder (int): Degree of polynomial along Y direction.
        maxiter (int): Maximim number of interation in polynomial fitting.
        clipping (float): Threshold of sigma-clipping.
        q_threshold (float): Minimum Q (S/N) of the spectral lines to be
            accepted in the wavelength fitting.
        fit_filter: Fitting filter.
    """
    def __init__(self, master, width, height, dpi, spec, figfilename, title,
                 identlist, linelist, fitfuncname, window_size, xorder, yorder,
                 maxiter, clipping, q_threshold, fit_filter,
                 ):
        """Constructor of :class:`CalibWindow`.
        """

        self.master    = master
        self.spec      = spec
        self.identlist = identlist
        self.linelist  = linelist

        tk.Frame.__init__(self, master, width=width, height=height)

        self.param = {
            'mode':         'ident',
            'aperture':     self.spec['aperture'].min(),
            'figfilename':  figfilename,
            'title':        title,
            'aperture_min': self.spec['aperture'].min(),
            'aperture_max': self.spec['aperture'].max(),
            'npixel':       self.spec[0]['wavelength'].size,
            # parameters of displaying
            'xlim':         {},
            'ylim':         {},
            # line fitting parameters
            'i1':           None,
            'i2':           None,
            'fitfuncname':  fitfuncname,
            'window_size':  window_size,
            # line fitting results
            'fitfunc':      None,
            'param':        None,
            'std':          None,
            'residuals':    None,
            'center':       None,
            'amplitude':    None,
            'fwhm':         None,
            'background':   None,
            # parameters of converting aperture and order
            'k':            None,
            'offset':       None,
            # wavelength fitting parameters
            'xorder':       xorder,
            'yorder':       yorder,
            'maxiter':      maxiter,
            'clipping':     clipping,
            'q_threshold':  q_threshold,
            # wavelength fitting results
            'std':          0,
            'coeff':        np.array([]),
            'nuse':         0,
            'ntot':         0,
            'direction':    '',
            'fit_filter':   fit_filter,
            }

        self.fitdata = {}

        for row in self.spec:
            aperture = row['aperture']
            self.param['xlim'][aperture] = (0, len(row['wavelength'])-1)
            self.param['ylim'][aperture] = (None, None)
            self.fitdata[aperture] = {}

        # determine widget size
        info_width    = 400  # 500
        info_height   = height # - 500
        canvas_width  = width - info_width
        canvas_height = height
        # generate plot frame and info frame
        self.plot_frame = PlotFrame(master    = self,
                                    width     = canvas_width,
                                    height    = canvas_height,
                                    dpi       = dpi,
                                    identlist = self.identlist,
                                    linelist  = self.linelist,
                                    )
        self.info_frame = InfoFrame(master    = self,
                                    width     = info_width,
                                    height    = info_height,
                                    identlist = self.identlist,
                                    linelist  = self.linelist,
                                    )
        # pack plot frame and info frame
        self.plot_frame.pack(side=tk.LEFT, fill=tk.Y)
        self.info_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0,10))

        self.pack()

        self.plot_aperture()

        self.plot_linefit(None)

        self.plot_frame.canvas.draw()

        if len(self.identlist)>0:
            self.recenter()


        self.update_fit_buttons()

    def fit(self):
        """Fit the wavelength and plot the solution in the figure.
        """

        coeff, std, k, offset, nuse, ntot = fit_wavelength(
                identlist   = self.identlist,
                npixel      = self.param['npixel'],
                xorder      = self.param['xorder'],
                yorder      = self.param['yorder'],
                maxiter     = self.param['maxiter'],
                clipping    = self.param['clipping'],
                fit_filter  = self.param['fit_filter'],
                )

        # update self.param by new fitting results
        self.param['coeff']  = coeff
        self.param['std']    = std
        self.param['k']      = k
        self.param['offset'] = offset
        self.param['nuse']   = nuse
        self.param['ntot']   = ntot

        message = 'Wavelength fitted. std={:.6f}, Ntot={}, Nuse={}'.format(
                    std, ntot, nuse)
        print(message)

        self.plot_wavelength()

        # udpdate the order/aperture string
        aperture = self.param['aperture']
        order = k*aperture + offset
        text = 'Order {} (Aperture {})'.format(order, aperture)
        self.info_frame.order_label.config(text=text)

        self.update_fit_buttons()

    def recenter(self):
        """Relocate the peaks for all the identified lines.
        """
        for aperture, list1 in self.identlist.items():
            mask = (self.spec['aperture'] == aperture)
            specdata = self.spec[mask][0]
            flux = specdata['flux']
            for row in list1:
                pix = int(round(row['pixel']))
                window_size = self.param['window_size']
                fitfuncname = self.param['fitfuncname']
                result = find_local_peak(flux, pix, window_size, fitfuncname)
                center = result['center']
                result = find_local_peak(flux, center, window_size, fitfuncname)

                # update the line parameters with the new fitting results
                row['i1']         = result['i1']
                row['i2']         = result['i2']
                row['pixel']      = result['center']
                row['amplitude']  = result['amplitude']
                row['fwhm']       = result['fwhm']
                row['background'] = result['background']
                row['q']          = result['amplitude']/result['std']

        # replot
        self.plot_aperture()

        self.plot_linefit(None)

        self.plot_frame.canvas.draw()

    def clearall(self):
        """Clear all the identified lines."""

        self.identlist = {}

        self.param['k']      = None
        self.param['offset'] = None
        self.param['std']    = 0
        self.param['coeff']  = np.array([])
        self.param['nuse']   = 0
        self.param['ntot']   = 0

        info_frame = self.info_frame
        # update the status of 3 buttons
        info_frame.line_frame.clr_button.config(state=tk.DISABLED)
        info_frame.line_frame.add_button.config(state=tk.DISABLED)
        info_frame.line_frame.del_button.config(state=tk.DISABLED)

        # update buttons
        info_frame.recenter_button.config(state=tk.DISABLED)
        info_frame.clearall_button.config(state=tk.DISABLED)
        info_frame.switch_button.config(state=tk.DISABLED)
        info_frame.auto_button.config(state=tk.DISABLED)

        # replot
        self.plot_aperture()

        self.plot_linefit(None)

        self.plot_frame.canvas.draw()


    def auto_identify(self):
        """Identify all lines in the wavelength standard list automatically.
        """
        k           = self.param['k']
        offset      = self.param['offset']
        coeff       = self.param['coeff']
        npixel      = self.param['npixel']
        window_size = self.param['window_size']
        fitfuncname = self.param['fitfuncname']

        n_insert = 0
        for aperture in sorted(self.spec['aperture']):
            mask = self.spec['aperture'] == aperture
            flux = self.spec[mask][0]['flux']

            # scan every order and find the upper and lower limit of wavelength
            order = k*aperture + offset

            # generated the wavelengths for every pixel in this order
            pix_lst = np.arange(npixel)
            wave_lst = get_wavelength(coeff, npixel, pix_lst,
                                      np.repeat(order, npixel))
            # get wavelength range of this order
            w1 = min(wave_lst[0], wave_lst[-1])
            w2 = max(wave_lst[0], wave_lst[-1])

            # now scan the linelist
            for row in self.linelist:
                wl      = row['wave_air']
                element = row['element']
                ion     = row['ion']
                source  = row['source']
                if wl < w1:
                    continue
                if wl > w2:
                    break

                # wavelength in the range of this order
                # check if this line has already been identified
                if is_identified(wl, self.identlist, aperture):
                    continue

                # if this line has not been identified. find the peak
                diff = np.abs(wave_lst - wl)
                i = diff.argmin()
                result = find_local_peak(flux, i, window_size, fitfuncname)
                center = result['center']
                result = find_local_peak(flux, center, window_size, fitfuncname)

                if result is None:
                    # fitting failed. probably because of too few points
                    continue

                keep = auto_line_fitting_filter(result)
                if not keep:
                    continue

                # unpack the fitted parameters
                i1          = result['i1']
                i2          = result['i2']
                amplitude   = result['amplitude']
                center      = result['center']
                std         = result['std']
                fwhm        = result['fwhm']
                background  = result['background']

                # q = A/std is a proxy of signal-to-noise ratio.
                q = amplitude/std
                if q < self.param['q_threshold']:
                    continue

                '''
                fig = plt.figure(figsize=(6,4),tight_layout=True)
                ax = fig.gca()
                ax.plot(np.arange(i1,i2), flux[i1:i2], 'ro')
                newx = np.arange(i1, i2, 0.1)
                ax.plot(newx, gaussian_bkg(param[0], param[1], param[2],
                            param[3], newx), 'b-')
                ax.axvline(x=param[1], color='k',ls='--')
                y1,y2 = ax.get_ylim()
                ax.text(0.9*i1+0.1*i2, 0.1*y1+0.9*y2, 'A=%.1f'%param[0])
                ax.text(0.9*i1+0.1*i2, 0.2*y1+0.8*y2, 'BKG=%.1f'%param[3])
                ax.text(0.9*i1+0.1*i2, 0.3*y1+0.7*y2, 'FWHM=%.1f'%param[2])
                ax.text(0.9*i1+0.1*i2, 0.4*y1+0.6*y2, 'q=%.1f'%q)
                ax.set_xlim(i1,i2)
                fig.savefig('tmp/%d-%d-%d.png'%(aperture, i1, i2))
                plt.close(fig)
                '''

                # initialize line table
                if aperture not in self.identlist:
                    self.identlist[aperture] = np.array([], dtype=identlinetype)

                item = np.array((aperture, order, element, ion, wl, source,
                                 i1, i2, center, amplitude, fwhm, background,
                                 q, True, 0.0, 'a'), dtype=identlinetype)

                # find the insert position
                insert_pos = np.searchsorted(self.identlist[aperture]['pixel'],
                                             center)

                self.identlist[aperture] = np.insert(self.identlist[aperture],
                                                     insert_pos, item)

                n_insert += 1


        message = '{} new lines inserted'.format(n_insert)
        print(message)

        self.fit()

    def switch(self):
        """Response funtion of switching between "ident" and "fit" mode.
        """

        if self.param['mode']=='ident':
            # switch to fit mode
            self.param['mode']='fit'

            self.plot_wavelength()

            self.info_frame.switch_button.config(text='Identify')

        elif self.param['mode']=='fit':
            # switch to ident mode
            self.param['mode']='ident'

            self.plot_aperture()
            self.plot_frame.canvas.draw()

            self.info_frame.switch_button.config(text='Plot')
        else:
            pass

        # update order navigation and aperture label
        self.info_frame.update_nav_buttons()
        self.info_frame.update_aperture_label()

    def next_aperture(self):
        """Response function of pressing the next aperture."""
        if self.param['aperture'] < self.spec['aperture'].max():
            self.param['aperture'] += 1
            self.plot_aperture()
            self.plot_linefit(None)
            self.plot_frame.canvas.draw()

    def prev_aperture(self):
        """Response function of pressing the previous aperture."""
        if self.param['aperture'] > self.spec['aperture'].min():
            self.param['aperture'] -= 1
            self.plot_aperture()
            self.plot_linefit(None)
            self.plot_frame.canvas.draw()

    def plot_linefit(self, info):

        # redraw spectra in ax2.
        # get the corresponding axes
        ax2 = self.plot_frame.ax2
        ax3 = self.plot_frame.ax3

        ax2.cla()
        ax3.cla()

        # if nothing to plot, return a None value
        if info is None:
            return None

        i1          = info['i1']
        i2          = info['i2']
        center      = info['center']
        fitfunc     = info['fitfunc']
        param       = info['param']
        std         = info['std']
        residuals   = info['residuals']

        # get flux
        aperture = self.param['aperture']
        mask = (self.spec['aperture'] == aperture)
        specdata = self.spec[mask][0]
        flux = specdata['flux']

        # plot fitting in ax3
        xdata = np.arange(i1, i2)
        ydata = flux[i1:i2]
        ax3.plot(xdata, ydata, 'o', color='C3')
        ax3.axvline(center, ls='--', color='k')
        # plot the fitted curve
        newx = np.linspace(xdata[0], xdata[-1], 100)
        ax3.plot(newx, fitfunc(param, newx), '-', color='C0', lw=1)
        ax3.set_xlim(xdata[0], xdata[-1])
        ax3._text.set_text('')

        # plot residuals in ax2
        ax2.plot(xdata, residuals, 'o', color='C3')
        ax2.axhline(y=0, color='k', ls='--', zorder=-1)
        ax2.axhline(y=std, color='k', ls=':', zorder=-1)
        ax2.axhline(y=-std, color='k', ls=':', zorder=-1)
        ax2.set_xlim(xdata[0], xdata[-1])
        ax2.set_ylim(-4*std, 4*std)
        ax2._text.set_text('RMS={:7.3e}'.format(std))

    def plot_aperture(self):
        """Plot a specific aperture in the figure.
        """
        aperture = self.param['aperture']
        mask = (self.spec['aperture'] == aperture)
        specdata = self.spec[mask][0]
        ydata = specdata['flux']
        npixel = len(ydata)
        xdata = np.arange(npixel)

        # redraw spectra in ax1
        ax1 = self.plot_frame.ax1
        fig = self.plot_frame.fig
        ax1.cla()
        ax1.plot(xdata, ydata, 'r-')
        x1, x2 = self.param['xlim'][aperture]
        y1, y2 = self.param['ylim'][aperture]
        if y1 is None:
            y1 = ax1.get_ylim()[0]
        if y2 is None:
            y2 = ax1.get_ylim()[1]

        #x1, x2 = xdata[0], xdata[-1]
        #y1, y2 = ax1.get_ylim()
        y1 = min(y1, 0)
        # plot identified line list
        # calculate ratio = value/pixel
        bbox = ax1.get_window_extent().transformed(
                fig.dpi_scale_trans.inverted())
        axwidth_pixel = bbox.width*fig.dpi
        pix_ratio = abs(x2-x1)/axwidth_pixel

        # plot identified lines with vertial dash lines
        if aperture in self.identlist and len(self.identlist[aperture])>0:
            self.ident_objects = []
            list1 = self.identlist[aperture]
            for item in list1:
                pixel      = item['pixel']
                wavelength = item['wavelength']

                # draw vertial dash line
                line = ax1.axvline(pixel, ls='--', color='k', lw=0.6)

                # draw text
                x = pixel+pix_ratio*10
                y = 0.4*y1+0.6*y2
                text = ax1.text(x, y, '{:.4f}'.format(wavelength), color='k',
                                rotation='vertical', fontstyle='italic',
                                fontsize=10)
                self.ident_objects.append((line, text))

        # plot the temporarily identified line
        #if self.param['center'] is not None:
        #    ax1.axvline(self.param['center'], linestyle='--', color='k', lw=0.6)

        #plot line list
        # get the (estimated) wavelength range of current displayed aperture
        w1, w2 = None, None

        # check global fitting results
        k       = self.param['k']
        offset  = self.param['offset']
        coeff   = self.param['coeff']
        if len(coeff)>0 and k is not None and offset is not None:
            # get the order nomalization factros order1 and order2, which are
            #required by the get_wavelength() function
            pix_lst = np.arange(npixel)
            order = k*aperture + offset
            wave_lst = get_wavelength(coeff, npixel, pix_lst,
                                      np.repeat(order, npixel))
            diffwave_lst = np.diff(wave_lst)
            if (diffwave_lst>0).sum()==0 or (diffwave_lst<0).sum()==0:
                # all wavelength are monotonic
                # get wavelength range of this order
                w1 = min(wave_lst[0], wave_lst[-1])
                w2 = max(wave_lst[0], wave_lst[-1])
        elif aperture in self.identlist and len(self.identlist[aperture])>1:
            n = len(self.identlist[aperture])
            pix_lst = np.arange(npixel)
            x_lst = self.identlist[aperture]['pixel']
            wl_lst  = self.identlist[aperture]['wavelength']
            deg = min(self.param['xorder'], n-1)
            c = np.polyfit(x_lst, wl_lst, deg=deg)
            wave_lst = np.polyval(c, pix_lst)
            diffwave_lst = np.diff(wave_lst)
            if (diffwave_lst>0).sum()==0 or (diffwave_lst<0).sum()==0:
                # all wavelength are monotonic
                # get wavelength range of this order
                w1 = min(wave_lst[0], wave_lst[-1])
                w2 = max(wave_lst[0], wave_lst[-1])
        else:
            pass

        # wavelength range is known for this order
        if None not in [w1, w2]:
            m1 = self.linelist['wave_air'] > w1
            m2 = self.linelist['wave_air'] < w2
            m = m1 * m2
            for row in self.linelist[m]:
                dist = np.abs(wave_lst - row['wave_air'])
                pix = pix_lst[dist.argmin()]
                # plotted tick length is proportional to the line intensity
                if 'intlev' in self.linelist.colnames:
                    intlevel = row['intlev']
                    if intlevel is np.ma.masked:
                        intlevel = 4
                elif 'int' in self.linelist.colnames:
                    intensity = row['int']
                    if intensity is np.ma.masked:
                        intlevel = 4
                    elif intensity > 100:
                        intlevel = 1
                    elif intensity > 20:
                        intlevel = 2
                    elif intensity > 5:
                        intlevel = 3
                    else:
                        intlevel = 4
                else:
                    intlevel = 4

                ylen = (5 - intlevel)*0.02*(y2-y1)
                # use different colors and line width for identified lines
                if is_identified(row['wave_air'], self.identlist, aperture):
                    color = 'C3'
                    lw = 0.6
                else:
                    color = 'gray'
                    lw = 0.5
                # plot
                ax1.plot([pix, pix], [y2-ylen, y2], ls='-', color=color, lw=lw)

        # update the aperture number
        ax1._text.set_text('Aperture {}'.format(aperture))
        ax1.set_ylim(y1, y2)
        ax1.set_xlim(x1, x2)
        ax1.xaxis.set_major_locator(tck.MultipleLocator(500))
        ax1.xaxis.set_minor_locator(tck.MultipleLocator(100))
        ax1.set_xlabel('Pixel')
        ax1.set_ylabel('Flux')


        # update order navigation and aperture label
        self.info_frame.update_nav_buttons()
        self.info_frame.update_aperture_label()

    def plot_wavelength(self):
        """A wrap for plotting the wavelength solution."""

        aperture_lst = np.arange(self.param['aperture_min'],
                                 self.param['aperture_max']+1)

        kwargs = {
                'offset':   self.param['offset'],
                'k':        self.param['k'],
                'coeff':    self.param['coeff'],
                'npixel':   self.param['npixel'],
                'std':      self.param['std'],
                'nuse':     self.param['nuse'],
                'ntot':     self.param['ntot'],
                'xorder':   self.param['xorder'],
                'yorder':   self.param['yorder'],
                'clipping': self.param['clipping'],
                'maxiter':  self.param['maxiter'],
                }

        if self.param['mode']=='fit':
            plot_ax1 = True
        else:
            plot_ax1 = False

        self.plot_frame.fig.plot_solution(self.identlist, aperture_lst,
                                            plot_ax1, **kwargs)

        self.plot_frame.canvas.draw()
        self.plot_frame.fig.patch.set_facecolor('#ffffff')
        self.plot_frame.fig.savefig(self.param['figfilename'])
        self.plot_frame.fig.patch.set_facecolor('#d9d9d9')
        message = 'Wavelength solution plotted in {}'.format(
                    self.param['figfilename'])
        print(message)

    def on_click(self, event):
        """Response function of clicking the axes.

        Double click means find the local peak and prepare to add a new
        identified line.
        """
        # double click on ax1: want to add a new identified line
        if event.inaxes == self.plot_frame.ax1 and event.dblclick:
            fig = self.plot_frame.fig
            ax1 = self.plot_frame.ax1
            line_frame = self.info_frame.line_frame
            aperture = self.param['aperture']
            if aperture in self.identlist:
                list1 = self.identlist[aperture]

            # get width of current ax in pixel
            x1, x2 = ax1.get_xlim()
            y1, y2 = ax1.get_ylim()
            iarray = fig.dpi_scale_trans.inverted()
            bbox = ax1.get_window_extent().transformed(iarray)
            width, height = bbox.width, bbox.height
            axwidth_pixel = width*fig.dpi
            # get physical Values Per screen Pixel (VPP) in x direction
            vpp = abs(x2-x1)/axwidth_pixel

            # check if peak has already been identified
            if aperture in self.identlist:

                dist = np.array([abs(line.get_xdata()[0] - event.xdata)/vpp
                                 for line, text in self.ident_objects])

                if dist.min() < 5:
                    # found. change the color of this line
                    imin = dist.argmin()
                    for i, (line, text) in enumerate(self.ident_objects):
                        if i == imin:
                            plt.setp(line, color='b')
                            plt.setp(text, color='b')
                        else:
                            plt.setp(line, color='k')
                            plt.setp(text, color='k')
                    wl = list1[imin]['wavelength']

                    # plot fitting of this line
                    for _wl, info in self.fitdata[aperture].items():
                        if abs(wl - _wl) < 1e-3:
                            self.plot_linefit(info)
                            break

                    # redraw the canvas
                    self.plot_frame.canvas.draw()

                    # select this line in the linetable
                    for i, record in enumerate(line_frame.item_lst):
                        item = record[0]
                        wave = record[1]
                        if abs(wl - wave)<1e-3:
                            break

                    line_frame.line_tree.selection_set(item)
                    pos = i/float(len(line_frame.item_lst))
                    line_frame.line_tree.yview_moveto(pos)

                    # put the wavelength in the search bar
                    line_frame.search_text.set(str(wl))

                    # update the status of 3 buttons
                    line_frame.clr_button.config(state=tk.NORMAL)
                    line_frame.add_button.config(state=tk.DISABLED)
                    line_frame.del_button.config(state=tk.NORMAL)

                    # end this function without more actions
                    return True
                else:
                    # not found. this is a new line.

                    # reset the colors of all other lines in this aperture
                    for line, text in self.ident_objects:
                        plt.setp(line, color='k')
                        plt.setp(text, color='k')

                    # clear the search bar
                    line_frame.search_text.set('')

                    # de-select the line table
                    sel_items = line_frame.line_tree.selection()
                    line_frame.line_tree.selection_remove(sel_items)

                    # update the status of 3 button
                    line_frame.clr_button.config(state=tk.DISABLED)
                    line_frame.add_button.config(state=tk.DISABLED)
                    line_frame.del_button.config(state=tk.DISABLED)

                    # continue to act

            # get the flux of current aperture
            mask = (self.spec['aperture'] == aperture)
            specdata = self.spec[mask][0]
            flux = specdata['flux']

            # find the peak of this line
            # first, find the local maximum around the clicking point
            # the searching window is set to be +-5 pixels
            i0 = int(event.xdata)
            i1 = max(int(round(i0 - 5*vpp)), 0)
            i2 = min(int(round(i0 + 5*vpp)), flux.size)
            local_max = i1 + flux[i1:i2].argmax()
            # now found the local max point
            window_size = self.param['window_size']
            fitfuncname = self.param['fitfuncname']

            result = find_local_peak(flux, local_max, window_size, fitfuncname)
            center = result['center']
            result = find_local_peak(flux, center, window_size, fitfuncname)

            # unpack the fitted parameters
            self.param['i1']            = result['i1']
            self.param['i2']            = result['i2']
            self.param['amplitude']     = result['amplitude']
            self.param['center']        = result['center']
            self.param['fwhm']          = result['fwhm']
            self.param['background']    = result['background']
            self.param['fitfunc']       = result['fitfunc']
            self.param['param']         = result['param']
            self.param['std']           = result['std']
            self.param['residuals']     = result['residuals']

            # plot fitting of this line
            self.plot_linefit(result)

            # temporarily plot this line
            #self.plot_aperture()
            # plot the temporarily identified line
            if self.param['center'] is not None:
                ax1.axvline(self.param['center'], ls='--', color='k', lw=0.6)

            # refresh canvas
            self.plot_frame.canvas.draw()

            line_frame.clr_button.config(state=tk.NORMAL)

            # guess the input wavelength
            guess_wl = guess_wavelength(center, aperture, self.identlist,
                                        self.linelist, self.param)

            if guess_wl is None:
                # wavelength guess failed
                #line_frame.search_entry.focus()
                # update buttons
                line_frame.add_button.config(state=tk.NORMAL)
                line_frame.del_button.config(state=tk.DISABLED)
            else:
                # wavelength guess succeed

                # check whether wavelength has already been identified
                if is_identified(guess_wl, self.identlist, aperture):
                    # has been identified, do nothing
                    # update buttons
                    line_frame.add_button.config(state=tk.DISABLED)
                    line_frame.del_button.config(state=tk.NORMAL)
                else:
                    # has not been identified yet
                    # put the wavelength in the search bar
                    line_frame.search_text.set(str(guess_wl))
                    # select this line in the linetable
                    for i, record in enumerate(line_frame.item_lst):
                        iid  = record[0]
                        wave = record[1]
                        if abs(guess_wl - wave)<1e-3:
                            break
                    line_frame.line_tree.selection_set(iid)
                    pos = i/float(len(line_frame.item_lst))
                    line_frame.line_tree.yview_moveto(pos)
                    # update buttons
                    line_frame.add_button.config(state=tk.NORMAL)
                    line_frame.del_button.config(state=tk.DISABLED)
                    # unset focus
                    self.focus()

    def on_add_ident(self):
        """Response function of identifying a new line.
        """
        aperture = self.param['aperture']
        k        = self.param['k']
        offset   = self.param['offset']

        line_frame = self.info_frame.line_frame

        if aperture not in self.identlist:
            self.identlist[aperture] = np.array([], dtype=identlinetype)

        list1 = self.identlist[aperture]

        i1         = self.param['i1']
        i2         = self.param['i2']
        pixel      = self.param['center']
        amplitude  = self.param['amplitude']
        fwhm       = self.param['fwhm']
        background = self.param['background']
        fitfunc    = self.param['fitfunc']
        param      = self.param['param']
        std        = self.param['std']
        residuals  = self.param['residuals']

        q = amplitude/std

        # get wavelength, element and ion from line table
        selected_iid_lst = line_frame.line_tree.selection()
        iid = selected_iid_lst[0]
        lineinfo = line_frame.line_tree.item(iid, 'values')
        wavelength = float(lineinfo[0])
        species = lineinfo[1]
        cols = species.split()
        element = cols[0]
        if len(cols)>1:
            ion = cols[1].strip()
        else:
            ion = ''
        source = lineinfo[2]
        line_frame.line_tree.selection_remove(selected_iid_lst)


        if k is None or offset is None:
            order = 0
        else:
            order = k*aperture + offset

        # add to identlist
        item = np.array((aperture, order, element, ion, wavelength, source,
                         i1, i2, pixel, amplitude, fwhm, background, q,
                         True, 0.0, 'm'), dtype=identlinetype)

        # find the insert position
        insert_pos = np.searchsorted(list1['pixel'], pixel)

        # insert into identified line list
        self.identlist[aperture] = np.insert(self.identlist[aperture],
                                             insert_pos, item)

        # insert into fitdata
        info = {'i1': i1, 'i2': i2, 'center': pixel, 'fitfunc': fitfunc,
                'param': param, 'std': std, 'residuals': residuals,
                }
        self.fitdata[aperture][wavelength] = info

        # reset line fitting parameters
        self.param['i1']         = None
        self.param['i2']         = None
        self.param['center']     = None
        self.param['fitfunc']    = None
        self.param['amplitude']  = None
        self.param['fwhm']       = None
        self.param['background'] = None
        self.param['param']      = None
        self.param['std']        = None
        self.param['residuals']  = None

        # reset the line table
        line_frame.search_text.set('')

        # update the status of 3 buttons
        line_frame.clr_button.config(state=tk.NORMAL)
        line_frame.add_button.config(state=tk.DISABLED)
        line_frame.del_button.config(state=tk.NORMAL)

        self.update_fit_buttons()

        # replot
        self.plot_aperture()

        self.plot_frame.canvas.draw()

    def on_delete_ident(self):
        """Response function of deleting an identified line.
        """
        line_frame = self.info_frame.line_frame
        target_wl = float(line_frame.search_text.get())
        aperture = self.param['aperture']
        list1 = self.identlist[aperture]

        wl_diff = np.abs(list1['wavelength'] - target_wl)
        mindiff = wl_diff.min()
        argmin  = wl_diff.argmin()
        if mindiff < 1e-3:
            # delete this line from ident list
            list1 = np.delete(list1, argmin)
            self.identlist[aperture] = list1

            # clear the search bar
            line_frame.search_text.set('')

            # de-select the line table
            sel_items = line_frame.line_tree.selection()
            line_frame.line_tree.selection_remove(sel_items)

            # update the status of 3 buttons
            line_frame.clr_button.config(state=tk.DISABLED)
            line_frame.add_button.config(state=tk.DISABLED)
            line_frame.del_button.config(state=tk.DISABLED)

            # update fit buttons
            self.update_fit_buttons()

            # replot
            self.plot_aperture()

            self.plot_linefit(None)

            self.plot_frame.canvas.draw()

    def on_draw(self, event):
        """Response function of drawing.
        """
        if self.param['mode'] == 'ident':
            ax1 = self.plot_frame.ax1
            aperture = self.param['aperture']
            self.param['xlim'][aperture] = ax1.get_xlim()
            self.param['ylim'][aperture] = ax1.get_ylim()

    def update_fit_buttons(self):
        """Update the status of fitting buttons.
        """
        nident = 0
        for aperture, list1 in self.identlist.items():
            nident += list1.size

        xorder = self.param['xorder']
        yorder = self.param['yorder']

        info_frame = self.info_frame

        if nident > (xorder+1)*(yorder+1) and len(self.identlist) > yorder+1:
            info_frame.fit_button.config(state=tk.NORMAL)
        else:
            info_frame.fit_button.config(state=tk.DISABLED)

        if len(self.param['coeff'])>0:
            info_frame.switch_button.config(state=tk.NORMAL)
            info_frame.auto_button.config(state=tk.NORMAL)
        else:
            info_frame.switch_button.config(state=tk.DISABLED)
            info_frame.auto_button.config(state=tk.DISABLED)

    def update_batch_buttons(self):
        """Update the status of batch buttons (recenter and clearall).
        """
        # count how many identified lines
        nident = 0
        for aperture, list1 in self.identlist.items():
            nident += list1.size

        info_frame = self.info_frame

        if nident > 0:
            info_frame.recenter_button.config(state=tk.NORMAL)
            info_frame.clearall_button.config(state=tk.NORMAL)
        else:
            info_frame.recenter_button.config(state=tk.DISABLED)
            info_frame.clearall_button.config(state=tk.DISABLED)

def ident_wavelength(spec, figfilename, title, linelist, identfilename=None,
    fitfuncname='gaussian', window_size=13, xorder=3, yorder=3, maxiter=10,
    clipping=3, q_threshold=10, fit_filter=None, figsize=(2500, 1500), dpi=150,
    ):
    """Identify the wavelengths of emission lines in the spectrum of a
    hollow-cathode lamp.

    Args:
        spec (:class:`numpy.dtype`): 1-D spectra.
        figfilename (str): Name of the output wavelength figure to be saved.
        title (str): A string to display as the title of calib figure.
        linelist (str): Name of wavelength standard file.
        identfilename (str): Name of an ASCII formatted wavelength identification
            file.
        window_size (int): Size of the window in pixel to search for the
            lines.
        xorder (int): Degree of polynomial along X direction.
        yorder (int): Degree of polynomial along Y direction.
        maxiter (int): Maximim number of interation in polynomial fitting.
        clipping (float): Threshold of sigma-clipping.
        q_threshold (float): Minimum *Q*-factor of the spectral lines to be
            accepted in the wavelength fitting.
        fit_filter (function): Function checking if a pixel/oder combination is
            within the accepted range.

    Returns:
        dict: A dict containing:

            * **coeff** (:class:`numpy.ndarray`) – Coefficient array.
            * **npixel** (*int*) – Number of pixels along the main dispersion
              direction.
            * **k** (*int*) – Coefficient in the relationship `order =
              k*aperture + offset`.
            * **offset** (*int*) – Coefficient in the relationship `order =
              k*aperture + offset`.
            * **std** (*float*) – Standard deviation of wavelength fitting in Å.
            * **nuse** (*int*) – Number of lines used in the wavelength
              fitting.
            * **ntot** (*int*) – Number of lines found in the wavelength
              fitting.
            * **identlist** (*dict*) – Dict of identified lines.
            * **window_size** (*int*) – Length of window in searching the
              line centers.
            * **xorder** (*int*) – Order of polynomial along X axis in the
              wavelength fitting.
            * **yorder** (*int*) – Order of polynomial along Y axis in the
              wavelength fitting.
            * **maxiter** (*int*) – Maximum number of iteration in the
              wavelength fitting.
            * **clipping** (*float*) – Clipping value of the wavelength fitting.
            * **q_threshold** (*float*) – Minimum *Q*-factor of the spectral
              lines to be accepted in the wavelength fitting.

    Notes:
        If **identfilename** is given and exist, load the identified wavelengths
        from this ASCII file, and display them in the calibration window. If not
        exist, save the identified list into **identfilename** with ASCII
        format.

    See also:
        :func:`recalib`
    """

    # initialize fitting list
    if identfilename is not None and os.path.exists(identfilename):
        identlist = load_ident(identfilename)
    else:
        identlist = {}

    # load the wavelengths
    if os.path.exists(linelist):
        line_list = Table.read(linelist, format='ascii.fixed_width_two_line')
    else:
        line_list = get_linelist(linelist)

    # display an interactive figure
    # reset keyboard shortcuts
    mpl.rcParams['keymap.pan']        = ''   # reset 'p'
    mpl.rcParams['keymap.fullscreen'] = ''   # reset 'f'
    mpl.rcParams['keymap.back']       = ''   # reset 'c'

    # initialize tkinter window
    master = tk.Tk()
    master.resizable(width=False, height=False)

    screen_width  = master.winfo_screenwidth()
    screen_height = master.winfo_screenheight()

    fig_width, fig_height  = figsize
    if None in [fig_width, fig_height]:
        # detremine window size and position
        window_width = int(screen_width-200)
        window_height = int(screen_height-200)
    else:
        window_width = fig_width + 500
        window_height = fig_height + 34

    x = int((screen_width-window_width)/2.)
    y = int((screen_height-window_height)/2.)
    master.geometry('%dx%d+%d+%d'%(window_width, window_height, x, y))

    # display window
    calibwindow = CalibWindow(master,
                              width       = window_width,
                              height      = window_height-34,
                              dpi         = dpi,
                              spec        = spec,
                              figfilename = figfilename,
                              title       = title,
                              identlist   = identlist,
                              linelist    = line_list,
                              fitfuncname = fitfuncname,
                              window_size = window_size,
                              xorder      = xorder,
                              yorder      = yorder,
                              maxiter     = maxiter,
                              clipping    = clipping,
                              q_threshold = q_threshold,
                              fit_filter  = fit_filter,
                              )

    master.mainloop()

    coeff  = calibwindow.param['coeff']
    npixel = calibwindow.param['npixel']
    k      = calibwindow.param['k']
    offset = calibwindow.param['offset']

    aper = spec['aperture'][0]
    order = k*aper + offset
    wl = get_wavelength(coeff, npixel, np.arange(npixel), order)
    # refresh the direction code
    if wl[0] < wl[-1]:
        sign = '+'
    else:
        sign = '-'
    new_direction = 'x' + {1:'r', -1:'b'}[k] + sign

    # organize results
    result = {
              'coeff':       coeff,
              'npixel':      npixel,
              'k':           k,
              'offset':      offset,
              'std':         calibwindow.param['std'],
              'nuse':        calibwindow.param['nuse'],
              'ntot':        calibwindow.param['ntot'],
              'identlist':   calibwindow.identlist,
              'fitfuncname': calibwindow.param['fitfuncname'],
              'window_size': calibwindow.param['window_size'],
              'xorder':      calibwindow.param['xorder'],
              'yorder':      calibwindow.param['yorder'],
              'maxiter':     calibwindow.param['maxiter'],
              'clipping':    calibwindow.param['clipping'],
              'q_threshold': calibwindow.param['q_threshold'],
              'direction':   new_direction,
            }


    return result

if __name__=='__main__':

    parser = argparse.ArgumentParser(description='Wavelength Identification')

    parser.add_argument('filename')
    parser.add_argument('-l', '--linelist', nargs='?')
    parser.add_argument('-x', '--xorder', nargs='?', type=int, default=3)
    parser.add_argument('-y', '--yorder', nargs='?', type=int, default=3)
    parser.add_argument('-c', '--clipping', nargs='?', type=float, default=3)
    parser.add_argument('-f', '--fitfunc', nargs='?', default='gaussian')
    parser.add_argument('-w', '--windowsize', nargs='?', dest='window_size',
                        type=int, default=15)
    parser.add_argument('-d', '--dpi', nargs='?', type=int, default=150)
    parser.add_argument('-s', '--size', nargs='?', type=int, default=2500)
    parser.add_argument('-q', '--qthreshold', nargs='?', type=float, default=10)
    parser.add_argument('-r', '--resume', nargs='?', default=None)
    parser.add_argument('-o', '--output', required=True)

    args = parser.parse_args()

    # open input file
    filename = args.filename
    hdulst = fits.open(filename)
    head = hdulst[0].header
    spec = hdulst[1].data
    hdulst.close()

    # get output figname
    mobj = re.match('(\S*)\.fit[s]?', os.path.basename(filename))
    fname = mobj.group(1)
    figname = '{}.png'.format(fname)
    
    # pop up a calibration window and identify the wavelengths manually
    calib = ident_wavelength(spec,
                figfilename = figname,
                title       = os.path.basename(filename),
                linelist    = args.linelist,
                identfilename = args.resume,
                fitfuncname = args.fitfunc,
                window_size = args.window_size,
                xorder      = args.xorder,
                yorder      = args.yorder,
                maxiter     = 5,
                clipping    = args.clipping,
                q_threshold = args.qthreshold,
                figsize     = (args.size, args.size*0.6),
                dpi         = args.dpi,
                )

    # save ident list as ASCII file
    # get outfilename
    # split dir and basename of input filename
    dirname = os.path.dirname(filename)
    fname = os.path.basename(filename)
    # get timestamp
    now = datetime.datetime.now()
    suffix = datetime.datetime.strftime(now, '%y%m%d-%H%M%S')
    outfilename = os.path.join(dirname, 'ident.{}.{}.dat'.format(fname, suffix))
    # save ident list
    save_ident(calib['identlist'], outfilename)

    # reference the ThAr spectra
    spec, card_lst, identlist = reference_self_wavelength(spec, calib)


    # save result as FITS file
    prefix = 'HIERARCH GAMSE WLCALIB '
    for key, value in card_lst:
        # clear previous WLCALIB cards, if any
        # make sure there is only one such key in the header
        head[prefix+key] = value

    hdu_lst = fits.HDUList([
                fits.PrimaryHDU(header=head),
                fits.BinTableHDU(spec),
                fits.BinTableHDU(identlist),
                ])

    # save in midproc path as a wlcalib reference file
    hdu_lst.writeto(args.output, overwrite=True)
    message = 'Wavelength calibrated spectra written to {}'.format(args.output)
    print(message)
