package ID3FS::Path;

use strict;
use warnings;
use ID3FS::PathElement::Artist;
use ID3FS::PathElement::Album;
use ID3FS::PathElement::Boolean;
use ID3FS::PathElement::File;
use ID3FS::PathElement::Tag;
use ID3FS::PathElement::Tagval;
use ID3FS::Path::Node;

our ($STATE_INVALID, $STATE_ROOT, $STATE_TAG, $STATE_TAGVAL,
     $STATE_BOOLEAN, $STATE_ALBUMS, $STATE_TRACKLIST,
     $STATE_FILE)=(0..7);

our %priorities=( "OR" => 0, "AND" => 1, "NOT" => 2 );

sub new
{
    my $proto=shift;
    my $class=ref($proto) || $proto;
    my $self={};
    bless($self,$class);

    $self->{elements}=[];
    $self->{db}=shift;
    $self->{path}=shift;
    $self->parse();
#    print "STATE: ", $self->state(), "\n";
    return $self;
}

sub isdir
{
    my($self)=@_;
    if(($self->state() == $STATE_FILE) ||
       ($self->state() == $STATE_INVALID))
    {
	return 0;
    }
    return 1;
}

sub isfile
{
    my($self)=@_;
    return 1 if($self->state() == $STATE_FILE);
    return 0;
}

sub isvalid
{
    my($self)=@_;
    return($self->state() != $STATE_INVALID);
}

sub dest
{
    my($self, $mountpoint)=@_;
    if($self->state() == $STATE_FILE)
    {
	return $self->filename($mountpoint, $self);
    }
    return "ERROR"; #should never happen?
}

sub dirents
{
    my($self)=@_;
    my @dents=();
    my $state=$self->state();
#    print "DIRENTS: STATE: $state\n";
#    print "DIRENTS: FILE: $self->{path}\n";
    if($state==$STATE_TAG || $state==$STATE_TAGVAL)
    {
	my $tag=$self->tail();
	if($state==$STATE_TAG &&
	   defined($tag) &&
	   ref($tag) eq "ID3FS::PathElement::Tag" &&
	   $self->{db}->tag_has_values($tag->{id}))
	{
	    @dents=$self->tags();
	}
	else
	{
	    @dents=(qw(AND OR TRACKS NOARTIST),
		    $self->artists());
	}
    }
    elsif($state==$STATE_BOOLEAN)
    {
	my $parent=$self->tail();
	unless(defined($parent) &&
	       ref($parent) eq "ID3FS::PathElement::Boolean" &&
	       $parent->{name} eq "NOT")
	{
	    push(@dents, "NOT");
	}
	push(@dents, $self->tags());
    }
    elsif($state==$STATE_ROOT)
    {
	@dents=(qw(ALL NOT), $self->tags());
    }
    elsif($state==$STATE_ALBUMS)
    {
	@dents=(qw(TRACKS NOALBUM),$self->albums());
    }
    elsif($state==$STATE_TRACKLIST)
    {
	@dents=$self->tracks();
    }
    else
    {
	print "DIRENTS: UNHANDLED STATE: $state\n";
    }
    return(@dents);
}

