/*
 * last modified---
 * 	08-14-26 abandon use of getPastEvents() due to RPC throttling; use config
 * 	06-09-26 add DOM ids for automation support
 * 	03-19-25 implement chain.link price feeds
 * 	02-24-25 limit memo lines to 1024 chars
 * 	07-26-24 minor wording tweaks
 * 	12-12-23 import PayeeConfig from its own file
 * 	11-21-23 use BigNumber for all amount computations
 * 	10-18-23 remove use of deposits and payeeConfigs passed in from Enshroud
 * 	10-05-23 move SmartContractSubmitter invocation here from Enshroud
 * 	09-21-23 debug
 * 	09-11-23 use Contract.getPastEvents(), check for DepositETH events
 * 	08-18-23 auto-generate catch-all overage payee
 * 	08-09-23 new
 *
 * purpose---
 * 	provide UI for minting new eNFTs from token balances deposited in Enshroud
 */

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


// max number of array entries (inputs or outputs), EnshroudProtocol.ARRAY_LIMIT
const ARRAY_LIMIT = 20;
const MEMO_MAX = 1024;	// max length of a memo field

/* render a deposited asset as a table row
 * @param props.deposit the particular user deposit balance (a ChainAsset)
 * @param props.selectedBalance the currently selected balance (a ChainAsset;
 * might or might not be the same as props.deposit)
 * @param props.selectBalance the method to change to a new selected balance
 * @param props.mintAmount the total amount currently being minted
 * @param props.setMintAmount the method to update the mintAmount in caller
 */
function DepositAssetRenderer(props) {
	const { state: { web3 } } = useEth();

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

	// aliases
	const depBalance = props.deposit;
	const tok = depBalance.symbol;
	var selected = props.selectedBalance !== undefined
					&& depBalance.contractAddress
					=== props.selectedBalance.contractAddress ?  true : false;
	const amtId = "amt-" + tok;
	const maxAmt = web3.utils.fromWei(depBalance.balanceOf.toString());
	var amtValue = selected ? props.mintAmount : "0.0";

	// process a change to the amount field (triggered by Set button)
	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 mintAmtStr = inpVal;
		var enteredAmt = new BigNumber(mintAmtStr);
		if (enteredAmt.isNegative()) {
			// go back to previous amount
			mintAmtStr = amtValue;
			enteredAmt = new BigNumber(mintAmtStr);
		}
		amt = BigNumber.minimum(enteredAmt, maxAmt);
		if (!amt.eq(enteredAmt)) {
			mintAmtStr = amt.toString();
		}
		if (selected) {
			props.setMintAmount(mintAmtStr);
			amtValue = mintAmtStr;
		}

		// find the market price of this asset if we have one
		var usdPrice = "0";
		if (depBalance.priceFeed !== undefined && depBalance.priceFeed !== "") {
			// fetch price from feed
			const clFeed = new ChainLinkPriceFeed();
			const feedPrice = await clFeed.getPrice(web3, depBalance.priceFeed);
			if (feedPrice !== undefined) {
				usdPrice = feedPrice.toString();
				setMarketPrice(usdPrice);
			}
		}
	};

	// record which asset is selected for minting
	function handleAssetChange(tokenAddr) {
		if (tokenAddr !== undefined) {
			props.selectBalance(tokenAddr);
		}
	}

	// set max amount
	function setMaxAmount() {
		// set both our input and state in <MintENFTs>
		let tokInput = document.getElementById(amtId);
		tokInput.value = web3.utils.fromWei(depBalance.balanceOf.toString());
		if (selected) {
			props.setMintAmount(tokInput.value);
		}
	}

	// shorthands
	const selId = "sel-" + tok;
	const symId = "sym-" + tok;
	const contract = "(" + depBalance.contractAddress + ")";
	const availId = "avail-" + tok;
	const maxId = "max-" + tok;
	var dispAmt = amtValue === "0.0" ? "" : amtValue;
	// check for entry of a series of zeros
	const zeroPat = /0*\.0*/;
	if (amtValue.match(zeroPat)) dispAmt = amtValue;
	const usdValue = new BigNumber(amtValue).times(marketPrice)
					.decimalPlaces(2).toString();
	
	// render balance row object, including form controls
	return (
		<tr align="center" valign="middle">
			<td>
				<Form.Check type="radio" id={selId} name="mintAsset"
					onClick={(tokenAddr) => handleAssetChange(depBalance.contractAddress)}
					aria-label={"select " + tok}
					selected={selected}
				/>
			</td>
			<td id={symId} title={contract}>{tok}</td>
			<td id={availId} title="(in ethers, 1e-18)">
				{web3.utils.fromWei(depBalance.balanceOf.toString())}
			</td>
			<td>{usdValue > 0 ? usdValue : "N/A"}</td>
			<td>
				<InputGroup className="p-2">
					<Form.Control type="text" id={amtId} name={amtId}
						value={dispAmt} onChange={handleAmountChange}
						title={"Enter minting amount in " + tok}
						placeholder="0"
					/>
					<Button type="button" variant="link" className="btn btn-sm"
						title="Set to Current Balance" id={maxId}
						onClick={setMaxAmount}
					>
						Max
					</Button>
				</InputGroup>
			</td>
		</tr>
	);
}

