contract SignkeysBonusProgram is Ownable {
    using SafeMath for uint256;

    /* Token contract */
    SignkeysToken public token;

    /* Crowdsale contract */
    SignkeysCrowdsale public crowdsale;

    /* SignkeysBonusProgramRewards contract to keep bonus state */
    SignkeysBonusProgramRewards public bonusProgramRewards;

    /* Ranges in which we transfer the given amount of tokens as reward. See arrays below.
     For example, if this array is [199, 1000, 10000] and referrerRewards array is [5, 50],
       it considers as follows:
       [0, 199] - 0 tokens
       [200, 1000] - 5 tokens
       [1001, 10000] - 50 tokens,
       > 10000 - 50 tokens
       */
    uint256[] public referralBonusTokensAmountRanges = [199, 1000, 10000, 100000, 1000000, 10000000];

    /* Amount of tokens as bonus for referrer according to referralBonusTokensAmountRanges */
    uint256[] public referrerRewards = [5, 50, 500, 5000, 50000];

    /* Amount of tokens as bonus for buyer according to referralBonusTokensAmountRanges */
    uint256[] public buyerRewards = [5, 50, 500, 5000, 50000];

    /* Purchase amount ranges in cents for any purchase.
     For example, if this array is [2000, 1000000, 10000000] and purchaseRewardsPercents array is [10, 15, 20],
       it considers as follows:
       [2000, 1000000) - 10% of tokens
       [1000000, 10000000) - 15% of tokens
       > 100000000 - 20% tokens
       */
    uint256[] public purchaseAmountRangesInCents = [2000, 1000000, 10000000];

    /* Percetage of reward for any purchase according to purchaseAmountRangesInCents */
    uint256[] public purchaseRewardsPercents = [10, 15, 20];

    event BonusSent(
        address indexed referrerAddress,
        uint256 referrerBonus,
        address indexed buyerAddress,
        uint256 buyerBonus,
        uint256 purchaseBonus,
        uint256 couponBonus
    );

    constructor(address _token, address _bonusProgramRewards) public {
        token = SignkeysToken(_token);
        bonusProgramRewards = SignkeysBonusProgramRewards(_bonusProgramRewards);
    }

    function setCrowdsaleContract(address _crowdsale) public onlyOwner {
        crowdsale = SignkeysCrowdsale(_crowdsale);
    }

    function setBonusProgramRewardsContract(address _bonusProgramRewards) public onlyOwner {
        bonusProgramRewards = SignkeysBonusProgramRewards(_bonusProgramRewards);
    }

    /* Calculate bonus for the given amount of tokens according to referralBonusTokensAmountRanges
    and rewards array which is one of referrerRewards or buyerRewards */
    function calcBonus(uint256 tokensAmount, uint256[] rewards) private view returns (uint256) {
        uint256 multiplier = 10 ** uint256(token.decimals());
        if (tokensAmount <= multiplier.mul(referralBonusTokensAmountRanges[0])) {
            return 0;
        }
        for (uint i = 1; i < referralBonusTokensAmountRanges.length; i++) {
            uint min = referralBonusTokensAmountRanges[i - 1];
            uint max = referralBonusTokensAmountRanges[i];
            if (tokensAmount > min.mul(multiplier) && tokensAmount <= max.mul(multiplier)) {
                return multiplier.mul(rewards[i - 1]);
            }
        }
        if (tokensAmount >= referralBonusTokensAmountRanges[referralBonusTokensAmountRanges.length - 1].mul(multiplier)) {
            return multiplier.mul(rewards[rewards.length - 1]);
        }
    }

    function calcPurchaseBonus(uint256 amountCents, uint256 tokensAmount) private view returns (uint256) {
        if (amountCents < purchaseAmountRangesInCents[0]) {
            return 0;
        }
        for (uint i = 1; i < purchaseAmountRangesInCents.length; i++) {
            if (amountCents >= purchaseAmountRangesInCents[i - 1] && amountCents < purchaseAmountRangesInCents[i]) {
                return tokensAmount.mul(purchaseRewardsPercents[i - 1]).div(100);
            }
        }
        if (amountCents >= purchaseAmountRangesInCents[purchaseAmountRangesInCents.length - 1]) {
            return tokensAmount.mul(purchaseRewardsPercents[purchaseAmountRangesInCents.length - 1]).div(100);
        }
    }

    /* Having referrer, buyer, amount of purchased tokens, value of purchased tokens in cents and coupon campaign id
    this method transfer all the required bonuses to referrer and buyer */
    function sendBonus(address referrer, address buyer, uint256 _tokensAmount, uint256 _valueCents, uint256 _couponCampaignId) external returns (uint256)  {
        require(msg.sender == address(crowdsale), "Bonus may be sent only by crowdsale contract");

        uint256 referrerBonus = 0;
        uint256 buyerBonus = 0;
        uint256 purchaseBonus = 0;
        uint256 couponBonus = 0;

        uint256 referrerBonusAmount = calcBonus(_tokensAmount, referrerRewards);
        uint256 buyerBonusAmount = calcBonus(_tokensAmount, buyerRewards);
        uint256 purchaseBonusAmount = calcPurchaseBonus(_valueCents, _tokensAmount);

        if (referrer != 0x0 && !bonusProgramRewards.areReferralBonusesSent(buyer)) {
            if (referrerBonusAmount > 0 && token.balanceOf(this) > referrerBonusAmount) {
                token.transfer(referrer, referrerBonusAmount);
                bonusProgramRewards.setReferralBonusesSent(buyer, true);
                referrerBonus = referrerBonusAmount;
            }

            if (buyerBonusAmount > 0 && token.balanceOf(this) > buyerBonusAmount) {
                bonusProgramRewards.setReferralBonusesSent(buyer, true);
                buyerBonus = buyerBonusAmount;
            }
        }

        if (token.balanceOf(this) > purchaseBonusAmount.add(buyerBonus)) {
            purchaseBonus = purchaseBonusAmount;
        }

        if (_couponCampaignId > 0 && !bonusProgramRewards.isCouponUsed(buyer, _couponCampaignId)) {
            if (
                token.balanceOf(this) > purchaseBonusAmount
                .add(buyerBonus)
                .add(bonusProgramRewards.getCouponCampaignBonusTokensAmount(_couponCampaignId))
            ) {
                bonusProgramRewards.setCouponUsed(buyer, _couponCampaignId, true);
                couponBonus = bonusProgramRewards.getCouponCampaignBonusTokensAmount(_couponCampaignId);
            }
        }

        if (buyerBonus > 0 || purchaseBonus > 0 || couponBonus > 0) {
            token.transfer(buyer, buyerBonus.add(purchaseBonus).add(couponBonus));
        }

        emit BonusSent(referrer, referrerBonus, buyer, buyerBonus, purchaseBonus, couponBonus);
    }

    function getReferralBonusTokensAmountRanges() public view returns (uint256[]) {
        return referralBonusTokensAmountRanges;
    }

    function getReferrerRewards() public view returns (uint256[]) {
        return referrerRewards;
    }

    function getBuyerRewards() public view returns (uint256[]) {
        return buyerRewards;
    }

    function getPurchaseRewardsPercents() public view returns (uint256[]) {
        return purchaseRewardsPercents;
    }

    function getPurchaseAmountRangesInCents() public view returns (uint256[]) {
        return purchaseAmountRangesInCents;
    }

    function setReferralBonusTokensAmountRanges(uint[] ranges) public onlyOwner {
        referralBonusTokensAmountRanges = ranges;
    }

    function setReferrerRewards(uint[] rewards) public onlyOwner {
        require(rewards.length == referralBonusTokensAmountRanges.length - 1);
        referrerRewards = rewards;
    }

    function setBuyerRewards(uint[] rewards) public onlyOwner {
        require(rewards.length == referralBonusTokensAmountRanges.length - 1);
        buyerRewards = rewards;
    }

    function setPurchaseAmountRangesInCents(uint[] ranges) public onlyOwner {
        purchaseAmountRangesInCents = ranges;
    }

    function setPurchaseRewardsPercents(uint[] rewards) public onlyOwner {
        require(rewards.length == purchaseAmountRangesInCents.length);
        purchaseRewardsPercents = rewards;
    }

    /* Withdraw all tokens from contract for any emergence case */
    function withdrawTokens() external onlyOwner {
        uint256 amount = token.balanceOf(this);
        address tokenOwner = token.owner();
        token.transfer(tokenOwner, amount);
    }
}
function withdrawTokens() external onlyOwner {
    uint256 amount = token.balanceOf(this);
    address tokenOwner = token.owner();
    token.transfer(tokenOwner, amount);
}

/* Withdraw all tokens from contract for any emergence case */