contract FundProposalManager is Initializable {
  using SafeMath for uint256;
  using Counters for Counters.Counter;
  using ArraySet for ArraySet.AddressSet;
  using ArraySet for ArraySet.Uint256Set;

  // 100% == 100 ether
  uint256 public constant ONE_HUNDRED_PCT = 100 ether;

  event NewProposal(uint256 indexed proposalId, address indexed proposer, bytes32 indexed marker);
  event AyeProposal(uint256 indexed proposalId, address indexed voter);
  event NayProposal(uint256 indexed proposalId, address indexed voter);

  event Approved(uint256 ayeShare, uint256 support, uint256 indexed proposalId, bytes32 indexed marker);
  event Execute(uint256 indexed proposalId, address indexed executer, bool indexed success, bytes response);

  struct ProposalVoting {
    uint256 creationBlock;
    uint256 creationTotalSupply;
    uint256 createdAt;
    uint256 timeoutAt;
    uint256 requiredSupport;
    uint256 minAcceptQuorum;
    uint256 totalAyes;
    uint256 totalNays;
    mapping(address => Choice) participants;
    ArraySet.AddressSet ayes;
    ArraySet.AddressSet nays;
  }

  struct Proposal {
    ProposalStatus status;
    address creator;
    address destination;
    uint256 value;
    bytes32 marker;
    bytes data;
    string dataLink;
  }

  IFundRegistry public fundRegistry;
  Counters.Counter public idCounter;

  mapping(uint256 => Proposal) public proposals;
  mapping(uint256 => ProposalVoting) internal _proposalVotings;
  mapping(uint256 => address) private _proposalToSender;

  enum ProposalStatus {
    NULL,
    ACTIVE,
    EXECUTED
  }

  enum Choice {
    PENDING,
    AYE,
    NAY
  }

  modifier onlyMember() {
    require(_fundRA().balanceOf(msg.sender) > 0, "Not valid member");

    _;
  }

  constructor() public {
  }

  function initialize(IFundRegistry _fundRegistry) external isInitializer {
    fundRegistry = _fundRegistry;
  }

  function propose(
    address _destination,
    uint256 _value,
    bool _castVote,
    bool _executesIfDecided,
    bytes calldata _data,
    string calldata _dataLink
  )
    external
    onlyMember
  {
    idCounter.increment();
    uint256 id = idCounter.current();

    Proposal storage p = proposals[id];
    p.creator = msg.sender;
    p.destination = _destination;
    p.value = _value;
    p.data = _data;
    p.dataLink = _dataLink;
    p.marker = _fundStorage().getThresholdMarker(_destination, _data);

    p.status = ProposalStatus.ACTIVE;
    _onNewProposal(id);

    emit NewProposal(id, msg.sender, p.marker);

    if (_castVote) {
      _aye(id, msg.sender, _executesIfDecided);
    }
  }

  function aye(uint256 _proposalId, bool _executeIfDecided) external {
    require(_isProposalOpen(_proposalId), "Proposal isn't open");

    _aye(_proposalId, msg.sender, _executeIfDecided);
  }

  function nay(uint256 _proposalId) external {
    require(_isProposalOpen(_proposalId), "Proposal isn't open");

    _nay(_proposalId, msg.sender);
  }

  function executeProposal(uint256 _proposalId, uint256 _gasToKeep) external {
    require(proposals[_proposalId].status == ProposalStatus.ACTIVE, "Proposal isn't active");

    (bool canExecuteThis, string memory reason) = _canExecute(_proposalId);
    require(canExecuteThis, reason);

    _unsafeExecuteProposal(_proposalId, _gasToKeep);
  }

  // INTERNAL

  function _aye(uint256 _proposalId, address _voter, bool _executeIfDecided) internal {
    ProposalVoting storage pV = _proposalVotings[_proposalId];
    uint256 reputation = reputationOf(_voter, pV.creationBlock);
    require(reputation > 0, "Can't vote with 0 reputation");

    if (pV.participants[_voter] == Choice.NAY) {
      pV.nays.remove(_voter);
      pV.totalNays = pV.totalNays.sub(reputation);
    }

    pV.participants[_voter] = Choice.AYE;
    pV.ayes.add(_voter);
    pV.totalAyes = pV.totalAyes.add(reputation);

    emit AyeProposal(_proposalId, _voter);

    (bool canExecuteThis,) = _canExecute(_proposalId);

    // Fail silently without revert
    if (_executeIfDecided && canExecuteThis) {
      // We've already checked if the vote can be executed with `_canExecute()`
      _unsafeExecuteProposal(_proposalId, 0);
    }
  }

  function _nay(uint256 _proposalId, address _voter) internal {
    ProposalVoting storage pV = _proposalVotings[_proposalId];
    uint256 reputation = reputationOf(_voter, pV.creationBlock);
    require(reputation > 0, "Can't vote with 0 reputation");

    if (pV.participants[_voter] == Choice.AYE) {
      pV.ayes.remove(_voter);
      pV.totalAyes = pV.totalAyes.sub(reputation);
    }

    pV.participants[msg.sender] = Choice.NAY;
    pV.nays.add(msg.sender);
    pV.totalNays = pV.totalNays.add(reputation);

    emit NayProposal(_proposalId, _voter);
  }

  function _onNewProposal(uint256 _proposalId) internal {
    bytes32 marker = proposals[_proposalId].marker;

    uint256 blockNumber = block.number.sub(1);
    uint256 totalSupply = _fundRA().totalSupplyAt(blockNumber);
    require(totalSupply > 0, "Total reputation is 0");

    ProposalVoting storage pv = _proposalVotings[_proposalId];

    pv.creationBlock = blockNumber;
    pv.creationTotalSupply = totalSupply;

    (uint256 support, uint256 quorum, uint256 timeout) = _fundStorage().getProposalVotingConfig(marker);
    pv.createdAt = block.timestamp;
    // pv.timeoutAt = block.timestamp + timeout;
    pv.timeoutAt = block.timestamp.add(timeout);

    pv.requiredSupport = support;
    pv.minAcceptQuorum = quorum;
  }

  function _unsafeExecuteProposal(uint256 _proposalId, uint256 _gasToKeep) internal {
    uint256 gasToKeep = 0;
    if (_gasToKeep == 0) {
      gasToKeep = 100000;
    }

    Proposal storage p = proposals[_proposalId];

    p.status = ProposalStatus.EXECUTED;

    (bool ok, bytes memory response) = address(p.destination)
      .call
      .value(p.value)
      .gas(gasleft().sub(gasToKeep))(p.data);

    if (ok == false) {
      p.status = ProposalStatus.ACTIVE;
    }

    emit Execute(_proposalId, msg.sender, ok, response);
  }

  function _canExecute(uint256 _proposalId) internal view returns (bool can, string memory errorReason) {
    Proposal storage p = proposals[_proposalId];
    ProposalVoting storage pv = _proposalVotings[_proposalId];

    // Voting is not executed yet
    if (p.status != ProposalStatus.ACTIVE) {
      return (false, "Proposal isn't active");
    }

    // Voting is already decided
    if (_isValuePct(pv.totalAyes, pv.creationTotalSupply, pv.requiredSupport)) {
      return (true, "");
    }

    // Vote ended?
    if (_isProposalOpen(_proposalId)) {
      return (false, "Proposal is still active");
    }

    // Has enough support?
    uint256 support = getCurrentSupport(_proposalId);
    if (support < pv.requiredSupport) {
      return (false, "Support hasn't been reached");
    }

    // Has min quorum?
    uint256 ayeShare = getAyeShare(_proposalId);
    if (ayeShare < pv.minAcceptQuorum) {
      return (false, "MIN aye quorum hasn't been reached");
    }

    return (true, "");
  }

  function _isValuePct(uint256 _value, uint256 _total, uint256 _pct) internal pure returns (bool) {
    if (_total == 0) {
      return false;
    }

    uint256 computedPct = _value.mul(ONE_HUNDRED_PCT) / _total;
    return computedPct > _pct;
  }

  function _isProposalOpen(uint256 _proposalId) internal view returns (bool) {
    Proposal storage p = proposals[_proposalId];
    ProposalVoting storage pv = _proposalVotings[_proposalId];

    return block.timestamp < pv.timeoutAt && p.status == ProposalStatus.ACTIVE;
  }

  function _fundStorage() internal view returns (IAbstractFundStorage) {
    return IAbstractFundStorage(fundRegistry.getStorageAddress());
  }

  function _fundRA() internal view returns (IFundRA) {
    return IFundRA(fundRegistry.getRAAddress());
  }

  // GETTERS

  function getProposalVoting(
    uint256 _proposalId
  )
    external
    view
    returns (
      uint256 creationBlock,
      uint256 creationTotalSupply,
      uint256 totalAyes,
      uint256 totalNays,
      address[] memory ayes,
      address[] memory nays
    )
  {
    ProposalVoting storage pV = _proposalVotings[_proposalId];

    return (
      pV.creationBlock,
      pV.creationTotalSupply,
      pV.totalAyes,
      pV.totalNays,
      pV.ayes.elements(),
      pV.nays.elements()
    );
  }

  function getProposalVotingProgress(
    uint256 _proposalId
  )
    external
    view
    returns (
      uint256 ayesShare,
      uint256 naysShare,
      uint256 totalAyes,
      uint256 totalNays,
      uint256 currentSupport,
      uint256 requiredSupport,
      uint256 minAcceptQuorum,
      uint256 timeoutAt
    )
  {
    ProposalVoting storage pV = _proposalVotings[_proposalId];

    return (
      getAyeShare(_proposalId),
      getNayShare(_proposalId),
      pV.totalAyes,
      pV.totalNays,
      getCurrentSupport(_proposalId),
      pV.requiredSupport,
      pV.minAcceptQuorum,
      pV.timeoutAt
    );
  }

  function reputationOf(address _address, uint256 _blockNumber) public view returns (uint256) {
    return _fundRA().balanceOfAt(_address, _blockNumber);
  }

  function canExecute(uint256 _proposalId) external view returns (bool can, string memory errorReason) {
    return _canExecute(_proposalId);
  }

  function getParticipantProposalChoice(uint256 _proposalId, address _participant) external view returns (Choice) {
    return _proposalVotings[_proposalId].participants[_participant];
  }

  function getCurrentSupport(uint256 _proposalId) public view returns (uint256) {
    ProposalVoting storage pv = _proposalVotings[_proposalId];

    uint256 totalVotes = pv.totalAyes.add(pv.totalNays);

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

    return pv.totalAyes.mul(ONE_HUNDRED_PCT) / totalVotes;
  }

  function getAyeShare(uint256 _proposalId) public view returns (uint256) {
    ProposalVoting storage p = _proposalVotings[_proposalId];

    return p.totalAyes.mul(ONE_HUNDRED_PCT) / p.creationTotalSupply;
  }

  function getNayShare(uint256 _proposalId) public view returns (uint256) {
    ProposalVoting storage p = _proposalVotings[_proposalId];

    return p.totalNays.mul(ONE_HUNDRED_PCT) / p.creationTotalSupply;
  }
}
function _aye(uint256 _proposalId, address _voter, bool _executeIfDecided) internal {
  ProposalVoting storage pV = _proposalVotings[_proposalId];
  uint256 reputation = reputationOf(_voter, pV.creationBlock);
  require(reputation > 0, "Can't vote with 0 reputation");

  if (pV.participants[_voter] == Choice.NAY) {
    pV.nays.remove(_voter);
    pV.totalNays = pV.totalNays.sub(reputation);
  }

  pV.participants[_voter] = Choice.AYE;
  pV.ayes.add(_voter);
  pV.totalAyes = pV.totalAyes.add(reputation);

  emit AyeProposal(_proposalId, _voter);

  (bool canExecuteThis,) = _canExecute(_proposalId);

  // Fail silently without revert
  if (_executeIfDecided && canExecuteThis) {
    // We've already checked if the vote can be executed with `_canExecute()`
    _unsafeExecuteProposal(_proposalId, 0);
  }
}

// INTERNAL