package Local::Class::DBI::SQL::Transformer;

use strict;
use warnings;

=head1 NAME

Local::Class::DBI::SQL::Transformer - Transform SQL

=head1 SYNOPSIS

  my $trans = $tclass->new($self, $sql, @args);
  return $self->SUPER::transform_sql($trans->sql => $trans->args);

=head1 DESCRIPTION

Local::Class::DBI hooks into the transform_sql() method in Ima::DBI to provide
its own SQL extensions. Local::Class::DBI::SQL::Transformer does the heavy
lifting of these transformations.

=head1 CONSTRUCTOR

=head2 new

  my $trans = $tclass->new($self, $sql, @args);

Create a new transformer for the SQL and arguments that will be used
with the given object (or class).

=cut

sub new {
	my ($me, $caller, $sql, @args) = @_;
	bless {
		_caller      => $caller,
		_sql         => $sql,
		_args        => [@args],
		_transformed => 0,
	} => $me;
}

=head2 sql / args

  my $sql = $trans->sql;
  my @args = $trans->args;

The transformed SQL and args.

=cut

# TODO Document what the different transformations are
# and factor out how they're called so that people can pick and mix the
# ones they want and add new ones.

sub sql {
	my $self = shift;
	$self->_do_transformation if !$self->{_transformed};
	return $self->{_transformed_sql};
}

sub args {
	my $self = shift;
	$self->_do_transformation if !$self->{_transformed};
	return @{ $self->{_transformed_args} };
}

sub _expand_table {
	my $self = shift;
	my ($class, $alias) = split /=/, shift, 2;
	my $caller = $self->{_caller};
	my $table = $class ? $class->table : $caller->table;
	$self->{cmap}{ $alias || $table } = $class || ref $caller || $caller;
	($alias ||= "") &&= " $alias";
	return $table . $alias;
}

sub _expand_join {
	my $self  = shift;
	my $joins = shift;
	my @table = split /\s+/, $joins;

	my $caller = $self->{_caller};
	my %tojoin = map { $table[$_] => $table[ $_ + 1 ] } 0 .. $#table - 1;
	my @sql;
	while (my ($t1, $t2) = each %tojoin) {
		my ($t1, $col) = split /\./, $t1;

		my $join_col = sub {
			my ($t1, $t2) = @_;

			my ($c1, $c2) = map $self->{cmap}{$_}
				|| $caller->_croak("Don't understand table '$_' in JOIN"), ($t1, $t2);

			# has_many can actually specify the foreign key column...

			# first look up has_many by name embedded in alias
			if ($t1 =~ /$t2\_(.*)/) {
				my $meta = $c2->meta_info('has_many' => $1);
				my ($col) = $meta->{'args'}->{'foreign_key'} if (defined $meta && $meta->{'args'}->{'foreign_key'});
				return $col if (defined $col);
			}

			# then might_have by name embedded in alias
			if ($t1 =~ /$t2\_(.*)/) {
				my $meta = $c2->meta_info('might_have' => $1);
				my ($col) = $meta->{'args'}->{'foreign_key'} if (defined $meta && $meta->{'args'}->{'foreign_key'});
				return $col if (defined $col);
			}

			# then has_a_mk by name embedded in alias
			if ($t2 =~ /$t1\_(.*)/) {
				my $meta = $c1->meta_info('has_a_mk' => $1);
				my ($col) = $meta->{'args'}->{'key'} if (defined $meta && $meta->{'args'}->{'key'});
				return $col if (defined $col);
			}

			# In self referential joins (e.g. parent/children in same table), these general fallback cases might reverse the intended relationship
			if ($c1 ne $c2) {
				# failing that, by class
				my $meta = $c2->meta_info('has_many');
				my ($col) = map { $meta->{$_}->args->{'foreign_key'} } grep $meta->{$_}->foreign_class eq $c1, keys %$meta;
				return $col if (defined $col);

				$meta = $c1->meta_info('has_a_mk');
				($col) = map { $meta->{$_}->args->{'key'} } grep $c2->isa($meta->{$_}->foreign_class), keys %$meta;
				return $col if (defined $col);

				# default to has_a data
				$meta = $c1->meta_info('has_a');
				($col) = grep $c2->isa($meta->{$_}->foreign_class), keys %$meta;
				return $col if (defined $col);
			}
		};

		unless (defined $col) {
			$col = $join_col->($t1 => $t2) || do {
				($t1, $t2) = ($t2, $t1);
				$join_col->($t1 => $t2);
			};
		}

		my ($c1, $c2) = map $self->{cmap}{$_}
			|| $caller->_croak("Don't understand table '$_' in JOIN"), ($t1, $t2);

		$caller->_croak("Don't know how to join $c1 to $c2") unless $col;
		if (ref($col) eq 'ARRAY') {
		  my @c2_pri = $c2->primary_columns;

		  # If c2 has multiple primaries, assume direct match to key
		  if (@c2_pri == @{$col}) {
		    my %key;
		    @key{@{$col}} = @c2_pri;
		    map { push @sql, sprintf " %s.%s = %s.%s ", $t1, $_, $t2, $key{$_} } keys %key;

		  # If c2 has one primary, find key referencing c2's type
		  } elsif (@c2_pri == 1) {
		    foreach my $c2_pri ($c2->primary_columns) {
		      my ($c1_col) = grep { $c1->meta_info('has_a' => $_)->{'foreign_class'} eq $c2 } @{$col};
		      push @sql, sprintf " %s.%s = %s.%s ", $t1, $c1_col, $t2, $c2->primary_column;
		    }
		  }
		} else {
		  push @sql, sprintf " %s.%s = %s.%s ", $t1, $col, $t2, $c2->primary_column;
		}
	}
	return join " AND ", @sql;
}

