package HTML::FormFu::Util;

use strict;

use HTML::FormFu::Constants qw( $SPACE );
use HTML::FormFu::Literal;
use Scalar::Util qw( blessed reftype );
use Readonly;
use Exporter qw/ import /;
use Carp qw/ croak /;

Readonly my $EMPTY_STR => q{};
Readonly my $SPACE     => q{ };

our $LAST_SUB = $EMPTY_STR;

our @EXPORT_OK = qw(
    DEBUG
    DEBUG_PROCESS
    DEBUG_CONSTRAINTS
    debug
    append_xml_attribute
    has_xml_attribute
    remove_xml_attribute
    _parse_args
    require_class
    xml_escape
    literal
    _filter_components
    _get_elements
    process_attrs
    split_name
    _merge_hashes
);

# the empty prototype () means that when false, all debugging calls
# will be optimised out during compilation

sub DEBUG {
    $ENV{HTML_FORMFU_DEBUG} || 0;
}

sub DEBUG_PROCESS () {
    DEBUG
        || $ENV{HTML_FORMFU_DEBUG_PROCESS}
        || 0;
}

sub DEBUG_CONSTRAINTS {
    DEBUG
        || DEBUG_PROCESS
        || $ENV{HTML_FORMFU_DEBUG_CONSTRAINTS}
        || 0;
}

sub debug {
    my ($message) = @_;

    my ( undef, undef, undef, $sub ) = caller(1);

    require 'Data/Dumper.pm';

    warn "\n" if $sub ne $LAST_SUB;

    if ( @_ > 1 ) {
        warn "$sub()\n" if $sub ne $LAST_SUB;

        while (@_) {
            my $key   = shift;
            my $value = shift;

            if ( !defined $value ) {
               $value = "is undef"; 
            }
            elsif ( ref $value ) {
                $value = Data::Dumper::Dumper($value);
                $value =~ s/^\$VAR1 = //;
            }
            else {
                $value = "'$value'\n";
            }

            warn "$key: $value";
        }
    }
    elsif ( ref $message ) {
        warn "$sub()\n" if $sub ne $LAST_SUB;

        $message = Data::Dumper::Dumper($message);
        $message =~ s/^\$VAR1 = /        /;

        warn "$message\n";
    }
    else {
        warn "$sub\n" if $sub ne $LAST_SUB;

        warn "$message\n";
    }

    $LAST_SUB = $sub;

    return;
}

sub _filter_components {
    my ( $args, $components ) = @_;

    for my $name ( keys %$args ) {

        # get_errors() handles this itself
        next if $name eq 'forced';

        my $value;

        @$components
            = grep {
                   $_->can($name)
                && defined( $value = $_->$name )
                && $value eq $args->{$name}
            } @$components;
    }

    return $components;
}

sub _get_elements {
    my ( $args, $elements ) = @_;

    for my $name ( keys %$args ) {
        my $value;
        next unless defined $args->{$name};
        @$elements = grep {
                   $_->can($name)
                && defined( $value = $_->$name )
                && (ref($args->{$name}) eq 'Regexp' ? $value =~ $args->{$name} : $value eq $args->{$name})
        } @$elements;
    }

    return $elements;
}

sub append_xml_attribute {
    my ( $attrs, $key, $value ) = @_;

    croak '$attrs arg must be a hash reference'
        if ref $attrs ne 'HASH';

    my %dispatcher = _append_subs();

    if ( exists $attrs->{$key} && defined $attrs->{$key} ) {
        my $orig = 'string';

        if ( blessed $attrs->{$key}
            && $attrs->{$key}->isa('HTML::FormFu::Literal') )
        {
            $orig = 'literal';
        }

        my $new = 'string';

        if ( blessed $value
            && $value->isa('HTML::FormFu::Literal') )
        {
            $new = 'literal';
        }

        $attrs->{$key} = $dispatcher{$orig}->{$new}->( $attrs->{$key}, $value );
    }
    else {
        $attrs->{$key} = $value;
    }

    return $attrs;
}

sub _append_subs {
    return (
        literal => {
            string => sub {
                $_[0]->push( xml_escape(" $_[1]") );
                return $_[0];
            },
            literal => sub {
                $_[0]->push(" $_[1]");
                return $_[0];
            },
        },
        string => {
            string => sub {
                $_[0] .= " $_[1]";
                return $_[0];
            },
            literal => sub {
                $_[1]->unshift( xml_escape("$_[0] ") );
                return $_[1];
            },
        },
    );
}

