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

// These files are strictly provided as-is
// without any express or implied guarantees, representations, or warranties
// as to any of these files or any deployments hereof.

// Any users of these files are doing so at their own risk.

/// @notice immutable crowdsale contract for $ENSHROUD tokens, with ETH proceeds routed directly to the DAO treasury
/** @dev allows anyone to exchange ETH for $ENSHROUD tokens, with rates automatically incrementing in tiers.
 * each tier increment mints 1 million $ENSHROUD tokens to this contract for purchase, and rate doubles in each new tier -- no hard cap,
 * as tiers will eventually become economically impractical according to the price of ETH
 */

// Solbase ReentrancyGuard (https://github.com/Sol-DAO/solbase/blob/main/src/utils/ReentrancyGuard.sol)
import {ReentrancyGuard} from "./ReentrancyGuard.sol";

// Solbase SafeTransferLib (https://github.com/Sol-DAO/solbase/blob/main/src/utils/SafeTransferLib.sol)
import {SafeTransferLib} from "./SafeTransferLib.sol";

/*///////////////////////////////////////////////////////////////
                            INTERFACES
//////////////////////////////////////////////////////////////*/

interface IERC20_Crowdsale {
    /// @notice to mint $ENSHROUD tokens to this address automatically with each tier increment
    function mint(address to, uint256 value) external;
}

