from dataclasses import dataclass
from typing import Optional
from .exit_codes import SQL_ERROR

import argparse
import gettext
import hashlib
import pathlib
import sqlite3
import sys

_ = gettext.gettext

PRGR_NAME = 'sqlite-to-cpp'


def list_tables(cx):
    """Return a list of tables in database."""
    cx.execute("""
               SELECT name FROM sqlite_schema
               WHERE name NOT LIKE 'sqlite_%'
               AND type IN ('table', 'view')
               UNION ALL
               SELECT name FROM temp.sqlite_schema
               WHERE name NOT LIKE 'sqlite_%'
               AND type IN ('table', 'view')
               """)
    return [name for (name,) in cx.fetchall()]


@dataclass
class TableInfo:
    """Information about one column of one SQLite table."""

    name: str
    type: str
    nullable: bool
    default: str


def table_info(cx, table_name):
    """
    Return information about each column in table.

    Returns a
    """
    cx.execute("""
               SELECT [name], [type], [notnull], [dflt_value]
               FROM pragma_table_info(?)
               ORDER BY cid ASC
               """,
               (table_name,))
    return [TableInfo(name=name,
                      type=type,
                      nullable=notnull == 0,
                      default=dflt_value)
            for (name, type, notnull, dflt_value)
            in cx.fetchall()]


def trans_name(name):
    """Transform the value of a column to something better matched with C++."""
    return ''.join(n.title() for n in name.split('_'))


def trans_table(name):
    """Transform the value of a table to something better matched with C++."""
    return trans_name(name)


def indent(s):
    """Prepend for spaces to string."""
    return ' '*4 + s


def reflow(block):
    """
    Take a multi-line string, remove indention, and return it as a list.

    The first line shoud be empty. The indentation to remove is taken
    from the second line.
    """
    lines = block.split('\n')
    if not lines:
        return lines

    prefix_len = 0
    for c in lines[1]:
        if not c.isspace():
            break
        prefix_len += 1

    return (line[prefix_len:] for line in lines)


def enum_declaration(table, info):
    """Return a C++ enum declaration."""
    return map(indent, [
        f'enum {trans_table(table)} {{',
        *(indent(f'{trans_name(record.name)},')
          for record
          in info),
        f'}}; /* enum {trans_table(table)} */'])


def enum_names_declaration(table, info):
    """Return a C++ string arroy of the names of an enum."""
    table = trans_table(table)
    return map(indent, [
        "const char* names[] = { ",
        *(indent(f'"{table}::{trans_name(record.name)}",')
          for record
          in info),
        "}; /* char* names[] */"])


def enum_out_operator_declaration(table):
    """Return a C++ operator<< definition for printing an enum."""
    type = trans_table(table)
    return map(indent, reflow(f"""
        std::ostream& operator<< (std::ostream& out, const {type}& it) {{
           out << names[it];
           return out;
        }} /* operator<< */
        """))


def run_from_commandline():
    """Entry point of program."""
    gettext.bindtextdomain(PRGR_NAME, 'translation')
    gettext.textdomain('translation')

    parser = argparse.ArgumentParser(
            prog=PRGR_NAME,
            description=_('Generate C++ enums from an SQLite database.'))

    parser.add_argument(
            '-o', '--output',
            type=pathlib.Path,
            help=_('Target output file'))
    parser.add_argument(
            'schema',
            help=_('File containing SQLite schema.'),
            type=argparse.FileType('r'))
    parser.add_argument(
            '--header-guard',
            help=_('Contents of header guard. Defaults to a hash of the file'))
    parser.add_argument(
            '--namespace',
            default='DB',
            action='store',
            help=_('Name of top-level C++ namespace.'))
    parser.add_argument(
            '--no-namespace',
            action='store_true',
            help=_('Disable top level C++ namespace.'))
    parser.add_argument(
            '--warning-directives',
            action='store_true',
            help=_('Include #warning directives.'))

    args = parser.parse_args()
    body = sqlite_to_cpp(
            schema=args.schema.read(),
            warning_directives=args.warning_directives,
            no_namespace=args.no_namespace,
            namespace=args.namespace)

    print(body, file=args.output)

    sys.exit(0)


def sqlite_to_cpp(*,
                  schema: str,
                  warning_directives: bool = False,
                  no_namespace: bool = False,
                  namespace: Optional[str] = None,
                  header_guard: Optional[str] = None):
    """
    Generate C++ code from an SQLite schema.

    [parameters]
        schema - String containing SQL statements for creating the database.
        warning_directives - Should '#warning' directives be inserted
                             into the output
        no_namespace - Omit the top-level namespace
        namespace - Change name of top level namespace
        header_guard - Explicit value to use for header guard. If none
                       is specified then one is generated.
    """
    return_value = 0

    db = sqlite3.connect(':memory:')
    cx = db.cursor()

    cx.executescript(schema)

    lines = []
    for table in list_tables(cx):
        try:
            lines.append(f'namespace {trans_table(table)} {{')

            info = table_info(cx, table)

            lines.extend(enum_declaration(table, info))
            # lines.append('')
            # lines.extend(enum_names_declaration(table, info))
            # lines.extend(enum_out_operator_declaration(table))

        except sqlite3.OperationalError as e:
            # TODO emit error somewhere more visibile
            return_value = SQL_ERROR
            # TODO sanitize e when included in the output
            lines.append(f"/*\n{e}\n*/")
            if warning_directives:
                lines.append(f'#warning "{PRGR_NAME}: {e}"')
            lines.append('')

        lines.extend([
            f'}}; /* namespace {trans_table(table)} */',
            ''])

    body = '\n'.join(lines)

    if not no_namespace:
        body = f'namespace {namespace} {{\n{body}\n}}\n'

    guard = header_guard \
        or ('SHA256' + hashlib.sha256(body.encode('UTF-8')).hexdigest())

    body = f"""
#ifndef {guard}
#define {guard}

{body}
#endif /* {guard} */
    """

    return body.strip()