contract ReserveFund is Auth {
    using StringUtil for *;
    using Math for int;

    enum Lock {
        UNLOCKED,
        PROFIT,
        MINING_TOKEN,
        BOTH
    }



    mapping(address => Lock) public lockedAccounts;
    uint private miningDifficulty = 200000; // $200
    uint private transferDifficulty = 1000; // $1
    uint private aiTokenG3; // 1 ETH = aiTokenG3 SFU
    uint public aiTokenG2; // in mili-dollar (1/1000 dollar)
    uint public minJoinPackage = 200000; // $200
    uint public maxJoinPackage = 50000000; // $50k
    uint public currentETHPrice;
    bool public enableJoinPackageViaEther = true;
    address[] private usersVIP;

    ICitizen private citizen;
    IWallet private wallet;
    IERC20 public sfuToken;

    event AccountsLocked(address[] addresses, uint8 lockingType);
    event AITokenG2Set(uint rate);
    event AITokenG3Set(uint rate);
    event ETHPriceSet(uint ethPrice);
    event MinJoinPackageSet(uint minJoinPackage);
    event MaxJoinPackageSet(uint maxJoinPackage);
    event EnableJoinPackageViaEtherSwitched(bool enabled);
    event EtherPriceUpdated(uint currentETHPrice);
    event MiningDifficultySet(uint rate);
    event TransferDifficultySet(uint value);
    event PackageJoinedViaEther(address buyer, address receiver, uint amount);
    event PackageJoinedViaToken(address buyer, address receiver, uint amount);
    event PackageJoinedViaDollar(address buyer, address receiver, uint amount);
    event Registered(uint id, string userName, address userAddress, address inviter);
    event TokenMined(address buyer, uint amount, uint walletAmount);
    event TokenSwapped(address seller, uint amount, uint ethAmount);

    constructor (
        address _citizen,
        address _wallet,
        address _mainAdmin,
        uint _currentETHPrice
    )
    Auth(_mainAdmin, msg.sender)
    public
    {
        citizen = ICitizen(_citizen);
        wallet = IWallet(_wallet);
        currentETHPrice = _currentETHPrice;
    }

    // ADMINS FUNCTIONS

    function setSFUankingToken(address _sfuToken) onlyAdmin public {
        sfuToken = IERC20(_sfuToken);
    }

    function updateETHPrice(uint _currentETHPrice) onlyAdmin public {
        require(_currentETHPrice > 0, "Must be > 0");
        require(_currentETHPrice != currentETHPrice, "Must be new value");
        currentETHPrice = _currentETHPrice;
        emit ETHPriceSet(currentETHPrice);
    }

    function updateContractAdmin(address _newAddress) onlyAdmin public {
        transferOwnership(_newAddress);
    }

    function setMinJoinPackage(uint _minJoinPackage) onlyAdmin public {
        require(_minJoinPackage > 0, "Must be > 0");
        require(_minJoinPackage < maxJoinPackage, "Must be < maxJoinPackage");
        require(_minJoinPackage != minJoinPackage, "Must be new value");
        minJoinPackage = _minJoinPackage;
        emit MinJoinPackageSet(minJoinPackage);
    }

    function setMaxJoinPackage(uint _maxJoinPackage) onlyAdmin public {
        require(_maxJoinPackage > minJoinPackage, "Must be > minJoinPackage");
        require(_maxJoinPackage != maxJoinPackage, "Must be new value");
        maxJoinPackage = _maxJoinPackage;
        emit MaxJoinPackageSet(maxJoinPackage);
    }

    function setEnableJoinPackageViaEther(bool _enableJoinPackageViaEther) onlyAdmin public {
        require(_enableJoinPackageViaEther != enableJoinPackageViaEther, "Must be new value");
        enableJoinPackageViaEther = _enableJoinPackageViaEther;
        emit EnableJoinPackageViaEtherSwitched(enableJoinPackageViaEther);
    }

    function aiSetTokenG2(uint _rate) onlyAdmin public {
        require(_rate > 0, "aiTokenG2 must be > 0");
        require(_rate != aiTokenG2, "aiTokenG2 must be new value");
        aiTokenG2 = _rate;
        emit AITokenG2Set(aiTokenG2);
    }

    function aiSetTokenG3(uint _rate) onlyAdmin public {
        require(_rate > 0, "aiTokenG3 must be > 0");
        require(_rate != aiTokenG3, "aiTokenG3 must be new value");
        aiTokenG3 = _rate;
        emit AITokenG3Set(aiTokenG3);
    }

    function setMiningDifficulty(uint _miningDifficulty) onlyAdmin public {
        require(_miningDifficulty > 0, "miningDifficulty must be > 0");
        require(_miningDifficulty != miningDifficulty, "miningDifficulty must be new value");
        miningDifficulty = _miningDifficulty;
        emit MiningDifficultySet(miningDifficulty);
    }

    function setTransferDifficulty(uint _transferDifficulty) onlyAdmin public {
        require(_transferDifficulty > 0, "MinimumBuy must be > 0");
        require(_transferDifficulty != transferDifficulty, "transferDifficulty must be new value");
        transferDifficulty = _transferDifficulty;
        emit TransferDifficultySet(transferDifficulty);
    }

    function lockAccounts(address[] _addresses, uint8 _type) onlyAdmin public {
        require(_addresses.length > 0, "Address cannot be empty");
        require(_addresses.length <= 256, "Maximum users per action is 256");
        require(_type >= 0 && _type <= 3, "Type is invalid");
        for (uint8 i = 0; i < _addresses.length; i++) {
            require(_addresses[i] != msg.sender, "You cannot lock yourself");
            lockedAccounts[_addresses[i]] = Lock(_type);
        }
        emit AccountsLocked(_addresses, _type);
    }

    function addUserVIP(address userVIP, string memory _userName) onlyAdmin public {
        require(usersVIP.length <= 10, "Max user vip.");
        address _inviter = msg.sender;
        if (usersVIP.length > 1) {
            _inviter = usersVIP[usersVIP.length - 1];
        }
        require(citizen.isCitizen(_inviter), "Inviter did not registered.");
        require(userVIP != msg.sender, "Cannot referral yourself");
        uint id = citizen.register(userVIP, _userName, _inviter);
        emit Registered(id, _userName, userVIP, _inviter);
        for (uint i = 0; i <= 10; i ++) {
            address _invitee = address(0x0);
            citizen.increaseInviterF1HaveJoinedPackageForUserVIP(userVIP, _invitee);
        }
        usersVIP.push(userVIP);
    }


    // PUBLIC FUNCTIONS

    function() public payable {}

    function getAITokenG3() view public returns (uint) {
        return aiTokenG3;
    }

    function getMiningDifficulty() view public returns (uint) {
        return miningDifficulty;
    }

    function getTransferDifficulty() view public returns (uint) {
        return transferDifficulty;
    }

    function getLockedStatus(address _investor) view public returns (uint8) {
        return uint8(lockedAccounts[_investor]);
    }
    
    function getUsersVIP() view public returns (address[]) {
        return usersVIP;
    }

    function register(string memory _userName, address _inviter) public {
        require(citizen.isCitizen(_inviter), "Inviter did not registered.");
        require(_inviter != msg.sender, "Cannot referral yourself");
        uint id = citizen.register(msg.sender, _userName, _inviter);
        emit Registered(id, _userName, msg.sender, _inviter);
    }

    function showMe() public view returns (uint, string memory, address, address[], uint, uint, uint, uint) {
        return citizen.showInvestorInfo(msg.sender);
    }

    function joinPackageViaEther(uint _rate, address _to) payable public {
        require(enableJoinPackageViaEther, "Can not buy via Ether now");
        validateJoinPackage(msg.sender, _to);
        require(_rate > 0, "Rate must be > 0");
        validateAmount(_to, (msg.value * _rate) / (10 ** 18));
        bool rateHigherUnder3Percents = (int(currentETHPrice - _rate).abs() * 100 / _rate) <= uint(3);
        bool rateLowerUnder5Percents = (int(_rate - currentETHPrice).abs() * 100 / currentETHPrice) <= uint(5);
        bool validRate = rateHigherUnder3Percents && rateLowerUnder5Percents;
        require(validRate, "Invalid rate, please check again!");
        doJoinViaEther(msg.sender, _to, msg.value, _rate);
    }

    function joinPackageViaDollar(uint _amount, address _to) public {
        validateJoinPackage(msg.sender, _to);
        validateAmount(_to, _amount);
        validateProfitBalance(msg.sender, _amount);
        wallet.deposit(_to, _amount, 2, _amount);
        wallet.bonusForAdminWhenUserBuyPackageViaDollar(_amount / 20, mainAdmin);
        emit PackageJoinedViaDollar(msg.sender, _to, _amount);
    }

    function joinPackageViaToken(uint _amount, address _to) public {
        validateJoinPackage(msg.sender, _to);
        validateAmount(_to, _amount);
        uint tokenAmount = (_amount / aiTokenG2) * (10 ** 18);
        require(sfuToken.allowance(msg.sender, address(this)) >= tokenAmount, "You must call approve() first");
        uint userOldBalance = sfuToken.balanceOf(msg.sender);
        require(userOldBalance >= tokenAmount, "You have not enough tokens");
        require(sfuToken.transferFrom(msg.sender, address(this), tokenAmount), "Transfer token failed");
        require(sfuToken.transfer(mainAdmin, tokenAmount / 10), "Transfer token to admin failed");
        wallet.deposit(_to, _amount, 1, tokenAmount);
        emit PackageJoinedViaToken(msg.sender, _to, _amount);
    }

    function miningToken(uint _tokenAmount) public {
        require(aiTokenG2 > 0, "Invalid aiTokenG2, please contact admin");
        require(citizen.isCitizen(msg.sender), "Please register first");
        validateLockingMiningToken(msg.sender);
        require(_tokenAmount > miningDifficulty, "Amount must be > miningDifficulty");
        uint fiatAmount = (_tokenAmount * aiTokenG2) / (10 ** 18);
        validateProfitBalance(msg.sender, fiatAmount);
        wallet.validateCanMineToken(fiatAmount, msg.sender);

        wallet.mineToken(msg.sender, fiatAmount);
        uint userOldBalance = sfuToken.balanceOf(msg.sender);
        require(sfuToken.transfer(msg.sender, _tokenAmount), "Transfer token to user failed");
        require(sfuToken.balanceOf(msg.sender) == userOldBalance + _tokenAmount, "User token changed invalid");
        emit TokenMined(msg.sender, _tokenAmount, fiatAmount);
    }

    function swapToken(uint _amount) public {
        require(_amount > 0, "Invalid amount to swap");
        require(sfuToken.balanceOf(msg.sender) >= _amount, "You have not enough balance");
        uint etherAmount = getEtherAmountFromToken(_amount);
        require(address(this).balance >= etherAmount, "The contract have not enough balance");
        require(sfuToken.allowance(msg.sender, address(this)) >= _amount, "You must call approve() first");
        require(sfuToken.transferFrom(msg.sender, address(this), _amount), "Transfer token failed");
        msg.sender.transfer(etherAmount);
        wallet.increaseETHWithdrew(etherAmount);
        emit TokenSwapped(msg.sender, _amount, etherAmount);
    }

    function getCurrentEthPrice() public view returns (uint) {
        return currentETHPrice;
    }

    // PRIVATE FUNCTIONS

    function getEtherAmountFromToken(uint _amount) private view returns (uint) {
        require(aiTokenG3 > 0, "Invalid aiTokenG3, please contact admin");
        return _amount / aiTokenG3;
    }

    function doJoinViaEther(address _from, address _to, uint _etherAmountInWei, uint _rate) private {
        uint etherForAdmin = _etherAmountInWei / 20;
        uint packageValue = (_etherAmountInWei * _rate) / (10 ** 18);
        wallet.deposit(_to, packageValue, 0, _etherAmountInWei);
        mainAdmin.transfer(etherForAdmin);
        emit PackageJoinedViaEther(_from, _to, packageValue);
    }

    function validateAmount(address _user, uint _packageValue) public view {
        require(_packageValue > 0, "Amount must be > 0");
        require(_packageValue <= maxJoinPackage, "Can not join with amount that greater max join package");
        uint lastBuy = wallet.getInvestorLastDeposited(_user);
        if (lastBuy == 0) {
            require(_packageValue >= minJoinPackage, "Minimum for first join is $200");
        } else {
            require(_packageValue >= lastBuy, "Can not join with amount that lower than your last join");
        }
    }

    function validateJoinPackage(address _from, address _to) private view {
        require(citizen.isCitizen(_from), "Please register first");
        require(citizen.isCitizen(_to), "You can only buy for an exists member");
        if (_from != _to) {
            require(citizen.checkInvestorsInTheSameReferralTree(_from, _to), "This user isn't in your referral tree");
        }
        require(currentETHPrice > 0, "Invalid currentETHPrice, please contact admin!");
    }

    function validateLockingMiningToken(address _from) private view {
        bool canBuy = lockedAccounts[_from] != Lock.MINING_TOKEN && lockedAccounts[_from] != Lock.BOTH;
        require(canBuy, "Your account get locked from mining token");
    }

    function validateProfitBalance(address _user, uint _amount) private view {
        uint profitBalance = wallet.getProfitBalance(_user);
        require(profitBalance >= _amount, "You have not enough balance");
    }
}
function setSFUankingToken(address _sfuToken) onlyAdmin public {
        sfuToken = IERC20(_sfuToken);
    }
// ADMINS FUNCTIONS