package DBIx::Class::SQL::Abstract;

use strict;
use warnings;

# Many thanks to SQL::Abstract, from which I stole most of this

sub _debug { }

sub _cond_resolve {
  my ($self, $cond, $attrs, $join) = @_;
  $cond = $self->_anoncopy($cond);   # prevent destroying original
  my $ref   = ref $cond || '';
  $join   ||= $attrs->{logic} || ($ref eq 'ARRAY' ? 'OR' : 'AND');
  my $cmp   = uc($attrs->{cmp}) || '=';

  # For assembling SQL fields and values
  my(@sqlf) = ();

  # If an arrayref, then we join each element
  if ($ref eq 'ARRAY') {
    # need to use while() so can shift() for arrays
    my $subjoin;
    while (my $el = shift @$cond) {
      
      # skip empty elements, otherwise get invalid trailing AND stuff
      if (my $ref2 = ref $el) {
        if ($ref2 eq 'ARRAY') {
          next unless @$el;
        } elsif ($ref2 eq 'HASH') {
          next unless %$el;
          $subjoin ||= 'AND';
        } elsif ($ref2 eq 'SCALAR') {
          # literal SQL
          push @sqlf, $$el;
          next;
        }
        $self->_debug("$ref2(*top) means join with $subjoin");
      } else {
        # top-level arrayref with scalars, recurse in pairs
        $self->_debug("NOREF(*top) means join with $subjoin") if $subjoin;
        $el = {$el => shift(@$cond)};
      }
      my @ret = $self->_cond_resolve($el, $attrs, $subjoin);
      push @sqlf, shift @ret;
    }
  }
  elsif ($ref eq 'HASH') {
    # Note: during recursion, the last element will always be a hashref,
    # since it needs to point a column => value. So this be the end.
    for my $k (sort keys %$cond) {
      my $v = $cond->{$k};
      if ($k =~ /^-(.*)/) {
        # special nesting, like -and, -or, -nest, so shift over
        my $subjoin = $self->_modlogic($attrs, uc($1));
        $self->_debug("OP(-$1) means special logic ($subjoin), recursing...");
        my @ret = $self->_cond_resolve($v, $attrs, $subjoin);
        push @sqlf, shift @ret;
      } elsif (! defined($v)) {
        # undef = null
        $self->_debug("UNDEF($k) means IS NULL");
        push @sqlf, $self->_cond_key($attrs => $k) . ' IS NULL'
      } elsif (ref $v eq 'ARRAY') {
        # multiple elements: multiple options
        # warnings... $self->_debug("ARRAY($k) means multiple elements: [ @$v ]");
        
        # special nesting, like -and, -or, -nest, so shift over
        my $subjoin = 'OR';
        if ($v->[0] =~ /^-(.*)/) {
          $subjoin = $self->_modlogic($attrs, uc($1));    # override subjoin
          $self->_debug("OP(-$1) means special logic ($subjoin), shifting...");
          shift @$v;
        }

        # map into an array of hashrefs and recurse
        my @ret = $self->_cond_resolve([map { {$k => $_} } @$v], $attrs, $subjoin);
        
        # push results into our structure
        push @sqlf, shift @ret;        
      } elsif (ref $v eq 'HASH') {
        # modified operator { '!=', 'completed' }
        for my $f (sort keys %$v) {
          my $x = $v->{$f};
          $self->_debug("HASH($k) means modified operator: { $f }");

          # check for the operator being "IN" or "BETWEEN" or whatever
          if (ref $x eq 'ARRAY') {
            if ($f =~ /^-?\s*(not[\s_]+)?(in|between)\s*$/i) {
              my $mod = $1 ? $1 . $2 : $2;  # avoid uninitialized value warnings
              my $u = $self->_modlogic($attrs, uc($mod));
              $self->_debug("HASH($f => $x) uses special operator: [ $u ]");
              if ($u =~ /BETWEEN/) {
                # SQL sucks
                $self->throw( "BETWEEN must have exactly two arguments" ) unless @$x == 2;
                push @sqlf, join ' ',
                              $self->_cond_key($attrs => $k), $u,
                              $self->_cond_value($attrs => $k => $x->[0]),
                              'AND',
                              $self->_cond_value($attrs => $k => $x->[1]);
              } else {
                push @sqlf, join ' ', $self->_cond_key($attrs, $k), $u, '(',
                        join(', ',
                          map { $self->_cond_value($attrs, $k, $_) } @$x),
                      ')';
              }
            } else {
              # multiple elements: multiple options
              $self->_debug("ARRAY($x) means multiple elements: [ @$x ]");
  
              # map into an array of hashrefs and recurse
              my @ret = $self->_cond_resolve([map { {$k => {$f, $_}} } @$x], $attrs);

              # push results into our structure
              push @sqlf, shift @ret;              
            }
          } elsif (! defined($x)) {
            # undef = NOT null
            my $not = ($f eq '!=' || $f eq 'not like') ? ' NOT' : '';
            push @sqlf, $self->_cond_key($attrs => $k) . " IS${not} NULL";
          } else {
            # regular ol' value
            $f =~ s/^-//;   # strip leading -like =>
            $f =~ s/_/ /;   # _ => " "
            push @sqlf, join ' ', $self->_cond_key($attrs => $k), uc($f),
                          $self->_cond_value($attrs => $k => $x);
          }
        }
      } elsif (ref $v eq 'SCALAR') {
        # literal SQL
        $self->_debug("SCALAR($k) means literal SQL: $$v");
        push @sqlf, join ' ', $self->_cond_key($attrs => $k), $$v;
      } else {
        # standard key => val
        $self->_debug("NOREF($k) means simple key=val: $k ${cmp} $v");
        push @sqlf, join ' ', $self->_cond_key($attrs => $k), $cmp,
                      $self->_cond_value($attrs => $k => $v);
      }
    }
  }
  elsif ($ref eq 'SCALAR') {
    # literal sql
    $self->_debug("SCALAR(*top) means literal SQL: $$cond");
    push @sqlf, $$cond;
  }
  elsif (defined $cond) {
    # literal sql
    $self->_debug("NOREF(*top) means literal SQL: $cond");
    push @sqlf, $cond;
  }

  # assemble and return sql
  my $wsql = @sqlf ? '( ' . join(" $join ", @sqlf) . ' )' : '1 = 1';
  return wantarray ? ($wsql, @{$attrs->{bind} || []}) : $wsql; 
}

sub _cond_key {
  my ($self, $attrs, $key) = @_;
  return $key;
}

sub _cond_value {
  my ($self, $attrs, $key, $value) = @_;
  push(@{$attrs->{bind}}, $value);
  return '?';
}

# Anon copies of arrays/hashes
sub _anoncopy {
  my ($self, $orig) = @_;
  return (ref $orig eq 'HASH' ) ? { %$orig }
     : (ref $orig eq 'ARRAY') ? [ @$orig ]
     : $orig;     # rest passthru ok
}

sub _modlogic {
  my ($self, $attrs, $sym) = @_;
  $sym ||= $attrs->{logic};
  $sym =~ tr/_/ /;
  $sym = $attrs->{logic} if $sym eq 'nest';
  return uc($sym);  # override join
}
  
1;

=head1 NAME 

DBIx::Class::SQL::Abstract - SQL::Abstract customized for DBIC.

=head1 SYNOPSIS

=head1 DESCRIPTION

This is a customized version of L<SQL::Abstract> for use in 
generating L<DBIx::Searchbuilder> searches.

=cut

=head1 AUTHORS

Matt S. Trout <perl-stuff@trout.me.uk>

=head1 LICENSE

You may distribute this code under the same terms as Perl itself.

=cut
