import argparse
import collections
import gettext
import importlib
import os
import os.path
import re
import sys
import subprocess
import tempfile
import urwid

import newfol.database
import newfol.exception
import newfol.filemanip as filemanip
import newfol.version

from newfol.database import DatabaseData, DatabaseVersion
from newfol.exception import ProcessFailureError

t = gettext.translation('newfol', fallback=True)
_ = t.gettext

__version__ = newfol.version.version()

logfd = None


def log(*args):
    global logfd
    try:
        path = os.environ['NEWFOL_DEBUG']
    except:
        return
    if logfd is None:
        logfd = open(path, "w")
    logfd.write(" ".join(map(str, args)))
    logfd.flush()


class KeyboardShortcuts(dict):
    def __init__(self, *args, **kwargs):
        self["Q"] = "quit"
        self["f1"] = "previous"
        self["f2"] = "sync-database"
        self["f3"] = "add"
        self["f4"] = "search"
        self["f5"] = "next"
        self["f6"] = "next-secondary"
        self["f9"] = "next-tertiary"
        self["f7"] = "menu"
        self["ctrl f5"] = "invert-selection"
        self["ctrl f"] = "browse-all"
        self["ctrl b"] = "browse"
        self["ctrl x"] = "about"
        self["enter"] = "select"
        self["meta h"] = "backspace"
        self["meta i"] = "up"
        self["meta n"] = "down"
        self["meta d"] = "right"
        self["meta k"] = "left"
        self["meta g"] = "delete"
        self["meta y"] = "remove"
        self["tab"] = "next-field"
        self["meta f"] = "next-field"
        self["meta j"] = "previous-field"
        self["shift tab"] = "previous-field"
        self["meta u"] = "home"
        self["meta ;"] = "end"
        super().__init__(*args, **kwargs)

        # Use list to prevent a RuntimeError (dictionary changed size during
        # iteration).
        for k, v in list(self.items()):
            if v is None:
                del self[k]
                continue
            mobj = re.search(r"^(.*\w+ )([a-z])$", k)
            if mobj is not None:
                self[mobj.group(1) + mobj.group(2).upper()] = v

    def shortcut(self, name):
        for k, v in self.items():
            if name == v:
                return k if len(k) == 1 else k.title().replace(" ", "-")
        raise KeyError


class SelectableListBox(urwid.ListBox):
    def __init__(self, body, obj=None):
        super().__init__(body)
        self.cb_obj = obj

    def _do_cb(self, type_, arg):
        if self.cb_obj is not None:
            self.cb_obj.callback(type_, arg)

    def keypress(self, size, key):
        try:
            key = DatabaseData().keys[key]
        except:
            pass
        if key in ("up", "down"):
            self._do_cb("key", key)
            off = -1 if key == "up" else 1
            focus = self.get_focus()[1] + off
            if focus < len(self.body) and focus >= 0:
                self.set_focus(focus)
        elif key in ("select", "remove"):
            self._do_cb(key, self.get_focus()[0])
        else:
            key = super().keypress(size, key)
            return key


class Tabbable:
    def set_grid(self, grid):
        self.grid = grid

    def select_item(self):
        pass

    def _handle_tab(self, key):
        try:
            key = DatabaseData().keys[key]
        except:
            pass
        if key in ("next-field", "previous-field"):
            item = 0
            for i, cell in enumerate(self.grid.cells):
                if self in cell.widget_list:
                    item = i
                    break
            offset = 1 if key == "next-field" else -1
            while True:
                item += offset
                if item >= len(self.grid.cells):
                    item = 0
                elif item < 0:
                    item = len(self.grid.cells)-1
                widget = self.grid.cells[item]
                if len(widget.widget_list) > 0:
                    self.grid.set_focus(self.grid.cells[item])
                    widget.widget_list[1].select_item()
                    return key


class FancyCheckBox(urwid.CheckBox, Tabbable):
    def select_item(self):
        self.toggle_state()
        self.toggle_state()

    def keypress(self, size, key):
        try:
            key = DatabaseData().keys[key]
        except:
            pass
        if len(key) == 1:
            self.toggle_state()
        elif key in ("next-field", "previous-field"):
            return self._handle_tab(key)
        else:
            return super().keypress(size, key)


class FancyRadioButton(urwid.RadioButton, Tabbable):
    def select_item(self):
        pass

    def keypress(self, size, key):
        try:
            key = DatabaseData().keys[key]
        except:
            pass
        if len(key) == 1:
            self.toggle_state()
        elif key in ("next-field", "previous-field"):
            return self._handle_tab(key)
        else:
            return super().keypress(size, key)


class RecordEditBox(urwid.Edit, Tabbable):
    def select_item(self):
        self.set_edit_pos(0)

    def get_state(self):
        return bool(self.get_edit_text())

    def keypress(self, size, key):
        if len(key) > 1 and key != "enter":
            key = DatabaseData().keys.get(key, key)
        if key in ("next-field", "previous-field"):
            return self._handle_tab(key)
        else:
            return super().keypress(size, key)


