#! /usr/bin/env python

"""Extract configuration items into various configuration headers.

This uses the configitems file, a database consisting of text lines with the
following single-tab-separated fields:
 - Name of the configuration item, e.g. PQXX_HAVE_PTRDIFF_T.
 - Publication marker: public or internal.
 - A single environmental factor determining the item, e.g. libpq or compiler.
"""

from __future__ import (
    absolute_import,
    print_function,
    unicode_literals,
    )

from argparse import ArgumentParser
import codecs
from errno import ENOENT
import os.path
from os import getcwd
import re

__metaclass__ = type


def read_text_file(path):
    """Read text file, return as string, or `None` if file is not there."""
    try:
        with codecs.open(path, encoding='ascii') as stream:
            return stream.read()
    except IOError as error:
        if error.errno == ENOENT:
            return None
        else:
            raise


def read_lines(path):
    """Read text file, return as list of lines."""
    with codecs.open(path, encoding='ascii') as stream:
        return list(stream)


def read_configitems(filename):
    """Read the configuration-items database.

    :param filename: Path to the configitems file.
    :return: Sequence of text lines from configitems file.
    """
    return [line.split() for line in read_lines(filename)]


def map_configitems(items):
    """Map each config item to publication/factor.

    :param items: Sequence of config items: (name, publication, factor).
    :return: Dict mapping each item name to a tuple (publication, factor).
    """
    return {
        item: (publication, factor)
        for item, publication, factor in items
        }


def read_header(source_tree, filename):
    """Read the original config.h generated by autoconf.

    :param source_tree: Path to libpqxx source tree.
    :param filename: Path to the config.h file.
    :return: Sequence of text lines from config.h.
    """
    return read_lines(os.path.join(source_tree, filename))


def extract_macro_name(config_line):
    """Extract a cpp macro name from a configuration line.

    :param config_line: Text line from config.h which may define a macro.
    :return: Name of macro defined in `config_line` if it is a `#define`
        statement, or None.
    """
    config_line = config_line.strip()
    match = re.match('\s*#\s*define\s+([^\s]+)', config_line)
    if match is None:
        return None
    else:
        return match.group(1)


def extract_section(header_lines, items, publication, factor):
    """Extract config items for given publication/factor from header lines.

    :param header_lines: Sequence of header lines from config.h.
    :param items: Dict mapping macro names to (publication, factor).
    :param publication: Extract only macros for this publication tag.
    :param factor: Extract only macros for this environmental factor.
    :return: Sequence of `#define` lines from `header_lines` insofar they
        fall within the requested section.
    """
    return sorted(
        line.strip()
        for line in header_lines
        if items.get(extract_macro_name(line)) == (publication, factor)
        )


def compose_header(lines, publication, factor):
    """Generate header text containing given lines."""
    intro = (
        "/* Automatically generated from config.h: %s/%s config. */"
        % (publication, factor)
        )
    return '\n'.join([intro, ''] + lines + [''])


def generate_config(source_tree, header_lines, items, publication, factor):
    """Generate config file for a given section, if appropriate.

    Writes nothing if the configuration file ends up either empty, or
    identical to one that's already there.

    :param source_tree: Location of the libpqxx source tree.
    :param header_lines: Sequence of header lines from config.h.
    :param items: Dict mapping macro names to (publication, factor).
    :param publication: Extract only macros for this publication tag.
    :param factor: Extract only macros for this environmental factor.
    """
    config_file = os.path.join(
        source_tree, 'include', 'pqxx',
        'config-%s-%s.h' % (publication, factor))
    section = extract_section(header_lines, items, publication, factor)
    if len(section) == 0:
        print("Generating %s: no items--skipping." % config_file)
        return

    contents = compose_header(section, publication, factor)
    if read_text_file(config_file) == contents:
        print("Generating %s: no changes--skipping." % config_file)
        return

    print("Generating %s: %d item(s)." % (config_file, len(section)))
    with codecs.open(config_file, 'wb', encoding='ascii') as header:
        header.write(contents)


def parse_args():
    """Parse command-line arguments."""
    default_source_tree = os.path.dirname(
        os.path.dirname(os.path.normpath(os.path.abspath(__file__))))
    parser = ArgumentParser(description=__doc__)
    parser.add_argument(
        'sourcetree', metavar='PATH', default=default_source_tree,
        help="Location of libpqxx source tree.  Defaults to '%(default)s'.")
    return parser.parse_args()


def check_args(args):
    """Validate command-line arguments."""
    if not os.path.isdir(args.sourcetree):
        raise Exception("Not a directory: '%s'." % args.sourcetree)


def main():
    """Main program entry point."""
    args = parse_args()
    check_args(args)
    # The configitems file is under revision control; it's in sourcetree.
    items = read_configitems(os.path.join(args.sourcetree, 'configitems'))
    publications = sorted(set(item[1] for item in items))
    factors = sorted(set(item[2] for item in items))
    # The config.h header is generated; it's in the build tree, which should
    # be where we are.
    original_header = read_header(getcwd(), 'include/pqxx/config.h')
    items_map = map_configitems(items)

    for publication in publications:
        for factor in factors:
            generate_config(
                getcwd(), original_header, items_map, publication, factor)


if __name__ == '__main__':
    main()
