contract YW_Finance_P1 is Ownable {
	using SafeMath for uint256;
	using SafeERC20 for IERC20;

	/**
	 * Stake section information struct
	 */
	struct Stake {
		address stakeholder;
		uint256 createdAt;
		uint256 stakeAmount;
		uint256 lastClaimAt;
		uint256 canUnlockAt;
	}

	struct StakePeriod {
		uint256 timestamp;
		uint256 stakeTotalAmount;
	}

	// Pool support tokens
	address public _stakeTokenAddress;
	// IERC20 interfaces of support tokens
	IERC20 internal _stakeToken;
	// Pool stakeholders
	address[] public _stakeholders;
	mapping(address => Stake) internal _userStakes;

	// Pool reward contract and IERC20 interface
	address public _rewardTokenAddress;
	IERC20 internal _rewardToken;
	// The accumulated rewards for each stakeholder.
	mapping(address => uint256) internal _rewards;
	mapping(address => uint256) internal _paidRewards;

	// 0% per transaction. Set later
	uint256 public _rewardClaimFee = 0;
	uint256 public _transactionFeeInWei = 20000000000000000; // 0.02 ETH
	uint256 public _stakeDepositFee = 1;

	// Pool rewards
	uint256 internal _poolTotalStake = 0;
	StakePeriod[] public _stakePeriods;
	uint256 internal _poolTotalReward = 0;
	uint256 internal _poolClaimedReward = 0;
	// Last time the pool distribute rewards to all stakeholders
	uint256 public _poolStartedAt = 0; // In date time
	uint256 public _poolRewardDistributionStartedAt = 0;
	uint256 public _poolRunDuration = 0; // In seconds
	uint256 public _poolEndedAt = 0; // In seconds
	uint256 public _poolRewardPerSecondRate = 0; // In seconds
	uint256 public _poolDepositFeeAmount = 0;

	// Pool owner
	address public pool;
	uint256 public poolDeployedAt = 0;

	// Pool developers
	mapping(address => bool) internal _developers;
	mapping(address => bool) internal _systemPools;

	constructor(
		address rewardTokenAddress,
		address stakeTokenAddress,
		uint256 poolStartedAt,
		uint256 poolRunDurationInHours,
		uint256 initRewardAmount
	) {
		_rewardTokenAddress = rewardTokenAddress;
		_rewardToken = IERC20(rewardTokenAddress);

		_stakeTokenAddress = stakeTokenAddress;
		_stakeToken = IERC20(stakeTokenAddress);

		// Current block
		poolDeployedAt = block.timestamp;
		// Current contract owner & pool
		pool = address(this);
		_poolStartedAt = poolStartedAt;
		_poolRunDuration = 60 * 60 * poolRunDurationInHours;
		_poolEndedAt = _poolStartedAt.add(_poolRunDuration);
		_poolTotalReward = _poolTotalReward.add(initRewardAmount);
		_poolRewardPerSecondRate = _poolTotalReward.div(_poolRunDuration);
	}

	event TransferSuccessful(
		address indexed _from,
		address indexed _to,
		uint256 _amount
	);

	event StakeSuccessful(
		address indexed _stakeholder,
		uint256 _amount,
		uint256 _timestamp
	);

	event RewardClaimSuccessful(
		address indexed _stakeholder,
		uint256 _amount,
		uint256 _timestamp
	);

	/**
	 * @notice Trigger to update stakeholder reward
	 * @param stakeholder Address of stakeholder to update reward
	 */
	modifier updateReward(address stakeholder) {
		uint256 amount = 0;
		(uint256 currentReward, , , ) = rewardOf(stakeholder);
		_rewards[stakeholder] = currentReward;
		_;
	}

	/**
	 * @notice Trigger to ensure transaction fee is paid
	 * @param amount Amount of current value of user transaction
	 */
	modifier validTransactionFee(uint256 amount) {
		require(amount >= _transactionFeeInWei, "Missing transaction fee.");
		_;
	}

	/**
	 * @notice Trigger to ensure pool start before user can stake
	 */
	modifier ensurePoolStart() {
		require(block.timestamp > _poolStartedAt, "Pool is not open yet.");
		_;
	}

	/**
	 * @notice Trigger to ensure pool available
	 */
	modifier poolAvailable() {
		require(block.timestamp < _poolEndedAt, "Pool is ended.");
		_;
	}

	/**
	 * @notice A method to set reward claim fee
	 * @param _fee Reward claim fee need to set
	 */
	function setRewardClaimFee(uint256 _fee) public onlyOwner {
		require(_fee > 0, "Reward claim fee must be none zero value.");
		_rewardClaimFee = _fee;
	}

	/**
	 * @notice A method to set transaction fee
	 * @param _fee The transaction fee to set
	 */
	function setTransactionFeeInWei(uint256 _fee) public onlyOwner {
		require(_fee > 0, "Reward claim fee must be none zero value.");
		_transactionFeeInWei = _fee;
	}

	/**
	 * @notice A method to add developer address to pool
	 * @param _address Developer wallet address
	 * @return bool True/False as the result
	 */
	function addPoolDeveloper(address _address)
		public
		onlyOwner
		returns (bool)
	{
		_developers[_address] = true;
		return true;
	}

	/**
	 * @notice A method remove developer wallet from pool
	 * @param _address Developer wallet address
	 * @return bool True/False as the result
	 */
	function removePoolDeveloper(address _address)
		public
		onlyOwner
		returns (bool)
	{
		_developers[_address] = false;
		return true;
	}

	/**
	 * @notice A method to add system related pools
	 * @param _pool Pool wallet address to add
	 * @return bool True/False as the result
	 */
	function addSystemPool(address _pool) public onlyOwner returns (bool) {
		_systemPools[_pool] = true;
		return true;
	}

	/**
	 * @notice A method to remove pool from this pool
	 * @param _pool Pool wallet address to remove
	 * @return bool True/False as the result
	 */
	function removeSystemPool(address _pool) public onlyOwner returns (bool) {
		_systemPools[_pool] = false;
		return true;
	}

	/**
	 * @notice A method to transfer remaining amount of reward to another pool (only related pools)
	 * @param _toPool Pool to transfer remaining reward
	 * @param _amount Amount of reward to transfer
	 * @return bool True/False as the result
	 */
	function transferUnpaidRewardsToPool(address _toPool, uint256 _amount)
		public
		onlyOwner
		returns (bool)
	{
		require(
			block.timestamp > _poolEndedAt,
			"This pool is active can not transfer rewards to another."
		);
		require(
			_developers[_toPool] == true,
			"Invalid pool address to transfer rewards."
		);
		uint256 balance = _rewardToken.balanceOf(pool);
		require(balance >= _amount, "Balance is not enough to transfer.");
		_rewardToken.safeTransfer(_toPool, _amount);
	}

	/**
	 * @notice A method to detect if an address is stakeholder wallet or not
	 * @param _address Pool to transfer remaining reward
	 * @return exists_ True/False as the result
	 * @return index_ Index of stakeholder in the pool
	 */
	function isStakeholder(address _address)
		public
		view
		returns (bool exists_, uint256 index_)
	{
		for (uint256 s = 0; s < _stakeholders.length; s += 1) {
			if (_address == _stakeholders[s]) return (true, s);
		}

		return (false, 0);
	}

	/**
	 * @notice A method to add new stakeholder/update existed stakeholder of the pool
	 * @param _stakeholder Pool to transfer remaining reward
	 * @param _amount Pool to transfer remaining reward
	 * @param _timestamp Pool to transfer remaining reward
	 */
	function addOrUpdateStakeholder(
		address _stakeholder,
		uint256 _amount,
		uint256 _timestamp
	) internal returns (uint256) {
		(bool exists_, ) = isStakeholder(_stakeholder);
		if (!exists_) {
			_stakeholders.push(_stakeholder);

			Stake memory newStake = Stake({
				stakeholder: _stakeholder,
				createdAt: _timestamp,
				stakeAmount: _amount,
				lastClaimAt: 0,
				canUnlockAt: _poolEndedAt
			});

			_userStakes[_stakeholder] = newStake;
			_rewards[_stakeholder] = 0;
			return _amount;
		}

		// If stakeholder add new amount of stake tokens. Paid current reward to them and update new stake tokens of stakeholder
		claimRewardInternal(_stakeholder, _timestamp);
		_userStakes[_stakeholder].lastClaimAt = _timestamp;
		_userStakes[_stakeholder].createdAt = _timestamp;
		_userStakes[_stakeholder].stakeAmount = _userStakes[_stakeholder]
			.stakeAmount
			.add(_amount);

		return _amount;
	}

	/**
	 * @notice A method to take token
	 * @param _amount Amount of token to stake
	 * @return success_ True/False as result
	 * @return stakeAmount_ Succeed stake amount
	 */
	function stake(uint256 _amount)
		public
		payable
		updateReward(msg.sender)
		validTransactionFee(msg.value)
		poolAvailable
		ensurePoolStart
		returns (bool success_, uint256 stakeAmount_)
	{
		address stakeholder = msg.sender;
		require(_amount > 0, "Staking token amount must be none zero value.");

		uint256 _stakeAmount = _amount;
		if (_stakeDepositFee > 0) {
			uint256 _fee = _amount.mul(_stakeDepositFee).div(100);
			_stakeAmount = _amount.sub(_fee);
			_poolDepositFeeAmount = _poolDepositFeeAmount.add(_fee);
		}

		uint256 allowance = _stakeToken.allowance(msg.sender, pool);
		require(
			allowance >= _amount,
			"You have reach the token allowance to transfer to contract pool. Please approve and try again."
		);

		_stakeToken.safeTransferFrom(stakeholder, pool, _amount);
		emit TransferSuccessful(stakeholder, pool, _amount);

		uint256 timestamp = block.timestamp;
		// Reset lifetime of pool if first stakeholder stake their tokens
		// to make sure all reward will be distributed to all stakeholders
		if (_stakeholders.length == 0) {
			_poolRewardDistributionStartedAt = timestamp;
			_poolEndedAt = timestamp.add(_poolRunDuration);
		}

		uint256 addToTotalAmount = addOrUpdateStakeholder(
			stakeholder,
			_stakeAmount,
			timestamp
		);
		emit StakeSuccessful(stakeholder, _stakeAmount, timestamp);
		_poolTotalStake = _poolTotalStake.add(addToTotalAmount);
		return (true, _stakeAmount);
	}

	/**
	 * @notice A method to claim reward of stakeholder. Internal call only
	 * @param _stakeholder Stakeholder address to claim reward
	 * @param _timestamp The block timestamp
	 */
	function claimRewardInternal(address _stakeholder, uint256 _timestamp)
		internal
		returns (bool claimSuccess_, uint256 claimedAmount_)
	{
		(uint256 currentRewards, , , ) = rewardOf(_stakeholder);
		if (currentRewards == 0) {
			return (true, 0);
		}

		uint256 _receiveAmount = currentRewards;
		if (_rewardClaimFee > 0) {
			uint256 _fee = (currentRewards.mul(_rewardClaimFee)).div(100);
			_receiveAmount = currentRewards.sub(_fee);
		}

		_rewards[_stakeholder] = 0;
		_paidRewards[_stakeholder] = _paidRewards[_stakeholder].add(
			currentRewards
		);
		_rewardToken.safeTransfer(_stakeholder, _receiveAmount);
		_poolClaimedReward = _poolClaimedReward.add(currentRewards);
		_userStakes[_stakeholder].lastClaimAt = _timestamp;
		emit RewardClaimSuccessful(_stakeholder, _receiveAmount, _timestamp);
		return (true, currentRewards);
	}

	/**
	 * @notice A method to claim reward
	 * @return success_ True/False as result
	 * @return amount_ Claimable amount
	 */
	function claimReward()
		public
		payable
		updateReward(msg.sender)
		validTransactionFee(msg.value)
		ensurePoolStart
		returns (bool success_, uint256 amount_)
	{
		address stakeholder = msg.sender;
		uint256 timestamp = block.timestamp;
		(bool claimSuccess_, uint256 claimedAmount_) = claimRewardInternal(
			stakeholder,
			timestamp
		);
		return (claimSuccess_, claimedAmount_);
	}

	/**
	 * @notice A method to withdraw stake tokens
	 * @return success_ True/False as result
	 * @return withdrawAmount_ Withdraw amount
	 */
	function withdrawAndClaimReward()
		public
		payable
		// updateReward(msg.sender)
		validTransactionFee(msg.value)
		ensurePoolStart
		returns (bool success_, uint256 withdrawAmount_)
	{
		address _stakeholder = msg.sender;
		uint256 _timestamp = block.timestamp;
		(bool exists_, ) = isStakeholder(_stakeholder);
		require(exists_, "You are not stakeholder of this pool.");
		require(
			_userStakes[_stakeholder].stakeAmount > 0,
			"You have withdraw your token."
		);

		uint256 _withdrawAmount = _userStakes[_stakeholder].stakeAmount;
		require(
			block.timestamp > _userStakes[_stakeholder].canUnlockAt,
			"Tokens are in locked. Please wait until it's released."
		);

		// Transfer reward if any
		(bool claimSuccess_, ) = claimRewardInternal(_stakeholder, _timestamp);

		if (!claimSuccess_) {
			return (false, 0);
		}

		_userStakes[_stakeholder].stakeAmount = 0;
		// Transfer stake tokens
		_stakeToken.safeTransfer(_stakeholder, _withdrawAmount);
		return (true, _withdrawAmount);
	}

	/**
	 * @notice A method to withdraw stake tokens fee to pay for pool development team
	 * @param toAddress Add ress to receive deposit fee
	 */
	function withdrawDepositFee(address toAddress) public onlyOwner {
		uint256 balanceOfPool = _stakeToken.balanceOf(pool);
		require(
			balanceOfPool >= _poolDepositFeeAmount,
			"Balance is not enough to withdraw."
		);
		_stakeToken.safeTransfer(toAddress, _poolDepositFeeAmount);
	}

	/**
	 * @notice A method to get reward of stakeholder
	 * @param _stakeholder Pool to transfer remaining reward
	 */
	function rewardOf(address _stakeholder)
		public
		view
		returns (
			uint256 reward_,
			uint256 stakeAmount_,
			uint256 userStakePoolSeconds_,
			uint256 claimedReward_
		)
	{
		if (_poolRewardDistributionStartedAt == 0) {
			return (0, 0, 0, 0);
		}

		if (_userStakes[_stakeholder].stakeAmount == 0) {
			return (0, 0, 0, 0);
		}

		if (_poolTotalStake == 0) {
			return (0, 0, 0, 0);
		}

		uint256 lastTimePeriod = 0;
		if (block.timestamp > _poolEndedAt) {
			lastTimePeriod = _poolEndedAt;
		} else {
			lastTimePeriod = block.timestamp;
		}

		uint256 userStakePoolSeconds = 0;
		if (_userStakes[_stakeholder].lastClaimAt > 0) {
			userStakePoolSeconds = lastTimePeriod.sub(
				_userStakes[_stakeholder].lastClaimAt
			);
		} else {
			userStakePoolSeconds = lastTimePeriod.sub(
				_userStakes[_stakeholder].createdAt
			);
		}

		if (userStakePoolSeconds == 0) {
			return (0, 0, 0, 0);
		}

		uint256 availableReward = _userStakes[_stakeholder]
			.stakeAmount
			.mul(_poolRewardPerSecondRate.mul(userStakePoolSeconds))
			.div(_poolTotalStake);

		return (
			availableReward,
			_userStakes[_stakeholder].stakeAmount,
			userStakePoolSeconds,
			_paidRewards[_stakeholder]
		);
	}

	/**
	 * @notice A method to get pool distributed reward
	 * @param timestamp The timestamp to get distributed reward
	 * @return uint256 The pool distributed reward
	 */
	function getPoolDistributedReward(uint256 timestamp)
		public
		view
		returns (uint256)
	{
		if (_stakeholders.length == 0) {
			return 0;
		}

		if (timestamp < _poolStartedAt) {
			return 0;
		}

		if (_poolRewardDistributionStartedAt == 0) {
			return 0;
		}

		uint256 lastTimePeriod = 0;
		if (block.timestamp > _poolEndedAt) {
			lastTimePeriod = _poolEndedAt;
		} else {
			lastTimePeriod = block.timestamp;
		}

		uint256 poolSpentSeconds = lastTimePeriod.sub(
			_poolRewardDistributionStartedAt
		);

		return _poolRewardPerSecondRate.mul(poolSpentSeconds);
	}

	/**
	 * @notice A method to withdraw fee by developer (Use for development)
	 * @param _amount Amount to withdraw
	 * @return bool True/False as result
	 */
	function developerWithdraw(uint256 _amount) public returns (bool) {
		require(msg.sender != pool, "Invalid address to withdraw.");
		require(
			_developers[msg.sender] == true,
			"Your are not a developer of this pool."
		);

		require(address(this).balance > _amount, "Invalid amount to withdraw");
		msg.sender.transfer(_amount);
		return true;
	}

	/**
	 * @notice A method to add new stakeholder/update existed stakeholder of the pool
	 */
	function myPoolInformation()
		public
		view
		returns (
			address address_,
			uint256 stakeAmount_,
			uint256 createdAt_,
			uint256 lastClaimAt_,
			uint256 reward_,
			uint256 claimedReward_,
			uint256 canUnlockWithdrawAt
		)
	{
		address stakeholder = msg.sender;
		(bool exists_, ) = isStakeholder(stakeholder);
		if (!exists_) {
			return (
				0x0000000000000000000000000000000000000000,
				0,
				0,
				0,
				0,
				0,
				0
			);
		}

		(uint256 _reward, , , ) = rewardOf(stakeholder);
		uint256 _paidReward = _paidRewards[stakeholder];
		return (
			_userStakes[stakeholder].stakeholder,
			_userStakes[stakeholder].stakeAmount,
			_userStakes[stakeholder].createdAt,
			_userStakes[stakeholder].lastClaimAt,
			_reward,
			_paidReward,
			_userStakes[stakeholder].canUnlockAt
		);
	}

	/**
	 * @notice A method to get pool information
	 * @return poolTotalReward_ The pool total rewards
	 * @return poolRemainingReward_ The pool remaining rewards
	 * @return poolRunDuration_ The pool lifetime duration in seconds
	 * @return poolStartedAt_ The pool start date
	 * @return poolEnded_ Pool status is active/inactive
	 * @return poolDistributedReward_ The pool distributed rewards
	 * @return poolClaimedReward_ The pool paid rewards
	 * @return poolNumberOfStakeholders_ Total stakeholders of the pool
	 * @return poolTotalStake_ Total stake volume of the pool
	 */
	function getPoolInformation()
		public
		view
		returns (
			uint256 poolTotalReward_,
			uint256 poolRemainingReward_,
			uint256 poolRunDuration_,
			uint256 poolStartedAt_,
			bool poolEnded_,
			uint256 poolDistributedReward_,
			uint256 poolClaimedReward_,
			uint256 poolNumberOfStakeholders_,
			uint256 poolTotalStake_,
			uint256 poolRewardPerSecond_,
			uint256 poolEndedAt_
		)
	{
		uint256 _poolNumberOfStakeholders = _stakeholders.length;
		uint256 _poolDistributedReward = getPoolDistributedReward(
			block.timestamp
		);
		uint256 _poolRemainingReward = _poolTotalReward.sub(
			_poolDistributedReward
		);

		bool _poolEnded = block.timestamp > _poolEndedAt;
		return (
			_poolTotalReward,
			_poolRemainingReward,
			_poolRunDuration,
			_poolStartedAt,
			_poolEnded,
			_poolDistributedReward,
			_poolClaimedReward,
			_poolNumberOfStakeholders,
			_poolTotalStake,
			_poolRewardPerSecondRate,
			_poolEndedAt
		);
	}
}
/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
function addOrUpdateStakeholder(
	address _stakeholder,
	uint256 _amount,
	uint256 _timestamp
) internal returns (uint256) {
	(bool exists_, ) = isStakeholder(_stakeholder);
	if (!exists_) {
		_stakeholders.push(_stakeholder);

		Stake memory newStake = Stake({
			stakeholder: _stakeholder,
			createdAt: _timestamp,
			stakeAmount: _amount,
			lastClaimAt: 0,
			canUnlockAt: _poolEndedAt
		});

		_userStakes[_stakeholder] = newStake;
		_rewards[_stakeholder] = 0;
		return _amount;
	}

	// If stakeholder add new amount of stake tokens. Paid current reward to them and update new stake tokens of stakeholder
	claimRewardInternal(_stakeholder, _timestamp);
	_userStakes[_stakeholder].lastClaimAt = _timestamp;
	_userStakes[_stakeholder].createdAt = _timestamp;
	_userStakes[_stakeholder].stakeAmount = _userStakes[_stakeholder]
		.stakeAmount
		.add(_amount);

	return _amount;
}

/**
 	 * @notice A method to add new stakeholder/update existed stakeholder of the pool
 	 * @param _stakeholder Pool to transfer remaining reward
 	 * @param _amount Pool to transfer remaining reward
 	 * @param _timestamp Pool to transfer remaining reward
 	 */