#!/usr/bin/python3
import textwrap
import argparse
import sys
import re
from itertools import zip_longest
import logging

log = logging.getLogger()


class TextTable:
    def __init__(self, colsizes, titles):
        self.colsizes = colsizes
        self.titles = titles
        self.wrappers = [textwrap.TextWrapper(width=x) for x in colsizes]

    def print_row(self, vals):
        wrapped_vals = [wrapper.wrap(val) for wrapper, val in zip(self.wrappers, vals)]
        for cols in zip_longest(*wrapped_vals, fillvalue=""):
            row = []
            for sz, val in zip(self.colsizes, cols):
                row.append(val.ljust(sz))
            print(" ".join(row))

    def print_head(self):
        self.print_row(self.titles)
        self.print_row(["-" * x for x in self.colsizes])


class MdTable:
    def __init__(self, colsizes, titles):
        self.colsizes = colsizes
        self.titles = titles

    def print_row(self, vals):
        row = []
        for sz, val in zip(self.colsizes, vals):
            row.append(val.ljust(sz))
        print("|", " | ".join(row), "|")

    def print_head(self):
        self.print_row(self.titles)
        self.print_row(["-" * x for x in self.colsizes])


def mkformat(unit, sz, dec):
    if "CHARACTER" in unit or "CODE TABLE" in unit:
        return "{} chars".format(sz)
    elif dec == 0:
        return "{} digits".format(sz)
    elif dec > 0:
        return ('#' * (sz - dec)) + '.' + ('#' * dec)
    else:
        return ('#' * sz) + ('0' * (-dec))


class Aliases:
    def read(self):
        # Skip the declaration section
        for line in sys.stdin:
            if line.startswith("%%"):
                break

        alias_line = re.compile(r"^(?P<name>[^,]+),\s*WR_VAR\((?P<f>\d+),\s*(?P<x>\d+),\s*(?P<y>\d+)\)")

        # Read the data
        data = []
        for line in sys.stdin:
            line = line.strip()
            if line == "%%":
                break
            mo = alias_line.match(line)
            if not mo:
                raise RuntimeError("Cannot parse {}".format(repr(line)))

            data.append((
                mo.group("name"),
                "B{x:02d}{y:03d}".format(x=int(mo.group("x")), y=int(mo.group("y")))
            ))

        data.sort()

        self.data = data

    def print_dox(self):
        print("""/**@defgroup dba_core_aliases Variable aliases
@ingroup tables

This table lists the aliases that can be used to refer to varcodes.

\\verbatim""")

        table = TextTable(
            colsizes=(13, 8),
            titles=("Alias", "Variable"))
        table.print_head()

        for alias, var in self.data:
            table.print_row((alias, var))

        print("""\\endverbatim
*/""")

    def print_md(self):
        print("""# Varcode aliases

This table lists the aliases that can be used to refer to varcodes.
""")

        table = MdTable(
            colsizes=(13, 8),
            titles=("Alias", "Variable"))
        table.print_head()

        for alias, var in self.data:
            table.print_row((alias, var))

    def print_rst(self):
        print(""".. _aliases:

Varcode aliases
===============

This table lists the aliases that can be used to refer to varcodes.

.. list-table::
   :widths: auto
   :header-rows: 1

   * - Alias
     - Vartable
""")
        for alias, var in self.data:
            print("   * - {}".format(alias))
            print("     - :ref:`{} <{}>`".format(var, var))


