/// <reference path="../Person.ts" />
/// <reference path="../Bodypart/HumanoidParts.ts" />
/// <reference path="../../Action/ActionRemove.ts" />
/// <reference path="../../Action/ActionWear.ts" />
// TODO : Implements LiquidContainer for Milk udders. Maybe prostates.

interface HumanoidOptions extends ThingOptions {
    isMale : boolean;
}

interface HumanoidGender {
    hasPenis : boolean;
    hasPenisBulge : boolean;
    hasVagina : boolean;
    hasTits : boolean;
    genderValue : number; // Only includes clothing and bodyparts, good measure for bisexual NPCs
    genderValueCorrected : number; // Includes sexual characteristics being visible: breast size existing, a crotch bulge, etc. Heavily altered by a penis/vagina being out in the open
}

interface HumanoidSluttiness {
    halfNaked : boolean;
    naked : boolean;
    sluttiness : number; // Only includes clothing
    sluttinessCorrected : number; // Includes mannerisms caused by personality
}

interface HumanoidBulges {
    breasts : number;
    crotch : number;
    butt : number;
    waist : number;
}

class Humanoid extends Person {
    public static SEX_MALE = 0;
    public static SEX_FEMALE = 1;
    public static SEX_HERM = 2;

    public breastVisibleSize = 0;
    public isBreastVisible = false;
    public isBreastTight = false;
    public isBreastLoose = false;

    public crotchVisibleSize = 0;
    public isCrotchVisible = false;
    public isCrotchTight = false;

    public buttVisibleSize = 0;
    public isButtVisible = false;
    public isButtTight = false;

    public isGenderCached : boolean = false;
    public isSluttinessCached : boolean = false;
    public cachedGenderValue : HumanoidGender;
    public cachedSluttiness : HumanoidSluttiness;
    public cachedBulges : HumanoidBulges;

    public uncoveredSlots : Array<number> = [];

    public invalidateCaches () {
        this.isGenderCached = false;
        this.isSluttinessCached = false;
    }

    public getGenderValue () : HumanoidGender {
        if (!this.isSluttinessCached) {
            this.updateCaches();
        }

        return this.cachedGenderValue;
    }

    public getSluttiness () : HumanoidSluttiness {
        if (!this.isSluttinessCached) {
            this.updateCaches();
        }

        return this.cachedSluttiness;
    }

    public getBulges () {
        if (!this.isGenderCached) {
            this.updateCaches();
        }
        return this.cachedBulges;
    }

    public updateCaches () {
        this.updateClothing();
        this.updateSlots();
        this.updateBodyparts();
        this.updateBulges();
        this.updateGenderValue();
        this.updateSluttiness();
    }

    public updateSlots () {
        this.uncoveredSlots = [];
        for (let i = 0; i < Humanoid.SLOT_SLOT_COUNT; i++) { this.uncoveredSlots.push(i); }

        let clothing = <Array<Clothing>>Thing.WearRelation.getRightType(this, Clothing);
        for (let i = 0; i < clothing.length; i++) {
            let covering = clothing[i].getCoveringSlots();
            for (let k = 0; k < covering.length; k++) {
                let idx = this.uncoveredSlots.indexOf(covering[k]);
                if (idx >= 0) {
                    this.uncoveredSlots.splice(idx, 1);
                }
            }

            if (this.uncoveredSlots.length == 0) break;
        }
    }

