/* eslint-disable max-classes-per-file */
var MATTIE_ModManager = MATTIE_ModManager || {};
/**
 * @namespace MATTIE.itemAPI
 * @description The api for interacting with items
 */
MATTIE.itemAPI = MATTIE.itemAPI || {};

MATTIE.static = MATTIE.static || {};

const EffectCodes = {
	RECOVER_HP: 11,
	RECOVER_MP: 12,
	GAIN_TP: 13,
	ADD_STATE: 21,
	REMOVE_STATE: 22,
	ADD_BUFF: 31,
	ADD_DEBUFF: 32,
	REMOVE_BUFF: 33,
	REMOVE_DEBUFF: 34,
	SPECIAL: 41,
	GROW: 42,
	LEARN_SKILL: 43,
	COMMON_EVENT: 44,
	SPECIAL_ESCAPE: 0,
};

const ITEM_TYPES = {
	ITEM: 'Item',
	ARMOR: 'Armor',
	WEAPON: 'Weapon',

};

/**
 *
 * @param {EffectCodes} code what type of effect is this? refer to game action effects
 * @param {int} dataId the dataid, mostly used for status effects and common events
 * @param {int} scalar multiplicitive value?
 * @param {int} value addative value?
 */
DataManager.buildEffect = function (code, dataId, scalar, value) {
	const effect = {};
	effect.code = code;
	effect.dataId = dataId;
	effect.value1 = scalar;
	effect.value2 = value;
	return effect;
};

DataManager.buildCommonEventEffect = function (commonEventId) {
	return DataManager.buildEffect(EffectCodes.COMMON_EVENT, commonEventId, 0, 0);
};

/**
 * @description change the activation condition of an item
 * @param {*} x
 * 0: always
 * 1: in battle
 * 2: in menu
 * 3: never
 */
DataManager.changeActivationCondition = function (obj, x) {
	obj.occasion = x;
};

/**
 * @description call a callback when the item is consumed
 * @param {*} id the item id
 * @param {*} cb the function to call when the item is consumed
 */
DataManager.setCallbackOnItem = function (id, cb) {
	$dataItems[id].hasCallback = true;
	$dataItems[id].cb = cb;
};

/**
 * @description set the callback that should be called when this item is equipped
 * @param {*} id the item id
 * @param {*} cb the function to call when the item is equipped
 */
DataManager.setOnEquipCallback = function (id, cb) {
	$dataItems[id].hasCallback = true;
	$dataItems[id].equipCb = cb;
};

/**
 * @description set the callback that should be called when this item is unequipped
 * @param {*} id the item id
 * @param {*} cb the function to call when the item is un equipped
 */
DataManager.setOnUnequipCallback = function (id, cb) {
	$dataItems[id].hasCallback = true;
	$dataItems[id].unequipCb = cb;
};

/**
 * @description call a callback when the item is consumed
 * @param {*} id the item id
 * @param {*} cb the function to call when the item is consumed
 */
DataManager.setCallbackOnObj = function (obj, cb) {
	obj.hasCallback = true;
	obj.cb = cb;
};

/**
 * @description clear all effects of the item, basically make it do nothing when used
 * @param {*} id the item id
 */
DataManager.disableBaseItem = function (id) {
	$dataItems[id].disableBase = true; // this is my var to know if base is diabled
	$dataItems[id].effects = []; // this is what stops the item effects
	$dataItems[id].repeats = 0; // probs not needed
};

DataManager.clearEffects = function (obj) {
	obj.effect = [];
};

/**
 * @description add an effect to an item
 * @param {*} id the id of the item
 * @param {*} effect the effect to add
 */
DataManager.addItemEffect = function (id, effect) {
	$dataItems[id].effects.push(effect);
};

/**
 * @description add an effect to an item
 * @param {*} id the id of the item
 * @param {*} effect the effect to add
 */
DataManager.addEffect = function (obj, effect) {
	obj.effects.push(effect);
};
MATTIE.itemAPI.object = Game_Item.prototype.object;
Game_Item.prototype.object = function () {
	return MATTIE.itemAPI.object.call(this);
	// else return null;
};

MATTIE.itemAPI.setObject = Game_Item.prototype.setObject;
Game_Item.prototype.setObject = function (item) {
	MATTIE.itemAPI.setObject.call(this, item);
	if (item) {
		if (item.hasCallback) {
			this.setCb(item.cb);
			this.setEquipCb(item.equipCb);
			this.setUnequipCb(item.unequipCb);
		}
		this.disableBase = item.disableBase;
	}
};

/** @param {*} cb the callback to call */
Game_Item.prototype.setCb = function (cb) {
	this.cb = cb;
};

