contract UpDownMarket is Ownable, ReentrancyGuard {

  using SafeMath for uint;

  uint public constant MIN_BID_TIME = 1 minutes;

  IORACLE public oracle;
  address public oraclePair;
  address public feeRecipient;

  uint public latestBidTime = 30 minutes; // before resolution

  mapping (uint => uint) public resolvedTo; // per epoch: 1 = up, 2 = down
  mapping (uint => uint) public totalSharesUp; // per epoch
  mapping (uint => uint) public totalSharesDown; // per epoch
  mapping (uint => uint) public purchasedEpoch; // per epoch
  mapping (uint => mapping (address => uint)) public purchasedOfUp; // epoch[account]
  mapping (uint => mapping (address => uint)) public purchasedOfDown; // epoch[account]
  mapping (uint => mapping (address => uint)) public sharesOfUp; // epoch[account]
  mapping (uint => mapping (address => uint)) public sharesOfDown; // epoch[account]

  // For the sake of simplicity, purchasedOf* and sharesOfUp* had different values in the previous implementation
  // but are equal in this implementation

  uint public currentEpoch = 0;
  uint public lastTWAP;
  uint public feeBps = 0;
  uint public maxSupply = 200e18; // ETH

  modifier checkTime() {
    require(isTimeOpen(block.timestamp), "UpDownMarket: buying is closed");
    _;
  }

  modifier enforceLimits() {
    _;
    require(purchasedEpoch[currentEpoch] <= maxSupply, "UpDownMarket: maxSupply reached");
  }

  constructor(IORACLE _oracle, address _oraclePair, uint _startTime) public {
    oracle = _oracle;
    oraclePair = _oraclePair;
    oracle.initialize();
    oracle.addPair(oraclePair, _startTime);
    lastTWAP = oracle.price0Last(oraclePair);
  }

  function buyUp(uint _minShares) public payable checkTime enforceLimits nonReentrant {
    _mintUp(msg.sender, currentEpoch, msg.value, _minShares);
  }

  function buyDown(uint _minShares) public payable checkTime enforceLimits nonReentrant {
    _mintDown(msg.sender, currentEpoch, msg.value, _minShares);
  }

  function claim(uint _epoch) public nonReentrant {
    uint transferAmount = winAmount(_epoch, msg.sender);
    require(transferAmount > 0, "UpDownMarket: not a winner");

    uint fee = transferAmount.mul(feeBps).div(10000);
    _sendEth(feeRecipient, _epoch, fee);
    transferAmount = transferAmount.sub(fee);

    _sendEth(msg.sender, _epoch, transferAmount);
    _burnAll(msg.sender, _epoch);
  }

  function resolve() public {
    require(oracle.isUpdateRequired(oraclePair), "UpDownMarket: too early");
    
    oracle.update(oraclePair);

    uint currentTWAP = oracle.price0Last(oraclePair);

    if (currentTWAP > lastTWAP) {
      resolvedTo[currentEpoch] = 1;
      _claimDustUp(currentEpoch);

    } else if (currentTWAP < lastTWAP) {
      resolvedTo[currentEpoch] = 2;
      _claimDustDown(currentEpoch);
    
    } else {
      revert("UpDownMarket: twap not changed yet");
    }

    currentEpoch = currentEpoch.add(1);
    lastTWAP = currentTWAP;
  }

  function setFeeBps(uint _value) public onlyOwner {
    feeBps = _value;
  }

  function setFeeRecipent(address _feeRecipient) public onlyOwner {
    feeRecipient = _feeRecipient;
  }

  function setLatestBidTime(uint _latestBidTime) public onlyOwner {
    require(latestBidTime > MIN_BID_TIME, "UpDownMarket: latestBidTime too short");
    latestBidTime = _latestBidTime;
  }

  function setMaxSupply(uint _maxSupply) public onlyOwner {
    maxSupply = _maxSupply;
  }

  function totalSupplyEpoch(uint _epoch) public view returns(uint) {
    return totalSharesUp[_epoch].add(totalSharesDown[_epoch]);
  }

  function winAmount(uint _epoch, address _account) public view returns(uint) {

    // Up won
    if (resolvedTo[_epoch] == 1) {
      if (totalSharesUp[_epoch] == 0) {
        return 0;
      } else {
        return purchasedEpoch[_epoch].mul(sharesOfUp[_epoch][_account]).div(totalSharesUp[_epoch]);
      }

    // Down won
    } else if (resolvedTo[_epoch] == 2) {
      if (totalSharesDown[_epoch] == 0) {
        return 0;
      } else {
        return purchasedEpoch[_epoch].mul(sharesOfDown[_epoch][_account]).div(totalSharesDown[_epoch]);
      }
    } else {
      return 0;
    }
  }

  // Buying closes latestBidTime seconds before resolution
  function isTimeOpen(uint _timestamp) public view returns(bool) {
    return block.timestamp < oracle.nextUpdateAt(oraclePair).sub(latestBidTime);
  }

  // UP wins but there is nobody to collect the winnings = claim as fees
  function _claimDustUp(uint _epoch) internal {
    if (totalSharesUp[_epoch] == 0) {
      _sendEth(feeRecipient, _epoch, purchasedEpoch[_epoch]);
      purchasedEpoch[_epoch] = 0;
    }
  }

  // DOWN wins but there is nobody to collect the winnings = claim as fees
  function _claimDustDown(uint _epoch) internal {
    if (totalSharesDown[_epoch] == 0) {
      _sendEth(feeRecipient, _epoch, purchasedEpoch[_epoch]);
      purchasedEpoch[_epoch] = 0;
    }
  }

  function _mintUp(address _account, uint _epoch, uint _amount, uint _minShares) internal {
    uint shares = _shares(_amount, _minShares);
    purchasedOfUp[_epoch][_account] = purchasedOfUp[_epoch][_account].add(_amount);
    purchasedEpoch[_epoch] = purchasedEpoch[_epoch].add(_amount);
    totalSharesUp[_epoch] = totalSharesUp[_epoch].add(shares);
    sharesOfUp[_epoch][_account] = sharesOfUp[_epoch][_account].add(shares);
  }

  function _mintDown(address _account, uint _epoch, uint _amount, uint _minShares) internal {
    uint shares = _shares(_amount, _minShares);
    purchasedOfDown[_epoch][_account] = purchasedOfDown[_epoch][_account].add(_amount);
    purchasedEpoch[_epoch] = purchasedEpoch[_epoch].add(_amount);
    totalSharesDown[_epoch] = totalSharesDown[_epoch].add(shares);
    sharesOfDown[_epoch][_account] = sharesOfDown[_epoch][_account].add(shares);
  }

  function _shares(uint _amount, uint _minShares) internal view returns(uint) {
    uint shares = _amount;
    require(shares > _minShares, "UpDownMarket: shares > _minShares");
    return shares;
  }

  function _burnUp(address _account, uint _epoch, uint _amount) internal {
    totalSharesUp[_epoch] = totalSharesUp[_epoch].sub(_amount);
    sharesOfUp[_epoch][_account] = sharesOfUp[_epoch][_account].sub(_amount);
  }

  function _burnDown(address _account, uint _epoch, uint _amount) internal {
    totalSharesDown[_epoch] = totalSharesDown[_epoch].sub(_amount);
    sharesOfDown[_epoch][_account] = sharesOfDown[_epoch][_account].sub(_amount);
  }

  function _burnAll(address _account, uint _epoch) internal {
    _burnUp(_account, _epoch, sharesOfUp[_epoch][_account]);
    _burnDown(_account, _epoch, sharesOfDown[_epoch][_account]);
  }

  function _sendEth(address _recipient, uint _epoch, uint _amount) internal {
    purchasedEpoch[_epoch] = purchasedEpoch[_epoch].sub(_amount);
    address(_recipient).call { value: _amount } (new bytes(0));
  }
}
function _claimDustDown(uint _epoch) internal {
  if (totalSharesDown[_epoch] == 0) {
    _sendEth(feeRecipient, _epoch, purchasedEpoch[_epoch]);
    purchasedEpoch[_epoch] = 0;
  }
}

// DOWN wins but there is nobody to collect the winnings = claim as fees