class RecordFormatter:
    def __init__(self, recs, fields):
        self._recs = recs
        self._fields = fields

    @staticmethod
    def longest(x):
        if len(x) == 0:
            return []
        res = [0] * len(x[0])
        for l in x:
            for i, li in enumerate(l):
                if len(li) > res[i]:
                    res[i] = len(li)
        return res

    def format(self):
        item_fields = []
        for rec in self._recs:
            item = []
            for field in self._fields:
                if field == "table":
                    item.append(rec.table)
                else:
                    item.append(rec.fields[field])
            item_fields.append(item)
        lengths = self.longest(item_fields)
        formatted = []
        for item in item_fields:
            pieces = []
            for field, length in zip(item, lengths):
                fmt = "{:<%d}" % length
                pieces.append(fmt.format(field))
            formatted.append(": ".join(pieces))
        return formatted


class Session:
    def __init__(self, loop):
        self.loop = loop

    def render_view(self, view):
        view.session = self
        view.render(self.loop)

    def render_previous_view(self):
        if len(DatabaseData().views) > 1:
            DatabaseData().views.pop()
            DatabaseData().views[-1].rerender(self.loop)


class View:
    def render(self, loop):
        pass

    def rerender(self, loop):
        pass

    def callback(self, type_, obj):
        pass

    @staticmethod
    def text(text, *classes):
        return urwid.AttrMap(urwid.Text(text), *classes)

    @staticmethod
    def is_callback_type(type_, category):
        if category == "select":
            return type_.startswith("select") or type_.startswith("next")
        elif category == "simple-select":
            return type_ in ("select", "next")
        elif category in ("change", "cancel"):
            return type_ == category
        else:
            raise newfol.exception.ImplementationError("Unknown type %s" %
                                                       type_)


class IntroView(View):
    def render(self, loop):
        self.loop = loop
        dbd = DatabaseData()
        text = ["newfol " + __version__,
                _("Press %s to quit at any time.") % dbd.keys.shortcut("quit"),
                _("Press %s to get started.") % dbd.keys.shortcut("menu"),
                _("Press %s to see database information.") %
                dbd.keys.shortcut("about")]
        pile = urwid.Pile([
            urwid.AttrMap(urwid.Text(("text", i), align="center"), "bg")
            for i in text])
        fill = urwid.Filler(pile)
        self.widget = urwid.AttrMap(fill, "bg")
        loop.widget = self.widget
        dbd.views.append(self)

    def rerender(self, loop):
        loop.widget = self.widget


class StandardView(View):
    def _do_render(self, loop, func):
        self.loop = loop
        body = func(loop)
        if body is None:
            return
        shortcuts = map(lambda x: DatabaseData().keys.shortcut(x),
                        ["quit", "menu", "add", "search", "browse-all",
                         "browse"])
        header1 = urwid.Text("newfol " + __version__)
        header2 = urwid.Text(_("%s: quit; %s: menu; %s: add; %s: search; " +
                               "%s: browse recs; %s: browse dbs") %
                             tuple(shortcuts))
        pile = urwid.Pile([header1, header2])
        head = urwid.AttrMap(pile, "top")
        top = urwid.Frame(body, head)
        self.widget = top
        loop.widget = self.widget
        return self.widget

    def render(self, loop):
        if self._do_render(loop, self._render_standard) is not None:
            DatabaseData().views.append(self)

    def rerender(self, loop):
        if hasattr(self, "_rerender_standard"):
            return self._do_render(loop, self._rerender_standard)
        loop.widget = self.widget


class AboutView(StandardView):
    @staticmethod
    def _display_option(name, *values):
        if isinstance(values[0], bool):
            return ("+" if values[0] else "-") + name
        else:
            return "".join(["+", name] +
                           ["".join(["(", str(val), ")"]) for val in values])

    def _render_standard(self, loop):
        self.loop = loop
        key = dict(map(lambda x: (x, DatabaseData().keys.shortcut(x)),
                       ["quit", "previous", "sync-database", "add",
                        "search", "next", "next-secondary",
                        "next-tertiary", "menu", "about", "browse"]))
        try:
            recver = DatabaseData().database().records()[0].version()
        except IndexError:
            recver = '?'
        db = DatabaseData().database()
        ver = db.version()
        msgs = [
            "newfol " + __version__,
            _("Keys: %s to quit, %s: previous view, %s: write to disk,") %
            (key['quit'], key['previous'], key['sync-database']),
            _("%s: add record, %s: search, %s: next, %s: secondary next,") %
            (key['add'], key['search'], key['next'], key['next-secondary']),
            _("%s: tertiary next, %s: menu, %s: this screen,") %
            (key['next-tertiary'], key['menu'], key['about']),
            _("%s: browse by table") %
            (key['browse']),
            "",
            " ".join([
                self._display_option("exe_ok",
                                     db.schema().execution_allowed()),
                self._display_option("homedir",
                                     db.location())]),
            " ".join([
                self._display_option("serialization",
                                     ver.serialization(),
                                     ver.serialization_version()),
                self._display_option("rec_ver",
                                     ver.record_version(),
                                     recver,
                                     filemanip.Record([]).version())
            ]),
        ]
        pile = urwid.Pile([
            urwid.AttrMap(urwid.Text(("text", i), align="center"), "bg")
            for i in msgs])
        fill = urwid.Filler(pile)
        return urwid.AttrMap(fill, "bg")


