package DBD::Mock::db;

use strict;
use warnings;

use List::Util qw( first );
use DBI;

our $imp_data_size = 0;

sub ping {
    my ($dbh) = @_;
    return $dbh->{mock_can_connect};
}

sub last_insert_id {
    my ($dbh) = @_;
    return $dbh->{mock_last_insert_id};
}

sub get_info {
    my ( $dbh, $attr ) = @_;
    $dbh->{mock_get_info} ||= {};
    return $dbh->{mock_get_info}{$attr};
}

sub table_info {
    my ( $dbh, @params ) = @_;

    my ($cataloge, $schema, $table, $type) = map { $_ || '' } @params[0..4];

    $dbh->{mock_table_info} ||= {};

    my @tables = @{ $dbh->{mock_table_info}->{ $cataloge }->{ $schema }->{ $table }->{ $type } || [] };

    my ($fieldNames, @rows) = map { [ @$_ ] } @tables;

    $fieldNames ||= [];

    my $sponge = DBI->connect('dbi:Sponge:', '', '' )
        or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");

    my $sth = $sponge->prepare("table_info", {
        rows          => \@rows,
        NUM_OF_FIELDS => scalar @$fieldNames,
        NAME          => $fieldNames
    }) or return $dbh->DBI::set_err( $sponge->err(), $sponge->errstr() );

    return $sth;
}

sub prepare {
    my ( $dbh, $statement ) = @_;

    unless ( $dbh->{mock_can_connect} ) {
        $dbh->set_err( 1, "No connection present" );
        return;
    }
    unless ( $dbh->{mock_can_prepare} ) {
        $dbh->set_err( 1, "Cannot prepare" );
        return;
    }
    $dbh->{mock_can_prepare}++ if $dbh->{mock_can_prepare} < 0;

    eval {
        foreach my $parser ( @{ $dbh->{mock_parser} } )
        {
            if ( ref($parser) eq 'CODE' ) {
                $parser->($statement);
            }
            else {
                $parser->parse($statement);
            }
        }
    };
    if ($@) {
        my $parser_error = $@;
        chomp $parser_error;
        $dbh->set_err( 1,
"Failed to parse statement. Error: ${parser_error}. Statement: ${statement}"
        );
        return;
    }

    my $sth = DBI::_new_sth( $dbh, { Statement => $statement } );
    $sth->trace_msg( "Preparing statement '${statement}'\n", 1 );
    my %track_params = ( statement => $statement );

    if ( my $session = $dbh->{mock_session} ) {
        eval {
            my $rs = $session->results_for($statement);
            if ( ref($rs) eq 'ARRAY' && scalar( @{$rs} ) > 0 ) {
                my $fields = @{$rs}[0];
                $track_params{return_data} = $rs;
                $track_params{fields}      = $fields;
                $sth->STORE( NAME          => $fields );
                $sth->STORE( NUM_OF_FIELDS => scalar @{$fields} );
            }
            else {
                $sth->trace_msg( "No return data set in DBH\n", 1 );
            }
        };

        if ($@) {
            $dbh->DBI::set_err( 1, "Session Error: $@. Statement: $statement" );
        }

    }

    else {
        # If we have available resultsets seed the tracker with one

        my ($rs, $callback, $failure);

        if ( my $all_rs = $dbh->{mock_rs} ) {
            if ( my $by_name = defined $all_rs->{named}{$statement} ? $all_rs->{named}{$statement} : first { $statement =~ m/$_->{regexp}/ } @{ $all_rs->{matching} } ) {
                # We want to copy this, because it is meant to be reusable
                $rs = [ @{ $by_name->{results} } ];
                $callback = $by_name->{callback};
                $failure = $by_name->{failure};
            }
            else {
                $rs = shift @{ $all_rs->{ordered} };
                if (ref($rs) eq 'HASH') {
                    $callback = $rs->{callback};
                    $failure = $rs->{failure};
                    $rs = [ @{ $rs->{results} } ];
                }
            }
        }

        if ( ref($rs) eq 'ARRAY' && ( scalar( @{$rs} ) > 0 || $callback ) ) {
            my $fields = shift @{$rs};
            $track_params{return_data} = $rs;
            $track_params{fields}      = $fields;
            $track_params{callback}    = $callback;
            $track_params{failure}     = $failure;

            if( $fields ) {
                $sth->STORE( NAME          => $fields );
                $sth->STORE( NUM_OF_FIELDS => scalar @{$fields});
            }

        }
        else {
            $sth->trace_msg( "No return data set in DBH\n", 1 );
        }

    }

    # do not allow a statement handle to be created if there is no
    # connection present.

    unless ( $dbh->FETCH('Active') ) {
        $dbh->set_err( 1, "No connection present" );
        return;
    }

    # This history object will track everything done to the statement

    my $history = DBD::Mock::StatementTrack->new(%track_params);
    $sth->STORE( mock_my_history => $history );

    # ...now associate the history object with the database handle so
    # people can browse the entire history at once, even for
    # statements opened and closed in a black box

    my $all_history = $dbh->FETCH('mock_statement_history');
    push @{$all_history}, $history;

    return $sth;
}

