contract NiceCash is Ownable, AccessControl {

    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    //ACL
    //Manager is the person allowed to manage funds
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

    IUniswapV2Router01 public router;
    IERC20 public basicToken;
    IERC20 public uniToken;
    NCLiquidityToken public ncLT;

    address public externalCommissionAddress;
    uint256 public externalCommissionBalance;
    uint8 public externalCommissionPercentNom;
    uint8 public externalCommissionPercentDenom;

    uint256 public totalCap;

    mapping (address => Pair) public inLiquidityPool;

    mapping (address => uint256) public deposits;
    mapping (address => uint256) public withdrawals;

    constructor(address _router, address _basicToken, address _uniToken) public {
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
        _setupRole(MANAGER_ROLE, _msgSender());

        //safe mode "on" with commissions
        externalCommissionAddress = address(this);
        externalCommissionPercentNom = 3;
        externalCommissionPercentDenom = 10;

        ncLT = new NCLiquidityToken();
        router = IUniswapV2Router01(_router);
        basicToken = IERC20(_basicToken);
        uniToken = IERC20(_uniToken);
    }

    modifier isWrappedEth() {
        require(address(basicToken) == router.WETH());
        _;
    }

    /**
    * @dev Throws if called by any account other than the one with the Manager role granted.
    */
    modifier onlyManager() {
        require(hasRole(MANAGER_ROLE, msg.sender), "Caller is not the Manager");
        _;
    }

    event Deposit(address who, uint256 amount);

    event Withdraw(address who, uint256 amount);

    fallback() payable external isWrappedEth {
        uint256 amount = msg.value;
        IWETH(router.WETH()).deposit{value: amount}();
        _deposit(msg.sender, amount);
    }

    function depositETH(
        address to
    ) payable external isWrappedEth {
        uint256 amount = msg.value;
        IWETH(router.WETH()).deposit{value: amount}();
        _deposit(to, amount);
    }

    function deposit(
        uint256 amount,
        address to
    ) external {
        basicToken.safeTransferFrom(msg.sender, address(this), amount);
        _deposit(to, amount);
    }

    function withdraw(
        uint256 amount,
        address to
    ) external {
        uint256 revenue = totalCap != 0 ? amount.mul(totalCap).div(ncLT.totalSupply()) : amount;
        require(revenue <= basicToken.balanceOf(address(this)), "Not enouth Basic Token tokens on the balance to withdraw");
        ncLT.burnFrom(msg.sender, amount);
        basicToken.safeTransfer(to, revenue);
        withdrawals[_msgSender()] = withdrawals[_msgSender()].add(revenue);
        emit Withdraw(_msgSender(), revenue);
    }

    function swapTokens(
        address fromToken,
        uint256 fromAmount,
        address toToken,
        uint256 deadline
    ) external onlyManager {
        address[] memory token_address = new address[](2);
        token_address[0] = fromToken;
        token_address[1] = toToken;
        IERC20(fromToken).safeIncreaseAllowance(address(router), fromAmount);
        uint[] memory out = router.swapExactTokensForTokens(
            fromAmount,
            uint256(0),
            token_address,
            address(this),
            deadline
        );

        //distribute profit from UNI tokens immediately after conversion
        if(fromToken == address(uniToken) && toToken == address(basicToken)){
            _distributeProfit(out[1]);
        }
    }

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        uint deadline
    ) external onlyManager {
        //require at least one token to be a basic token
        require(address(basicToken)==tokenA || address(basicToken)==tokenB);

        address pair = _pairFor(router.factory(), tokenA, tokenB);
        IERC20(tokenA).safeIncreaseAllowance(address(router), amountADesired);
        IERC20(tokenB).safeIncreaseAllowance(address(router), amountBDesired);

        (uint256 amountA, uint256 amountB, uint256 liquidity) = router.addLiquidity(
            tokenA,
            tokenB,
            amountADesired,
            amountBDesired,
            amountAMin,
            amountBMin,
            address(this),
            deadline
        );

        //tokenB in internal structures is always a basic token
        if(address(basicToken)==tokenB){
            inLiquidityPool[pair].amountA = inLiquidityPool[pair].amountA.add(amountA);
            inLiquidityPool[pair].amountB = inLiquidityPool[pair].amountB.add(amountB);
        }else{
            inLiquidityPool[pair].amountA = inLiquidityPool[pair].amountA.add(amountB);
            inLiquidityPool[pair].amountB = inLiquidityPool[pair].amountB.add(amountA);
        }

        // inLiquidityPool[pair].amountLT = inLiquidityPool[pair].amountLT.add(liquidity);
    }

    event LiquidityStat(address p, uint256 a, uint256 b);

    event FinRes(bool t, uint256 v);

    function removeLiquidity(
        uint256 liquidity,
        address tokenA,
        address tokenB,
        uint amountAMin,
        uint amountBMin,
        uint deadline
    ) external onlyManager{
        //require at least one token to be a basic token
        require(address(basicToken)==tokenA || address(basicToken)==tokenB, "A pairs with basicToken are available only");

        address pair = _pairFor(router.factory(), tokenA, tokenB);
        uint256 totalLiquidity = IERC20(pair).balanceOf(address(this));

        require(totalLiquidity > 0, "Contract to own some liquidity on a pair");
        require(liquidity <= totalLiquidity, "Liquidity requested to be lower then existing one");
        
        IERC20(pair).safeIncreaseAllowance(address(router), liquidity);
        (uint256 amtA, uint256 amtB) = router.removeLiquidity(
            tokenA,
            tokenB,
            liquidity,
            amountAMin,
            amountBMin,
            address(this),
            deadline
        );

        uint256 pAmountA = inLiquidityPool[pair].amountA.mul(liquidity).div(totalLiquidity);
        uint256 pAmountB = inLiquidityPool[pair].amountB.mul(liquidity).div(totalLiquidity);

        //tokenB to be a basic token for correct calculations. Flip numbers if not.
        uint256 amountA=(address(basicToken)==tokenB)?amtA:amtB;
        uint256 amountB=(address(basicToken)==tokenB)?amtB:amtA;

        // emit LiquidityStat(pair, inLiquidityPool[pair].amountA, inLiquidityPool[pair].amountB);
        // emit LiquidityStat(pair, amountA, amountB);

        //total operation profit to pay commission from
        bool operationFinResProf = true;
        uint256 operationFinRes = 0;

        //calculate financial result for base token
        if (amountB >= pAmountB){
            //profit
            uint256 profit = amountB.sub(pAmountB);
            operationFinRes = operationFinRes.add(profit);
            operationFinResProf=true;
        }else{
            //loss
            uint256 loss = pAmountB.sub(amountB);
            operationFinRes = operationFinRes.add(loss);
            operationFinResProf=false;
        }

        emit FinRes(operationFinResProf, operationFinRes);

        //calculate financial result for another token
        if (amountA >= pAmountA){
            //profit
            uint256 profit = amountA.sub(pAmountA).mul(amountB).div(amountA);
            emit FinRes(true, profit);
            //check base token fin res
            if(operationFinResProf){
                operationFinRes = operationFinRes.add(profit);
            }else{
                //if another token profit covers base token loss, then total operation is profitable.
                if(profit >= operationFinRes){
                    operationFinRes = profit.sub(operationFinRes);
                    operationFinResProf = true;
                }else{
                    operationFinRes = operationFinRes.sub(profit);
                }
            }
        }else{
            //loss
            uint256 loss = pAmountA.sub(amountA).mul(amountB).div(amountA);
            emit FinRes(false, loss);
            //check base token fin res
            if(operationFinResProf){
                //if another token loss is greater then base token profit, then total operation is unprofitable.
                if(loss > operationFinRes){
                    operationFinRes = loss.sub(operationFinRes);
                    operationFinResProf = false;
                }else{
                    operationFinRes = operationFinRes.sub(loss);
                }
            }else{
                operationFinRes = operationFinRes.add(loss);
            }
        }

        emit FinRes(operationFinResProf, operationFinRes);

        //apply operation fin res to totalcap and calculate commissions
        if(operationFinResProf){
            _distributeProfit(operationFinRes);
        }else{
            //decrease totalCap by operation loss result
            totalCap = totalCap.sub(operationFinRes);
        }

    }

    //external commissions operations

    function withdrawCommission(uint256 amount) public onlyManager {
        require(amount <= externalCommissionBalance, "Amount to be less then external commission available to withdraw");
        basicToken.safeTransfer(externalCommissionAddress, amount);
        externalCommissionBalance = externalCommissionBalance.sub(amount);
    }

    //support for Uniswap liquidity staking
    function stakeLiquidity(address _stakingContract, uint256 amount, address liquidityTokenAddress) public onlyManager {
        require(_stakingContract != address(0), "Staking should not be zero address");
        require(liquidityTokenAddress != address(0), "Liquidity token should not be zero address");
        IERC20(liquidityTokenAddress).safeIncreaseAllowance(address(_stakingContract), amount);
        StakingRewards(_stakingContract).stake(amount);
    }

    function getReward(address _stakingContract) public onlyManager {
        require(_stakingContract != address(0), "Staking should not be zero address");
        StakingRewards(_stakingContract).getReward();
    }

    function exitStake(address _stakingContract) public onlyManager {
        require(_stakingContract != address(0), "Staking should not be zero address");
        StakingRewards(_stakingContract).exit();
    }

    function withdrawStake(address _stakingContract, uint256 amount) public onlyManager {
        require(_stakingContract != address(0), "Staking should not be zero address");
        StakingRewards(_stakingContract).withdraw(amount);
    }

    function withdrawUniToken(uint256 amount) public onlyManager {
        uniToken.safeTransfer(externalCommissionAddress, amount);
    }

    //admin functions
    function setRouter(address _router) external onlyOwner {
        router = IUniswapV2Router01(_router);
    }

    function setExternalCommissionAddress(address _externalCommissionAddress) public onlyOwner {
        externalCommissionAddress = _externalCommissionAddress;
    }

    function setExternalCommissionPercent(uint8 _nom, uint8 _denom) public onlyOwner {
        require (_nom <= _denom, "Commission to be a natural fraction less then 1");
        externalCommissionPercentNom = _nom;
        externalCommissionPercentDenom = _denom;
    }

    //Views
    // function getLiquidityBalance() public view returns (uint256) {
    //     return basicToken.balanceOf(address(this)) - ncashFeePool;
    // }
    function getLiquidityBalance(address tokenA, address tokenB) public view returns (uint256){
        address pair = _pairFor(router.factory(), tokenA, tokenB);
        return IERC20(pair).balanceOf(address(this));
    }

    function getUserOperationsStat(address holder) public view returns (uint256, uint256, uint256) {
        return (deposits[holder], withdrawals[holder], ncLT.balanceOf(holder));
    }


    function _deposit(address to, uint256 amount) private {
        uint256 mintAmount = totalCap != 0 ? amount.mul(ncLT.totalSupply()).div(totalCap) : amount;
        ncLT.mintTo(to, mintAmount);
        totalCap = totalCap.add(amount);
        deposits[_msgSender()] = deposits[_msgSender()].add(amount);
        emit Deposit(_msgSender(), amount);
    }

    // returns sorted token addresses, used to handle return values from pairs sorted in this order
    function _sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
        require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
    }

    // calculates the CREATE2 address for a pair without making any external calls
    function _pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
        (address token0, address token1) = _sortTokens(tokenA, tokenB);
        pair = address(uint(keccak256(abi.encodePacked(
                hex'ff',
                factory,
                keccak256(abi.encodePacked(token0, token1)),
                hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
            ))));
    }

    //distribute profit between all users
    function _distributeProfit(uint256 protif) private {
        uint256 externalCommission = protif.mul(externalCommissionPercentNom).div(externalCommissionPercentDenom);
        externalCommissionBalance = externalCommissionBalance.add(externalCommission);

        uint256 userCommissionProfit = protif.sub(externalCommission);
        totalCap = totalCap.add(userCommissionProfit);
    }

}
function setRouter(address _router) external onlyOwner {
    router = IUniswapV2Router01(_router);
}

//admin functions