/// <reference path="NPCAccessor.ts" />


const npcFieldBoundaries = {
    rel:{min:0,max:100}
}



/*
    NPC-Fields:
        date:
            date.count:
                n:  Number of dates one had with the NPC.
                    During a date, it indicates that it is the nth date
            date.kissed:
                n:  Number of times one has kissed the NPC.
            date.nextDateExpected:
                -1: does not expect a date and won't call asking for one
                    most commonly set to -1 because a date is already scheduled
                n:  will start calling at day n and ask for a date


		rel: Relationship Value / how much the NPC likes you
		familiarity:
			0 - stranger
			1000 - knows everything about each other

*/

class NPCsDict{

	static ageToAgeBracket(age:number):number{
		if(age < 1)
			return EAgeBracket.Newborn;
		if(age < 2)
			return EAgeBracket.Infant;
		if(age < 5)
			return EAgeBracket.Toddler;
		if(age < 13)
			return EAgeBracket.Kid;
		if(age < 18)
			return EAgeBracket.Teen;
		if(age < 30)
			return EAgeBracket.YoungAdult;
		if(age < 45)
			return EAgeBracket.Adult;
		if(age < 65)
			return EAgeBracket.MiddleAged;
		if(age < 80)
			return EAgeBracket.Old;
		return EAgeBracket.Ancient;
	}


    _dynamicData = {}

    _generatedIds = [];
    _generatedIdsCounter = 0;

    _boyfriends = [];

    dec(npcId,field,v,min=-Infinity,max=Infinity){
        this.inc(npcId,field,v * -1,min,max);
    }

	
    
	/**
	 * Creates a new NPC and returns his ID.
	 * @param {string} purpose the reason the NPC exists. If all purposes are removed, it is flagged for removal.
	 * @returns {string}
	 */
	create(purpose:string='static'):string{
        this._generatedIdsCounter += 1;
        let id = 'C'+this._generatedIdsCounter;
        this._generatedIds.push(id);
        this.set(id,'_purposes',[purpose]);
        return id;
    }

	createNPC(purpose:string='static'):NPC{
		return this.npc(this.create(purpose));
	}

	delete(npcID:string):void{
		delete this._dynamicData[npcID];
		this._generatedIds.delete(npcID);
	}

	generate(purpose:string,flags:NPCGenerateOptions):NPC{
		return setup.generateNPC(this,purpose,flags);
	}
	
    /**
	 * Get a value of a field for a NPC.
	 * 
	 * Will use dynamic data, if present. Otherwise, will use constant data. If constant data is not defined, will try passage data.
	 *
	 * @param {string} npcId
	 * @param {string} field
	 * @param {*} [def=undefined]
	 * @returns {*}
	 */
	get(npcId:string,field:string,def:any=undefined):any{
		let data = this.npcData(npcId);

		if(field in data)
			return data[field];
		if("passage" in data){
			let resultFromPassage = setup.func(data.passage,'vars',field);
			if(resultFromPassage)
				return resultFromPassage;
		}

        

        return def;
    }

    

    getDynamicData(npcId){
        if(npcId in this._dynamicData)
            return this._dynamicData[npcId];
        return {};
    }

    getStaticData(npcId){
        if(npcId in setup.npcs)
            return setup.npcs[npcId];
        return {};
    }

    inc(npcId,field,v:string|number,min=-Infinity,max=Infinity){
        let d:string|number = '';
        if(typeof v === "number")
            d = 0;
        let current = this.get(npcId,field,d);
        let newValue = current + v;
        
        newValue = Math.min(Math.max(current,max),newValue);
        newValue = Math.max(Math.min(current,min),newValue);

        this.set(npcId,field,newValue);
    }

    incBulk(npcIds,field,v,min=-Infinity,max=Infinity){
        for(let npcId of npcIds){
            this.inc(npcId,field,v,min,max);
        }
    }

    incBulkByFilter(filter={},field,v,min=-Infinity,max=Infinity){
        let npcsIds = this.ids(filter);
        this.incBulk(npcsIds,field,v,min,max);
    }

