Neeve / PonyTown Import/Export

// ==UserScript==
// @name        PonyTown Import/Export
// @namespace   azedith
// @include     https://pony.town/*

// @author      Neeve
// @version     0.31.1pre2
// @copyright   2017, Neeve (https://openuserjs.org/users/Neeve)
// @license     MIT

// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       unsafeWindow

// @require     https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js
// @require     https://raw.githubusercontent.com/Neeve01/PonyTown-Import-Export/master/ponytown_utils.js
// @require     https://cdn.jsdelivr.net/npm/clipboard@1/dist/clipboard.min.js

// @updateURL   https://openuserjs.org/meta/Neeve/PonyTown_ImportExport.meta.js
// ==/UserScript==

(function() {
    'use strict';

    var observer_target = document.querySelector("pony-town-app");

    if (!observer_target) {
        return;
    }

    var Resources = {
        ['css']: `
        .nmw-overlay{background-color:rgba(51,51,51,0.7);position:fixed;width:100%;height:100%;left:0;top:0;z-index:99999;align-items:center}
        .nmw-overlay:not(:empty){display:flex}
        .nmw-overlay:empty{display:none}
        .nmw-overlay .nmw-form{display:inline-block;position:absolute;transform:translate(-50%,0);width:45vw;min-width:32rem;left:50%;background:#212121;font-family:Helvetica Neue,Helvetica,Arial,sans-serif}
        .nmw-overlay .nmw-form .nmw-contents{border:1px solid #9d603b;border-top:none;padding:.75% 1% .25%}
        .nmw-overlay .nmw-textarea{font-size:.9em;resize:none;display:block;margin-left:auto;margin-right:auto;width:100%;height:50vh}
        .nmw-overlay .nmw-form .nmw-text{display:block;text-align:center;font-size:1.25em;color:#CCC!important;font-weight:700}
        .nmw-overlay .nmw-warning{display:block;text-align:center;font-size:1em;line-height:150%;color:#CA7E4E!important;font-weight:700}
        .nmw-overlay hr{border:0;border-top:1px solid #555;margin-top:.5rem;margin-bottom:.5rem}
        .nmw-overlay .nmw-form .nmw-line-center{display:flex;justify-content:center}
        .nmw-overlay .nmw-input{width:.1px;height:.1px;opacity:0;overflow:hidden;position:absolute;z-index:-1}
        .nmw-overlay .nmw-link{text-decoration:none}
        .nmw-overlay .nmw-link i{fill:#9d603b;font-size:.9em}
        .nmw-overlay .nmw-form .nmw-btn-close{border:0 solid;background:#CA7E4E;width:2em;height:2em;padding:0;margin:0;color:#FFF;font-size:.75em;border-radius:0}
        .nmw-overlay .nmw-form .nmw-btn-wide{width:100%}
        .nmw-overlay .nmw-inpad{padding-left:1%;padding-right:1%}
        .nmw-overlay .nmw-header{margin:0;padding:0;background:#9d603b;text-align:center;line-height:150%;font-size:1.1em;color:#fff;font-weight:400;padding-left:1%;margin:0}
        .nmw-overlay .nmw-row{display:flex;justify-content:space-between}
        .nmw-overlay .nmw-row .nmw-col{padding:0;margin:0}
        .nmw-overlay .nmw-form .nmw-footer{position:absolute;bottom:0;right:0;transform:translate(0,100%);padding-top:0.15em;font-weight:700}
        .nmw-overlay .nmw-form .nmw-footer .nmw-elements{display:flex;justify-content:flex-end;line-height:1;color:#CA7E4E!important;font-size:.75em}
        #nmw-button-download{fill:#CCC}
        .nmw-span-dimmable{transition:all 1.25s ease-in-out;opacity:1}
        .nmw-span-dim{opacity:.65}
        .nmw-char-preview-controls{transform:translate(0,-100%);padding:0.25rem;position:absolute;top:100%;right: 0;}
        `,
        ['import-frame']: `
        <div class="nmw-row nmw-header">
            <label class="nmw-col">Import character</label>
            <button id="nmw-button-close" class="nmw-btn-close btn btn-primary nmw-col">
                <i class="fa fa-lg fa-times"></i>
            </button>
        </div>
        <div class="nmw-contents">
            <textarea autofocus class="nmw-textarea" cols="40" rows="5"></textarea>
            <hr>
            <div>
                <label style="margin-bottom:0" class="nmw-text">Paste your character code inside and press Import.</label>
                <label class="nmw-warning">Careful! This will erase current character settings.</label>
            </div>
            <input id="nmw-file" class="nmw-input" accept=".json,.txt" type="file">
            <div class="nmw-line-center">
                <label style="min-width: 12.5rem" id="nmw-button-import" class="btn btn-primary">
                    <i class="fas fa-align-center"></i>
                    <span class="nmw-span-dimmable">Import</span>
                </label>
                <label for="nmw-file" style="margin-left:1rem;min-width: 12.5rem" id="nmw-button-download" class="btn btn-primary">
                    <i class="fas fa-upload"></i>
                    <span>Import from file</span>
                </label>
            </div>
        </div>
        <div class="nmw-footer">
            <div class="nmw-elements">
                <span class="mr-1">Import/Export © NotMyWing</span>
                <a target="_blank" href="https://twitter.com/NotMyWing" class="mr-1 nmw-link">
                    <i class="fab fa-twitter"></i>
                </a>
                <a target="_blank" href="https://github.com/Neeve01" class="nmw-link">
                    <i class="fab fa-github"></i>
                </a>
            </div>
        </div>
        `,
        ['export-frame']: `
        <div class="nmw-row nmw-header">
            <label class="nmw-col">Exported character</label>
            <button id="nmw-button-close" class="nmw-btn-close btn btn-primary nmw-col">
                <i class="fa fa-lg fa-times"></i>
            </button>
        </div>
        <div class="nmw-contents">
            <textarea autofocus readonly class="nmw-textarea" cols="40" rows="5"></textarea>
            <hr>
            <div style="margin-bottom:0.5rem" class="nmw-line-center">
                <button style="min-width: 10.5rem" id="nmw-button-copy" class="btn btn-primary">
                    <i class="fas fa-clipboard"></i>
                    <span class="nmw-span-dimmable">Copy to clipboard</span>
                </button>

                <button style="margin-left:1rem;min-width: 10.5rem" id="nmw-button-download" class="btn btn-primary">
                    <i class="fas fa-download"></i>
                    <span>Download as file</span>
                </button>
            </div>
        </div>
        <div class="nmw-footer">
            <div class="nmw-elements">
                <span class="mr-1">Import/Export © NotMyWing</span>
                <a target="_blank" href="https://twitter.com/NotMyWing" class="mr-1 nmw-link">
                    <i class="fab fa-twitter"></i>
                </a>
                <a target="_blank" href="https://github.com/Neeve01" class="nmw-link">
                    <i class="fab fa-github"></i>
                </a>
            </div>
        </div>
        `,
        ['eol-frame']: `
        <div class="nmw-row nmw-header">
            <label class="nmw-col">End of support</label>
            <button id="nmw-button-close" class="nmw-btn-close btn btn-primary nmw-col">
            <i class="fa fa-lg fa-times"></i>
            </button>
        </div>
        <div class="nmw-contents">
            <h3>Hey there!</h3>
            The save/load project has been "officially" discontinued.<br>
            If you want to continue using your exported horses, please consider importing them right now, as I give no guarantee of this script being compatible with the next version.<br><br>
            Even if it stops working, you would still be able to get your characters' colour codes and patterns from JSON file, just open it with notepad and use the <a href="https://jsonformatter.curiousconcept.com/" target="_blank">JSON formatter</a> to make it readable.
            <br><img src="https://github.com/Neeve01/PonyTown-Import-Export/raw/master/quills.gif" style="
                width: 12em;
                -ms-interpolation-mode: nearest-neighbor;
                image-rendering: -webkit-optimize-contrast;
                image-rendering: -moz-crisp-edges;
                image-rendering: -o-pixelated;
                image-rendering: pixelated;
                margin: 0 auto;
                display: block;
                ">
            <div style="display:block; text-align:center"><br>If you were using this script to bypass the character limit, consider
                <br><a href="https://www.patreon.com/agamnentzar" target="_blank">dropping some bits on Aggie's Patreon page</a><br> to increase your character limit.
            </div>
            <br>- Neeve
        </div>
        <div class="nmw-footer">
            <div class="nmw-elements">
                <span class="mr-1">Import/Export © NotMyWing</span>
                <a target="_blank" href="https://twitter.com/NotMyWing" class="mr-1 nmw-link">
                <i class="fab fa-twitter"></i>
                </a>
                <a target="_blank" href="https://github.com/Neeve01" class="nmw-link">
                <i class="fab fa-github"></i>
                </a>
            </div>
        </div>
        `
    }
    GM_addStyle(Resources["css"]);

    var debugging = false;

    var githubLink = "https://github.com/Neeve01";
    var twitterLink = "https://twitter.com/NotMyWing";
    var githubScriptLink = "https://github.com/Neeve01/PonyTown-Import-Export";

    var debug = function(a) {
        if (debugging) {
            console.log(a);
        }
    }

    var rgb2hex = function(rgb) {
        if (/^#[0-9A-F]{6}$/i.test(rgb)) return rgb;

        rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);

        function hex(x) {
            return ("0" + parseInt(x).toString(16)).slice(-2);
        }
        return hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
    };

    var TabFunctions = {
        ["Body"]: {
            Tab: 0,
            Import: async function(data, tabdata) {
                tabdata.BodyColors = [data.Color, data.Outline];

                // Horn.
                PonyTownUtils.ImportSet(data.Horn, tabdata.Horn);

                // Wings.
                PonyTownUtils.ImportSet(data.Wings, tabdata.Wings);

                // Ears.
                PonyTownUtils.ImportSet(data.Ears, tabdata.Ears)

                // Front hooves.
                PonyTownUtils.ImportSet(data.FrontHooves, tabdata.FrontHooves);

                // Back hooves.
                PonyTownUtils.ImportSet(data.BackHooves, tabdata.BackHooves);

                // Buttmark
                tabdata.Buttmark = data.Buttmark;
                tabdata.FlipButtmark = data.FlipButtmark || false;
            },
            Export: async function(tabdata) {
                let exported = {};

                if (tabdata.CustomOutlines) {
                    exported.OutlinesEnabled = tabdata.CustomOutlines;
                }

                let [color, outline] = tabdata.BodyColors;
                if (color && color.toLowerCase() !== "ffffff") {
                    exported.Color = color;
                }
                if (outline && outline !== "000000") {
                    exported.Outline = outline;
                }

                // Horn
                if (tabdata.Horn.Type > 0) {
                    exported.Horn = PonyTownUtils.ExportSet(tabdata.Horn);
                }

                // Wings
                if (tabdata.Wings.Type > 0) {
                    exported.Wings = PonyTownUtils.ExportSet(tabdata.Wings);
                }

                // Ears
                if (tabdata.Ears.Type > 0) {
                    exported.Ears = PonyTownUtils.ExportSet(tabdata.Ears);
                }

                // Front hooves
                if (tabdata.FrontHooves.Type > 0) {
                    exported.FrontHooves = PonyTownUtils.ExportSet(tabdata.FrontHooves);
                }

                // Back hooves
                if (tabdata.BackHooves.Type > 0) {
                    exported.BackHooves = PonyTownUtils.ExportSet(tabdata.BackHooves);
                }

                // Buttmark
                let mark = tabdata.Buttmark;
                if (!mark.every((e) => e === "")) {
                    exported.Buttmark = mark;
                }
                if (tabdata.FlipButtmark) {
                    exported.FlipButtmark = true;
                }

                return exported;
            },
            GetButtmark: function(container) {
                let element = container.querySelector("bitmap-box");
                if (element.tagName == "BITMAP-BOX") {
                    element = element.children[0];

                    let pixels = [];

                    let rows = element.children;

                    let count = 0;
                    for (var i = 0; i < rows.length; i++) {
                        let bits = rows[i].children;
                        for (var j = 0; j < rows.length; j++) {
                            pixels[count++] = bits[j].style.backgroundColor ? rgb2hex(bits[j].style.backgroundColor) : "";
                        }
                    }
                    return pixels;
                }
            },
            SetButtmark: function(container, pixels) {
                let element = container.querySelector("bitmap-box");
                if (element) {
                    element = element.children[0];

                    PonyTownUtils.EraseButtmark();

                    if (!pixels) {
                        return;
                    }
                    if (pixels.every((e) => e === "")) {
                        return;
                    }

                    PonyTownUtils.PickBrush();

                    let rows = element.children;
                    let count = 0;
                    for (var i = 0; i < rows.length; i++) {
                        let bits = rows[i].children;
                        for (var j = 0; j < rows.length; j++) {
                            if (pixels[count] !== "") {
                                PonyTownUtils.SetPixel(bits[j], pixels[count]);
                            }
                            count++;
                        }
                    }
                    return pixels;
                }
            },
            SetupFunctions: async function(container) {
                let setup = {};

                let checkbox = document.evaluate('//div[text()=\'allow custom outlines\']')
                    .iterateNext()
                    .parentNode
                    .querySelector("CHECK-BOX");

                PonyTownUtils.DefineCheckbox(setup, "CustomOutlines", checkbox);
                PonyTownUtils.DefineFillOutline(setup, "BodyColors", container.querySelector('[label="Body color"]'));

                setup.Horn = PonyTownUtils.DefineSet(container, "Horn");
                setup.Wings = PonyTownUtils.DefineSet(container, "Wings");
                setup.Ears = PonyTownUtils.DefineSet(container, "Ears");
                setup.FrontHooves = PonyTownUtils.DefineSet(container, "Front hooves");
                setup.BackHooves = PonyTownUtils.DefineSet(container, "Back hooves");

                let io = this;
                Object.defineProperty(setup, "Buttmark", {
                    get: function() {
                        return io.GetButtmark(container);
                    },
                    set: function(value) {
                        return io.SetButtmark(container, value);
                    }
                });

                let labels = container.querySelectorAll("label");
                let flip_checkbox = [].filter.call(labels, function(element) {
                    return element.innerHTML.search("don't") != -1;
                })[0].parentNode.querySelector("check-box");

                PonyTownUtils.DefineCheckbox(setup, "FlipButtmark", flip_checkbox);

                return setup;
            }
        },
        ["Mane"]: {
            Tab: 1,
            Import: async function(data, tabdata) {
                PonyTownUtils.ImportSet(data.Mane, tabdata.Mane);
                PonyTownUtils.ImportSet(data.Backmane, tabdata.Backmane);
            },
            Export: async function(tabdata) {
                let exported = {};

                if (tabdata.Mane.Type > 0) {
                    exported.Mane = PonyTownUtils.ExportSet(tabdata.Mane);
                }

                if (tabdata.Backmane.Type > 0) {
                    exported.Backmane = PonyTownUtils.ExportSet(tabdata.Backmane);
                }

                return exported;
            },
            SetupFunctions: async function(container) {
                let setup = {};

                setup.Mane = PonyTownUtils.DefineSet(container, "Mane");
                setup.Backmane = PonyTownUtils.DefineSet(container, "Back mane");

                return setup;
            }
        },
        ["Tail"]: {
            Tab: 2,
            Import: async function(data, tabdata) {
                PonyTownUtils.ImportSet(data.Tail, tabdata.Tail);
            },
            Export: async function(tabdata) {
                let exported = {};

                if (tabdata.Tail.Type > 0) {
                    exported.Tail = PonyTownUtils.ExportSet(tabdata.Tail);
                }

                return exported;
            },
            SetupFunctions: async function(container) {
                let setup = {};

                setup.Tail = PonyTownUtils.DefineSet(container, "Tail");

                return setup;
            }
        },
        ["Face"]: {
            Tab: 3,
            Container: null,
            Import: async function(data, tabdata) {
                tabdata.EyeColor.Value = data.EyeColor || "000000";
                tabdata.EyeColorLeft.Value = data.EyeColorLeft || null;
                tabdata.EyeWhitesColor.Value = data.EyeWhitesColor || "ffffff";

                let right = data.Eyes || 0;
                let left = typeof(data.LeftEye) == "number" ? data.LeftEye : null;
                this.SetEyes([right, left]);

                tabdata.Eyeshadow.Value = data.Eyeshadow || null;
                tabdata.Eyelashes.Value = data.Eyelashes || 0;

                tabdata.Expression.Value = data.Expression || 0;
                tabdata.Fangs.Value = data.Fangs || 0;

                if (data.Markings) {
                    tabdata.Markings.Value = data.Markings || 0;
                    tabdata.MarkingsColor.Value = data.MarkingsColor || "FFFFFF";
                } else {
                    tabdata.Markings.Value = 0;
                }

                PonyTownUtils.ImportSet(data.FacialHair, tabdata.FacialHair);
                PonyTownUtils.ImportSet(data.Muzzle, tabdata.Muzzle);
            },
            Export: async function(tabdata) {
                let exported = {};

                if (tabdata.EyeColor.Value !== "000000") {
                    exported.EyeColor = tabdata.EyeColor.Value;
                }

                if (tabdata.EyeColorLeft.Enabled) {
                    exported.EyeColorLeft = tabdata.EyeColorLeft.Value;
                }

                if (tabdata.EyeWhitesColor.Value !== "ffffff") {
                    exported.EyeWhitesColor = tabdata.EyeWhitesColor.Value;
                }

                let [right, left] = this.GetEyes();

                if (right !== 0) {
                    exported.Eyes = right;
                }

                if (typeof(left) == "number") {
                    exported.LeftEye = left;
                }

                if (tabdata.Eyeshadow.Enabled) {
                    exported.Eyeshadow = tabdata.Eyeshadow.Value;
                }

                if (tabdata.Eyelashes.Value > 0) {
                    exported.Eyelashes = tabdata.Eyelashes.Value;
                }

                exported.Muzzle = PonyTownUtils.ExportSet(tabdata.Muzzle);

                if (tabdata.Expression.Value > 0) {
                    exported.Expression = tabdata.Expression.Value;
                }

                if (tabdata.Fangs.Value > 0) {
                    exported.Fangs = tabdata.Fangs.Value;
                }

                if (tabdata.Markings.Value > 0) {
                    exported.Markings = tabdata.Markings.Value;
                    exported.MarkingsColor = tabdata.MarkingsColor.Value;
                }

                if (tabdata.FacialHair.Type > 0) {
                    exported.FacialHair = PonyTownUtils.ExportSet(tabdata.FacialHair);
                }

                return exported;
            },
            GetEyeSelectors: function() {
                let right = PonyTownUtils.LookupFormGroupByName(this.Container, "Eyes");
                if (right) {
                    return [PonyTownUtils.FormGroup_DefineSpriteSelection(right), null];
                } else {
                    right = PonyTownUtils.LookupFormGroupByName(this.Container, "Right eye");
                    let left = PonyTownUtils.LookupFormGroupByName(this.Container, "Left eye");

                    right = right ? PonyTownUtils.FormGroup_DefineSpriteSelection(right) : null;
                    left = left ? PonyTownUtils.FormGroup_DefineSpriteSelection(left) : null;

                    return [right, left];
                }
            },
            SetEyes: async function(values) {
                let right = values[0] || 0;
                let left = values[1] || null;

                let [right_s, left_s] = this.GetEyeSelectors();
                if (left !== null) {
                    right_s.Checked = false;
                    [right_s, left_s] = this.GetEyeSelectors();

                    left_s.Type = left;
                } else {
                    right_s.Checked = true;
                }

                right_s.Type = right;
            },
            GetEyes: function() {
                let [right_s, left_s] = this.GetEyeSelectors();
                let right = right_s ? right_s.Value : 0;
                let left = left_s ? left_s.Value : null;
                return [right, left];
            },
            SetupFunctions: async function(container) {
                let setup = {};
                this.Container = container;

                setup.EyeColor = PonyTownUtils.FormGroup_DefineColorPicker(PonyTownUtils.LookupFormGroupByName(container, "Eye color"));
                setup.EyeColorLeft = PonyTownUtils.FormGroup_DefineColorPicker(PonyTownUtils.LookupFormGroupByName(container, "Eye color (left)"));
                setup.EyeWhitesColor = PonyTownUtils.FormGroup_DefineColorPicker(PonyTownUtils.LookupFormGroupByName(container, "Eye whites color"));

                setup.SetEyes = (values) => {
                    return this.SetEyes(values);
                }
                setup.GetEyes = () => {
                    return this.GetEyes;
                }

                setup.Eyeshadow = PonyTownUtils.FormGroup_DefineColorPicker(PonyTownUtils.LookupFormGroupByName(container, "Eyeshadow"));
                setup.Eyelashes = PonyTownUtils.FormGroup_DefineSpriteSelection(PonyTownUtils.LookupFormGroupByName(container, "Eyelashes"));

                setup.Expression = PonyTownUtils.FormGroup_DefineSpriteSelection(PonyTownUtils.LookupFormGroupByName(container, "Expression"));
                setup.Fangs = PonyTownUtils.FormGroup_DefineSpriteSelection(PonyTownUtils.LookupFormGroupByName(container, "Fangs"));

                setup.Markings = PonyTownUtils.FormGroup_DefineSpriteSelection(PonyTownUtils.LookupFormGroupByName(container, "Markings"));
                setup.MarkingsColor = PonyTownUtils.FormGroup_DefineColorPicker(PonyTownUtils.LookupFormGroupByName(container, "Markings color"));

                setup.Muzzle = PonyTownUtils.DefineSet(container, "Muzzle");
                setup.FacialHair = PonyTownUtils.DefineSet(container, "Facial hair");

                return setup;
            }
        },
        ["Other"]: {
            Tab: 4,
            TabFunctions: {
                ["Head"]: {
                    Tab: 0,
                    Import: async function(data, tabdata) {
                        PonyTownUtils.ImportSet(data.HeadAccessories, tabdata.HeadAccessories);
                        PonyTownUtils.ImportSet(data.EarAccessories, tabdata.EarAccessories);
                        PonyTownUtils.ImportSet(data.FaceAccessories, tabdata.FaceAccessories);
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        if (tabdata.HeadAccessories.Type > 0) {
                            exported.HeadAccessories = PonyTownUtils.ExportSet(tabdata.HeadAccessories);
                        }
                        if (tabdata.EarAccessories.Type > 0) {
                            exported.EarAccessories = PonyTownUtils.ExportSet(tabdata.EarAccessories);
                        }
                        if (tabdata.FaceAccessories.Type > 0) {
                            exported.FaceAccessories = PonyTownUtils.ExportSet(tabdata.FaceAccessories);
                        }

                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        let setup = {};

                        setup.HeadAccessories = PonyTownUtils.DefineSet(container, "Head accessories");
                        setup.EarAccessories = PonyTownUtils.DefineSet(container, "Ear accessories");
                        setup.FaceAccessories = PonyTownUtils.DefineSet(container, "Face accessories");

                        return setup;
                    }
                },
                ["Neck"]: {
                    Tab: 1,
                    Import: async function(data, tabdata) {
                        PonyTownUtils.ImportSet(data.NeckAccessories, tabdata.NeckAccessories);
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        if (tabdata.NeckAccessories.Type > 0) {
                            exported.NeckAccessories = PonyTownUtils.ExportSet(tabdata.NeckAccessories);
                        }

                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        let setup = {};

                        setup.NeckAccessories = PonyTownUtils.DefineSet(container, "Neck accessories");

                        return setup;
                    }
                },
                ["Legs"]: {
                    Tab: 2,
                    Import: async function(data, tabdata) {
                        let same_legs = (data.SameBackLegs == false) ? false : true;

                        await tabdata.SetSameBackLegs(same_legs);
                        PonyTownUtils.ImportSet(data.FrontLegAccessories, tabdata.FrontLegAccessories);

                        if (!same_legs) {
                            PonyTownUtils.ImportSet(data.BackLegAccessories, tabdata.BackLegAccessories);
                        }
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        if (tabdata.FrontLegAccessories.Type > 0) {
                            exported.FrontLegAccessories = PonyTownUtils.ExportSet(tabdata.FrontLegAccessories);
                        }

                        if (!tabdata.GetSameBackLegs()) {
                            exported.SameBackLegs = false;
                        }

                        if (!tabdata.GetSameBackLegs()) {
                            if (tabdata.BackLegAccessories.Type > 0) {
                                exported.BackLegAccessories = PonyTownUtils.ExportSet(tabdata.BackLegAccessories);
                            }
                        }

                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        let setup = {};

                        setup.FrontLegAccessories = PonyTownUtils.DefineSet(container, "Front leg accessories");

                        let same_back_legs = container.querySelector("div > div > div > check-box");
                        PonyTownUtils.DefineCheckbox(setup, "SameBackLegs", same_back_legs);

                        setup.SetSameBackLegs = (value) => {
                            PonyTownUtils.SetCheckbox(same_back_legs, value);

                            return new Promise(function(resolve) {
                                resolve();
                            }).then(() => {
                                if (!value) {
                                    setup.BackLegAccessories = PonyTownUtils.DefineSet(container, "Back leg accessories");
                                } else {
                                    setup.BackLegAccessories = null;
                                }
                            });
                        };

                        setup.GetSameBackLegs = () => {
                            return PonyTownUtils.IsCheckboxChecked(same_back_legs);
                        };

                        if (!PonyTownUtils.IsCheckboxChecked(same_back_legs)) {
                            setup.BackLegAccessories = PonyTownUtils.DefineSet(container, "Back leg accessories");
                        }

                        return setup;
                    }
                },
                ["Chest"]: {
                    Container: null,
                    Tab: 3,
                    Import: async function(data, tabdata) {
                        PonyTownUtils.ImportSet(data.ChestAccessories, tabdata.ChestAccessories);

                        // I know.
                        if (tabdata.ChestAccessories.Type > 1) {
                            let sleeves = PonyTownUtils.DefineSet(this.Container, "Sleeves");
                            PonyTownUtils.ImportSet(data.Sleeves, sleeves);
                        }
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        if (tabdata.ChestAccessories.Type > 0) {
                            exported.ChestAccessories = PonyTownUtils.ExportSet(tabdata.ChestAccessories);
                            if (tabdata.ChestAccessories.Type > 1) {
                                exported.Sleeves = PonyTownUtils.ExportSet(tabdata.Sleeves);
                            }
                        }

                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        this.Container = container;

                        let setup = {};

                        setup.ChestAccessories = PonyTownUtils.DefineSet(container, "Chest accessories");
                        setup.Sleeves = PonyTownUtils.DefineSet(container, "Sleeves");

                        return setup;
                    }
                },
                ["Back"]: {
                    Tab: 4,
                    Import: async function(data, tabdata) {
                        PonyTownUtils.ImportSet(data.BackAccessories, tabdata.BackAccessories);
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        if (tabdata.BackAccessories.Type > 0) {
                            exported.BackAccessories = PonyTownUtils.ExportSet(tabdata.BackAccessories);
                        }
                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        let setup = {};

                        setup.BackAccessories = PonyTownUtils.DefineSet(container, "Back accessories");

                        return setup;
                    }
                },
                ["Waist"]: {
                    Tab: 5,
                    Import: async function(data, tabdata) {
                        PonyTownUtils.ImportSet(data.WaistAccessories, tabdata.WaistAccessories);
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        if (tabdata.WaistAccessories.Type > 0) {
                            exported.WaistAccessories = PonyTownUtils.ExportSet(tabdata.WaistAccessories);
                        }

                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        let setup = {};

                        setup.WaistAccessories = PonyTownUtils.DefineSet(container, "Waist accessories");

                        return setup;
                    }
                },
                ["Other"]: {
                    Tab: 6,
                    Import: async function(data, tabdata) {
                        PonyTownUtils.ImportSet(data.ExtraAccessories, tabdata.ExtraAccessories);
                    },
                    Export: async function(tabdata) {
                        let exported = {};

                        exported.ExtraAccessories = PonyTownUtils.ExportSet(tabdata.ExtraAccessories);

                        return exported;
                    },
                    SetupFunctions: async function(container) {
                        let setup = {};

                        setup.ExtraAccessories = PonyTownUtils.DefineSet(container, "Extra accessories");

                        return setup;
                    }
                }
            },
            Import: async function(data, tabdata) {
                let exported = {};

                for (let i in this.TabFunctions) {
                    let v = this.TabFunctions[i];
                    debug("> Importing tab #" + i + " (" + i + ")...");

                    let localdata = data[i] || {};
                    await PonyTownUtils.CharacterEditor.SetAccessoryTab(v.Tab);
                    let _tabdata = await Character.SetupFunctions();

                    await v.Import(localdata, _tabdata);
                }

                return exported;
            },
            Export: async function(tabdata) {
                let data = {};
                for (var i in this.TabFunctions) {
                    let v = this.TabFunctions[i];
                    debug("> Exporting tab #" + i + " (" + i + ")...");

                    await PonyTownUtils.CharacterEditor.SetAccessoryTab(v.Tab);
                    let _tabdata = await Character.SetupFunctions();

                    let exported = await v.Export(_tabdata);

                    Object.keys(exported).forEach((key) => (exported[key] == null || exported[key] == undefined) && delete exported[key]);

                    if (Object.keys(exported).length > 0) {
                        data[i] = exported;
                    }
                }
                return data;
            },
            SetupFunctions: async function(container) {
                let current_tab = PonyTownUtils.CharacterEditor.GetAccessoryTab();

                for (let i in this.TabFunctions) {
                    let v = this.TabFunctions[i];

                    if (v.Tab === current_tab) {
                        let _container = container.querySelector("div.active.tab-pane");
                        return await v.SetupFunctions(_container);
                    }
                }
            }
        }
    };

    var Character = {
        SetupFunctions: async function() {
            let tab = PonyTownUtils.CharacterEditor.GetTab();
            for (let i in TabFunctions) {
                let v = TabFunctions[i];
                if (v.Tab === tab) {
                    let container = document.querySelector("tabset > div > div.active.tab-pane");
                    return this.TabData = await v.SetupFunctions(container);
                }
            }
            this.TabData = null;
        },
        Export: async function() {
            let data = {};

            data.Nickname = PonyTownUtils.CharacterEditor.GetCharacterName();

            for (var i in TabFunctions) {
                let v = TabFunctions[i];
                debug("Exporting tab #" + i + " (" + i + ")...");

                await PonyTownUtils.CharacterEditor.SetTab(v.Tab);
                let exported = await v.Export(await this.SetupFunctions());

                Object.keys(exported).forEach((key) => (exported[key] == null || exported[key] == undefined) && delete exported[key]);

                if (Object.keys(exported).length > 0) {
                    data[i] = exported;
                }
            }
            await PonyTownUtils.CharacterEditor.SetTab(0);

            return data;
        },
        Import: async function(data) {
            if (typeof(data) == "string") {
                data = JSON.parse(data);
            }
            data.Body = data.Body || {};

            await PonyTownUtils.CharacterEditor.SetTab(0);
            (await this.SetupFunctions()).CustomOutlines = data.Body.OutlinesEnabled || false;

            for (var i in TabFunctions) {
                let v = TabFunctions[i];
                debug("Importing tab #" + i + " (" + i + ")...");

                let localdata = data[i] || {};
                await PonyTownUtils.CharacterEditor.SetTab(v.Tab);

                await v.Import(localdata, await this.SetupFunctions());
            }

            if (data.Nickname) {
                PonyTownUtils.CharacterEditor.SetCharacterName(data.Nickname);
            }

            await PonyTownUtils.CharacterEditor.SetTab(0);
        },
        ExportAll: async function() {
            var zip = new JSZip();
            var used_filenames = [];

            try {
                let list = await PonyTownUtils.CharacterEditor.GetCharacterList();
                if (list) {
                    let unknown_count = 0;
                    for (let i = 0; i < list.length; i++) {
                        await PonyTownUtils.CharacterEditor.SelectCharacter(i);
                        let data = JSON.stringify(await Character.Export());
                        let name,
                            used_count = 0;

                        do {
                            let used_suffix = "";
                            if (used_count > 0) {
                                used_suffix = "." + used_count;
                            }

                            name = (list[i] || "unknown-" + (++unknown_count)).replace(/[^a-z0-9]/gi, '_').toLowerCase() + used_suffix;
                            used_count++;
                        } while (used_filenames.includes(name));
                        used_filenames.push(name);

                        zip.file(name + ".pt.json", data);
                    }
                    zip.generateAsync({
                            type: "blob"
                        })
                        .then(function(content) {
                            saveAs(content, "characters.zip");
                        });
                }
            } catch (err) {
                throw err;
            }
        }
    };

    var ProgressForm = (function() {
        function form() {
            this.style.display = 'table-cell';

            this.innerHTML = `

            `;
        };

        form.prototype.Close = function() {

        };
    })();

    var ImportForm = (function() {
        var html = Resources["import-frame"],
            import_btn,
            form;

        function form(overlay) {
            form = this;
            this.container = document.createElement("div");
            overlay.append(this.container);

            this.container.innerHTML = html;

            this.container.classList.add("nmw-form");

            let textarea = this.container.querySelector("textarea");
            textarea.onkeypress = function(ev) {
                if (ev.keyCode == 10 || (ev.ctrlKey && ev.keyCode == 13)) {
                    form.Import(textarea.value);
                }
            };

            let button = this.container.querySelector("[id='nmw-button-close']");
            button.onclick = function() {
                form.Close();
            };

            let import_button = this.container.querySelector("[id='nmw-button-import']");
            import_button.onclick = function() {
                if (textarea.value)
                    form.Import(textarea.value);
                else {
                    form.ImportFail("Input is empty! •`c´•");
                }
            };
            import_btn = import_button;

            let fileinput = this.container.querySelector("input");
            fileinput.onchange = function() {
                let file = fileinput.files[0];
                if (file) {
                    var reader = new FileReader();
                    reader.readAsText(file, "UTF-8");
                    reader.onload = function(evt) {
                        if (evt.target.result)
                            form.Import(evt.target.result, true);
                    }
                }
            }

            textarea.select();
        }

        let timer_handle;
        form.prototype.ImportFail = function(msg) {
            msg = msg || "Couldn't import ´• c •`";
            let span = import_btn.querySelector("span");

            span.innerHTML = msg;
            if (!span.classList.contains("nmw-span-dim"))
                span.classList.add("nmw-span-dim");

            clearTimeout(timer_handle);
            timer_handle = setTimeout(function() {
                if (span) {
                    import_btn.querySelector("span").innerHTML = "Import";
                    span.classList.remove("nmw-span-dim");
                }
            }, 3500);
        }

        form.prototype.Import = async function(data, force_close) {
            try {
                await Character.Import(data);

                // Let it close if import was successful.
                force_close = true;
            } catch (err) {
                form.ImportFail();
                throw err;
            } finally {
                if (force_close)
                    this.Close();
            }
        }

        form.prototype.Close = function() {
            this.container.remove();
        }

        return form;
    })();

    var ExportForm = (function() {
        var html = Resources["export-frame"];

        function form(overlay, data) {
            var form = this;
            this.container = document.createElement("div");
            overlay.append(this.container);

            this.container.innerHTML = html;

            this.container.classList.add("nmw-form");
            let textarea = this.container.querySelector("textarea");
            textarea.value = data;

            let button = this.container.querySelector("[id='nmw-button-close']");
            button.onclick = function() {
                form.Close();
            }

            let copy_button = this.container.querySelector("[id='nmw-button-copy']");
            let cpb = new Clipboard(copy_button, {
                text: function(trigger) {
                    return textarea.value;
                }
            });

            let timer_handle;
            cpb.on("success", function(e) {
                let span = copy_button.querySelector("span");
                span.innerHTML = "Copied!";
                if (!span.classList.contains("nmw-span-dim"))
                    span.classList.add("nmw-span-dim");

                clearTimeout(timer_handle);
                timer_handle = setTimeout(function() {
                    if (span) {
                        copy_button.querySelector("span").innerHTML = "Copy to clipboard";
                        span.classList.remove("nmw-span-dim");
                    }
                }, 3500);
            });

            let dl = this.container.querySelector("[id='nmw-button-download']");
            dl.onclick = function() {
                let name = document.querySelector("character-select > div > input");
                name = (name ? name.value : "character") + ".pt.json";

                var blob = new Blob([textarea.value], {
                    type: "application/json"
                });
                saveAs(blob, name);
            }
        }

        form.prototype.Close = function() {
            this.container.remove();
        }

        return form;
    })();

    var EOLForm = (function() {
        var html = Resources["eol-frame"];

        function form(overlay, data) {
            var form = this;
            this.container = document.createElement("div");
            overlay.append(this.container);

            this.container.innerHTML = html;
            this.container.classList.add("nmw-form");

            let button = this.container.querySelector("[id='nmw-button-close']");
            button.onclick = function() {
                form.Close();
            }
        }

        form.prototype.Close = function() {
            this.container.remove();

            let setvalue = GM && GM.setValue || GM_setValue;
            setvalue("EOL-Shown", true);
        }

        return form;
    })();

    var UI = {
        Overlay: null,
        InjectHTML: function() {
            if (!this.Overlay) {
                let body = document.querySelector("body");
                let e = this.Overlay = document.createElement("div");
                e.classList.add("nmw-overlay");

                body.appendChild(e);
            }
        },
        ShowImport: function() {
            this.InjectHTML();

            let form = new ImportForm(this.Overlay);
        },
        ShowEOL: function() {
            this.InjectHTML();

            let form = new EOLForm(this.Overlay);
        },
        ShowExport: async function() {
            this.InjectHTML();

            try {
                let data = JSON.stringify(await Character.Export());
                let form = new ExportForm(this.Overlay, data);
            } catch (err) {
                throw err;
            }
        }
    }

    // --
    //
    // Injection.
    //
    // --

    var InjectControls = function(el) {
        let preview = el.querySelector(".character-preview-box > character-preview").parentNode;

        let controls = document.createElement("div");
        controls.classList.add("nmw-char-preview-controls");
        controls.setAttribute("nmw", "");
        preview.append(controls);

        let import_btn = document.createElement("button");
        import_btn.classList.add("btn");
        import_btn.classList.add("btn-default");
        import_btn.innerHTML = '<i class="fas mr-1 fa-cloud-upload"></i>Import';
        controls.appendChild(import_btn);

        let export_btn = document.createElement("button");
        export_btn.classList.add("btn");
        export_btn.classList.add("btn-default");
        export_btn.classList.add("ml-1");
        export_btn.innerHTML = '<i class="fas mr-1 fa-cloud-download"></i>Export';
        controls.appendChild(export_btn);

        import_btn.onclick = function() {
            UI.ShowImport();
        };
        export_btn.onclick = function() {
            UI.ShowExport();
        };
    };

    // Set observer.
    var observer = new MutationObserver(function(mutations) {
        let controls_injected = false;
        let overlay_changed = false;

        for (var i = 0; i < mutations.length; i++) {
            let element = mutations[i].target;
            if (!overlay_changed && element.classList.contains("nmw-overlay")) {
                overlay_changed = true;

                let body = document.querySelector("body");
                if (element.childNodes.length > 0) {
                    body.style.overflow = "hidden";
                } else {
                    body.style.overflow = "";
                }
            }

            if (!controls_injected && mutations[i].removedNodes.length === 0 && element.tagName == "CHARACTER") {
                if (!element.querySelector('[nmw]')) {
                    InjectControls(element);
                }
                controls_injected = true;

                let getvalue = GM && GM.getValue || GM_getValue;
                (async() => {
                    if (!await getvalue("EOL-Shown", false)) {
                        UI.ShowEOL();
                    }
                })();
            }
        }
    });
    observer.observe(observer_target.parentNode, {
        childList: true,
        subtree: true,
        attributes: true
    });

    if (unsafeWindow) {
        unsafeWindow.NMW = unsafeWindow.NMW || {};
        unsafeWindow.NMW.PonyTownUtils = PonyTownUtils;
        unsafeWindow.NMW.Character = Character;
    }
})();