contract TulipMania is ERC20Token {

  /* Utility variable */
  
  uint constant E6 = 10**6;
  
  /* Basic token data */

  string public constant name     = "Tulip Mania";
  string public constant symbol   = "BULB";
  uint8  public constant decimals = 6;

  /* Wallet addresses - initially set to owner at deployment */
  
  address public wallet;
  address public adminWallet;

  /* ICO dates */

  uint public constant DATE_PRESALE_START = 1510758000; // 15-Nov-2017 15:00 UTC
  uint public constant DATE_PRESALE_END   = 1511362800; // 22-Nov-2017 15:00 UTC

  uint public constant DATE_ICO_START = 1511362801; // 22-Nov-2017 15:00:01 UTC
  uint public constant DATE_ICO_END   = 1513868400; // 21-Dec-2017 15:00 UTC

  /* ICO tokens per ETH */
  
  uint public tokensPerEth = 336 * E6;
  uint public constant BONUS_PRESALE = 100;

  /* Other ICO parameters */  
  
  uint public constant TOKEN_SUPPLY_TOTAL = 10000000 * E6; // 10M tokens
  uint public constant TOKEN_SUPPLY_ICO   = 8500000 * E6; // 8.5M tokens
  uint public constant TOKEN_SUPPLY_MKT   =  1500000 * E6; //  1.5M tokens

  uint public constant PRESALE_ETH_CAP =  750 ether;

  uint public constant MIN_CONTRIBUTION = 1 ether / 500; // 0.002 Ether
  uint public constant MAX_CONTRIBUTION = 300 ether;

  uint public constant COOLDOWN_PERIOD =  2 days;
  uint public constant CLAWBACK_PERIOD = 2 days;

  /* Crowdsale variables */

  uint public icoEtherReceived = 0; // Ether actually received by the contract

  uint public tokensIssuedIco   = 0;
  uint public tokensIssuedMkt   = 0;
  
  uint public tokensClaimedAirdrop = 0;
  
  /* Keep track of Ether contributed and tokens received during Crowdsale */
  
  mapping(address => uint) public icoEtherContributed;
  mapping(address => uint) public icoTokensReceived;

  /* Keep track of participants who 
  /* - have received their airdropped tokens after a successful ICO */
  /* - or have reclaimed their contributions in case of failed Crowdsale */
  /* - are locked */
  
  mapping(address => bool) public airdropClaimed;
  mapping(address => bool) public refundClaimed;
  mapping(address => bool) public locked;

  // Events ---------------------------
  
  event WalletUpdated(address _newWallet);
  event AdminWalletUpdated(address _newAdminWallet);
  event TokensPerEthUpdated(uint _tokensPerEth);
  event TokensMinted(address indexed _owner, uint _tokens, uint _balance);
  event TokensIssued(address indexed _owner, uint _tokens, uint _balance, uint _etherContributed);
  event Refund(address indexed _owner, uint _amount, uint _tokens);
  event Airdrop(address indexed _owner, uint _amount, uint _balance);
  event LockRemoved(address indexed _participant);

  // Basic Functions ------------------

  /* Initialize (owner is set to msg.sender by Owned.Owned() */

  function TulipMania() {
    require( TOKEN_SUPPLY_ICO + TOKEN_SUPPLY_MKT == TOKEN_SUPPLY_TOTAL );
    wallet = owner;
    adminWallet = owner;
  }

  /* Fallback */
  
  function () payable {
    buyTokens();
  }
  
  // Information functions ------------
  
  /* What time is it? */
  
  function atNow() constant returns (uint) {
    return now;
  }
  
  /* Are tokens transferable? */

  function isTransferable() constant returns (bool transferable) {
     if ( atNow() < DATE_ICO_END + COOLDOWN_PERIOD ) return false;
     return true;
  }
  
  // Lock functions -------------------

  /* Manage locked */

  function removeLock(address _participant) {
    require( msg.sender == adminWallet || msg.sender == owner );
    locked[_participant] = false;
    LockRemoved(_participant);
  }

  function removeLockMultiple(address[] _participants) {
    require( msg.sender == adminWallet || msg.sender == owner );
    for (uint i = 0; i < _participants.length; i++) {
      locked[_participants[i]] = false;
      LockRemoved(_participants[i]);
    }
  }

  // Owner Functions ------------------
  
  /* Change the crowdsale wallet address */

  function setWallet(address _wallet) onlyOwner {
    require( _wallet != address(0x0) );
    wallet = _wallet;
    WalletUpdated(wallet);
  }

  /* Change the admin wallet address */

  function setAdminWallet(address _wallet) onlyOwner {
    require( _wallet != address(0x0) );
    adminWallet = _wallet;
    AdminWalletUpdated(adminWallet);
  }

  /* Change tokensPerEth before ICO start */
  
  function updateTokensPerEth(uint _tokensPerEth) onlyOwner {
    require( atNow() < DATE_PRESALE_START );
    tokensPerEth = _tokensPerEth;
    TokensPerEthUpdated(_tokensPerEth);
  }

  /* Minting of marketing tokens by owner */

  function mintMarketing(address _participant, uint _tokens) onlyOwner {
    // check amount
    require( _tokens <= TOKEN_SUPPLY_MKT.sub(tokensIssuedMkt) );
    
    // update balances
    balances[_participant] = balances[_participant].add(_tokens);
    tokensIssuedMkt        = tokensIssuedMkt.add(_tokens);
    tokensIssuedTotal      = tokensIssuedTotal.add(_tokens);
    
    // locked
    locked[_participant] = true;
    
    // log the minting
    Transfer(0x0, _participant, _tokens);
    TokensMinted(_participant, _tokens, balances[_participant]);
  }

  /* Owner clawback of remaining funds after clawback period */
  /* (for use in case of a failed Crowdsale) */
  
  function ownerClawback() external onlyOwner {
    require( atNow() > DATE_ICO_END + CLAWBACK_PERIOD );
    wallet.transfer(this.balance);
  }

  /* Transfer out any accidentally sent ERC20 tokens */

  function transferAnyERC20Token(address tokenAddress, uint amount) onlyOwner returns (bool success) {
      return ERC20Interface(tokenAddress).transfer(owner, amount);
  }

  // Private functions ----------------

  /* Accept ETH during crowdsale (called by default function) */

  function buyTokens() private {
    uint ts = atNow();
    bool isPresale = false;
    bool isIco = false;
    uint tokens = 0;
    
    // minimum contribution
    require( msg.value >= MIN_CONTRIBUTION );
    
    // one address transfer hard cap
    require( icoEtherContributed[msg.sender].add(msg.value) <= MAX_CONTRIBUTION );

    // check dates for presale or ICO
    if (ts > DATE_PRESALE_START && ts < DATE_PRESALE_END) isPresale = true;  
    if (ts > DATE_ICO_START && ts < DATE_ICO_END) isIco = true;  
    require( isPresale || isIco );

    // presale cap in Ether
    if (isPresale) require( icoEtherReceived.add(msg.value) <= PRESALE_ETH_CAP );
    
    // get baseline number of tokens
    tokens = tokensPerEth.mul(msg.value) / 1 ether;
    
    // apply presale bonus
    if (isPresale) {
      tokens = tokens.mul(100 + BONUS_PRESALE) / 100;
    }
    
    // ICO token volume cap
    require( tokensIssuedIco.add(tokens) <= TOKEN_SUPPLY_ICO );

    // register tokens
    balances[msg.sender]          = balances[msg.sender].add(tokens);
    icoTokensReceived[msg.sender] = icoTokensReceived[msg.sender].add(tokens);
    tokensIssuedIco               = tokensIssuedIco.add(tokens);
    tokensIssuedTotal             = tokensIssuedTotal.add(tokens);
    
    // register Ether
    icoEtherReceived                = icoEtherReceived.add(msg.value);
    icoEtherContributed[msg.sender] = icoEtherContributed[msg.sender].add(msg.value);
    
    // locked
    locked[msg.sender] = true;
    
    // log token issuance
    Transfer(0x0, msg.sender, tokens);
    TokensIssued(msg.sender, tokens, balances[msg.sender], msg.value);

    wallet.transfer(this.balance);
  }
  
  // ERC20 functions ------------------

  /* Override "transfer" (ERC20) */

  function transfer(address _to, uint _amount) returns (bool success) {
    require( isTransferable() );
    require( locked[msg.sender] == false );
    require( locked[_to] == false );
    return super.transfer(_to, _amount);
  }
  
  /* Override "transferFrom" (ERC20) */

  function transferFrom(address _from, address _to, uint _amount) returns (bool success) {
    require( isTransferable() );
    require( locked[_from] == false );
    require( locked[_to] == false );
    return super.transferFrom(_from, _to, _amount);
  }

  // External functions ---------------

  /* Reclaiming of funds by contributors in case of a failed crowdsale */
  /* (it will fail if account is empty after ownerClawback) */

  /* While there could not have been any token transfers yet, a contributor */
  /* may have received minted tokens, so the token balance after a refund */ 
  /* may still be positive */
  
  function reclaimFunds() external {
    uint tokens; // tokens to destroy
    uint amount; // refund amount
    
    // ico is finished
    require( atNow() > DATE_ICO_END);
    
    // check if refund has already been claimed
    require( !refundClaimed[msg.sender] );
    
    // check if there is anything to refund
    require( icoEtherContributed[msg.sender] > 0 );
    
    // update variables affected by refund
    tokens = icoTokensReceived[msg.sender];
    amount = icoEtherContributed[msg.sender];

    balances[msg.sender] = balances[msg.sender].sub(tokens);
    tokensIssuedTotal    = tokensIssuedTotal.sub(tokens);
    
    refundClaimed[msg.sender] = true;
    
    // transfer out refund
    msg.sender.transfer(amount);
    
    // log
    Transfer(msg.sender, 0x0, tokens);
    Refund(msg.sender, amount, tokens);
  }

  /* Claiming of "airdropped" tokens in case of successful crowdsale */
  /* Can be done by token holder, or by adminWallet */ 

  function claimAirdrop() external {
    doAirdrop(msg.sender);
  }

  function adminClaimAirdrop(address _participant) external {
    require( msg.sender == adminWallet );
    doAirdrop(_participant);
  }

  function adminClaimAirdropMultiple(address[] _addresses) external {
    require( msg.sender == adminWallet );
    for (uint i = 0; i < _addresses.length; i++) doAirdrop(_addresses[i]);
  }  
  
  function doAirdrop(address _participant) internal {
    uint airdrop = computeAirdrop(_participant);

    require( airdrop > 0 );

    // update balances and token issue volume
    airdropClaimed[_participant] = true;
    balances[_participant] = balances[_participant].add(airdrop);
    tokensIssuedTotal      = tokensIssuedTotal.add(airdrop);
    tokensClaimedAirdrop   = tokensClaimedAirdrop.add(airdrop);
    
    // log
    Airdrop(_participant, airdrop, balances[_participant]);
    Transfer(0x0, _participant, airdrop);
  }

  /* Function to estimate airdrop amount. For some accounts, the value of */
  /* tokens received by calling claimAirdrop() may be less than gas costs */
  
  /* If an account has tokens from the ico, the amount after the airdrop */
  /* will be newBalance = tokens * TOKEN_SUPPLY_ICO / tokensIssuedIco */
      
  function computeAirdrop(address _participant) constant returns (uint airdrop) {
    // return 0 if it's too early
    if ( atNow() < DATE_ICO_END ) return 0;
    
    // return 0 if the airdrop was already claimed
    if( airdropClaimed[_participant] ) return 0;

    // return 0 if the account does not hold any crowdsale tokens
    if( icoTokensReceived[_participant] == 0 ) return 0;
    
    // airdrop amount
    uint tokens = icoTokensReceived[_participant];
    uint newBalance = tokens.mul(TOKEN_SUPPLY_ICO) / tokensIssuedIco;
    airdrop = newBalance - tokens;
  }  

  /* Multiple token transfers from one address to save gas */
  /* (longer _amounts array not accepted = sanity check) */

  function transferMultiple(address[] _addresses, uint[] _amounts) external {
    require( isTransferable() );
    require( locked[msg.sender] == false );
    require( _addresses.length == _amounts.length );
    for (uint i = 0; i < _addresses.length; i++) {
      if (locked[_addresses[i]] == false) super.transfer(_addresses[i], _amounts[i]);
    }
  }  

}
// ----------------------------------------------------------------------------
//
// TulipMania public token sale
//
// ----------------------------------------------------------------------------
function transfer(address _to, uint _amount) returns (bool success) {
  require( isTransferable() );
  require( locked[msg.sender] == false );
  require( locked[_to] == false );
  return super.transfer(_to, _amount);
}

/* Override "transfer" (ERC20) */