class RecordView(StandardView):
    def __init__(self, title, button, record=None):
        dbd = DatabaseData()
        if record is None:
            self.rec = filemanip.Record([None] * dbd.schema().nfields())
        else:
            self.rec = record
        self.button_name = button
        self.title = title

    def _compute_titles_from_table(self, table):
        titles = []
        schema = DatabaseData().schema()
        for i in range(schema.nfields() + 1):
            if i == 0:
                text = _("Table name")
            elif table in schema.mapping():
                text = schema.mapping()[table].desc[i-1] or ""
            else:
                text = ""
            titles.append(text)
        return titles

    def _set_titles_for_grid(self, table):
        titles = self._compute_titles_from_table(table)
        for i in range(DatabaseData().schema().nfields() + 1):
            self.cells[i].widget_list[0] = self.text(titles[i], "description")

    def _map_cells(self):
        cells = []
        for item in DatabaseData().schema().layout():
            if item is None:
                pile = urwid.Pile([])
            else:
                pile = self.cells[item]
            cells.append(pile)
        return cells

    def _create_body(self, rec, i):
        try:
            if i == 0:
                text = rec.table
            else:
                text = rec.fields[i-1]
            text = text or ""
        except:
            text = ""
        return RecordEditBox(edit_text=text, multiline=True)

    def _render_record(self, rec):
        self.cells = []
        schema = DatabaseData().schema()
        for i in range(schema.nfields() + 1):
            head = self.text("", "description")
            body = self._create_body(rec, i)
            pile = urwid.Pile([head, body])
            self.cells.append(pile)
        mapped_cells = self._map_cells()
        screen_cols = self.loop.screen.get_cols_rows()[0]
        grid = urwid.GridFlow(mapped_cells,
                              int((screen_cols - 2) / schema.columns()), 1,
                              1, "left")
        self.grid = grid
        for i in self.cells:
            i.widget_list[1].set_grid(self.grid)
        self._set_titles_for_grid(rec.table)
        return grid

    def callback(self, type_, obj):
        if type_ == "cancel":
            render_previous_view(self.loop)

    def _setup_button(self, button):
        button.cb_obj = self

        def callback_glue(button):
            type_ = "cancel" if button.label == _("Cancel") else "select"
            button.cb_obj.callback(type_, button)
        urwid.connect_signal(button, 'click', callback_glue)

    def _render_buttons(self):
        button_ok = urwid.Button(self.button_name)
        button_cancel = urwid.Button(_("Cancel"))
        self._setup_button(button_ok)
        self._setup_button(button_cancel)
        return urwid.Columns([button_ok, button_cancel])

    def _render_standard(self, loop):
        self.loop = loop
        head = self.text(self.title, "header")
        # We need a box widget for the frame, but GridFlow is a flow widget.
        pile = urwid.Pile([self._render_record(self.rec),
                           self._render_buttons()])
        filler = urwid.Filler(pile, valign="top")
        return urwid.Frame(filler, head)


class DisplayRecordView(RecordView):
    def __init__(self, title=None, record=None):
        if title is None:
            title = _("Add Record")
        super().__init__(title, _("Commit"), record)

    def _build_record(self):
        fields = [i.widget_list[1].get_edit_text() for i in self.cells]
        rec = filemanip.Record(fields[1:])
        rec.table = fields[0]
        return rec

    def _autofill(self, table):
        schema = DatabaseData().schema()
        if not schema.execution_allowed():
            return False
        if table not in schema.mapping():
            return False
        mapping = schema.mapping()[table].get_mapping_onto("internal",
                                                           "default")
        for i in range(0, len(mapping)):
            if mapping[i] == "" or mapping[i] is None:
                continue
            try:
                val = eval(mapping[i])
                if val is None:
                    continue
            except Exception:
                msg = _('%s is not a valid expression.')
                mbox = MessageBox(msg % mapping[i])
                mbox.render(self.loop)
                mapping[i] = None
                continue
            self.cells[i+1].widget_list[1].set_edit_text(str(val))

    def callback(self, type_, obj):
        if self.is_callback_type(type_, "simple-select"):
            dbd = DatabaseData()
            newrec = self._build_record()
            recs = []
            inserted = False
            for r in dbd.database().records():
                if r is self.rec:
                    r.fields[:] = newrec.fields[:]
                    r.table = newrec.table
                    inserted = True
                recs.append(r)
            if not inserted:
                recs.append(newrec)
            dbd.database().records()[:] = recs
            render_previous_view(self.loop)
        elif type_ == "change":
            self._set_titles_for_grid(obj[1])
            if not any(self.rec.fields) and not self.rec.table:
                self._autofill(obj[1])
        else:
            return super().callback(type_, obj)

    def _setup_db_field(self):
        def change(edit, new_text):
            self.callback("change", (edit, new_text))
        urwid.connect_signal(self.cells[0].widget_list[1], "change",
                             change)

    def _render_standard(self, loop):
        ret = super()._render_standard(loop)
        self._setup_db_field()
        return ret