    public updateGenderValue () {
        let clothingGender = Clothing.getGenderValueOn(this);
        let bodypartGender = Bodypart.getGenderValueOn(this);

        let genderWeight = clothingGender.weight + bodypartGender.weight;
        let genderValue = clothingGender.value + bodypartGender.value;
        let correctionWeight = Bodypart.WEIGHT_HIGHEST;
        let correctionValue = this.getStat(Attributes.GenderIdentity) * Bodypart.WEIGHT_HIGHEST; // Gender Identity has weight for Gender Value. It includes things such as intonation and manneirisms, so it counts.

        let hasPenis : boolean = false;
        let hasVagina : boolean = false;
        if (this.isCrotchVisible) {
            // Does this have a vagina or not? It's visible, so it affects OBVIOUSLY MALE/FEMALE
            if (Thing.PartRelation.getRightTypeOne(this, HumanoidPenis) != undefined) {
                hasPenis = true;
                hasVagina = false;
                correctionValue += 0 * Bodypart.WEIGHT_LOWEST;
                correctionWeight += Bodypart.WEIGHT_LOWEST;
            } else if (Thing.PartRelation.getRightTypeOne(this, HumanoidVagina) != undefined) {
                hasPenis = false;
                hasVagina = true;
                correctionValue += 100 * Bodypart.WEIGHT_LOWEST;
                correctionWeight += Bodypart.WEIGHT_LOWEST;
            }
        }

        if (this.breastVisibleSize > 0) {
            correctionValue += 100 * Bodypart.WEIGHT_LOWEST;
            correctionWeight += Bodypart.WEIGHT_LOWEST;
        }

        if (this.crotchVisibleSize > 0 && !hasVagina) {
            correctionValue += 0 * Bodypart.WEIGHT_LOWEST;
            correctionWeight += Bodypart.WEIGHT_LOWEST;
        }

        let genderValueFinal = genderValue / genderWeight;
        let genderValueCorrected = (genderValue + correctionValue) / (genderWeight + correctionWeight);

        this.cachedGenderValue = {
            hasTits : this.breastVisibleSize > 1, // ignore microtits
            hasPenisBulge : this.crotchVisibleSize > 0 && !hasVagina, //safe guard against puffy vaginas if they ever come up
            hasPenis : hasPenis,
            hasVagina : hasVagina,
            genderValue : genderValueFinal,
            genderValueCorrected : genderValueCorrected
        };
        this.isGenderCached = true;
    }

    public isVisibleOn (slot : number) {
        return this.uncoveredSlots.indexOf(slot) != -1;
    }

    /* TODO: This will suck. Goals: Sluttiness of a naked normal person should be between 45-50.
     *        A naked person should max out at around 80 with TERRIBLE holes. So some clothing is sluttier than being outright naked, and being naked is not that slutty unless you're fucked up.
     *        Being mostly covered with the exception of genitals should be seen as worse than being nake-der as well. Might just leave that for the outfits, though.
     */
    public updateSluttiness () {
        // TODO: Maybe use these.
        // let nakedBreakpoint = 0.3;
        // let halfnakedBreakpoint = 0.5;
        let clothingSluttiness = Clothing.getSluttinessValueOn(this);
        let bodypartSluttiness = Bodypart.getSluttinessValueOn(this);

        let sluttinessWeight = clothingSluttiness.weight + bodypartSluttiness.weight;
        let sluttinessValue = clothingSluttiness.value + bodypartSluttiness.value;

        let correctionWeight = 0;
        let correctionValue = 0;

        let somewhatSlutty = [Humanoid.SLOT_MIDRIFF, Humanoid.SLOT_LEG_UPPER, Humanoid.SLOT_WAIST, Humanoid.SLOT_HIPS, Humanoid.SLOT_BACK];
        for (let i = 0; i < somewhatSlutty.length; i++) {
            if (this.isVisibleOn(somewhatSlutty[i])) {
                correctionWeight += 1;
                correctionValue += 75;
            }
        }

        let verySlutty = [Humanoid.SLOT_BREASTS, Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK, Humanoid.SLOT_BUTT];
        for (let i = 0; i < verySlutty.length; i++) {
            if (this.isVisibleOn(verySlutty[i])) {
                correctionWeight += 3;
                correctionValue += 75;
            }
        }

        if ((this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) > 0.75) {
            let extremeSlutty = [Humanoid.SLOT_BREASTS, Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK, Humanoid.SLOT_BUTT];
            for (let i = 0; i < extremeSlutty.length; i++) {
                if (this.isVisibleOn(extremeSlutty[i])) {
                    correctionWeight += 5;
                    correctionValue += 85;
                }
            }
        }

        let halfNaked : boolean;
        let naked : boolean;
        if (!this.isMale()) {
            halfNaked = (this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) < 0.5 &&
                !this.isVisibleOnArray([Humanoid.SLOT_BREASTS, Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK]);
            naked = (this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) < 0.3 &&
                this.isVisibleOnArray([Humanoid.SLOT_BREASTS, Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK, Humanoid.SLOT_BUTT]);
        } else {
            if (this.hasBreasts()) {
                halfNaked = (this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) < 0.5 &&
                    !this.isVisibleOnArray([Humanoid.SLOT_BREASTS, Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK]);
                naked = (this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) < 0.3 &&
                    this.isVisibleOnArray([Humanoid.SLOT_BREASTS, Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK, Humanoid.SLOT_BUTT]);
            } else {
                halfNaked = (this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) < 0.5 &&
                    !this.isVisibleOnArray([Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK]);
                naked = (this.isVisibleOn.length / Humanoid.SLOT_SLOT_COUNT) < 0.3 &&
                    this.isVisibleOnArray([Humanoid.SLOT_CROTCH_FRONT, Humanoid.SLOT_CROTCH_BACK, Humanoid.SLOT_BUTT]);
            }
        }

        let sluttinessValueFinal = sluttinessValue / sluttinessWeight;
        let sluttinessCorrected = (sluttinessValue + correctionValue) / (sluttinessWeight + correctionWeight);

        this.cachedSluttiness = {
            halfNaked : halfNaked,
            naked : naked,
            sluttiness : sluttinessValueFinal,
            sluttinessCorrected : sluttinessCorrected
        };
        this.isSluttinessCached = true;
    }

