
#!/usr/bin/perl
#
# DW::Controller::Customize::Advanced
#
# This controller is for /customize/options and the helper functions for that view.
#
# Authors:
#      R Hatch <ruth.s.hatch@gmail.com>
#
# Copyright (c) 2016 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#
package DW::Controller::Customize::Advanced;

use strict;
use warnings;
use Digest::MD5;
use DW::Controller;
use DW::Routing;
use DW::Template;
use DW::Logic::MenuNav;

DW::Routing->register_string( '/customize/advanced',             \&advanced_handler,    app => 1 );
DW::Routing->register_string( '/customize/advanced/layerbrowse', \&layerbrowse_handler, app => 1 );
DW::Routing->register_string( '/customize/advanced/layers',      \&layers_handler,      app => 1 );
DW::Routing->register_string( '/customize/advanced/styles',      \&styles_handler,      app => 1 );
DW::Routing->register_string( '/customize/advanced/layersource', \&layersource_handler, app => 1 );
DW::Routing->register_string( '/customize/advanced/layeredit',   \&layeredit_handler,   app => 1 );

sub advanced_handler {
    my ( $ok, $rv ) = controller();
    return $rv unless $ok;

    my $r             = $rv->{r};
    my $POST          = $r->post_args;
    my $u             = $rv->{u};
    my $remote        = $rv->{remote};
    my $no_layer_edit = LJ::Hooks::run_hook( "no_theme_or_layer_edit", $u );

    my $vars;
    $vars->{u}             = $u;
    $vars->{remote}        = $remote;
    $vars->{no_layer_edit} = $no_layer_edit;

    return DW::Template->render_template( 'customize/advanced/index.tt', $vars );

}

