contract DragonSaga is ERC721Enumerable, Ownable {
    uint256 public constant MAXSUPPLY = 10000;
    uint256 public constant RESERVE = 100;
    uint256 public constant MAX_PER_PRESALE_TRANSACTION = 3;
    uint256 public constant MAX_PER_RESERVE_TRANSACTION = 5;
    uint256 public constant MAX_PER_PUBLIC_TRANSACTION  = 5;
    uint256 public constant MAX_PRESALE = 1500;
    uint256 private constant PRICE = 0.08 ether; 

    // launch dates - unix timestamp
    uint256 private _presaleDate = 0;
    uint256 private _reserveDate = 0;
    uint256 private _publicDate  = 0;

    // tokenURI for metadata
    string private _tokenURI = "";

    // presale whitelist logic 
    uint256 public presaleAllowedCount = 0;
    mapping(address => bool) presaleAddressAllowed;
    mapping(address => uint256) presaleAddressMinted;

    // reservation logic
    uint256 public reservationAllowedCount = 0;
    mapping(address => uint256) reservationAddressAllowed;
    mapping(address => uint256) reservationAddressMinted;

    // public sale logic
    mapping(address => uint256) publicAddressMinted;

    // Team wallet addresses for withdrawal.
    address public a1;
    address public a2;
    address public a3;
    address public a4;

    
    /**
     * Modifiers
     */

    // ensures that the max presale can't be over the limit
    modifier checkIfPresaleCapped(uint256 _amount) {
        require(presaleAllowedCount + _amount <= MAX_PRESALE, "Adding more will exceed presale total supply");
        _;
    }

    // checks if the presale is active.
    modifier checkIfPresaleSaleActive(){
        require(block.timestamp >= _presaleDate && _presaleDate > 0, "Presale Sale hasn't started yet!");
        require(block.timestamp < _reserveDate && _reserveDate > 0, "Presale has ended!");
        _;
    }

    // checks if the reserve sale is active.
    modifier checkIfReserveSaleActive() {
        require(block.timestamp >= _reserveDate && _reserveDate > 0, "Reserve sale hasn't started yet!");
        require(block.timestamp < _publicDate && _publicDate > 0, "Reserve sale has ended!");
        _;
    }

    // checks if the public sale is active.
    modifier checkIfPublicSaleActive(){
        require(block.timestamp >= _publicDate && _publicDate > 0, "Public sale hasn't started yet!");
        _;
    }


    constructor(string memory tokenURI) ERC721("DRAGONSAGA", "SAGA") {
        setBaseURI(tokenURI);
    }


    /**
     * sets the token uri for the metadata after mint
     *
     * @param baseURI metadata api url
     */
    function setBaseURI(string memory baseURI) public onlyOwner {
        _tokenURI = baseURI;
    }

    /**
     * Set withdrawal addresses. 
     */
    function setAddresses(address[] memory _a) public onlyOwner {
        a1 = _a[0];
        a2 = _a[1];
        a3 = _a[2];
        a4 = _a[3];
    }

    /**
     * Withdraw holdings and pay the hard working team.
     */
    function withdrawToTeam(uint256 amount) public payable onlyOwner {

        uint256 percent = amount / 100;
        
        // Give each member their share.
        require(payable(a1).send(percent * 25));
        require(payable(a2).send(percent * 25));
        require(payable(a3).send(percent * 25));
        require(payable(a4).send(percent * 25));
    }

    /**
     * Funny man
     */
    function emergencyWithdraw() public pure {
         require(false, "https://tenor.com/view/jake-jake-the-dog-funny-laugh-laughing-gif-5372688"); // LOL, just kidding.
    }

    /**
     * set the presale, public, or reserve launch date
     * 
     * @param timestamp unix formatted
     * @param group 0 for public launch
     */
    function setLaunchDate(uint256 timestamp, uint256 group) public onlyOwner {
        if (group == 0) {
            _presaleDate = timestamp;
        }
        if (group == 1){
            _reserveDate = timestamp;
        }
        if (group == 2){
            _publicDate = timestamp;
        }
    }

    /**
     * adds a bulk array of address to the presale list
     * 
     * @param _list group of addresses
     */
    function addListToPresale(address[] memory _list) public onlyOwner checkIfPresaleCapped(_list.length) {
        for (uint256 i; i < _list.length; i++) {
            presaleAddressAllowed[_list[i]] = true;
            presaleAllowedCount += 1;
        }
    }

    /**
     * adds a bulk array of address to the reservation list
     *
     * @param _list group of addresses
     */
    function addListToReservation(address[] memory _list, uint256[] memory _amount) public onlyOwner {
        for (uint256 i; i < _list.length; i++) {
            reservationAddressAllowed[_list[i]] = _amount[i];
            reservationAllowedCount += 1;
        }
    }

    /**
     * Minting functions.
     */

    /**
     * reserves the dragons for giveaways, community events, and team
     *
     * @param _amount the amount we're minting
     */
    function reserveDragons(uint256 _amount) public onlyOwner {
        uint256 amountOfTokens = balanceOf(msg.sender);
        uint256 totalSupply = totalSupply();

        require(_amount <= RESERVE, "Your amount exceeds the reserve supply!");
        require(_amount + amountOfTokens <= RESERVE, "Your amount plus your balance exceeds the reserve supply!");

        for (uint256 i; i < _amount; i++) {
            _safeMint(msg.sender, totalSupply + i);
        }
    }

    /**
     * this is the mint function for presale release
     *
     * @param _amount the amount we're minting
     */
    function mintPresale(uint256 _amount) public payable checkIfPresaleSaleActive {
        
        uint256 totalSupply = totalSupply();

        require(_amount <= MAX_PER_PRESALE_TRANSACTION, "Your amount exceeds presale transaction limit!");
        require(presaleAddressAllowed[msg.sender] == true, "You're not whitelisted for presale!");
        require(presaleAddressMinted[msg.sender] < 1, "You already purchased during presale!");
        require(presaleAddressMinted[msg.sender] + _amount <= MAX_PER_PRESALE_TRANSACTION, "You already purchased during presale!");
        require(PRICE * _amount <= msg.value, "Transaction value is too low");
        require(totalSupply + _amount + RESERVE <= MAXSUPPLY, "You can not mint more than the max supply" );

        for (uint256 i;i < _amount;i++) {
            _safeMint(msg.sender, totalSupply + i);
        }

        presaleAddressMinted[msg.sender] += _amount;
    }

    /**
     * Let the user mint from the amount reserved.
     *
     * @param _amount the amount we're minting
     */
    function mintReservation(uint256 _amount) public payable checkIfReserveSaleActive {
        
        uint256 totalSupply = totalSupply();
        uint256 reservedAmount = reservationAddressAllowed[msg.sender];

        require(reservationAddressMinted[msg.sender] < 1, "You already minted your reserved amount!");
        require(reservedAmount > 0, "You're address is not reserved!");
        require(_amount == reservedAmount, "You have to mint the number reserved to you.");
        require(PRICE * _amount <= msg.value, "Transaction value is too low");
        require(totalSupply + _amount + RESERVE <= MAXSUPPLY, "You can not mint more than the max supply" );

        for (uint256 i;i < _amount;i++) {
            _safeMint(msg.sender, totalSupply + i);
        }

        reservationAddressMinted[msg.sender] += _amount;
    }

    /**
     * Let the user mint from our public
     *
     * @param _amount the amount we're minting
     */
    function mintPublic(uint256 _amount) public payable checkIfPublicSaleActive {
        
        uint256 totalSupply = totalSupply();

        require(_amount > 0, "You have to mint at least 1.");
        require(_amount <= MAX_PER_PUBLIC_TRANSACTION, "You cannot mint more then 5 per request.");
        require(PRICE * _amount <= msg.value, "Transaction value is too low");
        require(totalSupply + _amount + RESERVE <= MAXSUPPLY, "You can not mint more than the max supply" );
        
        for (uint256 i;i < _amount;i++) {
            _safeMint(msg.sender, totalSupply + i);
        }

        publicAddressMinted[msg.sender] += _amount;
    }



    /**
     * Helper functions.
     */
     
    // checks if the address provided is part of the presale
    function checkPresaleAddress(address _address) public view returns (bool) {
        return presaleAddressAllowed[_address];
    }

    // Checks if the address provided is part of the reserved list
    function checkReservationAddress(address _address) public view returns (uint256) {
        return reservationAddressAllowed[_address];
    }

    // Gets the tokens owned by the address provided and returns it
    function getTokensFromAddress(address _address) public view returns (uint256[] memory) {
        uint256 amountOfTokens = balanceOf(_address);

        if (amountOfTokens == 0) {
            return new uint256[](0);
        }

        uint256[] memory tokens = new uint256[](amountOfTokens);

        for (uint256 i; i < amountOfTokens;i++) {
            tokens[i] = tokenOfOwnerByIndex(_address, i);
        }

        return tokens;
    }
    
    /**
     * Getter functions
     */
    function getPresaleDate() public view returns (uint256) { return _presaleDate; }
    function getReserveDate() public view returns (uint256) { return _reserveDate; }
    function getPublicDate()  public view returns (uint256) { return _publicDate;  }

    // Get the max per transaction
    function getMaxPerPresale() public pure returns (uint256) { return MAX_PER_PRESALE_TRANSACTION; }
    function getMaxPerReserve() public pure returns (uint256) { return MAX_PER_RESERVE_TRANSACTION; }
    function getMaxPerPublic()  public pure returns (uint256) { return MAX_PER_PUBLIC_TRANSACTION;  }

    // get the price for each token
    function getPrice() public pure returns (uint256) { return PRICE; }

    // returns the amount of addresses in the presale
    function getPresaleAddressCount() public view returns (uint256) { return presaleAllowedCount; }

    // returns the amount of address in the reservation
    function getReservationAddressCount() public view returns (uint256) { return reservationAllowedCount; }
}
/*
 ,     \    /      ,
 / \    )\__/(     / \
 /   \  (_\  /_)   /   \
 __________________/_____\__\@  @/___/_____\_________________
 |                          |\../|                          |
 |                           \VV/                           |
 |                                                          |
 |                       DRAGON SAGA                        |
 |                                                          |
 |                                                          |
 |__________________________________________________________|
 |    /\ /      \\       \ /\    |
 |  /   V        ))       V   \  |
 |/     `       //        '     \|
 `              V                '
 */
function setLaunchDate(uint256 timestamp, uint256 group) public onlyOwner {
    if (group == 0) {
        _presaleDate = timestamp;
    }
    if (group == 1){
        _reserveDate = timestamp;
    }
    if (group == 2){
        _publicDate = timestamp;
    }
}

/**
 * set the presale, public, or reserve launch date
 * 
 * @param timestamp unix formatted
 * @param group 0 for public launch
 */