package Search::Query::Dialect;
use Moo;
use Carp;
use Data::Dump qw( dump );
use overload
    '""'     => sub { $_[0]->stringify; },
    'bool'   => sub {1},
    fallback => 1;

use Data::Transformer;
use Scalar::Util qw( blessed );
use Types::Standard qw( Int Undef );
use Type::Utils qw( declare as where coerce inline_as from );
use namespace::autoclean;

my $PositiveInt = declare
    as Int,
    where { defined($_) and $_ >= 0 },
    inline_as {"defined($_) and $_ =~ /^[0-9]\$/ and $_ >= 0"};

coerce $PositiveInt, from Int, q{ abs( $_ || $ENV{PERL_DEBUG} || 0) },
    from Undef, q{ 0 };

has default_field => ( is => 'rw' );
has parser        => ( is => 'ro' );
has debug         => (
    is      => 'rw',
    isa     => $PositiveInt,
    lazy    => 1,
    coerce  => $PositiveInt->coercion,
    builder => '_init_debug',
);

sub _init_debug { $ENV{PERL_DEBUG} || 0 }

our $VERSION = '0.306';

=head1 NAME

Search::Query::Dialect - abstract base class for query language dialects

=head1 SYNOPSIS

 my $query = Search::Query->parser->parse('foo');
 print $query;

=head1 DESCRIPTION

Search::Query::Dialect is the base class from which all query dialects
inherit.

A Dialect subclass must implement at least two methods:

=over

=item stringify

Returns the serialized query tree.

=item stringify_clause( I<leaf> )

Returns one clause of a serialized query tree.

=back

See Search::Query::Dialect::Native for a working example.

=head1 METHODS

=head2 debug

Get/set flag.

=head2 default_field

Standard attribute accessor. Default value is undef.

=head2 init 

B<DEPRECATED>. Use BUILD() instead.

=cut

sub init {
    my $self = shift;
    confess "Use BUILD() instead of init()";
}

=head2 stringify

All subclasses must override this method. The default behavior is to croak.

=cut

sub stringify { croak "must implement stringify() in $_[0]" }

=head2 tree

Returns the query Dialect instance as a hashref structure, similar
to that of Search::QueryParser.

=cut

sub tree {
    my $self = shift;
    my $clause_class
        = $self->parser
        ? $self->parser->clause_class
        : 'Search::Query::Clause';
    my $dialect_class = blessed($self);
    my %tree;

    #warn "before tree: " . dump($self);
    foreach my $prefix ( '+', '', '-' ) {
        next if !exists $self->{$prefix};
        my @clauses;
        for my $clause ( @{ $self->{$prefix} } ) {

            if ( !blessed($clause) ) {
                croak "unblessed clause in Dialect object: " . dump($clause);
            }

            if ( $clause->can('tree') ) {

                #warn "clause isa Dialect: " . dump($clause);
                push @clauses, $clause->tree;
            }
            elsif ( blessed( $clause->value ) ) {

                #warn "clause->value isa Dialect: " . dump($clause);
                my $clause_ref = {%$clause};
                $clause_ref->{value} = $clause->{value}->tree;
                push @clauses, $clause_ref;
            }
            else {

                #warn "clause isa Clause: " . dump($clause);
                push @clauses, {%$clause};
            }
        }
        $tree{$prefix} = \@clauses;
    }

    #warn "after tree: " . dump( \%tree );

    return \%tree;
}

=head2 walk( I<CODE> )

Traverse a Dialect object, calling I<CODE> on each Clause.
The I<CODE> reference should expect 4 arguments:

=over

=item

The Clause object.

=item

The Dialect object.

=item

The I<CODE> reference.

=item

The prefix ("+", "-", and "") for the Clause.

=back

=cut

sub walk {
    my $self = shift;
    my $code = shift;
    if ( !$code or !ref($code) or ref($code) ne 'CODE' ) {
        croak "CODE ref required";
    }
    my $tree = shift || $self;
    foreach my $prefix ( '+', '', '-' ) {
        next if !exists $tree->{$prefix};
        for my $clause ( @{ $tree->{$prefix} } ) {

            #warn "clause: " . dump $clause;
            $code->( $clause, $tree, $code, $prefix );
        }
    }
    return $tree;
}

=head2 translate_to( I<dialect> )

Translate from one Dialect to another. Returns an object
blessed into the I<dialect> class.

=cut