/* produce table of all deposited assets available for minting
 * @param props.userAssets description of each asset for which user has deposit
 * (these are ChainAssetS)
 * @param props.onNewUserAssets method to handle recording assets deposited
 * @param props.selectedBalance the balance (in deposits) currently selected
 * @param props.selectBalance method to handle setting the selectedBalance
 * @param props.mintAmount amount of selected asset which user wants to mint
 * @param props.setMintAmount method to handle setting the mintAmount
 * @param props.onSelect method to switch to another page
 */
function DepositTable(props) {
	// enable use of Web3
	const { state: { contracts, accounts, web3, chainConn } } = useEth();
	var enshProtoContract = contracts["EnshroudProtocol"];
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	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;
	};

	// 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 function
		props.onNewUserAssets(userAssets);
		resolve(true);
	};

	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>
					Minting eNFTs from your Deposited Balances
				</Card.Title>
				<Card.Text>
					1. Select the deposited Balance you wish to mint against by
					clicking on the radio button in the eligible balances table
					below.  If the table is empty, use the
					big <b>Refresh Deposited Balances</b> button
					to populate it.
				</Card.Text>
				<Card.Text>
					2. Next, fill out the eNFT Minting Configuration form in the
					table below that.  This specifies the recipient addresses
					and amounts for each eNFT you want to mint.  Note you must
					use the '+' icon to add each payee to the configuration.
					Any overage you don't allocate will get minted to your own
					account address.
				</Card.Text>
				<Card.Text>
					3. Finally, click the <b>Configure Mint</b> button to
					preprocess your eNFT mintage via the Enshroud MVO Layer2.
				</Card.Text>
				<Card.Text>
					4. You will then be able to review the transaction again
					before you submit it to the blockchain.
				</Card.Text>
				<Card.Text>
					If you do not yet have any assets deposited, go to the
					Deposit page.  Use the Refresh button to populate the
					balances table with your existing deposits to Enshroud,
					and make any new deposits you wish.  Then return here.
				</Card.Text>
				<Card.Link href="#" title="Go to Deposit page" role="button"
					onClick={() => props.onSelect('depositAssets')}
					id="goDeposits"
				 >
					Deposit
				</Card.Link>
				<br/><br/>
				<Card.Text>
					If you want to work with a custom token not present in our
					configured list of common tokens, go to the Asset Config
					page and add its contract address.  Then Deposit some of
					this token and return here to Mint from your deposit.
				</Card.Text>
				<Card.Link href="#" title="Go to Asset Config page"
					role="button" onClick={() => props.onSelect('assetConfig')}
					id="goConfig"
				 >
					Asset Config
				</Card.Link>
				<br/><br/><br/>
				<LoadingButton variant="primary"
					buttonStyle="m-3"
					buttonText="Refresh Deposited Balances"
					buttonIcon="images/download.svg"
					buttonTitle="Populate table with your already-deposited balances"
					netMethod={(resolve, reject) => buildAssetConfig(resolve, reject)}
				/>
			</Card.Body>
			</Card>
			<br/><br/>
			<Table striped bordered hover responsive variant="dark">
				<caption className="caption-top">
					Your eligible asset balances now on deposit:
				</caption>
				<thead>
					<tr align="center" key={hdr}>
						<th scope="col"
							title="Click a radio button to select the asset">
							Select
						</th>
						<th scope="col"
							title="Use hover text to confirm contract address">
							Asset (symbol)
						</th>
						<th scope="col"
							title="The amount you have on deposit, in ethers units">
							Current Balance
						</th>
						<th scope="col" title="Value of available amount in USD, at market prices if available">
							Value in USD
						</th>
						<th scope="col">Mint Amount (ethers, 1e-18)</th>
					</tr>
				</thead>
				<tbody>
					{props.userAssets.length > 0 && props.userAssets.map(depositAsset =>
						<DepositAssetRenderer
							key={nf.format(aIdx++)}
							deposit={depositAsset}
							selectedBalance={props.selectedBalance}
							selectBalance={props.selectBalance}
							mintAmount={props.mintAmount}
							setMintAmount={props.setMintAmount}
						/>
					)}
				</tbody>
			</Table>
		</Fragment>
	);
}

/* render a single row of the payee config table
 * @param props.payeeNo the number of the payee (index)
 * @param props.payee the config for the payee (a SpendPayee)
 * @param props.userAcct the user's own address (accounts[0])
 * @param props.onDelPayee method to record deletion of this payee
 * @param props.onUpdPayee method to record a change to this payee (esp. amount
 * which affects <RunningTotal/>)
 */