class SearchRecordView(RecordView):
    def __init__(self, record=None):
        super().__init__(_("Search Records"), _("Select Display Template"),
                         record)

    def _get_fields(self):
        return [i.widget_list[1].get_edit_text() for i in self.cells]

    def callback(self, type_, obj):
        dbd = DatabaseData()
        if self.is_callback_type(type_, "select"):
            fields = self._get_fields()
            fieldres = []
            for f in fields:
                try:
                    fieldres.append(re.compile(f, re.IGNORECASE))
                except:
                    msg = _("'%s' is not a valid regular expression")
                    mb = MessageBox(msg % f)
                    mb.render(self.loop)
                    return

            def filter_(rec):
                if fieldres[0].search(rec.table) is None:
                    return False
                for field, reobj in zip(rec.fields, fieldres[1:]):
                    if reobj.search(field) is None:
                        return False
                return True
            recs = dbd.database().records(filter_)
            self.session.render_view(DisplayTemplateRecordView(fields[0],
                                                               recs))
        elif type_ == "change":
            self._set_titles_for_grid(obj[1])
        else:
            return super().callback(type_, obj)

    def _setup_db_field(self):
        def change(edit, new_text):
            self.callback("change", (edit, new_text))
        urwid.connect_signal(self.cells[0].widget_list[1], "change",
                             change)

    def _render_standard(self, loop):
        ret = super()._render_standard(loop)
        self._setup_db_field()
        return ret


class DisplayTemplateRecordView(RecordView):
    def __init__(self, table, records):
        super().__init__(_("Display Template"), _("Search"))
        self.records = records
        self.table = table

    def _create_body(self, rec, i):
        if i == 0:
            return super()._create_body(rec, i)
        else:
            return FancyCheckBox("")

    def _get_fields(self):
        return [i.widget_list[1].get_state() for i in self.cells]

    def callback(self, type_, obj):
        if self.is_callback_type(type_, "select"):
            selected = self._get_fields()
            self.session.render_view(SortingTemplateRecordView(self.table,
                                                               self.records,
                                                               selected))
        elif type_ == "invert-selection":
            for cell in self.cells[1:]:
                editbox = cell.widget_list[1]
                editbox.toggle_state()
        elif type_ == "change":
            self._set_titles_for_grid(obj[1])
        else:
            return super().callback(type_, obj)

    def _setup_db_field(self):
        def change(edit, new_text):
            self.callback("change", (edit, new_text))
        urwid.connect_signal(self.cells[0].widget_list[1], "change",
                             change)

    def _setup_button(self, button):
        button.cb_obj = self

        def callback_glue(button):
            if button.label == _("Cancel"):
                type_ = "cancel"
            else:
                type_ = "select"
            button.cb_obj.callback(type_, button)
        urwid.connect_signal(button, 'click', callback_glue)

    def _render_buttons(self):
        button_ok = urwid.Button(_("Select Sort Order"))
        button_cancel = urwid.Button(_("Cancel"))
        self._setup_button(button_ok)
        self._setup_button(button_cancel)
        return urwid.Columns([button_ok, button_cancel])

    def _render_standard(self, loop):
        ret = super()._render_standard(loop)
        self._setup_db_field()
        self.cells[0].widget_list[1].set_edit_text(self.table)
        return ret


class SortingTemplateRecordView(RecordView):
    def __init__(self, table, records, selected):
        super().__init__(_("Sorting Template"), _("Search"))
        self.records = records
        self.table = table
        self.selected = selected
        self._group = []

    def _create_body(self, rec, i):
        if i == 0:
            return super()._create_body(rec, i)
        else:
            return FancyRadioButton(self._group, "", False)

    def _get_fields(self):
        l = [i.widget_list[1].get_state() for i in self.cells]
        # We can't use True in l here because it looks for a truthy value, not
        # actually the fixed value True.
        try:
            l[1:].index(True)
            l[0] = False
        except ValueError:
            l[0] = True
        return l

    def callback(self, type_, obj):
        if self.is_callback_type(type_, "select"):
            fieldno = self._get_fields().index(True)

            def lookup(x):
                text = ""
                if fieldno == 0:
                    text = x.table
                else:
                    text = x.fields[fieldno-1]
                return str.lower(text or "")
            keyfunc = lookup
            self.records.sort(key=keyfunc)
            view = SearchListView(self.records, self.selected)
            view.set_render_type(type_)
            self.session.render_view(view)
        elif type_ == "change":
            self._set_titles_for_grid(obj[1])
        else:
            return super().callback(type_, obj)

    def _setup_db_field(self):
        def change(edit, new_text):
            self.callback("change", (edit, new_text))
        urwid.connect_signal(self.cells[0].widget_list[1], "change",
                             change)

    def _setup_button(self, button):
        button.cb_obj = self

        def callback_glue(button):
            if button.label == _("Cancel"):
                type_ = "cancel"
            elif button.label == _("Search"):
                type_ = "select"
            else:
                type_ = "next-secondary"
            button.cb_obj.callback(type_, button)
        urwid.connect_signal(button, 'click', callback_glue)

    def _render_buttons(self):
        button_ok = urwid.Button(_("Search"))
        button_ed = urwid.Button(_("Search and View in Editor"))
        button_cancel = urwid.Button(_("Cancel"))
        self._setup_button(button_ok)
        self._setup_button(button_ed)
        self._setup_button(button_cancel)
        return urwid.Columns([button_ok, button_ed, button_cancel])

    def _render_standard(self, loop):
        ret = super()._render_standard(loop)
        self._setup_db_field()
        self.cells[0].widget_list[1].set_edit_text(self.table)
        return ret