    public isVisibleOnArray (arr : Array<number>) {
        for (let i =0 ; i < arr.length; i++) {
            if (!this.isVisibleOn(arr[i])) {
                return false;
            }
        }
        return true;
    }

    public addMaleParts () {
        if (!this.isMale() && !this.isHerm()) {
            this.addParts(
                new HumanoidTesticles(), new HumanoidPenis()
            );
        }
        this.invalidateCaches();
    }

    public addFemaleParts () {
        if (!this.isFemale() && !this.isHerm()) {
            this.addParts(
                new HumanoidVagina()
            );
        }
        this.invalidateCaches();
    }

    public removeGenderedParts () {
        this.removeParts(HumanoidVagina);
        this.removeParts(HumanoidPenis);
        this.removeParts(HumanoidTesticles);
        this.invalidateCaches();
    }

    public setGenderValue (value : number) {
        let bp = this.getParts(Bodypart);
        for (let i = 0; i < bp.length; i++) {
            if (bp[i] instanceof HumanoidBreasts && this.isMale()) { // don't add breasts automatically to a male!
                bp[i].arrangeGenderValue(0);
            } else {
                bp[i].arrangeGenderValue(value);
            }
        }
    }

    public constructor (options? : HumanoidOptions) {
        super(options);
        this.addParts(
            new HumanoidSkin(), new HumanoidHead(), new HumanoidArms(),
            new HumanoidHands(), new HumanoidBreasts(), new HumanoidFeet(),
            new HumanoidTorso(), new HumanoidButt()
        );

        if (options.isMale) {
            this.addMaleParts();
            this.setGenderValue(25);
        } else {
            this.addFemaleParts();
            this.setGenderValue(75);
        }

        this.addGetAlterations((humanoid : Humanoid) => {
            // Let's not do this for NPCs. It can break patches.
            if (humanoid.isPlayer()) {
                return {
                    HumanoidGender: humanoid.isMale() ? Humanoid.SEX_MALE :
                        humanoid.isFemale() ? Humanoid.SEX_FEMALE :
                            Humanoid.SEX_HERM
                }
            }
        });

        this.addSetAlterations((humanoid : Humanoid, changes) => {
            // Let's not do this for NPCs. It can break patches.
            if (humanoid.isPlayer()) {
                humanoid.removeGenderedParts();
                if (changes.HumanoidGender == Humanoid.SEX_MALE) {
                    humanoid.addMaleParts();
                } else if (changes.HumanoidGender == Humanoid.SEX_FEMALE) {
                    humanoid.addFemaleParts();
                } else {
                    humanoid.addMaleParts();
                    humanoid.addFemaleParts();
                }
            }
        });
    }

    public updateBodyparts () {
        let parts = <Array<Bodypart>> Thing.PartRelation.getRightType(this, Bodypart);
        for (let i = 0; i < parts.length; i++) {
            parts[i].updateStatus();
        }
    }

