# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import sys
import os
import time
from pathlib import Path
from psutil import pid_exists
import frappy
from frappy.errors import ConfigError
from frappy.lib import generalConfig
from frappy.lib import formatExtendedTraceback
from frappy.config import to_config_path
from frappy.editcurses.configdata import Value, cfgdata_to_py, cfgdata_from_py, \
    get_datatype, site, ModuleClass, stringtype, class_completion, recommended_prs, \
    make_value, ModuleNameCompletion, Module
from frappy.io import IOBase
import frappy.editcurses.terminalgui as tg
from frappy.editcurses.terminalgui import Main, MenuItem, TextEdit, PushButton, \
    ModalDialog
from frappy.editcurses.screenwriter import KEY


KEY.add(
    TOGGLE_DETAILED='^t',
    NEXT_VERSION='^n',
    PREV_VERSION='^b',
)


VERSION_SEPARATOR = "\n''''"
TIMESTAMP_FMT = '%Y-%m-%d-%H%M%S'


def get_timestamp(file):
    return time.strftime(TIMESTAMP_FMT, time.localtime(file.stat().st_mtime))


class TopWidget:
    parent_cls = Main

    def handle(self, main):
        key = super().handle(main)
        if key == KEY.LEFT:
            main.toggle_detailed()
            return None
        return key


class Child(tg.Widget):
    """child widget of NodeWidget ot ModuleWidget"""
    parent = TopWidget

    def get_name(self):
        raise NotImplementedError

    def collect(self, result):
        pass

    def check_data(self):
        pass

    def is_valid(self):
        return True


class HasValue(Child):
    clsobj = None

    def get_name(self):
        raise NotImplementedError

    def init_value_widget(self, parent, valobj):
        self.init_parent(parent)
        self.valobj = valobj
        if isinstance(valobj.completion, ModuleNameCompletion):
            valobj.completion.get_names = self.get_module_list

    def validate(self, strvalue, main=None):
        pname = self.get_name()
        valobj = self.valobj
        try:
            if pname != 'cls':
                if self.clsobj != self.parent.clsobj:
                    self.clsobj = self.parent.clsobj
                    self.valobj = make_value(self.get_name(), self.clsobj, valobj.value)
                    if isinstance(self.valobj.completion, ModuleNameCompletion):
                        valobj.completion.get_names = self.get_module_list
            self.valobj.validate_from_string(strvalue)
            self.error = None
        except Exception as e:
            self.error = str(e)
        if main and valobj != self.valobj:
            main.touch()
        return valobj.strvalue

    def get_module_list(self, basecls):
        assert isinstance(self.valobj.completion, ModuleNameCompletion)
        module = self.parent
        main = module.parent
        return main.get_module_list(basecls, module.get_name())

    def check_data(self):
        self.validate(self.valobj.strvalue)

    def is_valid(self):
        return self.get_name() and self.valobj.strvalue


class ValueWidget(HasValue, tg.LineEdit):
    fixedname = None
    error = None

    def __init__(self, parent, name, valobj, label=None):
        """init a value widget

        :param parent: the parent widget
        :param name: the initial name
        :param valobj: the object containing value and datatype
        :param label: None: the name is changeable, else: a label (which might be different from name)
        """
        self.init_value_widget(parent, valobj)
        if label is not None:
            labelwidget = tg.TextWidget(label)
            self.fixedname = name
        else:
            labelwidget = tg.NameEdit(name, self.validate_name)
        if valobj.completion:
            valueedit = tg.TextEditCompl(valobj.strvalue, self.validate, valobj.completion)
        else:
            valueedit = TextEdit(valobj.strvalue, self.validate)
        super().__init__(labelwidget, valueedit)

    def validate_name(self, name, main):
        widget_dict = self.parent.widget_dict
        pname, dot, prop = name.partition('.')
        if pname.isidentifier() and (prop.isidentifier or dot == ''):
            other = widget_dict.get(name)
            if other and other != self:
                self.error = f'duplicate name {name!r}'
                return self.get_name()
            self.clsobj = None
            self.error = None
            widget = widget_dict.pop(self.get_name(), None)
            if widget:
                widget_dict[name] = widget
        else:
            self.error = f'illegal name {name!r}'
            return self.get_name()
        return name

    def get_name(self):
        if self.fixedname:
            return self.fixedname
        return self.labelwidget.value

    def collect(self, result):
        """collect data"""
        name = self.get_name()
        if name:
            result[name] = self.valobj

    def draw(self, wr, in_focus=False):
        super().draw(wr, in_focus)
        valobj = self.valobj
        if valobj.strvalue == '' and valobj.default is not None:
            default = valobj.datatype.to_string(valobj.default)
            if not in_focus:
                wr.dim(default)
            return
        if self.error:
            wr.norm('  ')
            wr.error(self.error)


