package HTTP::Headers::Fast;
use strict;
use warnings;
use 5.00800;
use Carp ();

our $VERSION = '0.22';

our $TRANSLATE_UNDERSCORE = 1;

# "Good Practice" order of HTTP message headers:
#    - General-Headers
#    - Request-Headers
#    - Response-Headers
#    - Entity-Headers

# yappo says "Readonly sucks".
my $OP_GET    = 0;
my $OP_SET    = 1;
my $OP_INIT   = 2;
my $OP_PUSH   = 3;

my @general_headers = qw(
  Cache-Control Connection Date Pragma Trailer Transfer-Encoding Upgrade
  Via Warning
);

my @request_headers = qw(
  Accept Accept-Charset Accept-Encoding Accept-Language
  Authorization Expect From Host
  If-Match If-Modified-Since If-None-Match If-Range If-Unmodified-Since
  Max-Forwards Proxy-Authorization Range Referer TE User-Agent
);

my @response_headers = qw(
  Accept-Ranges Age ETag Location Proxy-Authenticate Retry-After Server
  Vary WWW-Authenticate
);

my @entity_headers = qw(
  Allow Content-Encoding Content-Language Content-Length Content-Location
  Content-MD5 Content-Range Content-Type Expires Last-Modified
);

my %entity_header = map { lc($_) => 1 } @entity_headers;

my @header_order =
  ( @general_headers, @request_headers, @response_headers, @entity_headers, );

# Make alternative representations of @header_order.  This is used
# for sorting and case matching.
my %header_order;
our %standard_case;

{
    my $i = 0;
    for (@header_order) {
        my $lc = lc $_;
        $header_order{$lc}  = ++$i;
        $standard_case{$lc} = $_;
    }
}

sub new {
    my ($class) = shift;
    my $self = bless {}, $class;
    $self->header(@_) if @_;    # set up initial headers
    $self;
}

sub isa {
    my ($self, $klass) = @_;
    my $proto = ref $self || $self;
    return ($proto eq $klass || $klass eq 'HTTP::Headers') ? 1 : 0;
}

sub header {
    my $self = shift;
    Carp::croak('Usage: $h->header($field, ...)') unless @_;
    my (@old);

    if (@_ == 1) {
        @old = $self->_header_get(@_);
    } elsif( @_ == 2 ) {
        @old = $self->_header_set(@_);
    } else {
        my %seen;
        while (@_) {
            my $field = shift;
            if ( $seen{ lc $field }++ ) {
                @old = $self->_header_push($field, shift);
            } else {
                @old = $self->_header_set($field, shift);
            }
        }
    }
    return @old    if wantarray;
    return $old[0] if @old <= 1;
    join( ", ", @old );
}

sub clear {
    my $self = shift;
    %$self = ();
}

sub push_header {
    my $self = shift;

    if (@_ == 2) {
        my ($field, $val) = @_;
        $field = _standardize_field_name($field) unless $field =~ /^:/;

        my $h = $self->{$field};
        if (!defined $h) {
            $h = [];
            $self->{$field} = $h;
        } elsif (ref $h ne 'ARRAY') {
            $h = [ $h ];
            $self->{$field} = $h;
        }
    
        push @$h, ref $val ne 'ARRAY' ? $val : @$val;
    } else {
        while ( my ($field, $val) = splice( @_, 0, 2 ) ) {
            $field = _standardize_field_name($field) unless $field =~ /^:/;

            my $h = $self->{$field};
            if (!defined $h) {
                $h = [];
                $self->{$field} = $h;
            } elsif (ref $h ne 'ARRAY') {
                $h = [ $h ];
                $self->{$field} = $h;
            }
    
            push @$h, ref $val ne 'ARRAY' ? $val : @$val;
        }
    }
    return ();
}

sub init_header {
    Carp::croak('Usage: $h->init_header($field, $val)') if @_ != 3;
    shift->_header( @_, $OP_INIT );
}

