Source: src/policies/thread_management_policy.js

/**
 * @file manage policy for common thread management actions
 * @author Andrew Sayers
 * This file defines thread management policy in the abstract,
 * but sadly needs to be tightly coupled to the interface.
 */

/**
 * @summary manage policy for common thread management actions
 * @constructor
 * @example
 * var policy = new ThreadManagementPolicy({
 *
 *     // objects:
 *     v : v, // Variables object
 *     bb: bb, // BulletinBoard object
 *     mod_team_bb: bb, // BulletinBoard object for the moderation team
 *     mc: mc, // MiscellaneousCache object
 *     vi: vi, // Violations object
 *
 *     // configuration:
 *     thread_id     : 1234,
 *     thread_desc   : 'thread description',
 *     loading_html  : 'loading, please wait...',
 *     user          : { username: 'thread creator username', user_id: 12345 }
 *     callback: function( action, summary, template, has_post, has_pm ) { ... };
 *
 *     // widget placement:
 *         post_selector_args: { container: $(    '.post_selector_container') },
 *           pm_selector_args: { container: $(      '.pm_selector_container') },
 *     template_selector_args: { container: $('.template_selector_container') }
 *      message_selector_args: { container: $( '.message_selector_container') }
 * });
 */
function ThreadManagementPolicy(args) {

    Policy.call( this, args );

    var policy = this;

    var action_data = {
        thread: { thread_id: args.thread_id, thread_desc: args.thread_desc, forum_id: args.forum_id },
        template: 'no template'
    };
    var variable_suffix = [action_data.template];
    this.variable_suffix = variable_suffix;

    var forum_has_icons;

    var unmerge_data;

    var macro_defaults = { template: 'no template' };
    var recent_macros = new RecentList({ ss: args.ss, name: 'recent thread management macros', });

    var thread_creator = this.user = {
        is_target: true,
        username: args.first_post.username,
        user_id : args.first_post.user_id
    };
    this.default_keys = [

        { type: 'thread'     , name: 'old thread'      , value: { thread_id: args.thread_id, thread_desc: args.thread_desc } },
        { type: 'thread'     , name: 'new thread'      , value: { thread_id: args.thread_id, thread_desc: args.thread_desc } },
        { type: 'forum'      , name: 'old forum'       , value: {  forum_id: args. forum_id,  forum_desc: args. forum_desc } },
        { type: 'forum'      , name: 'new forum'       , value: {  forum_id: args. forum_id,  forum_desc: args. forum_desc } },
        { type: 'literal'    , name: 'new prefix'      , value: '' },
        { type: 'image'      , name: 'new icon'        , value: '' },

        { type: 'literal'    , name: 'user-friendly description of actions', value: '' },
        { type: 'literal'    , name:  'mod-friendly description of actions', value: '' },

        { type: 'username'   , name: 'username'        , value: thread_creator },
        { type: 'username'   , name: 'thread creator'  , value: thread_creator },
        { type: 'literal'    , name: 'mod team user id', value: args.mod_team_user.user_id },
        { type: 'literal'    , name: 'first post id'   , value: args.first_post.post_id },
        { type: 'action data', name: 'action data'     , value: action_data },
        { type: 'literal'    , name: 'template'        , value: 'no template' }

    ];

    /*
     * BUILD CALLBACK
     */

    var original, metadata;
    function callback_timeout() {

        cb_timeout = null;

        var actions = [];

        var has_post = message_selector.find('[name="post"]').prop('checked');
        var has_pm   = message_selector.find('[name="pm"]'  ).prop('checked');

        variable_suffix.splice(1);

        if ( status_widget.val() == 'merged' ) { // Merging is a completely different process to everything else

            variable_suffix.push('merge');

            var merge_post_id;

            var edit_action = new Action( 'close thread', {
                fire: function(keys) {
                    var destination_thread = title_widget.val();
                    post.thread_id = destination_thread.target_thread_id;
                    action_data.destination_thread = destination_thread = {
                        thread_id: destination_thread.target_thread_id,
                        thread_desc: destination_thread.target_thread_desc,
                        forum_id: destination_thread.target_forum_id
                    };
                    post.bb = args.mod_team_bb; // merge posts are always sent from the mod account, so they can be searched for
                    return args.bb.thread_edit({
                        thread_id   : args.thread_id,
                        title       : args.thread_desc,
                        notes       : policy.resolve('edit notes', keys ),
                        close_thread: true
                    }).then(function(html) { // Get information from the closed thread
                        return args.bb.thread_posts( args.thread_id, html ).then(function (posts) {
                            var return_keys = {
                                'list of posts in merged thread':
                                    '[TABLE="class: outer_border"]\n' +
                                        '[TR][TH]Post #[/TH][TH]author[/TH][TH]summary[/TH][/TR]\n' +
                                        posts.map(function(post, index) {
                                            return (
                                                '[TR][TD][center][post=' + post.post_id + ']#' + (index+1) + '[/post][/center][/TD][TD]' +
                                                '[URL="' + location.origin + args.bb.url_for.user_show({ user_id: post.user_id }) + '"]' + post.username + '[/URL][/TD][TD]' +
                                                args.bb.post_summary(post) + '[/TD][/TR]\n'
                                             );
                                        }).join('') +
                                    '[/TABLE]',
                                'merge data': args.bb.stringify( 'merge data', {
                                    date: new Date().getTime(),
                                    source_thread: unmerge_data,
                                    destination_thread: destination_thread,
                                    posts: posts.map(function(post) { return post.post_id })
                                })
                            };
                            post.add_known_keys( return_keys );
                            pm  .add_known_keys( return_keys );
                            return { keys: return_keys };
                        });
                    });
                },
                description: function() {
                    return [
                        {
                            type: 'change thread status',
                            target: { thread_id: args.thread_id, thread_desc: args.thread_desc, forum_id: args.forum_id }
                        }
                    ];
                },
                blockers: function() {
                    if ( !title_widget.val().target_thread_id ) return [ 'Please select a valid merge target' ];
                }
            });

            var log_action = new Action( 'post in merge log', {
                fire: function(keys) {
                    return args.bb.thread_reply({ // Notify/save all posts in thread
                        thread_id: args.v.resolve('frequently used posts/threads', 'merge log'),
                        title    : policy.resolve('merge title' , keys),
                        bbcode   : policy.resolve('merge bbcode', keys)
                    }).then(function(post_id) {
                        merge_post_id = action_data.merge_log_post_id = post_id;
                        return { keys: { 'merge log post id': merge_post_id } };
                    });
                },
                description: function() {
                    var destination_thread = title_widget.val();
                    destination_thread = {
                        thread_id: destination_thread.target_thread_id,
                        thread_desc: destination_thread.target_thread_desc,
                        forum_id: destination_thread.target_forum_id
                    };
                    return [
                        {
                            type: 'post',
                            target: {
                                thread_desc: 'the merge log',
                                thread_id  : args.v.resolve('frequently used posts/threads', 'merge log'),
                                post_id    : merge_post_id
                            }
                        },
                    ];
                }
            });

            var merge_action = new Action( 'merge threads', {
                fire: function(keys) {
                    var destination_thread = title_widget.val();
                    return args.bb.thread_merge({
                        forum_id  :   destination_thread.target_forum_id,
                        thread_ids: [ destination_thread.target_thread_id, args.thread_id ],
                    });
                },
                description: function() {
                    var destination_thread = title_widget.val();
                    destination_thread = {
                        thread_id: destination_thread.target_thread_id,
                        thread_desc: destination_thread.target_thread_desc,
                        forum_id: destination_thread.target_forum_id
                    };
                    return [
                        {
                            type: 'merge threads',
                            source_thread: { thread_id: args.thread_id, thread_desc: args.thread_desc, forum_id: args.forum_id },
                            thread_creator: { username: args.first_post.username, user_id : args.first_post.user_id },
                            destination_thread: destination_thread
                        }
                    ];
                }
            });

            actions[0] = new Action( 'merge wrapper', edit_action.then(log_action.then(merge_action)) )

            policy.default_keys[7].value = ['merge'];

            if ( has_pm   ) {
                pm  .val( policy.notification_selector_args({ html: 'PM', type: 'PM' }) );
                actions[0].then( policy.notification_selector_action(pm  ) );
            }
            if ( has_post ) {
                post.val( policy.notification_selector_args({ html: 'post', type: 'post' }) );
                actions[0].then( policy.notification_selector_action(post) );
            }

        } else {

            var edit_actions = [],
                userfriendly_description = [],
                edit_target = { thread_id: args.thread_id, thread_desc: args.thread_desc, forum_id: args.forum_id }
            ;

            if ( status_widget.val() != original.status ) {
                var status = status_widget.val();
                if ( status == 'closed temporarily' ) {
                    action_data.deadline = status_widget.find('option:selected').data('deadline');
                    var thread_id = args.v.resolve('frequently used posts/threads', 'Moderation Chase-Up Thread' );
                    var post_id;
                    actions.push( new Action( 'chase-up post', {
                        fire: function(keys) {
                            return args.bb.thread_reply({
                                thread_id: thread_id,
                                title    : policy.resolve( 'deadline post title' , keys ),
                                bbcode   : policy.resolve( 'deadline post bbcode', keys ),
                            }).then(function(_post_id) {
                                action_data.chaseup_post_id = post_id = _post_id;
                                return { keys: {
                                    'chase-up post id': post_id,
                                    'extra actions'   : '[post=' + post_id + ']replied to the chase-up thread[/post]'
                                }};
                            });
                        },
                        description: function() { // description
                            return [{
                                type: 'post',
                                target: {
                                    thread_desc: 'the chase-up thread',
                                    thread_id  : thread_id,
                                      post_id  :   post_id
                                }
                            }];
                        }
                    }));
                }
                edit_actions.push( { type: 'change thread status', target: edit_target } );
                variable_suffix.push('change status', status);
                userfriendly_description.push('{{'+policy._namespace+': change status: ' + status + '}}');
            }
            if ( (prefix_widget.val()||'') != original.prefix ) {
                edit_actions.push( { type: 'change thread prefix', target: edit_target } );
                variable_suffix.push('change prefix');
                userfriendly_description.push('{{'+policy._namespace+': change prefix}}');
            }
            if ( forum_has_icons && icon_widget.getSelectedIndex() != original.icon ) {
                edit_actions.push( { type: 'change thread icon', target: edit_target } );
                variable_suffix.push('change icon');
                userfriendly_description.push('{{'+policy._namespace+': change icon}}');
            }

            if ( forum_widget.val() != original.forum_id ) {
                variable_suffix.push('change forum');
                userfriendly_description.push('{{'+policy._namespace+': change forum}}');
                if ( title_widget.val().thread_desc != original.title.thread_desc ) {
                    variable_suffix.push('change title');
                    userfriendly_description.push('{{'+policy._namespace+': change title}}');
                }
                actions.push( new Action(
                    'move thread',
                    {
                        fire: function(keys) {
                            var option = forum_widget.find(':selected');
                            action_data.destination_forum_id = option.val();
                            return args.bb.thread_move({
                                thread_id     : args.thread_id,
                                         title: title_widget.val().thread_desc,
                                redirect_title: original.title.thread_desc,
                                forum_id      : option.val(),
                                redirect      : parse_duration( policy.resolve( 'redirect deadline', keys ) )
                            });
                        },
                        description: function() {
                            var option = forum_widget.find(':selected');
                            var change_forum = {
                                type: 'change thread forum',
                                target: $.extend({
                                    forum_id: option.val(),
                                    forum_desc: option.text()
                                }, edit_target )
                            };
                            if ( title_widget.val().thread_desc == original.title.thread_desc )
                                return [change_forum];
                            else
                                return [
                                    change_forum,
                                    { type: 'change thread title', target: edit_target }
                                ];
                        },
                        blockers: function() {
                            if ( !forum_widget.val() ) return [ 'Please select a target forum' ];
                        }
                    }
                ));
            } else if ( title_widget.val().thread_desc != original.title.thread_desc ) {
                // we can retitle a thread at the same time as moving it
                edit_actions.push( { type: 'change thread title', target: edit_target } );
                variable_suffix.push('change title');
                userfriendly_description.push('{{'+policy._namespace+': change title}}');
            }

            policy.default_keys[6].value = userfriendly_description;
            policy.default_keys[7].value = userfriendly_description
                .map(function(d) { return d.replace(/{{'+policy._namespace+': (.*)}}/, '$1') });

            if ( edit_actions.length )
                actions.push( new Action(
                    'edit thread',
                    {
                        fire: function(keys) {
                            var edit_info = action_data.edit_info = {
                                thread_id       : args.thread_id,
                                title           : title_widget.val().thread_desc,
                                notes           : policy.resolve('edit notes', keys ),
                                icon_id         : forum_has_icons ? icon_widget.getSelectedValue() : undefined,
                                prefix_id       : prefix_widget.val(),
                                close_thread    : status_widget.val() == 'closed' || status_widget.val() == 'closed temporarily',
                                unapprove_thread: status_widget.val() == 'moderated',
                                delete_thread   : status_widget.val() == 'deleted',
                                delete_reason   : policy.resolve('delete reason', keys ),
                            };
                            return args.bb.thread_edit(edit_info);
                        },
                        description: function() {
                            return edit_actions;
                        },
                    }
                ));

            // need to do this before building the messages...
            var bump_value = bump_selector.find(':checked').val();
            //if ( bump_value ) variable_suffix.push(bump_value); // informing users about this would only cause problems

            if ( has_pm   ) {
                pm  .val( policy.notification_selector_args({ html: 'PM', type: 'PM' }) );
                actions.push( policy.notification_selector_action(pm  ) );
            }
            if ( has_post ) {
                post.val( policy.notification_selector_args({ html: 'post', type: 'post' }) );
                actions.push( policy.notification_selector_action(post) );
            }

            // ... but de-bumping must happen *after* any post action bumps the thread
            switch ( bump_value ) {
            case 'bump':
                if ( !has_post ) {
                    // bumping is redundant if you reply to a thread
                    actions.push( new Action(
                        'bump thread',
                        {
                            fire: function(keys) { return args.bb.thread_bump(args.thread_id) },
                            description: function() {
                                return [{ type: 'bump thread', target: edit_target }];
                            }
                        }
                    ));
                }
                break;
            case 'debump':
                var debump_action = new Action(
                    'debump thread',
                    {
                        fire: function(keys) { return args.bb.thread_debump(args.thread_id) },
                        description: function() {
                            return [{ type: 'debump thread', target: edit_target }];
                        }
                    }
                );
                if ( has_post )
                    actions[actions.length-1].then( debump_action );
                else
                    actions.push(debump_action);
                break;
            case '': // nothing to do
            }

        }

        if ( has_pm   ) policy.default_keys[7].value.push('PM');
        if ( has_post ) policy.default_keys[7].value.push('reply');

        policy.default_keys[7].value = policy.default_keys[7].value.join('; ');

        if ( macro_defaults.template && policy.check( [ 'macro' ].concat(variable_suffix) ) ) {

            var macro_values = { template: macro_defaults.template };

            switch ( status_widget.val() ) {
            case 'closed temporarily':
                var deadline = status_widget.find('option:selected').data('deadline');
                if ( (macro_defaults.deadline||'') != deadline ) {
                    macro_values.status = 'closed temporarily';
                    macro_values.deadline = deadline;
                }
                break;
            case 'merged':
                var target_thread_id = title_widget.val().target_thread_id;
                if ( (macro_defaults.target_thread_id||NaN) != target_thread_id ) {
                    macro_values.status = 'merged';
                    macro_values.target_thread_id = target_thread_id;
                }
            case macro_defaults.status:
                break;
            default:
                macro_values.status = status_widget.val();
            }

            if ( macro_defaults. forum_id !=   forum_widget.val()      ) macro_values.forum_id = parseInt( forum_widget.val(), 10 );
            if ( macro_defaults.prefix_id != (prefix_widget.val()||'') ) macro_values.prefix_id = prefix_widget.val()||'';
            if ( macro_defaults.  icon    !=    icon_widget.getSelectedIndex() && forum_has_icons )
                macro_values.icon = icon_widget.getSelectedIndex();


            if ( macro_defaults.has_pm   != has_pm   ) macro_values.has_pm   = has_pm  ;
            if ( macro_defaults.has_post != has_post ) macro_values.has_post = has_post;

            if ( Object.keys(macro_values).length > 1 ) {
                actions.push(new Action( 'register macro', {
                    fire: function(keys) {
                        macro_values.name = policy.resolve( [ 'macro' ].concat(variable_suffix), keys );
                        return recent_macros.push(macro_values);
                    }
                }));
            }

        }

        if ( actions.length ) {

            args.callback(
                new Action(
                    'root action',
                    // this has to be split out from the notification actions because they're performed with the mod team account:
                    new Action( policy._namespace + ' wrapper', actions ).then(new Action( 'add usernote', {
                        fire: function(keys) {
                            var return_keys = {};
                            if ( !keys.hasOwnProperty(  'pm result') ) return_keys[  'pm result'] = 'no action';
                            if ( !keys.hasOwnProperty('post result') ) return_keys['post result'] = 'no action';
                            $.extend( keys, return_keys );
                            return args.bb.usernote_add(
                                thread_creator.user_id,
                                policy.resolve( 'combined note title' , keys ),
                                policy.resolve( 'combined note bbcode', keys )
                            ).then(function() {
                                if ( keys['notification error' ] == 'fail' ) {
                                    return $.Deferred()
                                        .reject('Notification failed, but the error was postponed so we could add a usernote:\n' + error)
                                        .promise();
                                } else {
                                    return { keys: return_keys };
                                }
                            });
                        },
                        description: function() {
                            return [{ type: 'usernote', target: thread_creator }];
                        }
                    }))
                ),
                'manage [thread=' + args.thread_id + ']' + args.thread_desc + '[/thread]',
                has_post, has_pm,
                status_widget.val() == 'merged' && sender_selector.find('input:checked').val() == 'personal'
            );

        } else {
            args.callback( null, '', has_post, has_pm );
        }

    }

    var cb_timeout, callback = (
        args.callback
        ? function() {
            if ( cb_timeout ) clearTimeout(cb_timeout);
            cb_timeout = setTimeout(callback_timeout, 0 );
        }
        : function() {}
    );

    /*
     * INITIALISE WIDGETS
     */

    var was_merged = false;
    var status_widget = $(
        '<select title="set the thread status">' +
            '<option title="open the thread to replies from ordinary users" value="open">Open: </option>' +

            this.resolve( 'close periods', {}, 'array of items' ).map(function(period) {
                period = period.value.split(/\s*:\s*/, 2 );
                return '<option title="close the thread and make a note to reopen it" value="closed temporarily" data-deadline="' + period[0] + '">' + period[1] + ': </options>';
            }).join('') +
            '<option title="permanently close the thread" value="closed" data-deadline="permanent">Closed: </option>' +

            '<option title="merge this with the specified thread" value="merged">Merged with: </option>' +
            '<option title="unapprove this thread" value="moderated">Moderated: </option>' +
            '<option title="(soft-)delete this thread" value="deleted">Deleted: </option>' +
        '</select>'
    )
        .appendTo(args.status_args.container)
        .change(function() {
            if ( this.value == 'merged' ) {
                 forum_widget              .prop( 'disabled', true );
                prefix_widget              .prop( 'disabled', true );
                bump_selector.find('input').prop( 'disabled', true );
                title_widget.val({ mode: 'merge', target_thread_id: null, target_thread_desc: '' });
                title_widget.focus();
                $('#mod-friend-thread-metadata-icons').addClass('disabled');
                was_merged = true;
            } else {
                if ( was_merged ) {
                     forum_widget              .prop( 'disabled', false );
                    status_widget              .prop( 'disabled', false );
                    bump_selector.find('input').prop( 'disabled', false );
                    $('#mod-friend-thread-metadata-icons').removeClass('disabled');
                    title_widget.val({ mode: 'edit', target_thread_id: args.thread_id });
                    forum_widget.val(metadata.forum_id);
                    if ( metadata.prefixes.length ) {
                        prefix_widget.empty().show().append(metadata.prefixes.map(function(option) {
                            return $('<option>').val(option.prefix_id).text(option.text).prop( 'checked', option.checked );
                        }));
                    } else {
                        prefix_widget.empty().hide();
                    }
                }
            }
            callback();
        });

    args.icon_args.container.append('<span title="specify the thread icon" id="mod-friend-thread-metadata-icons"></span>' );
    var icon_widget = new IconSelect('mod-friend-thread-metadata-icons', {
        selectedIconWidth: 16,
        selectedIconHeight: 16,
        iconsWidth: 16,
        iconsHeight: 16,
        boxIconSpace: 0,
        vectoralIconNumber: 3,
        horizontalIconNumber: 5
    });

    var prefix_widget = $('<select title="specify the thread prefix"></select>').appendTo(args.prefix_args.container).hide();
    prefix_widget.change(function() {
        policy.default_keys[4].value = $( ':selected', this ).text();
        callback();
    });

    var prev_title_thread_id = args.thread_id;
    var title_widget = new ThreadTitleSelector($.extend({}, args, args.title_args, {
        mode: 'edit',
        callback: function(thread) {
            if ( !forum_widget ) return; // skip during creation
            if ( thread ) {
                policy.default_keys[1].value = thread;
                if ( prev_title_thread_id != thread.thread_id ) {
                    prev_title_thread_id = thread.thread_id;
                    forum_widget.val( thread.forum_id ).change();
                }
            } else {
                policy.default_keys[1].value = { thread_id: args.thread_id, thread_desc: args.thread_desc, forum_id: args.forum_id };
                forum_widget.val( args.forum_id ).change();
            }
            callback();
        }
    }));

    var forum_widget = $('<select title="move this thread to another forum" required><option value="">Please select a forum...</option></select>').appendTo(args.forum_args.container);
    var forum_metadata = {}, stored_prefixes = {}, old_forum_id;
    forum_widget.change(function() {
        if ( !this.value ) return;
        policy.default_keys[3].value.forum_id   = this.value;
        policy.default_keys[3].value.forum_desc = $( ':selected', this ).text().replace( /^\s+/, '' );
        var old_prefix = stored_prefixes[old_forum_id] = prefix_widget.val();
        var stored_prefix = stored_prefixes[this.value];
        old_forum_id = this.value;
        if ( !forum_metadata[this.value] ) forum_metadata[this.value] = args.bb.forum_metadata(this.value);
        forum_metadata[this.value].then(function(forum_metadata) {
            metadata = forum_metadata;
            if ( forum_has_icons ) {
                forum_has_icons = forum_metadata.icons.length;
                if ( !forum_has_icons )
                    $('#mod-friend-thread-metadata-icons').hide();
            } else {
                forum_has_icons = forum_metadata.icons.length;
                if ( forum_has_icons ) {
                    var default_icon;
                    icon_widget.refresh(
                        forum_metadata.icons.map(function(icon, index) {
                            if ( !icon.file ) default_icon = index;
                            return {
                                'iconFilePath': icon.file || 'data:image/gif;base64,R0lGODlhEAAQAIAAAP///////yH5BAEKAAEALAAAAAAQABAAAAIOjI+py+0Po5y02ouzPgUAOw==',
                                'iconValue': icon.icon_id
                            };
                        })
                    );
                    icon_widget.setSelectedIndex( ( original.icon == -1 ) ? default_icon : original.icon );
                    $('#mod-friend-thread-metadata-icons').show();
                }
            }

            if ( forum_metadata.prefixes.length ) {
                var has_old_prefix, has_stored_prefix;
                prefix_widget.show().empty().append(forum_metadata.prefixes.map(function(option) {
                    if (    old_prefix && option.prefix_id ==    old_prefix ) has_old_prefix    = true;
                    if ( stored_prefix && option.prefix_id == stored_prefix ) has_stored_prefix = true;
                    return $('<option>').val(option.prefix_id).text(option.text);
                })).val(
                    has_old_prefix    ?    old_prefix :
                    has_stored_prefix ? stored_prefix :
                    ''
                );
                macro_defaults.prefix_id = prefix_widget.val()||'';
            } else {
                prefix_widget.empty().hide();
            }
            callback();
        });
    });

    var post = new NotificationSelector(this.notification_selector_args(
        { html: 'post', type: 'post' },
        args.post_selector_args,
        { known_keys: ['list of posts in merged thread'], key_prefix: 'post ' }
    ));

    var pm = new NotificationSelector(this.notification_selector_args(
        { html: 'PM', type: 'PM' },
        args.pm_selector_args,
        { known_keys: ['list of posts in merged thread'], key_prefix: 'pm ' }
    ));

    var message_selector = $(
        '<div>' +
            '<label title="post a new reply in this thread"><input type="checkbox" name="post">post to thread</label>' +
            '<label><input type="checkbox" name="pm">PM the thread creator</label>' +
        '</div>'
    ).appendTo(args.message_selector_args.container);
    message_selector.find('input').change(callback)
        .last().parent().attr( 'title', 'send a private message to ' + thread_creator.username );

    var sender_selector = $(
        '<div>' +
            '<label title="more official, protects your privacy"><input type="radio" value="team" name="sender" checked>send from mod team account</label>' +
            '<label title="more individual, can ease tensions"><input type="radio" value="personal" name="sender">send from personal account</label>' +
        '</div>'
    ).appendTo(args.sender_selector_args.container);
    sender_selector.find('input').change(function() {
        if ( this.checked == (this.value == 'team' ) )
            post.bb = pm.bb = args.mod_team_bb;
        else
            post.bb = pm.bb = args.bb;
    }).filter(':checked').change();

    var bump_selector = $(
        '<div>' +
            '<label title="push this thread up to the top of the listings"><input type="radio" name="bump" value="bump">bump thread</label>' +
            '<label><input type="radio" name="bump" checked>no bump</label>' +
            '<label title="hide this thread far down in the listings"><input type="radio" name="bump" value="debump">de-bump thread</label>' +
        '</div>'
    ).appendTo(args.bump_selector_args.container);
    bump_selector.find('input').change(callback);

    var template_selector = $(
        '<select title="select a template reply or specify actions by hand" name="response_template"><option value="No template">Please choose a template...</option></select>'
    ).appendTo(args.template_selector_args.container);

    var seen_macros = {};
    var macros_to_insert = recent_macros.get().reverse().filter(function(macro) {
        if ( seen_macros.hasOwnProperty(macro.name) ) return false;
        seen_macros[macro.name] = true;
        return true;
    });
    if ( macros_to_insert.length ) {
        $('<optgroup>').attr( 'label', 'Recent' )
            .appendTo(template_selector)
            .append(
                macros_to_insert.map(function(macro) {
                    return $('<option>')
                        .text (macro.name)
                        .val  (macro.template)
                        .attr( 'title', policy.resolve(['hint',macro.template]) )
                        .data( 'macro', macro )
                })
            );
    }
    template_selector.append(

        this.resolve( 'visible templates', {}, 'array of items' ).map(function(templates) {
            templates = templates.value.split( /\s*[:,]\s*/g );
            var name = templates.shift();
            var optgroup = $('<optgroup>').attr( 'label', name );
            optgroup.append( templates.map(function(t) {
                return $('<option>')
                    .text(t)
                    .val (t)
                    .attr( 'title', policy.resolve(['hint',t]) )
            }) );
            return optgroup;
        })

    ).change(function() {

        /*
         * Update everything based on the new template
         */

        policy.default_keys[policy.default_keys.length-1].value
            = variable_suffix[0]
            = action_data.template
            = $(this).val();

        title_widget.val(original.title);

        if        ( policy.check( [ 'new thread id' ].concat(variable_suffix) ) ) {
            status_widget.val( 'merged' ).change();
            title_widget.val({
                target_thread_id: parseInt( policy.resolve([ 'new thread id' ].concat(variable_suffix)).toLowerCase(), 10 )
            });
        } else if ( policy.check( [ 'deadline' ].concat(variable_suffix) ) ) {
            var deadline = policy.resolve([ 'deadline' ].concat(variable_suffix)).toLowerCase();
            status_widget.find('option')
                .prop( 'selected', false )
                .filter(function() { return $(this).data('deadline') == deadline })
                .prop( 'selected', true )
                .change()
            ;
        } else if ( policy.check( [ 'new status' ].concat(variable_suffix) ) ) {
            status_widget.val( policy.resolve([ 'new status' ].concat(variable_suffix)).toLowerCase() ).change();
        } else {
            status_widget.val( original.status ).change();
        }

        if ( policy.check( [ 'new thread title' ].concat(variable_suffix) ) ) {
            title_widget.val({
                target_thread_id  : args.thread_id,
                target_thread_desc: policy.resolve( [ 'new thread title' ].concat(variable_suffix), { 'old thread title': args.thread_desc } ),
                target_forum_id   : args.forum_id,
            });
        }

        if ( policy.check( [ 'new icon' ].concat(variable_suffix) ) ) {
            var icon_name = policy.resolve( [ 'new icon' ].concat(variable_suffix) );
            metadata.icons.map(function(icon, index) {
                if ( icon.name == icon_name ) icon_widget.setSelectedIndex(index);
            });
        } else if ( forum_has_icons ) {
            if ( original.icon == -1 ) {
            metadata.icons.map(function(icon, index) {
                if ( !icon.file ) icon_widget.setSelectedIndex(index);
            });
            } else {
                icon_widget.setSelectedIndex(original.icon);
            }
        }

        if ( policy.check( [ 'new forum' ].concat(variable_suffix) ) ) {
            var new_forum = policy.resolve( [ 'new forum' ].concat(variable_suffix) ).toLowerCase();
            forum_widget.find('option')
                .prop( 'selected', false )
                .filter(function() { return this.textContent.toLowerCase() == new_forum })
                .prop( 'selected', true )
            ;
        } else {
            forum_widget.val( original.forum_id );
        }
        forum_widget.change();

        if ( policy.check( [ 'new prefix' ].concat(variable_suffix) ) ) {
            var new_prefix = policy.resolve( [ 'new prefix' ].concat(variable_suffix) );
            prefix_widget.find('option')
                .prop( 'selected', false )
                .filter(function() { return this.textContent.toLowerCase() == new_prefix })
                .prop( 'selected', true )
            ;
        } else {
            prefix_widget.val( original.prefix );
        }
        prefix_widget.change();

        message_selector.find('[name="post"]').prop( 'checked', policy.check( [ 'post bbcode' ].concat(variable_suffix) ) );
        post.val( policy.notification_selector_args({ html: 'post', type: 'post' }) );

        message_selector.find('[name="pm"]'  ).prop( 'checked', policy.check( [   'pm bbcode' ].concat(variable_suffix) ) );
        pm  .val( policy.notification_selector_args({ html: 'PM', type: 'PM' }) );

        // record macro defaults even if this action came from a macro, so frequent actions are frequently updated:
        macro_defaults = {
            template: variable_suffix[0],
              status: status_widget.val(),
            deadline: status_widget.find('option:selected').data('deadline') || '',
            has_post: message_selector.find('[name="post"]').prop( 'checked' ),
            has_pm  : message_selector.find('[name="pm"]'  ).prop( 'checked' ),

                     icon   :   forum_has_icons ? icon_widget.getSelectedIndex() : -1,
                    forum_id:  forum_widget.find('option:selected').val() || '',
                   prefix_id: prefix_widget.val()||'',
            target_thread_id:  title_widget.val().target_thread_id || NaN
        };

        var macro = $( 'option:selected', this ).data( 'macro' );
        if ( macro ) {

            switch ( macro.status ) {
            case undefined: break;
            case 'closed temporarily':
                status_widget.find('option')
                    .prop( 'selected', false )
                    .filter(function() { return $(this).data('deadline') == macro.deadline })
                    .prop( 'selected', true )
                ;
                status_widget.change();
                title_widget.val({ mode: 'edit' });
                break;
            case 'merged':
                status_widget.val('merged').change();
                title_widget.val({ mode: 'merge', target_thread_id: macro.target_thread_id });
                break;
            default:
                title_widget.val({ mode: 'edit' });
                status_widget.val( macro[status] ).change();
            }

            if ( macro.hasOwnProperty('icon'    ) ) icon_widget.setSelectedIndex( macro.icon );
            if ( macro.hasOwnProperty('has_pm'  ) ) message_selector.find('[name="pm"]'  ).prop('checked', macro.has_pm  ).change();
            if ( macro.hasOwnProperty('has_post') ) message_selector.find('[name="post"]').prop('checked', macro.has_post).change();

            if ( macro.hasOwnProperty('prefix_id') ) {
                if ( macro.hasOwnProperty('forum_id') ) {
                    forum_widget.val(macro.forum_id);
                    forum_metadata[macro.forum_id].then(function() {
                        prefix_widget.val(macro.prefix_id).change();
                    });
                } else {
                    prefix_widget.val(macro.prefix_id).change();
                }
            } else if ( macro.hasOwnProperty('forum_id') ) {
                forum_widget.val(macro.forum_id).change();
            }

        }

        callback();

    });

    /*
     * Download information and initialise a few last things
     */

    this.promise = $.when(
        args.bb.thread_metadata(args.thread_id),
        args.bb.forums()
    ).then(function(_metadata, forums) {

        metadata = _metadata;

        status_widget.val(
            metadata.deleted   ? 'deleted'   :
            metadata.moderated ? 'moderated' :
            metadata.open      ? 'open'      :
                                 'closed'
        );

        var selected_index;
        forum_has_icons = metadata.icons.length;
        if ( forum_has_icons ) {
            icon_widget.refresh(
                metadata.icons.map(function(icon, index) {
                    if ( icon.icon_id == metadata.icon_id ) selected_index = index;
                    return {
                        'iconFilePath': icon.file || 'data:image/gif;base64,R0lGODlhEAAQAIAAAP///////yH5BAEKAAEALAAAAAAQABAAAAIOjI+py+0Po5y02ouzPgUAOw==',
                        'iconValue': icon.icon_id
                    };
                })
            );
            icon_widget.setSelectedIndex(selected_index);
            $('#mod-friend-thread-metadata-icons').show();
        } else {
            $('#mod-friend-thread-metadata-icons').hide();
        }
        $('#mod-friend-thread-metadata-icons').on( 'changed', function() {
            policy.default_keys[5].value = (
                                          metadata.icons[icon_widget.getSelectedIndex()].file
                ? location.origin + '/' + metadata.icons[icon_widget.getSelectedIndex()].file
                : 'data:image/gif;base64,R0lGODlhEAAQAIAAAP///////yH5BAEKAAEALAAAAAAQABAAAAIOjI+py+0Po5y02ouzPgUAOw=='
            );
            callback();
        });

        if ( metadata.prefixes.length ) {
            prefix_widget
                .show()
                .empty()
                .append(metadata.prefixes.map(function(option) { return $('<option>').val(option.prefix_id).text(option.text) }))
                .val( metadata.prefix_id );
        }

        (function build_forums(options, prefix, optgroup) {
            options.forEach(function(option) {
                if ( option.forum_id ) {
                    var node = $('<option>').val(option.forum_id);
                    if ( option.forum_id == args.forum_id )
                        node.text(prefix + option.name + ' (current forum)').addClass('current-forum');
                    else
                        node.text(prefix + option.name);
                    optgroup.append( node );
                    if ( option.children )
                        build_forums( option.children, prefix + '\xA0\xA0\xA0\xA0', optgroup );
                } else if ( option.children ) {
                    build_forums( option.children, prefix, $('<optgroup>').attr( 'label', prefix + option.name ).appendTo(forum_widget) );
                }
            });
        })( forums, '', forum_widget );
        old_forum_id = metadata.forum[metadata.forum.length-1].forum_id;
        forum_widget.val( metadata.forum[metadata.forum.length-1].forum_id );

        original = action_data.original = {
            thread_id: args.thread_id,
            status   : status_widget.val(),
            title    : $.extend( {}, title_widget.val() ),
            forum_id : parseInt( forum_widget.val(), 10 ),
            prefix   : prefix_widget.val() || '',
            icon     : icon_widget.getSelectedIndex(),
            icon_id  : forum_has_icons ? icon_widget.getSelectedValue() : undefined,
        };

        unmerge_data = {
             forum_id: original.forum_id,
            thread_id: args.thread_id,
            title    : original.title.thread_desc,
              icon_id: original.icon_id,
            prefix   : original.prefix,
            status   : original.status,
                close_thread: metadata.open      ? undefined : true,
               delete_thread: metadata.deleted   ? true : undefined,
            unapprove_thread: metadata.moderated ? true : undefined,

            delete_reason: metadata.delete_reason,
            notes        : metadata.notes,
        };

        template_selector.change();

    });

}
ThreadManagementPolicy.prototype = Object.create(Policy.prototype, {
    _namespace: { writable: false, configurable: false, value: 'thread management' },
});
ThreadManagementPolicy.prototype.constructor = ThreadManagementPolicy;

