/*
 * last modified---
 * 	08-14-26 abandon use of getPastEvents() due to RPC throttling; use config
 * 	06-06-26 add id to Add Token button
 * 	03-24-25 fix case where assetList doesn't update if Network button not used
 * 	04-30-24 debug; show contract.symbol when added to table
 * 	09-11-23 use Contract.getPastEvents()
 * 	08-01-23 rework to key off DepositERC20 events as well as prefab token confs
 * 	07-20-23 new
 *
 * purpose---
 * 	display configured assets plus fee rates and deposited balances
 */

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


/* render the details for an asset
 * @param props.assetConf the asset configuration (a ChainAsset)
 * @param props.scanURL the URL for etherscan.io or its equivalent on this chain
 */
function AssetDetails(props) {
	const url = `${props.scanURL}/address/${props.assetConf.contractAddress}`;
	const aConf = props.assetConf;
	// skip native token
	if (aConf.method === 'native') return;

	var balance = 0;
	if (typeof(aConf.balanceOf) !== 'number') {
		// must be raw value; scale down by 1e18
		const bnBal = new BigNumber(aConf.balanceOf.toString());
		balance = bnBal.dividedBy("1e18").valueOf();
	} else {
		// as-is should display fine
		balance = aConf.balanceOf;
	}

	// render the row
	return (
		<tr align="center" valign="middle">
			<td>
				<a href={url} target="_blank" rel="noreferrer noopener">
					{aConf.contractAddress}
				</a>
			</td>
			<td>{aConf.symbol}</td>
			<td>{balance}</td>
			<td>{aConf.depositFee}</td>
			<td>{aConf.withdrawFee}</td>
		</tr>
	);
}

/* render the whole table of asset details
 * @param props.assetConfigs the set of known (and possibly modified) configs
 * @param props.scanURL the URL for etherscan.io or equivalent on this chain
 */
function AssetTable(props) {
	// NB: safety measure in case chain assets had same length and not updated
	if (!(props.assetConfigs instanceof Array)) {
		alert("Stale asset config.  Please use only the Network dropdown button to switch between chains.");
		return (<tbody></tbody>);
	}

	// render body of table
	let aIdx = 1;
	return (
		<tbody>
			{props.assetConfigs.map((assetConf) =>
				<AssetDetails key={aIdx++}
					assetConf={assetConf}
					scanURL={props.scanURL}
				/>
			)}
		</tbody>
	);
}

/* method to display entry form for an entirely new token asset
 * @param props.onNewAsset method to call when a new asset successfully added
 * @param props.assetConfigs all existing previously used chain assets
 * @param props.downloaded true if we have used the download button already
 */
function AddTokenAsset(props) {
	// enable use of our web3 connection
	const { state: { accounts, chainConn, web3 } } = useEth();
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);

	// track state of input field
	const [tokenContract, setTokenContract] = useState("");

	// process a token adress change
	const handleAssetAddressChange = e => {
		if (/^0x[0-9a-fA-F]+/.test(e.target.value)) {
			setTokenContract(e.target.value);
		}
	}

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

	// 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;
	};

	// validate a new asset and add it to list if it passes checks
	const processAsset = async () => {
		// sanity check input
		if (!web3.utils.isAddress(tokenContract)) {
			alert("Illegal token contract, \"" + tokenContract + "\"");
			return;
		}

		// check that this contract has a "public symbol" getter
		var symbol = await getTokenSymbol(tokenContract);
		if (symbol === undefined || symbol === '' || symbol === '0x') {
			alert("Contract does not appear to be ERC20-compatible");
			return;
		}

		// add to the asset config for the chain, if not already present
		const assetList = props.assetConfigs;
		const found
			= assetList.some(elt => elt.contractAddress === tokenContract);
		if (found) {
			alert("Contract is already in the list");
		} else {
			var assetConf = new ChainAsset();
			assetConf.contractAddress = tokenContract;
			assetConf.symbol = symbol;
			// NB: method defaults to 'tokens'

			// get tokenContract.nonces(userAcct) and see if it's >= 0
			const permitNonce = getNonce(tokenContract);
			if (permitNonce >= 0) {
				// this contract appears to permit ERC20Permit/ERC-2612
				assetConf.method = 'permit';
			}

			// new assets will have default deposit and withdraw fees
			assetConf.depositFee = chainConn.chainConfig.defaultDepFee;
			assetConf.withdrawFee = chainConn.chainConfig.defaultWithFee;

			// add to chain's config so it will also show up on Deposits page
			chainConn.chainConfig.addAsset(assetConf);

			// tell React so table updates and reset form input
			props.onNewAsset(assetConf);
			setTokenContract("");
		}
	};

	// render new asset entry form
	return (
		<Fragment key="addAssetForm">
			<h5 className="text-lead m-3">
				To add new custom assets to the list, enter a new
				contract address below.  The token must be ERC-20, ERC-777, or
				ERC-4626 compatible.
				<br/><br/>
				You must use the Refresh Assets button once before adding a
				fresh token, to insure it's not already in the list.
				Doing this will unlock the Add Token button.
				<br/><br/>
				Please note that market price feeds exist only for canonical
				token addresses, so pricing data may not be available on this
				site for some added tokens.
				<br/><br/>
			</h5>
			<br/>
			<Form>
				<Form.Group className="mb-3">
					<Form.Label htmlFor="newAssetId">
						Token contract adress:
					</Form.Label>
					<Form.Control type="text" placeholder="0x"
						value={tokenContract} id="newAssetId"
						onChange={handleAssetAddressChange}
					/>
				</Form.Group>
				<Button variant="secondary" className="m-3 btn-large"
					title="Configure the entered token contract address as a usable asset in Enshroud"
					onClick={() => processAsset()}
					disabled={!props.downloaded} id="newAssetButton"
				>
					Add Token
					<Image src="images/bag-plus-fill.svg" fluid rounded
					 className="p-2" height="40" width="40"/>
				</Button>
			</Form>
		</Fragment>
	);
}