sub remove_header {
    my ( $self, @fields ) = @_;
    my $field;
    my @values;
    for my $field (@fields) {
        $field =~ tr/_/-/ if $field !~ /^:/ && $TRANSLATE_UNDERSCORE;
        my $v = delete $self->{ lc $field };
        push( @values, ref($v) eq 'ARRAY' ? @$v : $v ) if defined $v;
    }
    return @values;
}

sub remove_content_headers {
    my $self = shift;
    unless ( defined(wantarray) ) {

        # fast branch that does not create return object
        delete @$self{ grep $entity_header{$_} || /^content-/, keys %$self };
        return;
    }

    my $c = ref($self)->new;
    for my $f ( grep $entity_header{$_} || /^content-/, keys %$self ) {
        $c->{$f} = delete $self->{$f};
    }
    $c;
}

my %field_name;
sub _standardize_field_name {
    my $field = shift;

    $field =~ tr/_/-/ if $TRANSLATE_UNDERSCORE;
    if (my $cache = $field_name{$field}) {
        return $cache;
    }

    my $old = $field;
    $field = lc $field;
    unless ( defined $standard_case{$field} ) {
        # generate a %standard_case entry for this field
        $old =~ s/\b(\w)/\u$1/g;
        $standard_case{$field} = $old;
    }
    $field_name{$old} = $field;
    return $field;
}

sub _header_get {
    my ($self, $field, $skip_standardize) = @_;

    $field = _standardize_field_name($field) unless $skip_standardize || $field =~ /^:/;

    my $h = $self->{$field};
    return (ref($h) eq 'ARRAY') ? @$h : ( defined($h) ? ($h) : () );
}

sub _header_set {
    my ($self, $field, $val) = @_;

    $field = _standardize_field_name($field) unless $field =~ /^:/;

    my $h = $self->{$field};
    my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );
    if ( defined($val) ) {
        if (ref $val eq 'ARRAY' && scalar(@$val) == 1) {
            $val = $val->[0];
        }
        $self->{$field} = $val;
    } else {
        delete $self->{$field};
    }
    return @old;
}

sub _header_push {
    my ($self, $field, $val) = @_;

    $field = _standardize_field_name($field) unless $field =~ /^:/;

    my $h = $self->{$field};
    if (ref($h) eq 'ARRAY') {
        my @old = @$h;
        push @$h, ref $val ne 'ARRAY' ? $val : @$val;
        return @old;
    } elsif (defined $h) {
        $self->{$field} = [$h, ref $val ne 'ARRAY' ? $val : @$val ];
        return ($h);
    } else {
        $self->{$field} = ref $val ne 'ARRAY' ? $val : @$val;
        return ();
    }
}

sub _header {
    my ($self, $field, $val, $op) = @_;

    $field = _standardize_field_name($field) unless $field =~ /^:/;

    $op ||= defined($val) ? $OP_SET : $OP_GET;

    my $h = $self->{$field};
    my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );

    unless ( $op == $OP_GET || ( $op == $OP_INIT && @old ) ) {
        if ( defined($val) ) {
            my @new = ( $op == $OP_PUSH ) ? @old : ();
            if ( ref($val) ne 'ARRAY' ) {
                push( @new, $val );
            }
            else {
                push( @new, @$val );
            }
            $self->{$field} = @new > 1 ? \@new : $new[0];
        }
        elsif ( $op != $OP_PUSH ) {
            delete $self->{$field};
        }
    }
    @old;
}

sub _sorted_field_names {
    my $self = shift;
    return [ sort {
        ( $header_order{$a} || 999 ) <=> ( $header_order{$b} || 999 )
          || $a cmp $b
    } keys %$self ];
}

sub header_field_names {
    my $self = shift;
    return map $standard_case{$_} || $_, @{ $self->_sorted_field_names }
      if wantarray;
    return keys %$self;
}