/**
 * @summary build an action to unmerge a thread
 * @param {BulletinBoard} bb           Bulletin Board to manipulate
 * @param {BulletinBoard} mod_team_bb  Bulletin Board to manipulate
 * @param {Variables}     v            Variables to use
 * @param {Object}        unmerge_data 'merge data' created during merging
 * @return {Action}
 */
ThreadManagementPolicy.prototype.unmerge_action = function(bb, mod_team_bb, v, unmerge_data) {

    var merge_log = v.resolve('frequently used posts/threads', 'merge log'), merge_post_id;

    return new Action(
        'root action',
        new Action(
            'unmerge wrapper',

            new Action( 'create thread', {
                fire: function(keys) {
                    return mod_team_bb.thread_create($.extend(
                        {
                            bbcode: v.resolve('thread management', 'unmerge notification bbcode', $.extend( keys, unmerge_data.source_thread ) ),
                        },
                        unmerge_data.source_thread
                    )).then(function(new_thread_id) {
                        unmerge_data.source_thread.thread_id = new_thread_id;
                        return { keys: {
                            'new thread id': new_thread_id,
                            'old thread title with link': '[thread=' + new_thread_id + ']' + keys['old thread title'] + '[/thread]'
                        }};
                    });
                },
                description: function() {
                    return [{ type: 'create thread', target: unmerge_data.source_thread }];
                }
            }).then(
                new Action( 'move posts', {
                    fire: function(keys) {
                        return bb.posts_move( keys['new thread id'], unmerge_data.posts );
                    },
                    description: function() {
                        return [{ type: 'move posts', target: { thread: unmerge_data.source_thread, posts: unmerge_data.posts } }];
                    }
                }),
                new Action( 'post to merge log', {
                    fire: function(keys) {
                        return bb.thread_reply({
                            thread_id: merge_log,
                            title    : v.resolve('thread management', 'unmerge title' , keys),
                            bbcode   : v.resolve('thread management', 'unmerge bbcode', keys),
                        }).then(function(post_id) {
                            merge_post_id = post_id;
                        });
                    },
                    description: function() {
                        return [
                            {
                                type: 'post',
                                target: {
                                    thread_desc: 'the merge log',
                                    thread_id  : merge_log,
                                    post_id    : merge_post_id
                                }
                            },
                        ];
                    }
                }),
                ( unmerge_data.source_thread.delete_thread || unmerge_data.source_thread.unapprove_thread ) ? new Action( 'change thread status', {
                    fire: function(keys) {
                        return bb.thread_edit(unmerge_data.source_thread)
                    },
                    description: function() {
                        return [{ type: 'change thread status', target: unmerge_data.source_thread }];
                    }
                }) : undefined
            )

        ).then(
            unmerge_data.thread_creator
            ? new Action( 'add usernote', {
                fire: function(keys) {
                    var return_keys = {};
                    if ( !keys.hasOwnProperty(  'pm result') ) return_keys[  'pm result'] = 'no action';
                    if ( !keys.hasOwnProperty('post result') ) return_keys['post result'] = 'no action';
                    $.extend( keys, return_keys );
                    return args.bb.usernote_add(
                        thread_creator.user_id,
                        policy.resolve( 'combined note title' , keys ),
                        policy.resolve( 'combined note bbcode', keys )
                    ).then(function() {
                        if ( keys['notification error' ] == 'fail' ) {
                            return $.Deferred()
                                .reject('Notification failed, but the error was postponed so we could add a usernote:\n' + error)
                                .promise();
                        } else {
                            return { keys: return_keys };
                        }
                    });
                },
                description: function() {
                    return [{ type: 'usernote', target: thread_creator }];
                }
            })
            : new Action( 'fix results', {
                fire: function(keys) {
                    var return_keys = {};
                    if ( !keys.hasOwnProperty(  'pm result') ) return_keys[  'pm result'] = 'no action';
                    if ( !keys.hasOwnProperty('post result') ) return_keys['post result'] = 'no action';
                    return { keys: return_keys };
                }
            })
        )
    );

}