/*
 * last modified---
 * 	11-28-23 detect wallet provider not installed
 * 	10-10-23 turn on web3.eth.handleRevert
 * 	08-04-23 initialize chain's defaultDepFee and defaultWithFee inside init()
 * 	07-19-23 move downloadMVOconfig() here from <EnshNavbar>
 * 	07-17-23 add ChainConnection usage
 * 	07-14-23 force page reload on chain or accounts change
 *
 * purpose---
 * 	provide embeddable generic Web3 provider, with our deployed contracts
 */

import React, { useReducer, useCallback, useEffect } from "react";
import Web3 from "web3";
import EthContext from "./EthContext";
import { reducer, actions, initialState } from "./state";
import ChainConnection from "../ChainConnection.js";
import { MVOStaking } from '../MVOs.js';

function EthProvider({ children }) {
	const [state, dispatch] = useReducer(reducer, initialState);

	const init = useCallback(async (artifacts, chainConn) => {
		if (artifacts) {
			// detect whether wallet provider is installed
			if (typeof window.ethereum === 'undefined') {
				console.error("No Web3 wallet provider is installed");
				alert("No Web3 wallet provider found, please install one");
				return;
			}

			const web3 = new Web3(Web3.givenProvider || "ws://localhost:8545");
			// activate processing of revert reasons from the EVM
			web3.eth.handleRevert = true;
			const chainID = await web3.eth.getChainId();
			var accounts = [];
			try {
				accounts = await web3.eth.requestAccounts();
			} catch (err) {
				console.error(err.message);
				alert(err.message
					+ " Enshroud dApp functionality unavailable.");
				return;
			}
			var contracts = {};
			try {
				for (const [contractName, artifact]
					 of Object.entries(artifacts))
				{
					const artiNetwork = artifact.networks[chainID];
					var address = undefined;
					var contract = null;
					if (artiNetwork === undefined) {
						console.warn("warning: contract " + contractName
									+ " not deployed on chainId " + chainID);
					}
					else {
						address = artiNetwork.address;
						contract = new web3.eth.Contract(artifact.abi, address);
					}
					contracts[contractName] = contract;
				}

				/* NB: not all contracts will be deployed on any given chain.
				 * 	   However EnshroudProtocol and MVOStaking must be.  The
				 * 	   MVOStaking will not support actual staking except on Eth
				 * 	   mainnet.  DAOPool and TimelockManager will be deployed 
				 * 	   only on Eth mainnet.  The EnshroudToken and Crowdsale
				 * 	   contracts must be deployed on Eth mainnet, and the token
				 * 	   may be available elsewhere if bridging is implemented.
				 */
				// ensure EnshroudProtocol is deployed on this chain
				const enshProtoContract = contracts["EnshroudProtocol"];
				const mvoStakingContract = contracts["MVOStaking"];
				const userAcct = accounts[0];
				if (enshProtoContract === null) {
					alert("EnshroudProtocol contract not deployed on this "
						+ "chain; Enshroud dApp functionality unavailable");
					// try this nevertheless:
					if (chainConn.selectChain(chainID)) {
						console.log("Without EnshroudProtocol, set chain = "
									+ chainID);
					} else {
						chainConn.chainConfig.chain = `ID: ${chainID}`;
					}
				// ensure MVOStaking is deployed on this chain
				} else if (mvoStakingContract === null) {
					alert("MVOStaking contract not deployed on this chain; "
						+ "many functions will not be available");
					// try this nevertheless:
					if (chainConn.selectChain(chainID)) {
						console.log("Without MVOStaking, set chain = "
									+ chainID);
					} else {
						chainConn.chainConfig.chain = `ID: ${chainID}`;
					}
				} else {
					// switch the chain config
					if (chainConn.selectChain(chainID)) {
						// attempt to download the MVO config for this chain

						// first get the number of MVOs defined on this contract
						const mvoCnt
							= await mvoStakingContract.methods.mvoIndex().call(
															{ from: userAcct });

						// next get the list of MVO Ids
						var mvoIds = [];
						for (let iii = 0; iii < mvoCnt; iii++) {
							const mvoId = await mvoStakingContract.methods
										.mvoIds(iii).call({ from: userAcct });
							if (mvoId !== undefined) {
								mvoIds.push(mvoId);
							}
						}

						// then get the actual staking records
						var mvoRecords = [];
						for await (const mId of mvoIds) {
							const mvoRec = await mvoStakingContract.methods
										.idToMVO(mId).call({ from: userAcct });
							if (mvoRec !== undefined) {
								mvoRecords.push(mvoRec);
							} else {
								console.error("No staking found for MVOId "
											+ mId);
							}
						}

						// now create MVOStaking objects for each record found
						const mvoConfig = chainConn.MVOConf;
						var mvoStakings = [];
						var stakingTotal = 0;
						if (mvoRecords.length > 0) {
							for (let jjj = 0; jjj < mvoRecords.length; jjj++) {
								const mvoRec = new MVOStaking();
								mvoRec.setMVOId(mvoIds[jjj]);
								mvoRec.config(mvoRecords[jjj]);
								mvoStakings.push(mvoRec);
								stakingTotal += (mvoRec.stakingQuantum / 1e18);
							}
							mvoConfig.initialize(chainConn.chainConfig.chainId,
												 mvoStakings, stakingTotal);
						}
						else {
							console.error("Unable to initialize MVOConf for "
											+ chainConn.chainConfig.chain);
							alert("Error downloading MVO configs for "
								+ chainConn.chainConfig.chain);
						}

						// obtain the deposit fee for (address)0 [default]
						enshProtoContract.methods.assetDepositFee(
								"0x0000000000000000000000000000000000000000")
							.call({ from: userAcct })
							.then(fee => {
								let dFee
									= web3.utils.fromWei(fee, 'milliether');
								let pct = +dFee / 10.0;
								chainConn.chainConfig.defaultDepFee = pct;
							})
							.catch(err => console.error(
								"Error fetching default deposit fee, " + err));

						// obtain the withdraw fee for (address)0 [default]
						enshProtoContract.methods.assetWithdrawFee(
								"0x0000000000000000000000000000000000000000")
							.call({ from: userAcct })
							.then(fee => {
								let wFee
									= web3.utils.fromWei(fee, 'milliether');
								let pct = +wFee / 10.0;
								chainConn.chainConfig.defaultWithFee = pct;
							})
							.catch(err => console.error(
								"Error fetching default withdraw fee, " + err));
					} else {
						throw new Error("No config for chainID " + chainID);
					}
				}
			} catch (err) {
				// handle any uncaught errors
				contracts = null;
				console.error(err);
				alert(err.message);
			}
			dispatch({
			  type: actions.init,
			  data: { artifacts, web3, accounts, chainID, contracts, chainConn }
			});
		}
	}, []);

	useEffect(() => {
	const tryInit = async () => {
	  try {
		// init all 6 of our contracts
		const artifacts = {
			EnshroudToken: require("../artifacts/EnshroudToken.json"),
			EnshroudProtocol: require("../artifacts/EnshroudProtocol.json"),
			MVOStaking: require("../artifacts/MVOStaking.json"),
			DAOPool: require("../artifacts/DAOPool.json"),
			TimelockManager: require("../artifacts/TimelockManager.json"),
			Crowdsale: require("../artifacts/Crowdsale.json")
		};
		const chainConn = new ChainConnection();
		chainConn.initConfigs();
		await init(artifacts, chainConn);
	  } catch (err) {
		console.error(err);
	  }
	};

	tryInit();
	}, [init]);

	useEffect(() => {
		const events = ["chainChanged", "accountsChanged"];
		const handleChange = async (ev) => {
			await init(state.artifacts, state.chainConn);
			// force reload of the page
			//window.location.reload();
		};

		events.forEach(e => window.ethereum.on(e, handleChange));
		return () => {
			events.forEach(e => window.ethereum.removeListener(e,
															   handleChange));
		};
	}, [init, state.artifacts, state.chainConn]);

	// render
	return (
		<EthContext.Provider value={{
			state,
			dispatch
		}}>
			{children}
		</EthContext.Provider>
	);
}

export default EthProvider;