sub scan {
    my ( $self, $sub ) = @_;
    for my $key (@{ $self->_sorted_field_names }) {
        next if substr($key, 0, 1) eq '_';
        my $vals = $self->{$key};
        if ( ref($vals) eq 'ARRAY' ) {
            for my $val (@$vals) {
                $sub->( $standard_case{$key} || $key, $val );
            }
        }
        else {
            $sub->( $standard_case{$key} || $key, $vals );
        }
    }
}

sub _process_newline {
    local $_ = shift;
    my $endl = shift;
    # must handle header values with embedded newlines with care
    s/\s+$//;        # trailing newlines and space must go
    s/\n(\x0d?\n)+/\n/g;     # no empty lines
    s/\n([^\040\t])/\n $1/g; # intial space for continuation
    s/\n/$endl/g;    # substitute with requested line ending
    $_;
}

sub _as_string {
    my ($self, $endl, $fieldnames) = @_;

    my @result;
    for my $key ( @$fieldnames ) {
        next if index($key, '_') == 0;
        my $vals = $self->{$key};
        if ( ref($vals) eq 'ARRAY' ) {
            for my $val (@$vals) {
                my $field = $standard_case{$key} || $key;
                $field =~ s/^://;
                if ( index($val, "\n") >= 0 ) {
                    $val = _process_newline($val, $endl);
                }
                push @result, $field . ': ' . $val;
            }
        } else {
            my $field = $standard_case{$key} || $key;
            $field =~ s/^://;
            if ( index($vals, "\n") >= 0 ) {
                $vals = _process_newline($vals, $endl);
            }
            push @result, $field . ': ' . $vals;
        }
    }

    join( $endl, @result, '' );
}

sub as_string {
    my ( $self, $endl ) = @_;
    $endl = "\n" unless defined $endl;
    $self->_as_string($endl, $self->_sorted_field_names);
}

sub as_string_without_sort {
    my ( $self, $endl ) = @_;
    $endl = "\n" unless defined $endl;
    $self->_as_string($endl, [keys(%$self)]);
}


sub _psgi_flatten {
    my ($self, $keys) = @_;
    my @headers;
    for my $key ( @{$keys} ) {
        next if substr($key, 0, 1) eq '_';
        my $vals = $self->{$key};
        if ( ref($vals) eq 'ARRAY' ) {
            for my $val (@$vals) {
                $val =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
                $val =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
                push @headers, $standard_case{$key} || $key, $val;
            }
        }
        else {
            $vals =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
            $vals =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
            push @headers, $standard_case{$key} || $key, $vals;
        }
    }
    return \@headers;
}

sub psgi_flatten {
    $_[0]->_psgi_flatten($_[0]->_sorted_field_names);
}


sub psgi_flatten_without_sort {
    $_[0]->_psgi_flatten([keys %{$_[0]}]);
}

{
    my $storable_required;
    sub clone {
        unless ($storable_required) {
            require Storable;
            $storable_required++;
        }
        goto &Storable::dclone;
    }
}

sub _date_header {
    require HTTP::Date;
    my ( $self, $header, $time ) = @_;
    my $old;
    if ( defined $time ) {
        ($old) = $self->_header_set( $header, HTTP::Date::time2str($time) );
    } else {
        ($old) = $self->_header_get($header, 1);
    }
    $old =~ s/;.*// if defined($old);
    HTTP::Date::str2time($old);
}

sub date                { shift->_date_header( 'date',                @_ ); }
sub expires             { shift->_date_header( 'expires',             @_ ); }
sub if_modified_since   { shift->_date_header( 'if-modified-since',   @_ ); }
sub if_unmodified_since { shift->_date_header( 'if-unmodified-since', @_ ); }
sub last_modified       { shift->_date_header( 'last-modified',       @_ ); }

# This is used as a private LWP extension.  The Client-Date header is
# added as a timestamp to a response when it has been received.
sub client_date { shift->_date_header( 'client-date', @_ ); }