function PayeeRenderer(props) {
	const { state: { web3 } } = useEth();

	// shorthands
	const payee = props.payee;
	const uniq = props.payeeNo;
	const addrId = "addr-" + uniq;
	const amtId = "amt-" + uniq;
	const memoId = "memo-" + uniq;
	const selfId = "self-" + uniq;
	const delId = "rm-" + uniq;
	const defaultTitle = "Enter payee #" + uniq + " address";

	// form input address field
	const [inputAddr, setInputAddr] = useState(payee.address);

	// form input amount field
	const [inputAmount, setInputAmount] = useState(payee.amount.toString());

	// form memo field
	const [inputMemo, setInputMemo] = useState(payee.memo);

	// process a change to the amount field
	const handleAmountChange = 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 spendAmtStr = inpVal;
		var enteredAmt = new BigNumber(spendAmtStr);
		if (enteredAmt.isNegative()) {
			// go back to previous value
			spendAmtStr = inputAmount;
			enteredAmt = new BigNumber(spendAmtStr);
		}
		amt = amt.plus(enteredAmt);
		setInputAmount(spendAmtStr);
		const diff = amt.minus(payee.amount);
		if (!diff.isZero()) {
			payee.amount = amt;
			props.onUpdPayee();
		}
	};

	// process a change to the address field (cannot be empty)
	const handleAddressChange = e => {
		if (/^0x[0-9a-fA-F]+/.test(e.target.value)) {
			const bareAddr = web3.utils.stripHexPrefix(e.target.value);
			const addr = "0x" + bareAddr;
			if (web3.utils.isAddress(addr)) {
				const csAddr = web3.utils.toChecksumAddress(addr);
				setInputAddr(csAddr);
				if (payee.address !== csAddr) {
					payee.address = csAddr;
					props.onUpdPayee();
				}
			} else {
				alert("Invalid address, " + addr);
			}
		}
	};

	// process a change to the memo field
	const handleMemoChange = e => {
		const memo = e.target.value.toString();
		if (memo.length > MEMO_MAX) {
			alert("Memo too long, max " + MEMO_MAX + " characters");
			return;
		}
		setInputMemo(memo);
		if (payee.memo !== memo) {
			payee.memo = memo;
			props.onUpdPayee();
		}
	}

	// set the address field to self (active user account)
	const setSelf = e => {
		if (payee.address !== props.userAcct) {
			setInputAddr(props.userAcct);
			payee.address = props.userAcct;
			props.onUpdPayee();
		}
	};

	// remove the payee shown
	const removePayee = e => {
		props.onDelPayee(payee.sequence);
	};

	const addr = inputAddr.substring(0, 7) + '...' + inputAddr.substring(37);
	var dispAmt = inputAmount === "0.0" ? "" : inputAmount;
	// check for entry of a series of zeros
	const zeroPat = /0*\.0*/;
	if (inputAmount.match(zeroPat)) {
		dispAmt = inputAmount;
	}

	// render line
	return (
		<tr align="center" valign="middle">
			<td>
				<InputGroup className="p-2">
					<Form.Control type="text" maxLength={45} id={addrId}
						name={addrId} value={inputAddr} placeholder="0x"
						onChange={handleAddressChange}
						title={payee.address === '' ? defaultTitle : addr}
					/>
					<Button variant="link" className="btn-sm" role="button"
						title="Set this payee to your own account address"
						onClick={setSelf} id={selfId}
					>
						Self
					</Button>
				</InputGroup>
			</td>
			<td>
				<Form.Control type="text" id={amtId} name={amtId}
					value={dispAmt} onChange={handleAmountChange}
					title="Amount to mint to this address (ethers, will be converted to wei)"
					className="p-2" placeholder="0"
				/>
			</td>
			<td>
				<Form.Control type="text" id={memoId} name={memoId}
					value={inputMemo} title="Memo line to send to payee"
					onChange={handleMemoChange}
					className="p-2"
				/>
			</td>
			<th role="row">
				<Button title="Remove this address and amount from mint config"
					variant="secondary" className="btn-sm" id={delId}
					onClick={removePayee}
				>
					<Image src="images/x-lg.svg" className="p-2" fluid rounded
						height="40" width="40"/>
				</Button>
			</th>
		</tr>
	);
}

/* render a form to enter a new payee
 * @param props.selectedConfig the currently selected PayeeConfig record
 * @param props.userAcct the user wallet address
 * @param props.onNewPayee the method to invoke in caller to create a SpendPayee
 * @param props.mintTotal the max amount the user wants minted
 */