sub parse
{
    my($self)=@_;
    @{$self->{components}}=split(/\//, $self->{path});
    shift @{$self->{components}}; # drop empty field before leading /
#    print "PATH: $self->{path}\n";
    $self->state($STATE_ROOT);
    return if($self->{path} eq "/");
    my @parts=@{$self->{components}};
    my($tag, $tagval);
    $self->{elements}=[];
    while(my $name=shift @parts)
    {
#	print "NAME: $name\n";
	my $state=$self->state();
	if($state==$STATE_INVALID)
	{
#	    print "SM: INVALID: $name\n";
	    return;
	}
	elsif($state==$STATE_ROOT)
	{
#	    print "SM: ROOT: $name\n";
	    if($name eq "ALL")
	    {
		$self->state($STATE_TAG);
	    }
	    elsif($name eq "NOT")
	    {
	        push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
		$self->state($STATE_BOOLEAN);
	    }
	    else
	    {
		$tag=ID3FS::PathElement::Tag->new($self->{db}, $name);
		if($tag)
		{
		    push(@{$self->{elements}}, $tag);
		    $self->state($STATE_TAG);
		}
		else
		{
		    $self->state($STATE_INVALID);
		}
	    }
	}
	elsif($state==$STATE_TAG || $state==$STATE_TAGVAL)
	{
#	    print "SM: TAG/TAGVAL($state): $name\n";
	    my $tag=$self->tail();
	    if($state==$STATE_TAG &&
	       defined($tag) &&
	       ref($tag) eq "ID3FS::PathElement::Tag" &&
	       $self->{db}->tag_has_values($tag->{id}))
	    {
#		print "Parsing: parent: $tag->{id}\n";
		my $tagval=ID3FS::PathElement::Tag->new($self->{db}, $name, $tag->{id});
		if(defined($tagval))
		{
		    $self->state($STATE_TAGVAL);
		    # stay in tag state
		    push(@{$self->{elements}}, $tagval);
		}
		else
		{
		    $self->state($STATE_INVALID);
		}
	    }
	    elsif($name eq "TRACKS")
	    {
		$self->state($STATE_TRACKLIST);
	    }
	    elsif($name eq "NOARTIST")
	    {
		$self->state($STATE_TRACKLIST);
	    }
	    elsif($name eq "AND")
	    {
		$self->state($STATE_BOOLEAN);
	        push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
	    }
	    elsif($name eq "OR")
	    {
		$self->state($STATE_BOOLEAN);
	        push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
	    }
	    else
	    {
		my $artist=ID3FS::PathElement::Artist->new($self->{db}, $name);
		if($artist)
		{
		    push(@{$self->{elements}}, $artist);
		    $self->state($STATE_ALBUMS);
		}
		else
		{
		    $self->state($STATE_INVALID);
		}
	    }
	}
	elsif($state==$STATE_BOOLEAN)
	{
#	    print "SM: BOOLEAN: $name\n";
	    my $parent=$self->tail();
	    my $allownot=1;
	    if(defined($parent) &&
	       ref($parent) eq "ID3FS::PathElement::Boolean" &&
	       $parent->{name} eq "NOT")
	    {
		$allownot=0;
	    }
	    if($allownot && $name eq "NOT")
	    {
		$self->state($STATE_BOOLEAN);
	        push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
	    }
	    else
	    {
		my $tag=ID3FS::PathElement::Tag->new($self->{db}, $name);
		if($tag)
		{
		    push(@{$self->{elements}}, $tag);
		    $self->state($STATE_TAG);
		}
		else
		{
		    $self->state($STATE_INVALID);
		}
	    }
	}
	elsif($state==$STATE_ALBUMS)
	{
#	    print "SM: ALBUM: $name\n";
	    if($name eq "TRACKS")
	    {
		$self->state($STATE_TRACKLIST);
	    }
	    elsif($name eq "NOALBUM")
	    {
		$self->state($STATE_TRACKLIST);
	    }
	    else
	    {
		my $album=ID3FS::PathElement::Album->new($self->{db}, $name);
		if($album)
		{
		    push(@{$self->{elements}}, $album);
		    $self->state($STATE_TRACKLIST);
		}
		else
		{
		    $self->state($STATE_INVALID);
		}
	    }
	}
	elsif($state==$STATE_TRACKLIST)
	{
#	    print "SM: TRACKLIST: $name\n";
	    my $track=ID3FS::PathElement::File->new($self->{db}, $name);
	    if($track)
	    {
		push(@{$self->{elements}}, $track);
		$self->state($STATE_FILE);
	    }
	    else
	    {
		$self->state($STATE_INVALID);
	    }
	}
	elsif($state==$STATE_FILE)
	{
#	    print "SM: FILE: $name\n";
	    # Can't have anything after a filename
	    $self->state($STATE_INVALID);
	}
	else
	{
	    print "SM: ERROR: UNKNOWN STATE: $self->{state}\n";
	    $self->state($STATE_INVALID);
	}
    }
    # remove trailing boolean
    my @elements=@{$self->{elements}};
    while(@elements && ref($elements[$#elements]) eq "ID3FS::PathElement::Boolean")
    {
	pop @elements;
    }
    # sort elements by precedence
    @elements=$self->sort_elements(@elements);
    $self->{tagtree}=$self->elements_to_tree(\@elements);
    if($self->{tagtree})
    {
	($self->{sqlconditions},
	 $self->{andsneeded}) = $self->{tagtree}->to_sql();
#	print "TREE: ",  $self->{tagtree}->print(), "\n";
#	print("SQL CONDITION(", $self->{andsneeded}, "): ",
#	      $self->{sqlconditions}, "\n");
#	use Data::Dumper;
#	print Dumper $self->{tagtree};
    }
}

sub state
{
    my($self, $newstate)=@_;
    $self->{state}=$newstate if(defined($newstate));
    return $self->{state};
}

sub elements_to_tree
{
    my($self, $elements)=@_;
    return undef unless(@$elements);
    my ($left, $right, $op)=(undef, undef, undef);
    my $thing=pop @$elements;
    if(ref($thing) eq "ID3FS::PathElement::Boolean")
    {
	my $op=$thing;
	$right=$self->elements_to_tree($elements);
	if($op->{name} ne "NOT")
	{
	    $left=$self->elements_to_tree($elements);
	}
	return ID3FS::Path::Node->new($left, $op, $right);
    }
    else
    {
	return ID3FS::Path::Node->new($thing);
    }
}

# Dijkstra's shunting-yard algorithm
sub sort_elements
{
    my ($self, @input)=@_;
    my @opstack=();
    my @output=();
#    print "INPUT: ", join(', ', map { $_->{name}; } @input), "\n";
    while(my $thing = shift @input)
    {
	if(ref($thing) eq "ID3FS::PathElement::Tag")
	{
	    # Handle tag values by dropping parent
	    if(@input && ref($input[0]) eq "ID3FS::PathElement::Tag")
	    {
		$thing=shift @input;
	    }
	    push(@output, $thing);
	}
	elsif(ref($thing) eq "ID3FS::PathElement::Boolean")
	{
	    # bool
	    while(@opstack &&
		  ($priorities{$thing->{name}} <= $priorities{$opstack[$#opstack]->{name}}))
	    {
		push(@output, pop(@opstack));
	    }
	    push(@opstack, $thing);
	}
    }
    while(@opstack)
    {
	push(@output, pop(@opstack));
    }
#    print "STACK: ", join(', ', map { $_->{name}; } @output), "\n";
    return @output;
}

sub used_tags
{
    my($self)=@_;
    print "TAGTREE UNDEF\n" unless(defined($self->{tagtree}));
    return undef unless(defined($self->{tagtree}));
    return($self->{tagtree}->used_tags());
}

sub tag_has_values
{
    my($self)=@_;
    my $tail=$self->tail();
    if($tail && ref($tail) eq "ID3FS::PathElement::Tag")
    {
	return($self->{db}->tag_has_values($tail->{id}));
    }
}

sub trailing_tag_id
{
    my($self)=@_;
    my $tail=$self->tail();
    if($tail && ref($tail) eq "ID3FS::PathElement::Tag")
    {
	return($tail->{id});
    }
    return undef;
}

sub trailing_tag_parent
{
    my($self)=@_;
    my $tail=$self->tail();
    if($tail && ref($tail) eq "ID3FS::PathElement::Tag")
    {
	return($tail->{parents_id});
    }
    return undef;
}

sub tail
{
    my($self)=@_;
    return($self->{elements}->[$#{$self->{elements}}]);
}

# the one before last
sub tail_parent
{
    my($self)=@_;
    return($self->{elements}->[($#{$self->{elements}}) - 1]);
}

######################################################################

sub tags
{
    my($self)=@_;
    if(!@{$self->{elements}}) # /
    {
	my $sql="SELECT DISTINCT name FROM tags WHERE parents_id='';";
	return($self->{db}->cmd_firstcol($sql));
    }
    my $hasvals=$self->tag_has_values();
    my $parent=$self->trailing_tag_parent();
#    print "THASVALS: $hasvals\n";
#    print "TPARENT: ", (defined($parent)? $parent : "NO"), "\n";
    my @ids=();
    my $sql=("SELECT tags.name FROM (\n" .
	     $self->tags_subselect() .
	     ") AS subselect\n" .
	     "INNER JOIN files_x_tags ON subselect.files_id=files_x_tags.files_id\n" .
	     "INNER JOIN tags ON files_x_tags.tags_id=tags.id\n");
    my (@allused)=$self->used_tags();
    my @used=grep { ref($_) ne "ARRAY"; } @allused;
    my @used_with_vals=grep { ref($_) eq "ARRAY"; } @allused;
#    print "tags(): USED: ", join(", ", @used), "\n";
#    print "tags(): USED_WITH_VALS: ", join(", ", map { "[".$_->[0]. ", ".$_->[1]."]";} @used_with_vals), "\n";
    my @orclauses=();
    my @andclauses=();
    my $id=$self->trailing_tag_id();
    if($hasvals)
    {
#	print "HAS_VALUES\n";
	my @values=map { "'".$_->[1]."'"; } grep { $_->[0] == $id; } @used_with_vals;
	my $clause="(tags.parents_id='$id'";
	if(@values)
	{
	    $clause .= " AND tags.id NOT IN (" . join(', ', @values) . ")";
	}
	$clause .= ")";
	push(@orclauses, $clause);
    }
    else
    {
#	print "HASNT VALUES\n";;
	if(@used)
	{
	    push(@orclauses, "(NOT (tags.parents_id='' AND tags.id IN (" . join(', ', @used) . ")))");
	}
	for my $pair (@used_with_vals)
	{
	    push(@orclauses, "(NOT (tags.parents_id='" . $pair->[0] . "' AND tags.id='" . $pair->[1] . "'))");
	}
    }

    my $parentclause= "(tags.parents_id='";
    if($hasvals)
    {
	$parentclause .= $id;
    }
    elsif($parent)
    {
	$parentclause .= $parent;
    }
    $parentclause .= "')";
    push(@andclauses, $parentclause);

    if(@orclauses)
    {
	push(@andclauses, '( ' . join(' OR ', @orclauses) . ' )');
    }
    if(@andclauses)
    {
	$sql .= "WHERE " . join(' AND ', @andclauses) . "\n";
    }
    $sql .= "GROUP BY tags.name;";
    print "SQL: $sql\n";
    my @tagnames=$self->{db}->cmd_firstcol($sql);
    print "SUBNAMES: ", join(', ', @tagnames), "\n";
    return(@tagnames);
}

sub tag_values
{
    my($self, $tagid)=@_;
    my $sql=("SELECT DISTINCT name FROM tags\n" .
	     "WHERE parents_id=?");
    my @tags=$self->{db}->cmd_firstcol($sql, $tagid);
    @tags=map { length($_) ? $_ : "NOVALUE"; } @tags;
    return @tags;
}

sub artists
{
    my($self)=@_;
    if(!@{$self->{elements}}) # /ALL
    {
	my $sql="SELECT DISTINCT name FROM artists;";
	return($self->{db}->cmd_firstcol($sql));
    }
    my @ids=();
    my $sql=("SELECT artists.name FROM (\n" .
	     $self->tags_subselect() .
	     ") AS subselect\n" .
	     "INNER JOIN files ON subselect.files_id=files.id\n" .
	     "INNER JOIN artists ON files.artists_id=artists.id\n" .
	     "GROUP BY artists.name;");
    print "SQL: $sql\n";
    my @tagnames=$self->{db}->cmd_firstcol($sql);
    print "ARTISTS: ", join(', ', @tagnames), "\n";
    return(@tagnames);
}

sub albums
{
    my($self)=@_;
    my @ids=();
    my $tail=$self->tail();
    # FIXME: rework PathElements
    if(ref($tail) eq "ID3FS::PathElement::Artist")
    {
	return $self->artist_albums($tail->{id});
    }
    my $sql=("SELECT albums.name\n" .
	     "\tFROM (\n" .
	     $self->tags_subselect() .
	     "\t) AS subselect\n" .
	     "INNER JOIN files ON subselect.files_id=files.id\n" .
	     "INNER JOIN albums ON files.albums_id=albums.id\n" .
	     "GROUP BY albums.name;");
    print "SQL(ALBUMS): \n$sql\n";
    my @names=$self->{db}->cmd_firstcol($sql);
    print "ALBUMS: ", join(', ', @names), "\n";
    return(@names);
}

sub artist_albums
{
    my($self, $artist_id)=@_;
    my $sql=("SELECT albums.name FROM (\n" .
	     $self->tags_subselect() .
	     "\t) AS subselect\n" .
	     "INNER JOIN files ON subselect.files_id=files.id\n" .
	     "INNER JOIN albums ON albums.id=files.albums_id\n\t" .
	     "INNER JOIN artists ON artists.id=files.artists_id\n\t" .
	     "WHERE artists.id=? and albums.name <> ''\n\t" .
	     "GROUP BY albums.name\n");
    print "ARTIST_ALBUMS SQL: $sql\n";
    my @albums=$self->{db}->cmd_firstcol($sql, $artist_id);
    print "ALBUMS: ", join(', ', @albums), "\n";
    return(@albums);
}

sub artist_tracks
{
    my($self, $artist_id)=@_;
    my $sql=("SELECT files.name FROM (\n" .
	     $self->tags_subselect() .
	     "\t) AS subselect\n" .
	     "INNER JOIN files ON subselect.files_id=files.id\n" .
	     "INNER JOIN artists ON artists.id=files.artists_id\n\t" .
	     "INNER JOIN albums  ON albums.id=files.albums_id\n\t" .
	     "WHERE artists.id=? AND albums.name=''\n\t" .
	     "GROUP BY files.name\n");
    print "ARTIST_TRACKS SQL: $sql\n";
    my @names=$self->{db}->cmd_firstcol($sql, $artist_id);
    print "ARTISTTRACKS: ", join(', ', @names), "\n";
    return(@names);
}

sub album_tracks
{
    my($self, $artist_id, $album_id)=@_;
    my $sql=("SELECT files.name FROM files\n\t" .
	     "INNER JOIN albums  ON albums.id=files.albums_id\n\t" .
	     "INNER JOIN artists ON artists.id=files.artists_id\n\t" .
	     "WHERE artists.id=? AND albums.id=?\n\t" .
	     "GROUP BY files.name\n");
    print "ALBUM_TRACKS SQL($artist_id, $album_id): $sql\n";
    my @names=$self->{db}->cmd_firstcol($sql, $artist_id, $album_id);
    print "TRACKS: ", join(', ', @names), "\n";
    return(@names);
}

sub tracks
{
    my($self)=@_;
    # FIXME: rework PathElements
    my $tail=$self->tail();
    if(ref($tail) eq "ID3FS::PathElement::Artist")
    {
	return $self->artist_tracks($tail->{id});
    }
    elsif(ref($tail) eq "ID3FS::PathElement::Album")
    {
	my $artist_id=0;
	my $artist=$self->tail_parent();
	if(defined($artist) && (ref($artist) eq "ID3FS::PathElement::Artist"))
	{
	    # should always happen
	    $artist_id=$artist->{id};
	}
	return $self->album_tracks($artist_id, $tail->{id});
    }

    my $sql=("SELECT files.name\n" .
	     "\tFROM (\n" .
	     $self->tags_subselect() .
	     "\t) AS subselect\n" .
	     "INNER JOIN files ON files.id=subselect.files_id\n" .
	     "GROUP BY files.name;");
    print "SQL: $sql\n";
    my @names=$self->{db}->cmd_firstcol($sql);
    print "TRACKS: ", join(', ', @names), "\n";
    return(@names);
}

sub filename
{
    my($self, $mountpoint)=@_;
    my $tail=$self->tail();
    if(ref($tail) eq "ID3FS::PathElement::File")
    {
	my $id=$tail->{id};
	my $sql=("SELECT paths.name, files.name FROM files\n" .
		 "INNER JOIN paths ON files.paths_id=paths.id\n" .
		 "WHERE files.id=?\n" .
		 "GROUP BY paths.name, files.name");
	print "FILENAME SQL: $sql\n";
	my ($path, $name)=$self->{db}->cmd_onerow($sql, $id);
	my $id3fs_path=join('/', map { $_->{name}; }  @{$self->{elements}});
	return($self->{db}->relativise($path, $name, $mountpoint, $id3fs_path));
    }
    die("DB::filename: unhandled case\n"); #FIXME
}

sub tags_subselect
{
    my($self)=@_;
    my $tree=$self->{tagtree};
    my $hasvals=$self->tag_has_values();
    my $parent=$self->trailing_tag_parent();

#    print "ELEMENTS: ", join('/', map { $_->{name}; } @{$self->{elements}}), "\n";
#    print "TREE: ", $tree->print(), "\n";
    my $tag=undef;
    if($hasvals)
    {
	$tag=$self->trailing_tag_id();
#	print "Trailing id: $tag\n";
    }
    my ($sqlclause, $joinsneeded)=(undef, 1);
    ($sqlclause, $joinsneeded) = $tree->to_sql($tag) if($tree);
#    print "SQL($joinsneeded): $sqlclause\n";
    my $sql="\tSELECT fxt1.files_id FROM tags t1";
    my @crosses=();
    my @inners=();
#    $joinsneeded++ if($tag);
    for(my $i=1; $i <= $joinsneeded; $i++)
    {
	my $inner=("\tINNER JOIN files_x_tags fxt$i ON " .
		   "t${i}.id=fxt${i}.tags_id");
	if($i > 1)
	{
	    push(@crosses, "CROSS JOIN tags t$i");
	    $inner .= " AND fxt1.files_id=fxt${i}.files_id";
	}
	push(@inners, $inner);
    }
    $sql .= ("\n\t" . join(" ", @crosses)) if(@crosses);
    $sql .= ("\n" . join("\n", @inners)) if(@inners);
    $sql .= "\n\tWHERE $sqlclause" if($sqlclause);
#    if($tag)
#    {
#	$sql .= " AND t${joinsneeded}.parents_id='$tag'";
#    }
    $sql .= "\n\tGROUP BY fxt1.files_id\n";
    return $sql;
}

sub tags_subselect_and_not
{
    my($self,@constraints)=@_;
    my ($tags, $tags_vals, $parent)=$self->constraints_tag_list(@constraints);
    my @tags=@$tags;
    my @tags_vals=@$tags_vals;;
    my $cnt=1;
    my @andclauses=();
    my $sql='';
    for my $tag (@tags)
    {
	if($cnt == 1)
	{
	    $sql="\tSELECT fxt" . scalar(@tags) . ".files_id FROM files_x_tags fxt1\n";
	    push(@andclauses, "\t\tfxt${cnt}.tags_id=$tag");
	}
	else
	{
	    $sql .= ("\tLEFT JOIN files_x_tags fxt$cnt ON fxt" .
		 ($cnt-1) . ".files_id=fxt${cnt}.files_id\n");
	    push(@andclauses, "\t\tfxt${cnt}.tags_id IS NULL");
	}
	print "AND: @andclauses\n";
	$cnt++;
    }
    if(@andclauses)
    {
	$sql .= "\tWHERE\n\t\t";
	$sql .= join(" AND\n\t\t", @andclauses) . "\n";
    }
    $sql .= "\tGROUP BY fxt". scalar(@tags).".files_id\n";
    return $sql;
}


sub tags_subselect_and
{
    my($self,@constraints)=@_;
    my ($tags, $tags_vals, $parent)=$self->constraints_tag_list(@constraints);
    my @tags=@$tags;
    my @tags_vals=@$tags_vals;;
    my $cnt=1;
    my @andclauses=();
    my $sql='';
    for my $tag (@tags)
    {
	if($cnt == 1)
	{
	    $sql="\tSELECT fxt" . scalar(@tags) . ".files_id FROM files_x_tags fxt1\n";
	}
	else
	{
	    $sql .= ("\tINNER JOIN files_x_tags fxt$cnt ON fxt" .
		 ($cnt-1) . ".files_id=fxt${cnt}.files_id\n");
	}
	push(@andclauses, "\t\tfxt${cnt}.tags_id = $tag");
	print "AND: @andclauses\n";
	$cnt++;
    }
    if(@andclauses)
    {
	$sql .= "\tWHERE\n\t\t";
	$sql .= join(" AND\n\t\t", @andclauses) . "\n";
    }
    $sql .= "\tGROUP BY fxt". scalar(@tags).".files_id\n";
    return $sql;
}

sub tags_subselect_or
{
    my($self,@constraints)=@_;
    my ($tags, $tags_vals, $parent)=$self->constraints_tag_list(@constraints);
    my @tags=@$tags;
    my @tags_vals=@$tags_vals;;

    my $sql=("\tSELECT files_x_tags.files_id FROM tags t1\n" .
	     "\tINNER JOIN files_x_tags ON t1.id=files_x_tags.tags_id\n");
    my @orclauses=();
    my @andclauses=();
    # FIXME: and / or?
    if(@tags)
    {
	push(@andclauses, "( t1.parents_id=" . (defined($parent) ? $parent : "''") . " )");
	push(@andclauses, "( t1.id IN ( " . join(', ', @tags) ." ) )");
    }
    for my $pair (@tags_vals)
    {
	my($tag, $val)=@$pair;
	push(@orclauses, "( t1.parents_id=$tag AND t1.id=$val )");
    }
#    push(@andclauses, "( t1.parents_id=" . (defined($parent) ? $parent : "''") . " )");
    if(@orclauses)
    {
	push(@andclauses, join("\n\t\tOR ", @orclauses));
    }
    if(@andclauses)
    {
	$sql .= "\tWHERE\n\t\t";
	$sql .= join("\n\t\tAND ", @andclauses) . "\n";
    }
    $sql .= "\tGROUP BY files_x_tags.files_id\n";
    return $sql;
}

sub constraints_tag_list
{
    my($self, @constraints)=@_;
    my $lasttag=undef;
    my @tags=();
    my @tags_vals=();
    for my $constraint (@constraints)
    {
#	print ref($constraint), ": ", $constraint->{name}, "\n";
	if(ref($constraint) eq "ID3FS::PathElement::Tag")
	{
	    if(defined($lasttag))
	    {
#		print "TAGVAL\n";
		push(@tags_vals, [$lasttag, $constraint->{id}]) if defined($constraint->{id});
		$lasttag=undef;
	    }
	    elsif($self->tag_has_values($constraint->{id}))
	    {
#		print "HASVALUES\n";
		$lasttag=$constraint->{id} if defined($constraint->{id});
	    }
	    else
	    {
#		print "NOVALUES\n";
		push(@tags, $constraint->{id}) if(defined($constraint->{id}));
	    }
	}
    }
    unless($self->{db}->{postgres})
    {
	@tags=map{ "\"$_\""; } @tags;
	@tags_vals=map( { [ map({ "\"$_\""; } @$_ ) ] } @tags_vals);
	$lasttag="\"$lasttag\"" if defined($lasttag);
    }
    return(\@tags, \@tags_vals, $lasttag);
}


sub bare_tags
{
    my($self)=@_;
    my $sql=("SELECT tags.name FROM tags\n" .
	     "WHERE tags.parents_id=''\n" .
	     "GROUP BY tags.name\n");
    my @names=$self->{db}->cmd_firstcol($sql);
    return (@names);
}

sub tags_with_values
{
    # FIXME: only shows one level of tag depth
    my($self)=@_;
    my $sql=("SELECT p.name, t.name  FROM tags t\n" .
	     "INNER JOIN tags p ON t.parents_id=p.id\n" .
	     "GROUP BY p.name, t.name\n");
    print "SQL: $sql\n";
    my $result=$self->{db}->cmd_rows($sql);
    my $tags={};
    for my $pair (@$result)
    {
	push(@{$tags->{$pair->[0]}}, $pair->[1]);
    }
    return $tags;
}

sub lookup_id
{
    my($self, $table, $name)=@_;
    my($id)=$self->{db}->cmd_onerow("SELECT id FROM $table where name=?", $name);
    return $id;
}

1;