    ids(filter={}){
        let keysStatic = Object.keys(setup.npcs);
        let keysDynamic= Object.keys(this._dynamicData);
        let keys = keysStatic.concatUnique(keysDynamic);

        if(jQuery.isEmptyObject(filter))
            return keys;


        let keysMatchingFilter = [];

        for(let key of keys){
            if(this.npcFitsFilter(key,filter))
                keysMatchingFilter.push(key);
        }


        return keysMatchingFilter;
    }

    makeBoyfriend(npcId){
        if(this._boyfriends.includes(npcId)){
            console.warn('Already boyfriend:',npcId);
            return;
        }
        this.set(npcId,'keepAlive',true);
        let defaults = this.get(npcId,'defaults',[]);
        defaults.push('boyfriend');
        this.set(npcId,'defaults',defaults);
        this._boyfriends.push(npcId);

    }

    memoryUpdate(npcId){
        let rememberedFields = this.get(npcId,'memoryVars',[]);
		if(!rememberedFields.length)
			return;
		let now = State.variables.time.now;
		let memory = {time:now,vars:{}};
        for(let rememberedField of rememberedFields){
			memory.vars[rememberedField] = State.getVar(rememberedField);
        }
		this.set(npcId,'memory',memory);
		console.log('NPC memory updated',npcId,memory);
    }

	memoryUpdateAll(){
		const npcIds = this.ids();
		for(const npcId of npcIds){
			this.memoryUpdate(npcId);
		}
	}

	npc(npcId:string):NPC{
		return new Proxy(new NPCAccessor(npcId),NPCAccessor.handler);
	}

    npcData(npcId){
        let staticData = this.getStaticData(npcId);
        let dynamicData = this.getDynamicData(npcId);

        let data = Object.assign({},staticData,dynamicData);

        if("gender" in data)
            data = Object.assign({},setup.npcDefaults.gender[data.gender],data); //Apply the gender-defaults first.
        if("defaults" in data){
            for(let defaultId of data.defaults){
                let def = setup.npcDefaults[defaultId];
                data = Object.assign({},def,data);  //Apply the defaults first. They could be overwritten by static or dynamic data
            }
        }
        return data;
    }

	npcFitsFilter(npcId,filters:{[key:string]:any}={}){
		outerLoop:for (const [filterKey, filterValue] of Object.entries(filters)) {
			let npcValue = undefined;

			if(typeof filterValue === "object" && filterValue.filter){
				
				switch(filterValue.filter){
					case 'minMax':
						let min = filterValue.min ?? -Infinity;
						let max = filterValue.max ?? Infinity;
						npcValue = this.get(npcId,filterKey,0);
						if(npcValue > max || npcValue < min)
							return false;
						continue outerLoop;
				}
			}
			else if(typeof filterValue === "number")
				npcValue = this.get(npcId,filterKey,0);
			else
				npcValue = this.get(npcId,filterKey,'');
			if(npcValue != filterValue){
				return false;
			}
		}
		return true;
	}

	npcLikesAppearance(npcId):boolean{
		let appearanceDemand = this.get(npcId,'appearanceDemand',[null,null,5]);
		let hotcat = State.variables.pc.appearance;

		if(appearanceDemand[0] !== null && hotcat.geneticValue	< appearanceDemand[0])	return false;
		if(appearanceDemand[1] !== null && hotcat.lastingValue	< appearanceDemand[1])	return false;
		if(appearanceDemand[2] !== null && hotcat.currentValue	< appearanceDemand[2])	return false;
		
		let preferenceBalance = this.preferenceBalance(npcId);
		return (preferenceBalance >= 0);
		
	}
	
	/**
	 * Hof much the NPC likes / dislikes the PCs current appearance based on preferences.
	 * @date 8/30/2023 - 9:43:26 AM
	 *
	 * @param {string} npcId
	 * @returns {number}
	 */
	preferenceBalance(npcId):number{
		let preferenceValues = this.preferencesCompare(npcId);
		return Object.values(preferenceValues).reduce((a,x) => a+x, 0);
	}
    