function NewPayeeRenderer(props) {
	const { state: { web3 } } = useEth();

	// shorthands
	const payeeConf = props.selectedConfig;
	var payeeLen = 1;
	if (payeeConf !== undefined) {
		payeeLen = Math.max(1, payeeConf.payees.length+1);
	}
	const addrId = "addr-" + payeeLen;
	const amtId = "amt-" + payeeLen;
	const memoId = "memo-" + payeeLen;
	const maxId = "max-" + payeeLen;
	const addId = "add-" + payeeLen;
	const selfId = "self-" + payeeLen;
	const defaultTitle = "Enter payee #" + payeeLen + " address";

	// form input address field
	const [inputAddr, setInputAddr] = useState("");

	// form input amount field
	const [inputAmount, setInputAmount] = useState("0.0");

	// form memo field
	const [inputMemo, setInputMemo] = useState("");

	// compute max we can add for all payees
	var maxAvail = new BigNumber(0.0);
	if (props.mintTotal !== undefined && props.selectedConfig !== undefined) {
		maxAvail = maxAvail.plus(props.mintTotal).minus(
													props.selectedConfig.total);
	}

	// process a change to the amount field
	const handleAmountChange = 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 spendAmtStr = inpVal;
		var enteredAmt = new BigNumber(spendAmtStr);
		if (enteredAmt.isNegative()) {
			spendAmtStr = "0.0";
			enteredAmt = amt;
		}
		amt = amt.plus(enteredAmt);
		// get exact unrounded min
		const minAmt = BigNumber.minimum(amt, maxAvail);

		// update string quantities appropriately
		if (!minAmt.eq(enteredAmt)) {
			spendAmtStr = minAmt.toString();
		}
		setInputAmount(spendAmtStr);
	};

	// process a change to the address field
	const handleAddressChange = e => {
		if (e.target.value === '') {
			setInputAddr("");
		} else if (/^0x[0-9a-fA-F]+/.test(e.target.value)) {
			const bareAddr = web3.utils.stripHexPrefix(e.target.value);
			const addr = "0x" + bareAddr;
			if (web3.utils.isAddress(addr)) {
				setInputAddr(web3.utils.toChecksumAddress(addr));
			} else {
				alert("Invalid address, " + addr);
			}
		}
	};

	// process a change to the memo field
	const handleMemoChange = e => {
		const memo = e.target.value.toString();
		if (memo.length > MEMO_MAX) {
			alert("Memo too long, max " + MEMO_MAX + " characters");
		} else {
			setInputMemo(memo);
		}
	}

	// set the address field to self (active user account)
	const setSelf = e => {
		setInputAddr(props.userAcct);
	};

	// set the amount field to the max remaining
	const setMax = e => {
		setInputAmount(maxAvail.toString());
	};

	// shorthand
	const addr = inputAddr.substring(0, 7) + '...' + inputAddr.substring(37);
	var dispAmt = inputAmount === "0.0" ? "" : inputAmount;
	// check for entry of a series of zeros
	const zeroPat = /0*\.0*/;
	if (inputAmount.match(zeroPat)) {
		dispAmt = inputAmount;
	}

	// render line
	return (
		<tr align="center" valign="middle">
			<td>
				<InputGroup className="p-2">
					<Form.Control type="text" maxLength={45} id={addrId}
						name={addrId} value={inputAddr} placeholder="0x"
						onChange={handleAddressChange}
						title={inputAddr === '' ? defaultTitle : addr}
					/>
					<Button variant="link" className="btn-sm" id={selfId}
						title="Set this payee to your own account address"
						onClick={setSelf} role="button">
						Self
					</Button>
				</InputGroup>
			</td>
			<td>
				<InputGroup className="p-2">
					<Form.Control type="text" id={amtId} name={amtId}
						value={dispAmt}
						title="Amount to mint to this address (ethers, will be converted to wei)"
						onChange={handleAmountChange} placeholder="0"
					/>
					<Button variant="link" className="btn-sm" id={maxId}
						title="Set amount to unused mint amount" role="button"
						onClick={setMax}
					>
						Max
					</Button>
				</InputGroup>
			</td>
			<td>
				<Form.Control type="text" id={memoId} name={memoId}
					value={inputMemo} title="Memo line to send to payee"
					onChange={handleMemoChange} className="p-2"
				/>
			</td>
			<td>
				<Button variant="secondary" id={addId}
					title="Add this address and amount to the mint config"
					onClick={(addr, amt, memo) => props.onNewPayee(inputAddr, inputAmount, inputMemo)}
				>
					<Image src="images/plus-lg.svg" className="p-2"
						fluid rounded height="40" width="40" />
				</Button>
			</td>
		</tr>
	);
}

/* render a table section (two rows) showing the total eNFTs to be minted,
 * and how the color-coding is to be interpreted
 * @param props.selectedConfig the PayeeConfig currently selected
 * @param props.mintAmount the total amount the user wants to mint
 * (this object supports no changes to state)
 */
function RunningTotal(props) {
	var payeeTotal = new BigNumber(0.0);
	if (props.selectedConfig !== undefined) {
		payeeTotal = props.selectedConfig.total;
	}
	const mintTotal = props.mintAmount;
	// figure out color for total display
	var color = "text-bg-success";
	if (payeeTotal.lt(mintTotal)) {
		color = "text-bg-warning";
	} else if (payeeTotal.gt(mintTotal)) {
		color = "text-bg-danger";
	}

	return (
		<>
		<tr align="center" valign="middle">
			<th role="row" title="Total of all payees of this token type">
				Running Total:
			</th>
			<td colSpan="2">
				<Form.Control type="text" id="eNFTtotal" name="eNFTtotal"
					readOnly value={payeeTotal.toString()}
					className={"p-2 " + color} />
			</td>
			<td></td>
		</tr>
		<tr align="center" valign="middle">
			<td colSpan="4">
				<p className="text-white">
					Running Total color key -
				</p>
				<p className="text-success">
					Green: total of output eNFTs matches mint amount.
				</p>
				<p className="text-warning">
					Yellow: mint amount not fully utilized.  An implicit eNFT
					will be paid to your account address for the overage.
				</p>
				<p className="text-danger">
					Red: output eNFTs exceed mint amount (illegal!).
				</p>
			</td>
		</tr>
		</>
	);
}

/* produce a table of the eNFT payee config for the selected asset
 * @param props.selectedConfig the currently selected PayeeConfig record
 * @param props.userAcct the account address of the user
 * @param props.onNewPayee the method to define a new payee
 * @param props.onUpdPayee the method to handle a change to a SpendPayee
 * @param props.mintAmount the total amount user wants minted
 */