/* render the asset configurations page and input controls
 * @param props.onSelect method to shift to other pages
 */
function AssetPage(props) {
	const { state: { accounts, contracts, chainConn, artifacts, web3 } }
		= 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";
	}

	// enter all preset ChainAsset configs into initial array
	function createInitialAssetList() {
		const initialAssets = [].concat(chainConn.chainConfig.assetList);
		return initialAssets;
	}
	const [chainAssetConfigs, setChainAssetConfigs]
		= useState(createInitialAssetList);

	/* 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 !== chainAssetConfigs.length) {
		setChainAssetConfigs([].concat(chainConn.chainConfig.assetList));
	}
	
	// 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 = chainAssetConfigs.find(
					elt => elt.contractAddress === assetConfig.contractAddress);
			if (existing === undefined) {
				// add this one to the list
				addedList.push(assetConfig);
			} else {
				// we need to update the balance and fee settings
				setChainAssetConfigs(
					chainAssetConfigs => (chainAssetConfigs.map(c => {
					if (c.contractAddress === existing.contractAddress) {
						// replace with the modified one
						return assetConfig;
					} else {
						return c;
					}
				})));
			}
		});

		// add new ones all at once to prevent unnecessary re-renderings
		if (addedList.length > 0) {
			setChainAssetConfigs(
				chainAssetConfigs => ({...chainAssetConfigs, ...addedList}));
		}
	}

	// method to add a single new chain asset by replacing state
	function handleAddChainAsset(config) {
		// add only if not found in the current list already (double-check)
		const found = chainAssetConfigs.some(
						elt => elt.contractAddress === config.contractAddress);
		if (!found) {
			setChainAssetConfigs([...chainAssetConfigs, config]);
		} else {
			console.error("New asset " + config.symbol
						+ " was already in chainAssetConfigs list");
		}
	}

	// indicator whether Refresh of deposit events has been done
	const [downloadDone, setDownloadDone] = useState(false);

	// get deposit fee for the given asset
	const getDepositFee = async (tokenAddr) => {
		var depFee = 0.0;
		const dFee = await enshProtoContract.methods.assetDepositFee(tokenAddr)
			.call({ from: userAcct });
		let numFee = web3.utils.fromWei(dFee, 'milliether');
		let pct = +numFee / 10.0;
		if (pct !== 0.0) {
			// convert to percent
			depFee = pct;
		} else {
			// fallback to default
			depFee = chainConn.chainConfig.defaultDepFee;
		}
		return depFee;
	};

	// get withdraw fee for the given asset
	const getWithdrawFee = async (tokenAddr) => {
		var withFee = 0.0;
		const wFee = await enshProtoContract.methods.assetWithdrawFee(tokenAddr)
			.call({ from: userAcct });
		let numFee = web3.utils.fromWei(wFee, 'milliether');
		let pct = +numFee / 10.0;
		if (pct !== 0.0) {
			// convert to percent
			withFee = pct;
		} else {
			// fallback to default
			withFee = chainConn.chainConfig.defaultWithFee;
		}
		return withFee;
	};

	/* get balance of EnshroudProtocol contract in the given asset
	 * @param tokenAddr the token contract address to send to
	 * @return the balance of our contract in that contract (as hex string)
	 */
	const checkProtocolBalance = async (tokenAddr) => {
		var epBal = '';
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'balanceOf',
			type: 'function',
			constant: true,
			inputs: [{
				type: 'address',
				name: ''
			}],
			outputs: [{
				type: 'uint256'
			}]
		}, [enshProtoAddr]);
		await web3.eth.call({
			to: tokenAddr,	// erc20/erc777/erc4626 contract address
			from: userAcct,
			data: callData	// function call encoding
		})
			.then(protoBal => {
				epBal = protoBal;
			}).catch(err => {
				console.error("error fetching EP bal at " + tokenAddr + ": "
							+ err);
			});
		return epBal;
	};

	// obtain the set of all assets ever deposited on this chain, and by user
	const buildAssetConfig = async (resolve, reject) => {
		// init with all preconfigured assets (or old list from previous run)
		var protocolAssets = [].concat(chainAssetConfigs);

		// now fetch the contract's current deposited balance in every asset
		for await (const assetConfig of protocolAssets) {
			if (assetConfig.contractAddress
				=== '0x0000000000000000000000000000000000000000'
				|| assetConfig.method === 'native')
			{
				continue;
			}
			const contractBal
				= await checkProtocolBalance(assetConfig.contractAddress);
			if (contractBal === '0x') {
				// this implies the contract address is invalid on this chain
				assetConfig.balanceOf = 0n;
				continue;
			}
			// convert amount from hex to uint256 in decimal
			var amt = new BigNumber(contractBal, 16);
			assetConfig.balanceOf = amt.toFixed();

			// obtain the deposit fee and configure in record
			var depFee = await getDepositFee(assetConfig.contractAddress);
			assetConfig.depositFee = depFee;

			// obtain the withdraw fee and configure in record
			var withFee = await getWithdrawFee(assetConfig.contractAddress);
			assetConfig.withdrawFee = withFee;
		}

		// now tell React we have all the data
		handleAddChainAssets(protocolAssets);
		setDownloadDone(true);
		// also update in Enshroud's state, except for balances
		const allChainAssets = [];
		chainAssetConfigs.forEach(chainAsset => {
			const asset = new ChainAsset();
			asset.contractAddress = chainAsset.contractAddress;
			asset.symbol = chainAsset.symbol;
			asset.method = chainAsset.method;
			asset.depositFee = chainAsset.depositFee;
			asset.withdrawFee = chainAsset.withdrawFee;
			asset.wrapsTo = chainAsset.wrapsTo;
			asset.priceFeed = chainAsset.priceFeed;
			allChainAssets.push(asset);
		});
		chainConn.chainConfig.assetList = allChainAssets;
		resolve(true);
	};

	// render the Asset Config page
	const assetConfig =
	<div className="container">
		<Container fluid align="left">
			<h2>Asset Configuration</h2>
			<br/>
			<Card>
				<Card.Body>
					<Card.Title>Managing the Asset Config</Card.Title>
					<Card.Text>
						This page shows the token assets supported by Enshroud
						on this blockchain.
						In each case the symbol and contract
						address are shown, along with the deposit and withdraw
						fees assessed.  (Please verify contract addresses!)
					</Card.Text>
					<Card.Text>
						The "Enshroud Total" column represents the amount of
						each token (shown in ethers/1e-18 units) which has been
						deposited into the EnshroudProtocol contract
						({enshProtoAddr}).  This amount is either backing
						circulating eNFTs, or owned by users as deposited
						balances not yet minted into eNFTs.  <b>Note</b>: all
						native tokens deposited were auto-converted to a wrapped
						form (e.g. <i>ETH</i> became <i>WETH</i>).
					</Card.Text>
					<Card.Text>
						The table is seeded with various common token assets
						suggested by the Enshroud team.
						To populate the table with actual balances,
						use the <b>Refresh Assets</b> button.  This will
						obtain a list of all current balances and fee settings.
					</Card.Text>
					<Card.Text>
						You may add additional custom tokens to the table
						by using the form below.  By doing this, these assets
						will become available for you to use on the Deposit
						and Mint pages.  Assets without a specific deposit or
						withdraw fee determined by the EnshroudDAO will be
						subject to the default fees
						of {chainConn.chainConfig.defaultDepFee}% and {chainConn.chainConfig.defaultWithFee}%, respectively.
					</Card.Text>

					<LoadingButton variant="primary"
						netMethod={(resolve, reject) => buildAssetConfig(resolve, reject)}
						buttonText="Refresh Assets"
						buttonTitle="Populate table based on prior deposit events"
						buttonIcon="images/download.svg"
					/>
				</Card.Body>
			</Card>
		</Container>

		{ /* table for assets listed */ }
		<Container fluid>
			<br/>
			<Table striped bordered hover responsive>
				<caption className="caption-top">
					Configured Common Token Assets and Statistics
				</caption>
				<thead>
					<tr align="center" key="hdrDefinedAssets">
						<th scope="col">Asset (Contract)</th>
						<th scope="col">Symbol</th>
						<th scope="col">Enshroud Total (1e-18)</th>
						<th scope="col">Deposit Fee (%)</th>
						<th scope="col">Withdraw Fee (%)</th>
					</tr>
				</thead>

				{ /* this supplies the <tbody/> */ }
				<AssetTable assetConfigs={chainAssetConfigs}
					scanURL={chainConn.chainConfig.scanURL}
					onSelect={(page) => props.onSelect(page)}
				/>
			</Table>
			<br/><br/>

			{ /* support adding a new asset to config */ }
			<AddTokenAsset
				onNewAsset={handleAddChainAsset}
				assetConfigs={chainAssetConfigs}
				downloaded={downloadDone}
			/>
		</Container>
	</div>

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

export default AssetPage;
