setup.npcs = {};

const enum EDickSize{
	CLITLIKE=-3,
	TINY=-2,
	SMALL=-1,
	AVERAGE=0,
	BIG=1,
	MASSIVE=2,
	MONSTROUS=3
}

const enum EGender{
	UNKNOWN = -1,
	MALE = 0,
	FEMALE = 1
}


let pronounDictLowerCase = {
    'he': ['he','she'],
    'him': ['him','her'],
    'his': ['his','her'],
	'himself': ['himself','herself'],
}

let pronounDictUpperFirst = {};
let pronounDictUpperCase = {};

for (const [key, value] of Object.entries(pronounDictLowerCase)) {
    pronounDictUpperFirst[key.toUpperFirst()] = [value[0].toUpperFirst(),value[1].toUpperFirst()];
    pronounDictUpperCase[key.toUpperCase()] = [value[0].toUpperCase(),value[1].toUpperCase()];
}

const pronounDict = Object.assign({},pronounDictLowerCase,pronounDictUpperFirst,pronounDictUpperCase);

/**
 * A NPC-object is an interface to access and store information regarding a NPC.
 * 
 * Don't rely on it or any of its fields to be stored!
 * 
 * To get a NPC-object for a NPC, use `State.variables.npc(npcId)` (in JS and TS) or `$npc(npcId)` (in Twine).
 */
class NPCAccessor{

	_npcID: string;

	
	get id(){return this._npcID;}

	/**
	 * @static
	 * @readonly
	 * @type {NPCsDict}
	 */
	static get DICT(){ return State.variables.npcs;}

	static _getHandler = function(target, prop){
		if(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop)?.get){
			return target[prop];
		}
		if(typeof target[prop] === 'function')
			return new Proxy(target[prop], {
				apply: (functionName, thisArg, argumentsList) =>  Reflect.apply(functionName, target, argumentsList)
			});
			
