package DBD::Avatica::Adapter::Default;

use strict;
use warnings;

use Scalar::Util 'weaken';
use Carp 'croak';
use Time::Piece;
use Scalar::Util qw/looks_like_number/;
use DBI ':sql_types';

use Avatica::Client;

#
# JAVA Types https://github.com/JetBrains/jdk8u_jdk/blob/master/src/share/classes/java/sql/Types.java
#

use constant JAVA_TO_REP => {
    -6  => Avatica::Client::Protocol::Rep::BYTE(),              # TINYINT
    5   => Avatica::Client::Protocol::Rep::SHORT(),             # SMALLINT
    4   => Avatica::Client::Protocol::Rep::INTEGER(),           # INTEGER
    -5  => Avatica::Client::Protocol::Rep::LONG(),              # BIGINT
    6   => Avatica::Client::Protocol::Rep::DOUBLE(),            # FLOAT
    8   => Avatica::Client::Protocol::Rep::DOUBLE(),            # DOUBLE
    2   => Avatica::Client::Protocol::Rep::BIG_DECIMAL(),       # NUMERIC
    1   => Avatica::Client::Protocol::Rep::STRING(),            # CHAR
    91  => Avatica::Client::Protocol::Rep::JAVA_SQL_DATE(),     # DATE
    92  => Avatica::Client::Protocol::Rep::JAVA_SQL_TIME(),     # TIME
    93  => Avatica::Client::Protocol::Rep::JAVA_SQL_TIMESTAMP(),    # TIMESTAMP
    -2  => Avatica::Client::Protocol::Rep::BYTE_STRING(),       # BINARY
    -3  => Avatica::Client::Protocol::Rep::BYTE_STRING(),       # VARBINARY
    16  => Avatica::Client::Protocol::Rep::BOOLEAN(),           # BOOLEAN

    -7  => Avatica::Client::Protocol::Rep::BOOLEAN(),           # BIT
    7   => Avatica::Client::Protocol::Rep::DOUBLE(),            # REAL
    3   => Avatica::Client::Protocol::Rep::BIG_DECIMAL(),       # DECIMAL
    12  => Avatica::Client::Protocol::Rep::STRING(),            # VARCHAR
    -1  => Avatica::Client::Protocol::Rep::STRING(),            # LONGVARCHAR
    -4  => Avatica::Client::Protocol::Rep::BYTE_STRING(),       # LONGVARBINARY
    2004  => Avatica::Client::Protocol::Rep::BYTE_STRING(),     # BLOB
    2005  => Avatica::Client::Protocol::Rep::STRING(),          # CLOB
    -15 => Avatica::Client::Protocol::Rep::STRING(),            # NCHAR
    -9  => Avatica::Client::Protocol::Rep::STRING(),            # NVARCHAR
    -16 => Avatica::Client::Protocol::Rep::STRING(),            # LONGNVARCHAR
    2011  => Avatica::Client::Protocol::Rep::STRING(),          # NCLOB
    2009  => Avatica::Client::Protocol::Rep::STRING(),          # SQLXML
    2013 => Avatica::Client::Protocol::Rep::JAVA_SQL_TIME(),    # TIME_WITH_TIMEZONE
    2014 => Avatica::Client::Protocol::Rep::JAVA_SQL_TIMESTAMP(),    # TIMESTAMP_WITH_TIMEZONE

    # Returned by Avatica for Arrays in EMPTY resultsets
    2000  => Avatica::Client::Protocol::Rep::BYTE_STRING(),     # JAVA_OBJECT
};

