#!/usr/bin/e
nv python
#-*- coding:utf-8 -*-

from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import print_function

import __builtin__

from os import environ, path, isatty, makedirs
from contextlib im
port closing
import shelve
import pickle
from anydbm import error as DBError
from locale import getpreferredencoding
import re
from contextlib import contextmanager
from textwrap import TextWrapper, dedent
import sys


LANGUAGE = environ.get('JBO_LANGUAGE', 'en')
LANGUAGES = set(['id', 'ch', 'cy', 'da', 'de', 'et', 'en', 'es', 'eo', 'eu',
                 'ga', 'gl', 'ia', 'it', 'kw', 'la', 'lt', 'hu', 'mg', 'nl',
                 'pl', 'pt', 'ro', 'sl', 'sk', 'so', 'sr', 'fi', 'sv', 'test',
                 'vi', 'tr', 'wa', 'br', 'ca', 'fr', 'hr', 'art-loglan',
                 'jbo', 'no', 'sq', 'tpi', 'cs', 'el', 'be', 'bg', 'ru', 'uk',
                 'he', 'ne', 'sa', 'hi', 'gu', 'ta', 'ka', 'am', 'ar', 'fa',
                 'zh', 'ja', 'ko', 'tlh'])
LANGUAGE_NAMES = {'da': 'danish', 'nl': 'dutch', 'en': 'english',
                  'fi': 'finnish', 'fr': 'french', 'de': 'german',
                  'hu': 'hungarian', 'it': 'italian', 'no': 'norwegian',
                  'pt': 'portuguese', 'ro': 'romanian', 'ru': 'russian',
                  'es': 'spanish', 'sv': 'swedish', 'tr': 'turkish'}

try:
    if LANGUAGE not in LANGUAGE_NAMES:
        raise ImportError
    import Stemmer
except ImportError:
    try:
        if LANGUAGE != 'en':
            raise ImportError
        from stemming.porter2 import stem
    except ImportError:
        def stem(word):
            return word
else:
    stemmer = Stemmer.Stemmer(LANGUAGE_NAMES[LANGUAGE])
    stem = stemmer.stemWord


VERSION = '0.1dev'
DEBUG = 'JBO_DEBUG' in environ  # Show full tracebacks for errors
COLUMNS = int(environ.get('COLUMNS', 80))
DATADIR = environ.get('JBO_DATADIR', path.expanduser('~/.jbo'))
ESCAPES = isatty(1) if environ.get('JBO_ESCAPES', 'tty') == 'tty' \
                    else environ.get('JBO_ESCAPES') == 'always'
COMMANDS = {}


def expose(name=None, options=False):
    """Expose a function as a command."""
    def decorator(handler):
        command = name
        if command is None:
            command = handler.__name__
        COMMANDS[command] = handler
        return handler
    return decorator


def dbopen(db, flag='r', writeback=False, language=LANGUAGE):
    """Self-closing context-manager for
    opening a database from the right place.

    """
    return closing(shelve.open(path.join(DATADIR, language, db),
                               flag, pickle.HIGHEST_PROTOCOL, writeback))


def dbopenbuild(db, flag='r'):
    """Same as :func:`dbopen` but builds the database if needed."""
    try:
        return dbopen(db, flag)
    except DBError as error:
        if isinstance(error, IOError) and error.errno == 32:
            raise
        build_database()
        return dbopen(db, flag)


def dbfilter(terms=None):
    """Generator yielding entry names matching terms, sorted by score."""
    if not terms:
        raise StopIteration

    with dbopenbuild('tokens') as tokens:
        newterms = []
        for term in terms:
            for term in term.split():
                newterms.append(b(stem(term).lower()))
        terms = newterms

        # Intersect entries for all search terms.
        entry_scores = dict((word, 0) for word in
            set.intersection(*(set(tokens.get(term, {})) for term in terms)))

        # Combine score for all entries.
        for term in terms:
            for word, score in tokens.get(term, {}).iteritems():
                if word in entry_scores:
                    entry_scores[word] += score

    # We sort by the item tuples reversed so that the word is taken
    # into account when the score is equal.
    def reversed_tuple(iterable):
        return tuple(reversed(iterable))

    for word, score in sorted(entry_scores.iteritems(),
                              key=reversed_tuple, reverse=True):
        yield word


def u(object):
    """Adapt and coerce object to unicode, assuming UTF-8."""
    if isinstance(object, unicode):
        return object
    return str(object).decode('utf-8')