		return target.get(prop);
	}

	static handler = {
		get(target, prop) {
			if(prop.endsWith('_possessive')){
				prop = prop.replace('_possessive','');
				let value = NPCAccessor._getHandler(target,prop);
				if(value.endsWith('s'))
					return value+"'"; //https://dictionary.cambridge.org/grammar/british-grammar/possession-john-s-car-a-friend-of-mine
				return value + "'s";
			}
			return NPCAccessor._getHandler(target,prop);
			
		},
		set(target, prop, value, receiver){
			//console.warn(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop));
			if(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop)?.set){
				target[prop] = value;
				return true;
			}
			target.set(prop,value);
			return true;
		}
	};

	//#region Activity

		get activity():NPCActivity{
			return NPCAccessor.DICT.activity(this._npcID);
		}

		atActivity(activity:string|string[],location=undefined){
			return NPCAccessor.DICT.atActivity(this._npcID,activity,location);
		}

		endActivity(activity:string|string[]=undefined){
			console.warn('endActivity',this);
			return NPCAccessor.DICT.endActivity(this._npcID,activity);
		}

	//#endregion

	//#region Appearance
		get desc():string{
			return setup.npcDescription(this.id); // We can't use this, because this is a NPCAcessor and npcDescription needs a NPC.
		}
	//#endregion

	/**
	 * Returns the age of a character in years.
	 */
	get age():number{
        let dayOfBirth = this.get('dob');
        let day = dayOfBirth % 100;
        let month = Math.floor((dayOfBirth % 10000)/100);
        let year = Math.floor(dayOfBirth / 10000);
        return State.variables.time.ageOfDate(day,month,year);
	}
	/**
	 * Set the birthday of the characters to a random day that has the given age.
	 * 
	 * Directly set `birthday` if you want a specific day instead.
	 */
	set age(v){
		let birthday = State.variables.time.dateOfAge(v);
		this.birthday = birthday;
	}

	get ageBracket():EAgeBracket{
		return NPCsDict.ageToAgeBracket(this.age);
	}

	get appearanceDemand(){return this.get('ad');}

	/**
	 * Returns the birtday of the character as a `Date`-object with UTC-time set to 00:00:00	 */
	get birthday():Date{
		let dayOfBirth = this.get('dob');
		let day = dayOfBirth % 100;
		let month = Math.floor((dayOfBirth % 10000)/100);
		let year = Math.floor(dayOfBirth / 10000);
		return new Date(Date.UTC(year,month-1,day));
	}

	/**
	 * Sets the birthday of the character. Anything but year, month and day is ingored.
	 */
	set birthday(v:Date){
		let dob = v.getUTCFullYear()*10000 + (v.getUTCMonth()+1)*100 + v.getUTCDate();
		this.set('dob',dob);
	}

	get dick_girth(){
		let thdick = this.get('thdick');
		switch(thdick){
			case 'skinny':
				return 0;
			case 'slim':
				return 5;
			case 'well proportioned':
				return 10;
			case 'thicker than average':
				return 15;
			case 'thick':
				return 20;
			case 'massive':
				return 25;
			case 'monstrous':
				return 30;
			default :
				return 0;
		}
	}
	get dick_size(){ //Average: 3
		let ds_dick = this.get('dick',15);
		let ds_dick_girth = this.dick_girth;
		let ds_average = (ds_dick + ds_dick_girth) / 2;
		return Math.max(Math.ceil(ds_average / 5),0);
	}

	get fam(){
		return this.get('fam',0);
	}
	set fam(v){
		this.set('fam',v);
	}

	/**
	 * The full name, as it would appear on legal documents.
	 * 
	 * If you want to change it, you need to set `firstname` and/or `lastname`.
	 */
	get fullname(){return this.get('firstname')+' '+this.get('lastname');}


	get appearanceMemoryDifference(){
		return this.appearanceMemoryDifferenceByPC(State.variables.pc,State.variables.time.now);
	}

	appearanceMemoryDifferenceByPC(pc:PlayerCharacter,time:Date):{[key:string]:AppearanceChange}{
		const lastCommentTime:Date = this.get('appearanceCommentTime',State.variables.time.now);
		return pc.appearanceHistoryFiltered(lastCommentTime);
	}
	

	get preferences():{[preferenceKey:string]:NPCPreference&NPCPreferenceDefinition}{
		let result:{[preferenceKey:string]:NPCPreference&NPCPreferenceDefinition} = {};

		const preferences:{[preferenceKey:string]:NPCPreference} = this.get('preference',{});

		for(const [preferenceKey,preferenceData] of Object.entries(preferences)){
			const constData = setup.getNPCPreference(preferenceKey);
			result[preferenceKey] = Object.assign({},constData,preferenceData);
		}

		return result;
	}

	preferencesMatched(pc:PlayerCharacter):{[preferenceKey:string]:NPCPreference}{
		let result:{[preferenceKey:string]:NPCPreference} = {};

		for(const [preferenceKey,preferenceData] of Object.entries(this.preferences)){
			if(preferenceData.matches(pc))
				result[preferenceKey] = preferenceData;
		}

		return result;
	}

	preferenceCommentRegister(preferenceId:string){
		
	}

	get purposes():string[]{
		return this.get('_purposes',['static']);
	}

	set purposes(v:string[]){
		this.set('_purposes',v);
	}

	
	/**
	 * Returns -1 if the requested purpose wasn't found, 0 if the purpose was the last one and the NPC has been flagged for deletion and otherwise the number of remaining purposes.
	 * @param {string} purpose
	 * @returns {number}
	 */
	purposeRemove(purpose:string):number{
		let currentPurposes = this.purposes;
		if(!currentPurposes.includes(purpose))
			return -1;
		currentPurposes.delete(purpose);
		this.purposes = currentPurposes;
		if(currentPurposes.length)
			return currentPurposes.length;
		this.delete();
		return 0;
	}

	get title(){
		switch (this.get('gender')) {
			case 0:
				return 'Mr.'
			case 1:
				return 'Mrs.'
			default:
				return 'Mrx.'
		}
	}
	get titlename(){return `${this.title} ${this.get('lastname')}`;}
	get usedname(){
		const presetUsedName = this.get('usedname');
		if(presetUsedName)
			return presetUsedName;

		switch (this.get('nameScheme')) {
			case 'title_lastname':
				return this.titlename;
			case 'firstname':
				return this.get('firstname');
			default:
				return this.get('nickname') || this.get('firstname');
		}
	}

	w(wordIndex:string):string{
		if(pronounDict[wordIndex])
			return pronounDict[wordIndex][this.get('gender',0)];
		console.error('Word missing in NPC.w():',wordIndex);
		return wordIndex;
	}

    
	delete(){
		NPCAccessor.DICT.delete(this._npcID);
	}


	get(key:string,def:any=undefined){
		return NPCAccessor.DICT.get(this._npcID,key,def);
	}


	set(key:string,v:any){
		return NPCAccessor.DICT.set(this._npcID,key,v);
	}

	constructor(npcId=undefined){
		if(npcId)
			this._npcID = npcId;
	}


    _init(nPCsDict){
        Object.keys(nPCsDict).forEach(function (pn) {
            this[pn] = clone(nPCsDict[pn]);
        }, this);

        return this;
    }

    clone = function () {
        return (new setup.NPCAccessor())._init(this);
    };

    toJSON = function () {
        var ownData = {};
        Object.keys(this).forEach(function (pn) {
            if(typeof this[pn] !== "function")
                ownData[pn] = clone(this[pn]);
        }, this);
        return JSON.reviveWrapper('(new setup.NPCAccessor())._init($ReviveData$)', ownData);
    };
}


setup.NPCAccessor = NPCAccessor;