#!/usr/bin/python3

import os
os.environ["DBALLE_BUILDING_DOCS"] = "true"  # noqa
import sys
import re
import inspect
from contextlib import contextmanager
import argparse
import logging
import dballe
import dballe.volnd

log = logging.getLogger("doc-dballe")

re_docparsep = re.compile(r"\n(?:\s*\n)+")
re_signature = re.compile(r"\S+\(.*\)")
re_linebreak = re.compile(r"\s*\n\s*")


class Fail(Exception):
    pass


def split_doc_summary(doc):
    res = re_docparsep.split(doc, maxsplit=1)
    if len(res) == 1:
        dstrip = doc.strip()
        if "\n" in dstrip:
            return None, doc
        else:
            return doc, None
    else:
        summary = re_linebreak.sub(" ", res[0])
        return summary, res[1]


class Generator:
    def __init__(self, file=sys.stdout):
        self.whitespace = True
        self.file = file

    def spacer(self):
        if self.whitespace:
            return
        print(file=self.file)
        self.whitespace = True

    def print(self, *args, **kw):
        self.whitespace = False
        kw["file"] = self.file
        print(*args, **kw)

    def print_indented(self, spaces, *args):
        "Print a string, indented by the given number of spaces"
        self.whitespace = False
        for s in args:
            for line in s.split("\n"):
                for i in range(1, spaces):
                    self.file.write(" ")
                self.file.write(line.replace("wreport.", "dballe."))
                self.file.write("\n")

    def text(self, text: str):
        self.print(text.lstrip())

    def para(self, text: str):
        if not self.whitespace:
            print(file=self.file)
        print(text.lstrip(), file=self.file)
        print(file=self.file)
        self.whitespace = True

    def title(self, line, char='-', over=False):
        self.spacer()
        if over:
            print(char * len(line), file=self.file)
        print(line, file=self.file)
        print(char * len(line), file=self.file)
        print(file=self.file)

    def document_class(self, cls, **kw):
        name = cls.__name__
        self.title("dballe.{}".format(name))
        self.print(inspect.getdoc(cls).replace("wreport.", "dballe."))
        if self.has_members(cls, **kw):
            self.title("Members", char="`")
            self.document_members(cls, **kw)
        self.spacer()

    def document(self, obj):
        if obj.__doc__ is None:
            return

        if inspect.ismodule(obj):
            self.print_indented(0, inspect.getdoc(obj))
            self.spacer()
        elif inspect.isroutine(obj) and inspect.isfunction(obj):
            # A Python function
            signature = inspect.signature(obj)
            self.print_indented(2, '``' + obj.__name__ + str(signature) + "``")
            self.print_indented(4, inspect.getdoc(obj))
        else:
            self.print_indented(2, obj.__name__)
            self.print_indented(4, inspect.getdoc(obj))

    def document_getsetter(self, cls, name, obj):
        doc = inspect.getdoc(obj)

        summary, rest = split_doc_summary(doc)
        if summary:
            if rest:
                self.print_indented(2, "{}.{}: {}".format(cls.__name__, name, summary))
                self.print_indented(4, rest)
            else:
                self.print_indented(2, "{}.{}".format(cls.__name__, name))
                self.print_indented(4, summary)
        else:
            self.print_indented(2, "{}.{}".format(cls.__name__, name))
            self.print_indented(4, doc)

    def document_cfunction(self, cls, name, obj):
        # Split signature and summary
        doc = inspect.getdoc(obj)

        signature, rest = split_doc_summary(doc)
        if signature and re_signature.search(signature):
            if signature.startswith(cls.__name__ + "."):
                signature = signature[len(cls.__name__) + 1:]
            self.print_indented(2, "{}.{}".format(cls.__name__, signature))
            if rest:
                self.print_indented(4, rest)
        else:
            self.print_indented(2, "{}.{}(…)".format(cls.__name__, name))
            self.print_indented(4, doc)

    def document_function(self, cls, name, obj):
        argspec = inspect.formatargspec(*inspect.getargspec(obj))
        self.print_indented(2, "{}.{}{}".format(cls.__name__, name, argspec))
        self.print_indented(4, inspect.getdoc(obj))

    def has_members(self, cls, include=None, exclude=None):
        for name, m in inspect.getmembers(cls):
            if name[0] == '_':
                continue
            if include is not None and name not in include:
                continue
            if exclude is not None and name in exclude:
                continue
            if inspect.isroutine(m):
                return True
            elif inspect.isfunction(m):
                return True
            elif inspect.isclass(m) or inspect.ismodule(m):
                pass
            elif m.__class__.__name__ == "_Feature":
                pass
            else:
                return True

    def document_members(self, cls, include=None, exclude=None):
        # List and classify members
        member_vars = []
        member_cfuncs = []
        member_funcs = []

        for name, m in inspect.getmembers(cls):
            if name[0] == '_':
                continue
            if include is not None and name not in include:
                continue
            if exclude is not None and name in exclude:
                continue
            if inspect.isroutine(m):
                member_cfuncs.append((name, m))
            elif inspect.isfunction(m):
                member_funcs.append((name, m))
            elif inspect.isclass(m) or inspect.ismodule(m):
                pass
            elif m.__class__.__name__ == "_Feature":
                pass
            else:
                member_vars.append((name, m))

        member_vars.sort()
        member_cfuncs.sort()
        member_funcs.sort()

        # Hyperlink destinations
        for name, m in member_vars:
            self.print(".. _{}.{}:".format(cls.__name__, name))
        for name, m in member_cfuncs:
            self.print(".. _{}.{}():".format(cls.__name__, name))
        for name, m in member_funcs:
            self.print(".. _{}.{}():".format(cls.__name__, name))

        # Document vars and get/setters
        self.spacer()
        for name, m in member_vars:
            self.document_getsetter(cls, name, m)

        # Document C functions
        for name, m in member_cfuncs:
            self.document_cfunction(cls, name, m)

        # Document functions
        for name, m in member_funcs:
            self.document_function(cls, name, m)

        self.spacer()