sub _expand_join_on {
	my $self = shift;
	my $join_type = shift;
	my ($key, $table) = split / /, shift;
	my $alias = (split /=/, $table)[1];
	# Remove foreign key that might be present in alias
	$table = (split /\./, $table)[0];

	# If $alias contains a foreign key, pass that first to _expand_join as it only expects a foreign key on the first table
    my $join = $alias =~ /\./ ? "$alias $key" : "$key $alias";
	return join(' ', $join_type,  $self->_expand_table($table), 'ON (', $self->_expand_join($join), ')');
}

sub _expand_joined_essential {
	my ($self, $class, $alias, $member) = @_;
	my $prefix = "j__${member}__";
	$prefix =~ s/\./\$/g;

	return join ", ", map qq{${alias}.$_ AS "${prefix}$_"}, $class->_essential;
}

sub _do_transformation {
	my $me     = shift;
	my $sql    = $me->{_sql};
	my @args   = @{ $me->{_args} };
	my $caller = $me->{_caller};

	$sql =~ s/__TABLE\(?(.*?)\)?__/$me->_expand_table($1)/eg;
	$sql =~ s/__JOIN\((.*?)\)__/$me->_expand_join($1)/eg;
	$sql =~ s/__IJOIN\(?(.*?)\)?__/$me->_expand_join_on('JOIN', $1)/eg;
	$sql =~ s/__LJOIN\(?(.*?)\)?__/$me->_expand_join_on('LEFT JOIN', $1)/eg;
	$sql =~ s/__ESSENTIAL__/join ", ", $caller->_essential/eg;
	$sql =~ s/__ESSENTIAL\((.*?)\)__/join ", ", map "$1.$_", $caller->_essential/eg;
	$sql =~ s/__JOINED_ESSENTIAL\(([^,]+?), ([^,]+?), ([^)]+)\)__/$me->_expand_joined_essential($1, $2, $3)/eg;

	if ($sql =~ /__IDENTIFIER__/) {
		my $key_sql = join " AND ", map "$_=?", $caller->primary_columns;
		$sql =~ s/__IDENTIFIER__/$key_sql/g;
	}

	$me->{_transformed_sql}  = $sql;
	$me->{_transformed_args} = [@args];
	$me->{_transformed}      = 1;
	return 1;
}

1;

