package DBIEasy;

use strict;
use Carp qw/carp croak/;
use DBI;
use Data::Dumper;

use HaCi::Conf qw/getConfigValue/;

my $dbHandles		= {};
my $initConf		= {};
our $lastError	= '';

BEGIN {
	no strict 'refs';
	foreach my $attr (qw/error errorStrs dbhost dbname dbuser dbpass dbtype PK dbh cols2Meths meths2Cols/) {
		*{$attr} = sub {
			my $proto		= shift;
			my $value		= shift;
			my $self		= undef;
			my $caller	= (caller)[0];
			
		  if (ref $proto) {
		  	$self	= $proto;
			  if (defined $value) {
				  $self->{$attr}	= $value;
			  } else {
			  	return $self->{$attr};
			  }
		  } else {
			  if (defined $value) {
				  $initConf->{$proto}->{$attr}	= $value;
			  } else {
			  	return $initConf->{$proto}->{$attr}	= $value;
			  }
		  }
		}
	}
}

sub new {
	my $proto		= shift;
	my $config	= shift;
	my $self		= undef;

	if (ref $proto) {
		$self	= $proto;
	} else {
		$self	= (ref $initConf->{$proto} eq 'HASH') ? $initConf->{$proto} : {};
		unless (ref $self eq 'HASH') {
			_carp("Database Error");
			return undef;
		}
		bless $self, $proto;
	}

	if (ref $config eq 'HASH') {
		$self->{CONFIG}	= $config;
	}

	return undef unless $self->init();
	
	my $dbh		= $self->getDBConn();
	
	if ($dbh) {
		DBI->trace(&getConfigValue('misc', 'dbitrace'));
		$self->dbh($dbh);
		if ($self->initChecks()) {
			$self->PK($self->getPK());
			$self->initSubRefs();
			return $self;
		}
	}
	return undef;
}

sub getDBConn {
	my $self	= shift;
	my $dbh		= undef;

	if ($dbHandles->{$$}->{$self->dbhost}->{$self->dbname}) {
		$dbh	= $dbHandles->{$$}->{$self->dbhost}->{$self->dbname};
		$self->_carp("Taking allready connected database handle: $dbh ($$: " . $self->TABLE() . ")") if 0;
		$self->dbh($dbh);
		unless ($dbh->ping) {
			$self->_carp("Mhh, that old handle doesn't smell well. Better taking a new one!");
			$dbh	= undef;
		}
	}
	unless (defined $dbh) {
		$dbh = DBI->connect($self->getDSN(),  {
			PrintError => 0,
			AutoCommit => 1
		});
		$self->_croak($DBI::errstr) unless $dbh;
		$self->checkForOldHandles($dbh) if defined $dbh;
	}
	$dbHandles->{$$}->{$self->dbhost}->{$self->dbname}	= $dbh;
	$self->dbh($dbh);
	return $dbh;
}

sub init {
	my $self	= shift;

	foreach ('dbhost', 'dbname', 'dbuser', 'dbpass', 'dbtype') {
		my $key	= $_;
		$self->{CONFIG}->{$key}	= 'mysql' if $key eq 'dbtype' && !defined $self->{CONFIG}->{$key};
		unless ($self->{$key}) {
			if ($self->{CONFIG}->{$key}) {
				$self->$key($self->{CONFIG}->{$key});
			} else {
				$self->_carp("No '$key' defined");
				return 0;
			}
		}
	}
	return 1;
}

sub mapColNames {
	my $self					= shift;
	my @columns				= @_;
	my @mappedColumns	= ();
	my $col2Meth			= $self->col2Meth();

	return @columns	unless $self->can('columnMapping');

	my $columnMapping	= $self->columnMapping();
	die "columnMapping in " . ref($self) . " is not a valid hash!" unless ref($columnMapping) eq 'HASH';

	foreach (keys %{$columnMapping}) {
		$col2Meth->{$columnMapping->{$_}}	= $_;
	}

	map {
		$_ = $col2Meth->{$_} if exists $col2Meth->{$_} && $col2Meth->{$_};
	} @columns;

	$self->cols2Meths($col2Meth);
	$self->meths2Cols($columnMapping);

	return @columns;
}

sub meth2Col {
	my $self				= shift;
	my $meth				= shift;
	my $meths2Cols	= $self->meths2Cols();

	return (exists $meths2Cols->{$meth}) ? $meths2Cols->{$meth} : $meth;
}

sub col2Meth {
	my $self				= shift;
	my $col					= shift;
	my $cols2Meths	= $self->cols2Meths();

	return (exists $cols2Meths->{$col}) ? $cols2Meths->{$col} : $col;
}

sub initSubRefs {
	my $self		= shift;

	no strict 'refs';
	foreach ($self->mapColNames($self->getColoumns())) {
		my $col	= $_;
		next unless $col;

		unless ($self->can($col)) {
			*{(ref $self) . "::$col"} = sub {
				my $self	= shift;
				my $value	= shift;
				if (defined $value) {
					$self->{COLOUMNS}->{$col}	= $value;
				} else {
					$self->{COLOUMNS}->{$col}	= undef;
				}
			}
		}
	}
}

