contract BsCrowdsale is SafeMath, Ownable, Pausable {

    enum Currency { BTC, LTC, DASH, ZEC, WAVES, USD, EUR }

    struct Backer {
        uint256 weiReceived; // Amount of wei given by backer
        uint256 tokensSent;  // Amount of tokens received in return to the given amount of ETH.
    }

    // TODO rename to buyers?
    // (buyer_eth_address -> struct)
    mapping(address => Backer) public backers;

    // currency_code => (tx_hash => tokens)
    mapping(uint => mapping(bytes32 => uint256)) public externalTxs;

    BsToken public token;           // Token contract reference.
    address public beneficiary;     // Address that will receive ETH raised during this crowdsale.
    address public notifier;        // Address that can this crowdsale about changed external conditions.

    uint256 public usdPerEth;
    uint256 public usdPerEthMin = 200; // Lowest boundary of USD/ETH rate
    uint256 public usdPerEthMax = 500; // Highest boundary of USD/ETH rate

    struct UsdPerEthLog {
        uint256 rate;
        uint256 time;
        address changedBy;
    }

    UsdPerEthLog[] public usdPerEthLog; // History of used rates of USD/ETH

    uint256 public minInvestCents = 1;              // Because 1 token = 1 cent.
    uint256 public maxCapInCents  = 10 * 1e6 * 100; // 10m USD in cents.

    uint256 public tokensPerCents = 1 * 1e18;       // Ordinary price is 1 token = 1 USD cent.
    uint256 public tokensPerCentsDayOne = 1.1 * 1e18;
    uint256 public tokensPerCentsWeekOne = 1.05 * 1e18;

    // TODO set to last value from SnovWhitelist contract.
    uint256 public totalInCents = 71481400; // Total amount of USD raised during this crowdsale including (wei -> USD) + (external txs in USD).

    // Stats for current crowdsale
    uint256 public totalWeiReceived = 0;   // Total amount of wei received during this crowdsale smart contract.
    uint256 public totalTokensSold = 0;    // Total amount of tokens sold during this crowdsale.
    uint256 public totalEthSales = 0;      // Total amount of ETH contributions during this crowdsale.
    uint256 public totalExternalSales = 0; // Total amount of external contributions (USD, BTC, etc.) during this crowdsale.

    uint256 public startTime = 1509451200; // 2017-10-31 12:00:00Z
    uint256 public endTime   = 1512043200; // 2017-11-30 12:00:00Z

    // Use in bonuses:
    uint256 oneDayTime       = 1509537600; // 2017-11-01 12:00:00Z
    uint256 oneWeekTime      = 1510056000; // 2017-11-07 12:00:00Z

    uint256 public finalizedTime = 0;      // Unix timestamp when finalize() was called.

    bool public saleEnabled = true;       // if true, then contract will not sell tokens on payment received

    event BeneficiaryChanged(address indexed _oldAddress, address indexed _newAddress);
    event NotifierChanged(address indexed _oldAddress, address indexed _newAddress);
    event UsdPerEthChanged(uint256 _oldRate, uint256 _newRate);

    event EthReceived(address indexed _buyer, uint256 _amountInWei);
    event ExternalSale(Currency _currency, string _txHash, address indexed _buyer, uint256 _amountInCents, uint256 _tokensE18);

    modifier respectTimeFrame() {
        require(isSaleOn());
        _;
    }

    modifier canNotify() {
        require(msg.sender == owner || msg.sender == notifier);
        _;
    }

    function BsCrowdsale(address _token, address _beneficiary, uint256 _usdPerEth) {
        token = BsToken(_token);

        owner = msg.sender;
        notifier = 0x73E5B12017A141d41c1a14FdaB43a54A4f9BD490;
        beneficiary = _beneficiary;

        setUsdPerEth(_usdPerEth);
    }

    // Override this method to mock current time.
    function getNow() public constant returns (uint256) {
        return now;
    }

    function setSaleEnabled(bool _enabled) public onlyOwner {
        saleEnabled = _enabled;
    }

    function setBeneficiary(address _beneficiary) public onlyOwner {
        BeneficiaryChanged(beneficiary, _beneficiary);
        beneficiary = _beneficiary;
    }

    function setNotifier(address _notifier) public onlyOwner {
        NotifierChanged(notifier, _notifier);
        notifier = _notifier;
    }

    function setUsdPerEth(uint256 _usdPerEth) public canNotify {
        if (_usdPerEth < usdPerEthMin || _usdPerEth > usdPerEthMax) throw;

        UsdPerEthChanged(usdPerEth, _usdPerEth);
        usdPerEth = _usdPerEth;
        usdPerEthLog.push(UsdPerEthLog({ rate: usdPerEth, time: getNow(), changedBy: msg.sender }));
    }

    function usdPerEthLogSize() public constant returns (uint256) {
        return usdPerEthLog.length;
    }

    /*
     * The fallback function corresponds to a donation in ETH
     */
    function() public payable {
        if (saleEnabled) sellTokensForEth(msg.sender, msg.value);
    }

    function sellTokensForEth(address _buyer, uint256 _amountInWei) internal ifNotPaused respectTimeFrame {

        uint256 amountInCents = weiToCents(_amountInWei);
        require(amountInCents >= minInvestCents);

        totalInCents = safeAdd(totalInCents, amountInCents);
        require(totalInCents <= maxCapInCents); // If max cap reached.

        uint256 tokensSold = centsToTokens(amountInCents);
        require(token.sell(_buyer, tokensSold)); // Transfer tokens to buyer.

        totalWeiReceived = safeAdd(totalWeiReceived, _amountInWei);
        totalTokensSold = safeAdd(totalTokensSold, tokensSold);
        totalEthSales++;

        Backer backer = backers[_buyer];
        backer.tokensSent = safeAdd(backer.tokensSent, tokensSold);
        backer.weiReceived = safeAdd(backer.weiReceived, _amountInWei);  // Update the total wei collected during the crowdfunding for this backer

        EthReceived(_buyer, _amountInWei);
    }

    function weiToCents(uint256 _amountInWei) public constant returns (uint256) {
        return safeDiv(safeMul(_amountInWei, usdPerEth * 100), 1 ether);
    }

    function centsToTokens(uint256 _amountInCents) public constant returns (uint256) {
        uint256 rate = tokensPerCents;
        uint _now = getNow();

        if (startTime <= _now && _now < oneDayTime) rate = tokensPerCentsDayOne;
        else if (oneDayTime <= _now && _now < oneWeekTime) rate = tokensPerCentsWeekOne;

        return safeMul(_amountInCents, rate);
    }

    function externalSale(
        Currency _currency,
        string _txHash,
        address _buyer,
        uint256 _amountInCents,
        uint256 _tokensE18
    ) internal ifNotPaused canNotify {

        require(_buyer > 0 && _amountInCents > 0 && _tokensE18 > 0);

        var txsByCur = externalTxs[uint(_currency)];
        bytes32 txKey = keccak256(_txHash);

        // If this foreign transaction has been already processed in this contract.
        require(txsByCur[txKey] == 0);

        totalInCents = safeAdd(totalInCents, _amountInCents);
        require(totalInCents < maxCapInCents); // Max cap should not be reached yet.

        require(token.sell(_buyer, _tokensE18)); // Transfer tokens to buyer.

        totalTokensSold = safeAdd(totalTokensSold, _tokensE18);
        totalExternalSales++;

        txsByCur[txKey] = _tokensE18;
        ExternalSale(_currency, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForBtc(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.BTC, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForLtc(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.LTC, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForDash(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.DASH, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForZec(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.ZEC, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForWaves(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.WAVES, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForUsd(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.USD, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function sellTokensForEur(string _txHash, address _buyer, uint256 _amountInCents, uint256 _tokensE18) public {
        externalSale(Currency.EUR, _txHash, _buyer, _amountInCents, _tokensE18);
    }

    function tokensByExternalTx(Currency _currency, string _txHash) internal constant returns (uint256) {
        return externalTxs[uint(_currency)][keccak256(_txHash)];
    }

    function tokensByBtcTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.BTC, _txHash);
    }

    function tokensByLtcTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.LTC, _txHash);
    }

    function tokensByDashTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.DASH, _txHash);
    }

    function tokensByZecTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.ZEC, _txHash);
    }

    function tokensByWavesTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.WAVES, _txHash);
    }

    function tokensByUsdTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.USD, _txHash);
    }

    function tokensByEurTx(string _txHash) public constant returns (uint256) {
        return tokensByExternalTx(Currency.EUR, _txHash);
    }

    function totalSales() public constant returns (uint256) {
        return safeAdd(totalEthSales, totalExternalSales);
    }

    function isMaxCapReached() public constant returns (bool) {
        return totalInCents >= maxCapInCents;
    }

    function isSaleOn() public constant returns (bool) {
        uint256 _now = getNow();
        return startTime <= _now && _now <= endTime;
    }

    function isSaleOver() public constant returns (bool) {
        return getNow() > endTime;
    }

    function isFinalized() public constant returns (bool) {
        return finalizedTime > 0;
    }

    /*
     * Finalize the crowdsale. Raised money can be sent to beneficiary only if crowdsale hit end time or max cap (15m USD).
     */
    function finalize() public onlyOwner {

        // Cannot finalise before end day of crowdsale until max cap is reached.
        require(isMaxCapReached() || isSaleOver());

        beneficiary.transfer(this.balance);

        finalizedTime = getNow();
    }
}
/**
 * In this crowdsale We assume that ETH rate is 320 USD/ETH
 */
function getNow() public constant returns (uint256) {
    return now;
}

// Override this method to mock current time.