Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
function copycat_copy_page(_oldName, _newName, processContent, copyTalk, isTalk, createParents) {
    // Safe-prepend names with space key
    var spaceKey = isTalk ? 'Category talk' : 'Category';
    var oldName = `${spaceKey}:${_oldName.replace(/Category(?:[_ ]talk):/g, '')}`;
    var newName = `${spaceKey}:${_newName.replace(/Category(?:[_ ]talk):/g, '')}`;

    // Retrieve the original page to ge its content
    var oldPage = new Morebits.wiki.page(oldName, `Reading category ${oldName}`);
    oldPage.setCreateOption('nocreate');
    oldPage.load(
        function () {
            if (!oldPage.exists()) {
                Morebits.status.warn('Warning', `${oldName} does not exist.`);
                return;
            }

            // Process possible replacements
            var oldContent = oldPage.getPageText();
            var newContent = processContent(oldContent);

            // Create the new page
            var newPage = new Morebits.wiki.page(newName, `Creating category ${newName}`);
            newPage.setCreateOption('createonly');
            newPage.setAppendText(newContent);
            newPage.setWatchlist('default');
            newPage.setEditSummary(`Copied category from ${oldName} by [[w:User:IceWelder/copycat.js|script]]`);
            newPage.load(
                function () {
                    if (newPage.exists()) {
                        Morebits.status.info('Info', `${newName} already exists.`);
                        return;
                    }

                    newPage.append(
                        function () {
                            // Talk page -> Do nothing
                            if (isTalk) {
                                return;
                            }

                            // Copy talk page
                            if (copyTalk) {
                                copycat_copy_page(_oldName, _newName, processContent, copyTalk, true, createParents);
                            }

                            // Ignore parents -> Do nothing
                            if (!createParents) {
                                return;
                            }

                            // Retrieve all listed categories
                            var categoriesRegex = /(?<=\[\[[_ ]*[Cc]ategory[_ ]*:[_ ]*)[^\]|]+[_ ]*/g;
                            var match;
                            do {
                                match = categoriesRegex.exec(oldContent);
                                if (match) {
                                    var oldCategory = match[0];
                                    // Category did not contain an ordinal -> do nothing
                                    var newCategory = processContent(oldCategory);
                                    if (newCategory === oldCategory) {
                                        continue;
                                    }

                                    // Recursively create category
                                    copycat_copy_page(oldCategory, newCategory, processContent, copyTalk, false, createParents);
                                }
                            } while (match);
                        },
                        function () {
                            Morebits.status.error('Error', `An error occurred while creating ${newName}`);
                        }
                    );
                },
                function () {
                    Morebits.status.error('Error', `An error occurred while reading ${newName}`);
                }
            );
        },
        function () {
            Morebits.status.error('Error', `An error occurred while reading ${oldName}`);
        }
    );
}

function copycat_process_content(changeOrdinals, oldOrdinal, newOrdinal) {
    return function (content) {
        if (!changeOrdinals) {
            return content;
        }

        return content.replace(new RegExp(oldOrdinal, 'g'), newOrdinal);
    };
}

function copycat_submit(event) {
    var form = event.target;

    // Get parameters
    var newName = form['newName'].value;
    var copyTalk = form['copyTalk'].checked;
    var changeOrdinals = form['changeOrdinals'].checked;
    var newOrdinal = changeOrdinals ? form['changeOrdinals.newOrdinal'].value : null;
    var createParents = changeOrdinals ? form['createParents'].checked : null;

    // Configure actions
    Morebits.status.init(form);
    Morebits.wiki.actionCompleted.redirect = `Category:${newName}`;
    Morebits.wiki.actionCompleted.notice = 'Completed';
    Morebits.wiki.api.setApiUserAgent('Copycat ([[w:User:IceWelder/copycat.js]])');

    // Set up metadata
    var oldName = Morebits.pageNameNorm.replace(/Category:/g, '');
    var oldOrdinal = oldName.replace(/\D*(\d+)?\D*/, '$1');
    var processContent = copycat_process_content(changeOrdinals, oldOrdinal, newOrdinal);

    copycat_copy_page(oldName, newName, processContent, copyTalk, false, createParents);
}

function copycat_show_popup() {
    // Define window
    var dialog = new Morebits.simpleWindow(600, 450);
    dialog.setScriptName('Copycat');
    dialog.setTitle('Copy this category');
    dialog.addFooterLink('Report bug / request feature', 'w:User talk:IceWelder');

    // Define form
    var form = new Morebits.quickForm(copycat_submit);

    var oldName = Morebits.pageNameNorm.replace(/Category:/g, '');
    form.append({
        name: 'newName',
        type: 'input',
        label: 'New name: ',
        value: oldName,
    });

    var oldOrdinal = oldName.replace(/\D*(\d+)?\D*/, '$1');
    form.append({
        type: 'checkbox',
        list: [
            {
                name: 'copyTalk',
                label: 'Also copy talk page',
            },
            {
                name: 'changeOrdinals',
                label: 'Change year ordinals',
                tooltip: 'Changes all ordinals from the old category to the set value for the new category.',
                subgroup: [
                    {
                        name: 'newOrdinal',
                        type: 'input',
                        label: 'New ordinal: ',
                        value: oldOrdinal,
                    },
                    {
                        type: 'checkbox',
                        list: [
                            {
                                name: 'createParents',
                                type: 'input',
                                label: 'Create missing parent categories',
                                tooltip: 'Recursively creates new parent categories where required.',
                                subgroup: [
                                    {
                                        type: 'div',
                                        label: 'Warning: This can create a large number of new categories',
                                    },
                                ],
                            },
                        ],
                    },
                ],
            },
        ],
    });

    form.append({
        type: 'submit',
        label: 'Copy',
    });

    // Display window with form
    var render = form.render();
    dialog.setContent(render);
    dialog.display();
}

// If viewing category
if (mw.config.get('wgNamespaceNumber') === 14 && mw.config.get('wgAction') === 'view') {
    // Wait until article is loaded and MediaWiki utils are ready
    $.when($.ready, mw.loader.using(['mediawiki.util', 'ext.gadget.morebits'])).then(function () {
        // Add portlet link
        $(mw.util.addPortletLink('p-tb', '#', 'Copy this category')).click(copycat_show_popup);
    });
}