contract MenloSaleBase is Ownable {
  using SafeMath for uint256;

  // Whitelisted investors
  mapping (address => bool) public whitelist;

  // Special role used exclusively for managing the whitelist
  address public whitelister;

  // manual early close flag
  bool public isFinalized;

  // cap for crowdsale in wei
  uint256 public cap;

  // The token being sold
  MenloToken public token;

  // start and end timestamps where contributions are allowed (both inclusive)
  uint256 public startTime;
  uint256 public endTime;

  // address where funds are collected
  address public wallet;

  // amount of raised money in wei
  uint256 public weiRaised;

  /**
   * @dev Throws if called by any account other than the whitelister.
   */
  modifier onlyWhitelister() {
    require(msg.sender == whitelister, "Sender should be whitelister");
    _;
  }

  /**
   * event for token purchase logging
   * @param purchaser who bought the tokens
   * @param value weis paid for purchase
   * @param amount amount of tokens purchased
   */
  event TokenPurchase(address indexed purchaser, uint256 value, uint256 amount);

  /**
   * event for token redemption logging
   * @param purchaser who bought the tokens
   * @param amount amount of tokens redeemed
   */
  event TokenRedeem(address indexed purchaser, uint256 amount);

  // termination early or otherwise
  event Finalized();

  event TokensRefund(uint256 amount);

  /**
   * event refund of excess ETH if purchase is above the cap
   * @param amount amount of ETH (in wei) refunded
   */
  event Refund(address indexed purchaser, uint256 amount);

  constructor(
      MenloToken _token,
      uint256 _startTime,
      uint256 _endTime,
      uint256 _cap,
      address _wallet
  ) public {
    require(_startTime >= getBlockTimestamp(), "Start time should be in the future");
    require(_endTime >= _startTime, "End time should be after start time");
    require(_wallet != address(0), "Wallet address should be non-zero");
    require(_token != address(0), "Token address should be non-zero");
    require(_cap > 0, "Cap should be greater than zero");

    token = _token;

    startTime = _startTime;
    endTime = _endTime;
    cap = _cap;
    wallet = _wallet;
  }

  // fallback function can be used to buy tokens
  function () public payable {
    buyTokens();
  }

  // Abstract methods
  function calculateBonusRate() public view returns (uint256);
  function buyTokensHook(uint256 _tokens) internal;

  function buyTokens() public payable returns (uint256) {
    require(whitelist[msg.sender], "Expected msg.sender to be whitelisted");
    checkFinalize();
    require(!isFinalized, "Should not be finalized when purchasing");
    require(getBlockTimestamp() >= startTime && getBlockTimestamp() <= endTime, "Should be during sale");
    require(msg.value != 0, "Value should not be zero");
    require(token.balanceOf(this) > 0, "This contract must have tokens");

    uint256 _weiAmount = msg.value;

    uint256 _remainingToFund = cap.sub(weiRaised);
    if (_weiAmount > _remainingToFund) {
      _weiAmount = _remainingToFund;
    }

    uint256 _totalTokens = _weiAmount.mul(calculateBonusRate());
    if (_totalTokens > token.balanceOf(this)) {
      // Change _wei to buy rest of remaining tokens
      _weiAmount = token.balanceOf(this).div(calculateBonusRate());
    }

    token.unpause();
    weiRaised = weiRaised.add(_weiAmount);

    forwardFunds(_weiAmount);
    uint256 _weiToReturn = msg.value.sub(_weiAmount);
    if (_weiToReturn > 0) {
      msg.sender.transfer(_weiToReturn);
      emit Refund(msg.sender, _weiToReturn);
    }

    uint256 _tokens = ethToTokens(_weiAmount);
    emit TokenPurchase(msg.sender, _weiAmount, _tokens);
    buyTokensHook(_tokens);
    token.pause();

    checkFinalize();

    return _tokens;
  }

  // Allows the owner to take back the tokens that are assigned to the sale contract.
  function refund() external onlyOwner returns (bool) {
    require(hasEnded(), "Sale should have ended when refunding");
    uint256 _tokens = token.balanceOf(address(this));

    if (_tokens == 0) {
      return false;
    }

    require(token.transfer(owner, _tokens), "Expected token transfer to succeed");

    emit TokensRefund(_tokens);

    return true;
  }

  /// @notice interface for founders to whitelist investors
  /// @param _addresses array of investors
  /// @param _status enable or disable
  function whitelistAddresses(address[] _addresses, bool _status) public onlyWhitelister {
    for (uint256 i = 0; i < _addresses.length; i++) {
      address _investorAddress = _addresses[i];
      if (whitelist[_investorAddress] != _status) {
        whitelist[_investorAddress] = _status;
      }
    }
  }

  function setWhitelister(address _whitelister) public onlyOwner {
    whitelister = _whitelister;
  }

  function checkFinalize() public {
    if (hasEnded()) {
      finalize();
    }
  }

  function emergencyFinalize() public onlyOwner {
    finalize();
  }

  function withdraw() public onlyOwner {
    owner.transfer(address(this).balance);
  }

  function hasEnded() public constant returns (bool) {
    if (isFinalized) {
      return true;
    }
    bool _capReached = weiRaised >= cap;
    bool _passedEndTime = getBlockTimestamp() > endTime;
    return _passedEndTime || _capReached;
  }

  // @dev does not require that crowdsale `hasEnded()` to leave safegaurd
  // in place if ETH rises in price too much during crowdsale.
  // Allows team to close early if cap is exceeded in USD in this event.
  function finalize() internal {
    require(!isFinalized, "Should not be finalized when finalizing");
    emit Finalized();
    isFinalized = true;
    token.transferOwnership(owner);
  }

  // send ether to the fund collection wallet
  // override to create custom fund forwarding mechanisms
  function forwardFunds(uint256 _amount) internal {
    wallet.transfer(_amount);
  }

  function ethToTokens(uint256 _ethAmount) internal view returns (uint256) {
    return _ethAmount.mul(calculateBonusRate());
  }

  function getBlockTimestamp() internal view returns (uint256) {
    return block.timestamp;
  }
}
function whitelistAddresses(address[] _addresses, bool _status) public onlyWhitelister {
  for (uint256 i = 0; i < _addresses.length; i++) {
    address _investorAddress = _addresses[i];
    if (whitelist[_investorAddress] != _status) {
      whitelist[_investorAddress] = _status;
    }
  }
}

/// @notice interface for founders to whitelist investors
/// @param _addresses array of investors
/// @param _status enable or disable