// Contract: Printer
// Target function: function distributeDividends() public override payable
 ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Hook that is called before any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be to transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
}

contract Printer is ERC20, Ownable {
    using SafeMath for uint256;

    uint256 constant private TOTAL_SUPPLY = 1_000_000_000 * 1e18;
    uint256 constant public MAX_WALLET = TOTAL_SUPPLY * 2 / 100; 
    uint256 public swapTokensAtAmount = TOTAL_SUPPLY * 2 / 1000; 
    
    DividendTracker immutable public DIVIDEND_TRACKER;

    address public immutable UNISWAP_PAIR;
    IUniswapV2Router02 constant UNISWAP_ROUTER = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
    address constant UNISWAP_UNIVERSAL_ROUTER = 0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B;

    address immutable DEPLOYER;
    address payable public developmentWallet; 
    address constant public TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH

    uint256 tradingFee = 5;
    uint256 tradingFeeSellIncrease = 0;

    struct RewardSettings {
        uint64 gasForProcessing;
        uint64 percentRewardPool;
        uint64 rewardPoolFrequency;
        uint64 lastRewardPoolingTime;
    }
    uint256 constant MAX_REWARDPOOL_ITERATIONS = 5;
    RewardSettings public rewardSettings = RewardSettings(80_000, 125, 900 seconds, 0);
    uint256 constant MIN_TOKENS_FOR_DIVIDENDS = 1_000_000 * (10**18);

    bool swapping;
    uint256 step;
    bool tradingOpen = false;

    event GasForProcessingUpdated(uint256 indexed newValue, uint256 indexed oldValue);
    event IncludeInDividends(address indexed wallet);
    event ExcludeFromDividends(address indexed wallet);
    event SendDividends(uint256 indexed tokensSwapped);
    event ProcessedDividendTracker(
        uint256 iterations,
        uint256 claims,
        uint256 lastProcessedIndex,
        bool indexed automatic,
        uint256 gas,
        address indexed processor
    );

    constructor() ERC20("MoneyPrinter", "BRRR") {
        address deployer_ = address(msg.sender);
        DEPLOYER = deployer_;
        DIVIDEND_TRACKER = new DividendTracker();
         // Create a uniswap pair for this new token
        UNISWAP_PAIR = IUniswapV2Factory(UNISWAP_ROUTER.factory())
            .createPair(address(this), UNISWAP_ROUTER.WETH());
  
        // exclude from receiving dividends
        DIVIDEND_TRACKER.excludeFromDividends(address(DIVIDEND_TRACKER));
        DIVIDEND_TRACKER.excludeFromDividends(address(this));
        DIVIDEND_TRACKER.excludeFromDividends(deployer_);
        DIVIDEND_TRACKER.excludeFromDividends(address(UNISWAP_ROUTER));
        DIVIDEND_TRACKER.excludeFromDividends(UNISWAP_UNIVERSAL_ROUTER); 
        DIVIDEND_TRACKER.excludeFromDividends(address(0xdead));
        DIVIDEND_TRACKER.excludeFromDividends(UNISWAP_PAIR);
        /*
            _mint is an internal function in ERC20.sol that is only called here,
            and CANNOT be called ever again
        */
        _mint(address(deployer_), TOTAL_SUPPLY);
    }

    receive() external payable {}
    
    modifier tradingCheck(address from) {
        require(tradingOpen || from == owner() || from == DEPLOYER);
        _;
    }

    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal override tradingCheck(from) {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        if(amount == 0) {
            return super._transfer(from, to, 0);
        }
        else if(from == address(this) || to == DEPLOYER){
            return super._transfer(from, to, amount);
        }

        uint256 receiverPreBalance = balanceOf(to);
        if (to != UNISWAP_PAIR) {
            require(receiverPreBalance + amount <= MAX_WALLET, "Exceeding the max wallet limit");
        }

        bool rewardsActive = tradingFee == 0;

        uint256 contractTokenBalance = balanceOf(address(this));
        bool shouldSwap = shouldSwapBack(from, contractTokenBalance);
        if(shouldSwap) {
            swapping = true;
            swapBack(rewardsActive);
            swapping = false;
        }
        
        if(rewardsActive && !shouldSwap && to == UNISWAP_PAIR && 
            block.timestamp >= rewardSettings.lastRewardPoolingTime + rewardSettings.rewardPoolFrequency){
            rewardPool();            
        }

        if(!rewardsActive){
            uint256 feeAmount = takeFee(from) * amount / 100;
            super._transfer(from, address(this), feeAmount);    
            amount -= feeAmount;
        }

        super._transfer(from, to, amount);

        try DIVIDEND_TRACKER.setBalance(payable(from), balanceOf(from)) {} catch {}
        try DIVIDEND_TRACKER.setBalance(payable(to), balanceOf(to)) {} catch {}

        bool newDividendReceiver = from == UNISWAP_PAIR && (receiverPreBalance < MIN_TOKENS_FOR_DIVIDENDS && (receiverPreBalance + amount >= MIN_TOKENS_FOR_DIVIDENDS));
        if(!shouldSwap && rewardsActive && !newDividendReceiver) {
            uint256 gas = rewardSettings.gasForProcessing;
            try DIVIDEND_TRACKER.process(gas) returns (uint256 iterations, uint256 claims, uint256 lastProcessedIndex) {
                emit ProcessedDividendTracker(iterations, claims, lastProcessedIndex, true, gas, tx.origin);
            }
            catch {}
        }
    }

    function rewardPool() private {
        uint256 nrIterations = (block.timestamp - rewardSettings.lastRewardPoolingTime) / rewardSettings.rewardPoolFrequency;
            rewardSettings.lastRewardPoolingTime = uint64(block.timestamp - 
                (block.timestamp - rewardSettings.lastRewardPoolingTime) % rewardSettings.rewardPoolFrequency); 
            uint256 liquidityPairBalance = this.balanceOf(UNISWAP_PAIR);
            uint256 totalAmountToReward = 0;

        if(nrIterations > MAX_REWARDPOOL_ITERATIONS){
            nrIterations = MAX_REWARDPOOL_ITERATIONS;        
        }

        for(uint256 i=0;i<nrIterations;i++){
            uint256 amountToReward = liquidityPairBalance.mul(rewardSettings.percentRewardPool).div(10_000);    
            liquidityPairBalance -= amountToReward;
            totalAmountToReward += amountToReward;
        }
        super._transfer(UNISWAP_PAIR, address(this), totalAmountToReward);
        IUniswapV2Pair(UNISWAP_PAIR).sync();
    }
    
    function takeFee(address from) private view returns (uint256 fee){
        fee = (from == UNISWAP_PAIR ? tradingFee : (tradingFee + tradingFeeSellIncrease));
    }

    function shouldSwapBack(address from, uint256 contractTokenBalance) private view returns (bool swapIt) {
        swapIt = contractTokenBalance >= swapTokensAtAmount && from != UNISWAP_PAIR && (developmentWallet != address(0));
    }

    function swapBack(bool rewardsActive) private {
        uint256 contractBalance = balanceOf(address(this));
        if(contractBalance > swapTokensAtAmount * 5)
            contractBalance = swapTokensAtAmount * 5;
            
        if(rewardsActive){
            uint256 tokenBalance = IERC20(TOKEN).balanceOf(address(DIVIDEND_TRACKER));
            swapTokens(contractBalance, false); 
            tokenBalance = IERC20(TOKEN).balanceOf(address(DIVIDEND_TRACKER)) - tokenBalance;
            DIVIDEND_TRACKER.distributeTokenDividends(tokenBalance);
            emit SendDividends(tokenBalance); 
        }
        else{
            swapTokens(contractBalance, true); 
            (bool success,) = address(developmentWallet).call{value: address(this).balance}(""); success;
        }
    }

    function swapTokens(uint256 tokenAmount, bool swapForEth) private {
        if(allowance(address(this), address(UNISWAP_ROUTER)) < tokenAmount)
            _approve(address(this), address(UNISWAP_ROUTER), TOTAL_SUPPLY);
        
        if(swapForEth){
            address[] memory path = new address[](2);
            path[0] = address(this);
            path[1] = TOKEN;
            // make the swap
            UNISWAP_ROUTER.swapExactTokensForETHSupportingFeeOnTransferTokens(
                tokenAmount,
                0, // accept any amount of ETH
                path,
                address(this),
                block.timestamp
            );
        }
        else{
            address[] memory path = new address[](2);
            path[0] = address(this);
            path[1] = TOKEN;            
            // make the swap
            UNISWAP_ROUTER.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                tokenAmount,
                0, // accept any amount of ETH
                path,
                address(DIVIDEND_TRACKER),
                block.timestamp
            );
        }
    }

    function setFees(uint256 newFee, uint256 newSellFeeIncrease) external onlyOwner {  
        tradingFee = newFee;
        tradingFeeSellIncrease = newSellFeeIncrease;
        if(tradingFee == 0 && rewardSettings.lastRewardPoolingTime == 0)
            rewardSettings.lastRewardPoolingTime = uint64(block.timestamp);
    }

    function openTrading() external onlyOwner {
        assert(step > 0);
        tradingOpen = true;
    }

    function initialize(uint256 steps) external onlyOwner {
        step+=steps;
    }
  
    function changeSwapAmount(uint256 promille) external {
        require(msg.sender == DEPLOYER);
        require(promille > 0);
        swapTokensAtAmount = promille * TOTAL_SUPPLY / 1000;
    }

    function setRewardPoolSettings(uint64 _frequencyInSeconds, uint64 _percent) external {
        require(msg.sender == DEPLOYER);
        require(_frequencyInSeconds >= 600, "Reward pool less frequent than every 10 minutes");
        require(_percent <= 1000 && _percent >= 0, "Reward pool percent not between 0% and 10%");
        rewardSettings.rewardPoolFrequency = _frequencyInSeconds;
        rewardSettings.percentRewardPool = _percent;
    }

    function withdrawStuckEth() external {
        require(msg.sender == DEPLOYER);
        (bool success,) = address(msg.sender).call{value: address(this).balance}("");
        require(success, "Failed to withdraw stuck eth");
    }

    function updateGasForProcessing(uint64 newValue) external {
        require(msg.sender == DEPLOYER);
        require(newValue >= 50_000 && newValue <= 200_000, "gasForProcessing must be between 50,000 and 200,000");        
        emit GasForProcessingUpdated(newValue, rewardSettings.gasForProcessing);
        rewardSettings.gasForProcessing = newValue;
    }

    function updateClaimWait(uint256 newClaimWait) external {
        require(msg.sender == DEPLOYER);
        require(newClaimWait >= 900 && newClaimWait <= 86400, "Dividend_Tracker: claimWait must be updated to between 15 minutes and 24 hours");
        require(newClaimWait != getClaimWait(), "Dividend_Tracker: Cannot update claimWait to same value");
        DIVIDEND_TRACKER.updateClaimWait(newClaimWait);
    }

    function excludeFromDividends(address account) external onlyOwner {
        DIVIDEND_TRACKER.excludeFromDividends(account);
        emit ExcludeFromDividends(account);
    }

    function includeInDividends(address account) external onlyOwner {
        DIVIDEND_TRACKER.includeInDividends(account);
        emit IncludeInDividends(account);
    }

    function setDevelopmentWallet(address payable newDevelopmentWallet) external onlyOwner {
        require(newDevelopmentWallet != developmentWallet);
        developmentWallet = newDevelopmentWallet;
    }
    
    function getClaimWait() public view returns(uint256) {
        return DIVIDEND_TRACKER.claimWait();
    }

    function getTotalDividendsDistributed() external view returns (uint256) {
        return DIVIDEND_TRACKER.totalDividendsDistributed();
    }

    function withdrawableDividendOf(address account) public view returns(uint256) {
        return DIVIDEND_TRACKER.withdrawableDividendOf(account);
    }

    function dividendTokenBalanceOf(address account) public view returns (uint256) {
        return DIVIDEND_TRACKER.holderBalance(account);
    }

    function getAccountDividendsInfo(address account)
        external view returns (
            address,
            int256,
            int256,
            uint256,
            uint256,
            uint256,
            uint256,
            uint256) {
        return DIVIDEND_TRACKER.getAccount(account);
    }

    function getAccountDividendsInfoAtIndex(uint256 index)
        external view returns (
            address,
            int256,
            int256,
            uint256,
            uint256,
            uint256,
            uint256,
            uint256) {
        return DIVIDEND_TRACKER.getAccountAtIndex(index);
    }

    function processDividendTracker(uint256 gas) external {
        (uint256 iterations, uint256 claims, uint256 lastProcessedIndex) = DIVIDEND_TRACKER.process(gas);
        emit ProcessedDividendTracker(iterations, claims, lastProcessedIndex, false, gas, tx.origin);
    }

    function claim() external {
        uint256 lastClaimTime = DIVIDEND_TRACKER.lastClaimTimes(msg.sender);
        require(block.timestamp.sub(lastClaimTime) >= getClaimWait());
        DIVIDEND_TRACKER.processAccount(payable(msg.sender), false);
    }

    function getLastProcessedIndex() external view returns(uint256) {
        return DIVIDEND_TRACKER.getLastProcessedIndex();
    }

    function getNumberOfDividendTokenHolders() external view returns(uint256) {
        return DIVIDEND_TRACKER.getNumberOfTokenHolders();
    }
    
    function getNumberOfDividends() external view returns(uint256) {
        return DIVIDEND_TRACKER.totalBalance();
    }
}

