// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin-contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol";

contract Reserve is ERC20Permit {
    address private immutable _owner; // forge-lint: disable-line(screaming-snake-case-immutable)
    uint256 public accumulatedDonation;
    uint256 public conversionRate;
    uint256 public donationRate;
    error OnlyOwner();

    constructor(
        string memory name,
        string memory symbol,
        uint256 rate
    ) ERC20(name, symbol) ERC20Permit(name) {
        conversionRate = rate;
        _owner = msg.sender;
    }

    fallback() external payable {
        _accumulateDonation(msg.value);
        _mint(msg.sender, msg.value * conversionRate);
    }

    receive() external payable {
        _accumulateDonation(msg.value);
        _mint(msg.sender, msg.value * conversionRate);
    }

    function changeRate(uint256 rate) external {
        if (msg.sender != _owner) revert OnlyOwner();
        conversionRate = rate;
    }

    function changeDonationRate(uint256 rate) external {
        if (msg.sender != _owner) revert OnlyOwner();
        donationRate = rate;
    }

    function withdraw(address payable receiver) external {
        if (msg.sender != _owner) revert OnlyOwner();
        uint256 accumulatedDonationCached = accumulatedDonation;
        accumulatedDonation = 0;

        (bool sent,) = receiver.call{ value: accumulatedDonationCached }("");
        require(sent, "Failed to send Ether");
    }

    function superMint(address account, uint256 value) external {
        if (msg.sender != _owner) revert OnlyOwner();
        _mint(account, value);
    }

    function mint(address account) external payable {
        _accumulateDonation(msg.value);
        _mint(account, msg.value * conversionRate);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
        uint256 valueDiff = address(this).balance - accumulatedDonation;
        uint256 valueBack = amount * valueDiff / (amount + totalSupply());

        (bool sent,) = msg.sender.call{ value: valueBack }("");
        require(sent, "Failed to send Ether");
    }

    function estimateAmount(uint256 amount) external view returns (uint256) {
        uint256 valueDiff = address(this).balance - accumulatedDonation;
        return amount * valueDiff / totalSupply();
    }

    function _accumulateDonation(uint256 value) private {
        uint256 donation = value * donationRate / 1e5;
        accumulatedDonation += donation;
    }
}