function PayeeTable(props) {
	let pIdx = 1;
	const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
	const hdr = "header" + nf.format(pIdx);
	const frag = "fragment" + nf.format(pIdx);

	return (
		<Fragment key={frag}>
			<Table striped bordered hover responsive variant="dark">
				<caption className="caption-top">
					{props.selectedConfig === undefined ? ""
						: props.selectedConfig.symbol}&nbsp;
					eNFT Minting Configuration:
				</caption>
				<thead>
					<tr align="center" valign="middle" key={hdr}>
						<th scope="col"
							title="The account to which the eNFT will get minted">
							Mint to Address
						</th>
						<th scope="col" title="Will be converted to wei">
							Amount in ethers (1e-18)
						</th>
						<th scope="col"
							title="Any message you want to send to the recipient">
							Memo
						</th>
						<th scope="col"><h3>+/-</h3></th>
					</tr>
				</thead>
				<tbody>
					{props.selectedConfig !== undefined && props.selectedConfig.payees.map((payee) =>
						<PayeeRenderer payeeNo={pIdx}
							key={nf.format(pIdx++)}
							payee={payee}
							userAcct={props.userAcct}
							onDelPayee={(sequence) => props.onDelPayee(sequence)}
							onUpdPayee={props.onUpdPayee}
						/>
					)}

					{ /* add new payee entry row/form */ }
					<NewPayeeRenderer key={pIdx++}
						selectedConfig={props.selectedConfig}
						onNewPayee={(address, amount, memo) => props.onNewPayee(address, amount, memo)}
						userAcct={props.userAcct}
						mintTotal={props.mintAmount}
					/>

					{ /* show running total and color-codings */ }
					<RunningTotal key={pIdx}
						mintAmount={props.mintAmount}
						selectedConfig={props.selectedConfig}
					/>
				</tbody>
			</Table>
			<br/>
		</Fragment>
	);
}

/* generate main page content
 * @param props.parseReplyMethod bound callback to process MVO replies received
 * @param props.onSelect method to switch to other pages
 * @param props.opsBlock decrypted JSON object last received from the MVO
 * @param props.active whether SmartContractSubmitter should display content
 */