use constant REP_TO_TYPE_VALUE => {
    Avatica::Client::Protocol::Rep::INTEGER()            => 'number_value',
    Avatica::Client::Protocol::Rep::PRIMITIVE_INT()      => 'number_value',
    Avatica::Client::Protocol::Rep::SHORT()              => 'number_value',
    Avatica::Client::Protocol::Rep::PRIMITIVE_SHORT()    => 'number_value',
    Avatica::Client::Protocol::Rep::LONG()               => 'number_value',
    Avatica::Client::Protocol::Rep::PRIMITIVE_LONG()     => 'number_value',
    Avatica::Client::Protocol::Rep::BYTE()               => 'number_value',
    Avatica::Client::Protocol::Rep::JAVA_SQL_TIME()      => 'number_value',
    Avatica::Client::Protocol::Rep::JAVA_SQL_DATE()      => 'number_value',
    Avatica::Client::Protocol::Rep::JAVA_SQL_TIMESTAMP() => 'number_value',

    Avatica::Client::Protocol::Rep::BYTE_STRING()        => 'bytes_value',

    Avatica::Client::Protocol::Rep::DOUBLE()             => 'double_value',
    Avatica::Client::Protocol::Rep::PRIMITIVE_DOUBLE()   => 'double_value',

    Avatica::Client::Protocol::Rep::PRIMITIVE_CHAR()     => 'string_value',
    Avatica::Client::Protocol::Rep::CHARACTER()          => 'string_value',
    Avatica::Client::Protocol::Rep::BIG_DECIMAL()        => 'string_value',
    Avatica::Client::Protocol::Rep::STRING()             => 'string_value',

    Avatica::Client::Protocol::Rep::BOOLEAN()            => 'bool_value',
    Avatica::Client::Protocol::Rep::PRIMITIVE_BOOLEAN()  => 'bool_value',
};

use constant JAVA_TO_DBI => {
    -6  => SQL_TINYINT,                         # TINYINT
    5   => SQL_SMALLINT,                        # SMALLINT
    4   => SQL_INTEGER,                         # INTEGER
    -5  => SQL_BIGINT,                          # BIGINT
    6   => SQL_FLOAT,                           # FLOAT
    8   => SQL_DOUBLE,                          # DOUBLE
    2   => SQL_NUMERIC,                         # NUMERIC
    1   => SQL_CHAR,                            # CHAR
    91  => SQL_TYPE_DATE,                       # DATE
    92  => SQL_TYPE_TIME,                       # TIME
    93  => SQL_TYPE_TIMESTAMP,                  # TIMESTAMP
    -2  => SQL_BINARY,                          # BINARY
    -3  => SQL_VARBINARY,                       # VARBINARY
    16  => SQL_BOOLEAN,                         # BOOLEAN

    -7  => SQL_BIT,                             # BIT
    7   => SQL_REAL,                            # REAL
    3   => SQL_DECIMAL,                         # DECIMAL
    12  => SQL_VARCHAR,                         # VARCHAR
    -1  => SQL_LONGVARCHAR,                     # LONGVARCHAR
    -4  => SQL_LONGVARBINARY,                   # LONGVARBINARY
    2004  => SQL_BLOB,                          # BLOB
    2005  => SQL_CLOB,                          # CLOB
    -15 => SQL_CHAR,                            # NCHAR
    -9  => SQL_VARCHAR,                         # NVARCHAR
    -16 => SQL_LONGVARCHAR,                     # LONGNVARCHAR
    2011  => SQL_CLOB,                          # NCLOB
    2009  => SQL_LONGVARCHAR,                   # SQLXML
    2013 => SQL_TYPE_TIME_WITH_TIMEZONE,        # TIME_WITH_TIMEZONE
    2014 => SQL_TYPE_TIMESTAMP_WITH_TIMEZONE,   # TIMESTAMP_WITH_TIMEZONE

    # Returned by Avatica for Arrays in EMPTY resultsets
    2000  => SQL_ARRAY,                         # JAVA_OBJECT
    2003  => SQL_ARRAY,                         # ARRAY
};

sub new {
    my ($class, %params) = @_;

    my $self = {dbh => $params{dbh}};
    weaken $self->{dbh};

    return bless $self, $class;
}