/**
 * @description set a callback to be called when this item is equipped
 * @param {function} cb a callback taking the Game_Actor object that has equipped this item
 * */
Game_Item.prototype.setEquipCb = function (cb) {
	this.equipCb = cb;
};

/**
 *  @description set a callback to be called when this item is unequipped
 * @param {function} cb a callback taking the Game_Actor object that has unequipped this item
 *  */
Game_Item.prototype.setUnequipCb = function (cb) {
	this.unequipCb = cb;
};

/** @description clear all callbacks on this item */
Game_Item.prototype.clearCbs = function () {
	this.cb = null;
	this.unequipCb = null;
	this.equipCb = null;
};

Game_Item.prototype.getCb = function () {
	return this.cb;
};

Game_Item.prototype.useItem = function () {
	if (this.cb) this.cb();
};

Game_Item.prototype.onEquip = function () {
	if (this.equipCb) this.equipCb();
};

/** @description the base chance equip method */
MATTIE_RPG.Game_Actor_changeEquip = Game_Actor.prototype.changeEquip;
/**
 * @description the overridden change equip method to also call out callbacks on equip/unequip
 * Updated to allow meta tags for callbacks ie <equipCb:console.log("I was equipped")>
 * @param {int} slotId
 * @param {rm.types.Item} item
 */
Game_Actor.prototype.changeEquip = function (slotId, item) {
	if (item) if (item.equipCb) item.equipCb(this);
	if (item) if (item.meta.equipCb) eval(item.meta.equipCb);
	const otherItem = this.equips()[slotId];
	console.log(otherItem);
	if (otherItem) { if (otherItem.unequipCb) otherItem.unequipCb(this); }
	if (otherItem) { if (otherItem.meta.unequipCb)eval(otherItem.meta.unequipCb); }
	if (item) if (item.meta.equipCb) eval(item.meta.equipCb);
	MATTIE_RPG.Game_Actor_changeEquip.call(this, slotId, item);
};

// this is called anytime any battler uses a action/skill/...etc...
MATTIE.itemAPI.apply = Game_Battler.prototype.useItem;
Game_Battler.prototype.useItem = function (item) {
	MATTIE.itemAPI.apply.call(this, item);
	if (item.cb) item.cb();
};

MATTIE.itemAPI.RunTimeItems = [];

/**
 *
 * @param {*} name name of the book
 * @param {*} desc description
 * @param {*} iconName the name of the icon in the system folder
 * @param {*} bookOpenText the text to display when the book is red
 * @param {*} spawn whether to spawn the book or not
 * @returns the book obj
 */
MATTIE.itemAPI.quickBook = function (name, desc, iconName, bookOpenText, spawn = true) {
	const book = new MATTIE.itemAPI.RunTimeItem();
	book.setIconIndex(new MATTIE.itemAPI.RuntimeIcon(iconName));
	book.setName(name);
	book.setDescription(desc);
	book.setItemType(2); // set book
	book.setCallback(() => {
		SceneManager.goto(Scene_Map);
		setTimeout(() => {
			MATTIE.fxAPI.showImage('book_base', 1, 0, 0);
			MATTIE.msgAPI.displayMsg(`<WordWrap>${bookOpenText}`, 1, 1);

			const int = setInterval(() => {
				if (!$gameMessage.isBusy()) {
					clearInterval(int);
					MATTIE.fxAPI.deleteImage(1);
				}
			}, 100);
		}, 1000);
	});
	if (spawn) { book.spawn(); }
	return book;
};

/**
 * @description a class that represents a single runtime item
 * @class
 */