    public updateClothing () {
        let clothes = <Array<Clothing>> Thing.WearRelation.getRightType(this, Clothing);
        for (let i = 0; i < clothes.length; i++) {
            clothes[i].updateStatus();
        }
    }

    public updateBulges () {
        let clothes = Thing.WearRelation.getRightType(this, Clothing).sort(function (a : Clothing, b : Clothing) {
            return a.layer - b.layer;
        });

        this.isBreastVisible = true;
        this.isCrotchVisible = true;
        this.isButtVisible = true;

        let bras = [];
        let butts = [];
        let junks = [];

        clothes.forEach((cloth : Clothing) => {
            cloth.updateStatus();
            if (cloth.slots.indexOf(Humanoid.SLOT_BREASTS) != -1) {
                bras.push(cloth);
                if (cloth.transparentSlots.indexOf(Humanoid.SLOT_BREASTS) == -1) {
                    this.isBreastVisible = false;
                }
            }
            if (cloth.slots.indexOf(Humanoid.SLOT_BUTT) != -1) {
                butts.push(cloth);
                if (cloth.transparentSlots.indexOf(Humanoid.SLOT_BUTT) == -1) {
                    this.isButtVisible = false;
                }
            }
            if (cloth.slots.indexOf(Humanoid.SLOT_CROTCH_FRONT) != -1) {
                junks.push(cloth);
                if (cloth.transparentSlots.indexOf(Humanoid.SLOT_CROTCH_FRONT) == -1) {
                    this.isCrotchVisible = false;
                }
            }
        });

        this.updateBreastSizes(bras);
        this.updateCrotchSizes(junks);
        this.updateButtSizes(butts);

        this.cachedBulges = {
            breasts : this.breastVisibleSize,
            butt : this.buttVisibleSize,
            crotch : this.crotchVisibleSize,
            waist : 0
        };
    }

    public getTopClothOn (slot : number) {
        return Thing.WearRelation.getRightType(this, Clothing).filter(otherCloth => {
            return otherCloth.visibleOn.includes(slot);
        }).sort((a : Clothing, b : Clothing) => {
            return a.layer - b.layer;
        })[0];
    }

    public updateBreastSizes (bras : Array<Clothing>) {
        this.isBreastTight = false;
        this.isBreastLoose = true;
        let breasts = <HumanoidBreasts> Thing.PartRelation.getRightTypeOne(this, HumanoidBreasts);
        this.breastVisibleSize = breasts.getSize();
        bras.forEach((bra : Clothing) => {
            if (bra.tightBreastSize >= 0 && this.breastVisibleSize > bra.tightBreastSize) {
                this.isBreastTight = true;
            }

            if (bra.maxBreastSize >= 0 && this.breastVisibleSize > bra.maxBreastSize) {
                this.breastVisibleSize = bra.maxBreastSize;
            }

            if (this.breastVisibleSize >= bra.looseBreastSize) {
                this.isBreastLoose = false;
            }

            this.breastVisibleSize += bra.breastPadding;
            if (this.breastVisibleSize < 0) {
                this.breastVisibleSize = 0;
            }
        });
    }

    public updateCrotchSizes (cloths : Array<Clothing>) {
        this.isCrotchTight = false;
        this.crotchVisibleSize = 0;
        let penis = <HumanoidPenis> Thing.PartRelation.getRightTypeOne(this, HumanoidPenis);
        let testicles = <HumanoidTesticles> Thing.PartRelation.getRightTypeOne(this, HumanoidTesticles);
        this.crotchVisibleSize = 0;
        if (penis != undefined) {
            this.crotchVisibleSize += penis.getBulgeSize();
        }
        if (testicles != undefined) {
            this.crotchVisibleSize += testicles.getBulgeSize();
        }

        this.crotchVisibleSize = Math.floor(this.crotchVisibleSize * 10) / 10;

        cloths.forEach((worn : Clothing) => {
            if (worn.tightCrotchSize >= 0 && this.crotchVisibleSize > worn.tightCrotchSize) {
                this.isCrotchTight = true;
            }

            if (worn.maxCrotchSize >= 0 && this.crotchVisibleSize > worn.maxCrotchSize) {
                this.crotchVisibleSize = worn.maxCrotchSize;
            }

            this.crotchVisibleSize += worn.crotchPadding;
            if (this.crotchVisibleSize < 0) {
                this.crotchVisibleSize = 0;
            }
        });
    }