sub has_xml_attribute {
    my ( $attrs, $key, $value ) = @_;

    croak '$attrs arg must be a hash reference'
        if ref $attrs ne 'HASH';

    my %dispatcher = _has_subs();

    if ( exists $attrs->{$key} && defined $attrs->{$key} ) {
        my $orig = 'string';

        if ( blessed $attrs->{$key}
            && $attrs->{$key}->isa('HTML::FormFu::Literal') )
        {
            $orig = 'literal';
        }

        my $new = 'string';

        if ( blessed $value
            && $value->isa('HTML::FormFu::Literal') )
        {
            $new = 'literal';
        }

        return $dispatcher{$orig}->{$new}->( $attrs->{$key}, $value );
    }

    return;
}

sub _has_subs {
    return (
        literal => {
            string => sub {
                my $x = "$_[0]";
                my $y = xml_escape("$_[1]");
                return
                       $x =~ /^\Q$y\E ?/
                    || $x =~ / \Q$y\E /
                    || $x =~ / ?\Q$y\E$/;
            },
            literal => sub {
                my $x = "$_[0]";
                my $y = "$_[1]";
                return
                       $x =~ /^\Q$y\E ?/
                    || $x =~ / \Q$y\E /
                    || $x =~ / ?\Q$y\E$/;
            },
        },
        string => {
            string => sub {
                my ( $x, $y ) = @_;
                return
                       $x =~ /^\Q$y\E ?/
                    || $x =~ / \Q$y\E /
                    || $x =~ / ?\Q$y\E$/;
            },
            literal => sub {
                my $x = xml_escape( $_[0] );
                my $y = "$_[1]";
                return
                       $x =~ /^\Q$y\E ?/
                    || $x =~ / \Q$y\E /
                    || $x =~ / ?\Q$y\E$/;
            },
        },
    );
}

sub remove_xml_attribute {
    my ( $attrs, $key, $value ) = @_;

    croak '$attrs arg must be a hash reference'
        if ref $attrs ne 'HASH';

    my %dispatcher = _remove_subs();

    if ( exists $attrs->{$key} && defined $attrs->{$key} ) {
        my $orig = 'string';

        if ( blessed $attrs->{$key}
            && $attrs->{$key}->isa('HTML::FormFu::Literal') )
        {
            $orig = 'literal';
        }

        my $new = 'string';

        if ( blessed $value
            && $value->isa('HTML::FormFu::Literal') )
        {
            $new = 'literal';
        }

        $attrs->{$key} = $dispatcher{$orig}->{$new}->( $attrs->{$key}, $value );
    }
    else {
        $attrs->{$key} = $value;
    }

    return $attrs;
}

sub _remove_subs {
    return (
        literal => {
            string => sub {
                my $x = "$_[0]";
                my $y = xml_escape("$_[1]");
                $x        =~ s/^\Q$y\E ?//
                    || $x =~ s/ \Q$y\E / /
                    || $x =~ s/ ?\Q$y\E$//;
                return literal($x);
            },
            literal => sub {
                my $x = "$_[0]";
                my $y = "$_[1]";
                $x        =~ s/^\Q$y\E ?//
                    || $x =~ s/ \Q$y\E / /
                    || $x =~ s/ ?\Q$y\E$//;
                return literal($x);
            },
        },
        string => {
            string => sub {
                my ( $x, $y ) = @_;
                $x        =~ s/^\Q$y\E ?//
                    || $x =~ s/ \Q$y\E / /
                    || $x =~ s/ ?\Q$y\E$//;
                return $x;
            },
            literal => sub {
                my $x = xml_escape( $_[0] );
                my $y = "$_[1]";
                $x        =~ s/^\Q$y\E ?//
                    || $x =~ s/ \Q$y\E / /
                    || $x =~ s/ ?\Q$y\E$//;
                return literal($x);
            },
        },
    );
}

sub _parse_args {

    if ( !@_ ) {
        return;
    }
    elsif ( @_ > 1 ) {
        return @_;
    }
    elsif ( ref $_[0] ) {
        return %{ $_[0] };
    }
    else {
        return ( name => $_[0] );
    }
}

sub require_class {
    my ($class) = @_;

    croak "class argument missing" if !defined $class;

    $class =~ s|::|/|g;
    $class .= ".pm";

    if ( !exists $::INC{$class} ) {
        eval { require $class };
        croak $@ if $@;

    }

    return;
}