MATTIE.itemAPI.RunTimeItem = class {
	/**
	 * @description build a new item
	 * @param {ITEM_TYPES} type the type of the item, default item
	 */
	constructor(type = ITEM_TYPES.ITEM) {
		/** @type {ITEM_TYPES} */
		this.type = type;

		this.cb = () => {};

		/**
         * @description the actual data item of this class
         * @type {rm.types.Armor}
         * */
		this._data = this.buildDefaultParams();
		this.setId();

		/** @description whether this item is spawned or not */
		this.isSpawned = false;
	}

	/**
	 * @description have the party gain this item and display the pickup message
	 * @param {int} x quantity to gain
	 * @param {boolean} display whether to display the pickup message or not
	 */
	gainThisItem(x = 1, display = true) {
		if (display) { MATTIE.msgAPI.displayMsg(`You find a \\c[2]${this._data.name}\\c[0]!`); }
		$gameParty.gainItem(this._data, x);
	}

	/**
	 * @description have the party lose this item
	 * @param {int} x quantity to lose
	 */
	loseThisItem(x = 1) {
		$gameParty.loseItem(this._data, x);
	}

	/**
     * @description create the default data item
     * @returns the default dataItem obj
     */
	buildDefaultParams() {
		let obj = {};
		if (this.type === ITEM_TYPES.ARMOR) {
			let i = 1;
			obj = $dataArmors[i];
			while (!obj) {
				obj = $dataArmors[++i];
			}
		}
		if (this.type === ITEM_TYPES.WEAPON) {
			let i = 1;
			obj = $dataWeapons[i];
			while (!obj) {
				obj = $dataWeapons[++i];
			}
		}
		obj = JsonEx.makeDeepCopy(obj);

		obj.name = 'generic name';
		obj.animationId = 0;
		obj.consumable = 0;
		obj.damage = 1;
		obj.description = 'A generic Item';
		obj.effects = [];
		obj.hitType = 0;
		obj.iconIndex = 0;
		obj.itypeId = 1;
		obj.atypeId = 4;
		obj.etypeId = 4;
		obj.meta = '';
		obj.note = '';
		obj.occasion = 0;
		obj.price = 0;
		obj.repeats = 0;
		obj.scope = 0;
		obj.speed = 1;
		obj.successRate = 100;
		obj.tpGain = 0;
		return obj;
	}

	/**
	 * @description set whether this item is consumable or not
	 * @param {bool} bool true if consumable false if not
	 */
	setConsumable(bool) {
		this._data.consumable = bool;
	}

	/** @description 1: normal, 2: book/key item */
	setItemType(type) {
		if (type > 0 && type <= 2) { this._data.itypeId = type; } // in range (0-2]
	}

	/**
	 * @description set the type of this item, refer to enum above for types
	 * @param {ITEM_TYPES} type
	 */
	setType(type) {
		this.type = type;
	}

	/**
	 * @description set the category of the armor
	 * 3: accessory
	 * 4: torso
	 * 5: shield
	 * 6: head
	 * @param {rm.types.Armor} atype
	 */
	setEquipType(atype) {
		this._data.etypeId = atype;
		this._data.atypeId = atype;
	}

	setIconIndex(index) {
		this._data.iconIndex = index;
	}

	setId() {
		switch (this.type) {
		case ITEM_TYPES.ITEM:
			this._data.id = $dataItems.length;
			this.typeString = '$dataItems';
			break;
		case ITEM_TYPES.ARMOR:
			this._data.id = $dataArmors.length;
			this.typeString = '$dataArmors';
			break;
		case ITEM_TYPES.WEAPON:
			this._data.id = $dataWeapons.length;
			this.typeString = '$dataWeapons';
			break;

		default:
			this._data.id = $dataItems.length;
			break;
		}
	}

	/**
     * @description set the name of this item
     * @param {String} name the new name
     */
	setName(name) {
		this._data.name = name;
	}

	/**
     * @description set the desc of this item
     * @param {String} desc the new desc
     */
	setDescription(desc) {
		this._data.description = desc;
	}

	/**
     * @description set a callback to occur when this item is crafted
     * @param {Function} cb the callback to be called
     */
	setCraftingCallback(cb) {
		const openingTag = '<Custom Synthesis Effect>';
		const closingTag = '</Custom Synthesis Effect>';
		this._data.craftingCb = cb;
		const script = `${this.typeString}[${this._data.id}].craftingCb()`;
		this._data.customSynthEval = script;
	}

	/**
     * @description add a recipe to the current item
     * @param {id[]} itemIds array of items needed for this recipe
     * @param {id}unlockItemId, the item that will unlock the recipe for this item
     */
	addRecipe(itemIds, unlockItemId) {
		const openingTag = '<Synthesis Ingredients>\n';
		const closingTag = '</Synthesis Ingredients>\n';
		const items = itemIds.map((itemId) => (`item ${itemId}`)).join('\n');
		const recipeString = openingTag + items + closingTag;

		const recipeUnlock = `\n<${this.type} Recipe: ${this._data.id}>\n`;
		$dataItems[unlockItemId].note += recipeUnlock;

		this.addRecipeUnlock(unlockItemId);
		DataManager.processISNotetags1([null, this._data], 0);

		this._data.note += recipeString;

		itemIds.map((itemId) => DataManager.addSynthesisIngredient(this._data, `item ${itemId}`));
	}

	addRecipeUnlock(unlockItemId) {
		const recipeUnlock = `\n<Item Recipe: ${this._data.id}>\n`;
		$dataItems[unlockItemId].note += recipeUnlock;
		DataManager.processISNotetags1([null, $dataItems[unlockItemId]], 0);
	}

	/** @description set a callback to be called when this item is used */
	setCallback(cb) {
		this.cb = cb;
		this._data.cb = cb;
	}

	/**
	 *  @description set a callback to be called when this item is equipped
	 *  @param {function} cb a callback taking the Game_Actor object that has equipped this item
	*/
	setEquipCallback(cb) {
		Game_Item.prototype.setEquipCb.call(this, cb);
		this._data.equipCb = cb;
	}

	/** @description set a callback to be called when this item is equipped */
	setUnequipCallback(cb) {
		Game_Item.prototype.setUnequipCb.call(this, cb);
		this._data.unequipCb = cb;
	}

	spawn() {
		this.spawned = true;
		MATTIE.itemAPI.RunTimeItems.push(this);
		switch (this.type) {
		case (ITEM_TYPES.ITEM):
			$dataItems[this._data.id] = this._data;
			DataManager.setCallbackOnItem(this._data.id, this.cb);
			break;
		case (ITEM_TYPES.ARMOR):
			$dataArmors[this._data.id] = this._data;
			break;
		case (ITEM_TYPES.WEAPON):
			$dataWeapons[this._data.id] = this._data;
			break;
		}

		if (DataManager.processItemCategoriesNotetags1) DataManager.processItemCategoriesNotetags1([null, this._data]); // termina compatability
	}
};