    public updateButtSizes (cloths : Array<Clothing>) {
        this.isButtTight = false;
        let butt = <HumanoidButt> Thing.PartRelation.getRightTypeOne(this, HumanoidButt);
        this.buttVisibleSize = butt == undefined ? 0 : butt.getSize();

        cloths.forEach((worn : Clothing) => {
            if (worn.tightButtSize >= 0 && this.buttVisibleSize > worn.tightButtSize) {
                this.isButtTight = true;
            }

            if (worn.maxButtSize >= 0 && this.buttVisibleSize > worn.maxButtSize) {
                this.buttVisibleSize = worn.maxButtSize;
            }

            this.buttVisibleSize += worn.buttPadding;
            if (this.buttVisibleSize < 0) {
                this.buttVisibleSize = 0;
            }
        });
    }

    public hasBreasts () {
        let breasts = <HumanoidBreasts> this.getPart(HumanoidBreasts);
        if (breasts.size > 1) {
            return true;
        }
        return false;
    }

    public isMale () {
        return this.getParts(HumanoidPenis).length > 0 && this.getParts(HumanoidVagina).length == 0;
    }

    public isFemale () {
        return this.getParts(HumanoidPenis).length == 0 && this.getParts(HumanoidVagina).length > 0;
    }

    public isHerm () {
        return this.getParts(HumanoidPenis).length > 0 && this.getParts(HumanoidVagina).length > 0;
    }

    public getShortestDescription () {
        let playerGender = this.getGenderValue();
        let playerSluttiness = this.getSluttiness();

        let presentation = "";
        if (playerSluttiness.sluttinessCorrected > 75) {
            presentation += ("slutty, ");
        } else if (playerSluttiness.sluttinessCorrected > 35) {

        } else if (playerSluttiness.sluttinessCorrected > 20) {
            presentation += ("prude, ");
        } else {
            presentation += ("saintly, ");
        }

        if (playerGender.genderValueCorrected < 40) {
            presentation += ("masculine ");
        } else if (playerGender.genderValueCorrected < 60) {
            presentation += ("androgynous ");
        } else {
            presentation += ("feminine ");
        }

        if (playerGender.hasTits) {
            if (playerGender.hasPenis || playerGender.hasPenisBulge) {
                // Shemale Status (Tits + Penis)
                presentation += ("shemale");
            } else if (playerGender.hasVagina) {
                // is a woman
                presentation += ("woman");
            }
        } else {
            // No tits
            if (playerGender.hasPenis) {
                // Simply male
                if (playerGender.genderValueCorrected < 60) {
                    presentation += ("man");
                } else {
                    presentation += ("trap");
                }
            } else if (playerGender.hasPenisBulge) {
                // Possible Shemale status - has bulge and tits maybe get better terms sometime
                if (playerGender.genderValueCorrected < 60) {
                    presentation += ("man");
                } else {
                    presentation += ("trap");
                }
            } else if (playerGender.hasVagina) {
                // is a woman
                presentation += ("woman");
            }
        }

        return presentation;
    }

    public static SLOT_HAIR = 0;
    public static SLOT_HEADGEAR = 1;
    public static SLOT_FACE = 2;
    public static SLOT_EARS = 3;
    public static SLOT_EYES = 4;
    public static SLOT_NOSE = 5;
    public static SLOT_MOUTH = 6;
    public static SLOT_NECK = 7;
    public static SLOT_SHOULDERS = 8;
    public static SLOT_ARMS = 9;
    public static SLOT_HANDS = 10;
    public static SLOT_FINGERS = 11;
    public static SLOT_FINGERNAILS = 12;
    public static SLOT_UPPER_CHEST = 13;
    public static SLOT_MIDRIFF = 14;
    public static SLOT_WAIST = 15;
    public static SLOT_BACK = 16;
    public static SLOT_HIPS = 17;
    public static SLOT_CROTCH_FRONT = 18;
    public static SLOT_CROTCH_BACK = 19;
    public static SLOT_BUTT = 20;
    public static SLOT_LEG_UPPER = 21;
    public static SLOT_LEG_LOWER = 22;
    public static SLOT_FEET = 23;
    public static SLOT_FEET_NAILS = 24;
    public static SLOT_BREASTS = 25;
    public static SLOT_SLOT_COUNT = 26;