class ListView(StandardView):
    def _rerender_listview(self):
        pass

    def rerender(self, loop):
        self._rerender_listview()
        return super().rerender(loop)

    def _current_item(self):
        pos = self._get_selected_item_position()
        if pos is None:
            return None
        return self.items[pos]

    def _get_selected_item_position(self):
        return self.listwalker.get_focus()[1]

    def _render_listview(self, title, items, listwalker):
        self.items = items
        self.listwalker = listwalker
        self.title = title
        self.listbox = SelectableListBox(listwalker, self)
        head = self.text(title + ' ' + _('(%d items)') % len(items), "header")
        top = urwid.Frame(self.listbox, head)
        return top


class RecordListView(ListView):
    def __init__(self):
        self._render_type = "urwid"
        super().__init__()

    def set_render_type(self, t):
        if t in ("next", "select", "primary", "urwid"):
            self._render_type = "urwid"
        elif t in ("next-secondary", "secondary", "editor-ro"):
            self._render_type = "editor-ro"
        elif t in ("next-tertiary", "tertiary", "editor-rw"):
            self._render_type = "editor-rw"
        else:
            self._render_type = "urwid"

    def _toggle_selected_record(self):
        rec = self._current_item()
        if rec is None:
            return
        rec.deleted = not rec.deleted
        selected = self._get_selected_item_position()
        self.rerender(self.loop)
        self.listwalker.set_focus(selected)

    def _rerender_listview(self):
        """This is the re-rendering counterpart to _render_records."""
        selected = self._get_selected_item_position()
        retval = self._render_records(self.loop, self.title, self.items,
                                      self.fields)
        if selected is not None:
            self.listwalker.set_focus(selected)
        return retval

    def _rerender_standard(self, loop):
        return self._rerender_listview()

    def _render_records(self, loop, title, recs, fields):
        self.loop = loop
        formatter = RecordFormatter(recs, fields)

        def attr_maps(formatter, recs):
            pairs = zip(formatter.format(), recs)
            result = []
            for text, rec in pairs:
                attrs = ["item", "focused-item"]
                if rec.deleted:
                    attrs = ["deleted", "focused-deleted"]
                result.append(self.text(text, *attrs))
            return result
        content = urwid.SimpleListWalker(attr_maps(formatter, recs))
        self.fields = fields
        try:
            if self._render_type == "urwid":
                return self._render_listview(title, recs, content)
            elif self._render_type == "editor-ro":
                return self._render_in_editor_ro(title, recs, content, fields)
            else:
                return self._render_in_editor_rw(title, recs, content)
        except ProcessFailureError as e:
            MessageBox([_('The editor process failed.  The message was:'),
                        str(e)]).render(self.loop)

    def _render_in_editor_ro(self, title, recs, content, selected):
        dbd = DatabaseData()
        items = []
        for rec in recs:
            recvals = []
            for field in selected:
                if field == "table":
                    recvals.append(rec.table)
                else:
                    recvals.append(rec.fields[field])
            items.append(filemanip.Record(recvals))
            if dbd.database().schema().options("editor", "blank-lines"):
                items.append(filemanip.Record([]))
        self._display_in_editor(items, True)

    def _render_in_editor_rw(self, title, recs, content):
        items = self._display_in_editor(recs, False)
        if len(items) != len(recs):
            MessageBox([_("There was a mismatch in the number of lines."),
                        _("Your edits have been discarded.")]).render(
                            self.loop)
        else:
            for i, r in zip(items, recs):
                r.fields[:] = i.fields

    def _display_in_editor(self, recs, ro):
        tempdir = tempfile.TemporaryDirectory()
        tempname = tempdir.name + "/view"
        vault = filemanip.FileStorage('csv', tempname)
        vault.store(recs)
        os.chmod(tempname, 0o400 if ro else 0o600)
        try:
            retval = subprocess.call(["sensible-editor", tempname])
        except Exception as e:
            raise ProcessFailureError(_("editor failed with error: %s") % e)
        if retval:
            raise ProcessFailureError(_('editor exited %d') % retval)
        self.loop.screen.clear()
        return vault.load()

    def callback(self, type_, obj):
        if type_ == "remove":
            self._toggle_selected_record()
        if not self.is_callback_type(type_, "select"):
            return
        cur = self._current_item()
        if cur is None:
            return
        self.session.render_view(DisplayRecordView(_("Current Record"), cur))