class DocWidget(HasValue, tg.MultiLineEdit):
    parent_cls = TopWidget

    def __init__(self, parent, name, valobj):
        self.init_value_widget(parent, valobj)
        self.valobj = valobj
        self.name = name
        super().__init__(name, valobj.strvalue, 'doc:   ')

    def get_name(self):
        return self.name

    def collect(self, result):
        self.valobj.set_value(self.value)
        result[self.name] = self.valobj


class BaseWidget(TopWidget, tg.Container):
    """base for Module or Node"""
    clsobj = None
    header = None
    special_names = 'name', 'cls', 'description'
    endline_help = 'RET: add module  p: add property'

    def init(self, parent):
        self.widgets = []
        self.init_parent(parent, EditorMain)
        self.focus = 0
        self.widget_dict = {}
        self.fixed_names = self.get_fixed_names()

    def get_menu(self):
        main = self.parent
        if main.version_view:
            return main.get_menu()
        return self.context_menu + main.get_menu()

    def get_fixed_names(self):
        result = {k: k for k in self.special_names}
        result['name'] = self.header
        return result

    def add_widget(self, name, valobj, pos=None):
        label = self.fixed_names.get(name)
        widget = ValueWidget(self, name, valobj, label)
        self.widget_dict[name] = widget
        if pos is None:
            self.widgets.append(widget)
        else:
            if pos < 0:
                pos += len(self.widgets)
            self.widgets.insert(pos, widget)
        return widget

    def new_widget(self):
        raise NotImplementedError

    def insert_module(self, module, after_current=False):
        main = self.parent
        main.insert(main.focus + after_current, module)
        main.set_focus(main.focus + 1)
        if not after_current:
            self.set_focus(1)
            main.advance(-1)
        # module.set_focus(0)  # go to name widget

    def add_module(self, after_current=False):
        modcfg = {'name': Value(''),
                  'cls': Value(f'{site.frappy_subdir}.', ModuleClass),
                  'description': Value('')}
        self.insert_module(ModuleWidget(self.parent, '', modcfg), after_current)

    def add_iomodule(self, after_current=False):
        modcfg = {'name': Value(''), 'uri': Value('')}
        self.insert_module(IOWidget(self.parent, '', modcfg), after_current)

    def get_widget_value(self, key):
        return self.widget_dict[key].valobj.strvalue

    def get_name(self):
        return self.get_widget_value('name')

    def draw_summary(self, wr, in_focus):
        raise NotImplementedError

    def draw(self, wr, in_focus=False):
        main = self.parent
        wr.set_leftwidth(main.leftwidth)

        if main.detailed:
            self.draw_widgets(wr, in_focus)
        else:
            self.draw_summary(wr, in_focus)

    def collect(self, result):
        name = self.get_name()
        if name:
            result[name] = modcfg = {}
            for w in self.widgets:
                w.collect(modcfg)


class ModuleName(Value):
    def __init__(self, main, name):
        self.main = main
        super().__init__(name)

    def validate_from_string(self, strvalue):
        if not strvalue:
            self.strvalue = self.value = ''
            raise ValueError('empty name')
        if strvalue != self.value and strvalue in self.main.modules:
            self.strvalue = self.value = ''
            raise ValueError(f'duplicate name {strvalue!r}')
        self.value = self.strvalue = strvalue