sub translate_to {
    my $self         = shift;
    my $dialect      = shift or croak "Dialect required";
    my $query_class  = Search::Query->get_dialect($dialect);
    my $copy         = $self->tree;
    my $new_dialect  = bless( $copy, $query_class );
    my $clause_class = $self->parser->clause_class;
    my $code         = sub {
        my ( $clause, $dialect, $sub, $prefix ) = @_;

        #warn "before: " . dump($clause);
        if ( exists $clause->{field} ) {

            #warn "clause: " . dump $clause;
            $clause = bless( $clause, $clause_class );
        }
        else {
            $clause = bless( $clause, $query_class );
            $clause->walk($sub);
        }

        #warn "after : " . dump($clause);
    };
    $new_dialect->walk($code);
    return $new_dialect;
}

=head2 add_or_clause( I<clause> )

Add I<clause> as an "or" leaf to the Dialect object.

=cut

sub add_or_clause {

    # DO NOT shift
    my $self   = $_[0];
    my $clause = $_[1] or croak "Clause object required";
    my $str    = "($self) OR ($clause)";
    $_[0] = $self->parser->parse($str);
    return $_[0];
}

=head2 add_and_clause( I<clause> )

Add I<clause> as an "and" leaf to the Dialect object.

=cut

sub add_and_clause {

    # DO NOT shift
    my $self   = $_[0];
    my $clause = $_[1] or croak "Clause object required";
    my $str    = "($self) AND ($clause)";
    $_[0] = $self->parser->parse($str);
    return $_[0];
}

=head2 add_not_clause( I<clause> )

Add I<clause> as a "not" leaf to the Dialect object.

=cut

sub add_not_clause {

    # DO NOT shift
    my $self   = $_[0];
    my $clause = $_[1] or croak "Clause object required";
    my $str    = "($self) NOT ($clause)";
    $_[0] = $self->parser->parse($str);
    return $_[0];
}

=head2 add_sub_clause( I<clause> )

Add I<clause> as a sub clause to the Dialect object. In this
case, I<clause> should also be a Dialect object.

=cut

sub add_sub_clause {

    # DO NOT shift
    my $self     = $_[0];
    my $self_ref = \$_[0];
    my $clause   = $_[1];
    if (   !$clause
        or !blessed($clause)
        or !$clause->isa('Search::Query::Dialect') )
    {
        croak "Dialect object required";
    }
    my %methods = (
        ""  => 'add_or_clause',
        "+" => 'add_and_clause',
        "-" => 'add_not_clause',
    );
    $clause->walk(
        sub {
            my ( $subclause, $dialect, $code, $prefix ) = @_;
            my $method = $methods{$prefix};
            $$self_ref = $self->$method($subclause);
        }
    );

}

=head2 field_class

Should return the name of the Field class associated with the Dialect.
Default is 'Search::Query::Field'.

=cut

sub field_class {'Search::Query::Field'}

=head2 get_default_field

Returns the default field for this Dialect.

=cut

sub get_default_field {
    my $self  = shift;
    my $field = $self->default_field;
    $field = $self->parser->default_field unless defined $field;
    if ( !defined $field ) {
        confess "must define a default_field";
    }
    return ref $field ? $field : [$field];
}

=head2 get_field( I<field_name> )

Returns a Field object instance for I<field_name>. The object
will be an instance of B<field_class>.

This is a shorthand wrapper around the method of the same
name in the internal B<parser> object.

=cut

sub get_field {
    my $self  = shift;
    my $name  = shift or croak "field name required";
    my $field = $self->parser->get_field($name);
    if ( !$field ) {
        if ( $self->parser->croak_on_error ) {
            confess "invalid field name: $name";
        }
        my $field_class = $self->field_class;
        $field = $field_class->new( name => $name );
    }
    return $field;
}

=head2 preprocess( I<query_string> )

Called by Parser in parse() before actually building the Dialect object
from I<query_string>.

This allows for any "cleaning up" or other munging of I<query_string>
to support the official Parser syntax.

The default just returns I<query_string> untouched. Subclasses should
return a parseable string.

=cut

sub preprocess { return $_[1] }

=head2 parser

Returns the Search::Query::Parser object that generated the Dialect
object.

=cut

1;

__END__

=head1 AUTHOR

Peter Karman, C<< <karman at cpan.org> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-search-query at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Search-Query>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Search::Query


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Search-Query>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Search-Query>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Search-Query>

=item * Search CPAN

L<http://search.cpan.org/dist/Search-Query/>

=back

=head1 COPYRIGHT & LICENSE

Copyright 2010 Peter Karman.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut
