/*
 * last modified---
 * 	08-14-26 abandon getPastEvents() usage due to RPC throttling; use config
 * 	06-06-26 use DOM ids for automation support
 * 	03-20-25 correct DAI Permit params; implement use of chain.link price feeds
 * 	11-15-23 remove vestigial calls to change deposit list at Enshroud level
 * 	09-19-23 implement depositTokensWithPermit()
 * 	09-09-23 fetch DepositETH events and wrap into the native.wrapsTo asset
 * 	08-01-23 rework to key off DepositERC20 events as well as prefab token confs
 * 	07-24-23 new
 *
 * purpose---
 * 	provide UI for listing and depositing tokens into the Enshroud contract
 */

import React, { useState, useEffect, Fragment } from 'react';
import Table from 'react-bootstrap/Table';
import Button from 'react-bootstrap/Button';
import Image from 'react-bootstrap/Image';
import Card from 'react-bootstrap/Card';
import Form from 'react-bootstrap/Form';
import useEth from './EthContext/useEth';
import { ChainAsset } from './ChainConnection.js';
import LoadingButton from './LoadingButton.jsx';
import NoticeWrongNetwork, { NoticeNoArtifact } from './Notices.jsx';
import ChainLinkPriceFeed from './priceFeeds.js';
const BigNumber = require("bignumber.js");


/* custom button with loading indicator which invokes doDepositAsset()
 * @param props.depMethod the method to invoke to effect a deposit
 * @param props.depAmount the amount to deposit
 * @param props.assetConfig the ChainAsset record for the deposited token
 */
function MakeDepositButton(props) {
	const [isLoading, setLoading] = useState(false);
	const depositMethod = props.depMethod;
	const assetConfig = props.assetConfig;
	const depAmount = props.depAmount;

	useEffect(() => {
		function doRequest() {
			return new Promise((resolve, reject) => depositMethod(resolve,
																  reject,
																  assetConfig,
																  depAmount));
		}

		if (isLoading) {
			doRequest().then(() => {
				setLoading(false);
			})
			.catch((err) => {
				setLoading(false);
				console.error(err.message);
			});
		}
	}, [isLoading, depositMethod, assetConfig, depAmount]);
	const handleClick = () => setLoading(true);

	// indicate method of transfer
	var xferMethod = 'ERC-20 approve + transfer';
	if (assetConfig.method === 'permit') {
		xferMethod = 'ERC-2612 Permit transfer';
	} else if (assetConfig.method === 'native') {
		xferMethod = 'native token spend';
	}
	return (
		<Button type="button" variant="success"
			title={"Send Amount of " + assetConfig.symbol + " to Enshroud smart contract via " + xferMethod}
			disabled={isLoading}
			onClick={!isLoading ? handleClick : null}
			id={"deposit-" + assetConfig.symbol}
		>
			{isLoading ? 'Loading…' : ''}
				<Image src="images/plus-circle.svg" fluid rounded
					height="30" width="30" />
		</Button>
	);
}

/* render a depositable asset as a table row, with change inputs embedded
 * @param props.keyIdx the value to use for list entry
 * @param props.assetConfig the current asset configuration (ChainAsset)
 * @param props.onDepositAsset method to trigger a deposit of this asset
 */
function DepositAssetRenderer(props) {
	// aliases
	const assetConf = props.assetConfig;

	// enable use of Web3
	const { state: { accounts, contracts, chainConn, web3 } } = useEth();
	const enshProtoContract = contracts["EnshroudProtocol"];
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);

	// create a form state for this particular table row
	const [updateForm, setUpdateForm] = useState({
		contract: assetConf.contractAddress,
		symbol: assetConf.symbol,
		amount: 0n,
		depFee: chainConn.chainConfig.defaultDepFee,
	});

	// state for entered string amount to display
	const [addDepAmount, setAddDepAmount] = useState("0.0");

	// local state for asset price
	const [marketPrice, setMarketPrice] = useState("0");

	// method to get the deposit fee for an asset, or return the default
	function getDepositFee(tokenAddr) {
		var dFee = "0";
		enshProtoContract.methods.assetDepositFee(tokenAddr)
			.call({ from: userAcct })
			.then(fee => {
				if (fee !== "0") {
					dFee = web3.utils.fromWei(fee, 'milliether');
					// convert to percent
					let pct = +dFee / 10.0;
					// must use updater function to avoid altering a stale state
					setUpdateForm(updateForm => ({...updateForm, depFee: pct}));
					assetConf.depositFee = pct;
				} else {
					// fall back to default, using updater function
					setUpdateForm(updateForm =>
								({...updateForm,
								depFee: chainConn.chainConfig.defaultDepFee}));
					assetConf.depositFee = chainConn.chainConfig.defaultDepFee;
				}
			})
			.catch(err => {
				console.error("Error fetching deposit fee, " + err);
			});
	}

	// process a change to the amount field
	const handleAmountChange = async (e) => {
		var amt = new BigNumber(0.0);
		var inpVal = e.target.value.toString();
		if (inpVal === '') inpVal = "0.0";
		if (isNaN(inpVal)) inpVal = "0.0";
		// in order to support trailing zeros, track string value separately
		var depAmtStr = inpVal;
		var enteredAmt = new BigNumber(depAmtStr);
		if (enteredAmt.isNegative()) {
			depAmtStr = "0.0";
			enteredAmt = amt;
		}
		amt = amt.plus(enteredAmt);
		const oldAmt
			= new BigNumber(web3.utils.fromWei(updateForm.amount.toString()));
		if (!amt.eq(oldAmt)) {
			depAmtStr = amt.toString();
		}
		setAddDepAmount(depAmtStr);
		setUpdateForm({...updateForm, amount: web3.utils.toWei(depAmtStr)});
		if (assetConf.depositFee === 0.0) {
			// trigger fee fetch for this token
			getDepositFee(assetConf.contractAddress);
		} else {
			// already have non-default value
			setUpdateForm(updateForm => ({...updateForm,
										depFee: assetConf.depositFee}));
		}

		// get market price for this asset if we have a feed available
		if (assetConf.priceFeed !== '' && assetConf.priceFeed !== undefined) {
			// fetch price from feed
			const clFeed = new ChainLinkPriceFeed();
			const usdPrice = await clFeed.getPrice(web3, assetConf.priceFeed);
			if (usdPrice !== undefined) setMarketPrice(usdPrice.toString());
		}
	};

	// render asset row object
	const tokenBal = new BigNumber(assetConf.balanceOf).dividedBy("1e18");
	const tokenVal = tokenBal.plus(addDepAmount).multipliedBy(marketPrice)
					.decimalPlaces(2).toString();
	const formId = assetConf.symbol + "-chgAmt";
	var dispDepAmt = addDepAmount === "0.0" ? "" : addDepAmount;
	// check for entry of a series of zeros
	const zeroPat = /0*\.0*/;
	if (addDepAmount.match(zeroPat)) {
		dispDepAmt = addDepAmount;
	}

	// render line
	return (
		<tr align="center" valign="middle">
			<td title={assetConf.contractAddress}>{assetConf.symbol}</td>
			<td>{tokenBal.toFixed()}</td>
			<td>{tokenVal > 0 ? tokenVal : "N/A"}</td>
			<td>
				<Form.Control type="text" value={dispDepAmt} id={formId}
					onChange={handleAmountChange} placeholder="0"
					title={"Enter additional amount in " + updateForm.symbol + ", ethers units"}
				/>
			</td>
			<td>Deposit fee that will be assessed: {updateForm.depFee} %</td>
			<td>
				<MakeDepositButton
					assetConfig={assetConf}
					depAmount={updateForm.amount}
					depMethod={(resolve, reject, config, amt) => props.onDepositAsset(resolve, reject, config, amt)}
				/>
			</td>
		</tr>
	);
}

/* additional row to allow entry of an additional asset deposited balance
 * @param props.zeroBalConfigs Map of known assets user has no balance in
 * @param props.zeroBalTokens list of assets (key to zeroBalConfigs)
 * @param props.onDepositAsset method to pass to make deposit button
 */
