contract StakingPools is ReentrancyGuard {
  using FixedPointMath for FixedPointMath.FixedDecimal;
  using Pool for Pool.Data;
  using Pool for Pool.List;
  using SafeERC20 for IERC20;
  using SafeMath for uint256;
  using Stake for Stake.Data;

  event PendingGovernanceUpdated(
    address pendingGovernance
  );

  event GovernanceUpdated(
    address governance
  );

  event RewardRateUpdated(
    uint256 rewardRate
  );

  event PoolRewardWeightUpdated(
    uint256 indexed poolId,
    uint256 rewardWeight
  );

  event PoolCreated(
    uint256 indexed poolId,
    IERC20 indexed token
  );

  event TokensDeposited(
    address indexed user,
    uint256 indexed poolId,
    uint256 amount
  );

  event TokensWithdrawn(
    address indexed user,
    uint256 indexed poolId,
    uint256 amount
  );

  event TokensClaimed(
    address indexed user,
    uint256 indexed poolId,
    uint256 amount
  );

  /// @dev The token which will be minted as a reward for staking.
  IMintableERC20 public reward;

  /// @dev The address of the account which currently has administrative capabilities over this contract.
  address public governance;

  address public pendingGovernance;

  /// @dev address of the vesting contract
  address public vesting;

  /// @dev Tokens are mapped to their pool identifier plus one. Tokens that do not have an associated pool
  /// will return an identifier of zero.
  mapping(IERC20 => uint256) public tokenPoolIds;

  /// @dev The context shared between the pools.
  Pool.Context private _ctx;

  /// @dev A list of all of the pools.
  Pool.List private _pools;

  /// @dev A mapping of all of the user stakes mapped first by pool and then by address.
  mapping(address => mapping(uint256 => Stake.Data)) private _stakes;
  /// @dev like _stakes but for vesting based staking
  mapping(address => mapping(uint256 => Stake.Data)) private _vestingStakes;

  constructor(
    IMintableERC20 _reward,
    address _governance
  ) {
    require(_governance != address(0), "StakingPools: governance address cannot be 0x0");

    reward = _reward;
    governance = _governance;
  }

  /// @dev A modifier which reverts when the caller is not the governance.
  modifier onlyGovernance() {
    require(msg.sender == governance, "StakingPools: only governance");
    _;
  }

  modifier onlyVesting() {
    require(msg.sender == vesting, "StakingPools: only vesting");
    _;
  }

  /// @dev Sets the governance.
  ///
  /// This function can only called by the current governance.
  ///
  /// @param _pendingGovernance the new pending governance.
  function setPendingGovernance(address _pendingGovernance) external onlyGovernance {
    require(_pendingGovernance != address(0), "StakingPools: pending governance address cannot be 0x0");
    pendingGovernance = _pendingGovernance;

    emit PendingGovernanceUpdated(_pendingGovernance);
  }

  /// @dev used to set the vesting contract address
  function setVesting(address _vesting) external onlyGovernance {
    vesting = _vesting;
  }

  function acceptGovernance() external {
    require(msg.sender == pendingGovernance, "StakingPools: only pending governance");

    address _pendingGovernance = pendingGovernance;
    governance = _pendingGovernance;

    emit GovernanceUpdated(_pendingGovernance);
  }

  /// @dev Sets the distribution reward rate.
  ///
  /// This will update all of the pools.
  ///
  /// @param _rewardRate The number of tokens to distribute per second.
  function setRewardRate(uint256 _rewardRate) external onlyGovernance {
    _updatePools();

    _ctx.rewardRate = _rewardRate;

    emit RewardRateUpdated(_rewardRate);
  }

  /// @dev Creates a new pool.
  ///
  /// The created pool will need to have its reward weight initialized before it begins generating rewards.
  ///
  /// @param _token The token the pool will accept for staking.
  ///
  /// @return the identifier for the newly created pool.
  function createPool(IERC20 _token) external onlyGovernance returns (uint256) {
    require(tokenPoolIds[_token] == 0, "StakingPools: token already has a pool");

    uint256 _poolId = _pools.length();

    _pools.push(Pool.Data({
      token: _token,
      totalDeposited: 0,
      rewardWeight: 0,
      accumulatedRewardWeight: FixedPointMath.FixedDecimal(0),
      lastUpdatedBlock: block.number
    }));

    tokenPoolIds[_token] = _poolId + 1;

    emit PoolCreated(_poolId, _token);

    return _poolId;
  }

  /// @dev Sets the reward weights of all of the pools.
  ///
  /// @param _rewardWeights The reward weights of all of the pools.
  function setRewardWeights(uint256[] calldata _rewardWeights) external onlyGovernance {
    require(_rewardWeights.length == _pools.length(), "StakingPools: weights length mismatch");

    _updatePools();

    uint256 _totalRewardWeight = _ctx.totalRewardWeight;
    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
      Pool.Data storage _pool = _pools.get(_poolId);

      uint256 _currentRewardWeight = _pool.rewardWeight;
      if (_currentRewardWeight == _rewardWeights[_poolId]) {
        continue;
      }

      _totalRewardWeight = _totalRewardWeight.sub(_currentRewardWeight).add(_rewardWeights[_poolId]);
      _pool.rewardWeight = _rewardWeights[_poolId];

      emit PoolRewardWeightUpdated(_poolId, _rewardWeights[_poolId]);
    }

    _ctx.totalRewardWeight = _totalRewardWeight;
  }

  /// @dev like deposit but used to enbale staking of vested tokens
  /// @notice the vesting account must not be used for staking outside of the vesting contract saking
  function depositVesting(
    address _account, 
    uint256 _poolId, 
    uint256 _depositAmount
  ) 
    external 
    onlyVesting 
    nonReentrant 
    returns (bool)
  {
    Pool.Data storage _pool = _pools.get(_poolId);
    _pool.update(_ctx);

    Stake.Data storage _stake = _vestingStakes[_account][_poolId];
    _stake.update(_pool, _ctx);

    _pool.totalDeposited = _pool.totalDeposited.add(_depositAmount);
    _stake.totalDeposited = _stake.totalDeposited.add(_depositAmount);

    // we need to transfer the tokens from the vesting address to this contract
    _pool.token.safeTransferFrom(msg.sender, address(this), _depositAmount);

    emit TokensDeposited(_account, _poolId, _depositAmount);
    return true;
  }

  /// @dev allows the vesting contract to eithr withdraw, claim, or exit staking
  /// @dev if you want to claim provide `false` for both `_doWithdraw` and `_doExit`
  /// @dev if you want to completely exit provide `true` for both `_doWithdraw` and `_doExit`
  /// @dev for exiting you may supply a value of `0` for _withdrawAmount
  function withdrawOrClaimOrExitVesting(
    address _account,
    uint256 _poolId,
    uint256 _withdrawAmount,
    bool _doWithdraw,
    bool _doExit
  )
    external
    onlyVesting
    nonReentrant
    // (ok, amountClaimed)
    returns (bool, uint256)
  {
    Pool.Data storage _pool = _pools.get(_poolId);
    _pool.update(_ctx);

    Stake.Data storage _stake = _vestingStakes[_account][_poolId];
    _stake.update(_pool, _ctx);

  // claim
    uint256 _claimAmount = _stake.totalUnclaimed;
    _stake.totalUnclaimed = 0;

    // make sure to mint to the vesting contract
    reward.mint(msg.sender, _claimAmount);

    emit TokensClaimed(_account, _poolId, _claimAmount);

    if (_doWithdraw) {
      // if exit withdraw all staked amount
      if (_doExit) {
        _withdrawAmount = _stake.totalDeposited;
      }
      // if the amount they are attempting to withdraw is more
      // than the amount they have staked, just withdraw total deposited
      if (_withdrawAmount > _stake.totalDeposited) {
        _withdrawAmount = _stake.totalDeposited;
      }
      _pool.totalDeposited = _pool.totalDeposited.sub(_withdrawAmount);
      _stake.totalDeposited = _stake.totalDeposited.sub(_withdrawAmount);
      // make sure to transfer to the vesting contract
      _pool.token.safeTransfer(msg.sender, _withdrawAmount);
      emit TokensWithdrawn(_account, _poolId, _withdrawAmount);
    }
    return (true, _claimAmount);
  }

  /// @dev Stakes tokens into a pool.
  ///
  /// @param _poolId        the pool to deposit tokens into.
  /// @param _depositAmount the amount of tokens to deposit.
  function deposit(uint256 _poolId, uint256 _depositAmount) external nonReentrant {
    Pool.Data storage _pool = _pools.get(_poolId);
    _pool.update(_ctx);

    Stake.Data storage _stake = _stakes[msg.sender][_poolId];
    _stake.update(_pool, _ctx);

    _deposit(msg.sender, _poolId, _depositAmount);
  }

  /// @dev Withdraws staked tokens from a pool.
  ///
  /// @param _poolId          The pool to withdraw staked tokens from.
  /// @param _withdrawAmount  The number of tokens to withdraw.
  function withdraw(uint256 _poolId, uint256 _withdrawAmount) external nonReentrant {
    Pool.Data storage _pool = _pools.get(_poolId);
    _pool.update(_ctx);

    Stake.Data storage _stake = _stakes[msg.sender][_poolId];
    _stake.update(_pool, _ctx);
    
    _claim(msg.sender, _poolId);
    _withdraw(msg.sender, _poolId, _withdrawAmount);
  }

  /// @dev Claims all rewarded tokens from a pool.
  ///
  /// @param _poolId The pool to claim rewards from.
  ///
  /// @notice use this function to claim the tokens from a corresponding pool by ID.
  function claim(uint256 _poolId) external nonReentrant {
    Pool.Data storage _pool = _pools.get(_poolId);
    _pool.update(_ctx);

    Stake.Data storage _stake = _stakes[msg.sender][_poolId];
    _stake.update(_pool, _ctx);

    _claim(msg.sender, _poolId);
  }

  /// @dev Claims all rewards from a pool and then withdraws all staked tokens.
  ///
  /// @param _poolId the pool to exit from.
  function exit(uint256 _poolId) external nonReentrant {
    Pool.Data storage _pool = _pools.get(_poolId);
    _pool.update(_ctx);

    Stake.Data storage _stake = _stakes[msg.sender][_poolId];
    _stake.update(_pool, _ctx);

    _claim(msg.sender, _poolId);
    _withdraw(msg.sender,_poolId, _stake.totalDeposited);
  }

  /// @dev Gets the rate at which tokens are minted to stakers for all pools.
  ///
  /// @return the reward rate.
  function rewardRate() external view returns (uint256) {
    return _ctx.rewardRate;
  }

  /// @dev Gets the total reward weight between all the pools.
  ///
  /// @return the total reward weight.
  function totalRewardWeight() external view returns (uint256) {
    return _ctx.totalRewardWeight;
  }

  /// @dev Gets the number of pools that exist.
  ///
  /// @return the pool count.
  function poolCount() external view returns (uint256) {
    return _pools.length();
  }

  /// @dev Gets the token a pool accepts.
  ///
  /// @param _poolId the identifier of the pool.
  ///
  /// @return the token.
  function getPoolToken(uint256 _poolId) external view returns (IERC20) {
    Pool.Data storage _pool = _pools.get(_poolId);
    return _pool.token;
  }

  /// @dev Gets the total amount of funds staked in a pool.
  ///
  /// @param _poolId the identifier of the pool.
  ///
  /// @return the total amount of staked or deposited tokens.
  function getPoolTotalDeposited(uint256 _poolId) external view returns (uint256) {
    Pool.Data storage _pool = _pools.get(_poolId);
    return _pool.totalDeposited;
  }

  /// @dev Gets the reward weight of a pool which determines how much of the total rewards it receives per block.
  ///
  /// @param _poolId the identifier of the pool.
  ///
  /// @return the pool reward weight.
  function getPoolRewardWeight(uint256 _poolId) external view returns (uint256) {
    Pool.Data storage _pool = _pools.get(_poolId);
    return _pool.rewardWeight;
  }

  /// @dev Gets the amount of tokens per block being distributed to stakers for a pool.
  ///
  /// @param _poolId the identifier of the pool.
  ///
  /// @return the pool reward rate.
  function getPoolRewardRate(uint256 _poolId) external view returns (uint256) {
    Pool.Data storage _pool = _pools.get(_poolId);
    return _pool.getRewardRate(_ctx);
  }

  /// @dev Gets the number of tokens a user has staked into a pool.
  ///
  /// @param _account The account to query.
  /// @param _poolId  the identifier of the pool.
  ///
  /// @return the amount of deposited tokens.
  function getStakeTotalDeposited(address _account, uint256 _poolId, bool _vesting) external view returns (uint256) {
    if (_vesting) {
      Stake.Data storage _stake = _vestingStakes[_account][_poolId];
      return _stake.totalDeposited;
    } else {
      Stake.Data storage _stake = _stakes[_account][_poolId];
      return _stake.totalDeposited;
    }
  }

  /// @dev Gets the number of unclaimed reward tokens a user can claim from a pool.
  ///
  /// @param _account The account to get the unclaimed balance of.
  /// @param _poolId  The pool to check for unclaimed rewards.
  ///
  /// @return the amount of unclaimed reward tokens a user has in a pool.
  function getStakeTotalUnclaimed(address _account, uint256 _poolId, bool _vesting) external view returns (uint256) {
    if (_vesting) {
      Stake.Data storage _stake = _vestingStakes[_account][_poolId];
      return _stake.getUpdatedTotalUnclaimed(_pools.get(_poolId), _ctx);
    } else {
      Stake.Data storage _stake = _stakes[_account][_poolId];
      return _stake.getUpdatedTotalUnclaimed(_pools.get(_poolId), _ctx);
    }
  }

  /// @dev Updates all of the pools.
  function _updatePools() internal {
    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
      Pool.Data storage _pool = _pools.get(_poolId);
      _pool.update(_ctx);
    }
  }

  /// @dev Stakes tokens into a pool.
  ///
  /// The pool and stake MUST be updated before calling this function.
  ///
  /// @param _poolId        the pool to deposit tokens into.
  /// @param _depositAmount the amount of tokens to deposit.
  function _deposit(address _account, uint256 _poolId, uint256 _depositAmount) internal {
    Pool.Data storage _pool = _pools.get(_poolId);
    Stake.Data storage _stake = _stakes[_account][_poolId];

    _pool.totalDeposited = _pool.totalDeposited.add(_depositAmount);
    _stake.totalDeposited = _stake.totalDeposited.add(_depositAmount);

    _pool.token.safeTransferFrom(_account, address(this), _depositAmount);

    emit TokensDeposited(_account, _poolId, _depositAmount);
  }

  /// @dev Withdraws staked tokens from a pool.
  ///
  /// The pool and stake MUST be updated before calling this function.
  ///
  /// @param _poolId          The pool to withdraw staked tokens from.
  /// @param _withdrawAmount  The number of tokens to withdraw.
  function _withdraw(address _account, uint256 _poolId, uint256 _withdrawAmount) internal {
    Pool.Data storage _pool = _pools.get(_poolId);
    Stake.Data storage _stake = _stakes[_account][_poolId];

    _pool.totalDeposited = _pool.totalDeposited.sub(_withdrawAmount);
    _stake.totalDeposited = _stake.totalDeposited.sub(_withdrawAmount);

    _pool.token.safeTransfer(_account, _withdrawAmount);

    emit TokensWithdrawn(_account, _poolId, _withdrawAmount);
  }

  /// @dev Claims all rewarded tokens from a pool.
  ///
  /// The pool and stake MUST be updated before calling this function.
  ///
  /// @param _poolId The pool to claim rewards from.
  ///
  /// @notice use this function to claim the tokens from a corresponding pool by ID.
  function _claim(address _account, uint256 _poolId) internal {
    Stake.Data storage _stake = _stakes[_account][_poolId];

    uint256 _claimAmount = _stake.totalUnclaimed;
    _stake.totalUnclaimed = 0;

    reward.mint(_account, _claimAmount);

    emit TokensClaimed(_account, _poolId, _claimAmount);
  }
}
/// @title StakingPools
/// @dev A contract which allows users to stake to farm tokens.
///
/// This contract is copied from Alchemix https://github.com/alchemix-finance/alchemix-protocol
function withdrawOrClaimOrExitVesting(
  address _account,
  uint256 _poolId,
  uint256 _withdrawAmount,
  bool _doWithdraw,
  bool _doExit
)
  external
  onlyVesting
  nonReentrant
  // (ok, amountClaimed)
  returns (bool, uint256)
{
  Pool.Data storage _pool = _pools.get(_poolId);
  _pool.update(_ctx);

  Stake.Data storage _stake = _vestingStakes[_account][_poolId];
  _stake.update(_pool, _ctx);

// claim
  uint256 _claimAmount = _stake.totalUnclaimed;
  _stake.totalUnclaimed = 0;

  // make sure to mint to the vesting contract
  reward.mint(msg.sender, _claimAmount);

  emit TokensClaimed(_account, _poolId, _claimAmount);

  if (_doWithdraw) {
    // if exit withdraw all staked amount
    if (_doExit) {
      _withdrawAmount = _stake.totalDeposited;
    }
    // if the amount they are attempting to withdraw is more
    // than the amount they have staked, just withdraw total deposited
    if (_withdrawAmount > _stake.totalDeposited) {
      _withdrawAmount = _stake.totalDeposited;
    }
    _pool.totalDeposited = _pool.totalDeposited.sub(_withdrawAmount);
    _stake.totalDeposited = _stake.totalDeposited.sub(_withdrawAmount);
    // make sure to transfer to the vesting contract
    _pool.token.safeTransfer(msg.sender, _withdrawAmount);
    emit TokensWithdrawn(_account, _poolId, _withdrawAmount);
  }
  return (true, _claimAmount);
}

/// @dev allows the vesting contract to eithr withdraw, claim, or exit staking
/// @dev if you want to claim provide `false` for both `_doWithdraw` and `_doExit`
/// @dev if you want to completely exit provide `true` for both `_doWithdraw` and `_doExit`
/// @dev for exiting you may supply a value of `0` for _withdrawAmount