contract PoolTogether_ZapIn_V1 is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    bool public stopped = false;
    uint256 public goodwill;

    // if true, goodwill is not deducted
    mapping(address => bool) feeWhitelist;

    uint256 affiliateSplit;
    // restrict affiliates
    mapping(address => bool) public affiliates;
    // affiliate => token => amount
    mapping(address => mapping(address => uint256)) public affiliateBalance;
    // token => amount
    mapping(address => uint256) public totalAffiliateBalance;

    address
        private constant ETHAddress = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    address
        private constant zGoodwillAddress = 0x3CE37278de6388532C3949ce4e886F365B14fB56;

    // circuit breaker modifiers
    modifier stopInEmergency {
        if (stopped) {
            revert("Temporarily Paused");
        } else {
            _;
        }
    }

    constructor(uint256 _goodwill, uint256 _affiliateSplit) public {
        goodwill = _goodwill;
        affiliateSplit = _affiliateSplit;
    }

    event zapIn(address sender, address pool, uint256 tokensRec);

    /**
    @notice This function adds liquidity to a PoolTogether prize pool with ETH or ERC20 tokens
    @param _fromTokenAddress The token used for entry (address(0) if ether)
    @param _toTokenAddress The intermediate ERC20 token to swap to
    @param _prizePoolAddress Prize pool to enter
    @param _incomingTokenQty The quantity of fromToken to invest
    @param _minTokens The minimum acceptable quantity of intermediate ERC20 to swap to. Reverts otherwise
    @param _swapTarget Excecution target for swap
    @param _swapCallData DEX quote data
    @param affiliate Affiliate address
     */
    function ZapIn(
        address _fromTokenAddress,
        address _toTokenAddress,
        address _prizePoolAddress,
        uint256 _incomingTokenQty,
        uint256 _minTokens,
        address _swapTarget,
        bytes calldata _swapCallData,
        address affiliate
    ) external payable stopInEmergency {
        uint256 toInvest = _pullTokens(
            _fromTokenAddress,
            _incomingTokenQty,
            affiliate
        );

        IPoolTogether prizePool = IPoolTogether(_prizePoolAddress);

        if (prizePool.token() == _fromTokenAddress) {
            _enterPrizePool(prizePool, toInvest);
        } else {
            uint256 tokensBought = _fillQuote(
                _fromTokenAddress,
                _toTokenAddress,
                toInvest,
                _swapTarget,
                _swapCallData
            );

            require(tokensBought > _minTokens, "Received less than _minTokens");
            require(
                prizePool.token() == _toTokenAddress,
                "Swapped to wrong token"
            );
            _enterPrizePool(prizePool, tokensBought);
        }

        emit zapIn(msg.sender, _prizePoolAddress, _incomingTokenQty);
    }

    function _enterPrizePool(IPoolTogether prizePool, uint256 amount) internal {
        address poolToken = prizePool.token();

        IERC20(poolToken).safeApprove(address(prizePool), 0);
        IERC20(poolToken).safeApprove(address(prizePool), amount);

        prizePool.depositTo(
            msg.sender,
            amount,
            prizePool.tokens()[1],
            zGoodwillAddress
        );
    }

    function _pullTokens(
        address token,
        uint256 amount,
        address affiliate
    ) internal returns (uint256 value) {
        if (token == address(0)) {
            require(msg.value > 0, "No eth sent");
            token = ETHAddress;
            value = msg.value;
        } else {
            require(amount > 0, "Invalid token amount");
            require(msg.value == 0, "ERR: ETH sent with tokens");

            IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
            value = amount;
        }

        uint256 totalGoodwillPortion = _subtractGoodwill(
            token,
            value,
            affiliate
        );

        return value.sub(totalGoodwillPortion);
    }

    function _subtractGoodwill(
        address token,
        uint256 amount,
        address affiliate
    ) internal returns (uint256 totalGoodwillPortion) {
        bool whitelisted = feeWhitelist[msg.sender];
        if (!whitelisted && goodwill > 0) {
            totalGoodwillPortion = SafeMath.div(
                SafeMath.mul(amount, goodwill),
                10000
            );

            if (affiliates[affiliate] && affiliateSplit > 0) {
                uint256 affiliatePortion = totalGoodwillPortion
                    .mul(affiliateSplit)
                    .div(100);
                affiliateBalance[affiliate][token] = affiliateBalance[affiliate][token]
                    .add(affiliatePortion);
                totalAffiliateBalance[token] = totalAffiliateBalance[token].add(
                    affiliatePortion
                );
            }
        }
    }

    function _fillQuote(
        address _fromTokenAddress,
        address _toTokenAddress,
        uint256 _amount,
        address _swapTarget,
        bytes memory _swapCallData
    ) internal returns (uint256 amountBought) {
        uint256 valueToSend;

        if (_fromTokenAddress == _toTokenAddress) {
            return _amount;
        }

        if (_fromTokenAddress == address(0)) {
            valueToSend = _amount;
        } else {
            IERC20 fromToken = IERC20(_fromTokenAddress);
            fromToken.safeApprove(address(_swapTarget), 0);
            fromToken.safeApprove(address(_swapTarget), _amount);
        }

        uint256 initialBalance = _getBalance(_toTokenAddress);
        (bool success, ) = _swapTarget.call.value(valueToSend)(_swapCallData);
        require(success, "Error Swapping Tokens");
        amountBought = (_getBalance(_toTokenAddress)).sub(initialBalance);

        require(amountBought > 0, "Swapped To Invalid Intermediate");
    }

    function _getBalance(address token)
        internal
        view
        returns (uint256 balance)
    {
        if (token == address(0)) {
            balance = address(this).balance;
        } else {
            balance = IERC20(token).balanceOf(address(this));
        }
    }

    function set_new_goodwill(uint16 _new_goodwill) external onlyOwner {
        require(
            _new_goodwill >= 0 && _new_goodwill <= 100,
            "GoodWill Value not allowed"
        );
        goodwill = _new_goodwill;
    }

    function set_feeWhitelist(address zapAddress, bool status)
        external
        onlyOwner
    {
        require(
            feeWhitelist[zapAddress] != status,
            "Whitelist status already set"
        );
        feeWhitelist[zapAddress] = status;
    }

    // - to Pause the contract
    function toggleContractActive() external onlyOwner {
        stopped = !stopped;
    }

    function set_new_affiliateSplit(uint16 _new_affiliateSplit)
        external
        onlyOwner
    {
        require(
            _new_affiliateSplit <= 100,
            "Affiliate Split Value not allowed"
        );
        affiliateSplit = _new_affiliateSplit;
    }

    function set_affiliate(address _affiliate, bool status) external onlyOwner {
        affiliates[_affiliate] = status;
    }

    ///@notice Withdraw goodwill share, retaining affiliate share
    function withdrawTokens(address[] calldata tokens) external onlyOwner {
        for (uint256 i = 0; i < tokens.length; i++) {
            uint256 qty;

            if (tokens[i] == ETHAddress) {
                qty = address(this).balance.sub(
                    totalAffiliateBalance[tokens[i]]
                );
                Address.sendValue(Address.toPayable(owner()), qty);
            } else {
                qty = IERC20(tokens[i]).balanceOf(address(this)).sub(
                    totalAffiliateBalance[tokens[i]]
                );
                IERC20(tokens[i]).safeTransfer(owner(), qty);
            }
        }
    }

    ///@notice Withdraw affiliate share, retaining goodwill share
    function affiliateWithdraw(address[] calldata tokens) external {
        uint256 tokenBal;
        for (uint256 i = 0; i < tokens.length; i++) {
            tokenBal = affiliateBalance[msg.sender][tokens[i]];
            affiliateBalance[msg.sender][tokens[i]] = 0;
            totalAffiliateBalance[tokens[i]] = totalAffiliateBalance[tokens[i]]
                .sub(tokenBal);

            if (tokens[i] == ETHAddress) {
                Address.sendValue(msg.sender, tokenBal);
            } else {
                IERC20(tokens[i]).safeTransfer(msg.sender, tokenBal);
            }
        }
    }

    function() external payable {
        require(msg.sender != tx.origin, "Do not send ETH directly");
    }
}
function withdrawTokens(address[] calldata tokens) external onlyOwner {
    for (uint256 i = 0; i < tokens.length; i++) {
        uint256 qty;

        if (tokens[i] == ETHAddress) {
            qty = address(this).balance.sub(
                totalAffiliateBalance[tokens[i]]
            );
            Address.sendValue(Address.toPayable(owner()), qty);
        } else {
            qty = IERC20(tokens[i]).balanceOf(address(this)).sub(
                totalAffiliateBalance[tokens[i]]
            );
            IERC20(tokens[i]).safeTransfer(owner(), qty);
        }
    }
}

///@notice Withdraw goodwill share, retaining affiliate share