/* 
 * Roll20: https://app.roll20.net/users/6205674/angelo
 * Github: https://github.com/ocangelo/roll20/
*/

/*jshint -W069 */
/*jshint -W014 */
/*jshint -W083 */
//class WildMenu {   };
//class WildUtils {   };

const WS_API = {
    NAME : "WildShape",
    VERSION : "1.4.3",
    REQUIRED_HELPER_VERSION: "1.3.4",

    STATENAME : "WILDSHAPE",

    // storage in the state
    DATA_CONFIG : "config",
    DATA_SHIFTERS : "shifters",

    // general info
    SETTINGS : {
        BASE_SHAPE : "base",
        SHIFTER_SIZE : "normal",
        SHAPE_SIZE : "auto",        
        SHAPE_SIZES : [
            "auto",
            "tiny",
            "normal",
            "large",
            "huge",
            "gargantuan",
        ],
        SHAPE_SIZES_SCALE : [
            0, //"auto",
            0.5, //"tiny",
            1, //"normal",
            2, //"large",
            3, //"huge",
            4, //"gargantuan",
        ],

        STATS: {
            NAMES: ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"],
            SHORT_NAMES: ["str", "dex", "con", "int", "wis", "cha"],
            SKILLS: {
                "strength" : [ "athletics"], 
                "dexterity" : [ "acrobatics", "sleight_of_hand", "stealth"],
                "constitution" : [],
                "intelligence" : ["arcana", "history", "investigation", "nature", "religion"],
                "wisdom" : ["animal_handling", "insight", "medicine", "perception", "survival"],
                "charisma" : ["deception", "intimidation", "performance", "persuasion"]
            },

            PROF: "pb",

            PREFIX: {
                NPC: "npc_",
            },

            SUFFIX: {
                BASE:       "_base",
                MOD:        "_mod",
                BONUS:      "_bonus",
                SAVE:       "_save",
                SAVE_BONUS: "_save_bonus",
                PROF:       "_prof",
                FLAG:       "_flag",
            },

            DRUID_COPY_ATTR: ["intelligence", "wisdom", "charisma"],
        },
    },

    // KEYS  in the config MUST match VALUES in the FIELDS object
    DEFAULT_CONFIG : {
        SEP: " ---",              // separator used in commands

        DRUID_WS_RES : "",
        MUTE_SHIFT: false,
        ENABLE_DEBUG: false,

        PC_DATA : {
            HP: "hp",
            SPEED: "speed",
            bar1: "hp",
            bar2: "ac",
            bar3: "speed",

            FORCEROLL_NEVER_WHISPER: true,
            FORCEROLL_TOGGLE_ADVANTAGE: true,
            FORCEROLL_MANUAL_DAMAGEROLL: true,
        },

        NPC_DATA : {
            HP: "hp",
            SPEED: "npc_speed",
            bar1: "hp",
            bar2: "npc_ac",
            bar3: "npc_speed",

            SENSES: "npc_senses",

            FORCEROLL_NEVER_WHISPER: false,
            FORCEROLL_TOGGLE_ADVANTAGE: false,
            FORCEROLL_MANUAL_DAMAGEROLL: false,
        },

        SENSES: {
            OVERRIDE_SENSES: true,

            light_radius: 5,
            light_dimradius: -5,
            light_otherplayers: false,
            light_hassight: true,
            light_angle: 360,
            light_losangle: 360,
            light_multiplier: 1, 
        },
    },


    // available commands
    CMD : {
        ROOT : "!ws",
        USAGE : "Please select a token then run: !ws",

        HELP : "help",

        CONFIG : "config",
        ADD : "add",
        REMOVE : "remove",
        EDIT : "edit",
        TOGGLE: "toggle",
        RESET : "reset",
        IMPORT : "import",
        EXPORT : "export",

        SHIFT : "shift",

        SHOW_SHIFTERS : "showshifters",
    },

    // VALUES are used as KEYS for the CONFIG
    FIELDS : {
        SEP: "sep",
        ENABLE_DEBUG: "enable_debug",

        // target of a command
        TARGET : {
            CONFIG: "config",
            SHIFTER : "shifter",
            SHAPE : "shape",
            SHAPEFOLDER : "shapefolder",
            CHAR_DATA: "charData",
        },

        SETTINGS: "settings",
        SHAPES: "shapes",

        ID: "ID",
        NAME: "name",
        CHARACTER: "character",
        SIZE: "size",
        ISDRUID: "isdruid",
        ISNPC: "isnpc",
        CURRENT_SHAPE: "currshape",
        ISDUPLICATE: "isDuplicate",

        
        DRUID_WS_RES: "DRUID_WS_RES",
        MUTE_SHIFT: "MUTE_SHIFT",

        STATS_CACHE: {
            ROOT: "stats_cache",
            STATS: "stats",
            MODS: "modifiers",
            SAVES: "saves",
            SKILLS: "skills",
        },

        // global shifter character settings, stored separately for PC and NPC types
        CHAR_DATA: {
            PC_ROOT : "PC_DATA",
            NPC_ROOT : "NPC_DATA",

            HP: "HP",
            SPEED: "SPEED",

            HP_CACHE: "HP_CACHE",
            SENSES: "SENSES",

            BAR_1: "bar1",
            BAR_2: "bar2",
            BAR_3: "bar3",

            FORCEROLL_NEVER_WHISPER: "FORCEROLL_NEVER_WHISPER",
            FORCEROLL_TOGGLE_ADVANTAGE: "FORCEROLL_TOGGLE_ADVANTAGE",
            FORCEROLL_MANUAL_DAMAGEROLL: "FORCEROLL_MANUAL_DAMAGEROLL",
        },

        SENSES: {
            ROOT: "SENSES",
            OVERRIDE: "OVERRIDE_SENSES",

            LIGHT_ATTRS: [
                "light_radius",
                "light_dimradius",
                "light_otherplayers",
                "light_hassight",
                "light_angle",
                "light_losangle",
                "light_multiplier"],
        },
    },

    DEPRECATED: {
        MAKEROLLPUBLIC: "makerollpublic",   // v1.4 -> split into separate ROLL_SETTINGS
        
        TOKEN_DATA : {                      // v1.4 -> converted to CHAR_DATA
            ROOT: "TOKEN_DATA",
            HP: "HP",
            AC: "AC",
            SPEED: "SPEED",
        },

        NPC_DATA : {                        // v1.4 -> converted to CHAR_DATA
            ROOT: "NPC_DATA",
            HP: "HP",
            AC: "AC",
            SPEED: "SPEED",
            SENSES: "SENSES",
            EMPTYBAR: "EMPTYBAR",
            HP_CACHE: "HP_CACHE",
        },

        PC_DATA : {                         // v1.4 -> converted to CHAR_DATA
            ROOT: "PC_DATA",
            HP: "HP",
            AC: "AC",
            SPEED: "SPEED",
        },
    },

    // major changes
    CHANGELOG : {
        "1.4.1" : "New option to enable debug chat/log, enabled auto size for PC sheets, changed default separator, plus a few more small fixes",
        "1.4"   : "Split Bar 1/2/3 setitngs and added overrides for different roll settings (toggle advantage, never whisper, auto roll damage)",
        "1.3"   : "automatically duplicate/delete characters when adding/removing new shapes",
        "1.2.6" : "added setting to mute players chat messages",
        "1.2.5" : "Wild Shape Resource added to config, automatically check and decrease when Druids transform",
        "1.2"   : "automatically add corrected saving throws and proficiencies for druids",
        "1.1"   : "automatically shapeshift tokens to the last shape when copied/dropped from the journal",
        "1.0.7" : "added senses attribute setting in NPC Data",
        "1.0.6" : "added automatic senses setup for NPCs (e.g. vision, light) and senses overrides for shifters and single shapes",
        "1.0.5" : "changed default separator to minimize collisions",
        "1.0.4" : "added override roll settings (default true on PCs) to automatically set target shapes to never whisper, toggle advantage",
        "1.0.2" : "restructured pc/npc data",
    }
};

// ========================================= MENU HANDLING =========================================

class WildShapeMenu extends WildMenu
{
    constructor() {
        super();

        this.UTILS = new WildUtils(WS_API.NAME);
    }

    updateConfig()
    {
        let apiConfig = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
        this.SEP = apiConfig.SEP;
        this.CMD = {};
        this.CMD.ROOT            = WS_API.CMD.ROOT + this.SEP;
        this.CMD.CONFIG          = this.CMD.ROOT + WS_API.CMD.CONFIG;
        this.CMD.CONFIG_ADD      = this.CMD.CONFIG + this.SEP + WS_API.CMD.ADD + this.SEP;
        this.CMD.CONFIG_REMOVE   = this.CMD.CONFIG + this.SEP + WS_API.CMD.REMOVE + this.SEP;
        this.CMD.CONFIG_EDIT     = this.CMD.CONFIG + this.SEP + WS_API.CMD.EDIT + this.SEP;
        this.CMD.CONFIG_RESET    = this.CMD.CONFIG + this.SEP + WS_API.CMD.RESET;

        this.SHAPE_SIZES = WS_API.SETTINGS.SHAPE_SIZES.join("|");
        this.UTILS.debugEnable(apiConfig[WS_API.FIELDS.ENABLE_DEBUG]);
    }

    showEditSenses(shifterId = null, shapeId = null) {
        const config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
        let settings;
        let cmdEdit;
        let cmdBack;
        let cmdBackName;
        let overrideName;
        let menuTitle;

        // check what are editing senses on
        if (shifterId)
        {
            menuTitle = WS_API.NAME + ": " + shifterId;
            const shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterId];

            if (shapeId)
            {
                settings = shifter[WS_API.FIELDS.SHAPES][shapeId];
                cmdEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHAPE + this.SEP + shifterId + this.SEP + shapeId + this.SEP;
                cmdBack = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHAPE + this.SEP + shifterId + this.SEP + shapeId;
                cmdBackName = "Edit Shape: " + shapeId;
                menuTitle = menuTitle + " - " + shapeId;
            }
            else
            {
                settings = shifter[WS_API.FIELDS.SETTINGS];
                cmdEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + shifterId + this.SEP;
                cmdBack = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + shifterId;
                cmdBackName = "Edit Shifter: " + shifterId;
            }

            menuTitle = menuTitle + " - Senses";
            overrideName = "Force Senses";
        }
        else
        {
            settings = config;
            cmdEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.CONFIG + this.SEP;
            cmdBack = this.CMD.CONFIG;
            cmdBackName = "Main Menu";
            menuTitle = "Default Senses";

            overrideName = "Write Senses";
        }

        cmdEdit = cmdEdit + WS_API.FIELDS.SENSES.ROOT + this.SEP;

