#!/usr/bin/env perl6
use v6;
use NativeCall;

module libpq {
    constant LIBPQ = "libpq";

    sub PQconnectdb(Str --> Pointer[void]) is native(LIBPQ) {*}

    sub PQstatus(Pointer[void] --> int32) is native(LIBPQ) {*}
    sub PQerrorMessage(Pointer[void] --> Str) is native(LIBPQ) {*}
    sub PQresultStatus(Pointer[void] --> int32) is native(LIBPQ) {*}
    sub PQresultErrorMessage(Pointer[void] --> Str) is native(LIBPQ) {*}

    sub PQprepare(Pointer[void], Str, Str, int32, Pointer[void] --> Pointer[void]) is native(LIBPQ) {*}
    sub PQdescribePrepared(Pointer[void], Str --> Pointer[void]) is native(LIBPQ) {*}
    sub PQnparams(Pointer[void] --> int32) is native(LIBPQ) {*}
    sub PQparamtype(Pointer[void], int32 --> uint32) is native(LIBPQ) {*}
    sub PQnfields(Pointer[void] --> int32) is native(LIBPQ) {*}
    sub PQftype(Pointer[void], int32 --> uint32) is native(LIBPQ) {*}

    our class Description {
        has Int:D @.parameters;
        has Int:D @.fields;
    }

    our class Connection {
        has Pointer[void] $!handle;

        method new(Str:D $connection-string) {
            self.bless(:$connection-string);
        }

        submethod BUILD(Str:D :$connection-string) {
            my $handle = PQconnectdb($connection-string);
            die PQerrorMessage($handle) unless PQstatus($handle) == 0;
            $!handle = $handle;
        }

        method prepare(Str:D $name, Str:D $source) {
            my $result = PQprepare($!handle, $name, $source, 0, Nil);
            die PQerrorMessage($!handle) without $result;
            die PQresultErrorMessage($result) unless PQresultStatus($result) == 1;
        }

        method describe-prepared(Str:D $name) {
            my $result = PQdescribePrepared($!handle, $name);
            die PQerrorMessage($!handle) without $result;
            die PQresultErrorMessage($result) unless PQresultStatus($result) == 1;
            my @parameters = (^PQnparams($result)).map({PQparamtype($result, $_)});
            my @fields = (^PQnfields($result)).map({PQftype($result, $_)});
            Description.new(:@parameters, :@fields);
        }
    }
}

constant DEFAULT-MAPPING = q:to/EOF/;
    16 Boolean
    17 ByteString
    18 Char
    23 Int
    25 String
    701 Number
    2950 UUID
    2951 List UUID
EOF

sub process-module(libpq::Connection $conn, %mapping, Str:D $segment --> Str:D) {
    S:g/'[query|' (.*?) '|]'/&process-query($conn, %mapping, ~$0)/ given $segment;
}

multi sub process-query(libpq::Connection:D $conn, %mapping, Str:D $source --> Str:D) {
    $conn.prepare: '', $source;
    my $description = $conn.describe-prepared('');
    sub convert-types(Int:D @oids --> Str:D) {
        die "unknown oid: $_" unless %mapping{$_}:exists for @oids;
        multi sub infix:<Tuple>() is assoc<right> { "Unit" }
        multi sub infix:<Tuple>(Str:D $a) { $a }
        multi sub infix:<Tuple>(Str:D $a, Str:D $b --> Str:D) { "Tuple ($a) ($b)" }
        [Tuple] |@oids.map({%mapping{$_}}), "Unit";
    }
    my $parameters = convert-types($description.parameters);
    my $fields = convert-types($description.fields);
    "(Query \"\"\"$source\"\"\" :: Query ($parameters) ($fields))";
}

multi sub process-query(libpq::Connection:U $conn, %mapping, Str:D $source --> Str:D) {
    "(Query \"\"\"$source\"\"\")";
}

sub main(libpq::Connection $conn, IO::Path:D $in-file, IO::Path:D $out-file, Bool:D $syntax-only, IO::Path $mapping-file) {
    my %mapping{Int:D} =
        (do with $mapping-file { .slurp } else { DEFAULT-MAPPING })
        .lines
        .map(*.trim.split(/\s+/, 2))
        .grep(*.elems == 2)
        .map({+$_[0] => $_[1]});
    $out-file.spurt: process-module($conn, %mapping, $in-file.slurp);
}

multi sub MAIN(Str $connection-string, IO(Cool) $in-file, IO(Cool) $out-file = '-', Bool :$syntax-only where !*, IO(Cool) :$mapping-file) {
    my $conn = libpq::Connection.new($connection-string);
    main($conn, $in-file, $out-file, $syntax-only // False, $mapping-file);
}

multi sub MAIN(IO(Cool) $in-file, IO(Cool) $out-file = '-', Bool :$syntax-only where *, IO(Cool) :$mapping-file) {
    main(libpq::Connection, $in-file, $out-file, $syntax-only // False, $mapping-file);
}