class ModuleWidget(BaseWidget):
    header = 'Module'
    endline_help = 'RET: add module  i: add io module  p: add parameter or property'

    def __init__(self, parent, name, modulecfg):
        assert name == modulecfg['name'].value
        modulecfg['name'] = ModuleName(parent, name)
        self.init(parent)
        self.context_menu = [
            MenuItem('add parameter/property', 'p', self.new_widget),
            MenuItem('add module', 'm', self.add_module),
            MenuItem('add io module', 'i', self.add_iomodule),
            MenuItem('purge empty items', 'e', self.purge_prs),
            MenuItem('add configurable items', 'a', self.complete_prs),
            MenuItem('cut module', KEY.CUT, parent.cut_module),
        ]

        self.configure_class(modulecfg.get('cls'))

        for pname, valobj in modulecfg.items():
            self.add_widget(pname, valobj)
        self.widgets.append(EndLine(self))

    def configure_class(self, clsvalue):
        clsvalue.callback = self.update_cls
        clsvalue.completion = class_completion
        clsobj = clsvalue.value
        if clsobj:
            self.fixed_names.update({k: k for k, v in recommended_prs(clsobj).items() if v})

    def get_class(self):
        clsvalue = self.widget_dict['cls'].valobj
        clsvalue.set_from_string(clsvalue.strvalue)
        return self.clsobj or Module

    def new_widget(self, name='', pos=None):
        self.add_widget(name, Value('', *get_datatype('', self.clsobj, ''), from_string=True), self.focus)

    def update_widget_dict(self):
        self.widget_dict = {w.get_name(): w for w in self.widgets}

    def get_name_info(self):
        return self.clsobj, self.widget_dict

    def handle(self, main):
        while True:
            key = super().handle(main) if main.detailed else main.get_key()
            if key in (KEY.RIGHT, KEY.TAB):
                main.detailed = True
                main.status('')
                main.offset = None  # recalculate offset from screen pos
            else:
                return key

    def current_row(self):
        main = self.parent
        return super().current_row() if main.detailed else 0

    def height(self, to_focus=None):
        main = self.parent
        return super().height() if main.detailed else 1

    def check_data(self):
        """check clsobj is valid and check all params and props"""
        for widget in self.widgets:
            widget.check_data()

    def update_cls(self, cls):
        if cls != self.clsobj:
            self.complete_prs(True)
            self.clsobj = cls
            self.check_data()
        return cls

    def complete_prs(self, only_mandatory=False):
        if self.clsobj:
            fixed_names = self.get_fixed_names()
            names = set(w.get_name() for w in self.widgets)
            for name, mandatory in recommended_prs(self.clsobj).items():
                if mandatory:
                    fixed_names[name] = name
                if name not in names and mandatory >= only_mandatory:
                    valobj = Value('', *get_datatype(name, self.clsobj, ''))
                    widget = self.add_widget(name, valobj, -1)
                    if mandatory:
                        widget.error = 'please set this mandatory property'
            self.fixed_names = fixed_names
            self.update_widget_dict()

    def purge_prs(self):
        self.widgets = [w for w in self.widgets if w.get_name() in self.fixed_names or w.is_valid()]
        self.update_widget_dict()

    def draw_summary_right(self, wr):
        half = (wr.width - wr.col) // 2
        wr.norm(f"{self.get_widget_value('description').ljust(half)} {self.get_widget_value('cls')} ")

    def draw_summary(self, wr, in_focus):
        wr.startrow()
        wr.norm(self.header.ljust(7))
        name = self.get_widget_value('name')
        if in_focus:
            wr.set_cursor_pos()
            wr.bright(name, round(wr.width * 0.2))
        else:
            wr.norm(name.ljust(round(wr.width * 0.2)))
        self.draw_summary_right(wr)

    def collect(self, result):
        super().collect(result)
        name = self.get_name()
        if name:
            assert result[name].pop('name').value == name


