pragma solidity ^0.4.19;

//--------- OpenZeppelin's Safe Math
//Source : https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/math/SafeMath.sol
/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {
  	function mul(uint256 a, uint256 b) internal pure returns (uint256) {
		if (a == 0) {
		return 0;
		}
		uint256 c = a * b;
		assert(c / a == b);
		return c;
	}

  	function div(uint256 a, uint256 b) internal pure returns (uint256) {
    	uint256 c = a / b;
    	return c;
  	}

  	function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    	assert(b <= a);
    	return a - b;
  	}

  	function add(uint256 a, uint256 b) internal pure returns (uint256) {
    	uint256 c = a + b;
    	assert(c >= a);
    	return c;
  	}
}
//-----------------------------------------------------B


//"EXTERN" CONTRACTS
//============================
// ERC20 Interface: https://github.com/ethereum/EIPs/issues/20
contract ERC20 {
  	function transfer(address _to, uint256 _value) public returns (bool success);
  	function balanceOf(address _owner) public constant returns (uint256 balance);
}

contract Viscous {
    function is_whitelisted(address) constant returns (bool);
}
//============================

contract Controller {

	address public owner;
  	//event ChangeOwner(address new_owner);

	modifier onlyOwner {
    	require(msg.sender == owner);
    	_;
  	}

  	function change_owner(address new_owner) onlyOwner {
    	require(new_owner != 0x0);
    	owner = new_owner;
    	//ChangeOwner(new_owner);
  	}

  	function Controller() {
    	owner = msg.sender;
  	}
}