class Levels:
    def read(self):
        re_split = re.compile(r"\t+")
        # Read the data
        self.data = []
        for line in sys.stdin:
            line = line.strip()
            lev = re_split.split(line)
            if len(lev) < 3:
                lev += [""] * (3 - len(lev))
            elif len(lev) > 3:
                raise RuntimeError("line {} has too many fields".format(repr(lev)))
            self.data.append(lev)

    def print_dox(self):
        print("""/**@defgroup level_table Level type values
@ingroup tables

This table lists the possible values for leveltype1 or
leveltype2 and the interpretation of the corresponding numerical
value l1 or l2.  Leveltype values in the range 0-255 can
be used for defining either a single level (leveltype1) or a surface
delimiting a layer (leveltype1 and leveltype2) with any meaningful
combination of leveltypes; values of leveltype >255 have a special use
for encoding cloud values in SYNOP reports and they do not strictly
define physical surfaces.

The idea is borrowed from the GRIB edition 2 fixed surface
concept and the values for leveltype coincide with the GRIB standard
where possible.

\\verbatim
""")
        table = TextTable(
            colsizes=(11, 38, 27),
            titles=("Level Type", "Meaning", "Unit/contents of l1/l2")
        )
        table.print_head()

        for type, desc, li in self.data:
            table.print_row((type, desc, li))

        print("""\\endverbatim
*/""")

    def print_md(self):
        print("""# Level type values

This table lists the possible values for leveltype1 or
leveltype2 and the interpretation of the corresponding numerical
value l1 or l2.  Leveltype values in the range 0-255 can
be used for defining either a single level (leveltype1) or a surface
delimiting a layer (leveltype1 and leveltype2) with any meaningful
combination of leveltypes; values of leveltype >255 have a special use
for encoding cloud values in SYNOP reports and they do not strictly
define physical surfaces.

The idea is borrowed from the GRIB edition 2 fixed surface
concept and the values for leveltype coincide with the GRIB standard
where possible.
""")
        table = MdTable(
            colsizes=(11, 38, 27),
            titles=("Level Type", "Meaning", "Unit/contents of l1/l2")
        )
        table.print_head()

        for type, desc, li in self.data:
            table.print_row((type, desc, li))

    def print_rst(self):
        print(""".. _levels:

Level type values
=================

This table lists the possible values for ``leveltype1`` or
``leveltype2`` and the interpretation of the corresponding numerical
value ``l1`` or ``l2``.  Leveltype values in the range 0-255 can
be used for defining either a single level (``leveltype1``) or a surface
delimiting a layer (``leveltype1`` and ``leveltype2``) with any meaningful
combination of leveltypes; values of ``leveltype`` ``>255`` have a special use
for encoding cloud values in SYNOP reports and they do not strictly
define physical surfaces.

The idea is borrowed from the GRIB edition 2 fixed surface
concept and the values for leveltype coincide with the GRIB standard
where possible.

.. list-table::
   :widths: auto
   :header-rows: 1

   * - Level type
     - Meaning
     - Unit/contents of ``l1``/``l2``
""")
        for type, desc, li in self.data:
            print("   * - {}".format(type))
            print("     - {}".format(desc))
            print("     - {}".format(li))


class Tranges:
    def __init__(self):
        self.descs = (
            (0, "Average"),
            (1, "Accumulation"),
            (2, "Maximum"),
            (3, "Minimum"),
            (4, "Difference (value at the end of the time range minus value at the beginning)"),
            (5, "Root Mean Square"),
            (6, "Standard Deviation"),
            (7, "Covariance (temporal variance)"),
            (8, "Difference (value at the beginning of the time range minus value at the end)"),
            (9, "Ratio"),
            (51, "Climatological Mean Value"),
            ('10-191', "Reserved"),
            ('192-254', "Reserved for Local Use"),
            (200, "Vectorial mean"),
            (201, "Mode"),
            (202, "Standard deviation vectorial mean"),
            (203, "Vectorial maximum"),
            (204, "Vectorial minimum"),
            (205, "Product with a valid time ranging inside the given period"),
            (254, "Istantaneous value"),
        )

        self.notes = (
            "Validity time is defined as the time at which the data are measured or at which forecast is valid;"
            " for statistically processed data, the validity time is the end of the time interval.",

            "Reference time is defined as the nominal time of an observation for observed values,"
            " or as the time at which a model forecast starts for forecast values.",

            "The date and time in DB-All.e are always the validity date and time of a value,"
            " regardless of the value being an observation or a forecast.",

            "P1 is defined as the difference in seconds between validity time and reference time."
            " For forecasts it is the positive forecast time."
            " For observed values, the reference time is usually the same as the validity time, therefore P1 is zero."
            " However P1 < 0 is a valid case for reports containing data in the past with respect to the nominal"
            " report time.",

            "P2 is defined as the duration of the period over which statistical processing is performed, and is always"
            " nonnegative. Note that, for instantaneous values, P2 is always zero.",

            # "The Eta (NAM) vertical coordinate system involves normalizing
            # the pressure at some point on a specific level by the mean sea
            # level pressure at that point.",
        )

    def read(self):
        pass

    def print_dox(self):
        print("""/**@defgroup trange_table Time range values
@ingroup tables

Definition of the main concepts related to the description of time
range and statistical processing for observed and forecast data:
""")

        re_newlines = re.compile(r"\n+")
        for n in self.notes:
            print("\\li {}".format(re_newlines.sub(n, "\n")))

        print("""
The following table lists the possible values for pindicator and the
interpretation of the corresponding values of P1 and P2 specifying a
time range:
""")

        for d in self.descs:
            print('\\li \b {} {}'.format(d[0], re_newlines.sub(d[1], "\n")))

        print("*/")

    def print_md(self):
        print("""# Time range values"

Definition of the main concepts related to the description of time
range and statistical processing for observed and forecast data:
""")
        re_newlines = re.compile(r"\n+")
        for n in self.notes:
            print("* {}".format(re_newlines.sub(n, "\n")))

        print("""
The following table lists the possible values for pindicator and the
interpretation of the corresponding values of P1 and P2 specifying a
time range:
""")

        for d in self.descs:
            print('* **{}** {}'.format(d[0], re_newlines.sub(d[1], "\n")))

    def print_rst(self):
        print(""".. _tranges:

Time range values
=================

Definition of the main concepts related to the description of time
range and statistical processing for observed and forecast data:
""")
        re_newlines = re.compile(r"\n+")
        for n in self.notes:
            print("* {}".format(re_newlines.sub(n, "\n")))

        print("""
The following table lists the possible values for pindicator and the
interpretation of the corresponding values of P1 and P2 specifying a
time range:

.. list-table::
   :widths: auto
   :header-rows: 1

   * - Value
     - Meaning
""")

        for d in self.descs:
            print("   * - {}".format(d[0]))
            print("     - {}".format(re_newlines.sub(d[1], "\n")))


