pragma solidity ^0.5.0;

interface PriceWatcherI
{
    function getUSDcentsPerETH() external view returns (uint256 _USDcentsPerETH);
}


contract PriceWatcherPlaceholder is PriceWatcherI
{
    function getUSDcentsPerETH() external view returns (uint256 _USDcentsPerETH)
    {
        return 12345; // $123.45 per ETH
    }
}

contract SuperLaunch
{
    // Constants
    uint256 public TOKEN_PRICE_USD_CENTS;
    uint256 public totalSupply;
    uint256 public AMOUNT_OF_FREE_TOKENS;
    address payable public root;
    address payable public bank;
    uint256 public REFERRER_COMMISSION_PERCENTAGE;
    uint256 public ROOT_COMMISSION_PERCENTAGE;
    PriceWatcherI public priceWatcher;

    // State variables
    mapping(address => uint256) private balances;
    address[] public participants;
    mapping(address => address payable) public address_to_referrer;
    mapping(address => address[]) public address_to_referrals;

    constructor(address _priceWatcherContract, uint256 _tokenPriceUSDcents, uint256 _totalSupply, uint256 _amountOfFreeTokens, address payable _root, address payable _bank, uint256 _referrerCommissionPercentage, uint256 _rootCommissionPercentage) public
    {
        if (_priceWatcherContract == address(0x0))
        {
            priceWatcher = new PriceWatcherPlaceholder();
        }
        else
        {
            priceWatcher = PriceWatcherI(_priceWatcherContract);
        }

        TOKEN_PRICE_USD_CENTS = _tokenPriceUSDcents;
        totalSupply = _totalSupply;
        AMOUNT_OF_FREE_TOKENS = _amountOfFreeTokens;
        root = _root;
        bank = _bank;
        REFERRER_COMMISSION_PERCENTAGE = _referrerCommissionPercentage;
        ROOT_COMMISSION_PERCENTAGE = _rootCommissionPercentage;

        // The root address is its own referrer
        address_to_referrer[root] = root;

        // Mint all the tokens and assign them to the root address
        balances[root] = totalSupply;
        emit Transfer(address(0x0), root, totalSupply);
    }

    function getTokenPriceETH() public view returns (uint256)
    {
        // Fetch the current ETH exchange rate
        uint256 USDcentsPerETH = priceWatcher.getUSDcentsPerETH();

        // Use the exchange rate to calculate the current token price in ETH
        return (1 ether) * TOKEN_PRICE_USD_CENTS / USDcentsPerETH;
    }

    function buyTokens(address payable _referrer) external payable
    {
        uint256 tokensBought;
        uint256 totalValueOfTokensBought;

        uint256 tokenPriceWei = getTokenPriceETH();

        // If there are still free tokens available
        if (participants.length < AMOUNT_OF_FREE_TOKENS)
        {
            tokensBought = 1;
            totalValueOfTokensBought = 0;

            // Only 1 free token per address
            require(address_to_referrer[msg.sender] == address(0x0));
        }

        // If there are no free tokens available
        else
        {
            tokensBought = msg.value / tokenPriceWei;

            // Limit the bought tokens to the amount of tokens still for sale
            if (tokensBought > balances[root])
            {
                tokensBought = balances[root];
            }

            totalValueOfTokensBought = tokensBought * tokenPriceWei;
        }

        // If 0 tokens are being purchased, cancel this transaction
        require(tokensBought > 0);

        // Return the change
        msg.sender.transfer(msg.value - totalValueOfTokensBought);

        // If we haven't seen this buyer before
        if (address_to_referrer[msg.sender] == address(0x0))
        {
            // Referrer must have owned at least 1 token
            require(address_to_referrer[_referrer] != address(0x0));

            // Add them to the particpants list and the referral tree
            address_to_referrer[msg.sender] = _referrer;
            address_to_referrals[_referrer].push(msg.sender);
            participants.push(msg.sender);
        }

        // If we have seen this buyer before
        else
        {
            // Referrer must be the same as their previous referrer
            require(_referrer == address_to_referrer[msg.sender]);
        }

        // Transfer the bought tokens from root to the buyer
        balances[root] -= tokensBought;
        balances[msg.sender] += tokensBought;
        emit Transfer(root, msg.sender, tokensBought);

        // Transfer commission to the referrer
        uint256 commissionForReferrer = totalValueOfTokensBought * REFERRER_COMMISSION_PERCENTAGE / 100;
        _referrer.transfer(commissionForReferrer);

        // Transfer commission to the root
        uint256 commissionForRoot = totalValueOfTokensBought * ROOT_COMMISSION_PERCENTAGE / 100;
        root.transfer(commissionForRoot);

        // Transfer the remaining ETH to the bank
        bank.transfer(totalValueOfTokensBought - commissionForReferrer - commissionForRoot);
    }

    function amountOfReferralsMade(address _byReferrer) external view returns (uint256)
    {
        return address_to_referrals[_byReferrer].length;
    }

    function amountOfTokensForSale() external view returns (uint256)
    {
        return balances[root];
    }

    function amountOfFreeTokensAvailable() external view returns (uint256)
    {
        if (participants.length < AMOUNT_OF_FREE_TOKENS)
        {
            return AMOUNT_OF_FREE_TOKENS - participants.length;
        }
        else
        {
            return 0;
        }
    }

    // ERC20 implementation
    string public constant name = "SuperLaunch";
    string public constant symbol = "SLX";
    uint8 public constant decimals = 0;

    mapping (address => mapping (address => uint256)) private allowed;

    event Transfer(address indexed _from, address indexed _to, uint256 _amount);
    event Approval(address indexed _owner, address indexed _spender, uint256 _amount);

    function balanceOf(address _who) external view returns (uint256)
    {
        return balances[_who];
    }
    function allowance(address _owner, address _spender) external view returns (uint256)
    {
        return allowed[_owner][_spender];
    }

    function transfer(address _to, uint256 _amount) external returns (bool)
    {
        require(balances[msg.sender] >= _amount);
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
        emit Transfer(msg.sender, _to, _amount);
        return true;
    }
    function transferFrom(address _from, address _to, uint256 _amount) external returns (bool)
    {
        require(allowed[_from][msg.sender] >= _amount);
        require(balances[_from] >= _amount);
        allowed[_from][msg.sender] -= _amount;
        balances[_from] -= _amount;
        balances[_to] += _amount;
        emit Transfer(_from, _to, _amount);
        emit Approval(_from, msg.sender, allowed[_from][msg.sender]);
        return true;
    }
    function approve(address _spender, uint256 _amount) external returns (bool)
    {
        allowed[msg.sender][_spender] = _amount;
        emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
    }
    function increaseAllowance(address _spender, uint256 _addedAmount) public returns (bool)
    {
        require(allowed[msg.sender][_spender] + _addedAmount >= _addedAmount);
        allowed[msg.sender][_spender] += _addedAmount;
        emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
    }
    function decreaseAllowance(address _spender, uint256 _subtractedAmount) public returns (bool)
    {
        require(allowed[msg.sender][_spender] >= _subtractedAmount);
        allowed[msg.sender][_spender] -= _subtractedAmount;
        emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
    }
}