*prepare_cached = \&prepare;

{
    my $begin_work_commit;

    sub begin_work {
        my $dbh = shift;
        if ( $dbh->FETCH('AutoCommit') ) {
            $dbh->STORE( 'AutoCommit', 0 );
            $begin_work_commit = 1;
            my $sth = $dbh->prepare('BEGIN WORK')
              or return $dbh->set_err( 1, $DBI::errstr );
            my $rc = $sth->execute()
              or return $dbh->set_err( 1, $DBI::errstr );
            $sth->finish();
            return $rc;
        }
        else {
            return $dbh->set_err( 1,
                'AutoCommit is off, you are already within a transaction' );
        }
    }

    sub commit {
        my $dbh = shift;
        if ( $dbh->FETCH('AutoCommit') && $dbh->FETCH('Warn') ) {
            return $dbh->set_err( 1, "commit ineffective with AutoCommit" );
        }

        my $sth = $dbh->prepare('COMMIT')
          or return $dbh->set_err( 1, $DBI::errstr );
        my $rc = $sth->execute()
          or return $dbh->set_err( 1, $DBI::errstr );
        $sth->finish();

        if ($begin_work_commit) {
            $dbh->STORE( 'AutoCommit', 1 );
            $begin_work_commit = 0;
        }

        return $rc;
    }

    sub rollback {
        my $dbh = shift;
        if ( $dbh->FETCH('AutoCommit') && $dbh->FETCH('Warn') ) {
            return $dbh->set_err( 1, "rollback ineffective with AutoCommit" );
        }

        my $sth = $dbh->prepare('ROLLBACK')
          or return $dbh->set_err( 1, $DBI::errstr );
        my $rc = $sth->execute()
          or return $dbh->set_err( 1, $DBI::errstr );
        $sth->finish();

        if ($begin_work_commit) {
            $dbh->STORE( 'AutoCommit', 1 );
            $begin_work_commit = 0;
        }

        return $rc;
    }
}

# NOTE:
# this method should work in most cases, however it does
# not exactly follow the DBI spec in the case of error
# handling. I am not sure if that level of detail is
# really nessecary since it is a weird error conditon
# which causes it to fail anyway. However if you find you do need it,
# then please email me about it. I think it would be possible
# to mimic it by accessing the DBD::Mock::StatementTrack
# object directly.
sub selectcol_arrayref {
    my ( $dbh, $query, $attrib, @bindvalues ) = @_;

    # get all the columns ...
    my $a_ref = $dbh->selectall_arrayref( $query, $attrib, @bindvalues );

    # if we get nothing back, or dont get an
    # ARRAY ref back, then we can assume
    # something went wrong, and so return undef.
    return undef unless defined $a_ref || ref($a_ref) ne 'ARRAY';

    my @cols = 0;
    if ( ref $attrib->{Columns} eq 'ARRAY' ) {
        @cols = map { $_ - 1 } @{ $attrib->{Columns} };
    }

    # if we do get something then we
    # grab all the columns out of it.
    return [ map { @$_[@cols] } @{$a_ref} ];
}