class Btable:
    def read(self):
        import struct
        reader = struct.Struct("x 6s x 64s x 24s x 3s x 12s x 3s x 24s x 3s x 10s")

        self.data = []
        for lineno, line in enumerate(sys.stdin.buffer, 1):
            self.data.append(tuple(x.decode("utf-8") for x in reader.unpack(line[:158])))

    def print_dox(self):
        print("""/**@defgroup local_b_table Local B table
@ingroup tables

This table lists all the entries of the local B table.  You can use them to
provide context information for a measured value.

Every entry is listed together with its measure unit, length in characters or
digits and description.

\\verbatim
""")

        table = TextTable(
            colsizes=(6, 66, 18, 14),
            titles=("Code", "Description", "Unit", "Format")
        )
        table.print_head()

        for info in self.data:
            code, desc, unit, dec, sz = info[0], info[1], info[6], int(info[7]), int(info[8])
            fmt = mkformat(unit, sz, dec)
            unit = re.sub("CHARACTER", "Character", unit)
            unit = re.sub("(NUMERIC|NUMBER)", "Numeric", unit)
            table.print_row((code, desc, unit, fmt))
        print("""\\endverbatim
*/""")

    def print_md(self):
        print("""# Local B table

This table lists all the entries of the local B table.  You can use them to
provide context information for a measured value.

Every entry is listed together with its measure unit, length in characters or
digits and description.
""")

        table = MdTable(
            colsizes=(6, 66, 18, 14),
            titles=("Code", "Description", "Unit", "Format")
        )
        table.print_head()

        for info in self.data:
            code, desc, unit, dec, sz = info[0], info[1], info[6], int(info[7]), int(info[8])
            fmt = mkformat(unit, sz, dec)
            unit = re.sub("CHARACTER", "Character", unit)
            unit = re.sub("(NUMERIC|NUMBER)", "Numeric", unit)
            table.print_row((code, desc, unit, fmt))

    def print_rst(self):
        print(""".. _btable:

Local B table codes
===================

This table lists all the entries of the local B table.  You can use them to
provide context information for a measured value.

Every entry is listed together with its measure unit, length in characters or
digits and description.

.. list-table::
   :widths: auto
   :header-rows: 1

   * - Code
     - Description
     - Unit
     - Format
""")

        for info in self.data:
            code, desc, unit, dec, sz = info[0], info[1], info[6], int(info[7]), int(info[8])
            if code[0] == '0':
                code = 'B' + code[1:]
            fmt = mkformat(unit, sz, dec)
            unit = re.sub("CHARACTER", "Character", unit)
            unit = re.sub("(NUMERIC|NUMBER)", "Numeric", unit)
            print("   * - .. _{}:".format(code))
            print("")
            print("       {}".format(code))
            print("     - {}".format(desc))
            print("     - {}".format(unit))
            print("     - {}".format(fmt))


def main():
    parser = argparse.ArgumentParser(description="Format dballe data snippets for documentation.")
    parser.add_argument("intype", help="input type")
    parser.add_argument("outtype", help="output type")
    parser.add_argument("-v", "--verbose", action="store_true", help="verbose output")
    parser.add_argument("--debug", action="store_true", help="verbose output")

    args = parser.parse_args()

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

    inputs = {
        "alias": Aliases,
        "levels": Levels,
        "tranges": Tranges,
        "btable": Btable,
    }

    Proc = inputs.get(args.intype, None)
    if Proc is None:
        raise RuntimeError("Input type {} not supported".format(args.intype))

    proc = Proc()
    proc.read()

    gen = getattr(proc, "print_" + args.outtype, None)
    if gen is None:
        raise RuntimeError("Output type {} not supported for input {}".format(args.outtype, args.intype))

    gen()


if __name__ == "__main__":
    main()