function AddBalanceRow(props) {
	const { state: { contracts, accounts, web3, chainConn } } = useEth();
	const enshProtoContract = contracts["EnshroudProtocol"];
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	const clFeed = new ChainLinkPriceFeed();

	// init a form state for the final drop-down item
	const [updateForm, setUpdateForm] = useState({
		contract: "0x0000000000000000000000000000000000000000",
		symbol: 'ETH',
		amount: 0n,
		depFee: 0.3,
		price: 0.0,
	});

	// state for entered string amount to display
	const [addDepAmount, setAddDepAmount] = useState("0.0");

	// method to get the deposit fee for an asset, or return the default
	function getDepositFee(tokenAddr) {
		var dFee = "0";
		enshProtoContract.methods.assetDepositFee(tokenAddr)
			.call({ from: userAcct })
			.then(fee => {
				if (fee !== "0") {
					dFee = web3.utils.fromWei(fee, 'milliether');
					// convert to percent
					let pct = +dFee / 10.0;
					// must use updater function to avoid altering a stale state
					setUpdateForm(updateForm => ({...updateForm, depFee: pct}));
				} else {	// no specific fee for token; fall back to default
					// use updater function to avoid altering a stale state
					setUpdateForm(updateForm =>
								({...updateForm,
								 depFee: chainConn.chainConfig.defaultDepFee}));
				}
			})
			.catch(err => {
				console.error("Error fetching deposit fee, " + err);
			});
	}

	// process a change to the amount field
	const handleAmountChange = async (e) => {
		var inpVal = e.target.value.toString();
		if (inpVal === '') inpVal = "0.0";
		if (isNaN(inpVal)) inpVal = "0.0";
		// in order to support trailing zeros, track string value separately
		var depAmtStr = inpVal;
		var enteredAmt = new BigNumber(depAmtStr);
		if (enteredAmt.isNegative()) {
			depAmtStr = "0.0";
			enteredAmt = new BigNumber(depAmtStr);
		}
		const oldAmt
			= new BigNumber(web3.utils.fromWei(updateForm.amount.toString()));
		if (!enteredAmt.eq(oldAmt)) {
			depAmtStr = enteredAmt.toString();
		}
		setAddDepAmount(depAmtStr);

		// if a price feed is available, obtain current market price
		const chAsset = chainConn.chainConfig.assetList.find(
							elt => elt.contractAddress === updateForm.contract);
		var usdPrice = new BigNumber(0);
		if (chAsset !== undefined && chAsset.priceFeed !== undefined) {
			const pFeed = chAsset.priceFeed;
			if (pFeed !== '') {
				const usdData = await clFeed.getPrice(web3, pFeed);
				if (usdData !== undefined) {
					usdPrice = usdPrice.plus(usdData);
				} else {
					console.error("Error getting market price for "
									+ chAsset.symbol);
				}
			}
		}

		if (!usdPrice.eq(0)) {
			setUpdateForm({...updateForm, amount: web3.utils.toWei(depAmtStr),
						price: usdPrice.toString()});
		} else {
			setUpdateForm({...updateForm, amount: web3.utils.toWei(depAmtStr)});
		}
	};

	// compute USD value of entered amount, if any
	const mktPrice = new BigNumber(updateForm.price);
	const usdValue = BigNumber(addDepAmount).times(mktPrice).decimalPlaces(2);
	var dispValue = usdValue.eq(0) ? "N/A" : usdValue.toString();

	// update a form entry's contract based on symbol selector (addBalanceRow)
	const handleFormChange = e => {
		const tokAddr = e.target.value;
		const config = props.zeroBalConfigs.get(tokAddr);
		setUpdateForm({...updateForm,
					symbol: config.symbol, contract: config.contractAddress});

		// redetermine asset fee for newly set token contract
		getDepositFee(config.contractAddress);
	}

	// render new asset entry
	const amtId = updateForm.symbol + "-amt";
	const selId = updateForm.symbol + "-select";
	var dispDepAmt = addDepAmount === "0.0" ? "" : addDepAmount;
	// check for entry of a series of zeros
	const zeroPat = /0*\.0*/;
	if (addDepAmount.match(zeroPat)) {
		dispDepAmt = addDepAmount;
	}

	// render line
	return (
		<tr align="center" valign="middle" key="addBalRow">
			<td>
				<select name="assetSymbol"
					title="Select new asset to deposit"
					onChange={handleFormChange} id={selId}
				>
					{props.zeroBalTokens.map((tAddr) =>
						<Fragment key={"opt-" + props.zeroBalConfigs.get(tAddr).contractAddress}>
							<option value={props.zeroBalConfigs.get(tAddr).contractAddress}>
								{props.zeroBalConfigs.get(tAddr).symbol}
							</option>
						</Fragment>
					)}
				</select>
			</td>
			<td>0</td>
			<td>{dispValue}</td>
			<td>
				<Form.Control type="text" value={dispDepAmt} placeholder="0"
					title={"Enter amount in " + updateForm.symbol + ", ethers units"}
					onChange={handleAmountChange} id={amtId}
				/>
			</td>
			<td>
				Deposit fee that will be assessed: {updateForm.depFee} %
			</td>
			<td>
				<MakeDepositButton
					assetConfig={props.zeroBalConfigs.get(updateForm.contract)}
					depAmount={updateForm.amount}
					depMethod={(resolve, reject, config, amt) => props.onDepositAsset(resolve, reject, config, amt)}
				/>
			</td>
		</tr>
	);
}

/* produce table of all deposited assets, plus additional line to add a new one
 * @param props.onSelect method to shift to other pages
 * @param props.protocolAssets all chain assets ever deposited to Enshroud
 * @param props.onNewProtocolAssets method to add a new (first-used) chain asset
 * @param props.userAssets all assets user has ever deposited to Enshroud
 * @param props.onNewUserAsset method to add a new asset to user's list
 * @param props.onNewUserAssets method to add multiple new assets to user's list
 */