sub xml_escape {
    my $val = shift;

    return undef if !defined $val;

    if ( ref $val eq 'HASH' ) {
        my %val = %$val;

        while ( my ( $key, $value ) = each %val ) {
            $val{$key} = xml_escape($value);
        }

        return \%val;
    }
    elsif ( ref $val eq 'ARRAY' ) {
        my @val = @$val;
        my @new;
        for my $val (@val) {
            push @new, xml_escape($val);
        }
        return \@new;
    }
    elsif ( ref $val ) {
        return "$val";
    }

    return $val if !length $val;

    $val =~ s/&/&#38;/g;
    $val =~ s/"/&#34;/g;
    $val =~ s/'/&#39;/g;
    $val =~ s/</&lt;/g;
    $val =~ s/>/&gt;/g;

    return $val;
}

sub literal {
    return HTML::FormFu::Literal->new(@_);
}

sub process_attrs {
    my ($attrs) = @_;

    croak 'argument to process_attrs() must be a hashref'
        if reftype( $attrs ) ne 'HASH';

    my @attribute_parts;

    for my $attribute ( sort keys %$attrs ) {
        my $value
            = defined $attrs->{$attribute}
            ? $attrs->{$attribute}
            : $EMPTY_STR;

        push @attribute_parts, sprintf '%s="%s"', $attribute, $value;
    }

    my $xml = join $SPACE, @attribute_parts;

    if ( length $xml ) {
        $xml = " $xml";
    }

    return $xml;
}

sub split_name {
    my ($name) = @_;

    croak "split_name requires 1 arg" if @_ != 1;

    return if !defined $name;

    if ( $name =~ /^ \w+ \[ /x ) {

        # copied from Catalyst::Plugin::Params::Nested::Expander
        # redistributed under the same terms as Perl

        return grep {defined} (
            $name =~ /
            ^  (\w+)      # root param
            | \[ (\w+) \] # nested
        /gx
        );
    }
    elsif ( $name =~ /\./ ) {

        # Copied from CGI::Expand
        # redistributed under the same terms as Perl

        # m// splits on unescaped '.' chars. Can't fail b/c \G on next
        # non ./ * -> escaped anything -> non ./ *
        $name =~ m/^ ( [^\\\.]* (?: \\(?:.|$) [^\\\.]* )* ) /gx;
        my $first = $1;
        $first =~ s/\\(.)/$1/g;    # remove escaping

        my (@segments) = $name =~

            # . -> ( non ./ * -> escaped anything -> non ./ * )
            m/\G (?:[\.]) ( [^\\\.]* (?: \\(?:.|$) [^\\\.]* )* ) /gx;

        # Escapes removed later, can be used to avoid using as array index

        return ( $first, @segments );
    }

    return ($name);
}

# sub _merge_hashes originally copied from Catalyst::Utils::merge_hashes()
# redistributed under the same terms as Perl

sub _merge_hashes {
    my ( $lefthash, $righthash ) = @_;

    return $lefthash if !defined $righthash || !keys %$righthash;

    my %merged = %$lefthash;

    while ( my ( $key, $right_value ) = each %$righthash ) {

        my $left_value = $lefthash->{$key};

        if ( exists $lefthash->{$key} ) {
            
            my $is_left_ref = exists $lefthash->{$key}
                && ref $lefthash->{$key} eq 'HASH';

            if ( ref $left_value eq 'HASH' && ref $right_value eq 'ARRAY' ) {
                $merged{$key} = _merge_hash_array( $left_value, $right_value );
            }
            elsif ( ref $left_value eq 'ARRAY' && ref $right_value eq 'HASH' ) {
                $merged{$key} = _merge_array_hash( $left_value, $right_value );
            }
            elsif ( ref $left_value eq 'ARRAY' && ref $right_value eq 'ARRAY' ) {
                $merged{$key} = _merge_array_array( $left_value, $right_value );
            }
            elsif ( ref $left_value eq 'HASH' && ref $right_value eq 'HASH' ) {
                $merged{$key} = _merge_hashes( $left_value, $right_value );
            }
            else {
                $merged{$key} = $right_value;
            }
        }
        else {
            $merged{$key} = $right_value;
        }
    }

    return \%merged;
}

sub _merge_hash_array {
    my ( $left, $right ) = @_;
    
    return [ $left, @$right ];
}

sub _merge_array_hash {
    my ( $left, $right ) = @_;
    
    return [ @$left, $right ];
}

sub _merge_array_array {
    my ( $left, $right ) = @_;
    
    return [ @$left, @$right ];
}

1;