/**
 *
 * @param {string} charName the name of the character spritesheet within www/img/characters that this is a costume of (do not include .png)
 * @param {int} startingIndex the index within that spritesheet IE: what portion of the sprite sheet to use if it contains multiple chars
 * (0 if only one char in the sheet)
 * @param {string} costumeName the name of the item
 * @param {int} costumeIconId the id of the icon that this costume should have.
 * @param {int} unlockItem the id of the item that should unlock the crafting recipe for this costume
 * @param {int[]} craftingItems an array of the items needed to craft this item
 * @param {boolean} spawn should this spawn the item or just return it so that you can modify it farther before spawning
 */
MATTIE.itemAPI.createCostume = function (
	charName,
	startingIndex,
	costumeName,
	costumeIconId,
	unlockItem = MATTIE.static.items.unobtainable,
	craftingItems = MATTIE.static.items.unobtainable,
	spawn = true,
) {
	const costume = new MATTIE.itemAPI.RunTimeItem(ITEM_TYPES.ARMOR);

	costume.setEquipCallback((actor) => {
		/** @type {Game_Actor} */
		const actor2 = actor;
		actor2.setCharacterImage(charName, startingIndex);
		actor2.baseCharNameFunc = actor2.characterName;
		actor2.characterName = () => charName;
	});
	costume.setUnequipCallback((actor) => {
		actor.characterName = actor.baseCharNameFunc;
		actor.baseCharNameFunc = null;
		actor.setCharacterImage(actor.characterName(), startingIndex);
	});
	costume.setName(costumeName);
	costume.setIconIndex(costumeIconId);
	costume.addRecipe(craftingItems, unlockItem);
	if (spawn) { costume.spawn(); }
	return costume;
};

/**
 * @description a class that allows loading an item icon IE a 96x96 img from system outside of the default iconset.png
 */
MATTIE.itemAPI.RuntimeIcon = class {
	constructor(file) {
		/** @description the name of the file within www/img/system excluding .png */
		this.file = file;

		// reserve the image so it loads right away
		setTimeout(() => {
			ImageManager.reserveSystem(this.file);
			ImageManager.loadSystem(this.file);
		}, 5000);
	}
};

/** @description the base draw icon method */
MATTIE_RPG.Window_Base_drawIcon = Window_Base.prototype.drawIcon;
/**
 * @description draw icon method overridden to check if icon index is a runtime icon and load the runtime icon accordingly.
 * */
Window_Base.prototype.drawIcon = function (iconIndex, x, y) {
	if (iconIndex instanceof MATTIE.itemAPI.RuntimeIcon) {
		/** @type {MATTIE.itemAPI.RuntimeIcon} */
		const runtimeIcon = iconIndex;

		var bitmap = ImageManager.loadSystem(runtimeIcon.file);
		console.log(bitmap);
		var pw = Window_Base._iconWidth;
		var ph = Window_Base._iconHeight;
		this.contents.blt(bitmap, 0, 0, pw, ph, x, y);
	} else {
		MATTIE_RPG.Window_Base_drawIcon.call(this, iconIndex, x, y);
	}
};

// override actions to have callbacks
MATTIE_RPG.GameActionApply = Game_Action.prototype.apply;
Game_Action.prototype.apply = function (target) {
	MATTIE_RPG.GameActionApply.call(this, target);
	if (this.cb) this.cb();
};