#!/usr/bin/perl
package LJ::EmbedModule;
use strict;
use Carp qw (croak);
use Class::Autouse qw (
                       LJ::Auth
                       HTML::TokeParser
                       );

# states for a finite-state machine we use in parse()
use constant {
    # reading plain html without <object>, <embed> or <lj-embed>
    REGULAR => 1,
    # inside <object> or <embed> tag
    IMPLICIT => 2,
    # inside explicit <lj-embed> tag
    EXPLICIT => 3,
    # maximum embed width and height
    MAX_WIDTH => 800,
    MAX_HEIGHT => 800,
};

# can optionally pass in an id of a module to change its contents
# returns module id
sub save_module {
    my ($class, %opts) = @_;

    my $contents = $opts{contents} || '';
    my $id       = $opts{id};
    my $journal  = $opts{journal}
        or croak "No journal passed to LJ::EmbedModule::save_module";
    my $preview = $opts{preview};
    
    # are we creating a new entry?
    unless (defined $id) {
        $id = LJ::alloc_user_counter($journal, 'D')
            or die "Could not allocate embed module ID";
    } 

    my $cmptext = 'C-' . LJ::text_compress($contents);

    ## embeds for preview are stored in a special table, 
    ## where new items overwrites old ones
    my $table_name = ($preview) ? 'embedcontent_preview' : 'embedcontent';
    $journal->do("REPLACE INTO $table_name (userid, moduleid, content) VALUES ".
                "(?, ?, ?)", undef, $journal->{'userid'}, $id, $cmptext);
    die $journal->errstr if $journal->err;
    
    # save in memcache
    my $memkey = $class->memkey($journal->{'userid'}, $id, $preview);
    LJ::MemCache::set($memkey, $contents);

    return $id;
}

# changes <div class="ljembed"... tags from the RTE into proper lj-embed tags
sub transform_rte_post {
    my ($class, $txt) = @_;
    return $txt unless $txt && $txt =~ /ljembed/i;
    # ghetto... shouldn't use regexes to parse this
    $txt =~ s/<div\s*class="ljembed"\s*(embedid="(\d+)")?\s*>(((?!<\/div>).)*)<\/div>/<lj-embed id="$2">$3<\/lj-embed>/ig;
    $txt =~ s/<div\s*(embedid="(\d+)")?\s*class="ljembed"\s*>(((?!<\/div>).)*)<\/div>/<lj-embed id="$2">$3<\/lj-embed>/ig;
    return $txt;
}

# takes a scalarref to entry text and expands lj-embed tags
# REPLACE
sub expand_entry {
    my ($class, $journal, $entryref, %opts) = @_;

    $$entryref =~ s/(<lj\-embed[^>]+\/>)/$class->_expand_tag($journal, $1, $opts{edit}, %opts)/ge;        
}

sub _expand_tag {
    my $class = shift;
    my $journal = shift;
    my $tag = shift;
    my $edit = shift;
    my %opts = @_;
    
    my %attrs = $tag =~ /(\w+)="?(\-?\d+)"?/g;

    return '[invalid lj-embed, id is missing]' unless $attrs{id};
    
    if ($edit) {
        return '<lj-embed ' . join(' ', map {"$_=\"$attrs{$_}\""} keys %attrs) . ">\n" . 
                 $class->module_content(moduleid  => $attrs{id}, journalid => $journal->id) . 
                 "\n<\/lj-embed>";
    }
    elsif ($opts{'content_only'}) {
      return $class->module_content(moduleid  => $attrs{id}, journalid => $journal->{'userid'});
    }
    else {
        @opts{qw /width height/} = @attrs{qw/width height/};
        return $class->module_iframe_tag($journal, $attrs{id}, %opts)
    }            
};