# The retry_after field is dual format (can also be a expressed as
# number of seconds from now), so we don't provide an easy way to
# access it until we have know how both these interfaces can be
# addressed.  One possibility is to return a negative value for
# relative seconds and a positive value for epoch based time values.
#sub retry_after       { shift->_date_header('Retry-After',       @_); }

sub content_type {
    my $self = shift;
    my $ct   = $self->{'content-type'};
    $self->{'content-type'} = shift if @_;
    $ct = $ct->[0] if ref($ct) eq 'ARRAY';
    return '' unless defined($ct) && length($ct);
    my @ct = split( /;\s*/, $ct, 2 );
    for ( $ct[0] ) {
        s/\s+//g;
        $_ = lc($_);
    }
    wantarray ? @ct : $ct[0];
}

sub content_type_charset {
    my $self = shift;
    my $h = $self->{'content-type'};
    $h = $h->[0] if ref($h);
    $h = "" unless defined $h;
    my @v = _split_header_words($h);
    if (@v) {
	my($ct, undef, %ct_param) = @{$v[0]};
	my $charset = $ct_param{charset};
	if ($ct) {
	    $ct = lc($ct);
	    $ct =~ s/\s+//;
	}
	if ($charset) {
	    $charset = uc($charset);
	    $charset =~ s/^\s+//;  $charset =~ s/\s+\z//;
	    undef($charset) if $charset eq "";
	}
	return $ct, $charset if wantarray;
	return $charset;
    }
    return undef, undef if wantarray;
    return undef;
}

sub _split_header_words
{
    my(@val) = @_;
    my @res;
    for (@val) {
	my @cur;
	while (length) {
	    if (s/^\s*(=*[^\s=;,]+)//) {  # 'token' or parameter 'attribute'
		push(@cur, $1);
		# a quoted value
		if (s/^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"//) {
		    my $val = $1;
		    $val =~ s/\\(.)/$1/g;
		    push(@cur, $val);
		# some unquoted value
		}
		elsif (s/^\s*=\s*([^;,\s]*)//) {
		    my $val = $1;
		    $val =~ s/\s+$//;
		    push(@cur, $val);
		# no value, a lone token
		}
		else {
		    push(@cur, undef);
		}
	    }
	    elsif (s/^\s*,//) {
		push(@res, [@cur]) if @cur;
		@cur = ();
	    }
	    elsif (s/^\s*;// || s/^\s+//) {
		# continue
	    }
	    else {
		die "This should not happen: '$_'";
	    }
	}
	push(@res, \@cur) if @cur;
    }

    for my $arr (@res) {
	for (my $i = @$arr - 2; $i >= 0; $i -= 2) {
	    $arr->[$i] = lc($arr->[$i]);
	}
    }
    return @res;
}

sub content_is_text {
    my $self = shift;
    return $self->content_type =~ m,^text/,;
}

sub content_is_html {
    my $self = shift;
    return $self->content_type eq 'text/html' || $self->content_is_xhtml;
}

sub content_is_xhtml {
    my $ct = shift->content_type;
    return $ct eq "application/xhtml+xml"
      || $ct   eq "application/vnd.wap.xhtml+xml";
}

sub content_is_xml {
    my $ct = shift->content_type;
    return 1 if $ct eq "text/xml";
    return 1 if $ct eq "application/xml";
    return 1 if $ct =~ /\+xml$/;
    return 0;
}

sub referer {
    my $self = shift;
    if ( @_ && $_[0] =~ /#/ ) {

        # Strip fragment per RFC 2616, section 14.36.
        my $uri = shift;
        if ( ref($uri) ) {
            $uri = $uri->clone;
            $uri->fragment(undef);
        }
        else {
            $uri =~ s/\#.*//;
        }
        unshift @_, $uri;
    }
    ( $self->_header( 'Referer', @_ ) )[0];
}
*referrer = \&referer;    # on tchrist's request

