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

from collections import OrderedDict
import sys
import logging

logger = logging.getLogger(__name__)

if sys.version_info >= (3, 0):
    ustr = str
else:
    ustr = unicode


STANDARD_TYPES = set([
    'article',
    'book',
    'booklet',
    'conference',
    'inbook',
    'incollection',
    'inproceedings',
    'manual',
    'mastersthesis',
    'misc',
    'phdthesis',
    'proceedings',
    'techreport',
    'unpublished'])

COMMON_STRINGS = OrderedDict([
    ('jan', 'January'),
    ('feb', 'February'),
    ('mar', 'March'),
    ('apr', 'April'),
    ('may', 'May'),
    ('jun', 'June'),
    ('jul', 'July'),
    ('aug', 'August'),
    ('sep', 'September'),
    ('oct', 'October'),
    ('nov', 'November'),
    ('dec', 'December'),
    ])


class UndefinedString(KeyError):
    pass


class BibDatabase(object):
    """
    Bibliographic database object that follows the data structure of a BibTeX file.
    """

    def __init__(self):
        #: List of BibTeX entries, for example `@book{...}`, `@article{...}`, etc. Each entry is a simple dict with
        #: BibTeX field-value pairs, for example `'author': 'Bird, R.B. and Armstrong, R.C. and Hassager, O.'` Each
        #: entry will always have the following dict keys (in addition to other BibTeX fields):
        #:
        #: * `ID` (BibTeX key)
        #: * `ENTRYTYPE` (entry type in lowercase, e.g. `book`, `article` etc.)
        self.entries = []
        self._entries_dict = {}
        #: List of BibTeX comment (`@comment{...}`) blocks.
        self.comments = []
        #: OrderedDict of BibTeX string definitions (`@string{...}`). In order of definition.
        self.strings = OrderedDict()  # Not sure if order is import, keep order just in case
        #: List of BibTeX preamble (`@preamble{...}`) blocks.
        self.preambles = []

        #: List of fields that should not be updated when resolving crossrefs
        self._not_updated_by_crossref = ['_FROM_CROSSREF']

    def load_common_strings(self):
        self.strings.update(COMMON_STRINGS)

    def get_entry_list(self):
        """Get a list of bibtex entries.

        :returns: BibTeX entries
        :rtype: list
        .. deprecated:: 0.5.6
           Use :attr:`entries` instead.
        """
        return self.entries

    @staticmethod
    def entry_sort_key(entry, fields):
        result = []
        for field in fields:
            result.append(ustr(entry.get(field, '')).lower())  # Sorting always as string
        return tuple(result)

    def _make_entries_dict(self):
        for entry in self.entries:
            self._entries_dict[entry['ID']] = entry

    def get_entry_dict(self):
        """Return a dictionary of BibTeX entries.
        The dict key is the BibTeX entry key
        """
        # If the hash has never been made, make it
        if not self._entries_dict:
            self._make_entries_dict()
        return self._entries_dict

    entries_dict = property(get_entry_dict)

    def expand_string(self, name):
        try:
            return BibDataStringExpression.expand_if_expression(
                self.strings[name])
        except KeyError:
            raise(UndefinedString(name))

    def _add_missing_from_crossref_entry(self, entry, dependencies=set()):
        if entry['ID'] in self._crossref_updated:
            return

        if entry['_crossref'] not in self.entries_dict:
            logger.error("Crossref reference %s for %s is missing.",
                         entry['_crossref'],
                         entry['ID'])
            return

        if entry['_crossref'] in dependencies:
            logger.error("Circular crossref dependency: %s->%s->%s.",
                         "->".join(dependencies),
                         entry['ID'],
                         entry['_crossref'])
            return

        crossref_entry = self.entries_dict[entry['_crossref']]
        if '_crossref' in crossref_entry:
            dependencies.add(entry['ID'])
            self._add_missing_from_crossref_entry(crossref_entry, dependencies)
            dependencies.remove(entry['ID'])

        from_crossref = {bibfield: bibvalue
                         for (bibfield, bibvalue) in crossref_entry.items()
                         if bibfield not in entry.keys() and
                            bibfield not in self._not_updated_by_crossref}

        entry.update(from_crossref)

        self._crossref_updated.append(entry['ID'])
        entry['_FROM_CROSSREF'] = sorted(from_crossref.keys())
        del entry['_crossref']

    def add_missing_from_crossref(self):
        """Resolve crossrefs and update entries accordingly.
        """
        self._crossref_updated = []
        for entry in self.entries:
            if "_crossref" in entry:
                self._add_missing_from_crossref_entry(entry)