class TableContentsListView(RecordListView):
    def __init__(self, table):
        self.table = table
        super().__init__()

    def _render_standard(self, loop):
        dbd = DatabaseData()
        recs = dbd.database().records(lambda rec: rec.table == self.table)
        title = _("List of records by key field for %s") % self.table
        return self._render_records(loop, title, recs,
                                    dbd.schema().key_fields())


class TableListView(ListView):
    def callback(self, type_, obj):
        if not self.is_callback_type(type_, "select"):
            return
        if obj is None:
            text = self._current_item()
        else:
            text = obj.original_widget.get_text()[0]
        dbcview = TableContentsListView(text)
        dbcview.set_render_type(type_)
        self.session.render_view(dbcview)

    def _render_standard(self, loop):
        self.loop = loop
        dbs = []
        for rec in DatabaseData().database().records():
            table = rec.table
            if table not in dbs:
                dbs.append(table)
        content = urwid.SimpleListWalker([
            self.text(w, "item", "focused-item") for w in dbs])
        return self._render_listview(_("List of available tables"), dbs,
                                     content)


class CompleteContentsListView(RecordListView):
    @staticmethod
    def _key(rec):
        return rec.table

    def _render_standard(self, loop):
        self.loop = loop
        sch = DatabaseData().schema()
        items = sorted(DatabaseData().database().records(), key=self._key)
        title = _("List of all records by key field")
        return self._render_records(loop, title, items, sch.key_fields())


class SearchListView(RecordListView):
    def __init__(self, records, selected):
        self.records = records
        self.selected = selected
        super().__init__()

    def _render_standard(self, loop):
        self.loop = loop
        selected = []
        if self.selected[0]:
            selected.append("table")
        for i in range(1, len(self.selected)):
            if self.selected[i]:
                selected.append(i-1)
        return self._render_records(self.loop, _("List of matching records"),
                                    self.records, selected)


class MessageBox(View):
    def __init__(self, message):
        if type(message) is str:
            self.message = [message]
        else:
            self.message = message

    def callback(self, type_, obj):
        if self.is_callback_type(type_, "select"):
            render_previous_view(self.loop)

    def _render_buttons(self):
        def callback_glue(button):
            self.callback("select", button)
        button = urwid.Button(_("OK"))
        urwid.connect_signal(button, 'click', callback_glue)
        return button

    def render(self, loop):
        self.loop = loop
        button = self._render_buttons()
        texts = [self.text(w, "text") for w in self.message]
        texts.append(button)
        pile = urwid.Pile(texts)
        linebox = urwid.LineBox(pile)
        underlay = urwid.Pile([])
        self.widget = urwid.Overlay(linebox, underlay, "center",
                                    ("relative", 50), "middle", None)
        loop.widget = self.widget
        DatabaseData().views.append(self)

    def rerender(self, loop):
        loop.widget = self.widget


class MenuView(ListView):
    OPTIONS = collections.OrderedDict([
        (_("Add Record"), "add"),
        (_("Search Records"), "search"),
        (_("Browse All Records"), "browse-all"),
        (_("Browse Tables"), "browse"),
        (_("Write to Disk"), "sync-database"),
        (_("About newfol"), "about")
    ])

    def callback(self, type_, obj):
        if not self.is_callback_type(type_, "select"):
            return
        text = self.listbox.get_focus()[0].original_widget.get_text()[0]
        command = self.OPTIONS[text]
        if command == "browse-all":
            ccview = CompleteContentsListView()
            ccview.set_render_type(type_)
            self.session.render_view(ccview)
            return
        self.loop.unhandled_input(command)

    def _render_standard(self, loop):
        self.loop = loop
        content = urwid.SimpleListWalker([
            self.text(w, "item", "focused-item") for w in self.OPTIONS.keys()])
        return self._render_listview(_("Main Menu"), self.OPTIONS.keys(),
                                     content)


def render_previous_view(loop):
    if len(DatabaseData().views) > 1:
        DatabaseData().views.pop()
        DatabaseData().views[-1].rerender(loop)


