/*
 * last modified---
 * 	12-26-23 prioritize greylisted above unconfirmed in isAvailable()
 * 	12-12-23 move PayeeConfig and SpendPayee to separate file
 * 	11-16-23 make emitEnftJSON() include empty memo lines, for EIP712 sigs
 * 	11-03-23 make isAvailable() check balanceOf() > 0 also
 * 	10-23-23 debug isAvailable(); add this.unavailReason
 * 	08-30-23 flesh out validateSig(), isAvailable(); add this.avail
 * 	08-28-23 add buildSignedData(), emitEnftJSON()
 * 	08-25-23 add isENFTselected()
 * 	05-24-23 relocate from Enshroud.jsx
 *
 * purpose---
 * 	provide classes to represent an Enft
 *
 * notes---
 * 	these 3 classes are defined together because PayeeConfigS maintain a Map
 * 	of selected eNFTs
 */

import Web3 from 'web3';

// class to represent a single eNFT
class Enft {
	constructor() {
		this.id = '';
		this.schema = '';
		this.owner = '';
		this.asset = '';
		this.amount = 0;		// a BigInt represented as a number string
		this.generation = 1;
		this.rand = '';
		this.memo = '';
		this.signer = '';
		this.signature = '';
		this.valid = false;
		this.avail = false;
		this.unavailReason = '';
		this.AESkey = '';
	}

	/* configure mandatory fields
	 * @param id the unique ID assigned to the eNFT (zero-padded 64-bit hex)
	 * @param schema the revision of the metadata schema
	 * @param owner the account address to which the eNFT was minted
	 * @param asset the token contract address
	 * @param amount the amount of the eNFT (in wei)
	 * @param gen the generation (circulation distance since value deposited)
	 * @param rand the random salt value
	 * @param signer the ID of the MVO that generated and signed the eNFT
	 * @param sig the MVO's signature
	 * @param aeskey the AES-256 key which decrypts the eNFT on the event log
	 */
	config(id, schema, owner, asset, amount, gen, rand, signer, sig, aeskey) {
		this.id = id;
		this.schema = schema;
		this.owner = owner;
		this.asset = asset;
		this.amount = amount;
		this.generation = gen;
		this.signer = signer;
		this.signature = sig;
		this.AESkey = aeskey;
		if (rand !== undefined && rand !== null) {
			this.rand = rand;
		}
		else {
			this.rand = '';
		}
	}

	/* add optional memo field
	 * @param memo the memo line (payee message) to add
	 */
	addMemo(memo) {
		if (memo !== undefined && memo !== null) {
			this.memo = memo;
		}
	}

	/* add optional expiration field
	 * @param exp expiry timestamp, after which eNFT is no longer usable
	 */
	addExpiration(exp) {
		if (exp !== undefined && exp !== null && exp !== "") {
			this.expiration = exp;
		}
	}

	/* add optional cost field
	 * @param cost factor to be added (e.g. agio percentage)
	 */
	addCost(cost) {
		if (cost !== undefined && cost !== null && cost !== "") {
			this.cost = cost;
		}
	}

	/* add optional growth field
	 * @param growth the growth factor to be added (e.g. # basis points)
	 */
	addGrowth(growth) {
		if (growth !== undefined && growth !== null && growth !== "") {
			this.growth = growth;
		}
	}

	/* build data to be signed for this eNFT
	 * @return the JSON value of the Enft object
	 */
	buildSignedData() {
		// must be emitted in same order as MVO's eNFTmetadata.buildSignedData()
		var sigData = `"id":"${this.id}",`;
		sigData += `"schema":"${this.schema}",`;
		sigData += `"owner":"${this.owner}",`
		sigData += `"asset":"${this.asset}",`;
		sigData += `"amount":"${this.amount}",`;
		if (this.rand !== "") {
			sigData += `"rand":"${this.rand}",`;
		}
		sigData += `"generation":"${this.generation}",`;
		if (this.expiration !== undefined && this.expiration !== "")
		{
			sigData += `"expiration":"${this.expiration}",`;
		}
		if (this.growth !== undefined && this.growth !== "") {
			sigData += `"growth":"${this.growth}",`;
		}
		if (this.cost !== undefined && this.cost !== "") {
			sigData += `"cost":"${this.cost}",`;
		}
		if (this.memo !== "") {
			sigData += `"memo":"${this.memo}",`;
		}
		//console.debug("sigData on " + this.id + " is: " + sigData);
		return sigData;
	}