# take a scalarref to a post, parses any lj-embed tags, saves the contents
# of the tags and replaces them with a module tag with the id.
# REPLACE
sub parse_module_embed {
    my ($class, $journal, $postref, %opts) = @_;

    return unless $postref && $$postref;

    return if LJ::conf_test($LJ::DISABLED{embed_module});

    # fast track out if we don't have to expand anything
    return unless $$postref =~ /lj\-embed|embed|object/i;

    # do we want to replace with the lj-embed tags or iframes?
    my $expand = $opts{expand};

    # if this is editing mode, then we want to expand embed tags for editing
    my $edit = $opts{edit};

    # previews are a special case (don't want to permanantly save to db)
    my $preview = $opts{preview};
    
    # deal with old-fashion calls
    if (($edit || $expand) && ! $preview) {
        return $class->expand_entry($journal, $postref, %opts);
    } 
    
    # ok, we can safely parse post text
    # machine state    
    my $state = REGULAR;
    my $p = HTML::TokeParser->new($postref);
    my $newtxt = '';
    my %embed_attrs = (); # ($eid, $ewidth, $eheight);
    my $embed = '';
    my @stack = ();
    my $next_preview_id = 1;
    
    while (my $token = $p->get_token) {
        my ($type, $tag, $attr) = @$token;
        $tag = lc $tag;
        my $newstate = undef;
        my $reconstructed = $class->reconstruct($token);
        
        if ($state == REGULAR) {
            if ($tag eq 'lj-embed' && $type eq 'S' && ! $attr->{'/'}) {
                # <lj-embed ...>, not self-closed 
                # switch to EXPLICIT state
                $newstate = EXPLICIT;
                # save embed id, width and height if they do exist in attributes
                $embed_attrs{id} = $attr->{id} if $attr->{id};
                $embed_attrs{width} = ($attr->{width} > MAX_WIDTH ? MAX_WIDTH : $attr->{width}) if $attr->{width};
                $embed_attrs{height} = ($attr->{height} > MAX_HEIGHT ? MAX_HEIGHT : $attr->{height}) if $attr->{height};
            } elsif (($tag eq 'object' || $tag eq 'embed') && $type eq 'S') {            
                # <object> or <embed>
                # switch to IMPLICIT state unless it is a self-closed tag
                unless ($attr->{'/'}) {
                    $newstate = IMPLICIT;
                    # tag balance
                    push @stack, $tag;
                }
                # append the tag contents to new embed buffer, so we can convert in to lj-embed later
                $embed .= $reconstructed;
            } else {
                # otherwise stay in REGULAR
                $newtxt .= $reconstructed;
            }
        } elsif ($state == IMPLICIT) {
            if ($tag eq 'object' || $tag eq 'embed') {
                if ($type eq 'E') {
                    # </object> or </embed>
                    # update tag balance, but only if we have a valid balance up to this moment
                    pop @stack if $stack[-1] eq $tag;
                    # switch to REGULAR if tags are balanced (stack is empty), stay in IMPLICIT otherwise
                    $newstate = REGULAR unless @stack;
                } elsif ($type eq 'S') {
                    # <object> or <embed>
                    # mind the tag balance, do not update it in case of a self-closed tag
                    push @stack, $tag unless $attr->{'/'};
                }
            }
            # append to embed buffer
            $embed .= $reconstructed;                
            
        } elsif ($state == EXPLICIT) {

            if ($tag eq 'lj-embed' && $type eq 'E') {
                # </lj-embed> - that's the end of explicit embed block, switch to REGULAR
                $newstate = REGULAR;
            } else {
                # continue appending contents to embed buffer
                $embed .= $reconstructed;
            }
        } else {
            # let's be paranoid
            die "Invalid state: '$state'";
        }
        
        # we decided to switch back to REGULAR and have something in embed buffer
        # so let's save buffer as an embed module and start all over again
        if (defined($newstate) && $newstate == REGULAR && $embed) {
            $embed_attrs{id} = $class->save_module(
                id => ($preview ? $next_preview_id++ : $embed_attrs{id}),
                contents => $embed,
                journal  => $journal,
                preview => $preview,
            );
                        
            $newtxt .= "<lj-embed " . join(' ', map { exists $embed_attrs{$_} ? "$_=\"$embed_attrs{$_}\"" : () } qw / id width height /) . "/>";
            
            $embed = '';
            %embed_attrs = ();
        }
        
        # switch the state if we have a new one
        $state = $newstate if defined $newstate;
        
    }    

    # update passed text
    $$postref = $newtxt;
}