        let sensesDataList = [
            this.makeLabelValue(overrideName, settings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE], 'false') + this.makeRightButton("Toggle", cmdEdit + WS_API.FIELDS.SENSES.OVERRIDE)
        ];

        if (shifterId && !config[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
        {
            sensesDataList.push(this.makeLabel("NOTE: Current Config Write Senses value is set to false, senses won't be applied", "font-size: 80%; padding-left: 10px; padding-bottom: 10px"));
        }

        // senses settings
        _.each(WS_API.FIELDS.SENSES.LIGHT_ATTRS, (attr) => {
            const currAttr = settings[WS_API.FIELDS.SENSES.ROOT][attr];
            
            let attrField = this.makeLabelValue(attr, currAttr);
            if (currAttr === false || currAttr === true) 
                attrField = attrField + this.makeRightButton("Toggle", cmdEdit + attr + this.SEP + WS_API.CMD.TOGGLE);
            else
                attrField = attrField + this.makeRightButton("Edit", cmdEdit + attr + this.SEP + "?{Attribute|" + currAttr + "}");
        
            sensesDataList.push(attrField);
        });

        let contents = this.makeList(sensesDataList)
            + "<hr>" + this.makeButton(cmdBackName, cmdBack, ' width: 100%');
        this.showMenu(WS_API.NAME, contents, WS_API.NAME + ': ' + menuTitle);
    }


    showEditCharData(dataRoot) {
        const config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
        let settings = config[dataRoot];

        let isNpc = dataRoot == WS_API.FIELDS.CHAR_DATA.NPC_ROOT;
        let charType =  (isNpc ? "NPC" : "PC");
        let menuTitle = charType + " Settings";

        let cmdEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.CONFIG + this.SEP + WS_API.FIELDS.TARGET.CHAR_DATA + this.SEP + dataRoot + this.SEP;
        let cmdToggle = this.SEP + WS_API.CMD.TOGGLE;

        // PC settings
        let attributesDataList = [
            this.makeLabel("<p style='font-size: 120%'><b>Attributes:</b></p>"),
            this.makeLabel("Attributes IDs for " + charType + " sheets","font-size: 80%"),

            this.makeList(
                [ 
                    this.makeLabelValue("HP Attribute", settings[WS_API.FIELDS.CHAR_DATA.HP]) + this.makeRightButton("Edit", cmdEdit + WS_API.FIELDS.CHAR_DATA.HP + this.SEP + "?{Attribute|" + settings[WS_API.FIELDS.CHAR_DATA.HP] + "}"),
                    this.makeLabelValue("SPEED Attribute", settings[WS_API.FIELDS.CHAR_DATA.SPEED]) + this.makeRightButton("Edit", cmdEdit + WS_API.FIELDS.CHAR_DATA.SPEED + this.SEP + "?{Attribute|" + settings[WS_API.FIELDS.CHAR_DATA.SPEED] + "}"),
                    ... (isNpc ? [this.makeLabelValue("SENSES", settings[WS_API.FIELDS.CHAR_DATA.SENSES]) + this.makeRightButton("Edit", cmdEdit + WS_API.FIELDS.CHAR_DATA.SENSES + this.SEP + "?{Attribute|" + settings[WS_API.FIELDS.CHAR_DATA.SENSES] + "}")] : []),
                ], " padding-left: 10px"),
        ];

        let tokenDataList = [
            this.makeLabel("<p style='font-size: 120%'><b>Token Bars:</b></p>"),
            this.makeLabel("Links Attributes to Bars when transforming into a " + charType + " (if not empty)","font-size: 80%"),

            this.makeList(
                [ 
                    this.makeLabelValue("BAR 1", settings[WS_API.FIELDS.CHAR_DATA.BAR_1]) + this.makeRightButton("Edit", cmdEdit + WS_API.FIELDS.CHAR_DATA.BAR_1 + this.SEP + "?{Attribute|" + settings[WS_API.FIELDS.CHAR_DATA.BAR_1] + "}"),
                    this.makeLabelValue("BAR 2", settings[WS_API.FIELDS.CHAR_DATA.BAR_2]) + this.makeRightButton("Edit", cmdEdit + WS_API.FIELDS.CHAR_DATA.BAR_2 + this.SEP + "?{Attribute|" + settings[WS_API.FIELDS.CHAR_DATA.BAR_2] + "}"),
                    this.makeLabelValue("BAR 3", settings[WS_API.FIELDS.CHAR_DATA.BAR_3]) + this.makeRightButton("Edit", cmdEdit + WS_API.FIELDS.CHAR_DATA.BAR_3 + this.SEP + "?{Attribute|" + settings[WS_API.FIELDS.CHAR_DATA.BAR_3] + "}"),
                ], " padding-left: 10px"),

        ];

        let rollSettingsDataList = [
            this.makeLabel("<p style='font-size: 120%'><b>Shifters - Force Roll Settings:</b></p>"),
            this.makeLabel("If set to true it will change the roll settings on the target shape when a " + charType + " Shifter transforms into it", "font-size: 80%"),
            this.makeList(
                [ 
                    this.makeLabelValue("Never Whisper", settings[WS_API.FIELDS.CHAR_DATA.FORCEROLL_NEVER_WHISPER], 'false') + this.makeRightButton("Toggle", cmdEdit + WS_API.FIELDS.CHAR_DATA.FORCEROLL_NEVER_WHISPER + cmdToggle),
                    this.makeLabelValue("Toggle Advantage", settings[WS_API.FIELDS.CHAR_DATA.FORCEROLL_TOGGLE_ADVANTAGE], 'false') + this.makeRightButton("Toggle", cmdEdit + WS_API.FIELDS.CHAR_DATA.FORCEROLL_TOGGLE_ADVANTAGE + cmdToggle),
                    this.makeLabelValue("Manual Damage Roll", settings[WS_API.FIELDS.CHAR_DATA.FORCEROLL_MANUAL_DAMAGEROLL], 'false') + this.makeRightButton("Toggle", cmdEdit + WS_API.FIELDS.CHAR_DATA.FORCEROLL_MANUAL_DAMAGEROLL + cmdToggle),
                ], " padding-left: 10px"),
        ];

        let contents = this.makeList(attributesDataList)
            + this.makeList(tokenDataList)
            + this.makeList(rollSettingsDataList)
            + "<hr>" + this.makeButton("Main Menu", this.CMD.CONFIG, ' width: 100%');

        this.showMenu(WS_API.NAME, contents, WS_API.NAME + ': ' + menuTitle);
    }

    showEditShape(shifterId, shapeId) {
        const cmdShapeEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHAPE + this.SEP + shifterId + this.SEP + shapeId + this.SEP;
        const cmdRemove = this.CMD.CONFIG_REMOVE;
        const cmdShifterEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + shifterId;

        const shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterId];

        let obj = shifter[WS_API.FIELDS.SHAPES][shapeId];
        if(!obj)
        {
            UTILS.chatError("cannot find shape " + shapeId + " when trying to create EditShape menu");
            return;
        }

        let listItems = [
            this.makeLabelValue("Character", obj[WS_API.FIELDS.CHARACTER]),
            this.makeLabelValue("Name", shapeId) + this.makeRightButton("Edit", cmdShapeEdit + WS_API.FIELDS.NAME + this.SEP + "?{Edit Name|" + shapeId + "}"),
            this.makeLabelValue("Size", obj[WS_API.FIELDS.SIZE]) + this.makeRightButton("Edit", cmdShapeEdit + WS_API.FIELDS.SIZE + this.SEP + "?{Edit Size|" + this["SHAPE_SIZES"] + "}"),
            this.makeLabelValue("Force Senses", obj[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE], 'false') + this.makeRightButton("Edit Senses", cmdShapeEdit + WS_API.FIELDS.SENSES.ROOT),
            this.makeLabel("Override the auto/default senses applied", "font-size: 80%"),
        ];

        const deleteShapeButton = this.makeButton("Delete Shape", cmdRemove + "?{Are you sure you want to delete " + shapeId + "?|no|yes}" + this.SEP + WS_API.FIELDS.TARGET.SHAPE + this.SEP + shifterId + this.SEP + shapeId, ' width: 100%');
        const editShifterButton = this.makeButton("Edit Shifter: " + shifterId, cmdShifterEdit, ' width: 100%');

        let contents = this.makeList(listItems) + '<hr>' + deleteShapeButton + '<hr>' + editShifterButton;
        this.showMenu(WS_API.NAME, contents, WS_API.NAME + ': ' + shifterId + " - " + shapeId);
    }

    showEditShifter(shifterId) {
        const cmdShapeEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHAPE + this.SEP;
        const cmdShapeAdd = this.CMD.CONFIG_ADD + WS_API.FIELDS.TARGET.SHAPE + this.SEP;
        const cmdShifterEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + shifterId + this.SEP ;
        const cmdRemove = this.CMD.CONFIG_REMOVE;
        const cmdImport = this.CMD.CONFIG + this.SEP + WS_API.CMD.IMPORT + this.SEP;
        const cmdExport = this.CMD.CONFIG + this.SEP + WS_API.CMD.EXPORT + this.SEP;

        const shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterId];
        const shifterSettings = shifter[WS_API.FIELDS.SETTINGS];
        const shifterShapes = shifter[WS_API.FIELDS.SHAPES];

        // get list of pcs and npcs
        const isNpc = shifterSettings[WS_API.FIELDS.ISNPC];
        const npcs = this.UTILS.getNPCNames().sort().join('|');
        const pcs = this.UTILS.getPCNames().sort().join('|');
        let shifterPcs = isNpc ? npcs : pcs;

        let pcTag = shifterSettings[WS_API.FIELDS.ISNPC] ? "<i>(NPC)</i>" : " <i>(PC)</i>";

        // settings section
        let listItems = [];

        let settingsDataList = [
            this.makeLabel("<p style='font-size: 120%'><b>Settings " + pcTag) + ":</b></p>",
            this.makeLabelValue("Token Name", shifterId) + this.makeRightButton("Edit", cmdShifterEdit + WS_API.FIELDS.NAME + this.SEP + "&#64;{target|token_name}"),
            this.makeLabel("Token name needs to match to be able to shapeshift", "font-size: 80%; padding-left: 10px; padding-bottom: 10px"),
            this.makeLabelValue(pcTag + " Character", shifterSettings[WS_API.FIELDS.CHARACTER]) + this.makeRightButton("Edit", cmdShifterEdit + WS_API.FIELDS.CHARACTER + this.SEP + "?{Edit Character|" + shifterPcs + "}"),
            this.makeLabelValue("Size", shifterSettings[WS_API.FIELDS.SIZE]) + this.makeRightButton("Edit", cmdShifterEdit + WS_API.FIELDS.SIZE + this.SEP + "?{Edit Size|" + this["SHAPE_SIZES"] + "}"),
            this.makeLabelValue("Is Druid", shifterSettings[WS_API.FIELDS.ISDRUID], 'false') + this.makeRightButton("Toggle", cmdShifterEdit + WS_API.FIELDS.ISDRUID),
            this.makeLabel("Is Druid automatically copies over proficiencies and INT/WIS/CHA attributes", "font-size: 80%"),
            this.makeLabelValue("Force Senses", shifterSettings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE], 'false') + this.makeRightButton("Edit Senses", cmdShifterEdit + WS_API.FIELDS.SENSES.ROOT),
            this.makeLabel("Override the auto/default senses applied", "font-size: 80%"),
        ];

        //listItems.push(this.makeList(settingsDataList, " padding-left: 10px"));

        // shapes section
        let shapesDataList = [
            this.makeLabel("<p style='font-size: 120%'><b>Shapes:</b></p>"),
        ];

        _.each(shifterShapes, (value, shapeId) =>
        {
            shapesDataList.push(this.makeLabel(shapeId) + this.makeRightButton("Del", cmdRemove + "?{Are you sure you want to delete " + shapeId + "?|no|yes}" + this.SEP + WS_API.FIELDS.TARGET.SHAPE + this.SEP + shifterId + this.SEP + shapeId) + this.makeRightButton("Edit", cmdShapeEdit + shifterId + this.SEP + shapeId));
        });
        //listItems.push(this.makeList(shapesDataList, " padding-left: 10px"));

        // bottom buttons
        const shapeButtons =
            "<table style='width: 100%'><tr><td>" + this.makeButton("Add NPC", cmdShapeAdd + shifterId + this.SEP + "?{Target Shape|" + npcs + "}", 'width: 100%') + "<td>" + this.makeButton("Add PC", cmdShapeAdd + shifterId + this.SEP + "?{Target Shape|" + pcs + "}", 'width: 100%') + "</table>"
            + this.makeButton("Import Shapes from Folder", cmdImport + WS_API.FIELDS.TARGET.SHAPEFOLDER + this.SEP + shifterId + this.SEP + "?{Folder Name}" + this.SEP + "?{Find in Subfolders?|no|yes}" + this.SEP + "?{Remove Prefix (optional)}", ' width: 100%')
            + this.makeLabelComment("Adding a single shape may take a several seconds (around 4s), importing several shapes from a folder may take a while (e.g. 15 shapes should take around 1 minute); please wait until you see the results in the chat");

        const deleteShapesButton = this.makeButton("Delete All Shapes", cmdRemove + "?{Are you sure you want to delete all shapes?|no|yes}" + this.SEP + WS_API.FIELDS.TARGET.SHAPE + this.SEP + shifterId, ' width: 100%');
        //const importShapesButton = this.makeButton("Import Shapes", cmdImport + WS_API.FIELDS.TARGET.SHAPE + this.SEP + "?{Shapes Data}", ' width: 100%');
        //const exportShapesButton = this.makeButton("Export Shapes", cmdExport + WS_API.FIELDS.TARGET.SHAPE, ' width: 100%');
        //const exportShifterButton = this.makeButton("Export Shifter", cmdExport + WS_API.FIELDS.TARGET.SHIFTER, ' width: 100%');
        const deleteShifterButton = this.makeButton("Delete Shifter: " + shifterId, cmdRemove + "?{Are you sure you want to delete the shifter?|no|yes}" + this.SEP + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + shifterId, ' width: 100%');
        const showShiftersButton = this.makeButton("Show ShapeShifters", this.CMD.ROOT + WS_API.CMD.SHOW_SHIFTERS, ' width: 100%');

        //let contents = this.makeList(listItems) + importShapesFromFolderButton /*+ importShapesButton + exportShapesButton + exportShifterButton*/ + '<hr>' + deleteShifterButton + '<hr>' + showShiftersButton;
        let contents = this.makeList(settingsDataList)
            + '<hr>' + this.makeList(shapesDataList) + shapeButtons /*+ importShapesButton + exportShapesButton + exportShifterButton*/ 
            + '<hr>' + deleteShapesButton + deleteShifterButton 
            + '<hr>' + showShiftersButton;
        
        this.showMenu(WS_API.NAME, contents, WS_API.NAME + ': ' + shifterId);
    }

    showShifters() {
        const cmdShifterAdd = this.CMD.CONFIG_ADD + WS_API.FIELDS.TARGET.SHIFTER + this.SEP;
        const cmdShifterEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHIFTER + this.SEP;
        const cmdRemove = this.CMD.CONFIG_REMOVE;
        const cmdImport = this.CMD.CONFIG + this.SEP + WS_API.CMD.IMPORT + this.SEP;

        let listItems = [];
        _.each(state[WS_API.STATENAME][WS_API.DATA_SHIFTERS], (value, shifterId) => {
            const shifterSettings = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterId][WS_API.FIELDS.SETTINGS];
            listItems.push(this.makeLabel(shifterId + (shifterSettings[WS_API.FIELDS.ISNPC] ? " <i>(NPC)</i>" : " <i>(PC)</i>")) + this.makeRightButton("Del", cmdRemove + "?{Are you sure you want to delete " + shifterId + "?|no|yes}" + this.SEP + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + shifterId)+ this.makeRightButton("Edit", cmdShifterEdit + shifterId));
        });

        const addShifterButton = this.makeButton("Add ShapeShifter", cmdShifterAdd + "&#64;{target|token_id}", ' width: 100%');
        //const importShifterButton = this.makeButton("Import Shifter", cmdImport + WS_API.FIELDS.TARGET.SHIFTER + this.SEP + "?{Shifter Data}", ' width: 100%');

        const configButton = this.makeButton("Main Menu", this.CMD.CONFIG, ' width: 100%');

        let contents = this.makeList(listItems) + addShifterButton /*+ importShifterButton*/ + '<hr>' + configButton;
        this.showMenu(WS_API.NAME, contents, WS_API.NAME + ': ShapeShifters');
    }

    showConfigMenu(newVersion) {
        const config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];

        const apiCmdBase = this.CMD.ROOT;
        const cmdConfigEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.CONFIG + this.SEP;

        const showShiftersButton = this.makeButton("Edit ShapeShifters", apiCmdBase + WS_API.CMD.SHOW_SHIFTERS, ' width: 100%');
        
        let settingsList = [
            this.makeLabel("<p style='font-size: 120%'><b>Config:</b></p>"),

            this.makeLabelValue("Commands Separator", this.SEP) + this.makeRightButton("Edit", cmdConfigEdit + WS_API.FIELDS.SEP + this.SEP + "?{New Separator}"),
            this.makeLabel("Please make sure your names/strings don't include the separator used by the API", "font-size: 80%; padding-left: 10px"),

            this.makeLabelValue("<br>WildShape Resource", config[WS_API.FIELDS.DRUID_WS_RES]) + this.makeRightButton("Edit", cmdConfigEdit + WS_API.FIELDS.DRUID_WS_RES + this.SEP + "?{Edit|" + config[WS_API.FIELDS.DRUID_WS_RES] + "}"),
            this.makeLabel("Automatically check and decrease resource for Druids (case insensitive)", "font-size: 80%; padding-left: 10px"),

            this.makeLabelValue("Mute Shift Messages", config[WS_API.FIELDS.MUTE_SHIFT], 'false') + this.makeRightButton("Toggle", cmdConfigEdit + WS_API.FIELDS.MUTE_SHIFT),
            this.makeLabel("Mute messages sent to players when shapeshifting", "font-size: 80%; padding-left: 10px"),

            this.makeLabel("PC Settings") + this.makeRightButton("Edit", cmdConfigEdit + WS_API.FIELDS.TARGET.CHAR_DATA + this.SEP + WS_API.FIELDS.CHAR_DATA.PC_ROOT),
            this.makeLabel("Global settings (attributes, rolls, etc.) for PC sheets", "font-size: 80%; padding-left: 10px"),

            this.makeLabel("NPC  Settings") + this.makeRightButton("Edit", cmdConfigEdit + WS_API.FIELDS.TARGET.CHAR_DATA + this.SEP + WS_API.FIELDS.CHAR_DATA.NPC_ROOT),
            this.makeLabel("Global settings (attributes, rolls, etc.) for NPC sheets", "font-size: 80%; padding-left: 10px"),

            // senses settings
            this.makeLabelValue("Write Senses", config[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE], 'false') + this.makeRightButton("Edit", cmdConfigEdit + WS_API.FIELDS.SENSES.ROOT),
            this.makeLabel("Write senses to token, defaults if data cannot be found", "font-size: 80%; padding-left: 10px"),

            this.makeLabelValue("Enable Debug Messages", config[WS_API.FIELDS.ENABLE_DEBUG], 'false') + this.makeRightButton("Toggle", cmdConfigEdit + WS_API.FIELDS.ENABLE_DEBUG),
            this.makeLabel("Enable debug messages in the chat and in the API log", "font-size: 80%; padding-left: 10px"),
        ];
        
        // finalization
        const resetButton = this.makeButton('Reset', this.CMD.CONFIG_RESET + this.SEP + "?{Are you sure you want to reset all configs?|no|yes}", ' width: 100%');

        let title_text = WS_API.NAME + " v" + WS_API.VERSION + ((newVersion) ? ': New Version Setup' : ': Config');
        let contents = showShiftersButton + '<hr>'
                        + this.makeList(settingsList)
                        + '<hr>' + resetButton;

        this.showMenu(WS_API.NAME, contents, title_text);
    }

    showShapeShiftMenu(who, playerid, shifterId, shapes) {
        const cmdShapeShift = this.CMD.ROOT + WS_API.CMD.SHIFT + this.SEP;

        let contents = '';

        if (playerIsGM(playerid))
        {
            const cmdShifterEdit = this.CMD.CONFIG_EDIT + WS_API.FIELDS.TARGET.SHIFTER + this.SEP;
            contents += this.makeButton("Edit", cmdShifterEdit + shifterId, ' width: 100%') + "<hr>";
        }

        contents += this.makeButton(shifterId, cmdShapeShift + WS_API.SETTINGS.BASE_SHAPE, ' width: 100%') + "<hr>";
        _.each(shapes, (value, key) => {
            contents += this.makeButton(key, cmdShapeShift + key, ' width: 100%');
        });

        this.showMenu(WS_API.NAME, contents, WS_API.NAME + ': ' + shifterId + ' ShapeShift', playerIsGM(playerid) ? null : {whisper: who});
    }
}