class IOWidget(ModuleWidget):
    header = 'IO'
    endline_help = 'RET: add module  p: add property'
    special_names = 'name', 'uri'

    def __init__(self, parent, name, modulecfg):
        urivalue = modulecfg.get('uri')
        if urivalue is None:
            modulecfg['uri'] = Value('uri', stringtype)
        super().__init__(parent, name, modulecfg)

    def add_widget(self, name, valobj, pos=None):
        if name != 'cls' and (
                name != 'description' or valobj.strvalue):
            super().add_widget(name, valobj)

    def draw_summary_right(self, wr):
        half = (wr.width - wr.col) // 2
        ioname = self.get_name()
        modules = [w.get_name() for w in self.parent.widgets if w.get_widget_value('io') == ioname]
        wr.norm(f"{self.get_widget_value('uri').ljust(half)} for {','.join(modules)} ")

    def get_class(self):
        return IOBase

    def collect(self, result):
        name = self.get_name()
        if name:
            super().collect(result)
            modcfg = result[name]
            modcfg['cls'] = Value('<auto>', stringtype)
            modcfg.setdefault('description', Value('', stringtype))

    def configure_class(self, clsvalue):
        pass


class EndLine(Child):
    parent_cls = TopWidget

    def __init__(self, parent):
        self.init_parent(parent)
        super().__init__()

    def draw(self, wr, in_focus=False):
        wr.startrow()
        if in_focus:
            wr.set_cursor_pos(wr.leftwidth)
            wr.col = wr.leftwidth
            wr.high(self.parent.endline_help)

    def collect(self, result):
        pass

    def check_data(self):
        pass

    def get_name(self):
        return None

    def handle(self, main):
        self.showhelp = False
        while True:
            key = main.get_key()
            if key in (KEY.RETURN, KEY.ENTER):
                self.parent.add_module(True)
            elif key in (KEY.LEFT, KEY.UP, KEY.DOWN, KEY.QUIT):
                return key
            elif key == 'i':
                self.parent.add_iomodule(True)
            elif key == 'p':
                self.parent.new_widget()
                return KEY.GOTO_MAIN
            return None


class NodeName(Value):
    def __init__(self, main, name):
        self.main = main
        super().__init__(name)

    def validate_from_string(self, strvalue):
        try:
            self.main.set_node_name(strvalue)
        except Exception:
            self.value = self.strvalue = self.main.cfgname
            raise


class NodeWidget(BaseWidget):
    header = 'Node'
    special_names = 'name', 'equipment_id', 'interface', 'title', 'doc'
    summ_edit = {'name', 'title', 'doc'}  # editable widgets in summary

    def __init__(self, parent, name, nodecfg):
        nodecfg['name'] = NodeName(parent, name)
        self.init(parent)
        self.context_menu = [
            MenuItem('add parameter/property', 'p', self.new_widget),
            # MenuItem('select line', '^K', self.select, None),
        ]
        for pname, valobj in nodecfg.items():
            if pname == 'doc':
                docwidget = DocWidget(self, pname, valobj)
                self.widgets.append(docwidget)
                self.widget_dict['doc'] = docwidget
            else:
                self.add_widget(pname, valobj)
        self.widgets.append(EndLine(self))

    def new_widget(self, name=''):
        """insert new widget at focus pos"""
        self.add_widget(name, Value('', None, from_string=True), self.focus)

    def get_name(self):
        return 'node'

    def set_focus(self, focus, step=1):
        while super().set_focus(focus, step):
            if self.parent.detailed or self.get_focus_widget().get_name() in self.summ_edit:
                return True
            focus = self.focus + step
        return False

    def focus_row(self, to_focus):
        main = self.parent
        if main.detailed:
            return super().focus_row(to_focus)
        height = 0
        for widget in self.widgets[:to_focus]:
            name = widget.get_name()
            if name in self.summ_edit:
                height += widget.height()
        return height

    def draw_summary(self, wr, in_focus):
        # wr.startrow()
        # wr.norm('Node   ')
        wr.set_leftwidth(7)
        focus = self.focus if in_focus else None
        for nr, widget in enumerate(self.widgets):
            name = widget.get_name()
            if name in self.summ_edit:
                widget.draw(wr, nr == focus)