def start_curses(db):
    palette = {
        'bg': ('black', 'yellow', '', 'black', '#ffa'),
        'text': ('black', 'yellow', '', '#860', '#ffa'),
        'focused-deleted': ('dark green', 'light red', '', '#006', '#f00'),
        'deleted': ('light red', 'dark green', '', '#f00', '#006'),
        'focused-item': ('yellow', 'black', '', '#ffa', '#860'),
        'item': ('text',),
        'description': ('text',),
        'editbox': ('focused-item',),
        'header': ('black', 'yellow', '', 'black', '#fd0'),
        'top': ('black', 'light blue', '', 'black', '#0df')
    }
    palette.update(db.schema().palette())
    urwid_palette = []
    for k, v in palette.items():
        data = [k]
        data.extend(v)
        # This allows only one level of reference for palette items.
        if len(v) == 1:
            urwid_palette.append(data)
        elif len(v) == 2:
            data[1:1] = [''] * 3
            urwid_palette.insert(0, data)
        else:
            urwid_palette.insert(0, data)
    DatabaseData().keys = KeyboardShortcuts(db.schema().keys())
    sess = None

    def key_handler(inp):
        def quit():
            raise urwid.ExitMainLoop()
        cmds = {
            'quit': quit,
            'previous': lambda: sess.render_previous_view(),
            'menu': lambda: sess.render_view(MenuView()),
            'add': lambda: sess.render_view(DisplayRecordView()),
            'search': lambda: sess.render_view(SearchRecordView()),
            'next': lambda: dbd.views[-1].callback(val, None),
            'next-secondary': lambda: dbd.views[-1].callback(val, None),
            'next-tertiary': lambda: dbd.views[-1].callback(val, None),
            'invert-selection': lambda: dbd.views[-1].callback(val, None),
            'browse-all': lambda: sess.render_view(CompleteContentsListView()),
            'browse': lambda: sess.render_view(TableListView()),
            'about': lambda: sess.render_view(AboutView()),
            'sync-database': lambda: dbd._database.store(),
        }
        dbd = DatabaseData()
        val = dbd.keys.get(inp, inp)
        cmd = cmds.get(val)
        if cmd:
            cmd()

    intro = IntroView()
    loop = urwid.MainLoop(None, urwid_palette, unhandled_input=key_handler)
    sess = Session(loop)
    loop.screen.set_terminal_properties(colors=256)
    sess.render_view(intro)
    loop.run()


def do_import(l):
    for i in l:
        names = i.split('.')
        globals()[names[0]] = importlib.import_module(names[0])
        cur = globals()[names[0]]
        for j in range(1, len(names)):
            if not hasattr(cur, names[j]):
                setattr(cur, names[j],
                        importlib.import_module(".".join(names[0:j+1])))
            cur = getattr(cur, names[j])


def import_into(db, dtbname, dbtype, minfields=0, strict=False,
                identity=False):
    vault = filemanip.FileStorage(dbtype, sys.stdin)
    msg = None
    mapobj = None
    schema = db.schema()
    if dtbname in schema.mapping() and not identity:
        msg = _("Using specified mapping for db %s")
        mapobj = schema.mapping()[dtbname]
    else:
        if identity:
            msg = _("Using identity mapping for db %s as requested")
        else:
            msg = _("No mapping specified for db %s; using identity mapping")
        mapobj = newfol.database.Mapping(dtbname, schema.nfields())
        for i in range(schema.nfields()):
            mapobj.set_field_mapping(i, i, i, "")
    print(msg.format(dtbname))
    db.records()[:] = db.records(lambda x: x.table != dtbname)
    recno = 0
    for rec in vault.load():
        recno += 1
        if len(rec.fields) == 0:
            continue
        if rec.fields[0].startswith("#"):
            continue
        if len(rec.fields) < minfields:
            continue
        if minfields > 0 and strict:
            if len(rec.fields) != minfields:
                msg = _("Record %d has %d fields, not %d")
                raise newfol.exception.DataError(msg % (recno, len(rec.fields),
                                                        minfields))
        try:
            newrec = filemanip.Record(mapobj.map_fields_forward(rec.fields))
            newrec.table = dtbname
            db.records().append(newrec)
        except TypeError:
            msg = _("Data has too many fields or corrupt mapping")
            raise newfol.exception.DataError(msg)


def export_from(db, table, dbtype):
    vault = filemanip.FileStorage(dbtype, sys.stdout)

    def f(x):
        return True

    def g(x):
        return x.table == table
    if table is None and dbtype == "csv":
        raise newfol.exception.NewfolError(_("A table is required for csv"))
    func = f if table is None else g
    vault.store(db.records(func))


