contract BDXCrowdsale is RefundableCrowdsale {

  uint256[3] public icoStartTimes;
  uint256[3] public icoEndTimes;
  uint256[3] public icoRates;
  uint256[3] public icoCaps;
  uint256[2] public icoVestingTimes;
  uint256[2] public icoVestingTokens;
  uint256 public nextVestingStage = 0;
  uint256 public bizDevTokenAllocation;
  address public bizDevWalletAddress;
  uint256 public marketingTokenAllocation;
  address public marketingWalletAddress;
  bool public contractInitialized = false;
  uint public constant MINIMUM_PURCHASE_EUR_CENT = 1900;
  mapping(uint256 => uint256) public totalTokensByStage;
  mapping(address => uint256) public unsoldTokensBeneficiaries;
  bool public refundingComplete = false;
  uint256 public refundingIndex = 0;
  mapping(address => uint256) public directInvestors;
  address[] private directInvestorsCollection;
  address public rateOracleAddress;
  address public preInvestorsTokenVaultAddress;
  uint256 public preInvestorsTokenAllocation;

  function BDXCrowdsale(
    uint256[3] _icoStartTimes,
    uint256[3] _icoEndTimes,
    uint256[3] _icoRates,
    uint256[3] _icoCaps,
    uint256[2] _icoVestingTimes,
    uint256[2] _icoVestingTokens,
    address _wallet,
    uint256 _goal,
    uint256 _bizDevTokenAllocation,
    address _bizDevWalletAddress,
    uint256 _marketingTokenAllocation,
    address _marketingWalletAddress,
    address _rateOracleAddress,
    uint256 _preInvestorsTokenAllocation
    ) public
    Crowdsale(_icoStartTimes[0], _icoEndTimes[2], _icoRates[0], _wallet, new BDXCoin())
    RefundableCrowdsale(_goal)
  {
    require((_icoCaps[0] > 0) && (_icoCaps[1] > 0) && (_icoCaps[2] > 0));
    require((_icoRates[0] > 0) && (_icoRates[1] > 0) && (_icoRates[2] > 0));
    require((_icoEndTimes[0] > _icoStartTimes[0]) && (_icoEndTimes[1] > _icoStartTimes[1]) && (_icoEndTimes[2] > _icoStartTimes[2]));
    require((_icoStartTimes[1] >= _icoEndTimes[0]) && (_icoStartTimes[2] >= _icoEndTimes[1]));
    require(_bizDevWalletAddress != owner && _wallet != _bizDevWalletAddress);
    require(_marketingWalletAddress != owner && _wallet != _marketingWalletAddress);
    icoStartTimes = _icoStartTimes;
    icoEndTimes = _icoEndTimes;
    icoRates = _icoRates;
    icoCaps = _icoCaps;
    icoVestingTimes = _icoVestingTimes;
    icoVestingTokens = _icoVestingTokens;
    bizDevTokenAllocation = _bizDevTokenAllocation;
    bizDevWalletAddress = _bizDevWalletAddress;
    marketingTokenAllocation = _marketingTokenAllocation;
    marketingWalletAddress = _marketingWalletAddress;
    rateOracleAddress = _rateOracleAddress;
    preInvestorsTokenAllocation = _preInvestorsTokenAllocation;
  }

  // fallback function can be used to buy tokens
  function () external payable {
    require(contractInitialized);
    buyTokens(msg.sender);
  }

  function initializeContract() public onlyOwner {
    require(!contractInitialized);
    preInvestorsTokenVaultAddress = new BDXVault(token);
    BDXCoin bdxcoin = BDXCoin(token);
    bdxcoin.mint(bizDevWalletAddress, toBDXWEI(bizDevTokenAllocation));
    bdxcoin.mint(marketingWalletAddress, toBDXWEI(marketingTokenAllocation));
    bdxcoin.mint(preInvestorsTokenVaultAddress, toBDXWEI(preInvestorsTokenAllocation));
    bdxcoin.setTransferAgent(bizDevWalletAddress, true);
    bdxcoin.setTransferAgent(marketingWalletAddress, true);
    bdxcoin.setTransferAgent(preInvestorsTokenVaultAddress, true);
    contractInitialized = true;
  }


  function vestTokens() public onlyOwner {
    require(isFinalized);
    require(goalReached());
    require(nextVestingStage <= 1);
    require(now > icoVestingTimes[nextVestingStage]);
    BDXCoin bdxcoin = BDXCoin(token);
    bdxcoin.mint(bizDevWalletAddress, toBDXWEI(icoVestingTokens[nextVestingStage]));
    nextVestingStage = nextVestingStage + 1;
  }

  // For Allocating Presold and Sale Via Fiat Deposits
  function allocateTokens(address beneficiary, uint256 tokensWithDecimals, uint256 stage, uint256 rateEurCents, bool isPreSold) public onlyOwner {
    require(stage <= 2);
    uint256 saleAmountEurCents = (tokensWithDecimals.mul(rateEurCents)).div(10**18);
    totalSalesEurCents = totalSalesEurCents.add(saleAmountEurCents);
    if (!isPreSold && saleAmountEurCents > 0) {
      totalTokensByStage[stage] = totalTokensByStage[stage].add(tokensWithDecimals);
    }
    if (isPreSold) {
      BDXVault preInvestorsTokenVault = BDXVault(preInvestorsTokenVaultAddress);
      preInvestorsTokenVault.credit(beneficiary, tokensWithDecimals);
    } else {
      token.transfer(beneficiary, tokensWithDecimals);
    }
  }

  function allocateUnsoldTokens(address beneficiary, uint256 tokensWithDecimals) public onlyOwner {
    require(isFinalized);
    require(goalReached());
    require(unsoldTokensBeneficiaries[beneficiary] == 0);
    unsoldTokensBeneficiaries[beneficiary] = unsoldTokensBeneficiaries[beneficiary].add(tokensWithDecimals);
    token.transfer(beneficiary, tokensWithDecimals);
  }

  //Override
  function buyTokens(address beneficiary) public payable {
    require(contractInitialized);
    require(beneficiary != address(0));
    require(validPurchase());
    RateOracle rateOracle = RateOracle(rateOracleAddress);
    uint ethEurXRate = rateOracle.rate();
    require(ethEurXRate > 0);
    uint256 currTime = now;
    uint256 stageCap = getStageCap(currTime);
    rate = getTokenRate(currTime);
    uint256 stage = getStage(currTime);
    uint256 weiAmount = msg.value;
    uint256 eurCentAmount = (weiAmount.mul(ethEurXRate)).div(10**18);

    require(eurCentAmount > MINIMUM_PURCHASE_EUR_CENT);

    uint256 tokenToGet = (weiAmount.mul(ethEurXRate)).div(rate);

    if (totalTokensByStage[stage].add(tokenToGet) > stageCap) {
      stage = stage + 1;
      rate = getRateByStage(stage);
      tokenToGet = (weiAmount.mul(ethEurXRate)).div(rate);
    }

    totalTokensByStage[stage] = totalTokensByStage[stage].add(tokenToGet);

    if (directInvestors[beneficiary] == 0) {
      directInvestorsCollection.push(beneficiary);
    }

    directInvestors[beneficiary] = directInvestors[beneficiary].add(tokenToGet);
    totalSalesEurCents = totalSalesEurCents.add(eurCentAmount);
    token.transfer(beneficiary, tokenToGet);
    TokenPurchase(msg.sender, beneficiary, weiAmount, tokenToGet);
    forwardFunds();
  }

  function ethToEurXRate() public view returns (uint) {
    RateOracle rateOracle = RateOracle(rateOracleAddress);
    return rateOracle.rate();
  }

  //Override
  function goalReached() public view returns (bool) {
    return totalSalesEurCents >= goal;
  }

  function refundInvestors() public onlyOwner {
    require(isFinalized);
    require(!goalReached());
    require(!refundingComplete);
    for (uint256 i = 0; i < 20; i++) {
      if (refundingIndex >= directInvestorsCollection.length) {
        refundingComplete = true;
        break;
      }
      vault.refund(directInvestorsCollection[refundingIndex]);
      refundingIndex = refundingIndex.add(1);
    }
  }

  function advanceEndTime(uint256 newEndTime) public onlyOwner {
    require(!isFinalized);
    require(newEndTime > endTime);
    endTime = newEndTime;
  }

  function getTokenRate(uint256 currTime) public view returns (uint256) {
    return getRateByStage(getStage(currTime));
  }

  function getStageCap(uint256 currTime) public view returns (uint256) {
    uint256 additionalTokensFromPreviousStage = 0;
    if (getStage(currTime) == 2) {
      additionalTokensFromPreviousStage = additionalTokensFromPreviousStage.add(getCapByStage(1) - totalTokensByStage[1]);
      additionalTokensFromPreviousStage = additionalTokensFromPreviousStage.add(getCapByStage(0) - totalTokensByStage[0]);
    } else if (getStage(currTime) == 1) {
      additionalTokensFromPreviousStage = additionalTokensFromPreviousStage.add(getCapByStage(0) - totalTokensByStage[0]);
    }
    return additionalTokensFromPreviousStage.add(getCapByStage(getStage(currTime)));
  }

  function getStage(uint256 currTime) public view returns (uint256) {
    if (currTime < icoEndTimes[0]) {
      return 0;
    } else if ((currTime > icoEndTimes[0]) && (currTime <= icoEndTimes[1])) {
      return 1;
    } else {
      return 2;
    }
  }

  function getCapByStage(uint256 stage) public view returns (uint256) {
    return icoCaps[stage];
  }

  function getRateByStage(uint256 stage) public view returns (uint256) {
    return icoRates[stage];
  }


  function toBDXWEI(uint256 value) internal view returns (uint256) {
    BDXCoin bdxcoin = BDXCoin(token);
    return (value * (10 ** uint256(bdxcoin.decimals())));
  }

  function finalization() internal {
    super.finalization();
    if (goalReached()) {
      BDXCoin bdxcoin = BDXCoin(token);
      bdxcoin.releaseTokenTransfer();
    }
  }

}
function allocateTokens(address beneficiary, uint256 tokensWithDecimals, uint256 stage, uint256 rateEurCents, bool isPreSold) public onlyOwner {
  require(stage <= 2);
  uint256 saleAmountEurCents = (tokensWithDecimals.mul(rateEurCents)).div(10**18);
  totalSalesEurCents = totalSalesEurCents.add(saleAmountEurCents);
  if (!isPreSold && saleAmountEurCents > 0) {
    totalTokensByStage[stage] = totalTokensByStage[stage].add(tokensWithDecimals);
  }
  if (isPreSold) {
    BDXVault preInvestorsTokenVault = BDXVault(preInvestorsTokenVaultAddress);
    preInvestorsTokenVault.credit(beneficiary, tokensWithDecimals);
  } else {
    token.transfer(beneficiary, tokensWithDecimals);
  }
}

// For Allocating Presold and Sale Via Fiat Deposits