class SaveDialog(ModalDialog):
    def __init__(self, filename):
        self.fileedit = tg.DialogInput(self, 'file', str(filename))
        self.result = None
        super().__init__([self.fileedit,
                          PushButton(self, 'save and quit', self.save),
                          PushButton(self, 'quit without saving', self.nosave),
                          PushButton(self, 'cancel', self.cancel)])

    def execute(self, main):
        self.set_focus(1)  # go to save button
        super().execute(main)
        if self.fileedit.result:
            return self.save()
        if self.result is None:
            # no button was pressed
            return self.cancel()
        return self.result()

    def save(self):
        self.filename = self.fileedit.get_value()
        return KEY.QUIT

    def nosave(self):
        self.filename = None
        return KEY.QUIT

    def cancel(self):
        return None


class EditorMain(Main):
    name = 'Main'
    detailed = False
    tmppath = None
    version_view = 0  # current version or when > 0 previous versions (not editable)
    completion_widget = None  # widget currently running a thread for guesses
    leftwidth = 0.15
    cut_modules = ()
    cut_extend = False

    def __init__(self, cfg):
        try:
            self.titlebar = tg.TitleBar('Frappy Cfg Editor')
            super().__init__([], [self.titlebar],  [tg.StatusBar(self)],
                             help_file=Path(frappy.__file__).parents[1] / 'resources/editcurses_help.txt')
            # self.select_menu = MenuItem('select module', CUT_KEY)
            self.version_menu = [
                MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
                MenuItem('next version', KEY.NEXT_VERSION, self.next_version),
                MenuItem('restore this version', 'r', self.restore_version),
                MenuItem('copy module', KEY.CUT, self.cut_module),
            ]
            self.main_menu = [
                MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
            ]
            self.detailed_menuitem = MenuItem('toggle detailed', KEY.TOGGLE_DETAILED, self.toggle_detailed)
            self.cut_menuitem = MenuItem('insert cut modules', KEY.PASTE, self.insert_cut)
            self.version_dir = Path('~/.local/share/frappy_config_editor').expanduser()
            self.version_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
            self.cfgname = None
            self.pidfile = None
            self.dirty = False
            # cleanup pidfiles
            for file in self.version_dir.glob('*.pid'):
                pidstr = file.read_text(encoding='ascii')
                if not pid_exists(int(pidstr)):
                    file.unlink()
            cfgpath = Path(cfg)
            if cfg != cfgpath.stem:  # this is a filename
                if cfg.endswith('_cfg.py'):
                    cfg = cfgpath.name[:-7]
                else:
                    cfg = self.cfgpath.stem
            else:
                cfgpath = None
            self.filecontent = None
            self.set_node_name(cfg, cfgpath)
            self.modules = {}  # dict <module name> of <module class>
            self.init_from_content(self.filecontent)
            self.module_clipboard = {}
            self.pr_clipboard = {}
        except Exception:
            print(formatExtendedTraceback())

    def get_menu(self):
        self.detailed_menuitem.name = 'summary view' if self.detailed else 'detailed view'
        menu = self.version_menu if self.version_view else self.main_menu
        if self.cut_modules and not self.version_view:
            self.cut_menuitem.name = f'insert {self.describe_buffer()}'
            menu.append(self.cut_menuitem)
        return menu + [self.detailed_menuitem] + self.context_menu

    def init_from_content(self, filecontent):
        widgets = []
        nodedata, moddata = cfgdata_from_py(self.cfgname, self.cfgpath, filecontent, self.log)
        widgets.append(NodeWidget(self, self.cfgname, nodedata))
        for key, modcfg in moddata.items():
            clsvalue = modcfg.get('cls')
            if clsvalue:
                if clsvalue.strvalue == '<auto>':
                    widgets.append(IOWidget(self, key, modcfg))
                    continue
            else:
                modcfg['cls'] = Value('', ModuleClass, from_string=True)
            widgets.append(ModuleWidget(self, key, modcfg))
        self.widgets = widgets
        # self.dirty = False

    def get_module_list(self, basecls, ownname):
        """get module list for Attached.

        filter by basecls ad exclude own name
        """
        return [w.get_name() for w in self.widgets
                if w.get_name() != ownname and isinstance(w, ModuleWidget)
                and issubclass(w.get_class(), basecls)]

    def toggle_detailed(self):
        self.detailed = not self.detailed
        self.offset = None  # recalculate offset from screen pos
        self.status(None)

    def get_key(self, timeout=None):
        if self.dirty:
            if not self.version_view:
                self.save()
            self.dirty = False
        while True:
            if self.completion_widget:
                key = super().get_key(0.1)
                if key is None:
                    # may need refresh as contents may be built in background
                    continue
            else:
                key = super().get_key(timeout)
            if self.version_view:
                if isinstance(key, str) or key in [KEY.DEL]:
                    self.status('', 'can not edit previous version')
                else:
                    break
            else:
                break
        return key

    def describe_buffer(self,):
        cm = self.cut_modules
        if not cm:
            return ''
        if len(cm) > 1:
            sep = ',' if len(cm) == 2 else '..'
            return f'modules {cm[0].get_name()}{sep}{cm[-1].get_name()}'
        return f'module {cm[0].get_name()}'

    def cut_module(self):
        if not self.cut_modules:
            self.cut_extend = False
        module = self.get_focus_widget()
        if not isinstance(module, ModuleWidget):
            self.status('', warn='can not cut node')
            return
        if not self.version_view:
            self.widgets[self.focus:self.focus+1] = []
        if not self.cut_extend:
            self.cut_modules = []
        self.cut_modules.append(module)
        self.cut_extend = True
        if self.version_view:
            text = 'copied'
            if not self.set_focus(self.focus + 1):
                if self.cut_modules[-1] == module:
                    self.cut_modules.pop()
        else:
            text = 'cut'
        self.status(f'{self.describe_buffer()} {text}')

    def insert_cut(self):
        if self.cut_modules:
            self.widgets[self.focus:self.focus] = self.cut_modules
            self.cut_modules = []

    def set_node_name(self, name, init_cfgpath=None):
        if name == self.cfgname:
            return
        if not name:
            raise ValueError(f'{name!r} is not a valid node name')
        self.write_pidfile(name)
        self.cfgname = name
        self.titlebar.mid = name
        versions_path = self.version_dir / f'{name}.versions'
        try:
            sections = versions_path.read_text(encoding='utf-8').split(VERSION_SEPARATOR)
            assert sections.pop(0) == ''
        except FileNotFoundError:
            sections = []
        self.versions = dict((v.split('\n', 1) for v in sections))
        # the path every change is written to:
        self.tmppath = self.version_dir / f'{name}.current'
        try:
            # if a file exists already, add it to the version history
            filecontent = self.tmppath.read_text(encoding='utf-8')
            self.add_version(filecontent, get_timestamp(self.tmppath))
        except FileNotFoundError:
            pass
        if init_cfgpath:
            cfgpaths = [init_cfgpath]
        else:
            try:
                cfgpaths = [to_config_path(name, self.log)]
            except ConfigError:
                cfgpaths = []
        cfgpaths.append(self.tmppath)
        for cfgpath in cfgpaths:
            try:
                filecontent = cfgpath.read_text(encoding='utf-8')
                self.cfgpath = cfgpath
                if cfgpath != self.tmppath:
                    self.titlebar.mid = str(cfgpath)
                timestamp = get_timestamp(cfgpath)
                break
            except FileNotFoundError:
                pass
        else:
            filecontent = None
            timestamp = time.strftime(TIMESTAMP_FMT)
            self.cfgpath = cfgpaths[0]
        self.filecontent = filecontent
        self.add_version(filecontent, timestamp)

    def add_version(self, filecontent, timestamp):
        if self.versions:
            to_remove = []
            # remove matching versions
            for key, content in self.versions.items():
                if content == filecontent:
                    to_remove.append(key)
            for key in to_remove:
                self.versions.pop(key)
        if filecontent:
            self.versions[timestamp] = filecontent
            sep = VERSION_SEPARATOR
            versions_path = self.version_dir / f'{self.cfgname}.versions'
            tmpname = versions_path.with_suffix('.tmp')
            with open(tmpname, 'w', encoding='utf-8') as f:
                for ts, section in self.versions.items():
                    f.write(sep)
                    f.write(f'{ts}\n')
                    f.write(section)
            os.rename(tmpname, versions_path)

    def restore_version(self):
        if not self.version_view:
            self.status('this is already the current version')
            return
        self.popupmenu = menu = tg.ConfirmDialog('restore this version? [N]')
        if not menu.handle(self):
            self.status('cancelled restore')
            return
        version = list(self.versions)[-self.version_view]
        self.status(f'restored from {version}')
        content = self.versions.pop(version)
        self.add_version(self.filecontent, time.strftime(TIMESTAMP_FMT))
        self.filecontent = content
        self.version_view = 0
        self.titlebar.right = ''
        self.init_from_content(self.filecontent)
        self.save()

    def set_version(self):
        if self.version_view:
            version = list(self.versions)[-self.version_view]
            self.titlebar.right = f'version {version}'
            try:
                self.init_from_content(self.versions[version])
                self.status(None)
            except Exception as e:
                self.status('', f'bad version: {e}')
        else:
            self.init_from_content(self.filecontent)
            self.titlebar.right = ''

    def prev_version(self):
        maxv = len(self.versions)
        self.version_view += 1
        if self.version_view > maxv:
            self.status('this is the oldest version')
            self.version_view = maxv
        else:
            self.set_version()

    def next_version(self):
        if self.version_view:
            self.version_view -= 1
            self.set_version()
        else:
            self.status('this is the current version')

    def current_row(self):
        return super().current_row() + self.get_topmargin()

    def touch(self):
        self.dirty = True

    def save(self):
        cfgdata = {}
        for widget in self.widgets:
            widget.collect(cfgdata)
        if 'node' not in cfgdata:
            raise ValueError(list(cfgdata), len(self.widgets))
        content = cfgdata_to_py(**cfgdata)
        # if self.cfgpath:
        #    self.cfgpath.write_text(config_code)
        self.tmppath.write_text(content)
        self.filecontent = content

    def quit(self):
        self.save()
        try:
            filecontent = self.cfgpath.read_text()
        except FileNotFoundError:
            filecontent = ''
        if filecontent == self.filecontent:
            self.log.info('%s was not changed', self.cfgpath)
            return True
        savedialog = SaveDialog(self.cfgpath)
        if savedialog.execute(self) == KEY.QUIT:
            filename = savedialog.filename
            if filename:
                self.log.info('saved %r to %r', self.cfgname, filename)
                Path(filename).write_text(self.filecontent, encoding='utf-8')
            return True
        return False

    def advance(self, step):
        done = super().advance(step)
        if done:
            self.get_focus_widget().set_focus(None, step)
        return done

    def write_pidfile(self, name):
        pidfile = self.version_dir / f'{name}.pid'
        mypid = os.getpid()
        for itry in range(15):
            try:
                with open(pidfile, 'x', encoding='ascii') as f:
                    f.write(str(mypid))
                if self.pidfile and self.pidfile.exists():
                    self.pidfile.unlink()
                self.pidfile = pidfile
                return None
            except FileExistsError:
                pass
            try:
                pid = int(pidfile.read_text(encoding='ascii'))
                if pid == mypid:
                    if self.pidfile and self.pidfile != pidfile and self.pidfile.exists():
                        self.pidfile.unlink()
                    return None
                if pid_exists(pid):
                    raise FileExistsError(f'{name} is already edited by process {pid}')
                pidfile.unlink()
            except FileNotFoundError:
                pass
            time.sleep(itry * 0.01)
        raise RuntimeError('pidfile error: too many tries')

    def run(self):
        try:
            super().run()
        except BaseException:
            print(formatExtendedTraceback())
        finally:
            if self.filecontent:
                # add current content to the version history
                try:
                    self.add_version(self.filecontent, time.strftime(TIMESTAMP_FMT))
                except FileNotFoundError:
                    pass
            if self.tmppath and self.tmppath.exists():
                # no need to keep the temporary file, as it has been added to the versions
                self.tmppath.unlink()
            if self.pidfile and self.pidfile.exists():
                self.pidfile.unlink()


if __name__ == "__main__":
    generalConfig.init()
    EditorMain(sys.argv[1]).run()