function MintENFTs(props) {
	// enable use of our contracts, accounts, and Web3 connection
	const { state: { accounts, contracts, chainConn, artifacts, web3 } }
		= useEth();
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);

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

	// 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 may 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]);
		}
	}

	// the balance (ChainAsset) currently selected for minting
	const [selectedBalance, setSelectedBalance] = useState(userAssets[0]);

	// the amount to mint (max == asset.balanceOf)
	const [mintAmount, setMintAmount] = useState("0");

	/* method to select a balance from the user assets
	 * @param contract the token address (index in passed map)
	 */
	function pickSelectedBalance(contract) {
		const selBal = userAssets.find(
									elt => elt.contractAddress === contract);
		if (selBal !== undefined) {
			setSelectedBalance(selectedBalance => ({...selBal}));

			// also set the current payee config to the same contract
			pickSelectedConfig(contract);

			// reset mint amount to zero
			setMintAmount("0");

			// reset total of payee amounts
			if (selectedConfig !== undefined) handleUpdatePayee();
		} else {
			console.error("unknown balance selected, " + contract);
		}
	}

	// payee configurations for each asset type
	const [payeeConfigs, setPayeeConfigs] = useState([]);

	// the currently payee config currently selected for minting
	const [selectedConfig, setSelectedConfig] = useState(payeeConfigs[0]);

	/* method to select a config from the available list
	 * @param contract the token address
	 */
	function pickSelectedConfig(contract) {
		const selConfig = payeeConfigs.find(elt => elt.asset === contract);
		if (selConfig !== undefined) {
			if (selectedConfig !== undefined) {
				selectedConfig.active = false;
			}
			selConfig.active = true;
			setSelectedConfig(selectedConfig => ({...selConfig}));
		} else {
			// initialize a new one
			const pConfig = new PayeeConfig();
			pConfig.asset = contract;
			pConfig.active = true;
			const selBal = userAssets.find(
									elt => elt.contractAddress === contract);
			pConfig.symbol = selBal.symbol;
			if (selectedConfig !== undefined) {
				selectedConfig.active = false;
			}
			setPayeeConfigs(payeeConfigs => ([...payeeConfigs, pConfig]));
			setSelectedConfig(pConfig);
		}
	}

	/* add a new payee to the selected config
	 * @param address the payee address
	 * @param amount the amount (in ethers, as string)
	 * @param memo the memo line (if any)
	 */
	function handleAddPayee(address, amount, memo) {
		if (selectedConfig === undefined) return;
		if (address === undefined || address === '' || amount === undefined) {
			console.error("Illegal input to handleAddPayee()");
			alert("Illegal input for new payee: need valid address, amount "
				+ ">= 0, optional memo text");
			return;
		}
		// add
		const newPayee = selectedConfig.boundAddPayee(address, amount);
		newPayee.memo = memo;

		// force re-render (serves no other purpose)
		handleUpdatePayee();
	}

	/* remove a payee from the selected config
	 * @param sequence the sequence Id of the config (used as search index)
	 */
	function handleDelPayee(sequence) {
		const existing = selectedConfig.payees.find(
											elt => elt.sequence === sequence);
		if (existing !== undefined) {
			selectedConfig.boundDelPayee(existing.sequence);
		}

		// force re-render (serves no other purpose)
		handleUpdatePayee();
	}

	/* record an update to the payee in the selected config (this method is
	 * used to trigger a re-render after a change in the SpendPayee record,
	 * e.g. to make <RunningTotal/> update after a <PayeeRenderer/> updates)
	 */
	function handleUpdatePayee() {
		if (selectedConfig === undefined) return;
		// update the total in the PayeeConfig
		var payeeTot = new BigNumber(0.0);
		selectedConfig.payees.forEach(payee => {
			payeeTot = payeeTot.plus(payee.amount);
		});

		// force re-render of anything that might have changed
		setSelectedConfig({...selectedConfig, total: payeeTot});
	}

	// result of smart contract submission (used only on success)
	const [smartResult, setSmartResult] = useState("");

	/* replace the user's payee configs with a new complete list (called by
	 * SmartContractSubmitter to zero out the payees in the used PayeeConfig)
	 * @param pConfig the modified PayeeConfig
	 */
	function replacePayeeConfig(pConfig) {
		/* To prevent errors re-rendering SmartContractSubmitter, add a done
		 * flag to the passed OperationsBlock json object.
		 */
		if (props.opsBlock !== undefined) {
			props.opsBlock.done = true;
		}

		// update our copy of PayeeConfig to the passed version
		const existing = payeeConfigs.find(elt => elt.asset === pConfig.asset);
		if (existing !== undefined) {
			setPayeeConfigs(payeeConfigs.map(p => {
				if (p.asset === existing.asset) {
					// replace this one with the passed copy
					return pConfig;
				} else {
					return p;
				}
			}));
			handleUpdatePayee();
			setMintAmount("0");
		} else {
			console.error("no preexisting config for " + pConfig.symbol);
		}

		// indicate that the mint worked
		if (smartResult !== '') setSmartResult('');
		const mintSuccess = <DAlert variant="success"
							title="Your eNFT minting succeeded!"
							data="Any new eNFTs minted to your account can now be downloaded on the Home page to view details." />;
		setSmartResult(smartResult => (mintSuccess));
	}

	/* adds or updates a deposit balance to/in the array
	 * @param depConfig the ChainAsset describing the deposited balance
	 */
	function addDeposit(depConfig) {
		if (depConfig === undefined) return;
		const existing = userAssets.find(
					elt => elt.contractAddress === depConfig.contractAddress);
		if (existing !== undefined) {
			setUserAssets(userAssets.map(d => {
				if (d.contractAddress === existing.contractAddress) {
					// replace this one
					return depConfig;
				} else {
					return d;
				}
			}));
		} else {
			// add new
			setUserAssets([...userAssets, depConfig]);
		}
	}

	// method to send a signed wallet deposit minting request to an MVO
	async function sendDepositReqToMVO(resolve, reject) {
		// access msg.sender and verifying contract address
		const sender = accounts[0];
		const enshContract = contracts["EnshroudProtocol"];
		const enshAddress = enshContract.options.address;

		/* Before doing anything else, we must audit the payee config vs. the
		 * mintAmount entered.  If the requested eNFTs exceed the mint amount,
		 * reject.  Also double-check the mint amount does not exceed the
		 * available balance on deposit for the given asset.  (This may not
		 * be dispositive however since by the time the transaction is mined
		 * the balance could have changed.)
		 *
		 * If the mint amount is not fully utilized, we must gin up a new
		 * implicit payee back to the sender to catch the difference.
		 */
		const mintAmountWei
			= web3.utils.toBN(web3.utils.toWei(mintAmount.toString()));
		if (mintAmountWei.isZero() || mintAmountWei.isNeg()) {
			let amtErr = new Error("Invalid total mint amount, "
									+ mintAmount.toString());
			alert(amtErr.message);
			reject(amtErr);
			return false;
		}
		const balAvail = web3.utils.toBN(selectedBalance.balanceOf);
		if (mintAmountWei.gt(balAvail)) {
			// in theory the BN.minimum() in handleAmountChange() will stop this
			let balErr = new Error("You have asked to mint more value in eNFTs "
								+ "than you appear to have deposited.");
			alert(balErr.message);
			reject(balErr);
			return false;
		}

		// NB: these values are all in ethers, not wei
		var mintTotal = new BigNumber(0.0);
		selectedConfig.payees.forEach(payee => {
			mintTotal = mintTotal.plus(payee.amount);
		});
		// double-check total in config
		if (!mintTotal.eq(selectedConfig.total)) {
			console.error("payee config total mismatch: sum = "
						+ mintTotal.toString() + " while integral total = "
						+ selectedConfig.total);
			// we'll continue, using the mintTotal figure
		}
		if (mintTotal.gt(mintAmount)) {
			let amtErr = new Error("You have asked to mint eNFTs totaling more "
								+ "than the Mint Amount you entered.  Reduce "
							+ "the payee total or increase the Mint Amount.");
			alert(amtErr.message);
			reject(amtErr);
			return false;
		}
		var overage = new BigNumber(mintAmount).minus(mintTotal);
		if (overage.gt(0.0)) {
			// add a payee for this difference
			const overagePayee = selectedConfig.boundAddPayee(sender, overage);
			overagePayee.memo = 'auto-generated overage amount';
		}

		// the smart contract (and the MVOs) will only allow us to mint 20
		if (selectedConfig.payees.length > ARRAY_LIMIT) {
			let numErr = new Error("Sorry, the maximum number of eNFTs that "
								+ "can be minted at once is "
								+ ARRAY_LIMIT + ".");
			alert(numErr.message);
			reject(numErr);
			return false;
		}

		// examine passed MVO configuration to ensure it's been downloaded
		const mvoConfig = chainConn.MVOConf;
		const chId = chainConn.chainConfig.chainId;
		if (mvoConfig.availableMVOs.length === 0) {
			let inputErr = new Error("No MVOs listed for chainId " + chId
					+ "; is " + chainConn.chainConfig.chain + " connected?");
			alert(inputErr.message);
			reject(inputErr);
			return false;
		}

		// obtain secure communicator to randomly selected MVO for this chain
		const mvoComm = mvoConfig.getMVOCommunicator('deposit', true);
		if (!(mvoComm instanceof MVOComm)) {
			let mvoErr = new Error("Could not select an MVO");
			alert(mvoErr.message);
			reject(mvoErr);
			return false;
		}

		/* assign salt values to all payees that don't have them, and build
		 * the payee list as a json array string (to send in non-encrypted case)
		 * and as a JSON array object (for encrypted case)
		 */
		var payeeIdx = 1;
		const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
		var payeeJson = '[';
		var payeeArray = [];
		selectedConfig.payees.forEach(payee => {
			// NB: 16 bytes of base64Url will be 22 chars
			if (payee.rand.length !== 22) {
				// either doesn't exist or improper
				payee.boundSetRand();
			}
			// add this payee to the JSON list
			let payeeAmt = web3.utils.toWei(payee.amount.toString());
			let payeeIdent = "payee" + nf.format(payeeIdx);
			let payeeStr = '{"payeeLabel":"' + payeeIdent + '","payeeSpec":"'
						+ '{"address":"'
						+ payee.address + '","amount":"' + payeeAmt
						+ '","units":"","rand":"' + payee.rand + '","memo":"'
						+ payee.memo + '"}}';
			if (payeeIdx < selectedConfig.payees.length) {
				// add a comma except on last one
				payeeStr += ',';
			}
			payeeJson += payeeStr;

			// do same thing with objects for the array
			const payeeDetails = {	address: payee.address,
									amount: payeeAmt,
									units: '',
									rand: payee.rand,
									memo: payee.memo };
			const payeeObj
				= {	payeeLabel: payeeIdent, payeeSpec: payeeDetails	};
			payeeArray.push(payeeObj);
			payeeIdx++;
		});
		payeeJson += ']';

		// generate reply key and the payload we must sign
		var replyKey = '';
		var payload = '';
		const totalAmt = web3.utils.toWei(mintAmount.toString());
		if (!mvoComm.encrypted) {
			// old version, for use without encryption (passed as POST param)
			payload = 'depositspec={"chainId":"' + chId + '","opcode":"deposit"'
						+ ',"sender":"' + sender + '","asset":"'
						+ selectedBalance.contractAddress + '","amount":"'
						+ totalAmt + '","payees":' + payeeJson + ',"signature":'
						// NB: in unencrypted case signature is never verified
						+ '"130 character signature string goes here"}';

			// send plain data unencrypted and unsigned
			mvoComm.sendToMVO(payload, props.parseReplyMethod);
			resolve(true);
		}
		else {
			// generate an AES key for the MVO to use to encrypt normal replies
			mvoComm.generateAesKey();
			// NB: generateAesKey() stored raw key in mvoComm.replyKeyB64
			replyKey = mvoComm.decryptKey;

			// define eth_signTypedData_v4 parameters
			const msgParams = JSON.stringify({
				// EIP-712 domain info (depends upon chId scan URL for display)
				domain: {
					chainId: chId,
					name: 'Enshroud',
					verifyingContract: enshAddress,
					version: '1',
				},
				// descriptive info on what's being signed and for whom
				message: {
					contents: 'Send encrypted request to mint eNFTs from your deposit balance',
					to: {
						MVOId: mvoComm.mvo,
						URL: mvoComm.mvoURL,
					},
					requestJson: {
						depositspec: {
							chainId: `${chId}`,
							opcode: 'deposit',
							sender: sender,
							asset: selectedBalance.contractAddress,
							amount: totalAmt,
							payees: payeeArray,
							replyKey: replyKey,
						},
					},
				},
				primaryType: 'Request',
				types: {
					// the domain the contract is hosted on
					EIP712Domain: [
						{ name: 'chainId', type: 'uint256' },
						{ name: 'name', type: 'string' },
						{ name: 'verifyingContract', type: 'address' },
						{ name: 'version', type: 'string' },
					],
					// refer to primaryType
					Request: [
						{ name: 'contents', type: 'string' },
						{ name: 'to', type: 'MVO' },
						{ name: 'requestJson', type: 'DepositSpec' },
					],
					// not an EIP712Domain definition
					MVO: [
						{ name: 'MVOId', type: 'string' },
						{ name: 'URL', type: 'string' },
					],
					// not an EIP712Domain definition
					DepositSpec: [
						{ name: 'depositspec', type: 'Payload' },
					],
					// not an EIP712Domain definition
					Payload: [
						{ name: 'chainId', type: 'string' },
						{ name: 'opcode', type: 'string' },
						{ name: 'sender', type: 'address' },
						{ name: 'asset', type: 'address' },
						{ name: 'amount', type: 'string' },
						{ name: 'payees', type: 'Payee[]' },
						{ name: 'replyKey', type: 'string' },
					],
					// not an EIP712Domain definition
					Payee: [
						// NB: smart contract supports max of 20 eNFTs at once
						{ name: 'payeeLabel', type: 'string'},
						{ name: 'payeeSpec', type: 'PayeeSpec'},
					],
					// not an EIP712Domain definition
					PayeeSpec: [
						{ name: 'address', type: 'address' },
						{ name: 'amount' , type: 'string' },
						{ name: 'units', type: 'string' },
						{ name: 'rand', type: 'string' },
						{ name: 'memo', type: 'string' },
					],
				},
			});
			const method = 'eth_signTypedData_v4';
			var params = [sender, msgParams];

			// now obtain signature on params in a EIP-712 compatible way
			var userSig;
			await web3.currentProvider.sendAsync(
				{
					method,
					params,
					from: sender,
				},
				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;
					}

					// append signature to the arguments
					const allArgs = JSON.parse(msgParams);
					allArgs.signature = userSig;

					// encrypt + send the message to the MVO, passing callback
					mvoComm.sendToMVO(JSON.stringify(allArgs),
									  props.parseReplyMethod);
					// NB: even if sendToMVO() fails, we still want to resolve
					resolve(true);
				}
			);
		}
		return true;
	}

	// create minting form
	const mintENFTs =
		<>
			<div className="container">
				<h2 align="center">Mint eNFTs from Deposited Balance</h2>
				<br/><br/>

				{ /* show table and controls of input deposit balances */ }
				<DepositTable
					userAssets={userAssets}
					onNewUserAssets={(configList) => handleAddUserAssets(configList)}
					selectedBalance={selectedBalance}
					selectBalance={(contract) => pickSelectedBalance(contract)}
					mintAmount={mintAmount}
					setMintAmount={(amount) => setMintAmount(amount)}
					onSelect={(page) => props.onSelect(page)}
				/>
				<br/><br/>
				<hr/>
				<br/>

				{ /* show summary in middle */ }
				<div align="center">
					<h4>Mint Summary</h4>
					<Form>
					<Form.Group as={Row} className="mb-3">
						<Form.Label column sm="2" htmlFor="selAsset">
							Asset to Mint:
						</Form.Label>
						<Col sm="10">
							<Form.Control readOnly type="text" id="selAsset"
								value={selectedBalance === undefined ? "" : selectedBalance.symbol}
							/>
						</Col>
					</Form.Group>
					<Form.Group as={Row} className="mb-3">
						<Form.Label column sm="2" htmlFor="totAmt">
							Total Mint Amount (in wei):
						</Form.Label>
						<Col sm="10">
							<Form.Control type="text" readOnly id="totAmt"
								value={web3.utils.toWei(mintAmount.toString())}
							/>
						</Col>
					</Form.Group>
					</Form>
				</div>
				<br/>
				<hr/>
				<br/><br/>

				{ /* show table and inputs of eNFT output payees */ }
				<PayeeTable
					selectedConfig={selectedConfig}
					selectedBalance={selectedBalance}
					mintAmount={mintAmount}
					onNewPayee={(address, amount, memo) => handleAddPayee(address, amount, memo)}
					onDelPayee={(sequence) => handleDelPayee(sequence)}
					onUpdPayee={handleUpdatePayee}
					userAcct={userAcct}
				/>
				<br/>
				
				{ /* add Configure button to send to MVO */ }
				<Container fluid>
				<h4>
					<LoadingButton variant="primary" buttonStyle="m-3"
						buttonTitle="This sends the mint config to an MVO for encryption"
						buttonText="Configure Mint"
						buttonIcon="images/send-check.svg"
						netMethod={(resolve, reject) => sendDepositReqToMVO(resolve, reject)}
					/>
					<i>(signature required)</i>
				</h4>
				<p className="text-muted">
					Note: Your wallet signature is required for authentication
					to the MVO.<br/>
					However no on-chain action occurs during the configuration
					step.<br/>
					You will also review and sign the encrypted transaction
					again before submitting it on-chain for mining.  (See the
					Smart Contract Submission Preview area below.)
				</p>
				</Container>
				<br/>
			</div>

			{ /* Show the result of the last successful smart contract
			   * submission.  Note this will not be visible once the user has
			   * closed the DAlert.
			   */ }
			<br/>
			{smartResult}
			<br/>

			{ /* show MVO results and prompt for smart contract submittal */ }
			<h4>Smart Contract Submission Preview</h4>
			<SmartContractSubmitter
				opsBlock={props.opsBlock}
				active={props.active}
				opcode='deposit'
				payeeConfigs={payeeConfigs}
				resetConfigs={(config) => replacePayeeConfig(config)}
				deposits={userAssets}
				onNewDeposit={(dep) => addDeposit(dep)}
			/>
			<br/>
		</>;
	
	return (
		<div id="MintENFTs">
		{
			!artifacts.EnshroudProtocol ? <NoticeNoArtifact /> :
			contracts == null ||
					!contracts["EnshroudProtocol"] ? <NoticeWrongNetwork /> :
				mintENFTs
		}
		</div>
	);
}

export default MintENFTs;