# params:
# self
# [Avatica::Client::Protocol::ColumnValue, ...]
# [Avatica::Client::Protocol::ColumnMetaData, ...]
sub row_from_jdbc {
    my ($self, $columns_values, $columns_meta) = @_;
    croak 'The number of arguments is not the same as the expected number' if $#{$columns_values} != $#{$columns_meta};
    return [
        map {
            $self->from_jdbc($columns_values->[$_], $columns_meta->[$_])
        }
        0 .. $#{$columns_meta}
    ];
}

# params:
# self
# Avatica::Client::Protocol::ColumnValue
# Avatica::Client::Protocol::ColumnMetaData
sub from_jdbc {
    my ($self, $column_value, $column_meta) = @_;

    my $scalar_value = $column_value->get_scalar_value;

    return (undef,) if $scalar_value && $scalar_value->get_null;

    if ($column_value->get_has_array_value) {
        my $jdbc_type_id = $column_meta->get_type->get_component->get_id;
        my $rep = $self->convert_jdbc_to_rep_type($jdbc_type_id);

        my $type = $self->REP_TO_TYPE_VALUE()->{$rep};
        my $method = "get_$type";

        my $values = [];
        for my $v (@{$column_value->get_array_value_list}) {
            my $res = $v->$method();
            push @$values, $self->convert_from_jdbc($res, $rep);
        }

        return $values;
    }

    my $jdbc_type_id = $column_meta->get_type->get_id;
    my $rep = $self->convert_jdbc_to_rep_type($jdbc_type_id);

    my $type = $self->REP_TO_TYPE_VALUE()->{$rep};
    my $method = "get_$type";

    my $res = $scalar_value->$method();

    return $self->convert_from_jdbc($res, $rep);
}

# params:
# self
# values
# [Avatica::Client::Protocol::AvaticaParameter, ...]
sub row_to_jdbc {
    my ($self, $values, $avatica_params) = @_;
    croak 'The number of arguments is not the same as the expected number' if $#{$values} != $#{$avatica_params};
    return [
        map {
            $self->to_jdbc($values->[$_], $avatica_params->[$_])
        }
        0 .. $#{$avatica_params}
    ];
}

# params:
# self
# value
# Avatica::Client::Protocol::AvaticaParameter
sub to_jdbc {
    my ($self, $value, $avatica_param) = @_;

    my $jdbc_type_id = $avatica_param->get_parameter_type;

    my $typed_value = Avatica::Client::Protocol::TypedValue->new;

    unless (defined $value) {
        $typed_value->set_null(1);
        $typed_value->set_type(Avatica::Client::Protocol::Rep::NULL());
        return $typed_value;
    }

    $typed_value->set_null(0);

    my $rep = $self->convert_jdbc_to_rep_type($jdbc_type_id);
    croak "Unknown jdbc type: $jdbc_type_id" unless $rep;
    my $type = $self->REP_TO_TYPE_VALUE->{$rep};
    croak "Unknown rep type: $rep" unless $type;

    my $method = "set_$type";

    $typed_value->set_type($rep);
    $typed_value->$method($self->convert_to_jdbc($value, $rep));

    return $typed_value;
}

sub convert_from_jdbc {
    my ($self, $value, $rep) = @_;

    if ($rep == Avatica::Client::Protocol::Rep::JAVA_SQL_TIME()) {
        my $sec = int($value / 1000);
        my $milli = $value % 1000;
        my $time = Time::Piece->strptime($sec, '%s')->time;
        $time .= '.' . $milli if $milli;
        return $time;
    }

    if ($rep == Avatica::Client::Protocol::Rep::JAVA_SQL_DATE()) {
        return Time::Piece->strptime($value * 86400, '%s')->ymd;
    }

    if ($rep == Avatica::Client::Protocol::Rep::JAVA_SQL_TIMESTAMP()) {
        my $sec = int($value / 1000);
        my $milli = $value % 1000;
        my $datetime = Time::Piece->strptime($sec, '%s')->strftime('%Y-%m-%d %H:%M:%S');
        $datetime .= '.' . $milli if $milli;
        return $datetime;
    }

    return $value;
}