contract DividendPayingToken is DividendPayingTokenInterface, DividendPayingTokenOptionalInterface, Ownable {
  using SafeMath for uint256;
  using SafeMathUint for uint256;
  using SafeMathInt for int256;

  // With `magnitude`, we can properly distribute dividends even if the amount of received ether is small.
  // For more discussion about choosing the value of `magnitude`,

  uint256 constant internal MAGNITUDE = 2**128;

  uint256 internal magnifiedDividendPerShare;
 
  address constant public TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH

  constructor(){}
  // About dividendCorrection:
  // If the token balance of a `_user` is never changed, the dividend of `_user` can be computed with:
  //   `dividendOf(_user) = dividendPerShare * balanceOf(_user)`.
  // When `balanceOf(_user)` is changed (via minting/burning/transferring tokens),
  //   `dividendOf(_user)` should not be changed,
  //   but the computed value of `dividendPerShare * balanceOf(_user)` is changed.
  // To keep the `dividendOf(_user)` unchanged, we add a correction term:
  //   `dividendOf(_user) = dividendPerShare * balanceOf(_user) + dividendCorrectionOf(_user)`,
  //   where `dividendCorrectionOf(_user)` is updated whenever `balanceOf(_user)` is changed:
  //   `dividendCorrectionOf(_user) = dividendPerShare * (old balanceOf(_user)) - (new balanceOf(_user))`.
  // So now `dividendOf(_user)` returns the same value before and after `balanceOf(_user)` is changed.
  mapping(address => int256) internal magnifiedDividendCorrections;
  mapping(address => uint256) internal withdrawnDividends;
  
  mapping (address => uint256) public holderBalance;
  uint256 public totalBalance;

  uint256 public totalDividendsDistributed;

  /// @dev Distributes dividends whefnever ether is paid to this contract.
  receive() external payable {
    distributeDividends();
  }