def b(object):
    """Adapt and coerce object to a bytestring,
    encoded as mandated by the locale if possible, otherwise UTF-8.

    """
    try:
        return u(object).encode(getpreferredencoding())
    except UnicodeError:
        return u(object).encode('utf-8')


def print(*args, **kwargs):
    """Enforce the encoding of printed output via :func:`b`;
    removes the need for some boilerplate when printing to a non-tty.

    """
    __builtin__.print(*map(b, args), **kwargs)


def ansi_encode(text, start, end=0, condition=ESCAPES):
    """Encode text with ANSI control codes."""
    if condition:
        return '\x1b[{0}m{1}\x1b[{2}m'.format(start, text, end)
    return text


def bold(text, condition=ESCAPES):
    """Make text bold if printed to a terminal."""
    return ansi_encode(text, 1, 22, condition)


def underline(text, condition=ESCAPES):
    """Underline text if printed to a terminal."""
    return ansi_encode(text, 4, 24, condition)


def latex_to_text(latex):
    """Turns LaTeX used in jbovlaste into a more text-friendly format,
    returns a tuple of an ANSI encoded version and a raw version.

    """
    def sub(m):
        x = m.group(1)
        for digit, sub in zip('0123456789+-', '₀₁₂₃₄₅₆₇₈₉₊₋'):
            x = x.replace(digit, sub)
        return x

    def sup(m):
        x = m.group(1)
        for digit, sup in zip('0123456789+-', '⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻'):
            x = x.replace(digit, sup)
        return x

    def math(m):
        t = []
        for x in m.group(1).split('='):
            x = x.replace('{', '').replace('}', '')
            x = x.replace('*', '×')
            x = x.replace('/', '÷')
            x = re.sub(r'_([\d+-]+)', sub, x)
            x = re.sub(r'\^([\d+-]+)', sup, x)
            t.append(x)
        return '='.join(t)

    def typography(m):
        if m.group(1) == 'emph':
            return underline(m.group(2), True)
        elif m.group(1) == 'textbf':
            return bold(m.group(2), True)

    latex = re.sub(r'\s{3,}', '\n', latex)
    text_math = re.sub(r'\$(.+?)\$', math, latex)
    text_ansi = re.sub(r'\\(emph|textbf)\{(.+?)\}', typography, text_math)
    text_raw = re.sub(r'\\(?:emph|textbf)\{(.+?)\}', r'\1', text_math)

    return text_ansi, text_raw


def underline_references(text):
    return re.sub(r'\{(.+?)\}', lambda m: underline(m.group(1), True), text)


def compound_to_affixes(compound):
    """Split a compound word into affixes."""
    c = r'[bcdfgjklmnprstvxz]'
    v = r'[aeiou]'
    cc = r'''(?:bl|br|
                cf|ck|cl|cm|cn|cp|cr|ct|
                dj|dr|dz|fl|fr|gl|gr|
                jb|jd|jg|jm|jv|kl|kr|
                ml|mr|pl|pr|
                sf|sk|sl|sm|sn|sp|sr|st|
                tc|tr|ts|vl|vr|xl|xr|
                zb|zd|zg|zm|zv)'''
    vv = r'(?:ai|ei|oi|au)'
    rafsi3v = r"(?:{cc}{v}|{c}{vv}|{c}{v}'{v})".format(**locals())
    rafsi3 = r'(?:{rafsi3v}|{c}{v}{c})'.format(**locals())
    rafsi4 = r'(?:{c}{v}{c}{c}|{cc}{v}{c})'.format(**locals())
    rafsi5 = r'{rafsi4}{v}'.format(**locals())

    for i in xrange(1, len(compound)/3+1):
        reg = r'(?:({rafsi3})[nry]??|({rafsi4})y)'.format(**locals()) * i
        reg2 = r'^{reg}({rafsi3v}|{rafsi5})$$'.format(**locals())
        matches = re.findall(reg2, compound, re.VERBOSE)
        if matches:
            return tuple(r for r in matches[0] if r)

    return tuple()


def compound_to_metaphor(compound, affixes):
    try:
        return ' '.join(affixes[b(affix)].word
                        for affix in compound_to_affixes(compound))
    except KeyError:
        return ''


@contextmanager
def exit_on_eof():
    try:
        yield
    except EOFError:
        raise SystemExit


class Entry(object):
    """Data type representing an entry in jbovlaste."""

    def __init__(self, word, type=None):
        self.word, self.type = word, type
        self._definition = self.raw_definition = None
        self._notes = self.raw_not