sub initChecks {
	my $self	= shift;
	my $dbh		= $self->dbh;
	my $error	= 0;
	
	$error	= 1 unless $self->checkIfTableExists();
	
	return ($error == 0) ? 1 : 0;
}

sub clear {
	my $self	= shift;
	foreach (keys %{$self->{COLOUMNS}}) {
		$self->$_(undef);
	}
	$self->error(0);
	$self->errorStrs('');
}

sub count {
	my $self			= shift;
  my $qryRestr	= shift || 0;
  my $bLike			= shift || 0;
	my $appStr		= shift || 0;
	my $distinct	= shift || 0;

	return $self->search('%COUNT%', $qryRestr, $bLike, $appStr, $distinct);
}

sub search {
	my $self			= shift;
  my $qryCols		= shift || ['*'];
  my $qryRestr	= shift || 0;
  my $bLike			= shift || 0;
	my $appStr		= shift || 0;
	my $distinct	= shift || 0;
  my $pk				= $self->PK;
	my $dbh				= $self->dbh;
	my $table			= $self->TABLE;
  my @rows			= ();
  my @whereStr	= ();
  my @whereVals	= ();
	my $con				= 'AND';	
	my $cs				= '';
	my $count			= 0;

	if (ref($qryCols) eq '' && $qryCols eq '%COUNT%') {
		$qryCols	= [];
		$count		= 1;
	}

	# map db method to db column
	foreach (@{$qryCols}) {
		$_ = $self->meth2Col($_)
	}
 
	unless ($count) {
		foreach (@$pk) {
			my $pkt	= $_;
			push @$qryCols, $pkt unless grep {/^$pkt$/} @$qryCols;
		}
	}

 	if ($qryRestr) { 
	  if (ref $qryRestr eq 'HASH') {
		  foreach (keys %{$qryRestr}) {
				if ($_ eq '%CON%') {
					$con	= $qryRestr->{$_};
				}
				elsif ($_ eq '%CS%') {
					$cs		= $qryRestr->{$_};
				} else {
			 	 	push @whereStr, $self->meth2Col($_) . (($bLike) ? ' LIKE ' : ' = ') . '?';
					push @whereVals, $qryRestr->{$_};
				}
		  }
	  }
	  elsif (ref $qryRestr eq '') {
	  	$whereStr[0]	= $qryRestr;
	  }
 	}
	@whereStr	= $self->mkCaseSensitive(@whereStr) if $cs;

  my $prStr			= 
		"SELECT " . 
		(($count) ? 'COUNT(*) as cnt' : '') . 
		(($distinct) ? 'DISTINCT ' : '') . 
		join(',', @$qryCols) . 
		' FROM ' . $dbh->quote_identifier($table) .
		(($#whereStr > -1) ? 
			' WHERE ' . join(" $con ", @whereStr) : 
			''
		);
	$prStr	.= ' ' . $appStr if $appStr;

	warn "SEARCH: " . join('', split(/\n/, $prStr)) . " < @whereVals" if 0;
	
  my $sth				= $dbh->prepare($prStr);
  $self->_carp($dbh->errstr()) if $dbh->errstr();
 
	$sth->trace('SQL') if 0;
  $sth->execute(@whereVals);
  $self->_carp($dbh->errstr()) if $dbh->errstr();
  
	if ($count) {
		my $cnter	= $sth->fetchall_arrayref();
		return ${${$cnter}[0]}[0];
	}

  my $hashRef = $sth->fetchall_hashref($pk);
  $self->_carp($dbh->errstr()) if $dbh->errstr();

	my $pkCnter	= 1;
	push @rows, $self->getRows($hashRef, $pkCnter);
  
	warn "RETURN: " . Dumper(@rows) if 0;

  return @rows;
}

sub getRows {
	my $self		= shift;
	my $hashRef	= shift;
	my $pkCnter	= shift;
	my @rows		= ();

	return @rows unless ref $hashRef eq 'HASH';

  foreach (keys %{$hashRef}) {
		my $cPK	= $_;
		if ($pkCnter < ($#{$self->{PK}} + 1)) {
			push @rows, $self->getRows($hashRef->{$cPK}, ($pkCnter + 1));
		} else {
			my $mappedHash	= {};
			foreach (keys %{$hashRef->{$cPK}}) {
				my $col	= $_;
				$mappedHash->{$self->col2Meth($col)}	= $hashRef->{$cPK}->{$col};
			}
			push @rows, $mappedHash;
		}
  }

	return @rows;
}

sub insert {
	my $self	= shift;
	
	return $self->modify('INSERT');
}

sub update {
	my $self			= shift;
	my $rowRestr	= shift;
	my $bLike			= shift;
	my $appStr		= shift || 0;
	
	return $self->modify('UPDATE', $rowRestr, $bLike, $appStr);
}

sub delete {
	my $self			= shift;
	my $rowRestr	= shift;
	my $bLike			= shift;
	my $appStr		= shift || 0;
	
	return $self->modify('DELETE', $rowRestr, $bLike, $appStr);
}

sub modify {
	my $self			= shift;
	my $type			= shift;
  my $rowRestr	= shift || 0;
  my $bLike			= shift || 0;
	my $appStr		= shift || 0;
	my $dbh				= $self->dbh;
	my $table			= $self->TABLE;
	my $col2Meth	= $self->col2Meth();
  my @whereStr	= ();
  my @whereVals	= ();
	my $modStr		= $type;
	my $con				= 'AND';
	$modStr			 .= ' INTO' if $type eq 'INSERT';
	$modStr			 .= ' FROM' if $type eq 'DELETE';
	$modStr			 .= ' ' . $dbh->quote_identifier($table);
	$modStr			 .= ' SET' if $type eq 'UPDATE';
	
 	if ($rowRestr) { 
	  if (ref $rowRestr eq 'HASH') {
		  foreach (keys %{$rowRestr}) {
				my $row	= $_;
				if ($row eq 'CON') {
					$con	= $rowRestr->{CON};
				} else {
			 	 	push @whereStr, $self->meth2Col($row) . (($bLike) ? ' LIKE ' : ' = ') . '?';
					push @whereVals, $rowRestr->{$row};
				}
		  }
	  }
	  elsif (ref $rowRestr eq '') {
	  	$whereStr[0]	= $rowRestr;
	  }
 	}
  
	$self->_log("$type in $table") if 0;
	
	my @cols	= ();
	my @vals	= ();
	foreach (keys %{$self->{COLOUMNS}}) {
		my $value	= $self->{COLOUMNS}->{$_};

		if (defined $value) {
			my $col	= $self->meth2Col($_);
			if ($type eq 'UPDATE') {
				$modStr	.= ',' if $modStr !~ / SET$/;
				$modStr	.= " $col = ?";
			}
			unless ($type eq 'DELETE') {
				push @cols, $col;
				push @vals, $value;
			}		
		}
	}
	if ($type eq 'INSERT') {
		$modStr	.= ' (' . join(', ', @cols) . ') VALUES (' . join(', ', map {'?'} (1 .. scalar @vals)) . ')'
	}

	$modStr	.= ' WHERE ' . join(" $con ", @whereStr) if $#whereStr > -1;
	$modStr	.= ' ' . $appStr if $appStr;
	
	warn "DB-UPDATE: $modStr (" . join(', ', @vals) . '; ', join(', ', @whereVals) if 0;
  my $sth		= $dbh->prepare($modStr);
  $self->_carp($dbh->errstr()) if $dbh->errstr();
 
	$sth->trace('SQL') if 0;
  my $rows	= $sth->execute(@vals, @whereVals);
  $self->_carp($dbh->errstr()) if $dbh->errstr();

  return $rows;
}

sub alter {
	my $self		= shift;
	my $altStr	= shift;
	my $dbh			= $self->dbh;

	warn $altStr if 0;
	my $rows	= $dbh->do($altStr);
  $self->_carp($dbh->errstr()) if $dbh->errstr();
  return $rows;
}

sub _log {
	my $self	= shift,
	my @msg		= @_;

	$self->_carp(@_);
}

sub _carp {
	my $self							= shift;
	my ($message, %info)	= @_;
	if (UNIVERSAL::isa($self, 'UNIVERSAL')) {
		$self->error(1);
		$self->errorStrs($message);
		$lastError	.= $message;
		warn "DB " . 
			(ref $self) . 
			' [' . (caller(4))[3] . 
			'->' . (caller(3))[3] . 
			'->' . (caller(2))[3] . ':' . (caller(1))[2] . 
			'->' . (caller(1))[3] . ':' . (caller(0))[2] .
			"]: $message";
	} else {
		Carp::carp($message || $self);
	}
	return;
}

sub _croak {
	my $self							= shift;
	my ($message, %info)	= @_;
	chomp($message);
	if (UNIVERSAL::isa($self, 'UNIVERSAL')) {
		$self->error(1);
		$self->errorStrs($message);
		$lastError	.= $message;
		warn "DB " . (ref $self) . ": $message\n";
	} else {
		Carp::croak($message || $self);
	}
	return;
}

END {
	foreach (keys %{$dbHandles}) {
		my $host	= $_;
		foreach (keys %{$dbHandles->{$$}->{$host}}) {
			my $name	= $_;
			if ($dbHandles->{$$}->{$host}->{$name}) {
				_carp("Disconnecting from DB $host:$name") if 0;
				$dbHandles->{$$}->{$host}->{$name}->disconnect();
			}
		}
	}
}

1;

# vim:ts=2:sts=2:sw=2
