#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# License: MIT (see LICENSE file provided)
# vim600: fdm=marker tabstop=4 shiftwidth=4 expandtab ai

# Description {{{
"""
**polib** allows you to manipulate, create, modify gettext files (pot, po
and mo files).  You can load existing files, iterate through it's entries,
add, modify entries, comments or metadata, etc... or create new po files
from scratch.

**polib** provides a simple and pythonic API, exporting only three
convenience functions (*pofile*, *mofile* and *detect_encoding*), and the
four core classes, *POFile*, *MOFile*, *POEntry* and *MOEntry* for creating
new files/entries.

**Basic example**:

>>> import polib
>>> # load an existing po file
>>> po = polib.pofile('tests/test_utf8.po')
>>> for entry in po:
...     # do something with entry...
...     pass
>>> # add an entry
>>> entry = polib.POEntry(msgid='Welcome', msgstr='Bienvenue')
>>> entry.occurrences = [('welcome.py', '12'), ('anotherfile.py', '34')]
>>> po.append(entry)
>>> # to save our modified po file:
>>> # po.save()
>>> # or you may want to compile the po file
>>> # po.save_as_mofile('tests/test_utf8.mo')
"""
# }}}

__author__    = 'David JEAN LOUIS <izimobil@gmail.com>'
__version__   = '0.3.1'


# dependencies {{{
try:
    import struct
    import textwrap
    import warnings
except ImportError, exc:
    raise ImportError('polib requires python 2.3 or later with the standard' \
        ' modules "struct", "textwrap" and "warnings" (details: %s)' % exc)
# }}}

__all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry',
           'detect_encoding', 'quote', 'unquote']

# shortcuts for performance improvement {{{
# yes, yes, this is quite ugly but *very* efficient
_dictget    = dict.get
_listappend = list.append
_listpop    = list.pop
_strjoin    = str.join
_strsplit   = str.split
_strstrip   = str.strip
_strreplace = str.replace
_textwrap   = textwrap.wrap
# }}}

default_encoding = 'utf-8'

def pofile(fpath, **kwargs):
    """
    Convenience function that parse the po/pot file *fpath* and return
    a POFile instance.

    **Keyword arguments**:
      - *fpath*: string, full or relative path to the po/pot file to parse
      - *wrapwidth*: integer, the wrap width, only useful when -w option was
        passed to xgettext (optional, default to 78)
      - *autodetect_encoding*: boolean, if set to False the function will
        not try to detect the po file encoding (optional, default to True)
      - *encoding*: string, an encoding, only relevant if autodetect_encoding
        is set to False

    **Example**:

    >>> import polib
    >>> po = polib.pofile('tests/test_utf8.po')
    >>> po #doctest: +ELLIPSIS
    <POFile instance at ...>
    >>> import os, tempfile
    >>> for fname in ['test_iso-8859-15.po', 'test_utf8.po']:
    ...     orig_po = polib.pofile('tests/'+fname)
    ...     tmpf = tempfile.NamedTemporaryFile().name
    ...     orig_po.save(tmpf)
    ...     try:
    ...         new_po = polib.pofile(tmpf)
    ...         for old, new in zip(orig_po, new_po):
    ...             if old.msgid != new.msgid:
    ...                 old.msgid
    ...                 new.msgid
    ...             if old.msgstr != new.msgstr:
    ...                 old.msgid
    ...                 new.msgid
    ...     finally:
    ...         os.unlink(tmpf)
    """
    # pofile {{{
    if _dictget(kwargs, 'autodetect_encoding', True) == True:
        enc = detect_encoding(fpath)
    else:
        enc = _dictget(kwargs, 'encoding', default_encoding)
    parser = _POFileParser(fpath)
    instance = parser.parse()
    instance.wrapwidth = _dictget(kwargs, 'wrapwidth', 78)
    instance.encoding  = enc
    return instance
    # }}}