contract Contract is Controller {

  //EVENTS
  //============================
  /* event BuyTheTokens(address sale, uint256 amount, bytes data);
  event Whitelist(bool boolean);
  event Contribution(address sender, uint256 amount, uint256 total_amount, uint256 contract_balance);
  event Withdraw(address sender, uint256 amount);
  event Refund(address sender, uint256 amount, uint256 total_amount, uint256 contract_balance); */
  //============================

	using SafeMath for uint256;

  	struct Contributor {
		uint256 balance;
	    uint256 fee;
	    uint8 rounds;
	    bool whitelisted;
  	}

	struct Snapshot {
		uint256 tokens_balance;
		uint256 eth_balance;
	}

	modifier minAmountReached {
		require(this.balance >= min_amount);
		_;
	}

  	modifier underMaxAmount {
    	require(max_amount == 0 || this.balance <= max_amount);
    	_;
  	}

	//FEES RELATED
	//============================
	address constant public DEVELOPER1 = 0x8C006d807EBAe91F341a4308132Fd756808e0126;
	address constant public DEVELOPER2 = 0x63F7547Ac277ea0B52A0B060Be6af8C5904953aa;
	uint256 constant public FEE_DEV = 670; //0.2% fee per dev -> so 0.4% fee in total
	//============================

	//VARIABLES TO BE CHANGED BY OWNER
	//============================
	uint256 public FEE_OWNER; //value as divisor (ie. 1 / FEE_OWNER = % Rate) or (1 / 200 = 0.4%)
	uint256 public max_amount;  //0 means there is no limit
	uint256 public min_amount;
	uint256 public individual_cap;
	uint256 public gas_price_max;
	uint8 public rounds;
	//flag controlled by owner to enable/disable whitelists for viscous or temporary whitelist
	bool public whitelist_enabled;
	//Contract containing the data
	Viscous public viscous_contract = Viscous(0x0);
	//============================

  //###############################################################################################################################

	//CONTRACT VARIABLES
	//============================
	//=== ARRAYS & MAPPINGS ===
	mapping (address => Contributor) public contributors;
	//First element will be the first wave of tokens, and so forth
	Snapshot[] public snapshots;

	//=== UINT ===
	// Record ETH value of tokens currently held by contract.
	uint256 public const_contract_eth_value;
	//The reduction of the allocation in % | example : 40 -> 40% reduction
	uint256 public percent_reduction;

	//=== ADDRESSES ===
	//The address of the contact.
	address public sale;
	//Token address
	ERC20 public token;
	//=== BOOLS ===
	//Track whether the contract has bought the tokens yet.
	bool public bought_tokens;
	//Track if the owner partially refunds his fee in the event of a partial refund post-buy
	bool public owner_supplied_eth;
	bool public allow_contributions = true;
  //============================

	function Contract(
		uint256 _max_amount,
		uint256 _min_amount,
		bool _whitelist,
		uint256 _owner_fee_divisor
		) {
			max_amount = calculate_with_fees(_max_amount);  //0 means there is no limit
		  	min_amount = calculate_with_fees(_min_amount);
		  	whitelist_enabled = _whitelist;
		  	FEE_OWNER = _owner_fee_divisor;
		  	Contributor storage contributor = contributors[msg.sender];
		  	contributor.whitelisted = true;
  		}

  //###############################################################################################################################

	//OWNER FUNCTIONS
	//============================
	// Buy the tokens. Sends ETH to the presale wallet and records the ETH amount held in the contract.
	function buy_the_tokens(bytes _data) onlyOwner minAmountReached {
		//Avoids burning the funds
		require(!bought_tokens && sale != 0x0);
		//Record that the contract has bought the tokens.
		bought_tokens = true;
		const_contract_eth_value = this.balance;
		take_fees_eth_dev();
		take_fees_eth_owner();
		//Record the amount of ETH sent as the contract's current value.
		const_contract_eth_value = this.balance;
		// Transfer all the funds to the crowdsale address.
		require(sale.call.gas(msg.gas).value(this.balance)(_data));
		//BuyTheTokens(sale, const_contract_eth_value, _data);
	}

	//These two functions concern the "temporary" whitelist
	function whitelist_addys(address[] _addys) onlyOwner {
		for (uint256 i = 0; i < _addys.length; i++) {
			Contributor storage contributor = contributors[_addys[i]];
			contributor.whitelisted = true;
		}
	}

	function blacklist_addys(address[] _addys) onlyOwner {
		for (uint256 i = 0; i < _addys.length; i++) {
			Contributor storage contributor = contributors[_addys[i]];
			contributor.whitelisted = false;
		}
	}

	function set_gas_price_max(uint256 _gas_price) onlyOwner {
		gas_price_max = _gas_price;
	}

	function set_sale_address(address _sale) onlyOwner {
		//Avoid mistake of putting 0x0
		require(_sale != 0x0);
		sale = _sale;
	}

	function set_token_address(address _token) onlyOwner {
		require(_token != 0x0);
		token = ERC20(_token);
	}

	function set_allow_contributions(bool _boolean) onlyOwner {
		allow_contributions = _boolean;
	}

	function set_tokens_received() onlyOwner {
		tokens_received();
	}

	function set_percent_reduction(uint256 _reduction) onlyOwner payable {
		require(bought_tokens && rounds == 0 && _reduction <= 100);
		percent_reduction = _reduction;
		if (msg.value > 0) {
			owner_supplied_eth = true;
		}
		//we substract by contract_eth_value*_reduction basically
		const_contract_eth_value = const_contract_eth_value.sub((const_contract_eth_value.mul(_reduction)).div(100));
	}

	function set_whitelist_enabled(bool _boolean) onlyOwner {
		whitelist_enabled = _boolean;
		//Whitelist(_boolean);
	}

	function change_viscous_contract(address _addy) onlyOwner {
		viscous_contract = Viscous(_addy);
	}

	function change_individual_cap(uint256 _cap) onlyOwner {
		individual_cap = _cap;
	}

	function change_max_amount(uint256 _amount) onlyOwner {
		//ATTENTION! The new amount should be in wei
		//Use https://etherconverter.online/
		max_amount = calculate_with_fees(_amount);
	}

	function change_min_amount(uint256 _amount) onlyOwner {
		//ATTENTION! The new amount should be in wei
		//Use https://etherconverter.online/
		min_amount = calculate_with_fees(_amount);
	}

	function change_fee(uint256 _fee) onlyOwner {
		FEE_OWNER = _fee;
	}

	function emergency_token_withdraw(address _address) onlyOwner {
	 	ERC20 temp_token = ERC20(_address);
		require(temp_token.transfer(msg.sender, temp_token.balanceOf(this)));
	}

	function emergency_eth_withdraw() onlyOwner {
		msg.sender.transfer(this.balance);
	}

//###############################################################################################################################


	//INTERNAL FUNCTIONS
	//============================
	// Allows any user to withdraw his tokens.
	function withdraw(address _user) internal {
		// Disallow withdraw if tokens haven't been bought yet.
		require(bought_tokens);
		uint256 contract_token_balance = token.balanceOf(address(this));
		// Disallow token withdrawals if there are no tokens to withdraw.
		require(contract_token_balance != 0);
		Contributor storage contributor = contributors[_user];
		if (contributor.rounds < rounds) {
            //contributor can claim his bonus tokens of previous rounds if he didn't withdrawn
            //uint256 this_contribution_claim = (rounds-contributor.rounds)*contributor.balance;
			Snapshot storage snapshot = snapshots[contributor.rounds];
            uint256 tokens_to_withdraw = contributor.balance.mul(snapshot.tokens_balance).div(snapshot.eth_balance);
			snapshot.tokens_balance = snapshot.tokens_balance.sub(tokens_to_withdraw);
			snapshot.eth_balance = snapshot.eth_balance.sub(contributor.balance);
            // Update the value of tokens currently held by the contract.
            //contract_eth_value -= contributor.balance;
            contributor.rounds++;
            // Send the funds.  Throws on failure to prevent loss of funds.
            require(token.transfer(_user, tokens_to_withdraw));
            //Withdraw(_user, tokens_to_withdraw);
        }
	}

	// Allows any user to get his eth refunded before the purchase is made.
	function refund(address _user) internal {
		require(!bought_tokens && percent_reduction == 0);
		Contributor storage contributor = contributors[_user];
		uint256 eth_to_withdraw = contributor.balance.add(contributor.fee);
		// Update the user's balance prior to sending ETH to prevent recursive call.
		contributor.balance = 0;
		contributor.fee = 0;
		// Return the user's funds.  Throws on failure to prevent loss of funds.
		_user.transfer(eth_to_withdraw);
		//Refund(_user, eth_to_withdraw, contributor.balance, this.balance);
	}

	//Allows any user to get a part of his ETH refunded, in proportion
	//to the % reduced of the allocation
	function partial_refund(address _user) internal {
		require(bought_tokens && rounds == 0 && percent_reduction > 0);
		Contributor storage contributor = contributors[_user];
		require(contributor.rounds == 0);
		uint256 eth_to_withdraw = contributor.balance.mul(percent_reduction).div(100);
		contributor.balance = contributor.balance.sub(eth_to_withdraw);
		if (owner_supplied_eth) {
			//dev fees aren't refunded, only owner fees
			//We don't care about updatng contributor's fee, it doesn't matter for receiving the tokens
			uint256 fee = contributor.fee.mul(percent_reduction).div(100);
			eth_to_withdraw = eth_to_withdraw.add(fee);
		}
		_user.transfer(eth_to_withdraw);
		//Refund(_user, eth_to_withdraw, contributor.balance, this.balance);
	}

	function take_fees_eth_dev() internal {
		if (FEE_DEV != 0) {
			DEVELOPER1.transfer(const_contract_eth_value.div(FEE_DEV));
			DEVELOPER2.transfer(const_contract_eth_value.div(FEE_DEV));
		}
	}

	function take_fees_eth_owner() internal {
	//Owner takes fees on the ETH in this case
	//In case owner doesn't want to take fees
		if (FEE_OWNER != 0) {
			owner.transfer(const_contract_eth_value.div(FEE_OWNER));
		}
	}

	function calculate_with_fees(uint256 _amount) internal returns (uint256) {
		//divided by two because 2 devs, so 0.4% in total
		uint256 temp = _amount;
		if (FEE_DEV != 0) {
			temp = temp.add(_amount.div(FEE_DEV/2));
		}
		if (FEE_OWNER != 0) {
			temp = temp.add(_amount.div(FEE_OWNER));
		}
		return temp;
	}

	function tokens_received() internal {
		//We need to check the previous token balance
		uint256 previous_balance;
		for (uint8 i = 0; i < snapshots.length; i++) {
			previous_balance = previous_balance.add(snapshots[i].tokens_balance);
		}
		snapshots.push(Snapshot(token.balanceOf(address(this)).sub(previous_balance), const_contract_eth_value));
		//we don't leave out the tokens that didn't get withdrawn
		rounds++;
	}


//###############################################################################################################################

  //PUBLIC FUNCTIONS
  //============================

  function tokenFallback(address _from, uint _value, bytes _data) {
		if (ERC20(msg.sender) == token) {
			tokens_received();
		}
	}

	function withdraw_my_tokens() {
		for (uint8 i = contributors[msg.sender].rounds; i < rounds; i++) {
			withdraw(msg.sender);
		}
	}

	function withdraw_tokens_for(address _addy) {
		for (uint8 i = contributors[_addy].rounds; i < rounds; i++) {
			withdraw(_addy);
		}
	}

	function refund_my_ether() {
		refund(msg.sender);
	}

	function partial_refund_my_ether() {
		partial_refund(msg.sender);
	}

	function provide_eth() payable {}

	// Default function.  Called when a user sends ETH to the contract.
	function () payable underMaxAmount {
		require(!bought_tokens && allow_contributions && (gas_price_max == 0 || tx.gasprice <= gas_price_max));
		Contributor storage contributor = contributors[msg.sender];
		//Checks if contributor is whitelisted
		if (whitelist_enabled) {
			require(contributor.whitelisted || viscous_contract.is_whitelisted(msg.sender));
		}
		//Manages cases of dev and/or owner taking fees
		//"Worst case", substract 0 from the msg.value
		uint256 fee = 0;
		if (FEE_OWNER != 0) {
			fee = SafeMath.div(msg.value, FEE_OWNER);
		}
		uint256 fees = fee;
		if (FEE_DEV != 0) {
			fee = msg.value.div(FEE_DEV/2);
			fees = fees.add(fee);
		}
		//Updates both of the balances
		contributor.balance = contributor.balance.add(msg.value).sub(fees);
		contributor.fee = contributor.fee.add(fees);

		//Checks if the individual cap is respected
		//If it's not, changes are reverted
		require(individual_cap == 0 || contributor.balance <= individual_cap);
		//Contribution(msg.sender, msg.value, contributor.balance, this.balance);
	}
}