contract Verse is ERC20, Ownable {
    using SafeMath for uint256;

    IUniswapV2Router02 public uniswapV2Router;
    address public uniswapV2Pair;

    bool private swapping;
    bool public maxTransactionLimitEnabled = true;
    
    VerseDividendTracker public dividendTracker;

    address public immutable deadAddress = 0x000000000000000000000000000000000000dEaD;
    address payable public devMarketingAndCharityAddress = payable(0x761E70C4317dD1a16592aB5c44D95aF03F4abFca);

    //maximum purchase amount for initial launch
    uint256 maxTransactionAmount = 3250 * (10**18);
    uint256 public maxDailySellAmount = 3250 * (10**18);
    uint256 public swapTokensAtAmount = 1000 * (10**18);

    mapping(address => bool) private _isBot;
    address[] private _confirmedBots;

    bool public mechanismsEnabled = true;
    bool public aidiBuybackEnabled = true;
    bool public ethRedistributionEnabled = true;
    bool public devMarketingAndCharityEnabled = true;

    uint256 public buybackFee = 3;
    uint256 public ethRedistributeFee = 4;
    uint256 public devMarketingAndCharityFee = 3;
    uint256 public totalFee = 10;
    // use by default 150,000 gas to process auto-claiming dividends
    uint256 public gasForProcessing = 50000;

    // once set to true can never be set false again
    bool public tradingOpen = false;
    uint256 public launchTime;

    uint256 minimumTokenBalanceForDividends = 500 * (10**18);


    // exlcude from fees and max transaction amount
    mapping (address => bool) private _isExcludedFromFees;

    // store addresses that a automatic market maker pairs. Any transfer *to* these addresses
    // could be subject to a maximum transfer amount
    mapping (address => bool) public automatedMarketMakerPairs;

    // store the amount sold for each account every day
    // to add extra tax
    mapping (uint256 => mapping(address => uint256)) public dailySell;

    // the last time an address transferred
    // used to detect if an account can be reinvest inactive funds to the vault
    mapping (address => uint256) public lastTransfer;

    mapping (address => bool) public isExcludedFromDailyLimit;

    event UpdateDividendTracker(address indexed newAddress, address indexed oldAddress);
    event UpdateUniswapV2Router(address indexed newAddress, address indexed oldAddress);
    event ExcludeFromFees(address indexed account, bool isExcluded);
    event ExcludeMultipleAccountsFromFees(address[] accounts, bool isExcluded);
    event SetAutomatedMarketMakerPair(address indexed pair, bool indexed value);
    event SendDividends(uint256 amount);
    event DividendClaimed(uint256 ethAmount, uint256 tokenAmount, address account);
    event ProcessedDividendTracker(uint256 iterations, uint256 claims, uint256 lastProcessedIndex, bool indexed automatic, uint256 gas, address indexed processor);
    event GasForProcessingUpdated(uint256 indexed newValue, uint256 indexed oldValue);

    constructor() ERC20("Verse", "VERSE") {

    	dividendTracker = new VerseDividendTracker();

    	IUniswapV2Router02 _uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
         // Create a uniswap pair for this new token
        address _uniswapV2Pair = IUniswapV2Factory(_uniswapV2Router.factory())
            .createPair(address(this), _uniswapV2Router.WETH());

        uniswapV2Router = _uniswapV2Router;
        uniswapV2Pair = _uniswapV2Pair;

        _setAutomatedMarketMakerPair(_uniswapV2Pair, true);

        // exclude from receiving dividends
        dividendTracker.excludeFromDividends(address(dividendTracker));
        dividendTracker.excludeFromDividends(address(this));
        dividendTracker.excludeFromDividends(owner());
        dividendTracker.excludeFromDividends(address(_uniswapV2Router));
        dividendTracker.excludeFromDividends(address(0x000000000000000000000000000000000000dEaD));

        // exclude from paying fees or having max transaction amount
        excludeFromFees(address(this), true);
        excludeFromFees(owner(), true);

        isExcludedFromDailyLimit[address(this)] = true;
        isExcludedFromDailyLimit[owner()] = true;

        _mint(owner(), 5000000 * (10**18));
    }

    receive() external payable {

  	}

    function updateDividendTracker(address newAddress) public onlyOwner {
        require(newAddress != address(dividendTracker), "Verse: The dividend tracker already has that address");

        VerseDividendTracker newDividendTracker = VerseDividendTracker(payable(newAddress));

        require(newDividendTracker.owner() == address(this), "Verse: The new dividend tracker must be owned by the Verse token contract");

        emit UpdateDividendTracker(newAddress, address(dividendTracker));

        dividendTracker = newDividendTracker;

        dividendTracker.excludeFromDividends(address(newDividendTracker));
        dividendTracker.excludeFromDividends(address(this));
        dividendTracker.excludeFromDividends(owner());
        dividendTracker.excludeFromDividends(address(uniswapV2Router));
        dividendTracker.excludeFromDividends(address(0x000000000000000000000000000000000000dEaD));
    }

    function updateUniswapV2Router(address newAddress) public onlyOwner {
        require(newAddress != address(uniswapV2Router), "Verse: The router already has that address");
        emit UpdateUniswapV2Router(newAddress, address(uniswapV2Router));
        uniswapV2Router = IUniswapV2Router02(newAddress);
        dividendTracker.excludeFromDividends(address(uniswapV2Router));
    }

    function excludeFromFees(address account, bool excluded) public onlyOwner {
        require(_isExcludedFromFees[account] != excluded, "Verse: Account is already the value of 'excluded'");
        _isExcludedFromFees[account] = excluded;

        emit ExcludeFromFees(account, excluded);
    }

    function excludeMultipleAccountsFromFees(address[] calldata accounts, bool excluded) public onlyOwner {
        for(uint256 i = 0; i < accounts.length; i++) {
            _isExcludedFromFees[accounts[i]] = excluded;
        }

        emit ExcludeMultipleAccountsFromFees(accounts, excluded);
    }

    function setAutomatedMarketMakerPair(address pair, bool value) public onlyOwner {
        require(pair != uniswapV2Pair, "Verse: The UniSwap pair cannot be removed from automatedMarketMakerPairs");

        _setAutomatedMarketMakerPair(pair, value);
    }

    function _setAutomatedMarketMakerPair(address pair, bool value) private {
        require(automatedMarketMakerPairs[pair] != value, "Verse: Automated market maker pair is already set to that value");
        automatedMarketMakerPairs[pair] = value;

        if(value) {
            dividendTracker.excludeFromDividends(pair);
        }

        emit SetAutomatedMarketMakerPair(pair, value);
    }

    function excludeFromDailyLimit(address account, bool excluded) public onlyOwner {
        require(isExcludedFromDailyLimit[account] != excluded, "Verse: Daily limit exclusion is already the value of 'excluded'");
        isExcludedFromDailyLimit[account] = excluded;
    }

    function setMaxTransactionLimitEnabled(bool enabled) public onlyOwner {
        require(maxTransactionLimitEnabled != enabled, "Verse: Max transaction limit enabled is already the value of 'enabled'");
        maxTransactionLimitEnabled = enabled;
    }
    
    
    function setMechanismsEnabled(bool enabled) public onlyOwner {
        mechanismsEnabled = enabled;
    }

    function setAidiBuybackEnabled(bool enabled) public onlyOwner {
        aidiBuybackEnabled = enabled;
    }

    function setETHRedistributionEnabled(bool enabled) public onlyOwner {
        ethRedistributionEnabled = enabled;
    }

    function setDevMarketingAndCharityFeeEnabled(bool enabled) public onlyOwner {
        devMarketingAndCharityEnabled = enabled;
    }

    function setMaxTransactionAmount(uint256 newAmount) public onlyOwner {
        maxTransactionAmount = newAmount;
    }
    
    function setSwapTokensAtAmount(uint256 newAmount) public onlyOwner {
        swapTokensAtAmount = newAmount;
    }

    function setMaxDailySellAmount(uint256 newAmount) public onlyOwner {
        maxDailySellAmount = newAmount;
    }

    function updateDevMarketingAndCharityAddress(address payable newAddress) public onlyOwner {
        devMarketingAndCharityAddress = newAddress;
    }


    function updateGasForTransfer(uint256 gasForTransfer) external onlyOwner {
        dividendTracker.updateGasForTransfer(gasForTransfer);
    }

    function updateGasForProcessing(uint256 newValue) public onlyOwner {
        // Need to make gas fee customizable to future-proof against Ethereum network upgrades.
        require(newValue != gasForProcessing, "Verse: Cannot update gasForProcessing to same value");
        emit GasForProcessingUpdated(newValue, gasForProcessing);
        gasForProcessing = newValue;
    }

    function updateClaimWait(uint256 claimWait) external onlyOwner {
        dividendTracker.updateClaimWait(claimWait);
    }

    function getGasForTransfer() external view returns(uint256) {
        return dividendTracker.gasForTransfer();
    }


    function getClaimWait() external view returns(uint256) {
        return dividendTracker.claimWait();
    }

    function getTotalDividendsDistributed() external view returns (uint256) {
        return dividendTracker.totalDividendsDistributed();
    }

    function isExcludedFromFees(address account) public view returns(bool) {
        return _isExcludedFromFees[account];
    }

    function withdrawableDividendOf(address account) public view returns(uint256) {
        return dividendTracker.withdrawableDividendOf(account);
    }

    function dividendTokenBalanceOf(address account) public view returns (uint256) {
        return dividendTracker.balanceOf(account);
    }

    function getAccountDividendsInfo(address account)
    external view returns (
        address,
        int256,
        int256,
        uint256,
        uint256,
        uint256,
        uint256,
        uint256) {
        return dividendTracker.getAccount(account);
    }

    function getAccountDividendsInfoAtIndex(uint256 index)
    external view returns (
        address,
        int256,
        int256,
        uint256,
        uint256,
        uint256,
        uint256,
        uint256) {
        return dividendTracker.getAccountAtIndex(index);
    }

    function processDividendTracker(uint256 gas) external {
        (uint256 iterations, uint256 claims, uint256 lastProcessedIndex) = dividendTracker.process(gas);
        emit ProcessedDividendTracker(iterations, claims, lastProcessedIndex, false, gas, tx.origin);
    }

    function claim() external {
        dividendTracker.processAccount(payable(msg.sender), false);
    }

    function getLastProcessedIndex() external view returns(uint256) {
        return dividendTracker.getLastProcessedIndex();
    }

    function getLastProcessedAccount()
    external view returns (
        address,
        int256,
        int256,
        uint256,
        uint256,
        uint256,
        uint256,
        uint256) {
        return dividendTracker.getLastProcessedAccount();
    }

    function getNumberOfDividendTokenHolders() external view returns(uint256) {
        return dividendTracker.getNumberOfTokenHolders();
    }

    function reinvestInactive(address payable account) public onlyOwner {
        uint256 tokenBalance = dividendTracker.balanceOf(account);
        require(tokenBalance <= minimumTokenBalanceForDividends, "Verse: Account balance must be less then minimum token balance for dividends");

        uint256 _lastTransfer = lastTransfer[account];
        require(block.timestamp.sub(_lastTransfer) > 12 weeks, "Verse: Account must have been inactive for at least 12 weeks");

        dividendTracker.processAccount(account, false);
        uint256 dividends = address(this).balance;
        (bool success,) = address(dividendTracker).call{value: dividends}("");

        if(success) {
   	 		emit SendDividends(dividends);
            try dividendTracker.setBalance(account, 0) {} catch {}
        }
    }


    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal override {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        require(amount > 0, "Transfer amount must be greater than zero");
        
        if(from != owner() && to != owner()) {
            if(maxTransactionLimitEnabled)
            {
                require(amount <= maxTransactionAmount, "Transfer amount exceeds the maxTransactionAmount.");
            }
            require(!_isBot[from], 'Bots not allowed here! Play fair.');//antibot
            require(!_isBot[to], 'Bots not allowed here! Play fair.');//antibot
        }

        // buy
        if (from == uniswapV2Pair &&
            to != address(uniswapV2Router) &&
            !_isExcludedFromFees[to]) {
            require(tradingOpen, 'Trading not yet enabled.');

            //antibot: block zero bots will be added to bot blacklist
            if (block.timestamp == launchTime) {
                _isBot[to] = true;
                _confirmedBots.push(to);
            }
        }
        
        if(mechanismsEnabled && !swapping)
        {

    		uint256 contractTokenBalance = balanceOf(address(this));
    
            bool canSwap = contractTokenBalance >= swapTokensAtAmount;
    
            if (from == uniswapV2Pair && to != address(uniswapV2Router) && !_isExcludedFromFees[to]) { //a buy
                if(aidiBuybackEnabled)
                {
                    buybackFee = 3;
                }else{
                    buybackFee = 0;
                }
                if(ethRedistributionEnabled)
                {
                    ethRedistributeFee = 4;
                }else{
                    ethRedistributeFee = 0;
                }
    
                if(devMarketingAndCharityEnabled)
                {
                    devMarketingAndCharityFee = 3;
                }else{
                    devMarketingAndCharityFee = 0;
                }
    
                totalFee = buybackFee + ethRedistributeFee + devMarketingAndCharityFee;
            }else if (to == uniswapV2Pair && from != address(uniswapV2Router) && !_isExcludedFromFees[to]) { //a sell
    
                if(dailySell[getDay()][from].add(amount) > maxDailySellAmount)
                {
    
                    if(aidiBuybackEnabled)
                    {
                        buybackFee = 6;
                    }else{
                        buybackFee = 0;
                    }
                    if(ethRedistributionEnabled)
                    {
                        ethRedistributeFee = 8;
                    }else{
                        ethRedistributeFee = 0;
                    }
    
                    if(devMarketingAndCharityEnabled)
                    {
                        devMarketingAndCharityFee = 6;
                    }else{
                        devMarketingAndCharityFee = 0;
                    }
    
                    totalFee = buybackFee + ethRedistributeFee + devMarketingAndCharityFee;
                }else{
                    if(aidiBuybackEnabled)
                    {
                        buybackFee = 3;
                    }else{
                        buybackFee = 0;
                    }
                    if(ethRedistributionEnabled)
                    {
                        ethRedistributeFee = 4;
                    }else{
                        ethRedistributeFee = 0;
                    }
                    if(devMarketingAndCharityEnabled)
                    {
                        devMarketingAndCharityFee = 3;
                    }else{
                        devMarketingAndCharityFee = 0;
                    }
    
                    totalFee = buybackFee + ethRedistributeFee + devMarketingAndCharityFee;
                }
                dailySell[getDay()][from] = dailySell[getDay()][from].add(amount);
            }
    
    
            if( canSwap &&
            !automatedMarketMakerPairs[from] &&
            !_isExcludedFromFees[from] &&
            !_isExcludedFromFees[to] &&
            (totalFee > 0))
            {
                swapping = true;
                swapAndDistribute();
                swapping = false;
            }
    
    
            bool takeFee = true;
    
            // if any account belongs to _isExcludedFromFee account then remove the fee
            // don't take a fee unless it's a buy / sell
            if((_isExcludedFromFees[from] || _isExcludedFromFees[to]) || (!automatedMarketMakerPairs[from] && !automatedMarketMakerPairs[to])) {
                takeFee = false;
            }
    
            if(takeFee && (totalFee >0)) {
    
            	uint256 fees = amount.mul(totalFee).div(100);
            	amount = amount.sub(fees);
    
                super._transfer(from, address(this), fees);
            }
        
            super._transfer(from, to, amount);
    
            try dividendTracker.setBalance(payable(from), balanceOf(from)) {} catch {}
            try dividendTracker.setBalance(payable(to), balanceOf(to)) {} catch {}
    
            lastTransfer[from] = block.timestamp;
            lastTransfer[to] = block.timestamp;
    
            if (ethRedistributionEnabled) {
                uint256 gas = gasForProcessing;
    
                try dividendTracker.process(gas) returns (uint256 iterations, uint256 claims, uint256 lastProcessedIndex) {
                    emit ProcessedDividendTracker(iterations, claims, lastProcessedIndex, true, gas, tx.origin);
                } catch {
    
                }
            }
        }else{
            super._transfer(from, to, amount);
        }
    }

    function swapTokensForEth(uint256 tokenAmount) private {
        // generate the uniswap pair path of token -> weth
        address[] memory path = new address[](2);
        path[0] = address(this);
        path[1] = uniswapV2Router.WETH();

        _approve(address(this), address(uniswapV2Router), tokenAmount);

        // make the swap
        uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
            tokenAmount,
            0, // accept any amount of ETH
            path,
            address(this),
            block.timestamp
        );
    }

    function swapEthForTokens(uint256 ethAmount, uint256 minTokens, address account) internal returns(uint256) {
        address[] memory path = new address[](2);
        path[0] = uniswapV2Router.WETH();
        path[1] = address(this);

        uint256 balanceBefore = balanceOf(account);

        uniswapV2Router.swapExactETHForTokensSupportingFeeOnTransferTokens{value: ethAmount}(
            minTokens,
            path,
            account,
            block.timestamp
        );

        uint256 tokenAmount = balanceOf(account).sub(balanceBefore);
        return tokenAmount;
    }

    function swapAndDistribute() private {
        uint256 tokenBalance = balanceOf(address(this));
        swapTokensForEth(tokenBalance);

        uint256 ethBalance = address(this).balance;
        uint256 devMarketingAndCharityPortion = ethBalance.mul(devMarketingAndCharityFee).div(totalFee);
        uint256 buybackAndBurnPortion = ethBalance.mul(buybackFee).div(totalFee);

        if(devMarketingAndCharityEnabled && devMarketingAndCharityPortion > 0)
        {
            devMarketingAndCharityAddress.transfer(devMarketingAndCharityPortion);
        }
        if(aidiBuybackEnabled && buybackAndBurnPortion > 0)
        {
            buybackAndBurnAidiTokens(buybackAndBurnPortion);
        }

        uint256 dividends = address(this).balance;
        if(ethRedistributionEnabled && dividends > 0)
        {
            (bool success,) = address(dividendTracker).call{value: dividends}("");

            if(success) {
       	 		emit SendDividends(dividends);
            }
        }
    }

    function buybackAndBurnAidiTokens(uint256 amount) private {
        // generate the uniswap pair path of weth -> token
        address[] memory path = new address[](2);
        path[0] = uniswapV2Router.WETH();
        path[1] = address(0xdA1E53E088023Fe4D1DC5a418581748f52CBd1b8);

        // make the swap
        uniswapV2Router.swapExactETHForTokensSupportingFeeOnTransferTokens{value: amount}(
            0, // accept any amount of Tokens
            path,
            deadAddress, // Burn address
            block.timestamp.add(300)
        );
    }

    function getDay() internal view returns(uint256){
        return block.timestamp.div(1 days);
    }

    function openTrading() external onlyOwner {
        tradingOpen = true;
        launchTime = block.timestamp;
    }

    function isBot(address account) public view returns (bool) {
        return _isBot[account];
    }
    //manual antibot, play fair!
    function _blacklistBot(address account) external onlyOwner() {
        require(account != 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D, 'We cannot blacklist Uniswap');
        require(!_isBot[account], "Account is already blacklisted");
        _isBot[account] = true;
        _confirmedBots.push(account);
    }

    function _amnestyBot(address account) external onlyOwner() {
        require(_isBot[account], "Account is not blacklisted");
        for (uint256 i = 0; i < _confirmedBots.length; i++) {
            if (_confirmedBots[i] == account) {
                _confirmedBots[i] = _confirmedBots[_confirmedBots.length - 1];
                _isBot[account] = false;
                _confirmedBots.pop();
                break;
            }
        }
    }
}
function _blacklistBot(address account) external onlyOwner() {
    require(account != 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D, 'We cannot blacklist Uniswap');
    require(!_isBot[account], "Account is already blacklisted");
    _isBot[account] = true;
    _confirmedBots.push(account);
}

//manual antibot, play fair!