contract DealsSupport is IDealsSupport {
  using SafeMath for uint;

  address public adminAddress;
  address public merchantAddress;

  mapping(uint => Affiliate) public affiliates;
  uint public affiliatesCount;

  mapping(uint => Deal) public deals;
  uint public dealsCount;

  mapping(address => uint[]) public affiliateDealIndexes;
  mapping(address => uint) public affiliateDealIndexesCount;

  mapping(uint => mapping(address => uint)) public dealAffiliateReferenceHash;
  mapping(uint => mapping(address => uint)) public dealAffiliateReferenceHashCount;
  mapping(uint => uint) public referenceHashDealIndex;

  event ApproveAffiliate(bytes4 _dealId, address indexed _merchantAddress, uint indexed _dealIndex, address indexed _affiliateAddress, uint _referenceHash);
  event CreateDeal(bytes4 _dealId, uint _dealIndex, address indexed _merchantAddress, address indexed _rewardTokenAddress, uint _rewardRatePpm, uint _daysOfCancellation, uint _daysBeforeClose, uint _dealsCount);
  event ToggleBlockAffiliate(bytes4 _dealId, uint _referenceHash, bool _isBlocked);
  event DealCancelled(bytes4 _dealId, uint _days, address _who);
  event StakeFilled(bytes4 _dealId, uint _fill, uint _left);
  event RewardClaimed(bytes4 _dealId, uint _referenceHash, uint _rewardAmount);
  event DealUpdated(bytes4 _dealId, uint _newRewardRatePpm);
  event RewardCreated(bytes4 _dealId, uint _referenceHash, uint _purchasedTokenAmount, uint _rewardAmount);

  struct Affiliate {
    address affiliateAddress;
    bool isBlocked;
    uint rewardAmount;
    uint affiliateRewardsCount;
    mapping(uint => AffiliateReward) affiliateRewards;
  }

  struct AffiliateReward {
    uint amount;
    uint createdAt;
  }

  struct Deal {
    address rewardTokenAddress;
    bool isCancelled;
    bool isCancelRequestedByMerchant;
    uint rewardRatePpm;
    uint daysOfCancellation;
    uint daysBeforeClose;
    uint createdAt;
    uint cancelRequestedAt;
    uint leftStakeAmount;
    uint affiliatesCount;
    uint lockedTokensAmount;
    uint[] referenceHashes;

    bytes4 dealId;
    bool allowAdminToAddAffiliate;
  }

  /**
  * Modifiers
  */

  /**
  * Checks that deal is still active, not cancelled or closed
  */
  modifier activeDeal(uint _dealIndex) {
    require(!isDealCancelled(_dealIndex),"Deal is cancelled.");
    require(!isDealClosed(_dealIndex),"Deal is closed.");
    _;
  }

  /**
  * Checks that method is called by admin
  */
  modifier onlyAdmin() {
    require(msg.sender == adminAddress, "Admin rights required.");
    _;
  }

  /**
  * Checks that method is called by merchant
  */
  modifier onlyMerchant() {
    require(msg.sender == merchantAddress, "Merchant rights required.");
    _;
  }

  /**
  * Modifier checks that deal exists
  */
  modifier validDealIndex(uint _dealIndex) {
    require(_dealIndex < dealsCount, "That deal index does not exist.");
    _;
  }

  /**
  * Modifier checks that affiliate exists
  */
  modifier validReferenceHash(uint _referenceHash) {
    // _referenceHash == 0 is reserved for usage in invest() method to distinguish whether user came from affiliate, so _referenceHash == 0 means that user didn't come from any affiliate
    require(_referenceHash != 0, "referenceHash can not be 0");
    require(_referenceHash <= affiliatesCount, "That referenceHash does not exist");
    _;
  }

  /**
  * @dev Contract constructor
  * @param _adminAddress admin address
  * @param _merchantAddress merchant address
  */
  constructor(address _adminAddress, address _merchantAddress) public {
    // validation
    require(_adminAddress != address(0), "Admin address can not be 0.");
    require(_merchantAddress != address(0), "Merchant address can not be 0.");
    // set contract properties
    adminAddress = _adminAddress;
    merchantAddress = _merchantAddress;
    // start affiliate reference hashes from 1 because 0 reference hash is used in invest() to detect whether user has come from affiliate or not
    affiliatesCount = 0;
  }

  /**
  * Public methods
  */

  /**
  * @dev Checks whether a deal can be cancelled by merchant
  * @param _dealIndex deal index
  * @return whether a deal can be cancelled by merchant
  */
  function canBeCancelledByMerchant(uint _dealIndex) public view validDealIndex(_dealIndex) returns(bool) {
    bool canBeCancelled = true;
    if (deals[_dealIndex].isCancelled || deals[_dealIndex].isCancelRequestedByMerchant) {
      canBeCancelled = false;
    }
    return canBeCancelled;
  }

  /**
  * @dev convert Deal ID -> Deal index.
  * @param _dealId deal ID
  * @return dealIndex
  */
  function dealIdToIndex(bytes4 _dealId) public view returns(uint) {
    for (uint i = 0; i<dealsCount; i++) {
      if (deals[i].dealId == _dealId) {
        return i;
      }
    }
    revert("dealID not found");
  }

  /**
  * @dev convert Deal index -> Deal ID
  * @param _dealIndex deal index
  * @return dealId
  */
  //Should
  function dealIndexToId(uint _dealIndex) public view returns(bytes4) {
    return deals[_dealIndex].dealId;
  }

  /**
  * @dev Returns affiliate reward info
  * @param _referenceHash affiliate's reference hash(index)
  * @param _affiliateRewardIndex affiliate reward index
  * @return affiliate reward info
  */
  function getAffiliateRewardInfo(uint _referenceHash, uint _affiliateRewardIndex) public view validReferenceHash(_referenceHash) returns(uint, uint) {
    require(_affiliateRewardIndex < affiliates[_referenceHash].affiliateRewardsCount, "affiliateRewardIndex does not exist.");
    AffiliateReward memory affiliateReward = affiliates[_referenceHash].affiliateRewards[_affiliateRewardIndex];
    return (
      affiliateReward.amount,
      affiliateReward.createdAt
    );
  }

  /**
  * @dev Returns deal index by reference hash
  * @param _referenceHash affiliate's reference hash(index)
  * @return deal index
  */
  function getDealIndex(uint _referenceHash) public view validReferenceHash(_referenceHash) returns(uint) {
    return referenceHashDealIndex[_referenceHash];
  }

  /**
  * @dev Returns array of deal indexes where provided affiliate takes part
  * @param _affiliateAddress affiliate address
  * @return array of deal indexes
  */
  function getDealIndexesByAffiliate(address _affiliateAddress) public view returns(uint[]) {
    require(_affiliateAddress != address(0), "affiliateAddress can not be 0.");
    return affiliateDealIndexes[_affiliateAddress];
  }

  /**
  * @dev Returns dynamic deal info: reference hashes.
  * @param _dealIndex deal index
  * @return array of reference hashes in deal
  */
  function getDynamicDealInfo(uint _dealIndex) public view validDealIndex(_dealIndex) returns(uint[]) {
    return deals[_dealIndex].referenceHashes;
  }

  /**
  * @dev Returns reference hash that should be passed in reference links
  * @param _dealIndex deal index
  * @param _affiliateAddress affiliate address
  * @return reference hash
  */
  function getReferenceHash(uint _dealIndex, address _affiliateAddress) public view validDealIndex(_dealIndex) returns(uint) {
    // validation
    require(_affiliateAddress != address(0), "affiliateAddress can not be 0.");
    require(dealAffiliateReferenceHashCount[_dealIndex][_affiliateAddress] == 1, "dealAffiliateReferenceHash does not exist.");
    // return reference hash
    return dealAffiliateReferenceHash[_dealIndex][_affiliateAddress];
  }

  /**
  * @dev Returns reference hash info: affiliate address and deal index
  * @param _referenceHash affiliate's reference hash(index)
  * @return affiliate address and deal index
  */
  function getReferenceHashInfo(uint _referenceHash) public view validReferenceHash(_referenceHash) returns (address, uint) {
    return (
      affiliates[_referenceHash].affiliateAddress,
      referenceHashDealIndex[_referenceHash]
    );
  }

  /**
  * @dev Checks whether deal is cancelled
  * @param _dealIndex deal index
  * @return bool whether deal is cancelled
  */
  function isDealCancelled(uint _dealIndex) public view validDealIndex(_dealIndex) returns(bool) {
    bool isCancelled = false;
    Deal memory deal = deals[_dealIndex];
    if (deal.isCancelled) {
      isCancelled = true;
    }
    if (deal.isCancelRequestedByMerchant && ((now - deal.cancelRequestedAt) >= (deal.daysOfCancellation * 24 * 60 * 60))) {
      isCancelled = true;
    }
    return isCancelled;
  }

  /**
  * @dev Checks whether deal is closed
  * @param _dealIndex deal index
  * @return bool whether deal is closed
  */
  function isDealClosed(uint _dealIndex) public view validDealIndex(_dealIndex) returns(bool) {
    bool isClosed = false;
    Deal memory deal = deals[_dealIndex];
    if ((now - deal.createdAt) >= (deal.daysBeforeClose * 24 * 60 * 60)) {
      isClosed = true;
    }
    return isClosed;
  }

  /**
  * Admin methods
  */

  /**
  * @dev Blocks/unblocks affiliate. As an admin i would like to block the selected affiliate.
  * @param _referenceHash affiliate's reference hash(index)
  */
  function toggleBlockAffiliate(uint _referenceHash) public onlyAdmin validReferenceHash(_referenceHash) {
    affiliates[_referenceHash].isBlocked = !affiliates[_referenceHash].isBlocked;
    bytes4 dealId = deals[referenceHashDealIndex[_referenceHash]].dealId;
    emit ToggleBlockAffiliate(dealId, _referenceHash, affiliates[_referenceHash].isBlocked);
  }

  /**
  * @dev Cancells deal immediately. As an admin I would like to immediately cancel the selected deal in order to prevent fraudulent behaviour.
  * @notice Will immediately move the selected deal into 'cancelled' state
  * @param _dealIndex deal index
  */
  function cancelDealByAdmin(uint _dealIndex) public onlyAdmin validDealIndex(_dealIndex) {
    deals[_dealIndex].isCancelled = true;
    emit DealCancelled(deals[_dealIndex].dealId, deals[_dealIndex].daysOfCancellation, msg.sender);
  }

  /**
  * @dev Changes admin address(transfer ownership)
  * @param _newAdminAddress new admin address
  */
  function changeAdmin(address _newAdminAddress) public onlyAdmin {
    require(_newAdminAddress != address(0), "newAdmin can not be 0.");
    adminAddress = _newAdminAddress;
  }

  /**
  * Affiliate methods
  */

  /**
  * @dev Withdraws a reward by affiliate. As an affiliate i would like to get my reward for the selected deal in order to get money.
  * Preconditions:
  *  1. The Deal index is valid.
  *  2. Affiliate is not blocked.
  *  3. The reward amount is non zero for the selected affiliate.
  *  4. The reward amount is non zero for the selected deal.
  * @param _dealIndex deal index
  * Result:
  *   All reward tokens are transferred from the current MultistageCrowdsale contract to the affiliate.
  *   The reward amount for the selected affiliate is set to zero.
  *   The reward amount for the selected deal is decreased.
  */
  function getMyReward(uint _dealIndex) public validDealIndex(_dealIndex) {
    // validation
    uint referenceHash = getReferenceHash(_dealIndex, msg.sender);
    require(!affiliates[referenceHash].isBlocked, "Affiliate is blocked.");
    require(affiliates[referenceHash].rewardAmount > 0, "rewardAmount for the affiliate is 0.");
    require(deals[_dealIndex].leftStakeAmount >= affiliates[referenceHash].rewardAmount, "There is not enough stake to pay the reward.");
    // update deal properties
    deals[_dealIndex].leftStakeAmount = deals[_dealIndex].leftStakeAmount.sub(affiliates[referenceHash].rewardAmount);
    // transfer reward tokens to affiliate
    uint tokenAmountToTransfer = affiliates[referenceHash].rewardAmount;
    affiliates[referenceHash].rewardAmount = 0;
    deals[_dealIndex].lockedTokensAmount = deals[_dealIndex].lockedTokensAmount.sub(tokenAmountToTransfer);
    ERC20(deals[_dealIndex].rewardTokenAddress).transfer(affiliates[referenceHash].affiliateAddress, tokenAmountToTransfer);
    emit RewardClaimed(deals[_dealIndex].dealId, referenceHash, tokenAmountToTransfer);
  }

  /**
  * Merchant methods
  */

  /**
  * @dev Approves affiliate address for deal. As a merchant i would like to manually approve selected affiliate in order to provide incentives.
  * @notice The same affiliate address can have different reference hashes because the same affiliate address can participate in multiple deals.
  * @param _dealIndex deal index
  * @param _affiliateAddress affiliate address
  */
  function approveAffiliate(uint _dealIndex, address _affiliateAddress) public validDealIndex(_dealIndex) activeDeal(_dealIndex) {
    if (!deals[_dealIndex].allowAdminToAddAffiliate) {
      require(msg.sender == merchantAddress, "Merchant permission required.");
    } else {
      require((msg.sender == adminAddress) || (msg.sender == merchantAddress), "Admin or merchant permission required.");
    }
    // validation
    require(_affiliateAddress != address(0), "affiliateAddress can not be 0.");
    // check that affiliate is not yet approved for this deal
    require(dealAffiliateReferenceHashCount[_dealIndex][_affiliateAddress] == 0, "Affiliate is already approved for this deal.");
    // create affiliate
    Affiliate memory affiliate;
    affiliate.affiliateAddress = _affiliateAddress;
    // add affiliate to global affiliates property
    affiliatesCount = affiliatesCount.add(1);
    affiliates[affiliatesCount] = affiliate;
    // add affliate to deal
    uint _referenceHash = affiliatesCount;
    deals[_dealIndex].referenceHashes.push(_referenceHash);
    deals[_dealIndex].affiliatesCount = deals[_dealIndex].affiliatesCount.add(1);
    // add deal to affiliate
    affiliateDealIndexes[_affiliateAddress].push(_dealIndex);
    affiliateDealIndexesCount[_affiliateAddress] = affiliateDealIndexesCount[_affiliateAddress].add(1);
    // update globals affiliates count
    dealAffiliateReferenceHash[_dealIndex][_affiliateAddress] = affiliatesCount;
    dealAffiliateReferenceHashCount[_dealIndex][_affiliateAddress] = 1;
    referenceHashDealIndex[affiliatesCount] = _dealIndex;
    // emitting event
    emit ApproveAffiliate(deals[_dealIndex].dealId, msg.sender, _dealIndex, _affiliateAddress, _referenceHash);
  }

  /**
  * @dev Cancels all deals by merchant
  */
  function cancelAllDealsByMerchant() public onlyMerchant {
    for (uint i = 0; i < dealsCount; i++) {
      if (canBeCancelledByMerchant(i)) {
        cancelDealByMerchant(i);
        emit DealCancelled(deals[i].dealId, deals[i].daysOfCancellation, msg.sender);
      }
    }
  }

  /**
  * @dev Marks deal to be cancelled by merchant. As a merchant I would like to request a deal cancelation in order to cancel it after “Cancelation period” is elapsed.
  * @param _dealIndex deal index
  */
  function cancelDealByMerchant(uint _dealIndex) public onlyMerchant validDealIndex(_dealIndex) {
    // validation
    require(canBeCancelledByMerchant(_dealIndex),"Deal can not be cancelled by merchant.");
    // mark deal to be cancelled
    deals[_dealIndex].isCancelRequestedByMerchant = true;
    deals[_dealIndex].cancelRequestedAt = now;
    if (deals[_dealIndex].daysOfCancellation == 0) {
      deals[_dealIndex].isCancelled = true;
    }
    emit DealCancelled(deals[_dealIndex].dealId, deals[_dealIndex].daysOfCancellation, msg.sender);
  }

  /**
  * @dev Creates a new deal by merchant. As a merchant i would like to create new deal in order to add incentives for affiliates.
  * @param _dealId deal dealId
  * @param _rewardTokenAddress ERC20 token address used for affiliates' rewards
  * @param _rewardRatePpm reward rate ppm, 100% == 1000000
  * @param _daysOfCancellation when deal is marked as cancelled it is still live for _daysOfCancellation number of days
  * @param _daysBeforeClose number of days since deal creation when deal is still live, the deal is over and any left over staked tokens can be withdrawn by merchant
  */

  function createDeal(bytes4 _dealId, address _rewardTokenAddress, uint _rewardRatePpm, uint _daysOfCancellation, uint _daysBeforeClose, bool _allowAdminToAddAffiliate) public onlyMerchant {
    // validation
    require(_rewardTokenAddress != address(0), "rewardTokenAddress can not be 0.");
    require(_rewardRatePpm > 0, "rewardRatePpm can not be 0.");
    require(_daysOfCancellation <= 90,"daysOfCancellation can not be >90.");
    require(_daysBeforeClose > 0, "daysBeforeClose should be >0.");
    require(_daysBeforeClose <= 365, "daysBeforeClose can not be >365.");
    // create a new deal
    Deal memory deal;
    deal.dealId = _dealId;
    deal.rewardTokenAddress = _rewardTokenAddress;
    deal.rewardRatePpm = _rewardRatePpm;
    deal.daysOfCancellation = _daysOfCancellation;
    deal.daysBeforeClose = _daysBeforeClose;
    deal.createdAt = now;
    deal.allowAdminToAddAffiliate = _allowAdminToAddAffiliate;
    deals[dealsCount] = deal;

    // update contract properties
    dealsCount = dealsCount.add(1);
    // emitting event

    emit CreateDeal(_dealId, dealsCount - 1, msg.sender, _rewardTokenAddress, _rewardRatePpm, _daysOfCancellation, _daysBeforeClose, dealsCount);
  }

  /**
  * @dev Stakes ERC20 tokens as reward for affiliates by merchant. As a merchant i would like to transfer ERC20 tokens to the selected deal in order to stake them.
  * @notice Merchant should allow _amount of tokens to be withdrawn (use ERC20.approve() method) BEFORE calling this method.
  * Preconditions:
  *  1. The Deal was created by Merchant before
  *  2. isCancelled == false
  *  3. isClosed == false
  *  4. Merchant allowed current MultistageCrowdsale to withdraw correct amount of tokens
  * @param _dealIndex deal index
  * @param _amount amount in wei
  */
  function fillStake(uint _dealIndex, uint _amount) public onlyMerchant validDealIndex(_dealIndex) activeDeal(_dealIndex) {
    // validation
    require(_amount > 0, "amount should be >0.");
    ERC20 rewardToken = ERC20(deals[_dealIndex].rewardTokenAddress);
    require(rewardToken.allowance(msg.sender, address(this)) >= _amount, "Allowance is not enough to send the required amount.");
    // stake tokens for affiliates' rewards
    deals[_dealIndex].leftStakeAmount = deals[_dealIndex].leftStakeAmount.add(_amount);
    rewardToken.transferFrom(msg.sender, address(this), _amount);
    emit StakeFilled(deals[_dealIndex].dealId, _amount, deals[_dealIndex].leftStakeAmount);
  }

  /**
  * @dev Returns tokens at stake back to merchant if deal is cancelled. As a merchant i would like to get my stake back in order to get my tokens back (if deal is cancelled).
  * Preconditions:
  *  1. The Deal was created by Merchant before.
  *  2. The Deal isCancelled==true.
  *  3. The Deal still has some stake left.
  *  4. The Deal has enough tokens to pay to all affiliates.
  * @param _dealIndex deal index
  * Result:
  *    All staked reward tokens are transferred from the current MultistageCrowdsale contract back to merchant.
  *    The “Current reward stake amount” of the selected Deal is decreased.
  */
  function getMyStakeBack(uint _dealIndex) public onlyMerchant validDealIndex(_dealIndex) {
    // validation
    require(isDealCancelled(_dealIndex), "Deal is not cancelled.");
    require(deals[_dealIndex].leftStakeAmount > 0, "There is no stake left.");
    require(deals[_dealIndex].leftStakeAmount >= deals[_dealIndex].lockedTokensAmount, "Stake is lower than lockedTokensAmount.");
    // transfer ERC20 tokens back to merchant
    uint tokenAmountToWithdraw = deals[_dealIndex].leftStakeAmount - deals[_dealIndex].lockedTokensAmount;
    deals[_dealIndex].leftStakeAmount = deals[_dealIndex].lockedTokensAmount;
    ERC20(deals[_dealIndex].rewardTokenAddress).transfer(msg.sender, tokenAmountToWithdraw);
  }

  /**
  * @dev Updates a deal by merchant
  * @param _dealIndex deal index
  * @param _newRewardRatePpm new reward rate ppm, 100% == 1000000
  */
  function updateDeal(uint _dealIndex, uint _newRewardRatePpm) public onlyMerchant validDealIndex(_dealIndex) activeDeal(_dealIndex) {
    require(_newRewardRatePpm > 0, "_newRewardRatePpm should be >0.");
    deals[_dealIndex].rewardRatePpm = _newRewardRatePpm;
    emit DealUpdated(deals[_dealIndex].dealId, _newRewardRatePpm);
  }

  /**
  * Internal methods
  */

  /**
  * @dev Increases affiliate's reward amount
  * @param _referenceHash affiliate's reference hash(index/referenceHash that is passed to invest() function)
  */
  function _rewardAffiliate(uint _referenceHash, uint _purchasedTokenAmount) internal validReferenceHash(_referenceHash) {
    // get deal index by reference hash
    uint dealIndex = getDealIndex(_referenceHash);
    // calculate reward amount for affiliate
    uint rewardAmount = _purchasedTokenAmount.div(1000000).mul(deals[dealIndex].rewardRatePpm);
    // add affiliate reward to affiliate
    AffiliateReward memory affiliateReward;
    affiliateReward.amount = rewardAmount;
    affiliateReward.createdAt = now;
    affiliates[_referenceHash].affiliateRewards[affiliates[_referenceHash].affiliateRewardsCount] = affiliateReward;
    affiliates[_referenceHash].affiliateRewardsCount = affiliates[_referenceHash].affiliateRewardsCount.add(1);
    // update affiliate reward amount
    affiliates[_referenceHash].rewardAmount = affiliates[_referenceHash].rewardAmount.add(rewardAmount);
    // update deal's locked token balance
    deals[dealIndex].lockedTokensAmount = deals[dealIndex].lockedTokensAmount.add(rewardAmount);
    emit RewardCreated(dealIndexToId(dealIndex), _referenceHash, _purchasedTokenAmount, rewardAmount);
  }

}
function updateDeal(uint _dealIndex, uint _newRewardRatePpm) public onlyMerchant validDealIndex(_dealIndex) activeDeal(_dealIndex) {
  require(_newRewardRatePpm > 0, "_newRewardRatePpm should be >0.");
  deals[_dealIndex].rewardRatePpm = _newRewardRatePpm;
  emit DealUpdated(deals[_dealIndex].dealId, _newRewardRatePpm);
}

/**
 * @dev Updates a deal by merchant
 * @param _dealIndex deal index
 * @param _newRewardRatePpm new reward rate ppm, 100% == 1000000
 */