function DepositTable(props) {
	// enable use of our contracts and accounts (guaranteed not to be null)
	const { state: { contracts, accounts, web3, chainConn } } = useEth();
	const enshProtoContract = contracts["EnshroudProtocol"];
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	const enshProtoAddr = enshProtoContract.options.address;
	var startBlock = chainConn.chainConfig.tokenGenesis;
	if (startBlock === undefined) {
		console.error("No tokenGenesis found for EnshroudToken on chain Id "
						+ chainConn.chainConfig.chainId);
		startBlock = "earliest";
	}

	// get balance on deposit in the EnshroudProtocol contract
	const checkDepositBalance = async (tokenAddr) => {
		const bal = await enshProtoContract.methods.assetBalances(
								userAcct, tokenAddr).call({ from: userAcct });
		return bal;
	};

	// get name from an erc20-compatible contract
	const getTokenName = async (tokenAddr) => {
		var name = '';
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'name',
			type: 'function',
			constant: true,
			inputs: []
		}, []);
		await web3.eth.call({
			to: tokenAddr,	// erc20/erc777/erc4626 contract address
			data: callData	// function call encoding
		})
			.then(nomen => {
				name = web3.eth.abi.decodeParameter('string', nomen);
			}).catch(err => {
				console.error("error fetching name at " + tokenAddr + ": "
							+ err);
			});
		return name;
	};

	// obtain the set of all assets configured on this chain, and user's bals
	const buildAssetConfig = async (resolve, reject) => {
		// start with preset asset configs
		var protocolAssets = [].concat(chainConn.chainConfig.assetList);

		// fetch the user's current deposited balance in every asset listed
		var userAssets = [];
		for await (const protoAsset of protocolAssets) {
			const userBal
				= await checkDepositBalance(protoAsset.contractAddress);
			if (userBal > 0) {
				const assetConf = new ChainAsset();
				assetConf.config(protoAsset);
				assetConf.balanceOf = userBal;
				assetConf.depositFee = protoAsset.depositFee;
				assetConf.withdrawFee = protoAsset.withdrawFee;
				assetConf.wrapsTo = protoAsset.wrapsTo;
				userAssets.push(assetConf);
			}
		}

		// now tell React we have all the data via passed functions
		props.onNewProtocolAssets(protocolAssets);
		props.onNewUserAssets(userAssets);

		resolve(true);
	};

	// determine the approval amount for EnshroudProtocol for a given token
	const checkApprovalBalance = async (tokenAddr) => {
		var apBal = 0n;
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'allowance',
			type: 'function',
			constant: true,
			inputs: [{
				type: 'address',
				name: ''
			},{
				type: 'address',
				name: ''
			}],
			outputs: [{
				type: 'uint256'
			}]
		}, [tokenAddr, enshProtoAddr]);
		await web3.eth.call({
			to: tokenAddr,	// erc20/erc777/erc4626 contract address
			from: userAcct,
			data: callData	// function call encoding
		})
			.then(approvAmt => {
				apBal = approvAmt;
			}).catch(err => {
				console.error("error fetching approval bal at " + tokenAddr
							+ ": " + err);
			});
		return apBal;
	};

	// obtain approval from erc-20 compatible contract for our contract
	const obtainSpendApproval = async (tokenAddr, amtWei) => {
		var appr = true;
		if (amtWei <= 0 || tokenAddr === '') {
			console.error("obtainSpendApproval: missing input");
			return false;
		}
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'approve',
			type: 'function',
			constant: false,
			inputs: [{
				type: 'address',
				name: 'spender',
			},{
				type: 'uint256',
				name: 'amount',
			}],
			outputs: [{
				type: 'bool'
			}]
		}, [enshProtoAddr, amtWei]);
		await web3.eth.sendTransaction({
			to: tokenAddr,
			from: userAcct,
			data: callData,
			chainId: chainConn.chainConfig.chainId
		})
			.then(approval => {
				appr = approval;
				if (!appr) {
					console.error("approve(" + amtWei + ") failed");
				}
			}).catch(err => {
				appr = false;
				console.error("approve() failed for " + tokenAddr + ": "
							+ err.code + ", " + err.message);
			});
		return appr;
	};

	// get the nonce value for an ERC-2612-compatible contract
	const getNonce = async (tokenAddr) => {
		var nonce = 0;
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'nonces',
			type: 'function',
			constant: 'true',
			inputs: [{
				type: 'address',
				name: ''
			}],
			outputs: [{
				type: 'uint256'
			}]
		}, [userAcct]);
		await web3.eth.call({
			to: tokenAddr,	// erc20/erc777/erc4626 contract address
			from: userAcct,
			data: callData	// function call encoding
		})
			.then(nonceVal => {
				if (nonceVal === undefined || nonceVal === '0x') {
					nonce = -1;
				} else {
					nonce = +nonceVal;
				}
			}).catch(err => {
				console.error("error fetching nonce at " + tokenAddr + ": "
							+ err);
			});
		return nonce;
	};

	// obtain the deadline as last block timestamp + 3 minutes
	const getDeadline = async () => {
		var deadline = 0;
		await web3.eth.getBlock("latest")
			.then(block => {
				const tStamp = block.timestamp + 180;
				/* NB: on some test chains (such as Ganache) new blocks are not
				 * generated absent new transactions, so this timestamp might
				 * be in the past.  If so take current time + 3 minutes instead.
				 */
				const nowSecs = new Date().getTime() / 1000;
				const altTstamp = parseInt(nowSecs) + 180;
				// return whichever is later
				deadline = Math.max(tStamp, altTstamp);
			})
		return deadline;
	};

	// perform a deposit to the EnshroudProtocol smart contract
	const doDepositAsset = async (resolve, reject, assetConfig, depAmt) => {
		if (depAmt <= 0) {
			let errRet = new Error("Illegal deposit amount, " + depAmt);
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		if (assetConfig.contractAddress
			=== '0x0000000000000000000000000000000000000000'
			&& assetConfig.method !== 'native')
		{
			let noTok = new Error("Missing token address for "
								+ assetConfig.symbol);
			console.error(noTok.message);
			reject(noTok);
			return false;
		}

		// calculate deposit fee smart contract will deduct
		var depRate = new BigNumber(assetConfig.depositFee).times("1e16");
		var deposited = new BigNumber(depAmt);
		var depFee = depRate.times(deposited).dividedToIntegerBy("1e18");
		const netDeposit = deposited.minus(depFee);
		const chId = chainConn.chainConfig.chainId;
		const depAmtEth = web3.utils.fromWei(depAmt);

		// based on the method setting, generate the calls to do the deposit
		var gotErr = false;
		if (assetConfig.method === 'native') {
			var wrappedAsset = props.protocolAssets.find(
							elt => elt.contractAddress === assetConfig.wrapsTo);

			// this is a deposit of ETH or equivalent native tokens for chain
			await enshProtoContract.methods.depositEth(userAcct)
					.send({ from: userAcct, value: depAmt })
				.then(receipt => {
					// increment balance, of wrapsTo asset not native one
					const incrAsset = wrappedAsset === undefined
						? assetConfig : wrappedAsset;
					let oldBal = BigNumber(incrAsset.balanceOf.toString());
					let newBal = oldBal.plus(netDeposit);
					incrAsset.balanceOf = newBal.toFixed();
					alert("Your deposit of " + depAmtEth + " "
						+ assetConfig.symbol + " (native tokens) succeeded!\n"
						+ "Re-download your deposits to confirm totals.");
				})
				.catch(err => {
					console.error("Deposit of " + assetConfig.symbol
								+ " (native tokens), amt = " + depAmtEth
								+ " failed: " + err.code + ", " + err.message);
					alert("Your deposit of " + depAmtEth + " "
						+ assetConfig.symbol + " (native tokens) to "
						+ "Enshroud did not succeed, due to:\n" + err.message);
					reject(err);
					gotErr = true;
				});
		} else if (assetConfig.method === 'permit') {
			// obtain the user's nonce on this contract
			const userNonce = await getNonce(assetConfig.contractAddress);
			if (userNonce === -1) {
				// shouldn't happen, but handle appropriately
				const notERC2612 = new Error("ERC20 contract for "
											+ assetConfig.symbol
											+ " does not support ERC-2612 "
											+ "nonces; cannot deposit with "
											+ "Permit");
				alert("Your deposit of " + depAmt + " "
						+ assetConfig.symbol + " (ERC20 tokens) to "
						+ "Enshroud did not succeed, due to:\n"
						+ notERC2612.message);
				// change deposit method to 'tokens' and tell user
				assetConfig.method = 'tokens';
				alert("To try making this deposit again using "
					+ "standard ERC-20 approve/transfer logic, "
					+ "simply click on the Add button again.");
				reject(notERC2612);
				return false;
			}

			// obtain the deadline as last block timestamp plus 120 seconds
			const deadline = await getDeadline();

			// obtain the name of this token (for EIP-712 domain purposes)
			const tokenName = await getTokenName(assetConfig.contractAddress);

			// there are two sub-types here: DAI and everything else
			var userSig = '';
			var msgParams = '';
			// NB: new DAI on Sepolia works like regular Permit
			if (assetConfig.symbol === 'DAI'
				&& chainConn.chainConfig.chain !== "Sepolia")
			{
				// use the Sky:DAI variant
				msgParams = JSON.stringify({
					types: {
						// the domain the contract is hosted on
						EIP712Domain: [
							{ name: 'name', type: 'string' },
							{ name: 'version', type: 'string' },
							{ name: 'chainId', type: 'uint256' },
							{ name: 'verifyingContract', type: 'address' },
						],
						// refer to primaryType
						Permit: [
							{ name: 'holder', type: 'address' },
							{ name: 'spender', type: 'address' },
							{ name: 'nonce', type: 'uint256' },
							{ name: 'expiry', type: 'uint256' },
							{ name: 'allowed', type: 'bool' },
						],
					},
					primaryType: 'Permit',
					// EIP-712 domain info
					domain: {
						name: tokenName,
						version: '1',
						chainId: chId,
						verifyingContract: assetConfig.contractAddress,
					},
					// descriptive info on what's being signed and for whom
					message: {
						holder: userAcct,
						spender: enshProtoAddr,
						nonce: userNonce,
						expiry: deadline,
						allowed: true,
					},
				});
			} else {
				// define eth_signTypedData_v4 parameters
				msgParams = JSON.stringify({
					types: {
						// the domain the contract is hosted on
						EIP712Domain: [
							{ name: 'name', type: 'string' },
							{ name: 'version', type: 'string' },
							{ name: 'chainId', type: 'uint256' },
							{ name: 'verifyingContract', type: 'address' },
						],
						// refer to primaryType
						Permit: [
							{ name: 'owner', type: 'address' },
							{ name: 'spender', type: 'address' },
							{ name: 'value', type: 'uint256' },
							{ name: 'nonce', type: 'uint256' },
							{ name: 'deadline', type: 'uint256' },
						],
					},
					primaryType: 'Permit',
					// EIP-712 domain info
					domain: {
						name: tokenName,
						version: '1',
						chainId: chId,
						verifyingContract: assetConfig.contractAddress,
					},
					// descriptive info on what's being signed and for whom
					message: {
						owner: userAcct,
						spender: enshProtoAddr,
						value: depAmt,
						nonce: userNonce,
						deadline: deadline,
					},
				});
			}

			// now obtain signature on params in a EIP-712 compatible way
			const method = 'eth_signTypedData_v4';
			//console.debug("built msgParams = \"" + msgParams + "\"");
			var params = [userAcct, msgParams];
			await web3.currentProvider.sendAsync(
				{
					method,
					params,
					from: userAcct,
				},
				await async function (err, result) {
					if (err) console.dir(err);
					if (result.error) alert(result.error.message);
					if (result.error) console.error('ERROR', result.error);
					userSig = result.result;
					if (userSig === undefined) {
						let sigErr
							= new Error("Error building EIP712 signature");
						reject(sigErr);
						return false;
					}

					// split signature
					const r = userSig.slice(0, 66);
					const s = '0x' + userSig.slice(66, 130);
					const v = web3.utils.hexToNumber('0x'
													+ userSig.slice(130, 132));

					/* invoke EnshroudProtocol.depositTokensWithPermit(), which
					 * handles both the regular and DAI forms
					 */
					await enshProtoContract.methods.depositTokensWithPermit(
													assetConfig.contractAddress,
													userAcct,
													depAmt,
													userNonce,
													deadline,
													v,
													r,
													s).send({ from: userAcct })
						.then(receipt => {
							// increment balance
							let oldBal
								= BigNumber(assetConfig.balanceOf.toString());
							let newBal = oldBal.plus(netDeposit).integerValue();
							assetConfig.balanceOf = newBal.toFixed();
							alert("Your deposit of " + depAmtEth + " "
								+ assetConfig.symbol + " succeeded!\n"
								+ "Re-download your deposits to confirm "
								+ "totals.");
						})
						.catch(err => {
							console.error("depositTokensWithPermit() error: "
										+ "code " + err.code + ", "
										+ err.message);
							gotErr = true;
							alert("Your transfer of " + depAmtEth + " "
								+ assetConfig.symbol
								+ " to Enshroud via ERC-2612 Permit failed, "
								+ "due to\n" + err.message);

							// change deposit method to 'tokens' and tell user
							assetConfig.method = 'tokens';
							alert("To try making this deposit again using "
								+ "standard ERC-20 approve/transfer logic, "
								+ "simply click on the Add button again.");
						});
				}
			);
		} else {	// === 'tokens'
			// examine whether we have enough approval balance
			const approvalBal
				= await checkApprovalBalance(assetConfig.contractAddress);
			var appr = true;
			if (approvalBal < depAmt) {
				appr = await obtainSpendApproval(assetConfig.contractAddress,
												 depAmt);
				if (!appr) {
					alert("Approval for transfer of " + depAmtEth + " "
						+ assetConfig.symbol + " to Enshroud "
						+ "contract was denied, deposit failed.")
					gotErr = true;
				}
			}
			if (appr) {
				await enshProtoContract.methods
						.depositTokens(userAcct, assetConfig.contractAddress,
										depAmt).send({ from: userAcct })
					.then(receipt => {
						// increment balance
						let oldBal
							= BigNumber(assetConfig.balanceOf.toString());
						let newBal = oldBal.plus(netDeposit).integerValue();
						assetConfig.balanceOf = newBal.toFixed();
						alert("Your deposit of " + depAmtEth + " "
							+ assetConfig.symbol + " succeeded!\n"
							+ "Re-download your deposits to confirm totals.");
					})
					.catch(err => {
						console.error("Deposit of " + assetConfig.symbol
									+ " tokens, amt = " + depAmtEth
									+ " failed: " + err.code + ", "
									+ err.message);
						alert("Your deposit of " + depAmtEth + " "
							+ assetConfig.symbol
							+ " tokens to Enshroud did not succeed, due to:\n"
							+ err.message);
						gotErr = true;
					});
			}
		}
		if (gotErr) {
			reject(new Error("Error making deposit to Enshroud"));
			return false;
		}
		resolve(true);
		return true;
	};
	
	/* examine the list of all known assets and determine which are not
	 * listed in the user's balances
	 */
	var zeroBalConfigs = new Map();
	var zeroBalTokens = [];
	props.protocolAssets.forEach(asset => {
		const balancePresent = (elt) =>
			elt.contractAddress === asset.contractAddress && elt.balanceOf >= 0;
		var missing = !props.userAssets.some(balancePresent);
		if (missing) {
			// record this one in the add drop-down list as a zero-bal for user
			zeroBalConfigs.set(asset.contractAddress, asset);
			zeroBalTokens.push(asset.contractAddress);
		}
	});

	let aIdx = 1;
	const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
	const hdr = "header" + nf.format(aIdx);
	const frag = "fragment" + nf.format(aIdx);
	return (
		<Fragment key={frag}>
		<Card>
			<Card.Body>
				<Card.Title>
					Depositing Tokens to the Enshroud Contract
				</Card.Title>
				<Card.Text>
					The table below shows assets you have already deposited
					(if any). These balances are available for eNFT Minting.
				</Card.Text>
				<Card.Text>
					The table is populated by clicking the Refresh Your
					Deposits button below.  You can then add more coins to
					any of your existing deposits using the form on each row.
				</Card.Text>
				<Card.Text>
					Use the drop-down on the bottom row to select another
					asset you wish to deposit.
				</Card.Text>
				<Card.Link href="#" title="Go to eNFT Minting page"
					onClick={() => props.onSelect('mintENFTs')}
					role="button" id="goMinting">
					eNFT Minting
				</Card.Link>
				<br/><br/>
				<LoadingButton variant="primary"
					buttonText="Refresh Your Deposits"
					buttonIcon="images/download.svg"
					buttonTitle="populate table with your balances, and list other popular tokens"
					netMethod={(resolve, reject) => buildAssetConfig(resolve, reject)}
				/>
			</Card.Body>
		</Card>
		<br/>
		<i>
			N.B.: deposited native tokens (such as ETH) will become wrapped.
			<br/>
			If possible, deposit-with-permit (ERC-2612) will be used to save
			gas.
		</i>
		<br/>
		<Table striped bordered hover responsive>
			<caption className="caption-top">
				Assets you currently have on deposit:
			</caption>
			<thead>
				<tr align="center" key={hdr}>
					<th scope="col" title="Symbol for this token">
						Asset
					</th>
					<th scope="col"
						title="Shown in ethers units, converted from wei for readability">
						Balance
					</th>
					<th scope="col" title="At market price, if available">
						Total Value in USD
					</th>
					<th scope="col"
						title="Additional amount you wish to deposit, specified in ethers units (will be converted to wei)">
						+ Amount (1e-18)
					</th>
					<th scope="col">Deposit fee in percent</th>
					<th scope="col">Add</th>
				</tr>
			</thead>
			<tbody>
				{props.userAssets.length > 0 && props.userAssets.map((assetConfig) =>
					<DepositAssetRenderer key={nf.format(aIdx++)}
						assetConfig={assetConfig}
						onDepositAsset={(resolve, reject, config, amt) => doDepositAsset(resolve, reject, config, amt)}
					/>
				)}

				{ /* add input for assets not on deposit */ }
				<AddBalanceRow key={nf.format(aIdx++)}
					zeroBalTokens={zeroBalTokens}
					zeroBalConfigs={zeroBalConfigs}
					onDepositAsset={(resolve, reject, config, amt) => doDepositAsset(resolve, reject, config, amt)}
				/>
			</tbody>
		</Table>
		</Fragment>
	);
}

