contract AtomicBlue is AtomicUtils, AtomicTypes{
    // IMPORTANT NOTICE:
    // NEVER set a token allowance to this contract, as everybody can do arbitrary calls from it.
    // When swapping tokens always go through AtomicTokenProxy.
    // This contract assumes token to swap has already been transfered to it when being called. Ether can be sent directly with the call.

    // perform a distributed swap and transfer outcome to _receipient
    function swapAndSend(
        SwapParams memory _swap,
        
        DistributionParams memory _distribution,
        
        address payable _receipient
    ) public payable returns (uint _output){
        // execute swaps on behalf of trader
        _output = doDistributedSwap(_swap, _distribution);

        // check if output of swap is sufficient        
        require(_output >= _swap.minOutput, "Slippage limit exceeded.");
        
        // send swap output to receipient
        send(_swap.buyToken, _receipient, _output);
        
        emit Trade(
            address(_swap.sellToken),
            _swap.input,
            address(_swap.buyToken),
            _output,
            msg.sender,
            _receipient
        );
    }
    
    function multiPathSwapAndSend(
        SwapParams memory _swap,
        
        Token[] calldata _path,
        
        DistributionParams[] memory _distribution,
        
        address payable _receipient
    ) public payable returns (uint _output){
        // verify path
        require(
            _path[0] == _swap.sellToken
            && _path[_path.length - 1] == _swap.buyToken
            && _path.length >= 2
        );
        
        // execute swaps on behalf of trader
        _output = _swap.input;
        for(uint i = 1; i < _path.length; i++){
            _output = doDistributedSwap(SwapParams({
                sellToken : _path[i - 1],
                input     : _output,      // output of last swap is input for this one
                buyToken  : _path[i],
                minOutput : 0            // we check the total outcome in the end
            }), _distribution[i - 1]);
        }

        // check if output of swap is sufficient        
        require(_output >= _swap.minOutput, "Slippage limit exceeded.");
        
        // send swap output to sender
        send(_swap.buyToken, _receipient, _output);
        
        emit Trade(
            address(_swap.sellToken),
            _swap.input,
            address(_swap.buyToken),
            _output,
            msg.sender,
            _receipient
        );
    }
    
    
    // internal function to perform a distributed swap
    function doDistributedSwap(
        SwapParams memory _swap,
        
        DistributionParams memory _distribution
    ) internal returns(uint){
        
        // count totalChunks
        uint totalChunks = 0;
        for(uint i = 0; i < _distribution.chunks.length; i++){
            totalChunks += _distribution.chunks[i];   
        }
        
        // route trades to the different exchanges
        for(uint i = 0; i < _distribution.exchangeModules.length; i++){
            IAtomicExchange exchange = _distribution.exchangeModules[i];
            
            uint thisInput = _swap.input * _distribution.chunks[i] / totalChunks;
            
            if(address(exchange) == address(0)){
                // trade is not using an exchange module but a direct call
                (address target, uint value, bytes memory callData) = abi.decode(_distribution.exchangeData[i], (address, uint, bytes));
                
                (bool success, bytes memory data) = address(target).call.value(value)(callData);
            
                require(success, "Exchange call reverted.");
            }else{
                // delegate call to the exchange module
                (bool success, bytes memory data) = address(exchange).delegatecall(
                    abi.encodePacked(// This encodes the function to call and the parameters we are passing to the settlement function
                        exchange.swap.selector, 
                        abi.encode(
                            SwapParams({
                                sellToken : _swap.sellToken,
                                input     : thisInput,
                                buyToken  : _swap.buyToken,
                                minOutput : 1 // we are checking the combined output in the end
                            }),
                            _distribution.exchangeData[i]
                        )
                    )
                );
            
                require(success, "Exchange module reverted.");
            }
        }
        
        return balanceOf(_swap.buyToken);
    }
    
    // perform a distributed swap
    function swap(
        SwapParams memory _swap,
        DistributionParams memory _distribution
    ) public payable returns (uint _output){
        return swapAndSend(_swap, _distribution, msg.sender);
    }
    
    // perform a multi-path distributed swap
    function multiPathSwap(
        SwapParams memory _swap,
        Token[] calldata _path,
        DistributionParams[] memory _distribution
    ) public payable returns (uint _output){
        return multiPathSwapAndSend(_swap, _path, _distribution, msg.sender);
    }

    // allow ETH receivals
    receive() external payable {}
}
function multiPathSwap(
    SwapParams memory _swap,
    Token[] calldata _path,
    DistributionParams[] memory _distribution
) public payable returns (uint _output){
    return multiPathSwapAndSend(_swap, _path, _distribution, msg.sender);
}

// perform a multi-path distributed swap