#!/usr/bin/env python

"""This program takes a couple of lines with

 * a class name

 * the names and types of some attributes (possibly with an alias)

and translates it into a Java class with potentially three parts:

 * A 'Plain Old Java Object' (POJO) with public final attributes.

 * A static method which creates an object from a ResultSet.

 * Methods for converting from/to Json (using Gson).

Example: We have a query collecting college application data:

  SELECT    college_name as college, major, COUNT() as cnt
  FROM      applications
  GROUP BY  college_name, major
  ORDER BY  cnt DESC, college, major

We want a POJO with college name, major, and count, and need four
lines, the first is the name of the class, the other three describes
the attributes:

AppStats
college: String
major: String
cnt -> count: int

We can run this program (`edaf75-pojo`), and tell we want both JDBC
related code (-j), and Json/Gson related code (-g), by issuing the
following command (assuming we have a soft link `edaf75-pojo` to this
file in our `$PATH`):

$ edaf75-pojo -j -g
AppStats
college : String
major : String
cnt -> count : int
<ctrl-D>

and get:

class AppStats {

    public final String college;
    public final String major;
    public final int count;

    private AppStats  (String college, String major, int count) {
        this.college = college;
        this.major = major;
        this.count = count;
    }

    public static AppStats fromRS(ResultSet rs) throws SQLException {
        return new AppStats(rs.getString("college"), rs.getString("major"), rs.getInt("cnt"));
    }

    public static AppStats fromJson(Gson gson, String json) {
        return gson.fromJson(json, AppStats.class);
    }

    public String toJson(Gson gson) {
        return gson.toJson(this);
    }
}

"""

import argparse
import re
import sys
from typing import Generator


def parse_cmd_line() -> (bool, bool):
    """See which of the switches for JDBC (-j/--jdbc) and Gson (-g/--gson)
    are given on the command line.

    """
    parser = argparse.ArgumentParser(description='Create POJOs for EDAF75')
    parser.add_argument('-j', '--jdbc', action="store_true", help="Generate JDBC-related code")
    parser.add_argument('-g', '--gson', action="store_true", help="Generate Gson-related code")
    args = parser.parse_args()
    return (args.jdbc, args.gson)


ALIASED_ATTRIBUTE = r'\s*(\w+)\s*->\s*(\w+)\s*:\s*(\w+)\s*'
UNALIASED_ATTRIBUTE = r'\s*(\w+)\s*:\s*(\w+)\s*'


def parse_attribute(line: str) -> (str, str, str):
    """Check if a line contains an attribute description. Returns a tuple
    with its original name (from the SELECT statement), its prefered
    name, and its type.

    """
    m = re.search(ALIASED_ATTRIBUTE, line)
    if m:
        return m.group(1), m.group(2), m.group(3)
    m = re.search(UNALIASED_ATTRIBUTE, line)
    if m:
        return m.group(1), m.group(1), m.group(2)
    if len(line.strip()) > 0:
        print('================================')
        print(f'Unrecognized line: {line}', end='')
        print('================================')
        exit(1)
    return None


def parse_attributes(lines: [str]) -> Generator[(str, str, str)]:
    """Generate a list of all attributes found in input
    """
    for line in lines:
        p = parse_attribute(line)
        if p:
            yield p


def parse_specs(lines: [str]) -> (str, [(str, str, str)]):
    """Split input into class name and a list of lines containing
    attribute declarations

    """
    name = lines[0].strip()
    return name, list(parse_attributes(lines[1:]))


def generate_pojo(class_name: str,
                  attributes: [(str, str, str)],
                  jdbc: bool, gson: bool) -> ():
    """Print a POJO definition for the given class
    """
    print(f"class {class_name} {{")
    print("")
    for _, name, tpe in attributes:
        print(f"    public final {tpe} {name};")
    if jdbc:
        print("")
        param_list = ', '.join(f"{tpe} {name}" for _, name, tpe in attributes)
        print(f"    private {class_name}  ({param_list}) {{")
        for _, name, _ in attributes:
            print(f"        this.{name} = {name};")
        print("    }")
        print("")
        print(f"    public static {class_name} fromRS(ResultSet rs) throws SQLException {{")
        getters = ', '.join(f'rs.get{tpe.capitalize()}("{orig_name}")'
                            for orig_name, _, tpe in attributes)
        print(f"        return new {class_name}({getters});")
        print("    }")
    if gson:
        print("")
        print(f"    public static {class_name} fromJson(Gson gson, String json) {{")
        print(f"        return gson.fromJson(json, {class_name}.class);")
        print("    }")
        print("")
        print("    public String toJson(Gson gson) {")
        print("        return gson.toJson(this);")
        print("    }")
    print("}")


def main():
    jdbc, gson = parse_cmd_line()
    class_name, attributes = parse_specs(list(sys.stdin))
    generate_pojo(class_name, attributes, jdbc, gson)


if __name__ == '__main__':
    main()