/* generate entire page content
 * @param props.onSelect method to switch to other pages
 */
function DepositAssets(props) {
	// enable use of our contracts, accounts, and Web3 connection
	const { state: { contracts, chainConn, artifacts } } = useEth();

	// seed the chainAssets state with all predefined ChainAsset configs
	function createInitialAssetList() {
		const initialAssets = [].concat(chainConn.chainConfig.assetList);
		return initialAssets;
	}

	// all assets ever deposited into EnshroudProtocol contract (ChainAssetS)
	const [chainAssets, setChainAssets] = useState(createInitialAssetList);

	// all assets ever deposited by the particular user account (ChainAssetS)
	const [userAssets, setUserAssets] = useState([]);

	/* Allow for possibility user switched chains by using the chain dropdown
	 * within MetaMask, rather than the dApp's own Network dropdown button.
	 * In this case we don't go to the Home page afterwards, and consequently
	 * createInitialAssetList() will not be called again.  So do it here.
	 * (Note this assumes there are different numbers of assets between chains.)
	 */
	if (chainConn.chainConfig.assetList.length !== chainAssets.length) {
		setChainAssets([].concat(chainConn.chainConfig.assetList));
		setUserAssets([]);
	}

	// method to add a list of chain assets by replacing state
	function handleAddChainAssets(configs) {
		var addedList = [];
		// add only if not found in the current list already
		configs.forEach(assetConfig => {
			const existing = chainAssets.find(
					elt => elt.contractAddress === assetConfig.contractAddress);
			if (existing === undefined) {
				// add this one to the list
				addedList.push(assetConfig);
			}
		});

		// add new entries all at once to prevent unnecessary re-renderings
		if (addedList.length > 0) {
			setChainAssets([...chainAssets, ...addedList]);
		}
	}

	// method to add a list of user chain assets by merging state
	function handleAddUserAssets(configs) {
		var addedList = [];
		// add only if not found in the current list already
		configs.forEach(assetConfig => {
			const existing = userAssets.find(
					elt => elt.contractAddress === assetConfig.contractAddress);
			if (existing === undefined) {
				// add this one to the list
				addedList.push(assetConfig);
			} else {
				// we need to update the user's balance
				setUserAssets(userAssets.map(a => {
					if (a.contractAddress === existing.contractAddress) {
						// replace with the modified one
						return assetConfig;
					} else {
						return a;
					}
				}));
			}
		});

		// add new ones all at once to prevent unnecessary re-renderings
		if (addedList.length > 0) {
			setUserAssets([...userAssets, ...addedList]);
		}
	}

	// method to add or update a single asset deposited by user by merging state
	function handleAddUserAsset(config) {
		const existing = userAssets.find(
						elt => elt.contractAddress === config.contractAddress);
		if (existing === undefined) {
			setUserAssets([...userAssets, config]);
		} else {
			// update it to record the new balance (just deposited)
			setUserAssets(userAssets.map(a => {
				if (a.contractAddress === existing.contractAddress) {
					// replace with modified one
					return config;
				} else {
					return a;
				}
			}));
		}
	}

	// rendering for deposit assets UI
	const depositAssets =
		<>
			<div className="container">
				<h2 align="center">Deposit Tokens</h2>
				<br/><br/>
				<DepositTable
					onSelect={props.onSelect}
					protocolAssets={chainAssets}
					onNewProtocolAssets={handleAddChainAssets}
					userAssets={userAssets}
					onNewUserAsset={handleAddUserAsset}
					onNewUserAssets={handleAddUserAssets}
				/>
				<br/><br/>
				<Card>
					<Card.Body>
						<Card.Title>Additional Token Assets</Card.Title>
						<Card.Text>
							If the asset you want to deposit isn't listed in
							the table or the drop-down selector, you can
							customize the list by adding a suitable
							(ERC20-compatible) token via the Asset Config page.
							The new symbol will then appear in the Asset
							drop-down control.
						</Card.Text>
						<Card.Link href="#" title="Go to Asset Config page"
							onClick={() => props.onSelect('assetConfig')}
							role="button" id="goConfig">
							Asset Config
						</Card.Link>
					</Card.Body>
				</Card>
				<br/>
			</div>
		</>;

	// render the actual deposits page
	return (
		<div id="DepositAssets">
		{
			!artifacts.EnshroudProtocol ? <NoticeNoArtifact /> :
			contracts == null ||
					!contracts["EnshroudProtocol"] ? <NoticeWrongNetwork /> :
				depositAssets
		}
		</div>
	);
}

export default DepositAssets;
