contract DCBW721 is IERC2981, Ownable, ERC721, ERC721Enumerable, ERC721Pausable, ReentrancyGuard {
    /// @notice using safe math for uints
    using SafeMath for uint256;

    /// @notice using safe math for uints
    using SafeMath for uint32;

    /// @notice using safe math for uints
    using SafeMath for uint64;

    /// @notice using safe math for uints
    using SafeMath for uint96;

    /// @notice using Strings for uints conversion (tokenId)
    using Strings for uint256;

    /// @notice using Address for addresses extended functionality
    using Address for address;

    /// @notice sate of the draw process
    enum DrawState {
        Open,
        Closed
    }

    /// @notice struct used to store each draw's data
    struct DrawData {
        uint64 drawId;
        uint64 start_time;
        uint64 winningEdition;
        uint64 isGrandPrize;
        address winner;
        uint96 prizePoolWon;
        DrawState state;
        bytes32 randomNumberRequestId;
    }

    /// @notice EIP2981-compliant struct
    struct RoyaltyReceiver {
        address account;
        uint96 shares; // per thousands
    }

    /// @notice EIP721-required Base URI
    string private _baseTokenURI;

    /// @notice Base extension for metadata
    string private _baseExtension;

    /// @notice minting, draws, reserve(public), grandPrize(public) and royalties-traking parameters
    uint256 public _reserves;
    uint256 public _grandPrizePool;
    uint256 public _mintPrice;
    uint32 public _drawsToDate;

    uint256 private _reservesRate;
    uint256 private _maxTokenId;

    /// @notice the lottery draw state
    DrawState private _state;

    /// @notice beneficiary wallet that will collect mint fees
    address payable private _mintingBeneficiary;

    /// @notice VRF-compliant contract to request randomness from
    address private _rngContract;

    /// @notice Chainlink Keeper-compliant contract to trigger draw
    address private _alarmContract;

    /// @notice minter address
    address private _minter;

    /// @notice public metadata locked flag
    bool public locked;

    /// @notice Mapping draws id => details of the draw
    mapping(uint => DrawData) public draws;

    /// @notice Mapping token id => royalty details
    mapping(uint => RoyaltyReceiver) public royalties;

    /// @notice Minting events definition
    event AdminMinted(address indexed to, uint indexed tokenId);
    event Minted(address indexed to, uint indexed tokenId);

    /// @notice Draw events definition
    event DrawInitiated(bytes32 requestId);
    event DrawFinished(address winner, uint winningEdition);

    /// @notice metadata not locked modifier
    modifier notLocked() {
        require(!locked, "DCBW721: Metadata URIs are locked");
        _;
    }

    /// @notice allows only closed lottery state
    modifier requireState(DrawState state) {
        require(_state == state, "DCBW721: Invalid state for this action");
        _;
    }

    /// @notice allows only address to perform action
    modifier requireAccount(address account) {
        require(
            _msgSender() == account,
            "DCBW721: Invalid account for this action"
        );
        _;
    }

    /// @notice owner or minter
    modifier ownerOrMinter() {
        require(
            _msgSender() == _minter || _msgSender() == owner(),
            "DCBW721: Invalid account for this action"
        );
        _;
    }

    /**
     * @notice constructor
     * @param name the name of the EIP721 Contract
     * @param symbol the token symbol
     * @param baseTokenURI EIP721-required Base URI
     * @param mbeneficiary the contract Splitter that will receive minting funds
     * @param rbeneficiary the royalties beneficiary
     * @param alarmContract the alarm contract address to trigger DrawNumber on interval (TO BE SET POST-DEPLOYMENT)
     * @param rngContract the RNG contract address to get randomness (TO BE SET POST-DEPLOYMENT)
     **/
    constructor(
        string memory name,
        string memory symbol,
        string memory baseTokenURI,
        address mbeneficiary,
        address rbeneficiary,
        address alarmContract,
        address rngContract
    ) ERC721(name, symbol) Ownable() {
        locked = false;

        _mintPrice = 1 ether;
        _reservesRate = 200;
        _maxTokenId = 1000000;

        _baseExtension = ".json";
        _baseTokenURI = baseTokenURI;

        _mintingBeneficiary = payable(mbeneficiary);
        _rngContract = rngContract;
        _alarmContract = alarmContract;
        _minter = _msgSender();

        _state = DrawState.Closed;

        royalties[0] = RoyaltyReceiver(rbeneficiary, 100);
    }

    /// @notice feed eth into the contract
    function feed(uint256 intoReserves, uint256 intoGrandPrize)
        external
        payable
        onlyOwner
    {
        require(
            msg.value > 0 && intoReserves.add(intoGrandPrize) == msg.value,
            "DCBW721: Balance-Value Mismatch"
        );
        _updateGrandPrizePool(_grandPrizePool.add(intoGrandPrize));
        _updateReserves(_reserves.add(intoReserves));
    }

    /// @notice receive fallback should revert
    receive() external payable {
        revert("DCBW721: Please use Mint or Admin calls");
    }

    /// @notice default fallback should revert
    fallback() external payable {
        revert("DCBW721: Please use Mint or Admin calls");
    }

    /// @notice returns the base URI for the contract
    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        return
            string(abi.encodePacked(super.tokenURI(tokenId), _baseExtension));
    }

    /**
     * @notice updates the all 4 addresses invovled in the contract flow
     * @param mintingBeneficiary the contract Splitter that will receive minting funds
     * @param alarmContract the alarm contract address to trigger DrawNumber on interval
     * @param rngContract the RNG contract address to trigger drawnNumber and pick winner
     * @param minter minter address
     * @param _owner the new admin address
     **/
    function updateAddressesAndTransferOwnership(
        address mintingBeneficiary,
        address alarmContract,
        address rngContract,
        address minter,
        address _owner
    ) external onlyOwner {
        changeMintBeneficiary(mintingBeneficiary);
        changeMinter(minter);

        setAlarmAccount(alarmContract);
        setVRFAccount(rngContract);

        transferOwnership(_owner);
    }

    /**
     * @dev Called with the sale price to determine how much royalty is owed and to whom.
     * @param tokenId - the NFT asset queried for royalty information
     * @param salePrice - the sale price of the NFT asset specified by `tokenId`
     * @return receiver - address of who should be sent the royalty payment
     * @return royaltyAmount - the royalty payment amount for `salePrice`
     */
    function royaltyInfo(uint tokenId, uint salePrice)
        external
        view
        returns (address, uint) {
        /// @notice all tokens have same royalty receiver
        tokenId = 0; // silence unused var warning
        return (royalties[tokenId].account, royalties[tokenId].shares.mul(salePrice).div(1000));
    }

    /**
     * @notice a function for admins to mint cost-free
     * @param to the address to send the minted token to
     * @param _tokenId to token ID to be minted
     **/
    function adminMint(address to, uint256 _tokenId)
        external
        whenNotPaused
        ownerOrMinter
        requireState(DrawState.Closed)
    {
        require(to != address(0), "DCBW721: Address cannot be 0");

        validateTokenId(_tokenId);

        _safeMint(to, _tokenId);

        emit AdminMinted(to, _tokenId);
    }

    /**
     * @notice the public minting function -- requires 1 ether sent
     * @param to the address to send the minted token to
     * @param _tokenId to token ID to be minted
     **/
    function mint(address to, uint256 _tokenId)
        external
        payable
        whenNotPaused
        requireState(DrawState.Closed)
    {
        uint256 received = msg.value;

        require(to != address(0), "DCBW721: Address cannot be 0");
        require(
            received == _mintPrice,
            "DCBW721: Ether sent mismatch with mint price"
        );

        validateTokenId(_tokenId);

        _safeMint(to, _tokenId);

        _forwardFunds(received);
        _updateReserves(_reserves.add(received.mul(_reservesRate).div(1000)));

        emit Minted(to, _tokenId);
    }

    /// @notice a function called by Alarm on each _interval and requests VRF to get randomness
    function drawNumber()
        external
        whenNotPaused
        requireAccount(_alarmContract)
        requireState(DrawState.Closed)
        nonReentrant
        returns (bool)
    {
        require(_reserves > 0, "DCBW721: Not enough reserves for draw");
        require(totalSupply() >= 500, "DCBW721: Supply of 500 not reached yet");

        require(
            _reserves != 0 && address(this).balance.sub(_grandPrizePool) >= _reserves,
            "DCBW721: Not enough balance for draw prize"
        );

        bytes32 reqId = IRNG(_rngContract).request();

        _state = DrawState.Open;
        draws[_drawsToDate] = DrawData(
            _drawsToDate,
            uint32(block.timestamp), /* solium-disable-line */
            0,
            0,
            address(0),
            0,
            _state,
            reqId
        );

        emit DrawInitiated(reqId);

        return true;
    }

    /// @notice a function called by Owner to request GrandPrize random number
    function drawNumberGrandPrize()
        external
        payable
        whenNotPaused
        onlyOwner
        requireState(DrawState.Closed)
        nonReentrant
        returns (bool)
    {
        if(msg.value > 0){
            _updateGrandPrizePool(_grandPrizePool.add(msg.value));
        }

        require(
            _grandPrizePool != 0 && address(this).balance.sub(_reserves) >= _grandPrizePool,
            "DCBW721: Not enough balance for grand prize"
        );

        bytes32 reqId = IRNG(_rngContract).request();

        _state = DrawState.Open;
        draws[_drawsToDate] = DrawData(
            _drawsToDate,
            uint32(block.timestamp), /* solium-disable-line */
            0,
            1,
            address(0),
            0,
            _state,
            reqId
        );

        emit DrawInitiated(reqId);

        return true;
    }

    /**
     * @notice a function called by DCBW721RNG fulfillRandomness Callback function to pick winner if it exists
     * @param _requestId The Id initially returned by DCBW721RNG requestRandomness
     * @param _randomness Random number generated by Chainlink VRF
     **/
    function numberDrawn(bytes32 _requestId, uint256 _randomness)
        external
        whenNotPaused
        requireAccount(_rngContract)
        nonReentrant
    {
        DrawData storage current = draws[_drawsToDate];
        require(
            current.randomNumberRequestId == _requestId,
            "DCBW721: Request ID mismatch"
        );

        current.winningEdition = uint64(uint256(_randomness % _maxTokenId).add(1));

        if (_exists(current.winningEdition)) {
            current.winner = ownerOf(current.winningEdition);
            current.prizePoolWon = current.isGrandPrize == uint96(1)
                ? uint96(_grandPrizePool)
                : uint96(_reserves);
            _payout(current);
        }

        emit DrawFinished(current.winner, current.winningEdition);

        /// @notice update state
        _drawsToDate++;
        _state = DrawState.Closed;

        /// @notice update draw record
        current.state = _state;
    }

    /**
     * @notice prize payout function
     * @param current DrawData object having a draw data
     **/
    function _payout(DrawData memory current) private {
        require(current.winner != address(0), "DCBW721: Address cannot be 0");
        require(current.prizePoolWon > 0, "DCBW721: Prize pool is empty");

        /// @notice updating reserve pool & grand prize pool
        if (current.isGrandPrize == 1) {
            _updateGrandPrizePool(0);
        } else {
            _updateReserves(0);
        }

        /// @notice forward prize to winner wallet using CALL to avoid 2300 stipend limit
        (bool success, ) = payable(current.winner).call{value: current.prizePoolWon}("");
        require(success, "DCBW721: Failed to forward prize");
    }

    /// @notice pausing the contract minting and token transfer
    function pause() public virtual onlyOwner {
        _pause();
    }

    /// @notice unpausing the contract minting and token transfer
    function unpause() public virtual onlyOwner {
        _unpause();
    }

    /**
     * @notice sets the state of the draw to closed
     * @dev for if the numberDrawn reverted
     */
    function resetDrawState() public onlyOwner {
        _state = DrawState.Closed;
    }

    /**
     * @notice changes the minter address
     * @param minter the new minter address
     **/
    function changeMinter(address minter) public onlyOwner {
        require(
            minter != address(0),
            "DCBW721: Minter cannot be address 0"
        );
        require(
            minter != _minter,
            "DCBW721: Minter cannot be same as previous"
        );
        _minter = minter;
    }

    /**
     * @notice changes the minting beneficiary payable address
     * @param beneficiary the contract Splitter that will receive minting funds
     **/
    function changeMintBeneficiary(address beneficiary) public onlyOwner {
        require(
            beneficiary != address(0),
            "DCBW721: Minting beneficiary cannot be address 0"
        );
        require(
            beneficiary != _mintingBeneficiary,
            "DCBW721: beneficiary cannot be same as previous"
        );
        _mintingBeneficiary = payable(beneficiary);
    }

    /**
     * @notice changes the royalties beneficiary EIP2981
     * @param beneficiary the address that will receive royalties
     * @param shares the shares as royalties
     **/
    function changeRoyaltiesBeneficiary(address beneficiary, uint32 shares) public onlyOwner {
        require(
            beneficiary != address(0),
            "DCBW721: Royalties beneficiary cannot be address 0"
        );
        require(shares > 0, 'DCBW721: Royalty shares cannot be 0');

        RoyaltyReceiver storage rr = royalties[0];
        rr.account = beneficiary;
        rr.shares = shares;
    }

    /**
     * @notice changes the minting cost
     * @param mintCost new minting cost
     **/
    function changeMintCost(uint256 mintCost) public onlyOwner {
        require(
            mintCost != _mintPrice,
            "DCBW721: mint Cost cannot be same as previous"
        );
        _mintPrice = mintCost;
    }

    /**
     * @notice changes the minting cost
     * @param newBaseURI the new Base URI
     **/
    function changeBaseURI(string memory newBaseURI)
        public
        onlyOwner
        notLocked
    {
        require((keccak256(abi.encodePacked((_baseTokenURI))) != keccak256(abi.encodePacked((newBaseURI)))), "DCBW721: Base URI cannot be same as previous");
        _baseTokenURI = newBaseURI;
    }

    /**
     * @notice changes the reserve rate
     * @param reservesRate new reserve rate
     **/
    function changeReserveRate(uint256 reservesRate) public onlyOwner {
        require(
            _reservesRate != reservesRate,
            "DCBW721: reservesRate cannot be same as previous"
        );
        _reservesRate = reservesRate;
    }

    /**
     * @notice grants alarm role to supplied address
     * @param account the address to receive the ALARM role
     */
    function setAlarmAccount(address account) public virtual onlyOwner {
        require(account != address(0), "DCBW721: Alarm cannot be address 0");
        require(
            account != _alarmContract,
            "DCBW721: Alarm Contract cannot be same as previous"
        );
        _alarmContract = account;
    }

    /**
     * @notice grants vrf role to supplied address
     * @param account the address to receive the VRF role
     */
    function setVRFAccount(address account) public virtual onlyOwner {
        require(account != address(0), "DCBW721: VRF/RNG cannot be address 0");
        require(
            account != _rngContract,
            "DCBW721: RNG cannot be same as previous"
        );
        _rngContract = account;
    }

    /// @notice lock metadata forever
    function lockMetadata() public onlyOwner notLocked {
        locked = true;
    }

    /**
     * @notice the public function for checking if a token ID has been minted
     * @param _tokenId to token ID to be checked
     **/
    function isTokenMinted(uint256 _tokenId) public view returns (bool) {
        return validateTokenId(_tokenId) && _exists(_tokenId);
    }

    /**
     * @notice the public function for validation of specific token ID
     * @param _tokenId to token ID to be validated
     **/
    function validateTokenId(uint256 _tokenId) public view returns (bool) {
        require(_tokenId >= 1, "DCBW721: Invalid token ID");
        require(_tokenId <= _maxTokenId, "DCBW721: Invalid token ID");
        return true;
    }

    /**
     * @notice updates the store reserves tracker
     * @param reserves current reserves after new minting
     **/
    function _updateReserves(uint256 reserves) internal {
        require(
            reserves <= address(this).balance.sub(_grandPrizePool),
            "DCBW721: Reserve-Balance Mismatch"
        );
        _reserves = reserves;
    }

    /**
     * @notice updates the store grandPrizePool tracker
     * @param grandPrize current grandPrize pool after receiving royalties
     **/
    function _updateGrandPrizePool(uint256 grandPrize) internal {
        require(
            grandPrize <= address(this).balance.sub(_reserves),
            "DCBW721: GrandPrize-Balance Mismatch"
        );
        _grandPrizePool = grandPrize;
    }

    /**
     * @notice Determines how ETH is stored/forwarded on purchases.
     * @param received amount to forward
     **/
    function _forwardFunds(uint256 received) internal {
        /// @notice forward fund to receiver wallet using CALL to avoid 2300 stipend limit
        (bool success, ) = _mintingBeneficiary.call{value: received.mul(uint256(1000).sub(_reservesRate)).div(1000)}("");
        require(success, "DCBW721: Failed to forward funds");
    }

    /**
     * @notice before transfer hook function
     * @param from the address to send the token from
     * @param to the address to send the token to
     * @param tokenId to token ID to be sent
     **/
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(IERC165, ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
/**
 * @title DCBW721
 * @notice ERC721-ready VRF DCBW721 contract
 * @author cryptoware.eth | DCB.world
 **/
function numberDrawn(bytes32 _requestId, uint256 _randomness)
    external
    whenNotPaused
    requireAccount(_rngContract)
    nonReentrant
{
    DrawData storage current = draws[_drawsToDate];
    require(
        current.randomNumberRequestId == _requestId,
        "DCBW721: Request ID mismatch"
    );

    current.winningEdition = uint64(uint256(_randomness % _maxTokenId).add(1));

    if (_exists(current.winningEdition)) {
        current.winner = ownerOf(current.winningEdition);
        current.prizePoolWon = current.isGrandPrize == uint96(1)
            ? uint96(_grandPrizePool)
            : uint96(_reserves);
        _payout(current);
    }

    emit DrawFinished(current.winner, current.winningEdition);

    /// @notice update state
    _drawsToDate++;
    _state = DrawState.Closed;

    /// @notice update draw record
    current.state = _state;
}

/**
 * @notice a function called by DCBW721RNG fulfillRandomness Callback function to pick winner if it exists
 * @param _requestId The Id initially returned by DCBW721RNG requestRandomness
 * @param _randomness Random number generated by Chainlink VRF
 **/