class BibDataString(object):
    """
    Represents a bibtex string.

    This object enables maintaining string expressions as list of strings
    and BibDataString. Can be interpolated from Bibdatabase.
    """

    def __init__(self, bibdatabase, name):
        self._bibdatabase = bibdatabase
        self.name = name.lower()

    def __eq__(self, other):
        return isinstance(other, BibDataString) and self.name == other.name

    def __repr__(self):
        return "BibDataString({})".format(self.name.__repr__())

    def get_value(self):
        """
        Query value from string name.

        :returns: string
        """
        return self._bibdatabase.expand_string(self.name)

    def get_dependencies(self, known_dependencies=set()):
        """Recursively tracks strings on which the expression depends.

        :param kown_dependencies: dependencies to ignore
        """
        raise NotImplementedError

    @staticmethod
    def expand_string(string_or_bibdatastring):
        """
        Eventually replaces a bibdatastring by its value.

        :param string_or_bibdatastring: the parsed token
        :type string_expr: string or BibDataString
        :returns: string
        """
        if isinstance(string_or_bibdatastring, BibDataString):
            return string_or_bibdatastring.get_value()
        else:
            return string_or_bibdatastring


class BibDataStringExpression(object):
    """
    Represents a bibtex string expression.

    String expressions are sequences of regular strings and bibtex strings.
    This object enables maintaining string expressions as list of strings.
    The expression are represented as lists of regular strings and
    BibDataStrings. They can be interpolated from Bibdatabase.

    BibDataStringExpression(e)

    :param e: list of strings and BibDataStrings
    """

    def __init__(self, expression):
        self.expr = expression

    def __eq__(self, other):
        return isinstance(other, BibDataStringExpression) and self.expr == other.expr

    def __repr__(self):
        return "BibDataStringExpression({})".format(self.expr.__repr__())

    def get_value(self):
        """
        Replaces bibdatastrings by their values in the expression.

        :returns: string
        """
        return ''.join([BibDataString.expand_string(s) for s in self.expr])

    def apply_on_strings(self, fun):
        """
        Maps a function on strings in expression, keeping unchanged
        BibDataStrings.

        :param fun: function from strings to strings
        """
        self.expr = [s if isinstance(s, BibDataString) else fun(s)
                     for s in self.expr]

    @staticmethod
    def expand_if_expression(string_or_expression):
        """
        Eventually replaces a BibDataStringExpression by its value.

        :param string_or_expression: the object to expand
        :type string_expr: string or BibDataStringExpression
        :returns: string
        """
        if isinstance(string_or_expression, BibDataStringExpression):
            return string_or_expression.get_value()
        else:
            return string_or_expression

    @staticmethod
    def expression_if_needed(tokens):
        """Build expression only if tokens are not a regular value.
        """
        if len(tokens) == 1 and not isinstance(tokens[0], BibDataString):
            return tokens[0]
        else:
            return BibDataStringExpression(tokens)


def as_text(text_string_or_expression):
    if isinstance(text_string_or_expression,
                  (BibDataString, BibDataStringExpression)):
        return text_string_or_expression.get_value()
    else:
        return ustr(text_string_or_expression)