sub FETCH {
    my ( $dbh, $attrib, $value ) = @_;
    $dbh->trace_msg("Fetching DB attrib '$attrib'\n");

    if ( $attrib eq 'Active' ) {
        return $dbh->{mock_can_connect};
    }
    elsif ( $attrib eq 'mock_all_history' ) {
        return $dbh->{mock_statement_history};
    }
    elsif ( $attrib eq 'mock_all_history_iterator' ) {
        return DBD::Mock::StatementTrack::Iterator->new(
            $dbh->{mock_statement_history} );
    }
    elsif ( $attrib =~ /^mock/ ) {
        return $dbh->{$attrib};
    }
    elsif ( $attrib =~ /^(private_|dbi_|dbd_|[A-Z])/ ) {
        $dbh->trace_msg(
            "... fetching non-driver attribute ($attrib) that DBI handles\n");
        return $dbh->SUPER::FETCH($attrib);
    }
    else {
        if ( $dbh->{mock_attribute_aliases} ) {
            if ( exists ${ $dbh->{mock_attribute_aliases}->{db} }{$attrib} ) {
                my $mock_attrib =
                  $dbh->{mock_attribute_aliases}->{db}->{$attrib};
                if ( ref($mock_attrib) eq 'CODE' ) {
                    return $mock_attrib->($dbh);
                }
                else {
                    return $dbh->FETCH($mock_attrib);
                }
            }
        }
        $dbh->trace_msg(
"... fetching non-driver attribute ($attrib) that DBI doesn't handle\n"
        );
        return $dbh->{$attrib};
    }
}

