contract Voting is KnowsRegistry, Ownable, ReentrancyGuard {
    using SafeMath for uint256;

    uint256 public constant proposalFstStake = 100 ether;
    uint256 public constant minimumVoteTime = 2 days;
    uint256 public proposalCount;
    uint256 public pauseTimestamp;

    event ProposalCreated(uint256 indexed proposalId, string action, string title, string description);
    event VotedCasted(address indexed voter, uint256 indexed proposalId, uint256 amountOfVotes, bool isYesVote);
    event ProposalAccepted(uint256 indexed proposalId);
    event ProposalRejected(uint256 indexed proposalId);
    event ProposalCallFailed(uint256 indexed proposalId);

    struct Proposal {
        uint256 id;
        uint256 votingEnds; // epoch time in seconds when a vote will be able to be resolved and further votes will be rejected
        address to; // The address to which the result of the vote will be sent to
        bool isVoteResolved;
        bool isUpgradeProposal; // bool if the true the proposal is an upgrade proposal that goes to this specific address and assumes the data is a 20 byte address, if false it is a regular vote
        uint256 yesVotes; // amount of yes votes
        uint256 noVotes; // amount of no votes
        uint256 fstSnapshotId; // snapshot of tokens when the proposal was started
        address proposer; // the address of the creator of the vote
        mapping(address => bool) didVote; // tracks which addresses have voted
        bytes data; // byte data to use in a call (regular vote abi encoded data) - upgrade vote 20 bytes address
        bool ownerApproved; // is initially used by the owner to force votes to succeed without a full vote. Ownership will be phased out
    }

    mapping(uint256 => Proposal) public proposals;

    constructor(address _registryHolder) public KnowsRegistry(_registryHolder) {}

    function createProposal(
        address _to,
        bytes memory _data,
        string memory _action,
        string memory _title,
        string memory _description
    ) public {
        require(pauseTimestamp == 0, "proposals are paused");

        // We make users stake FST for creating a proposal to avoid
        // spammers creating proposals.
        // Funds are only returned on a successful proposal.
        FsToken fsToken = getFsToken();
        fsToken.burnFrom(msg.sender, proposalFstStake);

        bool isUpgradeProposal = _to == address(this);
        if (isUpgradeProposal) {
            // If this is an upgrade propsal the update address is saved
            // in the data field. We ensure here that the bytes field
            // has the length of a address (20 bytes).
            require(_data.length == 20, "data needs to be 20 bytes long");
        }

        Proposal storage p = proposals[proposalCount];
        p.id = proposalCount;
        p.votingEnds = now.add(minimumVoteTime);
        p.data = _data;
        p.to = _to;
        p.fstSnapshotId = fsToken.snapshot();
        p.proposer = msg.sender;
        p.isUpgradeProposal = isUpgradeProposal;

        emit ProposalCreated(proposalCount, _action, _title, _description);
        proposalCount++;
    }

    function vote(uint256 _proposalId, bool _isYesVote) public {
        requireProposalExists(_proposalId);
        Proposal storage p = proposals[_proposalId];
        require(now <= p.votingEnds, "vote is not open");
        require(!p.didVote[msg.sender], "already voted");

        p.didVote[msg.sender] = true;

        uint256 amountOfVotes = getFsToken().balanceOfAt(msg.sender, p.fstSnapshotId);

        if (_isYesVote) {
            p.yesVotes = p.yesVotes.add(amountOfVotes);
        } else {
            p.noVotes = p.noVotes.add(amountOfVotes);
        }
        emit VotedCasted(msg.sender, _proposalId, amountOfVotes, _isYesVote);
    }

    function pause(uint256 _proposalId) public {
        requireProposalExists(_proposalId);
        Proposal storage p = proposals[_proposalId];
        requireProposalCanBeResolved(p);
        require(p.isUpgradeProposal);

        require(pauseTimestamp == 0, "system already paused");
        pauseTimestamp = now;
    }

    function resolve(uint256 _proposalId) public nonReentrant {
        // Gas estimation is broken in ethereum for contract
        // factories. This has been reported a long time ago but never
        // got resolved:
        // https://github.com/ethereum/go-ethereum/issues/1590
        //
        // Requiring a high gas amount to start here makes this work for now
        // Note: this does not mean that all the gas will actually be used.
        // If the particular vote requires less gas users will be refunded.
        require(gasleft() >= 8000000, "Please use 8M+ gas to resolve");

        requireProposalExists(_proposalId);
        Proposal storage p = proposals[_proposalId];
        requireProposalCanBeResolved(p);

        if (p.isUpgradeProposal && !p.ownerApproved) {
            require(pauseTimestamp != 0, "pause first");
            require(now >= pauseTimestamp.add(minimumVoteTime), "pauseVotes");
        }

        p.isVoteResolved = true;

        bool votePassed = p.yesVotes > p.noVotes || p.ownerApproved;

        if (!votePassed) {
            emit ProposalRejected(_proposalId);
            return;
        }

        bool successful = p.isUpgradeProposal ? upgradeVoteExecution(p) : regularVoteExecution(p);
        if (!successful) {
            emit ProposalCallFailed(_proposalId);
            return;
        }

        emit ProposalAccepted(_proposalId);
    }

    function regularVoteExecution(Proposal storage p) private returns (bool) {
        (bool success, ) = p.to.call(p.data);
        if (success) {
            // Only mint initial tokens back if the vote passed
            FsToken fsToken = getFsToken();
            fsToken.mint(p.proposer, proposalFstStake);
        }
        return success;
    }

    function upgradeVoteExecution(Proposal storage p) private returns (bool) {
        address newAddress = bytesToAddress(p.data);
        // We update all other contracts with the new address
        // If any of these calls should fail we let the entire transaction fail.
        // This would allow anybody to call resolve again and retry since the
        // vote would have not gotten marked as resolved.
        // However if any of the steps here fail the system is in a problematic state.
        // All of these contracts have been written by us and should not fail.
        getFsToken().mint(p.proposer, proposalFstStake);
        doUpgrade(newAddress);

        return true;
    }

    function didAddressVote(uint256 _proposalId, address _voter) public view returns (bool) {
        return proposals[_proposalId].didVote[_voter];
    }

    // Allows owners to approve a vote, ownership will be phased out.
    function approve(uint256 _proposalId) public onlyOwner {
        proposals[_proposalId].ownerApproved = true;
    }

    // Allows owners to veto a vote, ownership will be phased out.
    function veto(uint256 _proposalId) public onlyOwner {
        proposals[_proposalId].isVoteResolved = true;
    }

    // Allows owners to upgrade the contract without a vote, ownership will be phased out.
    function ownableUpgrade(address _newAddress) public onlyOwner {
        doUpgrade(_newAddress);
    }

    function doUpgrade(address newAddress) private {
        Ownable(getRegistry().getFsTokenProxyAdminAddress()).transferOwnership(newAddress);
        FsToken fsToken = getFsToken();
        fsToken.addMinter(newAddress);
        fsToken.renounceMinter();
        getRegistry().updateVotingAddress(newAddress);
    }

    function requireProposalExists(uint256 proposalId) private view {
        require(proposalId < proposalCount, "Nonexisting proposal");
    }

    function requireProposalCanBeResolved(Proposal memory p) private view {
        require(now > p.votingEnds || p.ownerApproved, "vote is still open");
        require(!p.isVoteResolved, "already resolved");

        FsToken fsToken = getFsToken();
        uint256 totalFstSupply = fsToken.totalSupplyAt(p.fstSnapshotId);
        uint256 totalVotes = p.yesVotes.add(p.noVotes);
        bool aboveThreshold = (totalFstSupply / 10) <= totalVotes;
        require(aboveThreshold || p.ownerApproved, "The voting threshold has not been met");
    }

    function bytesToAddress(bytes memory bys) private pure returns (address addr) {
        require(bys.length == 20, "data needs to be 20 bytes long");
        // A quick way to load 2 bytes into the address variable
        assembly {
            addr := mload(add(bys, 20))
        }
    }

    function getFsToken() private view returns (FsToken) {
        return FsToken(getRegistry().getFsTokenAddress());
    }

    function onRegistryRefresh() public onlyRegistry {
        // No op since we always read all address directly from the registryHolder pointer
    }
}
function approve(uint256 _proposalId) public onlyOwner {
    proposals[_proposalId].ownerApproved = true;
}

// Allows owners to approve a vote, ownership will be phased out.