    public static cacheInvalidationActionRule = new Rule({
        name : "Invalidate humanoid caches",
        firstPriority : Rule.PRIORITY_LOWEST,
        code : (runner : RulebookRunner<Action>) => {
            (<Humanoid> runner.noun.actor).invalidateCaches();
        },
        conditions : (runner : RulebookRunner<Action>) => {
            return runner.noun.actor instanceof Humanoid;
        }
    });

    public static getPlayerDescription () : Say {
        let say = new Say();

        say.add("You are ");

        let player = <Humanoid> WorldState.player;

        let male = player.getParts(HumanoidPenis).length > 0;
        let female = player.getParts(HumanoidVagina).length > 0;

        if (male && female) {
            say.add( "hermaphrodite");
        } else if (male) {
            say.add("male");
        } else {
            say.add("female");
        }

        say.add(". You are presenting as ", new SayAn(), player.getShortestDescription() + ". ");

        say.add(Attributes.GenderIdentity.getDescription(player.getStat(Attributes.GenderIdentity)));

        //let gv = player.getGenderValue();
        let sv = player.getSluttiness();
        let bulges = player.getBulges();

        let coveredPerc = 1 - (player.uncoveredSlots.length / Humanoid.SLOT_SLOT_COUNT);

        say.add(" ", (<HumanoidSkin> player.getPart(HumanoidSkin)).getDescription());

        if (sv.halfNaked) {
            say.add(" You are almost naked. ");
        } else if (sv.naked) {
            say.add(" You are naked. ");
        } else if (coveredPerc < 0.2) {
            say.add("You are not showing much of it, though.");
        }

        say.add(Attributes.Degeneration.getDescription(player.getStat(Attributes.Degeneration)));

        say.add(Say.PARAGRAPH_BREAK);

        say.add((<HumanoidArms> player.getPart(HumanoidArms)).getDescription(), " ");
        say.add((<HumanoidHands> player.getPart(HumanoidHands)).getDescription(), " ");
        say.add((<HumanoidFeet> player.getPart(HumanoidFeet)).getDescription(), " ");

        let penis = <HumanoidPenis> player.getPart(HumanoidPenis);
        let vagina = <HumanoidVagina> player.getPart(HumanoidVagina);
        if (penis != undefined) {
            say.add("You have a ",
                penis.getSizeText(), (penis.isFlaccid() ? " flaccid " : " erect "),
                Say.COCK, " between your legs"
            );
            if (!penis.isBig() && penis.isGrower() && penis.isFlaccid()) {
                say.add(", which is okay, since you're a grower, not a shower");
            }
            if (penis.isUncovered()) {
                let oneOf = new OneOf(OneOf.PURELY_AT_RANDOM, ...[
                    ", it is not covered by any clothing",
                    ", it is not covered by any clothes",
                    ", it is uncovered",
                    ", it is visible to all",
                    ", it is hanging freely"
                ]);
                say.add(oneOf.getOne());
            }
            let testicles = <HumanoidTesticles> player.getPart(HumanoidTesticles);
            if (HumanoidPenis.getSizeText(bulges.crotch) != HumanoidPenis.getSizeText(penis.getBulgeSize() + testicles.getBulgeSize())) {
                say.add(", but it looks like it's actually ", HumanoidPenis.getSizeText(bulges.crotch), " due to your clothing")
            }
            say.add(". ");
            if (testicles != undefined) {
                // TODO: Describe testicles. Are we making these variable?
            }
        } else {
            say.add(vagina.getDescription());
        }

        //say.add(Say.PARAGRAPH_BREAK);

        say.add( " ");

        let breasts = <HumanoidBreasts> player.getPart(HumanoidBreasts);
        let butt = <HumanoidButt> player.getPart(HumanoidButt);

        say.add("You have ", breasts.getSizeText(), " breasts and a ", butt.getSizeText(), " butt.");


        if (bulges.breasts != breasts.getSize() && bulges.butt != butt.getSize()) {
            say.add(" Your clothing makes your breasts look like they're actually ", HumanoidBreasts.getSizeText(bulges.breasts),
                " and make your butt appear ", HumanoidButt.getSizeText(bulges.butt), ".");
        } else if (bulges.breasts != breasts.getSize()) {
            say.add(" Your clothing makes your breasts look like they're actually ", HumanoidBreasts.getSizeText(bulges.breasts), ".");
        } else if (bulges.butt != butt.getSize()) {
            say.add(" Your clothing makes your butt appear ", HumanoidButt.getSizeText(bulges.butt), ".");
        }

        say.add(Say.PARAGRAPH_BREAK);

        let clothingSluttiness = Clothing.getSluttinessValueOn(player);
        let bodypartSluttiness = Bodypart.getSluttinessValueOn(player);

        if (clothingSluttiness.weight > 0) {
            let clothingSluttinessFinal = clothingSluttiness.value / clothingSluttiness.weight;
            if (clothingSluttinessFinal > 75) {
                say.add("Your outfit can only be described as \"whore-ish\", it is far too nasty.")
            } else if (clothingSluttinessFinal > 60) {
                say.add("Your clothing are obviously making you look a bit naughty.")
            } else if (clothingSluttinessFinal < 30 && sv.sluttinessCorrected < 30) {
                say.add("Your clothing are definitely a bit prude.")
            } else if (clothingSluttinessFinal < 15 && sv.sluttinessCorrected < 30) {
                say.add("Your outfit is so prude that it could be worn by a saint.")
            }
        }

        say.add(" ");

        if (bodypartSluttiness.weight > 0) {
            let bodypartSluttinessFinal = bodypartSluttiness.value / bodypartSluttiness.weight;
            if (bodypartSluttinessFinal > 75) {
                say.add("The way your body is makes you look like you were made purely for sex");
                if (coveredPerc > 0.7) {
                    say.add(", thankfully it's mostly covered")
                } else if (coveredPerc < 0.2) {
                    say.add(", and you didn't even bother covering it up")
                }
                say.add(".");
            } else if (bodypartSluttinessFinal > 60) {
                say.add("Your body definitely has a lot of sex-appeal going on");
                if (coveredPerc > 0.7) {
                    say.add(", although it's covered")
                } else if (coveredPerc < 0.2) {
                    say.add(", proudly displayed for all to see")
                }
                say.add(".");
            } else if (bodypartSluttinessFinal < 30) {
                say.add("You have very little sex-appeal, maybe you are a kitchen table?");
                if (coveredPerc > 0.7) {
                    say.add(" At least you covered it up.")
                } else if (coveredPerc < 0.2) {
                    say.add(" Maybe you could cover it up a bit.")
                }
            }
        }

        say.add(Say.PARAGRAPH_BREAK);



        let stats = new SayLeftRight();

        stats.addLeft(new SayBold("Strength: "), Attributes.Strength.getDescription(player.getStat(Attributes.Strength)));
        stats.addLeft(Say.LINE_BREAK);
        stats.addLeft(new SayBold("Agility: "), Attributes.Agility.getDescription(player.getStat(Attributes.Agility)));
        stats.addLeft(Say.LINE_BREAK);
        stats.addLeft(new SayBold("Charm: "), Attributes.Charm.getDescription(player.getStat(Attributes.Charm)));
        stats.addLeft(Say.LINE_BREAK);
        stats.addLeft(new SayBold("Intelligence: "), Attributes.Intelligence.getDescription(player.getStat(Attributes.Intelligence)));

        Skill.getSkills().forEach((skill: Skill) => {
            if (player.getSkill(skill) > 0) {
                stats.addRight(new SayBold(skill.id + ": "), skill.getDescription(player.getSkill(skill)));
            }
        });

        say.add(stats);

        return say;
    }
}

ActionWear.carry.addRule(Humanoid.cacheInvalidationActionRule);
ActionRemove.carry.addRule(Humanoid.cacheInvalidationActionRule);