for my $key (qw/content-length content-language content-encoding title user-agent server from warnings www-authenticate authorization proxy-authenticate proxy-authorization/) {
    no strict 'refs';
    (my $meth = $key) =~ s/-/_/g;
    *{$meth} = sub {
        my $self = shift;
        if (@_) {
            ( $self->_header_set( $key, @_ ) )[0]
        } else {
            my $h = $self->{$key};
            (ref($h) eq 'ARRAY') ? $h->[0] : $h;
        }
    };
}

sub authorization_basic { shift->_basic_auth( "Authorization", @_ ) }
sub proxy_authorization_basic {
    shift->_basic_auth( "Proxy-Authorization", @_ );
}

sub _basic_auth {
    require MIME::Base64;
    my ( $self, $h, $user, $passwd ) = @_;
    my ($old) = $self->_header($h);
    if ( defined $user ) {
        Carp::croak("Basic authorization user name can't contain ':'")
          if $user =~ /:/;
        $passwd = '' unless defined $passwd;
        $self->_header(
            $h => 'Basic ' . MIME::Base64::encode( "$user:$passwd", '' ) );
    }
    if ( defined $old && $old =~ s/^\s*Basic\s+// ) {
        my $val = MIME::Base64::decode($old);
        return $val unless wantarray;
        return split( /:/, $val, 2 );
    }
    return;
}

1;
__END__

=encoding utf8

=head1 NAME

HTTP::Headers::Fast - faster implementation of HTTP::Headers

=head1 SYNOPSIS

  use HTTP::Headers::Fast;
  # and, same as HTTP::Headers.

=head1 DESCRIPTION

HTTP::Headers::Fast is a perl class for parsing/writing HTTP headers.

The interface is same as HTTP::Headers.

=head1 WHY YET ANOTHER ONE?

HTTP::Headers is a very good. But I needed a faster implementation, fast  =)

=head1 ADDITIONAL METHODS

=over 4

=item as_string_without_sort

as_string method sorts the header names.But, sorting is bit slow.

In this method, stringify the instance of HTTP::Headers::Fast without sorting.

=item psgi_flatten

returns PSGI compatible arrayref of header.

    my $headers:ArrayRef = $header->flatten

=item psgi_flatten_without_sort

same as flatten but returns arrayref without sorting.

=back

=head1 @ISA HACK

If you want HTTP::Headers::Fast to pretend like it's really HTTP::Headers, you can try the following hack:

    unshift @HTTP::Headers::Fast::ISA, 'HTTP::Headers';

=head1 BENCHMARK

    HTTP::Headers 5.818, HTTP::Headers::Fast 0.01

    -- push_header
            Rate orig fast
    orig 144928/s   -- -20%
    fast 181818/s  25%   --

    -- push_header_many
            Rate orig fast
    orig 74627/s   -- -16%
    fast 89286/s  20%   --

    -- get_date
            Rate orig fast
    orig 34884/s   -- -14%
    fast 40541/s  16%   --

    -- set_date
            Rate orig fast
    orig 21505/s   -- -19%
    fast 26525/s  23%   --

    -- scan
            Rate orig fast
    orig 57471/s   --  -1%
    fast 57803/s   1%   --

    -- get_header
            Rate orig fast
    orig 120337/s   -- -24%
    fast 157729/s  31%   --

    -- set_header
            Rate orig fast
    orig  79745/s   -- -30%
    fast 113766/s  43%   --

    -- get_content_length
            Rate orig fast
    orig 182482/s   -- -77%
    fast 793651/s 335%   --

    -- as_string
            Rate orig fast
    orig 23753/s   -- -41%
    fast 40161/s  69%   --

=head1 AUTHOR

    Tokuhiro Matsuno E<lt>tokuhirom@gmail.comE<gt>
    Daisuke Maki

And HTTP::Headers' originally written by Gisle Aas.

=head1 THANKS TO

Markstos

Tatsuhiko Miyagawa

=head1 SEE ALSO

L<HTTP::Headers>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