def print_docs(file):
    gen = Generator(file=file)

    gen.title("README for DB-All.e Python bindings", char="=", over=True)
    gen.para("""
The DB-All.e Python bindings provide 2 levels of access to a DB-All.e database:
a complete API similar to the Fortran and C++ API, and a high-level API called
volnd that allows to automatically export matrices of data out of the database.

.. contents::
""")

    gen.title("The DB-All.e API", char="=")

    gen.para("The 'dballe' module has a few global methods:")
    gen.document_members(dballe)

    gen.para("and several classes, documented in their own sections.")

    gen.document_class(dballe.Var)
    gen.document_class(dballe.Varinfo)
    gen.document_class(dballe.Vartable)
    gen.document_class(dballe.Level)
    gen.document_class(dballe.Trange)
    gen.document_class(dballe.Station)
    gen.document_class(dballe.DBStation)
    gen.document_class(dballe.Data)
    gen.document_class(dballe.BinaryMessage)
    gen.document_class(dballe.File)
    gen.document_class(dballe.Message)
    gen.document_class(dballe.Importer)
    gen.document_class(dballe.ImporterFile)
    gen.document_class(dballe.Exporter)
    gen.document_class(dballe.DB, exclude=("attr_insert", "attr_remove", "query_attrs", "load", "export_to_file"))
    gen.document_class(dballe.Transaction, exclude=("load", "export_to_file"))
    gen.document_class(dballe.CursorStation)
    gen.document_class(dballe.CursorStationData, exclude=("query_attrs"))
    gen.document_class(dballe.CursorData, exclude=("query_attrs"))
    gen.document_class(dballe.CursorStationDB)
    gen.document_class(dballe.CursorStationDataDB, exclude=("query_attrs"))
    gen.document_class(dballe.CursorDataDB, exclude=("query_attrs"))
    gen.document_class(dballe.CursorSummaryDB)
    gen.document_class(dballe.CursorSummarySummary)
    gen.document_class(dballe.CursorSummaryDBSummary)
    gen.document_class(dballe.CursorMessage)
    gen.document_class(dballe.Explorer)
    gen.document_class(dballe.DBExplorer)
    gen.document_class(dballe.ExplorerUpdate)
    gen.document_class(dballe.DBExplorerUpdate)

    gen.title("The volnd API", char="=")

    gen.document(dballe.volnd)

    gen.para("This is the list of dimensions supported by dballe.volnd:")

    gen.document(dballe.volnd.AnaIndex)
    gen.document(dballe.volnd.NetworkIndex)
    gen.document(dballe.volnd.LevelIndex)
    gen.document(dballe.volnd.TimeRangeIndex)
    gen.document(dballe.volnd.DateTimeIndex)
    gen.document(dballe.volnd.IntervalIndex)

    gen.para("The data object used by ``AnaIndex`` is:")

    gen.document(dballe.volnd.AnaIndexEntry)

    gen.para("The extraction is done using the dballe.volnd.read function:")

    gen.document(dballe.volnd.read)

    gen.para("""
The result of dballe.volnd.read is a dict mapping output variable names to a
dballe.volnd.Data object with the results.  All the Data objects share their
indexes unless the *xxx*-Index definitions have been created with
``shared=False``.

This is the dballe.volnd.Data class documentation:
""")

    gen.document(dballe.volnd.Data)

    gen.para("The methods of dballe.volnd.Data are:")

    gen.document_members(dballe.volnd.Data)


@contextmanager
def outfile(args):
    if args.outfile:
        with open(args.outfile, "wt", encoding="utf8") as fd:
            try:
                yield fd
            except Exception as e:
                if os.path.exists(args.outfile):
                    os.unlink(args.outfile)
                raise e
    else:
        yield sys.stdout


def main():
    parser = argparse.ArgumentParser(
            description="build documentation for DB-All.e Python bindings")
    parser.add_argument("--verbose", "-v", action="store_true", help="verbose output")
    parser.add_argument("--debug", action="store_true", help="debug output")
    parser.add_argument("-o", "--outfile", action="store", help="output file (default: stdout)")
    parser.add_argument("pathname", nargs="?", help="source file with the lookup table description")
    args = parser.parse_args()

    log_format = "%(asctime)-15s %(levelname)s %(message)s"
    level = logging.WARN
    if args.debug:
        level = logging.DEBUG
    elif args.verbose:
        level = logging.INFO
    logging.basicConfig(level=level, stream=sys.stderr, format=log_format)

    with outfile(args) as out:
        print_docs(out)


if __name__ == "__main__":
    try:
        main()
    except Fail as e:
        log.error("%s", e)
        sys.exit(1)