	/* validate signature (bool)
	 * @param chainConn the chain's configuration (a ChainConnection)
	 * @param web3 the Web3 object (will be fetched if passed as null)
	 * @return true iff valid
	 */
	validateSig(chainConn,
				web3 = new Web3(Web3.givenProvider || "ws://localhost:8545"))
	{
		const sigData = this.buildSignedData();
		if (sigData === "") {
			return false;
		}
		if (chainConn == null) {
			console.error("validateSig() on Id " + this.id + " without a "
						+ "ChainConnection");
			return false;
		}

		// obtain signature address of this.signer
		const mvoConf = chainConn.MVOConf;
		const mvoStaking = mvoConf.getMVOStaking(this.signer);
		if (mvoStaking === undefined) {
			console.error("validateSig() on Id " + this.id + " but no MVO "
						+ "staking available for signer " + this.signer);
			return false;
		}
		const signingAddr = mvoStaking.signingAddress;

		// confirm sig was made by signer
		const dataHash = web3.eth.accounts.hashMessage(sigData);
		const sig = "0x" + this.signature;
		const acct = web3.eth.accounts.recover(dataHash, sig, true);
		this.valid = acct === signingAddr;
		return this.valid;
	}

	// dump out the original eNFT data including signer and signature
	emitEnftJSON() {
		var jsonData = '{"enshrouded":{';
		jsonData += this.buildSignedData();
		// if memo field empty, include it anyway so that EIP712 sigs work right
		if (this.memo === '') {
			jsonData += '"memo":"",';
		}
		jsonData
			+= `"signer":"${this.signer}","signature":"${this.signature}"}}`;
		return jsonData;
	}

	/* validate availability (minted / not burned, confirmed by at least
	 * EnshroudProtocol.confirmationBlocks blocks, plus not greylisted)
	 * @param web3 the Web3 object for making calls
	 * @param EnshroudProtocol contract address (contracts["EnshroudProtocol"])
	 * @return true iff eNFT is listed on-chain as eligible for spending
	 */
	async isAvailable(web3, contract) {
		if (web3 === undefined || contract === undefined) {
			console.error("isAvailable() missing input");
			return false;
		}

		// make sure ID has been minted and is not burned
		const idAsUint256 = "0x" + this.id;
		const owner = this.owner;
		var enftBal = 0;
		// balanceOf[owner][id] is 1 for minted, 0 for burned (or never minted)
		await contract.methods.balanceOf(owner, idAsUint256)
			.call({ from: owner })
			.then(balance => {
				enftBal = balance;
			})
			.catch(err => {
				console.error("isAvailable(): error fetching balanceOf for "
							+ this.id + " due to " + err.message);
			});
		const minted = enftBal > 0;

		/* Check blockchain for actual mint status, by calling
		 * EnshroudProtocol.enftUnlockTime(this.id) and comparing it
		 * against the current block.  Note that if the Enft is burned, the
		 * mapping entry is deleted and 0 will be returned.
		 */
		var confBlock = 0;
		await contract.methods.enftUnlockTime(idAsUint256)
			.call({ from: owner })
			.then(conf => {
				confBlock = conf;
			})
			.catch(err => {
				console.error("isAvailable(): error fetching unlock time for "
							+ this.id + " due to " + err.message);
			});
		var currBlock = 0;
		await web3.eth.getBlock("latest")
			.then(block => {
				currBlock = block.number;
			})
			.catch(err => {
				console.error("isAvailable() could not fetch latest block, "
							+ err.message);
			});
		const confirmed = (confBlock > 0 && currBlock >= confBlock);

		/* Additionally, check against presence of this ID on the greylist by
		 * calling EnshroudProtocol.idToAuditorGreylist(this.id) and seeing if
		 * it returns an empty string.
		 */
		var greyListReason = '';
		await contract.methods.idToAuditorGreylist(idAsUint256)
			.call({ from: owner })
			.then(reason => {
				greyListReason = reason;
			})
			.catch (err => {
				console.error("isAvailable(): error fetching greylist status "
							+ "for " + this.id + " due to " + err.message);
			});
		const greylisted = greyListReason !== '';
		this.avail = minted && confirmed && !greylisted;
		if (!this.avail) {
			if (!minted) {
				this.unavailReason = "was previously burned";
			} else if (greylisted) {
				// record greylist reason; separate lines with ';' for clarity
				greyListReason = greyListReason.replace("\n", "; ");
				this.unavailReason = "greylisted due to: " + greyListReason;
			} else if (!confirmed) {
				// record block where confirmation will become true
				this.unavailReason = "until after block " + confBlock;
			}
			else {
				console.error("isAvailable(): unknown reason for " + this.id);
				this.unavailReason = "unknown reason";
			}
		} else {
			this.unavailReason = "since block " + confBlock;
		}
		return this.avail;
	}
}

export default Enft;