sub layerbrowse_handler {
    my ( $ok, $rv ) = controller();
    return $rv unless $ok;

    my $r      = $rv->{r};
    my $POST   = $r->post_args;
    my $u      = $rv->{u};
    my $remote = $rv->{remote};
    my $GET    = $r->get_args;

    my $vars;
    $vars->{u}      = $u;
    $vars->{remote} = $remote;
    $vars->{expand} = $GET->{'expand'};

    my $pub = LJ::S2::get_public_layers();

    my $id;
    if ( $GET->{'id'} ) {
        if ( $GET->{'id'} =~ /^\d+$/ ) {    # numeric
            $id = $GET->{'id'};
        }
        else {                              # redist_uniq
            $id = $pub->{ $GET->{'id'} }->{'s2lid'};
        }
    }

    my $dbr = LJ::get_db_reader();

    my %layerinfo;
    my @to_load = grep { /^\d+$/ } keys %$pub;
    LJ::S2::load_layer_info( \%layerinfo, \@to_load );

    my @s2_keys =
        grep { /^\d+$/ && exists $pub->{$_}->{'b2lid'} && $pub->{$_}->{'b2lid'} == 0 } keys %$pub;

    $vars->{pub}     = $pub;
    $vars->{s2_keys} = \@s2_keys;

    $vars->{childsort} = sub {
        my $unsortedchildren = shift;
        my @sortedchildren   = sort {
                   ( $layerinfo{$b}{type} cmp $layerinfo{$a}{type} )
                || ( $layerinfo{$a}{name} cmp $layerinfo{$b}{name} )
        } @$unsortedchildren;

        return \@sortedchildren;
    };

    my $xlink = sub {
        my $r = shift;
        $r =~ s/\[class\[(\w+)\]\]/<a href=\"\#class.$1\">$1<\/a>/g;
        $r =~ s/\[method\[(.+?)\]\]/<a href=\"\#meth.$1\">$1<\/a>/g;
        $r =~ s/\[function\[(.+?)\]\]/<a href=\"\#func.$1\">$1<\/a>/g;
        $r =~ s/\[member\[(.+?)\]\]/<a href=\"\#member.$1\">$1<\/a>/g;
        return $r;
    };

    my $class;
    my $s2info;
    if ($id) {
        LJ::S2::load_layers($id);
        $s2info = S2::get_layer_all($id);
        $class  = $s2info->{'class'} || {};

        $vars->{s2info} = $s2info;
        $vars->{id}     = $id;
        $vars->{class}  = $class;

        # load layer info
        my $layer = defined $pub->{$id} ? $pub->{$id} : LJ::S2::load_layer($id);

        my $layerinfo = {};
        LJ::S2::load_layer_info( $layerinfo, [$id] );
        my $srcview =
            exists $layerinfo->{$id}->{'source_viewable'}
            ? $layerinfo->{$id}->{'source_viewable'}
            : undef;

        # do they have access?
        my $isadmin =
            !defined $pub->{$id} && $remote && $remote->has_priv( 'siteadmin', 'styleview' );
        my $can_manage = $remote && $remote->can_manage( LJ::load_userid( $layer->{userid} ) );

        $vars->{srcview}    = $srcview;
        $vars->{isadmin}    = $isadmin;
        $vars->{can_manage} = $can_manage;

    }

    my $xlink_args = sub {
        my $r = shift;
        return
            unless $r =~ /^(.+?\()(.*)\)$/;
        my ( $new, @args ) = ( $1, split( /\s*\,\s*/, $2 ) );
        foreach (@args) {
            s/^(\w+)/defined $class->{$1} ? "[class[$1]]" : $1/eg;
        }
        $new .= join( ", ", @args ) . ")";
        $r = $new;
        $xlink->($r);
    };

    my $format_value;
    $format_value = sub {
        my $v = shift;

        if ( ref $v eq "HASH" ) {
            if (   $v->{'_type'}
                && $v->{'_type'} eq "Color"
                && $v->{'as_string'} =~ /^#\w\w\w\w\w\w$/ )
            {
                my $ecolor = LJ::ehtml( $v->{'as_string'} );
                $v =
"<span style=\"border: 1px solid #000000; padding-left: 2em; background-color: $ecolor\">&nbsp;</span> <tt>$ecolor</tt>";
            }
            elsif ( defined $v->{'_type'} ) {
                $v = BML::ml( '.propformat.object', { 'type' => LJ::ehtml( $v->{'_type'} ) } );
            }
            else {
                if ( scalar(%$v) ) {
                    $v =
"<code>{</code><ul style='list-style: none; margin: 0 0 0 1.5em; padding: 0;'>"
                        . join(
                        "\n",
                        map {
                                  "<li><b>"
                                . LJ::ehtml($_)
                                . "</b> &rarr; "
                                . $format_value->( $v->{$_} )
                                . ",</li>"
                        } keys %$v
                        ) . "</ul><code>}</code>";
                }
                else {
                    $v = "<code>{}</code>";
                }
            }
        }
        elsif ( ref $v eq "ARRAY" ) {
            if ( scalar(@$v) ) {
                $v =
                    "<code>[</code><ul style='list-style: none; margin: 0 0 0 1.5em; padding: 0;'>"
                    . join( "\n", map { "<li>" . $format_value->($_) . ",</li>" } @$v )
                    . "</ul><code>]</code>";
            }
            else {
                $v = "<code>[]</code>";
            }
        }
        else {
            $v = $v ne '' ? LJ::ehtml($v) : "<i>" . LJ::Lang::ml('.propformat.empty') . "</i>";
        }

        return $v;
    };

    $vars->{xlink}      = $xlink;
    $vars->{xlink_args} = $xlink_args;
    $vars->{layer_is_active} =
        sub { my $x = shift; LJ::Hooks::run_hook( "layer_is_active", $pub->{$x}->{uniq} ); };
    $vars->{ehtml}        = \&LJ::ehtml;
    $vars->{defined}      = sub { return defined $_; };
    $vars->{format_value} = $format_value;
    $vars->{layerinfo}    = \%layerinfo;

    return DW::Template->render_template( 'customize/advanced/layerbrowse.tt', $vars );

}

sub layers_handler {
    my ( $ok, $rv ) = controller( authas => 1, form_auth => 1 );
    return $rv unless $ok;

    my $r             = $rv->{r};
    my $POST          = $r->post_args;
    my $u             = $rv->{u};
    my $remote        = $rv->{remote};
    my $no_layer_edit = LJ::Hooks::run_hook( "no_theme_or_layer_edit", $remote );
    my $GET           = $r->get_args;
    my $authas        = $u->user ne $remote->user ? "?authas=" . $u->user : "";

    # id is optional
    my $id;
    $id = $POST->{'id'} if $POST->{'id'} && $POST->{'id'} =~ /^\d+$/;

    # this catches core_hidden if it's set
    $POST->{'parid'} ||= $POST->{'parid_hidden'};

    my $pub  = LJ::S2::get_public_layers();
    my $ulay = LJ::S2::get_layers_of_user($u);

    my $vars;
    $vars->{u}             = $u;
    $vars->{remote}        = $remote;
    $vars->{no_layer_edit} = $no_layer_edit;
    $vars->{pub}           = $pub;
    $vars->{ulay}          = $ulay;
    $vars->{authas}        = $authas;
    $vars->{authas_html}   = $rv->{authas_html};

    return error_ml('/customize/advanced/index.tt.error.advanced.editing.denied')
        if $no_layer_edit;

    # if we don't have a u, maybe they're an admin and can view stuff anyway?
    my $noactions = 0;
    my $viewall   = $remote && $remote->has_priv( 'siteadmin', 'styleview' );

    if ( $GET->{user} && $viewall ) {
        return error_ml('/customize/advanced/layers.tt.error.cantuseonsystem')
            if $GET->{user} eq 'system';
        $noactions = 1;    # don't let admins change anything
    }
    return error_ml('.error.cantbeauthenticated')
        unless $u;

    return error_ml(
        (
            $remote->{user} eq $u->{user}
            ? '/customize/advanced/layers.tt.error.youcantuseadvanced'
            : '/customize/advanced/layers.tt.error.usercantuseadvanced'
        ),
        undef,
        { authas => $rv->{authas_html} }
    ) unless $u->can_create_s2_styles || $viewall;

    if ( $POST->{'action:create'} && !$noactions ) {

        return error_ml('/customize/advanced/layers.tt.error.maxlayers')
            if keys %$ulay >= $u->count_s2layersmax;

        my $type = $POST->{'type'}
            or return error_ml('/customize/advanced/layers.tt.error.nolayertypeselected');
        my $parid = $POST->{'parid'} + 0
            or return error_ml('/customize/advanced/layers.tt.error.badparentid');
        return error_ml('/customize/advanced/layers.tt.error.invalidlayertype')
            unless $type =~ /^layout|theme|user|i18nc?$/;
        my $parent_type =
            ( $type eq "theme" || $type eq "i18n" || $type eq "user" ) ? "layout" : "core";

        # parent ID is public layer
        if ( $pub->{$parid} ) {

            # of the wrong type
            return error_ml('/customize/advanced/layers.tt.error.badparentid')
                if $pub->{$parid}->{'type'} ne $parent_type;

            # parent ID is user layer, or completely invalid
        }
        else {
            return error_ml('.error.badparentid')
                if !$ulay->{$parid} || $ulay->{$parid}->{'type'} != $parent_type;
        }

        my $id = LJ::S2::create_layer( $u, $parid, $type );
        return error_ml('/customize/advanced/layers.tt.error.cantcreatelayer') unless $id;

        my $lay = {
            'userid' => $u->userid,
            'type'   => $type,
            'b2lid'  => $parid,
            's2lid'  => $id,
        };

        # help user out a bit, creating the beginning of their layer.
        my $s2 = "layerinfo \"type\" = \"$type\";\n";
        $s2 .= "layerinfo \"name\" = \"\";\n\n";
        my $error;
        unless ( LJ::S2::layer_compile( $lay, \$error, { 's2ref' => \$s2 } ) ) {
            return error_ml( '/customize/advanced/layers.tt.error.cantsetuplayer',
                { 'errormsg' => $error } );
        }

        # redirect so they can't refresh and create a new layer again
        return $r->redirect("$LJ::SITEROOT/customize/advanced/layers$authas");
    }

    # delete
    if ( $POST->{'action:del'} && !$noactions ) {

        my $id  = $POST->{'id'} + 0;
        my $lay = LJ::S2::load_layer($id);
        return error_ml('/customize/advanced/layers.tt.error.layerdoesntexist')
            unless $lay;

        return error_ml('/customize/advanced/layers.tt.error.notyourlayer')
            unless $lay->{userid} == $u->userid;

        LJ::S2::delete_layer( $u, $id );
        return $r->redirect("$LJ::SITEROOT/customize/advanced/layers$authas");
    }

    my %active_style = LJ::S2::get_style($u);

    # set up indices for the sort, because it's easier than the convoluted logic
    # of doing all this within the sort itself
    my @parentlayernames;
    my @layernames;
    my @weight;
    my %specialnamelayers;
    my @layerids = keys %$ulay;
    foreach my $layerid (@layerids) {
        my $parent = $ulay->{ $ulay->{$layerid}->{b2lid} } || $pub->{ $ulay->{$layerid}->{b2lid} };
        push @parentlayernames, $parent->{name};

        my $layername = $ulay->{$layerid}->{name};
        push @layernames, $layername;

        my $weight = {
            "Auto-generated Customizations" => 1,    # auto-generated
            ""                              => 2     # empty
        }->{$layername};
        push @weight, $weight;

        $specialnamelayers{$layerid} = 1 if $weight;
    }

    my @sortedlayers = @layerids[
        sort {
            # alphabetically by parent layer's name
            $parentlayernames[$a] cmp $parentlayernames[$b]

              # special case empty names and auto-generated customizations (push them to the bottom)
                || $weight[$a] cmp $weight[$b]

                # alphabetically by layer name (for regular layer names)
                || $layernames[$a] cmp $layernames[$b]

                # Auto-generated customizations then layers with no name sorted by layer id
                || $layerids[$a] <=> $layerids[$b]

        } 0 .. $#layerids
    ];

    my @corelayers = map { $_, $pub->{$_}->{'majorversion'} }
        sort { $pub->{$b}->{'majorversion'} <=> $pub->{$a}->{'majorversion'} }
        grep { $pub->{$_}->{'b2lid'} == 0 && $pub->{$_}->{'type'} eq 'core' && /^\d+$/ }
        keys %$pub;

    my @layouts = ( '', '' );
    push @layouts, map { $_, $pub->{$_}->{'name'} }
        sort { $pub->{$a}->{'name'} cmp $pub->{$b}->{'name'} || $a <=> $b }
        grep { $pub->{$_}->{'type'} eq 'layout' && /^\d+$/ }
        keys %$pub;
    if (%$ulay) {
        my @ulayouts = ();
        push @ulayouts, map {
            $_,
                LJ::Lang::ml(
                '/customize/advanced/layers.tt.createlayer.layoutspecific.select.userlayer',
                { 'name' => $ulay->{$_}->{'name'}, 'id' => $_ } )
            }
            sort { $ulay->{$a}->{'name'} cmp $ulay->{$b}->{'name'} || $a <=> $b }
            grep { $ulay->{$_}->{'type'} eq 'layout' }
            keys %$ulay;
        push @layouts, ( '', '---', @ulayouts ) if @ulayouts;
    }

    $vars->{authas_html}       = $rv->{authas_html};
    $vars->{noactions}         = $noactions;
    $vars->{layouts}           = \@layouts;
    $vars->{corelayers}        = \@corelayers;
    $vars->{sortedlayers}      = \@sortedlayers;
    $vars->{active_style}      = \%active_style;
    $vars->{specialnamelayers} = \%specialnamelayers;
    $vars->{ehtml}             = \&LJ::ehtml;
    $vars->{ejs}               = \&LJ::ejs;

    return DW::Template->render_template( 'customize/advanced/layers.tt', $vars );

}

sub styles_handler {
    my ( $ok, $rv ) = controller( authas => 1, form_auth => 1 );
    return $rv unless $ok;

    my $r             = $rv->{r};
    my $POST          = $r->post_args;
    my $u             = $rv->{u};
    my $remote        = $rv->{remote};
    my $no_layer_edit = LJ::Hooks::run_hook( "no_theme_or_layer_edit", $remote );
    my $GET           = $r->get_args;
    my $authas        = $u->user ne $remote->user ? "?authas=" . $u->user : "";

    my $vars;
    $vars->{u}             = $u;
    $vars->{remote}        = $remote;
    $vars->{no_layer_edit} = $no_layer_edit;
    $vars->{authas_html}   = $rv->{authas_html};
    $vars->{post}          = $POST;

    # if we don't have a u, maybe they're an admin and can view stuff anyway?
    my $noactions = 0;
    my $viewall   = $remote && $remote->has_priv( 'siteadmin', 'styleview' );

    if ( $GET->{user} && $viewall ) {
        return error_ml('/customize/advanced/styles.tt.error.cantuseonsystem')
            if $GET->{user} eq 'system';
        $noactions = 1;    # don't let admins change anything
    }

    return error_ml(
        (
            $remote->{user} eq $u->{user}
            ? '/customize/advanced/styles.tt.error.youcantuseadvanced'
            : '/customize/advanced/styles.tt.error.usercantuseadvanced'
        ),
        undef,
        { authas => $rv->{authas_html} }
    ) unless $u->can_create_s2_styles || $viewall;

    return error_ml('/customize/advanced/index.tt.error.advanced.editing.denied')
        if $no_layer_edit;

    # extra arguments for get requests
    my $getextra = $u->user ne $remote->user ? "?authas=" . $u->user : '';
    my $getextra_amp = $getextra ? "&authas=" . $u->user : '';
    if ($noactions) {
        $getextra     = "?user=" . $u->user;
        $getextra_amp = "&user=" . $u->user;
    }

    $vars->{getextra}     = $getextra;
    $vars->{getextra_amp} = $getextra_amp;

    # style id to edit, if we have one
    # if we have this we're assumed to be in 'edit' mode
    my $id = $GET->{'id'} + 0;

    my $dbh = LJ::get_db_writer();

    # variables declared here, but only filled in if $id
    my ( $core, $layout );         # scalars
    my ( $pub, $ulay, $style );    # hashrefs

    if ($id) {

        # load style
        $style = LJ::S2::load_style($id);
        return error_ml('/customize/advanced/styles.tt.error.stylenotfound') unless $style;

        # check that they own the style
        return error_ml('/customize/advanced/styles.tt.error.notyourstyle')
            unless $style->{userid} == $u->userid;

        # use selected style
        if ( $POST->{'action:usestyle'} && !$noactions ) {

            # save to db and update user object
            $u->set_prop(
                {
                    stylesys => '2',
                    s2_style => $id
                }
            );
            LJ::Hooks::run_hooks( 'apply_theme', $u );
            return $r->redirect("styles$getextra");
        }

        # get public layers
        $pub = LJ::S2::get_public_layers();
        $vars->{pub} = $pub;

        # get user layers
        $ulay = LJ::S2::get_layers_of_user($u);
        $vars->{ulay} = $ulay;

        # find effective layerids being used
        my %eff_layer    = ();
        my @other_layers = ();
        foreach (qw(i18nc layout theme i18n user)) {
            my $lid = $POST->{$_} eq "_other" ? $POST->{"other_$_"} : $POST->{$_};
            next unless $lid;
            $eff_layer{$_} = $lid;

            unless ( $ulay->{ $eff_layer{$_} } || $pub->{ $eff_layer{$_} } ) {
                push @other_layers, $lid;
            }
        }

        # core lid (can't use user core layer)
        $POST->{core} ||= $POST->{core_hidden};
        $core = defined $POST->{core} ? $POST->{core} : $style->{layer}->{core};
        my $highest_core;
        map {
            $highest_core = $_
                if $pub->{$_}->{type} eq 'core'
                && /^\d+$/
                && $pub->{$_}->{majorversion} > $pub->{$highest_core}->{majorversion}
        } keys %$pub;
        unless ($core) {
            $core = $highest_core;

            # update in POST to keep things in sync
            $POST->{core} = $highest_core;
        }
        $style->{layer}->{core} = $highest_core unless $style->{layer}->{core};

        $vars->{core} = $core;

        # layout lid
        $layout = $POST->{'action:change'} ? $eff_layer{'layout'} : $style->{'layer'}->{'layout'};

        # if we're changing core, clear everything
        if (   $POST->{'core'}
            && $style->{'layer'}->{'core'}
            && $POST->{'core'} != $style->{'layer'}->{'core'} )
        {
            foreach (qw(i18nc layout theme i18n user)) {
                delete $eff_layer{$_};
            }
            undef $layout;
        }

        # if we're changing layout, clear everything below
        if (   $eff_layer{'layout'}
            && $style->{'layer'}->{'layout'}
            && $eff_layer{'layout'} != $style->{'layer'}->{'layout'} )
        {
            foreach (qw(theme i18n user)) {
                delete $eff_layer{$_};
            }
        }

        ### process edit actions

        # delete
        if ( $POST->{'action:delete'} && !$noactions ) {
            LJ::S2::delete_user_style( $u, $id );
            undef $id;    # don't show form below
            return $r->redirect("styles$getextra");
        }

        # save changes
        if ( ( $POST->{'action:change'} || $POST->{'action:savechanges'} ) && !$noactions ) {
            $vars->{post_action} = 'change';

            # are they renaming their style?
            if (   $POST->{'stylename'}
                && $style->{'name'}
                && $POST->{'stylename'} ne $style->{'name'} )
            {

                # update db
                my $styleid = $style->{'styleid'};
                LJ::S2::rename_user_style( $u, $styleid, $POST->{stylename} );

                # update style object
                $style->{'name'} = $POST->{'stylename'};
            }

            # load layer info of any "other" layers
            my %other_info = ();
            if (@other_layers) {
                LJ::S2::load_layer_info( \%other_info, \@other_layers );
                foreach (@other_layers) {
                    return error_ml( '/customize/advanced/styles.tt.error.layernotfound',
                        { 'layer' => $_ } )
                        unless exists $other_info{$_};
                    return error_ml( '/customize/advanced/styles.tt.error.layernotpublic',
                        { 'layer' => $_ } )
                        unless $other_info{$_}->{'is_public'};
                }
            }

            # error check layer modifications
            my $get_layername = sub {
                my $lid = shift;

                my $name;
                $name = $pub->{$lid}->{'name'} if $pub->{$lid};
                $name ||= $ulay->{$lid}->{'name'} if $ulay->{$lid};
                $name ||= ml( '.layerid', { 'id' => $lid } );

                return $name;
            };

            # check layer hierarchy
            my $error_check = sub {
                my ( $type, $parentid ) = @_;

                my $lid = $eff_layer{$type};
                next if !$lid;

                my $layer      = $ulay->{$lid} || $pub->{$lid} || LJ::S2::load_layer($lid);
                my $parentname = $get_layername->($parentid);
                my $layername  = $get_layername->($lid);

                # is valid layer type?
                return error_ml(
                    '/customize/advanced/styles.tt.error.invalidlayertype',
                    { 'name' => "<i>$layername</i>", 'type' => $type }
                ) if $layer->{'type'} ne $type;

                # is a child?
                return error_ml(
                    '/customize/advanced/styles.tt.error.layerhierarchymismatch',
                    {
                        'layername'  => "<i>$layername</i>",
                        'type'       => $type,
                        'parentname' => "<i>$parentname</i>"
                    }
                ) unless $layer->{'b2lid'} == $parentid;

                return undef;
            };

            # check child layers of core
            foreach my $type (qw(i18nc layout)) {
                my $errmsg = $error_check->( $type, $core );
                return error_ml($errmsg) if $errmsg;
            }

            # don't check sub-layout layers if there's no layout
            if ($layout) {

                # check child layers of selected layout
                foreach my $type (qw(theme i18n user)) {
                    my $errmsg = $error_check->( $type, $layout );
                    return error_ml($errmsg) if $errmsg;
                }
            }

            # save in database
            my @layers = ( 'core' => $core );
            push @layers, map { $_, $eff_layer{$_} } qw(i18nc layout i18n theme user);
            LJ::S2::set_style_layers( $u, $style->{'styleid'}, @layers );

            # redirect if they clicked the bottom button
            return return $r->redirect("styles$getextra") if $POST->{'action:savechanges'};
        }
        $vars->{id}     = $id;
        $vars->{layout} = $layout;
        $vars->{style}  = $style;
    }
    else {
        # load user styles
        my $ustyle        = LJ::S2::load_user_styles($u);
        my @sortedustyles = sort { $ustyle->{$a} cmp $ustyle->{$b} || $a <=> $b } keys %$ustyle;

        $vars->{sortedustyles} = \@sortedustyles;
        $vars->{ustyle}        = $ustyle;

        # process create action
        if ( ( $POST->{'action:create'} && $POST->{'stylename'} ) && !$noactions ) {

            return error_ml( '/customize/advanced/styles.tt.error.maxstyles2',
                { 'numstyles' => scalar( keys %$ustyle ), 'maxstyles' => $u->count_s2stylesmax } )
                if scalar( keys %$ustyle ) >= $u->count_s2stylesmax;

            my $styleid = LJ::S2::create_style( $u, $POST->{'stylename'} );
            return error_ml('/customize/advanced/styles.tt.error.cantcreatestyle') unless $styleid;

            return $r->redirect("styles?id=$styleid$getextra_amp");
        }

        # load style currently in use
        $u->preload_props('s2_style');

    }

    my $layerselect_sub = sub {
        my ( $type, $b2lid ) = @_;

        my @opts = ();

        my $lid = $POST->{'action:change'} ? $POST->{$type} : $style->{layer}->{$type};
        $lid = $POST->{"other_$type"} if $lid eq "_other";

        # greps, and sorts a list
        my $greplist = sub {
            my $ref = shift;
            return sort { $ref->{$a}->{'name'} cmp $ref->{$b}->{'name'} || $a <=> $b }
                grep {
                my $is_active = LJ::Hooks::run_hook( "layer_is_active", $ref->{$_}->{uniq} );
                       $ref->{$_}->{'type'} eq $type
                    && $ref->{$_}->{'b2lid'} == $b2lid
                    && ( !defined $is_active || $is_active )
                    && !( $pub->{$_} && $pub->{$_}->{is_internal} )
                    && # checking this directly here, as I don't care if the parent layers are internal
                    /^\d+$/
                } keys %$ref;
        };

        # public layers
        my $name = $type eq 'core' ? 'majorversion' : 'name';
        push @opts, map { $_, $pub->{$_}->{$name} } $greplist->($pub);

        # no user core layers
        return { 'lid' => $lid, 'opts' => \@opts } if $type eq 'core';

        # user layers / using an internal layer %
        push @opts, ( '', '---' );
        my $startsize = scalar(@opts);

        # add the current layer if it's internal and the user is using it.
        push @opts,
            (
            $lid,
            LJ::Lang::ml(
                '/customize/advanced/styles.tt.stylelayers.select.layout.user',
                { 'layername' => $pub->{$lid}->{'name'}, 'id' => $lid }
            )
            ) if $lid && $pub->{$lid} && $pub->{$lid}->{is_internal};
        push @opts, map {
            $_,
                LJ::Lang::ml(
                '/customize/advanced/styles.tt.stylelayers.select.layout.user',
                { 'layername' => $ulay->{$_}->{'name'}, 'id' => $_ }
                )
        } $greplist->($ulay);

        # if we didn't push anything above, remove dividing line %
        pop @opts, pop @opts unless scalar(@opts) > $startsize;

        # add option for other layerids
        push @opts,
            (
            '_other', LJ::Lang::ml('/customize/advanced/styles.tt.stylelayers.select.layout.other')
            );

        # add blank option to beginning of list
        unshift @opts, ( '', @opts ? '' : ' ' );
        return { 'lid' => $lid, 'opts' => \@opts };
    };

    $vars->{layerselect_sub} = $layerselect_sub;
    $vars->{ehtml}           = \&LJ::ehtml;

    return DW::Template->render_template( 'customize/advanced/styles.tt', $vars );

}

sub layersource_handler {
    my ( $ok, $rv ) = controller();
    return $rv unless $ok;

    my $r             = $rv->{r};
    my $POST          = $r->post_args;
    my $GET           = $r->get_args;
    my $u             = $rv->{u};
    my $remote        = $rv->{remote};
    my $no_layer_edit = LJ::Hooks::run_hook( "no_theme_or_layer_edit", $u );

    my $pub = LJ::S2::get_public_layers();

    my $dbr = LJ::get_db_reader();

    my $id = $GET->{'id'};
    return $r->redirect('layerbrowse') unless $id =~ /^\d+$/;

    my $lay = defined $pub->{$id} ? $pub->{$id} : LJ::S2::load_layer($id);
    return error_ml('/customize/advanced/layerbrowse.tt.error.layerdoesntexist')
        unless $lay;

    my $layerinfo = {};
    LJ::S2::load_layer_info( $layerinfo, [$id] );
    my $srcview =
        exists $layerinfo->{$id}->{'source_viewable'}
        ? $layerinfo->{$id}->{'source_viewable'}
        : undef;

    # authorized to view this layer?
    my $isadmin = !defined $pub->{$id} && $remote && $remote->has_priv( 'siteadmin', 'styleview' );

    # public styles are pulled from the system account, so we don't
    # want to check privileges in case they're private styles
    return error_ml('/customize/advanced/layerbrowse.tt.error.cantviewlayer')
        unless defined $pub->{$id} && ( !defined $srcview || $srcview != 0 )
        || $srcview == 1
        || $isadmin
        || $remote && $remote->can_manage( LJ::load_userid( $lay->{userid} ) );

    my $s2code = LJ::S2::load_layer_source($id);

    # get html version of the code?
    if ( $GET->{'fmt'} eq "html" ) {
        my $html;
        my ( $md5, $save_cache );
        if ( $pub->{$id} ) {

            # let's see if we have it cached
            $md5 = Digest::MD5::md5_hex($s2code);
            my $cache =
                $dbr->selectrow_array("SELECT value FROM blobcache WHERE bckey='s2html-$id'");
            if ( $cache =~ s/^\[$md5\]// ) {
                $html = $cache;
            }
            else {
                $save_cache = 1;
            }
        }

        unless ($html) {
            my $cr = new S2::Compiler;
            $cr->compile_source(
                {
                    'source' => \$s2code,
                    'output' => \$html,
                    'format' => "html",
                    'type'   => $pub->{$id}->{'type'},
                }
            );
        }

        if ($save_cache) {
            my $dbh = LJ::get_db_writer();
            $dbh->do( "REPLACE INTO blobcache (bckey, dateupdate, value) VALUES (?,NOW(),?)",
                undef, "s2html-$id", "[$md5]$html" );
        }
        $r->print($html);
        return $r->OK;
    }

    # return text version
    $r->content_type("text/plain");
    my $ua = $r->header_in("User-Agent");
    if ( $ua && $ua =~ /\bMSIE\b/ ) {
        my $filename = "s2source-$id.txt";
        $r->header_out( 'Content-Disposition' => "attachment; filename=$filename" );
    }

    $r->print($s2code);
    return $r->OK;

}

sub layeredit_handler {
    my ( $ok, $rv ) = controller();
    return $rv unless $ok;

    my $r             = $rv->{r};
    my $POST          = $r->post_args;
    my $GET           = $r->get_args;
    my $u             = $rv->{u};
    my $remote        = $rv->{remote};
    my $no_layer_edit = LJ::Hooks::run_hook( "no_theme_or_layer_edit", $u );

    my $vars;

    # we need a valid id
    my $id;
    $id = $GET->{'id'} if $GET->{'id'} =~ /^\d+$/;
    return error_ml('/customize/advanced/layeredit.tt.error.nolayer')
        unless $id;

    return error_ml('/customize/advanced/index.tt.error.advanced.editing.denied')
        if $no_layer_edit;

    # load layer
    my $lay = LJ::S2::load_layer($id);
    return error_ml('/customize/advanced/layers.tt.error.layerdoesntexist')
        unless $lay;

    # if the b2lid of this layer has been remapped to a new layerid
    # then update the b2lid mapping for this layer
    my $b2lid = $lay->{b2lid};
    if ( $b2lid && $LJ::S2LID_REMAP{$b2lid} ) {
        LJ::S2::b2lid_remap( $remote, $id, $b2lid );
        $lay->{b2lid} = $LJ::S2LID_REMAP{$b2lid};
    }

    # get u of user they are acting as
    $u = LJ::load_userid( $lay->{userid} );

    # is authorized admin for this layer?
    return error_ml('/customize/advanced/layeredit.tt.error.layerunauthorized')
        unless $u && $remote->can_manage($u);

    # check priv and ownership
    return error_ml('/customize/advanced/layeredit.tt.error.stylesunauthorized')
        unless $u->can_create_s2_styles;

    # at this point, they are authorized, allow viewing & editing

    # get s2 code from db - use writer so we know it's up-to-date
    my $dbh    = LJ::get_db_writer();
    my $s2code = LJ::S2::load_layer_source( $lay->{s2lid} );

    # we tried to compile something
    my $build;
    if ( $POST->{'action'} eq "compile" ) {
        return error_ml("error.invalidform") unless LJ::check_form_auth( $POST->{lj_form_auth} );

        $build = "<b>S2 Compiler Output</b> <em>at " . scalar(localtime) . "</em><br />\n";

        my $error;
        $POST->{'s2code'} =~ tr/\r//d;    # just in case
        unless ( LJ::S2::layer_compile( $lay, \$error, { 's2ref' => \$POST->{'s2code'} } ) ) {

            $error =~ s/LJ::S2,.+//s;
            $error =~ s!, .+?(src/s2|cgi-bin)/!, !g;

            $error =~
s/^Compile error: line (\d+), column (\d+)/Compile error: <a href="javascript:moveCursor($1-1,$2)">line $1, column $2<\/a>/;

            $build .=
                "Error compiling layer:\n<pre style=\"border-left: 1px red solid\">$error</pre>";

        }
        else {
            $build .= "Compiled with no errors.\n";
        }
        $r->print($build);
        return $r->OK;
    }

    # load layer info
    my $layinf = {};
    LJ::S2::load_layer_info( $layinf, [$id] );

    # find a title to display on this page
    my $type = $layinf->{$id}->{'type'};
    my $name = $layinf->{$id}->{'name'};

    # find name of parent layer if this is a child layer
    if ( !$name && $type =~ /^(user|theme|i18n)$/ ) {
        my $par = $lay->{'b2lid'} + 0;
        LJ::S2::load_layer_info( $layinf, [$par] );
        $name = $layinf->{$par}->{'name'};
    }

    # Only use the layer name if there is one and it's more than just whitespace
    my $title = "[$type] ";
    $title .= $name && $name =~ /[^\s]/ ? "$name [\#$id]" : "Layer \#$id";
    $vars->{build}  = "Loaded layer $id.";
    $vars->{title}  = $title;
    $vars->{s2code} = $s2code;

    return DW::Template->render_template( 'customize/advanced/layeredit.tt',
        $vars, { no_sitescheme => 1 } );

}

1;
