contract AcjCrowdsale is Ownable {


    using SafeMath for uint256;

    /* Bonuses */

    // Presale bonus percentage
    uint public constant BONUS_PRESALE = 10;            
    // Medium bonus percentage
    uint public constant BONUS_MID = 10;                
    // High bonus percentage
    uint public constant BONUS_HI = 20;                 
    // Medium bonus threshold
    uint public constant BONUS_MID_QTY = 150 ether;     
    // High bonus threshold
    uint public constant BONUS_HI_QTY = 335 ether;      

    /* Absolute dates as timestamps */

    uint public startPresale;            
    uint public endPresale;             
    uint public startIco;              
    uint public endIco;               

    // 30 days refund period on fail
    uint public constant REFUND_PERIOD = 30 days;

    /* Tokens **/

    // Indicative token balances during the crowdsale 
    mapping(address => uint256) public tokenBalances;    
    // Token smart contract address
    address public token;
    // Total tokens created
    uint256 public tokensTotalSupply; 
    // Tokens available for sale
    uint256 public tokensForSale;    
    // Tokens sold via buyTokens
    uint256 public tokensSold;                             
    // Tokens created during the sale
    uint256 public tokensDistributed;                      
    // soft cap in ETH
    uint256 public tokensSoftCap;                          
    // ICO flat rate subject to bonuses
    uint256 public ethTokenRate;                             

    /* Management */

    // Allow multiple administrators
    mapping(address => bool) public admins;                

    /* Various */

    // Total wei received 
    uint256 public weiReceived;                            
    // Minimum contribution in ETH
    uint256 public weiMinContribution = 1 ether;           
    // Contributions in wei for each address
    mapping(address => uint256) public contributions;
    // Refund state for each address
    mapping(address => bool) public refunds;

    /* Wallets */

    // Company wallet that will receive the ETH
    address public companyWallet;                           

    /* Events */

    // Yoohoo someone contributed !
    event Contribute(address indexed _from, uint _amount); 
    // Token <> ETH rate updated
    event TokenRateUpdated(uint _newRate);                  
    // ETH Refund 
    event Refunded(address indexed _from, uint _amount);    

    /* Modifiers */ 

    modifier belowTotalSupply {
        require(tokensDistributed < tokensTotalSupply);
        _;
    }

    modifier belowHardCap {
        require(tokensDistributed < tokensForSale);
        _;
    }

    modifier adminOnly {
        require(msg.sender == owner || admins[msg.sender] == true);
        _;
    }

    modifier crowdsaleFailed {
        require(isFailed());
        _;
    }

    modifier crowdsaleSuccess {
        require(isSuccess());
        _;
    }

    modifier duringSale {
        require(now < endIco);
        require((now > startPresale && now < endPresale) || now > startIco);
        _;
    }

    modifier afterSale {
        require(now > endIco);
        _;
    }

    modifier aboveMinimum {
        require(msg.value >= weiMinContribution);
        _;
    }

    /* Public methods */

    /* 
     * Constructor
     * Creating the new Token smart contract
     * and setting its owner to the current sender
     * 
     */
    function AcjCrowdsale(
            uint _presaleStart, 
            uint _presaleEnd, 
            uint _icoStart, 
            uint _icoEnd, 
            uint256 _rate, 
            uint256 _cap, 
            uint256 _goal, 
            uint256 _totalSupply,
            address _token
            ) public {

        require(_presaleEnd > _presaleStart);
        require(_icoStart > _presaleEnd);
        require(_icoEnd > _icoStart);

        require(_rate > 0); 
        require(_cap > 0);
        require(_goal > 0);
        require(_totalSupply > _goal);

        startPresale = _presaleStart;
        endPresale = _presaleEnd;
        startIco = _icoStart;
        endIco = _icoEnd;

        ethTokenRate = _rate;        
        tokensSoftCap = _cap.mul(1 ether);
        tokensForSale = _goal.mul(1 ether);
        tokensTotalSupply = _totalSupply.mul(1 ether);

        admins[msg.sender] = true;
        companyWallet = msg.sender;

        token = _token;
    }


    /*
     * Fallback payable
     */
    function () external payable {

        buyTokens(msg.sender);
    }


    /* Crowdsale staff only */


    /*
     * Admin management
     */
    function addAdmin(address _adr) external onlyOwner {

        require(_adr != address(0));
        admins[_adr] = true;
    }

    function removeAdmin(address _adr) external onlyOwner {

        require(_adr != address(0));
        admins[_adr] = false;
    }

    /*
     * Change the company wallet
     */
    function updateCompanyWallet(address _wallet) external adminOnly {

        companyWallet = _wallet;
    }

    /*
     *  Change the owner of the token
     */
    function proposeTokenOwner(address _newOwner) external adminOnly {

        AcjToken _token = AcjToken(token);
        _token.proposeNewOwner(_newOwner);
    }

    function acceptTokenOwnership() external onlyOwner {
        
        AcjToken _token = AcjToken(token);
        _token.acceptOwnership();

    }

    /*
     * Activate the token
     */
    function activateToken() external adminOnly crowdsaleSuccess afterSale {
        AcjToken _token = AcjToken(token);
        _token.activate();
    }

    /* 
     * Adjust the token value before the ICO
     */
    function adjustTokenExchangeRate(uint _rate) external adminOnly {

        require(now > endPresale && now < startIco);
        ethTokenRate = _rate;
        TokenRateUpdated(_rate);
    }

    /* 
     * Start therefund period
     * Each contributor has to claim own  ETH 
     */     
    function refundContribution() external crowdsaleFailed afterSale {

        require(!refunds[msg.sender]);
        require(contributions[msg.sender] > 0);

        uint256 _amount = contributions[msg.sender];
        tokenBalances[msg.sender] = 0;
        refunds[msg.sender] = true;
        Refunded(msg.sender, contributions[msg.sender]);
        msg.sender.transfer(_amount);

    }

    /*
     * After the refund period, remaining tokens
     * are transfered to the company wallet
     * Allow withdrawal at any time if the ICO is a success.
     */     
    function withdrawUnclaimed() external adminOnly {

        require(now > endIco + REFUND_PERIOD || isSuccess());
        companyWallet.transfer(this.balance);
    }

    /*
     * Pre-ICO and offline Investors, collaborators and team tokens
     */
    function reserveTokens(address _beneficiary, uint256 _tokensQty) external adminOnly belowTotalSupply {

//        require(_beneficiary != address(0));
        uint _distributed = tokensDistributed.add(_tokensQty);

        require(_distributed <= tokensTotalSupply);

        tokenBalances[_beneficiary] = _tokensQty.add(tokenBalances[_beneficiary]);
        tokensDistributed = _distributed;

        AcjToken _token = AcjToken(token);
        _token.initialTransfer(_beneficiary, _tokensQty);
    }


    /*
     * Actually buy the tokens
     * requires an active sale time
     * and amount above the minimum contribution
     * and sold tokens inferior to tokens for sale
     */     
    function buyTokens(address _beneficiary) public payable duringSale aboveMinimum belowHardCap {

        require(_beneficiary != address(0));
        uint256 _weiAmount = msg.value;        
        uint256 _tokensQty = msg.value.mul(getBonus(_weiAmount));
        uint256 _distributed = _tokensQty.add(tokensDistributed);
        uint256 _sold = _tokensQty.add(tokensSold);

        require(_distributed <= tokensTotalSupply);
        require(_sold <= tokensForSale);

        contributions[_beneficiary] = _weiAmount.add(contributions[_beneficiary]);
        tokenBalances[_beneficiary] = _tokensQty.add(tokenBalances[_beneficiary]);
        weiReceived = weiReceived.add(_weiAmount);
        tokensDistributed = _distributed;
        tokensSold = _sold;

        Contribute(_beneficiary, msg.value);

        AcjToken _token = AcjToken(token);
        _token.initialTransfer(_beneficiary, _tokensQty);
    }

    /*
     * Crowdsale Helpers 
     */
    function hasEnded() public view returns(bool) {

        return now > endIco;
    }

    /*
     * Checks if the crowdsale is a success
     */
    function isSuccess() public view returns(bool) {

        if (tokensSold >= tokensSoftCap) {
            return true;
        }
        return false;
    }

    /*
     * Checks if the crowdsale failed
     */
    function isFailed() public view returns(bool) {

        if (tokensSold < tokensSoftCap && now > endIco) {
            return true;
        }
        return false;
    }

    /* 
     * Bonus calculations
     * Either time or ETH quantity based 
     */
    function getBonus(uint256 _wei) internal constant returns(uint256 ethToAcj) {

        uint256 _bonus = 0;

        // Time based bonus
        if (endPresale > now) {
            _bonus = _bonus.add(BONUS_PRESALE); 
        }

        // ETH Quantity based bonus
        if (_wei >= BONUS_HI_QTY) { 
            _bonus = _bonus.add(BONUS_HI);
        } else if (_wei >= BONUS_MID_QTY) {
            _bonus = _bonus.add(BONUS_MID);
        }

        return ethTokenRate.mul(100 + _bonus) / 100;
    }

}
function buyTokens(address _beneficiary) public payable duringSale aboveMinimum belowHardCap {

    require(_beneficiary != address(0));
    uint256 _weiAmount = msg.value;        
    uint256 _tokensQty = msg.value.mul(getBonus(_weiAmount));
    uint256 _distributed = _tokensQty.add(tokensDistributed);
    uint256 _sold = _tokensQty.add(tokensSold);

    require(_distributed <= tokensTotalSupply);
    require(_sold <= tokensForSale);

    contributions[_beneficiary] = _weiAmount.add(contributions[_beneficiary]);
    tokenBalances[_beneficiary] = _tokensQty.add(tokenBalances[_beneficiary]);
    weiReceived = weiReceived.add(_weiAmount);
    tokensDistributed = _distributed;
    tokensSold = _sold;

    Contribute(_beneficiary, msg.value);

    AcjToken _token = AcjToken(token);
    _token.initialTransfer(_beneficiary, _tokensQty);
}

/*
 * Actually buy the tokens
 * requires an active sale time
 * and amount above the minimum contribution
 * and sold tokens inferior to tokens for sale
 */