// ========================================= WILD SHAPE =========================================

var WildShape = WildShape || (function() {
    'use strict';

    let MENU  = new WildShapeMenu();
    let UTILS = new WildUtils(WS_API.NAME);

    const validateSeparator = (sep) => {
        return sep && typeof(sep) === 'string' && sep !== "" && !sep.match(/[.*+?^${}()|[\]\\]/g);
    };

    const sortShifters = () => {
        // order shifters
        state[WS_API.STATENAME][WS_API.DATA_SHIFTERS] = UTILS.sortByKey(state[WS_API.STATENAME][WS_API.DATA_SHIFTERS]);
    };

    const sortShapes = (shifter) => {
        // order shapes
        shifter[WS_API.FIELDS.SHAPES] = UTILS.sortByKey(shifter[WS_API.FIELDS.SHAPES]);
    };

    const copySenses = (from, to) =>
    {
        const fromSenses = from[WS_API.FIELDS.SENSES.ROOT];        
        let toSenses = {};

        _.each(WS_API.FIELDS.SENSES.LIGHT_ATTRS, function (attr)
        {
            toSenses[attr] = fromSenses[attr];
        });

        toSenses[WS_API.FIELDS.SENSES.OVERRIDE] = fromSenses[WS_API.FIELDS.SENSES.OVERRIDE];
        to[WS_API.FIELDS.SENSES.ROOT] = toSenses;
    }; 

    // cache basic stats and modifiers
    const cacheCharacterData = (target) => {
        const targetId = target[WS_API.FIELDS.ID];
        const isNpc = (getAttrByName(targetId, 'npc', 'current') == 1);

        const STATS = WS_API.SETTINGS.STATS;

        const PREFIX = isNpc ? STATS.PREFIX.NPC : "";
        const SUFFIX_MOD = STATS.SUFFIX.MOD;
        const SUFFIX_SAVE = STATS.SUFFIX.SAVE;
        const SUFFIX_PROF = STATS.SUFFIX.PROF;

        let stats = {};
        let mods = {};
        let saves = {};
        let skills = {};

        for (let statIndex = 0; statIndex < 6; ++statIndex) {
            const statName = STATS.NAMES[statIndex];
            const shortStatName = STATS.SHORT_NAMES[statIndex];

            // copy stat
            let attr = findObjs({_type: "attribute", name: statName, _characterid: targetId})[0];
            stats[statName] = attr ? Number(attr.get("current")) : 0;

            // copy modifier
            attr = findObjs({_type: "attribute", name: statName + SUFFIX_MOD, _characterid: targetId})[0];
            let mod = attr ? Number(attr.get("current")) : 0;
            mods[statName] = mod;

            // copy save
            attr = findObjs({_type: "attribute", name: PREFIX + (isNpc ? shortStatName : statName) + SUFFIX_SAVE, _characterid: targetId})[0];
            saves[statName] = attr && attr.get("current") != "" ? Number(attr.get("current")) : mod;

            // copy skills
            _.each(STATS.SKILLS[statName], (skillName) => {
                attr = findObjs({_type: "attribute", name: PREFIX + skillName, _characterid: targetId})[0];
                skills[skillName] = attr && attr.get("current") != "" ? Number(attr.get("current")) : mod;
            });
        }

        let statsCache = {};
        statsCache[WS_API.FIELDS.STATS_CACHE.STATS] = stats;
        statsCache[WS_API.FIELDS.STATS_CACHE.MODS] = mods;
        statsCache[WS_API.FIELDS.STATS_CACHE.SAVES] = saves;
        statsCache[WS_API.FIELDS.STATS_CACHE.SKILLS] = skills;

        target[WS_API.FIELDS.STATS_CACHE.ROOT] = statsCache;

    };

    const getCreatureSize = (targetSize) => {        
        return WS_API.SETTINGS.SHAPE_SIZES_SCALE[targetSize ? Math.max(_.indexOf(WS_API.SETTINGS.SHAPE_SIZES, targetSize.toLowerCase()), 0) : 0];
    };

    const getTokenBarData = (shifterSettings, targetCharacterId, isTargetDefault, isTargetNpc) => {
        const config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
        let charDataRoot = config[isTargetNpc ? WS_API.FIELDS.CHAR_DATA.NPC_ROOT : WS_API.FIELDS.CHAR_DATA.PC_ROOT];

        let hpAttr    = charDataRoot[WS_API.FIELDS.CHAR_DATA.HP];
        let speedAttr = charDataRoot[WS_API.FIELDS.CHAR_DATA.SPEED];

        let barData = {};

        function getValue(fieldId)
        {
            let attrName = charDataRoot[fieldId];
            if (attrName && attrName !== "")
            {
                let obj = findObjs({type: "attribute", characterid: targetCharacterId, name: attrName})[0];
                if(obj)
                {
                    barData[fieldId] = {};
                    barData[fieldId].id  = obj.id;
                    barData[fieldId].max = obj.get('max');
                    
                    let currValue = obj.get('current');

                    // HP SPECIAL HANDLING
                    if (attrName == hpAttr)
                    {
                        if (isTargetDefault)
                        {
                            // NPC ShapeShifter don't store the current in hp and need special handling to restore hp when going back to original form
                            if (shifterSettings[WS_API.FIELDS.ISNPC])
                            {
                                currValue = shifterSettings[WS_API.FIELDS.CHAR_DATA.HP_CACHE];
                                if (!currValue)
                                {
                                    currValue = barData[fieldId].max;
                                    UTILS.debugChat("cannot restore hp from HP CACHE, value not found; setting current hp to max");
                                }
                                else
                                {
                                    UTILS.debugChat("restoring " + currValue + " from HP_CACHE");
                                }
                            }
                        }
                        else
                        {
                            // always restore the max unless we are going back to the default Shape
                            currValue = barData[fieldId].max;
                        }
                    }
                    // SPEED SPECIAL HANDLING
                    else if (attrName == speedAttr && ((!_.isNumber(currValue)) && currValue.indexOf(' ') > 0))
                    {
                        // "speed" can have multiple values, just display the first number/word before the space
                        currValue = currValue.split(' ')[0];
                    }

                    // set value
                    barData[fieldId].current = currValue;
                }
                else
                {
                    UTILS.chatError("setting value on token bar, cannot find attribute [" + attrName + "] on character: " + targetCharacterId);
                }
            }
        }

        getValue(WS_API.FIELDS.CHAR_DATA.BAR_1);
        getValue(WS_API.FIELDS.CHAR_DATA.BAR_2);
        getValue(WS_API.FIELDS.CHAR_DATA.BAR_3);

        return barData;
    };

    const setTokenBarValues = (token, barData, controlledby) => {
        function setValue(barKey)
        {
            if (barData[barKey])
            {
                token.set(barKey + "_link", barData[barKey].id ? barData[barKey].id : "");
                token.set(barKey + "_value", barData[barKey].current ? barData[barKey].current : "");
                token.set(barKey + "_max", barData[barKey].max ? barData[barKey].max : "");

                // we need to turn bar visibility on if the target is controlled by a player
                if (controlledby && controlledby.length > 0)
                {
                    token.set("showplayers_" + barKey, false);
                    token.set("playersedit_" + barKey, true);
                }
            }
        }

        setValue(WS_API.FIELDS.CHAR_DATA.BAR_1);
        setValue(WS_API.FIELDS.CHAR_DATA.BAR_2);
        setValue(WS_API.FIELDS.CHAR_DATA.BAR_3);
    };

    const findShifterData = (tokenObj, silent = false) => {
        //let tokenObj = getObj(selectedToken._type, selectedToken._id);

        //const id = tokenObj.get("represents");
        //const targetId = _.findKey(state[WS_API.STATENAME][WS_API.DATA_SHIFTERS], function(s) { return s[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ID] == id; });
        //if (targetKey)

        const targetId = tokenObj.get("name");
        const target = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][targetId];

        if(target)
        {
            const targetCharacterId = target[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ID];
            const targetCharacter = findObjs({ type: 'character', id: targetCharacterId })[0];
            if(targetCharacter)
            {
                return {
                    token: tokenObj,
                    shifterId: targetId,
                    shifter: target,
                    shifterCharacterId: targetCharacterId,
                    shifterCharacter: targetCharacter,
                    shifterControlledby: targetCharacter.get("controlledby")
                };
            }
            else if (!silent)
                UTILS.chatError("Cannot find ShapeShifter: " + targetId + ", character id: " + target[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ID]);
        }
        else if (!silent)
            UTILS.chatError("Cannot find ShapeShifter: " + targetId);

        return null;
    };

    async function getTargetCharacterData(shiftData, isTargetNpc, isTargetDefault) {
        UTILS.debugChat("BUILDING TARGET CHARACTER DATA: START", true);

        const config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
        const shifterSettings = shiftData.shifter[WS_API.FIELDS.SETTINGS];
        const targetData = shiftData.targetShape ? shiftData.targetShape : shifterSettings;

        // the get on _defaulttoken is async, need to wait on it
        UTILS.debugChat("--- wait for token image");
        let targetImg = null;
        await UTILS.getDefaultTokenImage(shiftData.targetCharacter).then((img) => {
            targetImg = img;

            if (!targetImg || targetImg.trim() == "")
            {
                UTILS.debugChat("cannot find default token image, getting avatar image");
                targetImg = UTILS.getCleanImgsrc(shiftData.targetCharacter.get('avatar'));

                if (!targetImg || targetImg.trim() == "")
                {
                    targetImg = null;
                }
            }
        });

        if (!targetImg)
        {
            UTILS.debugFlush();
            UTILS.chatErrorToPlayer(shiftData.who, "cannot use image with marketplace link, the image needs to be re-uploaded into the library and set on the target character as either token or avatar image; character id = " + shiftData.targetCharacterId);
            return null;
        }
        else
        {
            UTILS.debugChat("token image found");
        }

        // NPCs shapeshifters need to cache their current HP so that we can restore it when they go back to their base shape as they don't save it in the current attribute
        if (shifterSettings[WS_API.FIELDS.ISNPC] && !isTargetDefault && shifterSettings[WS_API.FIELDS.CURRENT_SHAPE] == WS_API.SETTINGS.BASE_SHAPE)
        {
            UTILS.debugChat("--- npc hp special handling");

            // cache current npc hp value
            let npcDataRoot = config[WS_API.FIELDS.CHAR_DATA.NPC_ROOT];
            let hpAttr = npcDataRoot[WS_API.FIELDS.CHAR_DATA.HP];
            let hpBarValue = null;
            
            if (hpAttr == npcDataRoot[WS_API.FIELDS.CHAR_DATA.BAR_1])
                hpBarValue = "bar1_value";
            else if (hpAttr == npcDataRoot[WS_API.FIELDS.CHAR_DATA.BAR_2])
                hpBarValue = "bar2_value";
            else if (hpAttr == npcDataRoot[WS_API.FIELDS.CHAR_DATA.BAR_3])
                hpBarValue = "bar3_value";

            if (hpBarValue)
            {
                shifterSettings[WS_API.FIELDS.CHAR_DATA.HP_CACHE] = shiftData.token.get(hpBarValue);
                UTILS.debugChat("saving " + shifterSettings[WS_API.FIELDS.CHAR_DATA.HP_CACHE] + " from " + hpBarValue);
            }
            else
            {
                UTILS.debugFlush();
                UTILS.chatError("cannot save current HP, no bar value found connected to " + hpAttr + "; this is required for NPC shifters, please make sure the hp attribute is set on one of the bars for the NPC settings in the main config");
                return null;
            }
        }

        // get token size
        const tokenBaseSize = 70;
        let tokenWidth = tokenBaseSize;
        let tokenHeight = tokenBaseSize;
        {
           let targetSize = getCreatureSize(targetData[WS_API.FIELDS.SIZE]);

            // get token size on the auto setting
            if (targetSize == 0)
            {
                if(isTargetNpc)
                {
                    targetSize = getAttrByName(shiftData.targetCharacterId, "token_size");
                    if(!targetSize)
                        targetSize = 0;
                }
                else
                {
                    await UTILS.getDefaultTokenSize(shiftData.targetCharacter).then((ret) => { 
                        if (ret.width > 0) 
                            tokenWidth = ret.width;
                        if (ret.height > 0) 
                            tokenHeight = ret.height}
                    );
                }
            }

            if (targetSize > 1)
            {
                tokenWidth = tokenBaseSize * targetSize;
                tokenHeight = tokenBaseSize * targetSize;
            }

            UTILS.debugChat("--- tokenTargetSize: " + targetSize); 
        }
           
        // setup output data
        let data = {};
        {
            data.imgsrc = targetImg;
            data.characterId = shiftData.targetCharacterId;
            data.controlledby = shiftData.shifterControlledby;
            data.tokenWidth = tokenWidth;
            data.tokenHeight = tokenHeight;
        }

        // setup values for bar 1/2/3 on token
        data.barData = getTokenBarData(shifterSettings, shiftData.targetCharacterId, isTargetDefault, isTargetNpc);

        // setup senses
        if (config[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
        {
            UTILS.debugChat("--- setting up senses");

            let senses = {};

            if (targetData[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
            {
                _.each(WS_API.FIELDS.SENSES.LIGHT_ATTRS, (attr) => {
                    senses[attr] = targetData[WS_API.FIELDS.SENSES.ROOT][attr];
                });
            }
            else
            {
                // copy the defaults
                _.each(WS_API.FIELDS.SENSES.LIGHT_ATTRS, (attr) => {
                    senses[attr] = config[WS_API.FIELDS.SENSES.ROOT][attr];
                });

                if (isTargetNpc)
                {
                    // get npc senses
                    let targetSenses = getAttrByName(shiftData.targetCharacterId, config[WS_API.FIELDS.CHAR_DATA.NPC_ROOT][WS_API.FIELDS.CHAR_DATA.SENSES]);
                    if (targetSenses)
                    {
                        // set radius to darkvision
                        let visionSense = targetSenses.match(/darkvision\s([0-9]+)/);
                        let hasDarkvision = visionSense && visionSense.length >= 2; 
                        if (hasDarkvision)
                            senses.light_radius = visionSense[1];

                        visionSense = targetSenses.match(/blindsight\s([0-9]+)/);
                        if (visionSense && visionSense.length >= 2)
                        {
                            // set end of bright radius to blindsight
                            senses.light_dimradius = visionSense[1];
                            if (!hasDarkvision)
                                senses.light_radius = senses.light_dimradius;
                        }
                    }
                }
            }

            data.senses = senses;
        }

        UTILS.debugFlush("BUILDING TARGET CHARACTER DATA: DONE", true);

        return data;
    }

    const copyDruidProficiency = (profData) => {
        /*
        You also retain all of your skill and saving throw Proficiencies, in addition to gaining those of the creature.
        If the creature has the same proficiency as you and the bonus in its stat block is higher than yours, use the creature’s bonus instead of yours. 
        */
        let shapeStatAttr = findObjs({_type: "attribute", name: profData.shapeStatName, _characterid: profData.shapeId})[0];
        if (!shapeStatAttr)
        {
            shapeStatAttr = createObj('attribute', {
                characterid: profData.shapeId,
                name: profData.shapeStatName,
                current: "",
                max: ""
            });
        }

        if (shapeStatAttr)
        {
            // target could have proficiency in a stat we don't, default to that proficiency bonus
            let statPb = profData.shapeStatValue - profData.shapeStatMod;
            const isProficient = UTILS.isProficient(profData.druidId, profData.druidStatName + WS_API.SETTINGS.STATS.SUFFIX.PROF);
            UTILS.debugChat("stat " + profData.shapeStatName + ": " + profData.shapeStatValue.toString() + ", npc pb: " + statPb + ", npc mod: " + profData.shapeStatMod + ", druid pb: " + (isProficient ? profData.druidPb : 0));

            // check which proficiency bonus we should use
            if (isProficient && profData.druidPb > statPb)
            {
                statPb = profData.druidPb;
            }

            // update stat value based on current modifier and best proficiency
            const newShapeStatValue = profData.statMod + statPb;
            const oldShapeStatValue = Number(shapeStatAttr.get("current"));
            if (newShapeStatValue != oldShapeStatValue || shapeStatAttr.get("current") == "")
            {
                if (shapeStatAttr.get("current") == "")
                    UTILS.debugChat("SETTING " + profData.shapeStatName + ": to " + newShapeStatValue);
                else
                    UTILS.debugChat("CHANGING " + profData.shapeStatName + ": from " + oldShapeStatValue + " to " + newShapeStatValue);

                shapeStatAttr.set("current", newShapeStatValue);

                // also set _base value
                UTILS.setAttribute(profData.shapeId, profData.shapeStatName + WS_API.SETTINGS.STATS.SUFFIX.BASE, (newShapeStatValue > 0 ? "+" : "") + newShapeStatValue.toString());
            }

            // set _flag value so that stats are forced to be displayed on NPCs
            UTILS.setAttribute(profData.shapeId, profData.shapeStatName + WS_API.SETTINGS.STATS.SUFFIX.FLAG, 1);
        }
        else
        {
            UTILS.chatError("cannot find attribute " + profData.shapeStatName + " on shape " + profData.shapeId);
        }
    };

    const copyDruidData = (shiftData) => {
        const STATS = WS_API.SETTINGS.STATS;

        const targetStatsCache = shiftData.targetShape[WS_API.FIELDS.STATS_CACHE.ROOT];
        const statsCache = targetStatsCache[WS_API.FIELDS.STATS_CACHE.STATS];
        const modsCache = targetStatsCache[WS_API.FIELDS.STATS_CACHE.MODS];
        const savesCache = targetStatsCache[WS_API.FIELDS.STATS_CACHE.SAVES];
        const skillsCache = targetStatsCache[WS_API.FIELDS.STATS_CACHE.SKILLS];


        let druidCharacterId = shiftData.shifter[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ID];
        
        let targetCharacterId = shiftData.targetCharacterId;
        let targetCharacter = shiftData.targetCharacter;
        let targetShape = shiftData.targetShape;

        UTILS.debugChat("COPYING DRUID DATA: START", true);

        // copy attributes
        UTILS.debugChat("--- copying attributes");

        _.each(STATS.DRUID_COPY_ATTR, function (attrName) {
            UTILS.copyAttribute(druidCharacterId, attrName, targetCharacterId, attrName, 10);
            UTILS.copyAttribute(druidCharacterId, attrName + STATS.SUFFIX.BASE, targetCharacterId, attrName + STATS.SUFFIX.BASE, 10);
            UTILS.copyAttribute(druidCharacterId, attrName + STATS.SUFFIX.MOD, targetCharacterId, attrName + STATS.SUFFIX.MOD, "0");
        });

        // copy proficiencies
        UTILS.debugChat("--- copying proficiencies");

        let profData = {};
        profData.druidId = druidCharacterId;
        profData.shapeId = targetCharacterId;

        let druidPb  = findObjs({_type: "attribute", name: STATS.PROF, _characterid: druidCharacterId})[0];
        profData.druidPb = druidPb ? Number(druidPb.get("current")) : 0;
        UTILS.debugChat("druid pb: " + profData.druidPb);

        for (let statIndex = 0; statIndex < 6; ++statIndex)
        {
            const statName = STATS.NAMES[statIndex];
            const shortStatName = STATS.SHORT_NAMES[statIndex];

            // original stat modifier on target
            let statMod = findObjs({_type: "attribute", name: statName + WS_API.SETTINGS.STATS.SUFFIX.MOD, _characterid: targetCharacterId})[0];
            profData.statMod = statMod ? Number(statMod.get("current")) : 0;
            profData.shapeStatMod = modsCache[statName];

            // copy save proficiency
            profData.druidStatName = statName + STATS.SUFFIX.SAVE;
            profData.shapeStatName = STATS.PREFIX.NPC + shortStatName + STATS.SUFFIX.SAVE;
            profData.shapeStatValue = savesCache[statName];
            copyDruidProficiency(profData);

            // copy skill proficiency for all skills associated with this stat
            _.each(STATS.SKILLS[statName], (skillName) => {
                profData.druidStatName = skillName;
                profData.shapeStatName = STATS.PREFIX.NPC + skillName;
                profData.shapeStatValue = skillsCache[skillName];

                copyDruidProficiency(profData);
            });
        }

        // npc saving/skills flag
        let npc_attr_flag = findObjs({_type: "attribute", name: "npc_saving_flag", _characterid: targetCharacterId})[0];
        if (npc_attr_flag)
        {
            npc_attr_flag.set("current", 1);
        }

        npc_attr_flag = findObjs({_type: "attribute", name: "npc_skills_flag", _characterid: targetCharacterId})[0];
        if (npc_attr_flag)
        {
            npc_attr_flag.set("current", 1);
        }

        UTILS.debugFlush("COPYING DRUID DATA : DONE");
    };

    async function doShapeShift(shiftData) {
        const config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
        const shifterSettings = shiftData.shifter[WS_API.FIELDS.SETTINGS];

        let isTargetNpc = true;
        let isTargetDefault = false;
        let wildShapeResource = null;

        if(shiftData.targetShape)
        {
            // if it's a druid shapeshifting check that we have enough uses of the wildshape resource left
            if(shifterSettings[WS_API.FIELDS.ISDRUID] && !shiftData.ignoreDruidResource)
            {
                const wsResName = config[WS_API.FIELDS.DRUID_WS_RES];
                if(wsResName && wsResName.length > 0)
                {
                    wildShapeResource = UTILS.getResourceAttribute(shifterSettings[WS_API.FIELDS.ID], wsResName, false);
                    if (wildShapeResource)
                    {
                        if (wildShapeResource.get("current") <= 0)
                        {
                            if (shiftData.isGM)
                            {
                                wildShapeResource = null;
                                UTILS.chat("GM OVERRIDE - You have NO WildShape usage left for the day! resource name: " + wsResName);
                            }
                            else
                            {
                                UTILS.chatErrorToPlayer(shiftData.who, "You have NO WildShape usage left for the day! resource name: " + wsResName);
                                return false;                                
                            }
                        }
                    }
                    else
                    {
                        UTILS.chatErrorToPlayer(shiftData.who, "Cannot find wildshape resource attribute on character sheet = " + wsResName);
                        return false;
                    }
                }
            }

            shiftData.targetCharacterId = shiftData.targetShape[WS_API.FIELDS.ID];
            shiftData.targetCharacter = findObjs({ type: 'character', id: shiftData.targetCharacterId })[0];
            if (!shiftData.targetCharacter)
            {
                UTILS.chatErrorToPlayer(shiftData.who, "Cannot find target character = " + shiftData.targetShape[WS_API.FIELDS.CHARACTER] + " with id = " + shiftData.targetCharacterId);
                return false;
            }

            isTargetNpc = (getAttrByName(shiftData.targetCharacterId, 'npc', 'current') == 1);
        }
        else
        {
            // transform back into default shifter character
            shiftData.targetCharacterId = shiftData.shifterCharacterId;
            shiftData.targetCharacter = shiftData.shifterCharacter;
            isTargetNpc = shifterSettings[WS_API.FIELDS.ISNPC];
            isTargetDefault = true;
        }

        let targetData = null;

        await getTargetCharacterData(shiftData, isTargetNpc, isTargetDefault).then((ret) => { targetData = ret; });

        if (!targetData)
        {
            UTILS.debugChat("cannot get target character data, aborting");
            return false;
        }

        if (isTargetNpc)
        {
            // copy over druid attributes
            if (shifterSettings[WS_API.FIELDS.ISDRUID])
            {
                copyDruidData(shiftData);
            }
        }

        // set bar values
        setTokenBarValues(shiftData.token, targetData.barData, targetData.controlledby, isTargetNpc);

        // force roll settings when we are changing into another shape
        if (!isTargetDefault)
        {
            let targetDataRoot = shifterSettings[WS_API.FIELDS.ISNPC] ? config[WS_API.FIELDS.CHAR_DATA.NPC_ROOT] : config[WS_API.FIELDS.CHAR_DATA.PC_ROOT];

            if (targetDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_NEVER_WHISPER])
            {
                UTILS.setAttribute(shiftData.targetCharacterId, "wtype", "");
            }

            if (targetDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_TOGGLE_ADVANTAGE])
            {
                UTILS.setAttribute(shiftData.targetCharacterId, "rtype", "@{advantagetoggle}");
                UTILS.setAttribute(shiftData.targetCharacterId, "advantagetoggle", "{{query=1}} {{normal=1}} {{r2=[[0d20");
            }

            if (targetDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_MANUAL_DAMAGEROLL])
            {
                UTILS.setAttribute(shiftData.targetCharacterId, "dtype", "pick");
            }
        }

        // check if the token is on a scaled page
        let tokenPageScale = 1.0;
        var tokenPageData = getObj("page", shiftData.token.get("pageid"));
        if (tokenPageData)
        {
            let snapping_increment = tokenPageData.get("snapping_increment");
            if (snapping_increment && snapping_increment > 0) 
                tokenPageScale = snapping_increment;
        }

        // set other token data
        shiftData.token.set({
            imgsrc: targetData.imgsrc,
            represents: targetData.characterId,
            height: targetData.tokenHeight * tokenPageScale,
            width: targetData.tokenWidth * tokenPageScale,
        });

        // set token sense
        if (targetData.senses)
        {
            _.each(WS_API.FIELDS.SENSES.LIGHT_ATTRS, function setLightAttr(attr) {
                shiftData.token.set(attr, targetData.senses[attr]);
            });
        }

        if (!isTargetDefault)
        {
            shiftData.targetCharacter.set({controlledby: targetData.controlledby, inplayerjournals: targetData.controlledby});
        }

        shifterSettings[WS_API.FIELDS.CURRENT_SHAPE] = shiftData.targetShapeName;

        // update wildshape resource
        if (wildShapeResource)
        {
            let wsCurrent = wildShapeResource.get("current") - 1;
            wildShapeResource.set("current",  wsCurrent);
            
            if (!config[WS_API.FIELDS.MUTE_SHIFT])
            {
                UTILS.chatToPlayer(shiftData.who, config[WS_API.FIELDS.DRUID_WS_RES] + " left: " + wsCurrent + " / " + wildShapeResource.get("max"));
            }
        }

        return true;
    }

    async function addShapeToShifter(config, shifter, shapeCharacter, newCharacterName, shapeId = null, doSort = true) {
        const shapeName = shapeCharacter.get('name');
        if ((!shapeId) || (typeof shapeId !== 'string') || (shapeId.length == 0))
            shapeId = shapeName;

        if (shifter[WS_API.FIELDS.SHAPES][shapeId])
        {
            UTILS.chatError("Trying to add a shape with an ID that's already used, skipping: " + shapeId);
            return false;
        }

        UTILS.chat("adding shape " + shapeId + ", please wait...");

        await UTILS.duplicateCharacter(shapeCharacter, newCharacterName)
            .then(
                function(newShapeCharacter) { shapeCharacter = newShapeCharacter; }
            );

        if(!shapeCharacter)
        {
            UTILS.chatError("cannot duplicate character, skipping: " + shapeId);
            return false;
        }

        let shape = {};
        shape[WS_API.FIELDS.ID] = shapeCharacter.get('_id');
        shape[WS_API.FIELDS.CHARACTER] = newCharacterName;
        shape[WS_API.FIELDS.SIZE] = WS_API.SETTINGS.SHAPE_SIZE;
        shape[WS_API.FIELDS.ISDUPLICATE] = true;

        cacheCharacterData(shape);
        copySenses(config, shape);
        shape[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = false;
        
        shifter[WS_API.FIELDS.SHAPES][shapeId] = shape;

        const shifterCharacter = findObjs({ type: 'character', id: shifter[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ID] })[0];
        if (shifterCharacter)
        {
            const shifterControlledBy = shifterCharacter.get("controlledby");
            shapeCharacter.set({controlledby: shifterControlledBy, inplayerjournals: shifterControlledBy});
        }

        if (doSort)
        {
            sortShapes(shifter);
        }

        return true;
    }

    const handleInputShift = (msg, args, config) => 
    {
        if(!msg.selected)
        {
            UTILS.chatErrorToPlayer(msg.who, "Please select a token before shapeshifting");
            return;
        }

        const shapeName = args.shift();

        const tokenObj = getObj(msg.selected[0]._type, msg.selected[0]._id);
        const obj = findShifterData(tokenObj);
        if(obj)
        {            
            // check that the player sending the command can actually control the token
            obj.isGM = playerIsGM(msg.playerid);
            if (obj.isGM || obj.shifterControlledby.search(msg.playerid) >= 0 || obj.shifterControlledby.search("all") >= 0)
            {
                obj.who = msg.who;
                obj.targetShapeName = shapeName;

                if (obj.targetShapeName !== obj.shifter[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.CURRENT_SHAPE])
                {
                    if (obj.targetShapeName != WS_API.SETTINGS.BASE_SHAPE)
                    {
                        obj.targetShape = obj.shifter[WS_API.FIELDS.SHAPES][obj.targetShapeName];
                        if (!obj.targetShape)
                        {
                            UTILS.chatErrorToPlayer(msg.who, "Cannot find shape [" + obj.targetShapeName + "] for ShapeShifter: " + obj.shifterId);
                            return;
                        }
                    }

                    doShapeShift(obj).then((ret) => {
                        if (ret && !config[WS_API.FIELDS.MUTE_SHIFT])
                        {
                            if (obj.targetShape)
                                UTILS.chatAs(obj.shifterCharacter.get("id"), "Transforming into " + shapeName, null, null);
                            else
                                UTILS.chatAs(obj.shifterCharacter.get("id"), "Transforming back into " + obj.shifterId, null, null);
                        }
                    })
                }
                else
                {
                    UTILS.chatErrorToPlayer(msg.who, "You are already transformed into " + shapeName);
                }
            }
            else
            {
                UTILS.chatErrorToPlayer(msg.who, "Trying to shapeshift on a token you don't have control over");
            }
        }
        else
        {
            UTILS.chatErrorToPlayer(msg.who, "Cannot find a ShapeShifter for the selected token");
        }

    };

    const handleInputAddShifter = (msg, args, config) => 
    {
        let tokenId = args.shift();
        let tokenObj = findObjs({type:'graphic', id:tokenId})[0];                                                
        const shifterKey = tokenObj ? tokenObj.get("name") : null;
        if (shifterKey && shifterKey.length > 0)
        {
            let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
            if(!shifter)
            {
                const charId = tokenObj.get("represents");
                let charObj = findObjs({ type: 'character', id: charId });
                if(charObj && charObj.length == 1)
                {
                    const isNpc = (getAttrByName(charId, 'npc', 'current') == 1);

                    shifter = {};
                    
                    let shifterSettings = {};
                    shifterSettings[WS_API.FIELDS.ID] = charId;
                    shifterSettings[WS_API.FIELDS.CHARACTER] = charObj[0].get('name');
                    shifterSettings[WS_API.FIELDS.SIZE] = isNpc ? WS_API.SETTINGS.SHAPE_SIZE : WS_API.SETTINGS.SHIFTER_SIZE;
                    shifterSettings[WS_API.FIELDS.ISDRUID] = !isNpc;
                    shifterSettings[WS_API.FIELDS.ISNPC] = isNpc;
                    shifterSettings[WS_API.FIELDS.CURRENT_SHAPE] = WS_API.SETTINGS.BASE_SHAPE;
    
                    copySenses(config, shifterSettings);
                    shifterSettings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = false;

                    shifter[WS_API.FIELDS.SETTINGS] = shifterSettings;
                    shifter[WS_API.FIELDS.SHAPES] = {};

                    state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey] = shifter;

                    sortShifters();
                    MENU.showEditShifter(shifterKey);
                }
                else
                {
                    UTILS.chatError("Cannot find character with id [" + charId + "] in the journal");
                }

            }
            else
            {
                UTILS.chatError("Trying to add ShapeShifter " + shifterKey + " which already exists");
            }
        }
        else
        {
            UTILS.chatError("Trying to add ShapeShifter from a token without a name");
        }
    };

    async function handleInputAddShape(msg, args, config) 
    {
        const shifterKey = args.shift();
        let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
        if (shifter)
        {
            const shapeName = args.shift();
            let shapeObj = findObjs({ type: 'character', name: shapeName })[0];
            if(shapeObj)
            {
                let shapeCharacterName = shifterKey + " - " + shapeName;
                await addShapeToShifter(config, shifter, shapeObj, shapeCharacterName).then( 
                    (ret) => { if (ret) { MENU.showEditShifter(shifterKey); UTILS.chat("New shape added: " + shapeName); } } );

            }
            else
            {
                UTILS.chatError("Cannot find character [" + shapeName + "] in the journal");
                UTILS.chatError("If you see a list in the error above please make sure the last character in the list doesn't have any double quotes \" in the name, you can replace those with single quotes ' ");
            }
        }
        else
        {
            UTILS.chatError("Trying to add shape to ShapeShifter " + shifterKey + " which doesn't exist");
            MENU.showShifters();
        }
    }

    async function handleInputRemoveShifter(msg, args, config) 
    {
        const shifterKey = args.shift();
        if (shifterKey)
        {
            if(state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey])
            {
                let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
                _.each(shifter[WS_API.FIELDS.SHAPES], (shape) => {
                    removeShape(shape);
                });

                delete state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
            }
            else
            {
                UTILS.chatError("Trying to delete ShapeShifter " + shifterKey + " which doesn't exists");
            }

        }
        else
        {
            UTILS.chatError("Trying to delete a ShapeShifter without providing a name");
        }

        MENU.showShifters();
    }

    const removeShape = (shape) =>
    {
        if (shape)
        {
            const shapeCharacter = findObjs({ type: 'character', id: shape[WS_API.FIELDS.ID] })[0];
            if (shapeCharacter)
            {
                // versions earlier than 1.3 don't have the automatic duplicate, preserve characters
                if(shape[WS_API.FIELDS.ISDUPLICATE])
                {
                    shapeCharacter.remove();
                }
                else
                {
                    shapeCharacter.set({controlledby: "", inplayerjournals: ""});
                }
            }

            return true;
        }

        return false;
    };

    async function handleInputRemoveShape(msg, args, config)
    {
        const shifterKey = args.shift();
        const shapeKey = args.shift();
        let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
        if (shifter) 
        {
            if (shapeKey)
            {
                let shape = shifter[WS_API.FIELDS.SHAPES][shapeKey];
                if (removeShape(shape))
                {
                    delete shifter[WS_API.FIELDS.SHAPES][shapeKey];
                }
                else
                {
                    UTILS.chatError("Trying to remove shape " + shapeKey + " that doesn't exist from ShapeShifter " + shifterKey);
                }                
            }
            else
            {
                // delete all shapes
                _.each(shifter[WS_API.FIELDS.SHAPES], (shape) => {
                    const shapeCharacter = findObjs({ type: 'character', id: shape[WS_API.FIELDS.ID] })[0];
                    if (shapeCharacter)
                    {
                        // versions earlier than 1.3 don't have the automatic duplicate, preserve characters
                        if(shape[WS_API.FIELDS.ISDUPLICATE])
                        {
                            shapeCharacter.remove();
                        }
                        else
                        {
                            shapeCharacter.set({controlledby: "", inplayerjournals: ""});
                        }
                    }
                });

                delete shifter[WS_API.FIELDS.SHAPES][shapeKey];
                shifter[WS_API.FIELDS.SHAPES] = {};
            }

            MENU.showEditShifter(shifterKey);
        }
        else
        {
            UTILS.chatError("Trying to remove shape from ShapeShifter " + shifterKey + " which doesn't exist");
            MENU.showShifters();
        }
    }

    const handleInputEditSenses = (msg, args, config, senses) => 
    {
        const field = args.shift();
        if (field)
        {
            if (field == WS_API.FIELDS.SENSES.OVERRIDE)
                senses[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = !senses[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE];
            else if (field)
            {
                let newValue = args.shift();
                if (newValue)
                {
                    if (newValue == WS_API.CMD.TOGGLE)
                        senses[WS_API.FIELDS.SENSES.ROOT][field] = !senses[WS_API.FIELDS.SENSES.ROOT][field];
                    else 
                        senses[WS_API.FIELDS.SENSES.ROOT][field] = newValue;
                }
            }
        }

        return field;
    };

    const handleInputEditShifter = (msg, args, config) => 
    {
        let shifterKey = args.shift();
        let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
        if (shifter)
        {
            const field = args.shift();
            if (field)
            {
                if (field == WS_API.FIELDS.SENSES.ROOT)
                {
                    let shifterSettings = shifter[WS_API.FIELDS.SETTINGS];
                    const editedSense = handleInputEditSenses(msg, args, config, shifterSettings);

                    // copy defaults in shifter if we are setting override to false
                    if(editedSense == WS_API.FIELDS.SENSES.OVERRIDE && !shifterSettings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
                    {
                        copySenses(config, shifterSettings);
                        shifterSettings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = false;
                    }

                    MENU.showEditSenses(shifterKey);
                    return;
                }
                else
                { 
                    let isValueSet = true;
                    let newValue = args.shift();
                    if(field == WS_API.FIELDS.NAME)
                    {
                        let oldShifterKey = shifterKey; 
                        shifterKey = newValue.trim();

                        if (shifterKey && shifterKey.length > 0)
                        {
                            if(!state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey])
                            {
                                state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey] = shifter;
                                delete state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][oldShifterKey];
                                sortShifters();
                            }
                            else
                            {
                                UTILS.chatError("Trying to add ShapeShifter " + shifterKey + " which already exists");
                                isValueSet = false;
                            }
                        }
                        else
                        {
                            isValueSet = false;
                        }

                    }
                    else if(field == WS_API.FIELDS.CHARACTER)
                    {
                        let charObj = findObjs({ type: 'character', name: newValue });
                        if(charObj && charObj.length == 1)
                        {
                            shifter[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ID] = charObj[0].get('id');
                            shifter[WS_API.FIELDS.SETTINGS][field] = newValue;

                            const shifterControlledBy = charObj[0].get("controlledby");
                            _.each(shifter[WS_API.FIELDS.SHAPES], (shape) => {
                                let shapeObj = findObjs({ type: 'character', id: shape[WS_API.FIELDS.ID] });
                                if (shapeObj && shapeObj.length == 1)
                                    shapeObj[0].set({controlledby: shifterControlledBy, inplayerjournals: shifterControlledBy});
                            });
                        }
                        else
                        {
                            UTILS.chatError("Cannot find character [" + newValue + "] in the journal");
                            isValueSet = false;
                        }
                    }
                    else if(field == WS_API.FIELDS.ISDRUID)
                    {
                        shifter[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ISDRUID] = !shifter[WS_API.FIELDS.SETTINGS][WS_API.FIELDS.ISDRUID];
                    }
                    else
                    {
                        shifter[WS_API.FIELDS.SETTINGS][field] = newValue;
                    }

                    if(isValueSet)
                        MENU.showEditShifter(shifterKey);
                }
            }
            else
                MENU.showEditShifter(shifterKey);
        }
        else
        {
            UTILS.chatError("cannot find shifter [" + shifterKey + "]");
        }
    };

    const handleInputEditShape = (msg, args, config) => 
    {
        const shifterKey = args.shift();
        let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
        if (shifter)
        {
            let shapeKey = args.shift();
            let shape = shifter[WS_API.FIELDS.SHAPES][shapeKey];
            if (shape)
            {
                let field = args.shift();
                if (field)
                {
                    if (field == WS_API.FIELDS.SENSES.ROOT)
                    {
                        const editedSense = handleInputEditSenses(msg, args, config, shape);

                        // copy defaults in shape if we are setting override to false
                        if(editedSense == WS_API.FIELDS.SENSES.OVERRIDE && !shape[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
                        {
                            copySenses(config, shape);
                            shape[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = false;
                        }

                        MENU.showEditSenses(shifterKey, shapeKey);
                    }
                    else
                    {
                        let newValue = args.shift();

                        if(field == WS_API.FIELDS.NAME)
                        {
                            let oldShapeKey = shapeKey;
                            shapeKey = newValue.trim();
                            if (shapeKey && shapeKey.length > 0)
                            {
                                if(!shifter[WS_API.FIELDS.SHAPES][shapeKey])
                                {
                                    shifter[WS_API.FIELDS.SHAPES][shapeKey] = shape;
                                    delete shifter[WS_API.FIELDS.SHAPES][oldShapeKey];
                                    sortShapes(shifter);
                                    MENU.showEditShape(shifterKey, shapeKey);
                                }
                                else
                                {
                                    UTILS.chatError("Trying to rename shape to " + shapeKey + " which already exists");
                                }
                            }
                        }
                        else
                        {
                            shape[field] = newValue;
                            MENU.showEditShape(shifterKey, shapeKey);
                        }
                    }
                }
                else
                {
                    MENU.showEditShape(shifterKey, shapeKey);
                }
            }
            else
            {
                UTILS.chatError("cannot find shape [" + shapeKey + "]");
            }
        }
        else
        {
            UTILS.chatError("cannot find shifter [" + shifterKey + "]");
        }

    };

    const handleInputEditConfig = (msg, args, config) => 
    {
        switch (args.shift())
        {
            case WS_API.FIELDS.SEP:
            {
                let newSEP = args.shift();
                if (validateSeparator(newSEP))
                {
                    config.SEP = newSEP;
                    MENU.updateConfig();
                }
                else
                {
                    UTILS.chatError("invalid new separator specified, empty strings and special chars are not supported");
                }
            }
            break;

            case WS_API.FIELDS.ENABLE_DEBUG:
            {
                config[WS_API.FIELDS.ENABLE_DEBUG] = !config[WS_API.FIELDS.ENABLE_DEBUG];
                UTILS.debugEnable(config[WS_API.FIELDS.ENABLE_DEBUG]);
                MENU.updateConfig();
            }
            break;

            case WS_API.FIELDS.TARGET.CHAR_DATA:
            {
                const dataRoot = args.shift();
                if (dataRoot == WS_API.FIELDS.CHAR_DATA.PC_ROOT || dataRoot == WS_API.FIELDS.CHAR_DATA.NPC_ROOT)
                {
                    const field = args.shift();
                    if (field && field !== "")
                    {
                        let newValue = args.shift()
                        config[dataRoot][field] = newValue == WS_API.CMD.TOGGLE ? !config[dataRoot][field] : newValue;
                    }
                    
                    MENU.showEditCharData(dataRoot);
                    return;
                }
            }
            break;

            case WS_API.FIELDS.SENSES.ROOT:
            {
                const editedSense = handleInputEditSenses(msg, args, config, config);
                if(editedSense && editedSense !== WS_API.FIELDS.SENSES.OVERRIDE)
                {
                    let newSenseValue = config[WS_API.FIELDS.SENSES.ROOT][editedSense];
                    
                    // update default senses on all shifters/shapes that are not overriding
                    _.each(state[WS_API.STATENAME][WS_API.DATA_SHIFTERS], (shifterValue, shifterId) => {
                        let shifterSettings = shifterValue[WS_API.FIELDS.SETTINGS];

                        // copy defaults in shifters
                        if (!shifterSettings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
                        {
                            shifterSettings[WS_API.FIELDS.SENSES.ROOT][editedSense] = newSenseValue;
                        }

                        _.each(shifterValue[WS_API.FIELDS.SHAPES], (shapeValue, shapeId) => {
                            // copy defaults in shapes
                            if (!shapeValue[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE])
                            {
                                shapeValue[WS_API.FIELDS.SENSES.ROOT][editedSense] = newSenseValue;
                            }
                        });
                    });
                }

                MENU.showEditSenses();
                return;
            }
            break;

            case WS_API.FIELDS.DRUID_WS_RES:
            {
                config[WS_API.FIELDS.DRUID_WS_RES] = args.shift();
            }
            break;

            case WS_API.FIELDS.MUTE_SHIFT:
            {
                config[WS_API.FIELDS.MUTE_SHIFT] = !config[WS_API.FIELDS.MUTE_SHIFT];
            }
            break;

        }

        MENU.showConfigMenu();
    };

    async function handleInputImportShapeFolder(msg, args, config) 
    {
        const shifterKey = args.shift();
        let shifter = state[WS_API.STATENAME][WS_API.DATA_SHIFTERS][shifterKey];
        if (shifter)
        {
            const folderName = args.shift();
            const searchSubfolders = args.shift() == 'yes';
            let removePrefix = args.shift();
            if (removePrefix == "")
                removePrefix = null;

            let folderShapes = UTILS.findCharactersInFolder(folderName, searchSubfolders);
            if(folderShapes)
            {
                if (WS_API.DEBUG)
                {
                    let debugShapeNames = "";
                    _.each(folderShapes, function(shape) { debugShapeNames += shape.get("name") + ", "; });
                    UTILS.chat("shapes found in input folder = " + debugShapeNames);
                }

                UTILS.chat("start importing shapes...");
                let importedShapes = 0;

                for (let shapeIndex = folderShapes.length - 1; shapeIndex >= 0; --shapeIndex) {
                    let shape = folderShapes[shapeIndex]; 
                    let shapeObj = findObjs({ type: 'character', id: shape.id })[0];                                                            
                    if (shapeObj)
                    {
                        let newName = shapeObj.get("name");
                        if (removePrefix && newName.startsWith(removePrefix))
                        {
                            newName = newName.slice(removePrefix.length);
                        }

                        const shapeId = newName;
                        newName = shifterKey + " - " + newName;

                        // add shape to shifter
                        await addShapeToShifter(config, shifter, shapeObj, newName, shapeId, false).then(
                            (ret) => { if (ret) importedShapes += 1; });
                    }
                }

                if (importedShapes > 0)
                {
                    sortShapes(shifter);
                    MENU.showEditShifter(shifterKey);
                    UTILS.chat("Imported " + importedShapes + "/" + folderShapes.length + " Shapes from folder [" + folderName + "]");
                }

                if (importedShapes < folderShapes.length)
                {
                    UTILS.chat("Not all shapes were imported, please check for errors " + (importedShapes > 0 ? "above the menu" : ""));
                }
            }
            else
            {
                UTILS.chatError("Cannot find any shapes in the input folder  [" + folderName + "]");
            }
        }
        else
        {
            UTILS.chatError("Trying to add shape to ShapeShifter " + shifterKey + " which doesn't exist");
            MENU.showShifters();
        }                                                
    }

    const handleInput = (msg) => {
        if (msg.type === "api" && msg.content.indexOf(WS_API.CMD.ROOT) == 0)
        {
            let config = state[WS_API.STATENAME][WS_API.DATA_CONFIG];
            const args = msg.content.split(config.SEP);
            args.shift(); // remove WS_API.CMD.ROOT
            if(args.length == 0)
            {
                if (!msg.selected)
                {
                    if (playerIsGM(msg.playerid))
                    {
                        MENU.showConfigMenu();
                    }
                    else
                    {
                        UTILS.chatToPlayer(msg.who, WS_API.CMD.USAGE);
                    }
                    return;
                }

                const tokenObj = getObj(msg.selected[0]._type, msg.selected[0]._id);
                const obj = findShifterData(tokenObj);
                if (obj)
                {
                    if (playerIsGM(msg.playerid) || obj.shifterControlledby.search(msg.playerid) >= 0 || obj.shifterControlledby.search("all") >= 0)
                    {
                        MENU.showShapeShiftMenu(msg.who, msg.playerid, obj.shifterId, obj.shifter[WS_API.FIELDS.SHAPES]);
                    }
                    else
                    {
                        UTILS.chatErrorToPlayer(msg.who, "Trying to shapeshift on a token you don't have control over");
                    }
                }
                else {
                    UTILS.chatErrorToPlayer(msg.who, "Cannot find ShapeShifter for the selected token");
                }
                return;
            }
            else 
            {
                let cmd = args.shift();

                if (cmd == WS_API.CMD.SHIFT)
                {
                    handleInputShift(msg, args, config);
                }
                else if (playerIsGM(msg.playerid))
                {
                    // GM ONLY COMMANDS
                    switch (cmd)
                    {
                        case WS_API.CMD.SHOW_SHIFTERS:
                        {
                            MENU.showShifters();
                        }
                        break;

                        case WS_API.CMD.CONFIG:
                        {
                            switch (args.shift())
                            {
                                case WS_API.CMD.ADD:
                                {
                                    switch (args.shift())
                                    {
                                        case WS_API.FIELDS.TARGET.SHIFTER:  handleInputAddShifter(msg, args, config); break;
                                        case WS_API.FIELDS.TARGET.SHAPE:    handleInputAddShape(msg, args, config); break;
                                    }
                                }
                                break;

                                case WS_API.CMD.REMOVE:
                                {
                                    if (args.shift() == 'no')
                                        return;

                                    switch (args.shift())
                                    {
                                        case WS_API.FIELDS.TARGET.SHIFTER:  handleInputRemoveShifter(msg, args, config); break;
                                        case WS_API.FIELDS.TARGET.SHAPE:    handleInputRemoveShape(msg, args, config); break;
                                    }
                                }
                                break;

                                case WS_API.CMD.EDIT:
                                {
                                    switch (args.shift())
                                    {
                                        case WS_API.FIELDS.TARGET.SHIFTER:  handleInputEditShifter(msg, args, config); break;
                                        case WS_API.FIELDS.TARGET.SHAPE:    handleInputEditShape(msg, args, config); break;
                                        case WS_API.FIELDS.TARGET.CONFIG:   handleInputEditConfig(msg, args, config); break;
                                    }
                                }
                                break;

                                case WS_API.CMD.RESET:
                                {
                                    if (args.shift() == 'no')
                                        return;

                                    setDefaults(true);
                                }
                                break;

                                case WS_API.CMD.IMPORT:
                                {
                                    switch (args.shift())
                                    {
                                        case WS_API.FIELDS.TARGET.SHAPEFOLDER: handleInputImportShapeFolder(msg, args, config); break;
                                    }
                                }
                                break;

                                default: MENU.showConfigMenu();
                            }
                        }
                        break;

                        case WS_API.CMD.HELP:
                        {
                            UTILS.chat(WS_API.CMD.USAGE);
                        }
                        break;
                    }
                }
            }
        }
    };

    const handleAddToken = (token) => 
    {
        let obj = findShifterData(token, true);
        if(obj)
        {
            let shifterSettings = obj.shifter[WS_API.FIELDS.SETTINGS];
            obj.isGM = true;
            obj.ignoreDruidResource = true;
            obj.who = "gm";
            obj.targetShapeName = shifterSettings[WS_API.FIELDS.CURRENT_SHAPE];

            if (obj.targetShapeName != WS_API.SETTINGS.BASE_SHAPE)
            {
                obj.targetShape = obj.shifter[WS_API.FIELDS.SHAPES][obj.targetShapeName];
                if (!obj.targetShape)
                {
                    UTILS.chatError("Cannot find shape [" + obj.targetShapeName + "] for ShapeShifter: " + obj.shifterId);
                    return;
                }

                doShapeShift(obj).then((ret) => {
                    if (ret)
                    {
                        UTILS.chat("Dropping shifter token, transforming " + obj.shifterId + " into " + obj.targetShapeName);
                    }
                    else 
                    {
                        UTILS.chatError("Dropping shifter token, cannot transform " + obj.shifterId + " into " + obj.targetShapeName);
                    }
                });
            }
            else
            {
                // make sure bars are setup properly on token drop
                setTokenBarValues(token, getTokenBarData(shifterSettings, obj.shifterCharacterId, true, shifterSettings[WS_API.FIELDS.ISNPC]), obj.shifterControlledby);
            }
        }
    };

    const upgradeVersion = () => {
        const apiState = state[WS_API.STATENAME];
        const currentVersion = apiState[WS_API.DATA_CONFIG].VERSION;
        const newConfig = WS_API.DEFAULT_CONFIG;

        let config = apiState[WS_API.DATA_CONFIG];
        let shifters = apiState[WS_API.DATA_SHIFTERS];

        if (UTILS.compareVersion(currentVersion, "1.0.2") < 0)
        {
            const npcFields = WS_API.DEPRECATED.NPC_DATA;
            config[npcFields.ROOT] = {};
            config[npcFields.ROOT][npcFields.HP_CACHE] = newConfig[npcFields.ROOT][npcFields.HP_CACHE];
            config[npcFields.ROOT][npcFields.HP]       = newConfig[npcFields.ROOT][npcFields.HP];
            config[npcFields.ROOT][npcFields.AC]       = newConfig[npcFields.ROOT][npcFields.AC];
            config[npcFields.ROOT][npcFields.SPEED]    = newConfig[npcFields.ROOT][npcFields.SPEED];

            const pcFields = WS_API.DEPRECATED.PC_DATA;
            config[pcFields.ROOT] = {};
            config[pcFields.ROOT][pcFields.HP]        = newConfig[pcFields.ROOT][pcFields.HP];
            config[pcFields.ROOT][pcFields.AC]        = newConfig[pcFields.ROOT][pcFields.AC];
            config[pcFields.ROOT][pcFields.SPEED]     = newConfig[pcFields.ROOT][pcFields.SPEED];
        }

        if (UTILS.compareVersion(currentVersion, "1.0.4") < 0)
        {
            // add MAKEROLLPUBLIC field to shifters, default to true for non-npcs
            _.each(shifters, (value, shifterId) => {
                let shifterSettings = shifters[shifterId][WS_API.FIELDS.SETTINGS];
                shifterSettings[WS_API.DEPRECATED.MAKEROLLPUBLIC] = !shifterSettings[WS_API.FIELDS.ISNPC];
            });
        }

        if (UTILS.compareVersion(currentVersion, "1.0.5") < 0)
        {
            // updated separator to minimize collisions with names/strings
            if(config.SEP == "--")
                config.SEP = newConfig.SEP;
        }

        if (UTILS.compareVersion(currentVersion, "1.0.6") < 0)
        {
            // copy defaults in config
            copySenses(newConfig, config);

            _.each(shifters, (value, shifterId) => {
                let shifterSettings = shifters[shifterId][WS_API.FIELDS.SETTINGS];

                // copy defaults in all shifters
                copySenses(newConfig, shifterSettings);
                shifterSettings[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = false;

                _.each(shifters[shifterId][WS_API.FIELDS.SHAPES], (shapeValue, shapeId) => {
                    // copy defaults in all shapes
                    copySenses(newConfig, shapeValue);
                    shapeValue[WS_API.FIELDS.SENSES.ROOT][WS_API.FIELDS.SENSES.OVERRIDE] = false;
                });
            });
        }

        if (UTILS.compareVersion(currentVersion, "1.0.7") < 0)
        {
            config[WS_API.FIELDS.CHAR_DATA.NPC_ROOT][WS_API.DEPRECATED.NPC_DATA.SENSES] = newConfig[WS_API.FIELDS.CHAR_DATA.NPC_ROOT][WS_API.DEPRECATED.NPC_DATA.SENSES];
        }

        if (UTILS.compareVersion(currentVersion, "1.2") < 0)
        {
            // cache stats for all shapes
            _.each(shifters, (shifterValue, shifterId) => {
                _.each(shifters[shifterId][WS_API.FIELDS.SHAPES], (shapeValue, shapeId) => {
                    cacheCharacterData(shapeValue);
                });
            });
        }

        if (UTILS.compareVersion(currentVersion, "1.4.0") < 0)
        {
            let pcDataRoot = config[WS_API.FIELDS.CHAR_DATA.PC_ROOT];
            let npcDataRoot = config[WS_API.FIELDS.CHAR_DATA.NPC_ROOT];

            // convert MAKEROLLPUBLIC to separate settings
            // calculate majority of values across shifters separately for PCs and NPCs to use as default in the new setting
            let forceCountNPC = 0;
            let forceCountPC = 0;
            _.each(shifters, (shifterValue, shifterId) => {
                let shifterSettings = shifterValue[WS_API.FIELDS.SETTINGS];
                let oldVal = shifterSettings[WS_API.DEPRECATED.MAKEROLLPUBLIC];
                if (oldVal != null)
                {
                    let sign = oldVal ? 1 : -1;
                    if (shifterSettings[WS_API.FIELDS.ISNPC])
                    {
                        forceCountNPC = forceCountNPC + 1 * sign;
                    }
                    else
                    {
                        forceCountPC = forceCountPC + 1 * sign;
                    }

                    delete shifterSettings[WS_API.DEPRECATED.MAKEROLLPUBLIC];
                }
            });

            // new default for NPCs is FALSE, hence we need a majority to be previously set to true
            let forceRoll = forceCountNPC > 0;
            npcDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_NEVER_WHISPER] = forceRoll;
            npcDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_TOGGLE_ADVANTAGE] = forceRoll;
            npcDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_MANUAL_DAMAGEROLL] = forceRoll;

            // new default for PCs is TRUE, hence we need a tie or a majority to be previously set to true
            forceRoll = forceCountPC >= 0;
            pcDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_NEVER_WHISPER] = forceRoll;
            pcDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_TOGGLE_ADVANTAGE] = forceRoll;
            pcDataRoot[WS_API.FIELDS.CHAR_DATA.FORCEROLL_MANUAL_DAMAGEROLL] = forceRoll;

            // upgrade token bar data from HP/AC/SPEED to BAR1/2/3 settings
            let tokenDataRoot = config[WS_API.DEPRECATED.TOKEN_DATA.ROOT];

            function upgradeTokenDataValue(dataRoot, tokenKey, dataKey)
            {
                let barIndex = tokenDataRoot[tokenKey];
                if (barIndex == WS_API.FIELDS.CHAR_DATA.BAR_1 || barIndex == WS_API.FIELDS.CHAR_DATA.BAR_2 || barIndex == WS_API.FIELDS.CHAR_DATA.BAR_3)
                {
                    let dataValue = dataRoot[dataKey];
                    dataRoot[barIndex] = (dataValue && dataValue !== "") ? dataValue : "";
                    delete dataRoot[dataKey];
                }
            }

            upgradeTokenDataValue(pcDataRoot,  WS_API.DEPRECATED.TOKEN_DATA.HP,    WS_API.DEPRECATED.PC_DATA.HP);
            upgradeTokenDataValue(pcDataRoot,  WS_API.DEPRECATED.TOKEN_DATA.AC,    WS_API.DEPRECATED.PC_DATA.AC);
            upgradeTokenDataValue(pcDataRoot,  WS_API.DEPRECATED.TOKEN_DATA.SPEED, WS_API.DEPRECATED.PC_DATA.SPEED);
            upgradeTokenDataValue(npcDataRoot, WS_API.DEPRECATED.TOKEN_DATA.HP,    WS_API.DEPRECATED.NPC_DATA.HP);
            upgradeTokenDataValue(npcDataRoot, WS_API.DEPRECATED.TOKEN_DATA.AC,    WS_API.DEPRECATED.NPC_DATA.AC);
            upgradeTokenDataValue(npcDataRoot, WS_API.DEPRECATED.TOKEN_DATA.SPEED, WS_API.DEPRECATED.NPC_DATA.SPEED);

            delete config[WS_API.DEPRECATED.TOKEN_DATA.ROOT];
        }

        if (UTILS.compareVersion(currentVersion, "1.4.1") < 0)
        {
            // updated separator to make commands more readable
            if (config.SEP == "###")
                config.SEP = newConfig.SEP;

            // new enable debug field
            config[WS_API.FIELDS.ENABLE_DEBUG] = false;

            // make sure all defaults for fields added in 1.4.0 are properly set
            {
                function setCharDataDefault(dataRoot, key, val)
                {
                    if (dataRoot[key] == null || dataRoot[key] == "" || typeof(dataRoot[key]) !== typeof(val))
                    {
                        dataRoot[key] = val;
                    }
                }

                let pcDataRoot = config[WS_API.FIELDS.CHAR_DATA.PC_ROOT];
                let npcDataRoot = config[WS_API.FIELDS.CHAR_DATA.NPC_ROOT];

                setCharDataDefault(pcDataRoot, WS_API.FIELDS.CHAR_DATA.HP, WS_API.DEFAULT_CONFIG.PC_DATA.HP);
                setCharDataDefault(pcDataRoot, WS_API.FIELDS.CHAR_DATA.SPEED, WS_API.DEFAULT_CONFIG.PC_DATA.SPEED);
                setCharDataDefault(npcDataRoot, WS_API.FIELDS.CHAR_DATA.HP, WS_API.DEFAULT_CONFIG.NPC_DATA.HP);
                setCharDataDefault(npcDataRoot, WS_API.FIELDS.CHAR_DATA.SPEED, WS_API.DEFAULT_CONFIG.NPC_DATA.SPEED);
                setCharDataDefault(npcDataRoot, WS_API.FIELDS.CHAR_DATA.SENSES, WS_API.DEFAULT_CONFIG.NPC_DATA.SENSES);
            }
        }

        config.VERSION = WS_API.VERSION;
    };

    async function setDefaults(reset)
    {
        let oldVersionDetected = null;
        let apiState = state[WS_API.STATENAME];

        // check for basic state
        if(!apiState || typeof apiState !== 'object' || reset)
        {
            if(state[WS_API.STATENAME])
            {
                _.each(state[WS_API.STATENAME][WS_API.DATA_SHIFTERS], 
                    (shifter) => {
                        _.each(shifter[WS_API.FIELDS.SHAPES], (shape) => {
                            removeShape(shape);
                        });
                });                
            }

            state[WS_API.STATENAME] = {};
            apiState = state[WS_API.STATENAME];
            reset = true;
        }
 
        // check version
        if (!apiState[WS_API.DATA_CONFIG] || typeof apiState[WS_API.DATA_CONFIG] !== 'object' || reset) 
        {
            apiState[WS_API.DATA_CONFIG] = WS_API.DEFAULT_CONFIG;
            apiState[WS_API.DATA_CONFIG].VERSION = WS_API.VERSION;
            reset = true;
        }

        // initialize global references
        let config = apiState[WS_API.DATA_CONFIG];
        UTILS.debugEnable(config[WS_API.FIELDS.ENABLE_DEBUG]);

        // check for upgrades
        if (UTILS.compareVersion(config.VERSION, WS_API.VERSION) < 0)
        {
            oldVersionDetected = apiState[WS_API.DATA_CONFIG].VERSION;
            upgradeVersion();
        }

        // validate separator
        if (!validateSeparator(config.SEP) || reset)
        {
            UTILS.debugLog("resetting separator");
            config.SEP = WS_API.DEFAULT_CONFIG.SEP;
        }

        if (!apiState[WS_API.DATA_SHIFTERS] || typeof apiState[WS_API.DATA_SHIFTERS] !== 'object' || reset)
        {
            UTILS.debugLog("resetting DATA_SHIFTERS");
            apiState[WS_API.DATA_SHIFTERS] = {};
        }

        // update configuration after upgrade
        MENU.updateConfig();

        if (reset || oldVersionDetected)
        {
            MENU.showConfigMenu(true);

            if (oldVersionDetected)
            {
                _.each(WS_API.CHANGELOG, function (changes, version) 
                    {
                        if (UTILS.compareVersion(oldVersionDetected, version) < 0)
                            UTILS.chat("Updated to " + version + ": " + changes);
                    });

                UTILS.chat("New version detected, updated from " + oldVersionDetected + " to " + WS_API.VERSION);
            }
        }
    }

    async function start()
    {
        // check install
        if (!UTILS.VERSION || UTILS.compareVersion(UTILS.VERSION, WS_API.REQUIRED_HELPER_VERSION) < 0)
        {
            UTILS.chatError("This API version " + WS_API.VERSION + " requires WildUtils version " + WS_API.REQUIRED_HELPER_VERSION + ", please update your WildHelpers script");
            return;
        }

        await setDefaults();

        // register event handlers
        on('chat:message', handleInput);
        on('add:token', function (t) {
            _.delay(() => {
                handleAddToken(t);
            }, 100);
        });

        log(WS_API.NAME + ' v' + WS_API.VERSION + " Ready! WildUtils v" + UTILS.VERSION);
        UTILS.chat("API v" + WS_API.VERSION + " Ready! command: " + WS_API.CMD.ROOT);
    }
    
    return {
        start
    };
})();


on('ready', () => { 
    'use strict';

    WildShape.start();
});