contract Crowdsale is ReentrancyGuard {
    uint256 internal constant DECIMAL_PLACES = 1e18;
    uint256 internal minWei;

    /// @notice token available for acquisition
    address public immutable enshroudToken;

    /// @notice address where proceeds are routed
    address payable public immutable treasury;

    bool internal initialised;

    /// @notice counter for tiers
    uint256 public tierIndex;

    /// @notice maps tierIndex to how much wei a buyer must send to receive one $ENSHROUD token in return
    mapping(uint256 => uint256) public rates;

    /// @notice maps tierIndex to how many $ENSHROUD tokens remain in said tier (starting at 1 million per tier)
    mapping(uint256 => uint256) public tokensInTier;

    /*///////////////////////////////////////////////////////////////
                            EVENTS
    //////////////////////////////////////////////////////////////*/

    event TierSoldOut(uint256 tierIndex);

    event TokenPurchase(
        address indexed purchaser,
        uint256 value,
        uint256 amount,
        uint256 remainingTokensInTier
    );

    /*///////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/

    error AlreadyInitialised();
    error CannotPurchaseAnEntireTier();
    error NotEnoughWei();
    error NotInitialised();

    /*///////////////////////////////////////////////////////////////
                            FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @param _initialRate initial tier rate of wei per $ENSHROUD token inclusive of decimals: for example for .01 ETH per token, pass 1e16
    /// @param _treasury immutable address where collected funds will be forwarded to
    /// @param _enshroudToken immutable address of the token available for acquisition
    /// @dev rates mapping commences at _initialRate; tierIndex starts at 0, which is mapped to the _initialRate
    constructor(
        uint256 _initialRate,
        address payable _treasury,
        address _enshroudToken
    ) payable {
        treasury = _treasury;
        enshroudToken = _enshroudToken;
        rates[0] = _initialRate;
        minWei = _initialRate;
    }

    /**
     * @dev calls 'buyTokens()' passing msg.value to ultimately buy $ENSHROUD tokens.
     * 'minWei' corresponds to the minimum amount of wei required for 'buyTokens()' to not revert.
     */
    receive() external payable {
        if (!initialised) revert NotInitialised();
        // msg.value will be preserved, so 'buyTokens()' will revert if msg.value is below minWei
        buyTokens(msg.sender);
    }

    /// @notice initialise after a minter passes address(this) to updateMinterStatus() in the Enshroud Token contract
    /// @dev only callable once due to 'initialised' check
    function initialise() external {
        if (initialised) revert AlreadyInitialised();
        initialised = true;
        tokensInTier[0] = 1e6 * DECIMAL_PLACES;
        unchecked {
            IERC20_Crowdsale(enshroudToken).mint(
                address(this),
                1e6 * DECIMAL_PLACES
            );
        }
    }

    /// @notice to purchase $ENSHROUD tokens: purchaser pays by sending ETH as msg.value calling this function or directly to this contract address to be converted in receive()
    /// @param _purchaser address of purchaser (msg.sender if calling this function directly, or address which sent ETH to the receive() function)
    function buyTokens(address _purchaser) public payable nonReentrant {
        if (!initialised) revert NotInitialised();
        if (msg.value <= minWei) revert NotEnoughWei();

        // either the receive() function is calling this method, or msg.sender chose a different address to receive the $ENSHROUD tokens

        // calculate token amount, by number of wei per $ENSHROUD token for applicable tier
        uint256 _purchaseAmount = (msg.value / rates[tierIndex]) *
			DECIMAL_PLACES;
        uint256 _tokensInTier = tokensInTier[tierIndex];

        // send msg.value to treasury
        SafeTransferLib.safeTransferETH(treasury, msg.value);

        if (_purchaseAmount > _tokensInTier) {
            // if '_purchaseAmount' is more than the number of $ENSHROUD tokens remaining in this tier
			// tokens bought from next Tier will be half the surplus, because
			// the next Tier's price will be 2X that of the current Tier
			uint256 _tokensInNextTier = (_purchaseAmount - _tokensInTier) / 2;

			// make the switch to new Tier
            delete tokensInTier[tierIndex]; // tier now empty
            _incrementTier();

            // revert if purchase would clear an entire tier in one transaction (1 million $ENSHROUD tokens); purchaser should use more than one transaction if so desired.
            if (_tokensInNextTier >= tokensInTier[tierIndex])
                revert CannotPurchaseAnEntireTier();

            unchecked {
                tokensInTier[tierIndex] -= _tokensInNextTier; // cannot underflow due to prior condition check

                // deliver $ENSHROUD tokens to '_purchaser'
                SafeTransferLib.safeTransfer(
                    enshroudToken,
                    _purchaser,
                    _tokensInTier + _tokensInNextTier
                );

                emit TierSoldOut(tierIndex); // these events are 1-based
                emit TokenPurchase(
                    _purchaser,
                    msg.value,
                    _tokensInTier + _tokensInNextTier,
                    tokensInTier[tierIndex]
                );
            }
        } else if (_purchaseAmount == _tokensInTier) {
            // if '_purchaseAmount' exactly equals the number of $ENSHROUD tokens remaining in this tier
            // current tier now empty; increment tierIndex and deliver tokens
            delete tokensInTier[tierIndex];

            _incrementTier();

            // deliver $ENSHROUD tokens to '_purchaser'
            SafeTransferLib.safeTransfer(
                enshroudToken,
                _purchaser,
                _purchaseAmount
            );

            emit TierSoldOut(tierIndex);
            emit TokenPurchase(
                _purchaser,
                msg.value,
                _purchaseAmount,
                tokensInTier[tierIndex]
            );
        } else {
            // if '_purchaseAmount' does not empty tier
            tokensInTier[tierIndex] -= _purchaseAmount;

            // deliver $ENSHROUD tokens to '_purchaser'
            SafeTransferLib.safeTransfer(
                enshroudToken,
                _purchaser,
                _purchaseAmount
            );

            emit TokenPurchase(
                _purchaser,
                msg.value,
                _purchaseAmount,
                tokensInTier[tierIndex]
            );
        }
    }

    function _incrementTier() internal {
        unchecked {
            // increment tier, will not overflow
            ++tierIndex;

            // new tier rate is double the prior tier's rate; will not overflow until an economically impractical tier, nor underflow due to prior incrementation
            rates[tierIndex] = rates[tierIndex - 1] * 2;

			// minWei likewise doubles
			minWei *= 2;

            // fixed value of 1 million including decimals, will not overflow
            tokensInTier[tierIndex] = 1e6 * DECIMAL_PLACES;

            // mint 1 million $ENSHROUD tokens to this address for the next tier, fixed amount will not overflow
            IERC20_Crowdsale(enshroudToken).mint(
                address(this),
                1e6 * DECIMAL_PLACES
            );
        }
    }
}
