import sys
assert sys.version_info[0] == 3, "This program requires Python 3"

import re
from doctest import testfile
from io import StringIO

import bitarray.util


BASE_URL = "https://github.com/ilanschnell/bitarray"

NEW_IN = {
    'bitarray':               '2.3: optional `buffer` argument',
    'bitarray.bytereverse':   '2.2.5: optional start and stop arguments',
    'bitarray.clear':         '1.4',
    'bitarray.count':        ['1.1.0: optional start and stop arguments',
                              '2.3.7: optional step argument'],
    'bitarray.find':          '2.1',
    'bitarray.frombytes':     '2.5.0: allow bytes-like argument',
    'bitarray.invert':        '1.5.3: optional index argument',
    'bitarray.pack':          '2.5.0: allow bytes-like argument',
    'decodetree':             '1.6',
    'frozenbitarray':         '1.1',
    'get_default_endian':     '1.3',
    'util.any_and':           '2.7',
    'util.ba2base':           '1.9',
    'util.base2ba':           '1.9',
    'util.count_n':           '2.3.6: optional value argument',
    'util.deserialize':      ['1.8',
                              '2.5.0: allow bytes-like argument'],
    'util.intervals':         '2.7',
    'util.make_endian':       '1.3',
    'util.parity':            '1.9',
    'util.pprint':            '1.8',
    'util.rindex':            '2.3.0: optional start and stop arguments',
    'util.serialize':         '1.8',
    'util.urandom':           '1.7',
    'util.sc_encode':         '2.7',
    'util.sc_decode':         '2.7',
    'util.vl_decode':         '2.2',
    'util.vl_encode':         '2.2',
    'util.canonical_huffman': '2.5',
    'util.canonical_decode':  '2.5',
}

DOCS = {
    'chc': ('Canonical Huffman Coding', 'canonical.rst'),
    'rep': ('Bitarray representations', 'represent.rst'),
    'sc':  ('Compression of sparse bitarrays', 'sparse_compression.rst'),
    'vlf': ('Variable length bitarray format', 'variable_length.rst'),
}

DOC_LINKS = {
    'util.canonical_huffman':  'chc',
    'util.canonical_decode':   'chc',
    'util.ba2base':            'rep',
    'util.base2ba':            'rep',
    'util.deserialize':        'rep',
    'util.serialize':          'rep',
    'util.sc_decode':          'sc',
    'util.sc_encode':          'sc',
    'util.vl_decode':          'vlf',
    'util.vl_encode':          'vlf',
}

NOTES = {
    'bitarray.pack': """\
   This method, as well as the ``.unpack()`` method, are meant for efficient
   transfer of data between bitarray objects to other Python objects (for
   example NumPy's ndarray object) which have a different memory view.""",

    'bitarray.tolist': """\
   Note that the list object being created will require 32 or 64 times more
   memory (depending on the machine architecture) than the bitarray object,
   which may cause a memory error if the bitarray is very large.""",

    'util.count_xor': "   This is also known as the Hamming distance."
}

GETSET = {
    'bitarray.nbytes':     'int',
    'bitarray.padbits':    'int',
    'bitarray.readonly':   'bool',
}

_NAMES = set()

sig_pat = re.compile(r"""
(                # group 1
  (\w+)          # function name, group 2
  \([^()]*\)     # (...)
)
(                # optional group 3
  \s->\s(.+)     # return type, group 4
)?
""", re.VERBOSE)

def get_doc(name):
    parts = name.split('.')
    obj = bitarray
    while parts:
        obj = getattr(obj, parts.pop(0))

    lines = obj.__doc__.splitlines()

    if len(lines) == 1:
        sig = '``%s`` -> %s' % (obj.__name__, GETSET[name])
        return sig, lines

    m = sig_pat.match(lines[0])
    if m is None:
        raise Exception("signature invalid: %r" % lines[0])
    sig = '``%s``' %  m.group(1)
    assert m.group(2) == obj.__name__
    if m.group(4):
        sig += ' -> %s' % m.group(4)
    assert lines[1] == ''
    return sig, lines[2:]