sub STORE {
    my ( $dbh, $attrib, $value ) = @_;

    my $printed_value = $value || 'undef';
    $dbh->trace_msg("Storing DB attribute '$attrib' with '$printed_value'\n");

    if ( $attrib eq 'AutoCommit' ) {

        # These are magic DBI values that say we can handle AutoCommit
        # internally as well
        $value = ($value) ? -901 : -900;
    }

    if ( $attrib eq 'mock_clear_history' ) {
        if ($value) {
            $dbh->{mock_statement_history} = [];
        }
        return [];
    }
    elsif ( $attrib eq 'mock_add_parser' ) {
        my $parser_type = ref($value);
        my $is_valid_parser;

        if ( $parser_type eq 'CODE' ) {
            $is_valid_parser++;
        }
        elsif ( $parser_type && $parser_type !~ /^(ARRAY|HASH|SCALAR)$/ ) {
            $is_valid_parser = eval { $parser_type->can('parse') };
        }

        unless ($is_valid_parser) {
            my $error =
                "Parser must be a code reference or object with 'parse()' "
              . "method (Given type: '$parser_type')";
            $dbh->set_err( 1, $error );
            return;
        }
        push @{ $dbh->{mock_parser} }, $value;
        return $value;
    }
    elsif ( $attrib eq 'mock_add_resultset' ) {
        my @copied_values;

        $dbh->{mock_rs} ||= {
            named   => {},
            ordered => [],
            matching => [],
        };

        if ( ref $value eq 'ARRAY' ) {
            @copied_values = @{$value};
            push @{ $dbh->{mock_rs}{ordered} }, \@copied_values;
        }
        elsif ( ref $value eq 'HASH' ) {
            my $name = $value->{sql};

            @copied_values = @{ $value->{results} ? $value->{results} : [] };

            if (not defined $name) {
                push @{ $dbh->{mock_rs}{ordered} }, {
                    results => \@copied_values,
                    callback => $value->{callback},
                    failure => ref($value->{failure}) ? [ @{ $value->{failure} } ] : undef,
                };
            }
            elsif ( ref $name eq "Regexp" ) {
                my $matching = {
                    regexp => $name,
                    results => \@copied_values,
                    callback => $value->{callback},
                    failure => ref($value->{failure}) ? [ @{ $value->{failure} } ] : undef,
                };
                # either replace existing match or push
                grep { $_->{regexp} eq $name && ($_ = $matching) } @{ $dbh->{mock_rs}{matching} }
                  or push @{ $dbh->{mock_rs}{matching} }, $matching;
            }
            else {
                $dbh->{mock_rs}{named}{$name} = {
                    results => \@copied_values,
                    callback => $value->{callback},
                    failure => ref($value->{failure}) ? [ @{ $value->{failure} } ] : undef,
                };
            }
        }
        else {
            die "Must provide an arrayref or hashref when adding ",
              "resultset via 'mock_add_resultset'.\n";
        }

        return \@copied_values;
    }
    elsif ( $attrib eq 'mock_start_insert_id' ) {
        if ( ref $value eq 'ARRAY' ) {
            $dbh->{mock_last_insert_ids} = {}
              unless $dbh->{mock_last_insert_ids};
            $dbh->{mock_last_insert_ids}{ $value->[0] } = $value->[1];
        }
        else {

            # we start at one minus the start id
            # so that the increment works
            $dbh->{mock_last_insert_id} = $value - 1;
        }

    }
    elsif ( $attrib eq 'mock_session' ) {
        ( ref($value) && UNIVERSAL::isa( $value, 'DBD::Mock::Session' ) )
          || die
"Only DBD::Mock::Session objects can be placed into the 'mock_session' slot\n"
          if defined $value;
        $dbh->{mock_session} = $value;
    }
    elsif ( $attrib =~ /^mock_(add_)?data_sources/ ) {
        $dbh->{Driver}->STORE( $attrib, $value );
    }
    elsif ( $attrib =~ /^mock_add_table_info$/ ) {
        $dbh->{mock_table_info} ||= {};

        if ( ref $value ne "HASH" ) {
            die "mock_add_table_info needs a hash reference"
        }

        my ( $cataloge, $schema, $table, $type ) = map { defined $_ ? $_ : '' } @$value{qw( cataloge schema table type )};

        $dbh->{mock_table_info}->{ $cataloge }->{ $schema }->{ $table }->{ $type } = $value->{table_info}; 
    }
    elsif ( $attrib =~ /^mock_clear_table_info$/ ) {
        if ( $value ) {
            $dbh->{mock_table_info} = {};
        }

        return {};
    }
    elsif ( $attrib =~ /^mock/ ) {
        return $dbh->{$attrib} = $value;
    }
    elsif ( $attrib =~ /^(private_|dbi_|dbd_|[A-Z])/ ) {
        $dbh->trace_msg(
"... storing non-driver attribute ($attrib) with value ($printed_value) that DBI handles\n"
        );
        return $dbh->SUPER::STORE( $attrib, $value );
    }
    else {
        $dbh->trace_msg(
"... storing non-driver attribute ($attrib) with value ($printed_value) that DBI won't handle\n"
        );
        return $dbh->{$attrib} = $value;
    }
}

sub DESTROY {
    my ($dbh) = @_;
    if ( my $session = $dbh->{mock_session} ) {
        if ( $session->has_states_left ) {
            die "DBH->finish called when session still has states left\n";
        }
    }
}

sub disconnect {
    my ($dbh) = @_;
    if ( my $session = $dbh->{mock_session} ) {
        if ( $session->has_states_left ) {
            die "DBH->finish called when session still has states left\n";
        }
    }
}

1;
