contract YFVVoteV2 {
    using SafeMath for uint256;

    address public governance;

    uint8 public constant MAX_VOTERS_PER_ITEM = 200;

    uint16 public defaultVotingValueMin = 50; // reward scaling factor 50% (x0.5 times)
    uint16 public defaultVotingValueMax = 150; // reward scaling factor 150% (x1.5 times)

    mapping(address => mapping(uint256 => uint8)) public numVoters; // poolAddress -> votingItem (periodFinish) -> numVoters (the number of voters in this round)
    mapping(address => mapping(uint256 => address[MAX_VOTERS_PER_ITEM])) public voters; // poolAddress -> votingItem (periodFinish) -> voters (array)
    mapping(address => mapping(uint256 => mapping(address => bool))) public isInTopVoters; // poolAddress -> votingItem (periodFinish) -> isInTopVoters (map: voter -> in_top (true/false))
    mapping(address => mapping(uint256 => mapping(address => uint16))) public voter2VotingValue; // poolAddress -> votingItem (periodFinish) -> voter2VotingValue (map: voter -> voting value)

    mapping(address => mapping(uint256 => uint16)) public votingValueMinimums; // poolAddress -> votingItem (proposalId) -> votingValueMin
    mapping(address => mapping(uint256 => uint16)) public votingValueMaximums; // poolAddress -> votingItem (proposalId) -> votingValueMax

    address public governanceRewardScaler;

    event Voted(address poolAddress, address indexed user, uint256 votingItem, uint16 votingValue);

    constructor () public {
        governance = msg.sender;
    }

    function setDefaultVotingValueRange(uint16 minValue, uint16 maxValue) public {
        require(msg.sender == governance, "!governance");
        require(minValue < maxValue, "Invalid voting range");
        defaultVotingValueMin = minValue;
        defaultVotingValueMax = maxValue;
    }

    function setVotingValueRange(address poolAddress, uint256 votingItem, uint16 minValue, uint16 maxValue) public {
        require(msg.sender == governance, "!governance");
        require(minValue < maxValue, "Invalid voting range");
        votingValueMinimums[poolAddress][votingItem] = minValue;
        votingValueMaximums[poolAddress][votingItem] = maxValue;
    }

    function setGovernanceRewardsScaler(address _governanceRewardScaler) public {
        require(msg.sender == governance, "!governance");
        governanceRewardScaler = _governanceRewardScaler;
    }

    function isVotable(address poolAddress, address account, uint256 votingItem) public view returns (bool) {
        // already voted
        if (voter2VotingValue[poolAddress][votingItem][account] > 0) return false;

        IYFVRewards rewards = IYFVRewards(poolAddress);
        // hasn't any staking power
        if (rewards.stakingPower(account) == 0) return false;

        // number of voters is under limit still
        if (numVoters[poolAddress][votingItem] < MAX_VOTERS_PER_ITEM) return true;
        for (uint8 i = 0; i < numVoters[poolAddress][votingItem]; i++) {
            if (rewards.stakingPower(voters[poolAddress][votingItem][i]) < rewards.stakingPower(account)) return true;
            // there is some voters has lower staking power
        }

        return false;
    }

    function averageVotingValueNoGovernance(address poolAddress, uint256 votingItem) public view returns (uint16) {
        if (numVoters[poolAddress][votingItem] == 0) return 0; // no votes
        uint256 totalStakingPower = 0;
        uint256 totalWeightVotingValue = 0;
        IYFVRewards rewards = IYFVRewards(poolAddress);
        for (uint8 i = 0; i < numVoters[poolAddress][votingItem]; i++) {
            address voter = voters[poolAddress][votingItem][i];
            totalStakingPower = totalStakingPower.add(rewards.stakingPower(voter));
            totalWeightVotingValue = totalWeightVotingValue.add(rewards.stakingPower(voter).mul(voter2VotingValue[poolAddress][votingItem][voter]));
        }
        return (uint16) (totalWeightVotingValue.div(totalStakingPower));
    }

    function averageVotingValue(address poolAddress, uint256 votingItem) public view returns (uint16) {
        uint16 avgValue = 0;
        if (numVoters[poolAddress][votingItem] > 0) {
            avgValue = averageVotingValueNoGovernance(poolAddress, votingItem);
        }
        if (governanceRewardScaler != address(0)) {
            return IYFVGovernanceRewardScaler(governanceRewardScaler).votingValueGovernance(poolAddress, votingItem, avgValue);
        }
        return avgValue;
    }

    function vote(address poolAddress, uint256 votingItem, uint16 votingValue) public {
        if (votingValueMinimums[poolAddress][votingItem] > 0 && votingValueMaximums[poolAddress][votingItem] > 0) {
            require(votingValue >= votingValueMinimums[poolAddress][votingItem], "votingValue is smaller than minimum accepted value");
            require(votingValue <= votingValueMaximums[poolAddress][votingItem], "votingValue is greater than maximum accepted value");
        } else {
            require(votingValue >= defaultVotingValueMin, "votingValue is smaller than defaultVotingValueMin");
            require(votingValue <= defaultVotingValueMax, "votingValue is greater than defaultVotingValueMax");
        }
        if (!isInTopVoters[poolAddress][votingItem][msg.sender]) {
            require(isVotable(poolAddress, msg.sender, votingItem), "This account is not votable");
            uint8 voterIndex = MAX_VOTERS_PER_ITEM;
            if (numVoters[poolAddress][votingItem] < MAX_VOTERS_PER_ITEM) {
                voterIndex = numVoters[poolAddress][votingItem];
            } else {
                IYFVRewards rewards = IYFVRewards(poolAddress);
                uint256 minStakingPower = rewards.stakingPower(msg.sender);
                for (uint8 i = 0; i < numVoters[poolAddress][votingItem]; i++) {
                    if (rewards.stakingPower(voters[poolAddress][votingItem][i]) < minStakingPower) {
                        voterIndex = i;
                        minStakingPower = rewards.stakingPower(voters[poolAddress][votingItem][i]);
                    }
                }
            }
            if (voterIndex < MAX_VOTERS_PER_ITEM) {
                if (voterIndex < numVoters[poolAddress][votingItem]) {
                    // remove lower power previous voter
                    isInTopVoters[poolAddress][votingItem][voters[poolAddress][votingItem][voterIndex]] = false;
                } else {
                    ++numVoters[poolAddress][votingItem];
                }
                isInTopVoters[poolAddress][votingItem][msg.sender] = true;
                voters[poolAddress][votingItem][voterIndex] = msg.sender;
            }
        }
        voter2VotingValue[poolAddress][votingItem][msg.sender] = votingValue;
        emit Voted(poolAddress, msg.sender, votingItem, votingValue);
    }

    event EmergencyERC20Drain(address token, address governance, uint256 amount);

    // governance can drain tokens that are sent here by mistake
    function emergencyERC20Drain(ERC20 token, uint amount) external {
        require(msg.sender == governance, "!governance");
        emit EmergencyERC20Drain(address(token), governance, amount);
        token.transfer(governance, amount);
    }
}
function emergencyERC20Drain(ERC20 token, uint amount) external {
    require(msg.sender == governance, "!governance");
    emit EmergencyERC20Drain(address(token), governance, amount);
    token.transfer(governance, amount);
}

// governance can drain tokens that are sent here by mistake