#!/usr/bin/env python
"""Generate the errors module from PostgreSQL source code.

The script can be run at a new PostgreSQL release to refresh the module.
"""

# Copyright (C) 2018 Daniele Varrazzo  <daniele.varrazzo@gmail.com>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# psycopg2 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
# License for more details.
from __future__ import print_function

import re
import sys
import urllib2
from collections import defaultdict


def main():
    if len(sys.argv) != 2:
        print("usage: %s /path/to/errors.py" % sys.argv[0], file=sys.stderr)
        return 2

    filename = sys.argv[1]

    file_start = read_base_file(filename)
    # If you add a version to the list fix the docs (in errors.rst)
    classes, errors = fetch_errors(
        ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11'])

    f = open(filename, "w")
    for line in file_start:
        print(line, file=f)
    for line in generate_module_data(classes, errors):
        print(line, file=f)


def read_base_file(filename):
    rv = []
    for line in open(filename):
        rv.append(line.rstrip("\n"))
        if line.startswith("# autogenerated"):
            return rv

    raise ValueError("can't find the separator. Is this the right file?")


def parse_errors_txt(url):
    classes = {}
    errors = defaultdict(dict)

    page = urllib2.urlopen(url)
    for line in page:
        # Strip comments and skip blanks
        line = line.split('#')[0].strip()
        if not line:
            continue

        # Parse a section
        m = re.match(r"Section: (Class (..) - .+)", line)
        if m:
            label, class_ = m.groups()
            classes[class_] = label
            continue

        # Parse an error
        m = re.match(r"(.....)\s+(?:E|W|S)\s+ERRCODE_(\S+)(?:\s+(\S+))?$", line)
        if m:
            errcode, macro, spec = m.groups()
            # skip errcodes without specs as they are not publically visible
            if not spec:
                continue
            errlabel = spec.upper()
            errors[class_][errcode] = errlabel
            continue

        # We don't expect anything else
        raise ValueError("unexpected line:\n%s" % line)

    return classes, errors


errors_txt_url = \
    "http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob_plain;" \
    "f=src/backend/utils/errcodes.txt;hb=%s"


def fetch_errors(versions):
    classes = {}
    errors = defaultdict(dict)

    for version in versions:
        print(version, file=sys.stderr)
        tver = tuple(map(int, version.split()[0].split('.')))
        tag = '%s%s_STABLE' % (
            (tver[0] >= 10 and 'REL_' or 'REL'),
            version.replace('.', '_'))
        c1, e1 = parse_errors_txt(errors_txt_url % tag)
        classes.update(c1)

        for c, cerrs in e1.items():
            errors[c].update(cerrs)

    return classes, errors


def generate_module_data(classes, errors):
    tmpl = """

@for_sqlstate(%(errcode)r)
class %(cls)s(%(base)s):
    pass\
"""
    specific = {
        '38002': 'ModifyingSqlDataNotPermittedExt',
        '38003': 'ProhibitedSqlStatementAttemptedExt',
        '38004': 'ReadingSqlDataNotPermittedExt',
        '39004': 'NullValueNotAllowedExt',
        'XX000': 'InternalError_',
    }

    seen = set("""
        Error Warning InterfaceError DataError DatabaseError ProgrammingError
        IntegrityError InternalError NotSupportedError OperationalError
        QueryCanceledError TransactionRollbackError
        """.split())

    for clscode, clslabel in sorted(classes.items()):
        if clscode in ('00', '01'):
            # success and warning - never raised
            continue

        yield "\n\n# %s" % clslabel

        for errcode, errlabel in sorted(errors[clscode].items()):
            if errcode in specific:
                clsname = specific[errcode]
            else:
                clsname = errlabel.title().replace('_', '')
            if clsname in seen:
                raise Exception("class already existing: %s" % clsname)
            seen.add(clsname)

            yield tmpl % {
                'cls': clsname,
                'base': get_base_class_name(errcode),
                'errcode': errcode
            }


def get_base_class_name(errcode):
    """
    This is a python porting of exception_from_sqlstate code in pqpath.c
    """
    if errcode[0] == '0':
        if errcode[1] == 'A':   # Class 0A - Feature Not Supported
            return 'NotSupportedError'
    elif errcode[0] == '2':
        if errcode[1] in '01':
            # Class 20 - Case Not Found
            # Class 21 - Cardinality Violation
            return 'ProgrammingError'
        elif errcode[1] == '2':   # Class 22 - Data Exception
            return 'DataError'
        elif errcode[1] == '3':   # Class 23 - Integrity Constraint Violation
            return 'IntegrityError'
        elif errcode[1] in '45':
            # Class 24 - Invalid Cursor State
            # Class 25 - Invalid Transaction State
            return 'InternalError'
        elif errcode[1] in '678':
            # Class 26 - Invalid SQL Statement Name
            # Class 27 - Triggered Data Change Violation
            # Class 28 - Invalid Authorization Specification
            return 'OperationalError'
        elif errcode[1] in 'BDF':
            # Class 2B - Dependent Privilege Descriptors Still Exist
            # Class 2D - Invalid Transaction Termination
            # Class 2F - SQL Routine Exception
            return 'InternalError'
    elif errcode[0] == '3':
        if errcode[1] == '4':   # Class 34 - Invalid Cursor Name
            return 'OperationalError'
        if errcode[1] in '89B':
            # Class 38 - External Routine Exception
            # Class 39 - External Routine Invocation Exception
            # Class 3B - Savepoint Exception
            return 'InternalError'
        if errcode[1] in 'DF':
            # Class 3D - Invalid Catalog Name
            # Class 3F - Invalid Schema Name
            return 'ProgrammingError'
    elif errcode[0] == '4':
        if errcode[1] == '0':   # Class 40 - Transaction Rollback
            return 'TransactionRollbackError'
        if errcode[1] in '24':
            # Class 42 - Syntax Error or Access Rule Violation
            # Class 44 - WITH CHECK OPTION Violation
            return 'ProgrammingError'
    elif errcode[0] == '5':
        if errcode == "57014":
            return 'QueryCanceledError'
        # Class 53 - Insufficient Resources
        # Class 54 - Program Limit Exceeded
        # Class 55 - Object Not In Prerequisite State
        # Class 57 - Operator Intervention
        # Class 58 - System Error (errors external to PostgreSQL itself)
        else:
            return 'OperationalError'
    elif errcode[0] == 'F':     # Class F0 - Configuration File Error
        return 'InternalError'
    elif errcode[0] == 'H':     # Class HV - Foreign Data Wrapper Error (SQL/MED)
        return 'OperationalError'
    elif errcode[0] == 'P':     # Class P0 - PL/pgSQL Error
        return 'InternalError'
    elif errcode[0] == 'X':     # Class XX - Internal Error
        return 'InternalError'

    return 'DatabaseError'


if __name__ == '__main__':
    sys.exit(main())