sub module_iframe_tag {
    my ($class, $u, $moduleid, %opts) = @_;

    return '' if $LJ::DISABLED{embed_module};

    my $journalid = $u->{'userid'};
    $moduleid += 0;

    # parse the contents of the module and try to come up with a guess at the width and height of the content
    my $content = $class->module_content(moduleid => $moduleid, journalid => $journalid);
    my $preview = $opts{preview};
    my $width = 0;
    my $height = 0;
    my $p = HTML::TokeParser->new(\$content);
    my $embedcodes;
    
    # if the content only contains a whitelisted embedded video
    # then we can skip the placeholders (in some cases)
    my $no_whitelist = 0;
    my $found_embed = 0;

    # we don't need to estimate the dimensions if they are provided in tag attributes
    unless ($opts{width} && $opts{height}) {
        while (my $token = $p->get_token) {
            my $type = $token->[0];
            my $tag  = $token->[1] ? lc $token->[1] : '';
            my $attr = $token->[2];  # hashref
    
            if ($type eq "S") {
                my ($elewidth, $eleheight);
    
                if ($attr->{width}) {
                    $elewidth = $attr->{width}+0;
                    $width = $elewidth if $elewidth > $width;
                }
                if ($attr->{height}) {
                    $eleheight = $attr->{height}+0;
                    $height = $eleheight if $eleheight > $height;
                }
    
                my $flashvars = $attr->{flashvars};
    
                if ($tag eq 'object' || $tag eq 'embed') {
                    my $src;
                    next unless $src = $attr->{src};
    
                    # we have an object/embed tag with src, make a fake lj-template object
                    my @tags = (
                                ['S', 'lj-template', {
                                    name => 'video',
                                    (defined $elewidth     ? ( width  => $width  ) : ()),
                                    (defined $eleheight    ? ( height => $height ) : ()),
                                    (defined $flashvars ? ( flashvars => $flashvars ) : ()),
                                }],
                                [ 'T', $src, {}],
                                ['E', 'lj-template', {}],
                                );
    
                    $embedcodes = LJ::run_hook('expand_template_video', \@tags);
    
                    $found_embed = 1 if $embedcodes;
                    $found_embed &&= $embedcodes !~ /Invalid video/i;
    
                    $no_whitelist = !$found_embed;
                } elsif ($tag ne 'param') {
                    $no_whitelist = 1;
                }
            }
        }
    
        # add padding
        $width += 50 if $width;
        $height += 50 if $height;
    } 
    
    # use explicit values if we have them
    $width = $opts{width} if $opts{width};
    $height = $opts{height} if $opts{height};

    $width ||= 240;
    $height ||= 200;

    # some dimension min/maxing
    $width = 50 if $width < 50;
    $width = MAX_WIDTH if $width > MAX_WIDTH;
    $height = 50 if $height < 50;
    $height = MAX_HEIGHT if $height > MAX_HEIGHT;

    # safari caches state of sub-resources aggressively, so give
    # each iframe a unique 'name' attribute
    my $id = qq(name="embed_${journalid}_$moduleid");

    my $auth_token = LJ::eurl(LJ::Auth->sessionless_auth_token('embedcontent', moduleid => $moduleid, journalid => $journalid, preview => $preview,));
    my $iframe_url = qq {http://$LJ::EMBED_MODULE_DOMAIN/tools/embedcontent.bml?journalid=$journalid&moduleid=$moduleid&preview=$preview&auth_token=$auth_token};
    my $iframe_tag = qq {<iframe src="$iframe_url" } .
        qq{width="$width" height="$height" allowtransparency="true" frameborder="0" class="lj_embedcontent" $id></iframe>};

    my $remote = LJ::get_remote();
    my $do_placeholder;
    
    if ($remote) {
	return $iframe_tag if $opts{edit};
  
	# show placeholder instead of iframe?
	LJ::load_user_props($remote, "opt_embedplaceholders");
	my $placeholder_prop = $remote->prop('opt_embedplaceholders');
	$do_placeholder = $placeholder_prop && $placeholder_prop ne 'N';

	# if placeholder_prop is not set, then show placeholder on a friends
        # page view UNLESS the embedded content is only one embed/object
        # tag and it's whitelisted video.
        my $r = eval { Apache->request };
        my $view = $r ? $r->notes("view") : '';
        if (! $placeholder_prop && $view eq 'friends') {
            # show placeholder if this is not whitelisted video
            $do_placeholder = 1 if $no_whitelist;
        }
    }
    else {
	$do_placeholder = $BML::COOKIE{'flashpref'};
	$do_placeholder = 0 unless $do_placeholder eq "0" || $do_placeholder eq "1";
    }
    
    return $iframe_tag unless $do_placeholder;

    my $tmpcontent = $class->module_content(
  journalid => $journalid,
  moduleid  => $moduleid,
  0
  );
    $tmpcontent =~ s/.+param\s+name\s*?=\s*?"?movie"?\s*value\s*?=\s*?"?//sg; #"
    $tmpcontent =~ s/("|\s).+//sg; #"
    $tmpcontent = LJ::ehtml($tmpcontent);

    # placeholder
    return LJ::placeholder_link(
                                placeholder_html => $iframe_tag,
        placeholder_link => $iframe_url,
                                width            => $width,
                                height           => $height,
                                img              => "$LJ::IMGPREFIX/videoplaceholder.png",
        img_title => $tmpcontent,
                                );
}

sub module_content {
    my ($class, %opts) = @_;

    my $moduleid  = $opts{moduleid};
    croak "No moduleid" unless defined $moduleid;
    $moduleid += 0;

    my $journalid = $opts{journalid}+0 or croak "No journalid";
    my $journal = LJ::load_userid($journalid) or die "Invalid userid $journalid";
    my $preview = $opts{preview};

    # try memcache
    my $memkey = $class->memkey($journalid, $moduleid, $preview);
    my $content = LJ::MemCache::get($memkey);
    my ($dbload, $dbid); # module id from the database
    unless (defined $content) {
        my $table_name = ($preview) ? 'embedcontent_preview' : 'embedcontent';
        ($content, $dbid) = $journal->selectrow_array("SELECT content, moduleid FROM $table_name WHERE " .
                                                      "moduleid=? AND userid=?",
                                                      undef, $moduleid, $journalid);
        die $journal->errstr if $journal->err;
        $dbload = 1;
    }

    $content ||= '';

    LJ::text_uncompress(\$content) if $content =~ s/^C-//;

    # clean js out of content
    unless ($LJ::DISABLED{'embedmodule-cleancontent'}) {
        LJ::CleanHTML::clean(\$content, {
            addbreaks => 0,
            tablecheck => 0,
            mode => 'allow',
            allow => [qw(object embed)],
            deny => [qw(script iframe)],
            remove => [qw(script iframe)],
            ljcut_disable => 1,
            cleancss => 0,
            extractlinks => 0,
            noautolinks => 1,
            extractimages => 0,
            noexpandembedded => 1,
            transform_embed_nocheck => 1,
        });
    }

    # if we got stuff out of database
    if ($dbload) {
        # save in memcache
        LJ::MemCache::set($memkey, $content);

        # if we didn't get a moduleid out of the database then this entry is not valid
        return defined $dbid ? $content : "[Invalid lj-embed id $moduleid]";
    }

    # get rid of whitespace around the content
    return LJ::trim($content) || '';
}

sub memkey {
    my ($class, $journalid, $moduleid, $preview) = @_;
    my $pfx = $preview ? 'embedcontpreview' : 'embedcont';
    return [$journalid, "$pfx:$journalid:$moduleid"];
}

# create a tag string from HTML::TokeParser token
sub reconstruct {
    my $class = shift;
    my $token = shift;
    my ($type, $tag, $attr, $attord) = @$token;
    if ($type eq 'S') {
        my $txt = "<$tag";
        my $selfclose;
    
        # preserve order of attributes. the original order is
        # in element 4 of $token
        foreach my $name (@$attord) {
            if ($name eq '/') {
                $selfclose = 1;
                next;
            }
    
            # FIXME: ultra ghetto.
            $attr->{$name} = LJ::no_utf8_flag($attr->{$name});
    
            $txt .= " $name=\"" . LJ::ehtml($attr->{$name}) . "\"";
        }
        $txt .= $selfclose ? " />" : ">";    
        
    } elsif ($type eq 'E') {
        return "</$tag>";
    } else { # C, T, D or PI
        return $tag;
    }
}


1;