def write_doc(fo, name):
    _NAMES.add(name)
    sig, lines = get_doc(name)
    fo.write(sig + '\n')
    for line in lines:
        out = line.rstrip()
        fo.write("   %s\n" % out.replace('`', '``') if out else "\n")

    note = NOTES.get(name)
    if note:
        fo.write("\n%s\n" % note)

    link = DOC_LINKS.get(name)
    if link:
        title, filename = DOCS[link]
        url = BASE_URL + '/blob/master/doc/' + filename
        fo.write("\n   See also: `%s <%s>`__\n" % (title, url))

    new_in = NEW_IN.get(name)
    if new_in:
        for line in new_in if isinstance(new_in, list) else [new_in]:
            fo.write("\n   New in version %s.\n" % line.replace('`', '``'))

    fo.write('\n\n')


def write_reference(fo):
    fo.write("""\
Reference
=========

bitarray version: %s -- `change log <%s>`__

In the following, ``item`` and ``value`` are usually a single bit -
an integer 0 or 1.


The bitarray object:
--------------------

""" % (bitarray.__version__, BASE_URL + "/blob/master/doc/changelog.rst"))
    write_doc(fo, 'bitarray')

    fo.write("bitarray methods:\n"
             "-----------------\n\n")
    for method in sorted(dir(bitarray.bitarray)):
        if method.startswith('_'):
            continue
        name = 'bitarray.%s' % method
        if name not in GETSET:
            write_doc(fo, name)

    fo.write("bitarray data descriptors:\n"
             "--------------------------\n\n"
             "Data descriptors were added in version 2.6.\n\n")
    for getset in sorted(dir(bitarray.bitarray)):
        name = 'bitarray.%s' % getset
        if name in GETSET:
            write_doc(fo, name)

    fo.write("Other objects:\n"
             "--------------\n\n")
    write_doc(fo, 'frozenbitarray')
    write_doc(fo, 'decodetree')

    fo.write("Functions defined in the `bitarray` module:\n"
             "-------------------------------------------\n\n")
    for func in sorted(['test', 'bits2bytes', 'get_default_endian']):
        write_doc(fo, func)

    fo.write("Functions defined in `bitarray.util` module:\n"
             "--------------------------------------------\n\n"
             "This sub-module was added in version 1.2.\n\n")
    for func in bitarray.util.__all__:
        write_doc(fo, 'util.%s' % func)

    for name in list(NEW_IN) + list(DOC_LINKS):
        assert name in _NAMES, name


def update_readme(path):
    ver_pat = re.compile(r'(bitarray.+?)\s(\d+\.\d+\.\d+)')

    with open(path, 'r') as fi:
        data = fi.read()

    with StringIO() as fo:
        for line in data.splitlines():
            if line == 'Reference':
                break
            line = ver_pat.sub(r'\1 ' + bitarray.__version__, line)
            fo.write("%s\n" % line.rstrip())

        write_reference(fo)
        new_data = fo.getvalue()

    if new_data == data:
        print("already up-to-date")
    else:
        with open(path, 'w') as f:
            f.write(new_data)


def write_changelog(fo):
    ver_pat = re.compile(r'(\d{4}-\d{2}-\d{2})\s+(\d+\.\d+\.\d+)')
    hash_pat = re.compile(r'#([0-9a-f]+)')
    link_pat = re.compile(r'\[(.+)\]\((.+)\)')

    def hash_replace(match):
        group1 = match.group(1)
        if len(group1) >= 7:
            if len(group1) != 8:
                print("Warning: commit hash length != 8, got", len(group1))
            url = "%s/commit/%s" % (BASE_URL, group1)
        else:
            url = "%s/issues/%d" % (BASE_URL, int(group1))
        return "`%s <%s>`__" % (match.group(0), url)

    fo.write("Change log\n"
             "==========\n\n")

    for line in open('./CHANGE_LOG'):
        line = line.rstrip()
        match = ver_pat.match(line)
        if match:
            line = match.expand(r'**\2** (\1):')
        elif line.startswith('-----'):
            line = ''
        elif line.startswith('  '):
            line = line[2:]
            line = line.replace('`', '``')
            line = hash_pat.sub(hash_replace, line)
            line = link_pat.sub(r"`\1 <\2>`__", line)
        fo.write(line + '\n')


def main():
    if len(sys.argv) > 1:
        sys.exit("no arguments expected")

    update_readme('./README.rst')
    with open('./doc/reference.rst', 'w') as fo:
        write_reference(fo)
    with open('./doc/changelog.rst', 'w') as fo:
        write_changelog(fo)

    testfile('./README.rst')
    testfile('./doc/buffer.rst')
    testfile('./doc/canonical.rst')
    testfile('./doc/represent.rst')
    testfile('./doc/sparse_compression.rst')
    testfile('./doc/variable_length.rst')


if __name__ == '__main__':
    main()