	preferencesComment(npcId,positives=[],negatives=[],today=undefined){
		today ??= State.variables.time.daystart;

		if(positives === null)
			positives = [];
		else if(!Array.isArray(positives))
			positives = [positives];

		if(negatives === null)
			negatives = [];
		else if(!Array.isArray(negatives))
			negatives = [negatives];

		const positiveValue = today;
		const negativeValue = today * -1;
		let comments = this.get(npcId,'preferenceComments',{});
		positives.forEach(e => {comments[e] = positiveValue;});
		negatives.forEach(e => {comments[e] = negativeValue;});
		this.set(npcId,'preferenceComments',comments);
		console.log('NPC Preferences Comments updated',npcId,comments);

	}

	/**
     * Returns an object with all the preferenceIds of the requested NPC as keys and the days since they were commented on as values (negative if the comment was a negative one). The value is null for each preference which was never commented.
     * @date 8/21/2023 - 9:01:37 AM
     *
     * @param {*} npcId The Id of the NPC you want to get the result for.
     * @returns {{}}
     */
    preferenceCommentOffset(npcId,today=undefined){
    	today ??= State.variables.time.daystart;
		let result = {};
		let comments = this.get(npcId,'preferenceComments',{});
		let preferences = this.get(npcId,'preference');
        for (const preferenceId of Object.keys(preferences)) {
			if(!(preferenceId in comments)){
				result[preferenceId] = null;
				continue;	
			}
			// dayRaw is negative if the NPCs last comment on this issue was a negative one
			let dayRaw = comments[preferenceId];
			let day = Math.abs(dayRaw);
			let commentDirection = Math.sign(dayRaw);
			let dayOffset = today - day;
			result[preferenceId] = (dayOffset + 1) * commentDirection; //+1 so the value isn't 0 for today, which would make it impossible to distinguish between a positive and a negative comment
		}

		return result;
	}