def mofile(fpath, **kwargs):
    """
    Convenience function that parse the mo file *fpath* and return
    a MOFile instance.

    **Keyword arguments**:
      - *fpath*: string, full or relative path to the mo file to parse
      - *wrapwidth*: integer, the wrap width, only useful when -w option was
        passed to xgettext to generate the po file that was used to 
format
        the mo file (optional, default to 78)
      - *autodetect_encoding*: boolean, if set to False the function will
        not try to detect the po file encoding (optional, default to True)
      - *encoding*: string, an encoding, only relevant if autodetect_encoding
        is set to False

    **Example**:

    >>> import polib
    >>> mo = polib.mofile('tests/test_utf8.mo')
    >>> mo #doctest: +ELLIPSIS
    <MOFile instance at ...>

    >>> import os, tempfile
    >>> for fname in ['test_iso-8859-15.mo', 'test_utf8.mo']:
    ...     orig_mo = polib.mofile('tests/'+fname)
    ...     tmpf = tempfile.NamedTemporaryFile().name
    ...     orig_mo.save(tmpf)
    ...     try:
    ...         new_mo = polib.mofile(tmpf)
    ...         for old, new in zip(orig_mo, new_mo):
    ...             if old.msgid != new.msgid:
    ...                 old.msgstr
    ...                 new.msgstr
    ...     finally:
    ...         os.unlink(tmpf)
    """
    # mofile {{{
    if _dictget(kwargs, 'autodetect_encoding', True) == True:
        enc = detect_encoding(fpath)
    else:
        enc = _dictget(kwargs, 'encoding', default_encoding)
    parser = _MOFileParser(fpath)
    instance = parser.parse()
    instance.wrapwidth = _dictget(kwargs, 'wrapwidth', 78)
    instance.encoding = enc
    return instance
    # }}}


def detect_encoding(fpath):
    """
    Try to detect the encoding used by the file *fpath*. The function will
    return polib default *encoding* if it's unable to detect it.

    **Keyword argument**:
      - *fpath*: string, full or relative path to the mo file to parse.

    **Examples**:

    >>> print detect_encoding('tests/test_noencoding.po')
    utf-8
    >>> print detect_encoding('tests/test_utf8.po')
    UTF-8
    >>> print detect_encoding('tests/test_utf8.mo')
    UTF-8
    >>> print detect_encoding('tests/test_iso-8859-15.po')
    ISO_8859-15
    >>> print detect_encoding('tests/test_iso-8859-15.mo')
    ISO_8859-15
    """
    # detect_encoding {{{
    import re
    rx = re.compile(r'"?Content-Type:.+? charset=([\w_\-:\.]+)')
    f = open(fpath)
    for l in f:
        match = rx.search(l)
        if match:
            f.close()
            return _strstrip(match.group(1))
    f.close()
    return default_encoding
    # }}}


def quote(st):
    """
    Quote and return the given string *st*.

    **Examples**:

    >>> quote('\\t and \\n and \\r and " and \\\\')
    '\\\\t and \\\\n and \\\\r and \\\\" and \\\\\\\\'
    """
    # quote {{{
    st = _strreplace(st, '\\', r'\\')
    st = _strreplace(st, '\t', r'\t')
    st = _strreplace(st, '\r', r'\r')
    st = _strreplace(st, '\n', r'\n')
    st = _strreplace(st, '\"', r'\"')
    return st
    # }}}


def unquote(st):
    """
    Unquote and return the given string *st*.

    **Examples**:

    >>> unquote('\\\\t and \\\\n and \\\\r and \\\\" and \\\\\\\\')
    '\\t and \\n and \\r and " and \\\\'
    """
    # unquote {{{
    st = _strreplace(st, r'\"', '"')
    st = _strreplace(st, r'\n', '\n')
    st = _strreplace(st, r'\r', '\r')
    st = _strreplace(st, r'\t', '\t')
    st = _strreplace(st, r'\\', '\\')
    return st
    # }}}


class _BaseFile(list):
    """
    Common parent class for POFile and MOFile classes.
    This class must **not** be instanciated directly.
    """
    # class _BaseFile {{{


    def __init__(self, fpath=None, wrapwidth=78, encoding=default_encoding):
        """
        Constructor.

        **Keyword arguments**:
          - *fpath*: string, path to po or mo file
          - *wrapwidth*: integer, the wrap width, only useful when -w option
            was passed to xgettext to generate the po file that was used to
            format the mo file, default to 78 (optional).
        """
        list.__init__(self)
        # the opened file handle
        self.fpath = fpath
        # the width at which lines should be wrapped
        self.wrapwidth = wrapwidth
        # the file encoding
        self.encoding = encoding
        # header
        self.header = ''
        # both po and mo files have met