def parse_args(args):
    confhome = os.environ.get('XDG_CONFIG_HOME',
                              os.path.expanduser('~/.config'))
    conf = '%s/newfol/defaults' % confhome
    parser = argparse.ArgumentParser(description=_("store and manipulate fol"))
    parser.add_argument("--import", dest="cmd", action="store_const",
                        const="import", help=_("import a table"))
    parser.add_argument("--test", dest="cmd", action="store_const",
                        const="test", help=_("test this program"))
    parser.add_argument("--version", dest="cmd", action="store_const",
                        const="version",
                        help=_("print newfol and database versions"))
    parser.add_argument("--validate", dest="cmd", action="store_const",
                        const="validate", help=_("validate the database"))
    parser.add_argument("--repair", dest="cmd", action="store_const",
                        const="repair",
                        help=_("repair the database automatically"))
    parser.add_argument("--export", "--extract", dest="cmd",
                        action="store_const", const="export",
                        help=_("export a table"))
    parser.add_argument("--dbname", dest="dbname", action="store",
                        help=_("table name (obsolescent)"))
    parser.add_argument("--table", dest="table", action="store",
                        help=_("table name"))
    parser.add_argument("--upgrade", dest="cmd", action="store_const",
                        const="upgrade", help=_("upgrade data files"))
    parser.add_argument("--strict", dest="strict", action="store_true",
                        help=_("enable strict checking"))
    parser.add_argument("--identity", dest="identity", action="store_true",
                        help=_("use identity mapping when importing"))
    parser.add_argument("--curses", dest="cmd", action="store_const",
                        const="curses", help=_("start the curses interface"))
    parser.add_argument("--homedir", dest="homedir", action="store",
                        default=os.path.expanduser("~/.newfol"),
                        help=_("specify the location of database files"))
    parser.add_argument("--minfields", dest="minfields", action="store",
                        type=int, default=0,
                        help=_("minimum number of fields per record"))
    parser.add_argument("--txntype", dest="txntype", action="append",
                        default=None, help=_("type of transactional helper"))
    parser.add_argument("--from-txntype", dest="from_txntype", action="append",
                        default=None,
                        help=_("current type of transactional helper"))
    parser.add_argument("--dbtype", dest="dbtype", action="store",
                        default="csv", help="type of data for import/export")
    parser.add_argument('--no-config', dest='config', action='store_const',
                        const=[], default=[conf],
                        help=_('do not load the configuration file %s') % conf)
    parser.add_argument("--upgrade-version", dest="upgrade_version",
                        action="store", default=DatabaseVersion.preferred(),
                        type=int,
                        help=_("version of database to upgrade to (advanced)"))
    argobj = parser.parse_args(args)
    if argobj.cmd is None:
        argobj.cmd = "curses"
    return argobj


def ensure_db_dir(path):
    if not os.path.exists(path):
        os.mkdir(path, 0o700)
        with open(path + "/version", "w") as fp:
            fp.write("%d\n" % int(DatabaseVersion.preferred()))


def dump_database_information(txntypes, ver, label=None):
    stats = [
        "Database Version: %d (%#010x)" % (ver, ver),
        "Record Version: %d" % ver.record_version(),
        "Serialization Version: %d" % ver.serialization_version(),
        "Serialization Format: %s" % ver.serialization(),
        "Compression Version: %d" % ver.compression_version(),
        "Compression Format: %s" % ver.compression(),
        "Transaction Types: " + ", ".join(txntypes),
    ]
    print()
    for s in stats:
        print("%s%s" % (label or '', s))


def print_version(db):
    print("newfol " + __version__)
    if db is None:
        print("No database loaded.")
    else:
        txntypes = db.schema().transaction_types()
        ver = db.version()
        dump_database_information(txntypes, ver)

    default_txns = filemanip.FileStorage.DEFAULT_TRANSACTION_TYPES
    dump_database_information(default_txns, DatabaseVersion.preferred(),
                              "Default ")
    print()
    print("Supported Serialization Types: " + ", ".
          join(DatabaseVersion.serialization_types()))
    print("Supported Compression Types: " + ", ".
          join([str(x) for x in DatabaseVersion.compression_types()]))
    print('Supported Transaction Types: ' + ', '.
          join(newfol.filemanip.FileStorage.transaction_types()))


def upgrade_database(db, version, txntype):
    def notify(text):
        sys.stderr.write("%s\n" % text)
    db.upgrade(txntype, notify, version)


def main(args):
    argobj = parse_args(args)
    if argobj.dbname is not None:
        sys.stderr.write("--dbname is obsolete; use --table instead\n")
    if argobj.table is None and argobj.dbname is not None:
        argobj.table = argobj.dbname
    ensure_db_dir(argobj.homedir)
    db = None
    try:
        txntype = argobj.txntype
        if argobj.from_txntype and argobj.cmd == 'upgrade':
            txntype = argobj.from_txntype
        db = newfol.database.Database.load(argobj.homedir, txntype,
                                           argobj.config)
        db.lock()
        dbd = DatabaseData()
        dbd._database = db
        do_import(db.schema().imports())
        # Check to see if the database needs upgrading.
        if argobj.cmd != "upgrade":
            db.records()
    except Exception as e:
        if argobj.cmd != "version":
            raise e
        db = None
    if argobj.cmd == "curses":
        start_curses(db)
    elif argobj.cmd == "import":
        import_into(db, argobj.table, argobj.dbtype, argobj.minfields,
                    argobj.strict, argobj.identity)
    elif argobj.cmd == "validate":
        db.validate(True)
    elif argobj.cmd == "repair":
        db.repair()
        db.validate(True)
    elif argobj.cmd == "test":
        pass
    elif argobj.cmd == "upgrade":
        upgrade_database(db, argobj.upgrade_version, argobj.txntype)
        return
    elif argobj.cmd == "export":
        export_from(db, argobj.table, argobj.dbtype)
    elif argobj.cmd == "version":
        print_version(db)
        return
    db.store()
    db.unlock()