	preferenceCommentWeighted(npcId:string):any{
		let preferenceCommentOffset = this.preferenceCommentOffset(npcId);
		let preferencesCompare = this.preferencesCompare(npcId);

		let preferences = {
			positive:{
				currentMatches: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v>0))
			},
			negative:{
				currentMatches: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v<0))
			}
		}

		for(let positive_negative of ['positive','negative']){
			preferences[positive_negative].switched = {};		// Priority 1: The NPC made a negative comment before and his opinion is now positive or vice versa.
			preferences[positive_negative].neverCommented = {};	// Priority 2: The NPC never commented this specific aspect.
			preferences[positive_negative].repeat = {};			// Priority 3: The NPC has the same opinion as before and states it again.

			for (const [preferenceId, value] of Object.entries(preferences[positive_negative].currentMatches)) {
				// Value is the number that represents how negative or positive the opinion is
				if(typeof value != "number")
					continue;
				let opinionMagnitude = Math.abs(value);
				let opinionIsPositive = Math.sign(value);	//-1: Negative, 1: Positive

				let commentOffsetRaw = preferenceCommentOffset[preferenceId];
				if(commentOffsetRaw === null)
					preferences[positive_negative].neverCommented[preferenceId] = opinionMagnitude;
				else{
					let daysSinceLastCommend = Math.abs(commentOffsetRaw);
					let lastCommentWasPositive = Math.sign(commentOffsetRaw);
					if(opinionIsPositive != lastCommentWasPositive)
						preferences[positive_negative].switched[preferenceId] = opinionMagnitude * daysSinceLastCommend;
					else
						preferences[positive_negative].repeat[preferenceId] = opinionMagnitude * daysSinceLastCommend;
				}
			}
		}

		return preferences;

		/*let preferences = {
							positive: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v>0)),
							negative: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v<0))
		}

		let switchedTo = {positive:{},negative:{}};
		let neverCommentedOn = {positive:{},negative:{}};
		let other = {positive:{},negative:{}};

		for(let pn of ['positive','negative']){
			for (const [preferenceId, value] of Object.entries(preferences[pn])) {
				let commentOffset = preferenceCommentOffset[preferenceId];
				if(commentOffset === null)
					neverCommentedOn[pn][preferenceId] = value;
				else{
					let daysSinceComment = Math.abs(commentOffset);

					if(Math.sign(commentOffset) == Math.sign(value))
						switchedTo[pn][preferenceId] = value * daysSinceComment;
					else{
						other[pn][preferenceId] = value * daysSinceComment;
					}
				}
			}
		}*/


		//return [preferencesCompare,preferencesPositive,preferencesNegative];
	}

    preferencesToCommentNow(npcId){
        /*let preferenceCommentWeighted = this.preferenceCommentWeighted(npcId);
        let positive = null;
        let negative = null;
		let positiveResult = null;
		let negativeResult = null;
		let positiveResultType = null;
		let negativeResultType = null;
		if(Object.keys(preferenceCommentWeighted.positive.neverCommented).length > 0){
			positive = preferenceCommentWeighted.positive.neverCommented;
			positiveResultType = 'n';
		}else if(Object.keys(preferenceCommentWeighted.positive.switched).length > 0){
			positive = preferenceCommentWeighted.positive.switched;
			positiveResultType = 's';
		}else if(Object.keys(preferenceCommentWeighted.positive.repeat).length > 0){
			positive = preferenceCommentWeighted.positive.repeat;
			positiveResultType = 'r';
		}
		if(Object.keys(preferenceCommentWeighted.negative.switched).length > 0){	// Switch has a higher priority than uncommented for negative. It's the opposite fpr positive
			negative = preferenceCommentWeighted.negative.switched;
			negativeResultType = 's';
		}else if(Object.keys(preferenceCommentWeighted.negative.neverCommented).length > 0){
			negative = preferenceCommentWeighted.negative.neverCommented;
			negativeResultType = 'n';
		}else if(Object.keys(preferenceCommentWeighted.negative.repeat).length > 0){
			negative = preferenceCommentWeighted.negative.repeat;
			negativeResultType = 'r';
		}
		// https://stackoverflow.com/a/1069840
		if(positive){
			const positiveSorted = Object.entries(positive)
				.sort(([,a],[,b]) => b-a);
			positiveResult = positiveSorted.first()[0];
		}
		if(negative){
			const negativeSorted = Object.entries(negative)
				.sort(([,a],[,b]) => b-a);
			negativeResult = negativeSorted.first()[0];
		}

		let balance = Object.values(preferenceCommentWeighted.positive.currentMatches).reduce((a, b) => {return a + b;},0)
					+ Object.values(preferenceCommentWeighted.negative.currentMatches).reduce((a, b) => {return a + b;},0);

		return {
			positive: positiveResult,
			positiveResultType: positiveResultType,
			negative: negativeResult,
			negativeResultType:negativeResultType,
			balance: balance
		}*/
		//TODO
    }

	
    /**
	 * Checks Preferences of a NPC and returns an object with key: preference id, value: magnitued of success (positive) or failure (negative).
	 * @date 8/30/2023 - 7:20:46 AM
	 *
	 * @example
	 *  //Returns {"short_skirt_d": 1} 
	 * @param {*} npcId
	 * @returns {{}}
	 */
	preferencesCompare(npcId):{[key: string]:number}{
		
        /*let result = {};
		let preferences = this.get(npcId,'preference');
		for (const [preferenceId, value] of Object.entries(preferences)) {
			let preferenceData = setup.npcPreferences[preferenceId];
			if(!preferenceData){
				console.warn('Preference not found:',preferenceId);
				continue;
			}

			let valueSucceeded = value;
			let valueFailed = -value;
			let valueNeutral = 0;
			if(Array.isArray(value)){
				valueSucceeded = value[0];
				valueFailed = value[1];
			}

			if(!preferenceData.s)
				result[preferenceId] = valueSucceeded;
			else if(Scripting.evalTwineScript(preferenceData.s))
				result[preferenceId] = valueSucceeded;
			else if(preferenceData.f && Scripting.evalTwineScript(preferenceData.f))
				result[preferenceId] = valueFailed;
			else
				result[preferenceId] = valueNeutral;
        }
		return result;*/
		//TODO
		return {};
    }

	prerencesKnown(npcId){
		let result = {};
		let preference = this.get(npcId,'preference',{});
		let preferenceComments = this.get(npcId,'preferenceComments',{});
		for(var preferenceKey of Object.keys(preferenceComments)){
			result[preferenceKey] = preference[preferenceKey];
		}
		return result;
	}

    /*pronoun(npcId,pronoun){
        let gender = this.get(npcId,'gender',0);
        if(pronoun in pronounDict)
            return pronounDict[pronoun][gender];
        return 'GENDER KEY MISSING: '+pronoun+'['+gender+']';
    }*/

	rel(npcId){
		this.relUpdate(npcId);
		return this.get(npcId,'rel');
	}
	
	/**
	 * Increases the relationships of a npc by inc. Diminishing return applied.
	 * @deprecated
	 * @param {string} npcId The ID of the NPC
	 * @param {number} inc The amount by which to increase. Negative numbers decrease the relationship
     * @param {Array}  boundaries Won't increase/decrease past these values. Format: [min,max]
     * @param {number} today Daystart of the current day. Used for diminishing return algorithm.
	 */
	relInc(npcId,inc,boundaries=[0,100],today=undefined){
		console.warn('Use of $npcs.relInc is deprecated. Use <<relationship>> instead');
		let relGrace = 2; //The change that won't be diminished

        let boundaryLower = boundaries[0];
        let boundaryUpper = boundaries[1];

		today ??= State.variables.time.daystart;

		let rel_current = this.get(npcId,'rel',50);

		if(!inc)
			return;

		if(inc > 0 && rel_current >= boundaryUpper)
			return;

		if(inc < 0 && rel_current <= boundaryLower)
			return;
		

		let rel_date = this.get(npcId,'rel_date',undefined);
		let rel_base = undefined;
		let rel_change_raw = 0;
		if(!rel_date || rel_date != today){
			this.set(npcId,'rel_date',today);
			this.set(npcId,'rel_base',rel_current);
			rel_base = rel_current;
		}else{
			rel_base = this.get(npcId,'rel_base');
			rel_change_raw = this.get(npcId,'rel_change_raw');
		}

		rel_change_raw += inc;
		this.set(npcId,'rel_change_raw',rel_change_raw);

		let rel_new = 0;//TODO: setup.diminishingReturn(rel_base,rel_base + rel_change_raw,relGrace,'sqrt');

        if(inc > 0)
            rel_new = Math.min(rel_new,boundaryUpper);
		else if(inc < 0)
			rel_new = Math.max(rel_new,boundaryLower);

		this.set(npcId,'rel',rel_new);

	}

	relUpdate(npcId){
		const time = State.variables.time;
		const dayStart = time.daystart;
		const lastRelUpdate = this.get(npcId,'relu',-1);

		let currentRel = this.get(npcId,'rel');
		let lastingRel = this.get(npcId,'rell',currentRel);

		if(lastRelUpdate == -1){
			this.set(npcId,'rell',lastingRel);
			this.set(npcId,'relu',dayStart);
			return;
		}

		if(dayStart <= lastRelUpdate)
			return;
		

		for(let updateDay = lastRelUpdate; updateDay < dayStart; updateDay++){
			let relDifference = currentRel - lastingRel;
			let changeInLastingRel = Math.ceilAbs(relDifference / 10);
			lastingRel += changeInLastingRel;

			relDifference = lastingRel - currentRel;//note opposite order
			let changeInCurrentRel = Math.ceilAbs(relDifference / 4);
			currentRel += changeInCurrentRel;
		}
		this.set(npcId,'rel',currentRel);
		this.set(npcId,'rell',lastingRel);
		this.set(npcId,'relu',dayStart);
	}




    set(npcId,field,v){
        if(!(npcId in this._dynamicData))
            this._dynamicData[npcId] = {};

        if(field in npcFieldBoundaries){
            let fieldRules = npcFieldBoundaries[field];
            if("min" in fieldRules)
                v = Math.max(v, fieldRules.min);
            if("max" in fieldRules)
                v = Math.min(v, fieldRules.max);
        }

        
        this._dynamicData[npcId][field] = v;
        


        console.log("NPC value set",npcId,field,v);
    }

    //npcIds: Array of ids
    setBulk(npcIds,field,v){
        for(let npcId of npcIds){
            this.set(npcId,field,v);
        }
    }

    setBulkByFilter(filter={},field,v){
        let npcsIds = this.ids(filter);
        this.setBulk(npcsIds,field,v);
    }


    // ----- Dating -----
    _datingNPCs = [];
    get datingNPCs(){return this._datingNPCs;}

    // Only sets the NPC to dating. Does not make him call you at any point. Use <<datingStart>> for this.
    datingStart(npcID){
        console.log("Start Dating:",npcID);
        this._datingNPCs.push(npcID);
        this.set(npcID,'dating',true);
    }

    //#region Activity

		activity(npcId:string):NPCActivity{
			let activityInformationCached:ExpiringVar<NPCActivity>|null = this.get(npcId,'activity',null);

			if(!State.variables.time.isExpired(activityInformationCached))
				return activityInformationCached.val;

			return this.#updateActivity(npcId).val;
		}

		/**
		 * Returns whether the given NPC is at the given activity.
		 *
		 * @param {string} npcId
		 * @param {string|string[]} activity
		 * @param {string} [location=undefined] 
		 * @returns {boolean}
		 */
		atActivity(npcId:string,activity:string|string[],location=undefined){
			const locationInformation = this.activity(npcId);
			if(!locationInformation.activity)
				return false;
			if(location && locationInformation.location != location)
				return false;
			if(Array.isArray(activity))
				return locationInformation.activity.includesAll(activity);
			return locationInformation.activity.includes(activity);
		}

		
		/**
		 * Ends the current activity and starts the next one.
		 * @param {string} npcId
		 * @param {string} [activity=undefined]
		 * @returns {NPCActivity}
		 */
		endActivity(npcId:string,activity:string|string[]=undefined):NPCActivity{
			if(activity && !this.atActivity(npcId,activity))
				return this.activity(npcId);

			let activityInformationCached:ExpiringVar<NPCActivity>|null = this.get(npcId,'activity',null);
			
			if(!activityInformationCached)
				return this.#updateActivity(npcId).val;

			const expirationOfCurrentActivity = activityInformationCached.til;
			return (this.#updateActivity(npcId,expirationOfCurrentActivity)).val;

			
			
			/*const timeWhenNextActivityStarts = new Date(currentActivity.end.getTime()+1);
			this._updateLocation(npcId,timeWhenNextActivityStarts);
			return this.location(npcId);*/
		}

		
		/**
		 * @deprecated
		 * @param {*} npcId
		 * @returns {NPCActivity}
		 */
		location(npcId){
			return this.activity(npcId);
		}

		locationGroup(npcIds:string[]):{[locationId:string]:{[npcId:string]:NPCActivity}}{
			let locations:{[key:string]:any} = {};
			for(let npcId of npcIds){
				let activity = this.activity(npcId);
				locations[activity.location] ??= {};
				locations[activity.location][npcId] = activity;
			}
			return locations;
		}

		#updateActivity(npcId,dateTime:Date=undefined):ExpiringVar<NPCActivity>{
			let newActivity:NPCActivity = {location:'',activity:[]};
			let newExpiration = new Date(0);

			const time = State.variables.time;
			dateTime ??= time.now;

			const timeTable:Timetable<NPCActivity> = this.get(npcId,'timetable',null);

			const twoResultsFromTimeTable = time.timetableLookupMultiple(timeTable,dateTime,2);
			const currentActivity = twoResultsFromTimeTable[0];
			const nextActivity = twoResultsFromTimeTable[1];

			let result = {
				til: nextActivity.beginDate,
				val: currentActivity.val
			};

			this.set(npcId,'activity',result);

			return result;
		}

	//#endregion

	//#region System
		constructor(){}

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

			return this;
		}

		clone = function () {
			return (new NPCsDict())._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 NPCsDict())._init($ReviveData$)', ownData);
		};
	//#endregion
}

window.NPCsDict = NPCsDict;