sub convert_to_jdbc {
    my ($self, $value, $rep) = @_;

    if ($rep == Avatica::Client::Protocol::Rep::JAVA_SQL_TIME()) {
        return $value if looks_like_number($value);

        my ($datetime, $milli) = split /\./, $value;
        my ($date, $time) = split /[tT ]/, $datetime;
        $time = $date unless $time;

        my ($h, $m, $s) = split /:/, $time;
        return ((($h // 0) * 60 + ($m // 0)) * 60 + ($s // 0)) * 1000 + ($milli ? substr($milli . '00', 0, 3)  : 0);
    }

    if ($rep == Avatica::Client::Protocol::Rep::JAVA_SQL_DATE()) {
        return $value if looks_like_number($value);
        my ($datetime, $milli) = split /\./, $value;
        my ($date, $time) = split /[tT ]/, $datetime;
        return Time::Piece->strptime($date, '%Y-%m-%d')->epoch / 86400;
    }

    if ($rep == Avatica::Client::Protocol::Rep::JAVA_SQL_TIMESTAMP()) {
        return $value if looks_like_number($value);
        my ($datetime, $milli) = split /\./, $value;
        $datetime =~ s/[Tt]/ /;
        my $sec = Time::Piece->strptime($datetime, '%Y-%m-%d %H:%M:%S')->epoch;
        return $sec * 1000 + ($milli ? substr($milli . '00', 0, 3) : 0);
    }

    return $value;
}

sub convert_jdbc_to_rep_type {
    my ($self, $jdbc_type) = @_;
    if ($jdbc_type > 0x7FFFFFFF) {
        $jdbc_type = -(($jdbc_type ^ 0xFFFFFFFF) + 1);
    }
    return $self->JAVA_TO_REP()->{$jdbc_type};
}

# params:
# self
# Avatica::Client::Protocol::AvaticaType
sub to_dbi {
    my ($self, $avatica_type) = @_;
    my $java_type_id = $avatica_type->get_id;

    if ($java_type_id > 0x7FFFFFFF) {
        $java_type_id = -(($java_type_id ^ 0xFFFFFFFF) + 1);
    }

    my $dbi_type_id = $self->JAVA_TO_DBI()->{$java_type_id};
    return $java_type_id unless $dbi_type_id;
    return $dbi_type_id;
}

# may be phoenix specific code
sub map_database_properties {
    my ($self, $properties) = @_;

    my $res = {
        AVATICA_DRIVER_NAME => '',
        AVATICA_DRIVER_VERSION => '',
        DBMS_NAME => '',
        DBMS_VERSION => '',
        SQL_KEYWORDS => ''
    };

    for my $p (@$properties) {
        my $name = $p->get_key->get_name // '';
        if ($name eq 'GET_DRIVER_NAME') {
            $res->{AVATICA_DRIVER_NAME} = $p->get_value->get_string_value;
        } elsif ($name eq 'GET_DRIVER_VERSION') {
            $res->{AVATICA_DRIVER_VERSION} = $p->get_value->get_string_value;
        } elsif ($name eq 'GET_DATABASE_PRODUCT_VERSION') {
            $res->{DBMS_VERSION} = $p->get_value->get_string_value;
        } elsif ($name eq 'GET_DATABASE_PRODUCT_NAME') {
            $res->{DBMS_NAME} = $p->get_value->get_string_value;
        } elsif ($name eq 'GET_S_Q_L_KEYWORDS') {
            $res->{SQL_KEYWORDS} = $p->get_value->get_string_value;
        }
    }

    return $res;
}

# params:
# self
# Avatica::Client::Protocol::Signature
sub extend_primary_key_info_signature {
    my ($self, $signature) = @_;
    return $signature;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

DBD::Avatica::Adapter::Default

=head1 VERSION

version 0.2.0

=head1 AUTHOR

Alexey Stavrov <logioniz@ya.ru>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2021 by Alexey Stavrov.

This is free software, licensed under:

  The MIT (X11) License

=cut
