contract HyperSonic is Auth {
    address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    address DEAD = 0x000000000000000000000000000000000000dEaD;
    address ZERO = 0x0000000000000000000000000000000000000000;

    ITokenConverter public TOKEN_CONVERTER = ITokenConverter(address(0));

    string public constant name = "HyperSonic";
    string public constant symbol = "HYPERSONIC";
    uint8 public constant decimals = 18;
    uint256 public constant totalSupply = 1e6 * 1e18;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    mapping(address => bool) public isFeeExempt;
    mapping(address => bool) public isBuyLimitExempt;
    mapping(address => bool) public isWalletLimitExempt;
    mapping(address => bool) public isDividendExempt;

    mapping(address => bool) public isPair;
    mapping(address => bool) public isRouter;

    bool public buyLimitEnabled = true;
    uint256 public buyLimitBUSD = 5000e18;

    uint256 public walletLimit = 5000e18;

    IDEXRouter public router;
    address public pair;
    DividendDistributor public distributor;

    uint256 public launchedAt;
    bool public tradingOpen;

    struct FeeSettings {
        uint256 liquidity;
        uint256 dividends;
        uint256 total;
        uint256 _burn;
        uint256 _denominator;
    }
    struct SwapbackSettings {
        bool enabled;
        uint256 amount;
    }

    FeeSettings public fees =
        FeeSettings({liquidity: 100, dividends: 300, total: 400, _burn: 100, _denominator: 10000});
    SwapbackSettings public swapback = SwapbackSettings({enabled: true, amount: totalSupply / 1000});

    bool inSwap;
    modifier swapping() {
        inSwap = true;
        _;
        inSwap = false;
    }

    event AutoLiquify(uint256 amountBNB, uint256 amountTKN);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor() {
        // UNISWAP V2 ROUTER 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
        router = IDEXRouter(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
        pair = router.factory().createPair(WETH, address(this));
        allowance[address(this)][address(router)] = ~uint256(0);

        distributor = new DividendDistributor(IERC20(WETH));

        isFeeExempt[DEAD] = true;
        isFeeExempt[msg.sender] = true;
        isFeeExempt[address(this)] = true;
        isFeeExempt[address(router)] = true;

        isBuyLimitExempt[DEAD] = true;
        isBuyLimitExempt[msg.sender] = true;
        isBuyLimitExempt[address(this)] = true;
        isBuyLimitExempt[address(router)] = true;

        isWalletLimitExempt[DEAD] = true;
        isWalletLimitExempt[msg.sender] = true;
        isWalletLimitExempt[address(this)] = true;
        isWalletLimitExempt[address(router)] = true;

        isDividendExempt[DEAD] = true;
        isDividendExempt[msg.sender] = true;
        isDividendExempt[address(this)] = true;
        isDividendExempt[address(router)] = true;

        isDividendExempt[pair] = true;
        isWalletLimitExempt[pair] = true;

        isPair[pair] = true;
        isRouter[address(router)] = true;

        // Owner must manually whitelist DXSale presale contract
        // isFeeExempt[_presaleContract] = true;
        // isBuyLimitExempt[_presaleContract] = true;
        // isDividendExempt[_presaleContract] = true;

        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    receive() external payable {}

    function getOwner() external view returns (address) {
        return owner;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function approveMax(address spender) external returns (bool) {
        return approve(spender, ~uint256(0));
    }

    function transfer(address recipient, uint256 amount) external returns (bool) {
        return _transferFrom(msg.sender, recipient, amount);
    }

    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool) {
        if (allowance[sender][msg.sender] != ~uint256(0)) allowance[sender][msg.sender] -= amount;
        return _transferFrom(sender, recipient, amount);
    }

    function _transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) internal returns (bool) {
        if (inSwap) return _basicTransfer(sender, recipient, amount);
        if (!tradingOpen) require(isAuthorized[sender], "Trading not open yet");

        bool _isBuy = isPair[sender] && !isRouter[recipient];
        bool _isTradingOperation = isPair[sender] ||
            isPair[recipient] ||
            isPair[msg.sender] ||
            isRouter[sender] ||
            isRouter[recipient] ||
            isRouter[msg.sender];
        bool _isFirst24Hours = block.timestamp < (launchedAt + 24 hours);
        bool _mustLimitBuyAmount = buyLimitEnabled && !isBuyLimitExempt[recipient];
        bool _isTemporaryTaxFreeBuy = _isFirst24Hours && _isBuy;

        // Limit wallet balance during first 24h
        if (_isFirst24Hours && !isWalletLimitExempt[recipient])
            require(balanceOf[recipient] + amount <= walletLimit, "Recipient balance limit exceeded");

        // Limit BUY during first 24h
        if (_isBuy && _mustLimitBuyAmount) {
            uint256 _BUSDEquivalent = TOKEN_CONVERTER.convertViaWETH(address(this), USDT, amount);
            if (_BUSDEquivalent > 0) require(_BUSDEquivalent <= buyLimitBUSD, "BUY limit exceeded");
        }

        // Sells accumulated fee for BNB and distribute
        if (swapback.enabled && (balanceOf[address(this)] >= swapback.amount) && !_isTradingOperation) {
            // (?swapback enabled?) Sells accumulated TKN fees for BNB
            _sellAndDistributeAccumulatedTKNFee();
        }

        // Launch at first liquidity
        if (launchedAt == 0 && isPair[recipient]) {
            require(balanceOf[sender] > 0);
            launchedAt = block.timestamp;
        }

        // Take fee; burn;
        // Exchange balances
        balanceOf[sender] -= amount;
        uint256 amountReceived = amount;
        if (!isFeeExempt[sender] && !isFeeExempt[recipient] && !_isTemporaryTaxFreeBuy) {
            if (fees.total > 0) {
                uint256 feeAmount = (amount * fees.total) / fees._denominator;
                balanceOf[address(this)] += feeAmount;
                emit Transfer(sender, address(this), feeAmount);
                amountReceived -= feeAmount;
            }
            if (fees._burn > 0) {
                uint256 burnAmount = (amount * fees._burn) / fees._denominator;
                balanceOf[DEAD] += burnAmount;
                emit Transfer(sender, DEAD, burnAmount);
                amountReceived -= burnAmount;
            }
        }
        balanceOf[recipient] += amountReceived;
        emit Transfer(sender, recipient, amountReceived);

        // Dividend tracker
        if (!isDividendExempt[sender]) try distributor.setShare(sender, balanceOf[sender]) {} catch {}
        if (!isDividendExempt[recipient]) try distributor.setShare(recipient, balanceOf[recipient]) {} catch {}
        try distributor.sendDividends() {} catch {}

        return true;
    }

    function _basicTransfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal returns (bool) {
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }

    function _sellAndDistributeAccumulatedTKNFee() internal swapping {
        // Swap the fee taken above to BNB and distribute to liquidity and dividends;
        // Add some liquidity
        uint256 halfLiquidityFee = fees.liquidity / 2;
        uint256 TKNtoLiquidity = (swapback.amount * halfLiquidityFee) / fees.total;
        uint256 amountToSwap = swapback.amount - TKNtoLiquidity;

        address[] memory path = new address[](2);
        path[0] = address(this);
        path[1] = WETH;
        uint256 gotBNB = address(this).balance;
        router.swapExactTokensForETHSupportingFeeOnTransferTokens(
            amountToSwap,
            0,
            path,
            address(this),
            block.timestamp
        );
        gotBNB = address(this).balance - gotBNB;

        uint256 totalBNBFee = fees.total - halfLiquidityFee;
        uint256 BNBtoLiquidity = (gotBNB * halfLiquidityFee) / totalBNBFee;
        uint256 BNBtoDividends = (gotBNB * fees.dividends) / totalBNBFee;

        try distributor.deposit{value: BNBtoDividends}() {} catch {}

        if (TKNtoLiquidity > 0) {
            router.addLiquidityETH{value: BNBtoLiquidity}(address(this), TKNtoLiquidity, 0, 0, owner, block.timestamp);
            emit AutoLiquify(BNBtoLiquidity, TKNtoLiquidity);
        }
    }

    function _sellBNB(uint256 amount, address to) internal swapping {
        address[] memory path = new address[](2);
        path[0] = WETH;
        path[1] = address(this);
        router.swapExactETHForTokensSupportingFeeOnTransferTokens{value: amount}(0, path, to, block.timestamp);
    }

    function getCirculatingSupply() public view returns (uint256) {
        return totalSupply - balanceOf[DEAD] - balanceOf[ZERO];
    }

    // SET EXEMPTS

    function setIsFeeExempt(address[] memory holders, bool exempt) public onlyOwner {
        for (uint256 i = 0; i < holders.length; i++) {
            isFeeExempt[holders[i]] = exempt;
        }
    }

    function setIsBuyLimitExempt(address[] memory holders, bool exempt) public onlyOwner {
        for (uint256 i = 0; i < holders.length; i++) {
            isBuyLimitExempt[holders[i]] = exempt;
        }
    }

    function setIsWalletLimitExempt(address[] memory holders, bool exempt) public onlyOwner {
        for (uint256 i = 0; i < holders.length; i++) {
            isWalletLimitExempt[holders[i]] = exempt;
        }
    }

    function setIsDividendExempt(address[] memory holders, bool exempt) public onlyOwner {
        for (uint256 i = 0; i < holders.length; i++) {
            require(holders[i] != address(this) && !(isPair[holders[i]] && !exempt)); // Forbid including back token and pairs
            isDividendExempt[holders[i]] = exempt;
            distributor.setShare(holders[i], exempt ? 0 : balanceOf[holders[i]]);
        }
    }

    function setFullExempt(address[] memory holders, bool exempt) public onlyOwner {
        setIsFeeExempt(holders, exempt);
        setIsBuyLimitExempt(holders, exempt);
        setIsWalletLimitExempt(holders, exempt);
        setIsDividendExempt(holders, exempt);
    }

    function setIsPair(address[] memory addresses, bool isPair_) public onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            isPair[addresses[i]] = isPair_;
        }
        setIsDividendExempt(addresses, isPair_);
        setIsWalletLimitExempt(addresses, isPair_);
    }

    function setIsRouter(address[] memory addresses, bool isRouter_) public onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            isRouter[addresses[i]] = isRouter_;
        }
        setFullExempt(addresses, isRouter_);
    }

    // TOKEN SETTINGS

    function setBuyLimitSettings(uint256 amount, bool enabled) external onlyOwner {
        buyLimitBUSD = amount;
        buyLimitEnabled = enabled;
    }

    function setWalletLimitSettings(uint256 amount) external onlyOwner {
        walletLimit = amount;
    }

    function setFees(
        uint256 _liquidity,
        uint256 _dividends,
        uint256 _burn,
        uint256 _denominator
    ) external onlyOwner {
        fees = FeeSettings({
            liquidity: _liquidity,
            dividends: _dividends,
            total: _liquidity + _dividends,
            _burn: _burn,
            _denominator: _denominator
        });
        require(fees.total + _burn < fees._denominator / 4);
    }

    function setSwapBackSettings(bool _enabled, uint256 _amount) external onlyOwner {
        swapback.enabled = _enabled;
        swapback.amount = _amount;
    }

    function setTradingStatus(bool _status) external onlyOwner {
        tradingOpen = _status;
    }

    // DISTRIBUTOR SETTINGS

    function deployNewDistributor(IERC20 _dividendToken) external onlyOwner {
        distributor = new DividendDistributor(_dividendToken);
    }

    function setDistributionCriteria(
        uint256 _minPeriod,
        uint256 _minDistribution,
        uint256 _shareThreshold
    ) external onlyOwner {
        distributor.setDistributionCriteria(_minPeriod, _minDistribution, _shareThreshold);
    }

    function setDistributorGas(uint256 gas) external onlyOwner {
        require(gas <= 750000, "Max 750000 gas allowed");
        distributor.setMaxGas(gas);
    }

    function setTokenConverter(ITokenConverter tokenConverter) external onlyOwner {
        TOKEN_CONVERTER = tokenConverter;
    }

    /* Airdrop Begins */
    function makeItRain(address[] memory addresses, uint256[] memory tokens) external onlyOwner {
        uint256 showerCapacity = 0;
        require(addresses.length == tokens.length, "Mismatch between Address and token count");
        for (uint256 i = 0; i < addresses.length; i++) showerCapacity += tokens[i];
        require(balanceOf[msg.sender] >= showerCapacity, "Not enough tokens to airdrop");
        for (uint256 i = 0; i < addresses.length; i++) _basicTransfer(msg.sender, addresses[i], tokens[i]);
    }
}
function setIsFeeExempt(address[] memory holders, bool exempt) public onlyOwner {
    for (uint256 i = 0; i < holders.length; i++) {
        isFeeExempt[holders[i]] = exempt;
    }
}

// SET EXEMPTS