// Contract: Vendor
// Target function: function getUserInfo( address target, uint256 amountPrivate, bytes32[] calldata merkleProofPrivate, uint256 amountPartner, bytes32[] calldata merkleProofPartner, uint256 amountMax, bytes32[] calldata merkleProofMax ) external view returns (uint256[USER_INFO_MAX] memory)
 return( _token_reserved ); }
    function tokenReservedAt( uint256 colorId ) external view returns (uint256) { return( _arr_token_reserved[colorId] ); }

    function totalMinted() external view returns (uint256) { return( _total_minted ); }
    function colorMintedAt( uint256 colorId ) external view returns (uint256) { return( _arr_color_minted[colorId] ); }
    function userMinted( address target ) external view returns (uint256) { return( _getUserMinted( target ) ); }

    //--------------------------------------------------------
    // [external/onlyOwnerOrManager] set
    //--------------------------------------------------------
    function setToken( address target ) external onlyOwnerOrManager { _token = IMintable(target); }
    function setTokenIdOfs( uint256 ofs ) external onlyOwnerOrManager { _token_id_ofs = ofs; }
    function setTokenMax( uint256 max ) external onlyOwnerOrManager { _token_max = max; }
    function setTokenMaxPerColor( uint256 max ) external onlyOwnerOrManager { _token_max_per_color = max; }
    function setTokenMaxPerUser( uint256 max ) external onlyOwnerOrManager { _token_max_per_user = max; }
    function setTokenMaxPerUserMerkleRoot( bytes32 root ) external onlyOwnerOrManager { _token_max_per_user_merkle_root = root; }
    function setTokenReserved( uint256[COLOR_NUM] calldata arrReserved ) external onlyOwnerOrManager {
        _token_reserved = 0;
        for( uint256 i=0; i<COLOR_NUM; i++ ){
            require( arrReserved[i] <= _token_max_per_color && arrReserved[i] >= _arr_color_minted[i], "invalid arrReserved" );
            _arr_token_reserved[i] = arrReserved[i];
            _token_reserved += _arr_token_reserved[i];
        }

        require( _token_reserved <= _token_max, "invalid arrReserved total" );
    }

    //***********************************************************
    // [public] getInfo - PRIVATE(whitelist)
    //***********************************************************
    function PRIVATE_getInfo( address target, uint256 amount, bytes32[] calldata merkleProof, uint256 amountMax, bytes32[] calldata merkleProofMax ) public view returns (uint256[INFO_MAX] memory) {
        uint256[INFO_MAX] memory arrRet;

        if( _PRIVATE_is_suspended ){ arrRet[INFO_SALE_SUSPENDED] = 1; }
        arrRet[INFO_SALE_START] = _PRIVATE_start;
        arrRet[INFO_SALE_END] = _PRIVATE_end;
        arrRet[INFO_SALE_PRICE] = _PRIVATE_price;
        if( _checkWhitelisted( _PRIVATE_merkle_root, target, amount, merkleProof ) ){
            arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED] = _checkMintMaxOfUser( target, amountMax, merkleProofMax );
            arrRet[INFO_SALE_USER_MINTABLE] = amount;
        }
        arrRet[INFO_SALE_USER_MINTED] = _PRIVATE_map_user_minted[target];
        arrRet[INFO_SALE_USER_LIMIT] = _checkUserLimit( target, arrRet[INFO_SALE_USER_MINTABLE], arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED] );

        return( arrRet );
    }

    //***********************************************************
    // [external/onlyOwnerOrManager] write - PRIVATE(whitelist)
    //***********************************************************
    function PRIVATE_suspend( bool flag ) external onlyOwnerOrManager { _PRIVATE_is_suspended = flag; }
    function PRIVATE_setStartEnd( uint256 start, uint256 end ) external onlyOwnerOrManager { _PRIVATE_start = start; _PRIVATE_end = end; }
    function PRIVATE_setPrice( uint256 price ) external onlyOwnerOrManager { _PRIVATE_price = price; }
    function PRIVATE_setMerkleRoot( bytes32 root ) external onlyOwnerOrManager { _PRIVATE_merkle_root = root; }

    //***********************************************************
    // [external/payable/nonReentrant] mint - PRIVATE(whitelist)
    //***********************************************************
    function PRIVATE_mint( uint256[] calldata colorIds, uint256[] calldata nums, uint256 amount, bytes32[] calldata merkleProof, uint256 amountMax, bytes32[] calldata merkleProofMax ) external payable nonReentrant {
        require( _total_minted >= _token_reserved, "PRIVATE SALE: reservation not finished" );

        uint256[INFO_MAX] memory arrInfo = PRIVATE_getInfo( msg.sender, amount, merkleProof, amountMax, merkleProofMax );
        require( arrInfo[INFO_SALE_SUSPENDED] == 0, "PRIVATE SALE: suspended" );
        require( arrInfo[INFO_SALE_START] == 0 || arrInfo[INFO_SALE_START] <= (block.timestamp+BLOCK_SEC_MARGIN), "PRIVATE SALE: not opend" );
        require( arrInfo[INFO_SALE_END] == 0 || (arrInfo[INFO_SALE_END]+BLOCK_SEC_MARGIN) > block.timestamp, "PRIVATE SALE: finished" );
        require( arrInfo[INFO_SALE_USER_MAX_IF_WHITELISTED] > 0, "PRIVATE SALE: not whitelisted" );

        require( colorIds.length == nums.length, "PRIVATE SALE: invalid array sizes" );
        uint256 num;
        for( uint256 i=0; i<nums.length; i++ ){ num += nums[i]; }
        require( arrInfo[INFO_SALE_USER_MINTABLE] >= (arrInfo[INFO_SALE_USER_MINTED]+num), "PRIVATE SALE: reached the sale limit" );
        require( arrInfo[INFO_SALE_USER_LIMIT] >= num, "PRIVATE SALE: reached the user limit" );

        _checkPayment( msg.sender, arrInfo[INFO_SALE_PRICE]*num, msg.value );

        _PRIVATE_map_user_minted[msg.sender] += num;
        for( uint256 i=0; i<colorIds.length; i++ ){
            _mintTokens( msg.sender, colorIds[i], nums[i] );
        }
    }

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // [public] getInfo - PARTNER
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    function PARTNER_getInfo( address target, uint256 amount, bytes32[] calldata merkleProof, uint256 amountMax, bytes32[] calldata merkleProofMax ) public view returns (uint256[INFO_MAX] memory) {
        uint256[INFO_MAX] memory arrRet;

        if( _PARTNER_is_suspended ){ arrRet[INFO_SALE_SUSPENDED] = 1; }
        arrRet[INFO_SALE_START] = _PARTNER_start;
        arrRet[INFO_SALE_END] = _PARTNER_end;
        arrRet[INFO_SALE_PRICE] = _PARTNER_price;
        if( _checkWhitelisted( _PARTNER_merkle_root, target, amount, merkleProof ) ){
            arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED] = _checkMintMaxOfUser( target, amountMax, merkleProofMax );
            arrRet[INFO_SALE_USER_MINTABLE] = amount;
        }
        arrRet[INFO_SALE_USER_MINTED] = _PARTNER_map_user_minted[target];
        arrRet[INFO_SALE_USER_LIMIT] = _checkUserLimit( target, arrRet[INFO_SALE_USER_MINTABLE], arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED] );

        return( arrRet );
    }

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // [external/onlyOwnerOrManager] write - PARTNER
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    function PARTNER_suspend( bool flag ) external onlyOwnerOrManager { _PARTNER_is_suspended = flag; }
    function PARTNER_setStartEnd( uint256 start, uint256 end ) external onlyOwnerOrManager { _PARTNER_start = start; _PARTNER_end = end; }
    function PARTNER_setPrice( uint256 price ) external onlyOwnerOrManager { _PARTNER_price = price; }
    function PARTNER_setMerkleRoot( bytes32 root ) external onlyOwnerOrManager { _PARTNER_merkle_root = root; }

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // [external/payable/nonReentrant] mint - PARTNER(whitelist)
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    function PARTNER_mint( uint256[] calldata colorIds, uint256[] calldata nums, uint256 amount, bytes32[] calldata merkleProof, uint256 amountMax, bytes32[] calldata merkleProofMax ) external payable nonReentrant {
        require( _total_minted >= _token_reserved, "PARTNER SALE: reservation not finished" );

        uint256[INFO_MAX] memory arrInfo = PARTNER_getInfo( msg.sender, amount, merkleProof, amountMax, merkleProofMax );
        require( arrInfo[INFO_SALE_SUSPENDED] == 0, "PARTNER SALE: suspended" );
        require( arrInfo[INFO_SALE_START] == 0 || arrInfo[INFO_SALE_START] <= (block.timestamp+BLOCK_SEC_MARGIN), "PARTNER SALE: not opend" );
        require( arrInfo[INFO_SALE_END] == 0 || (arrInfo[INFO_SALE_END]+BLOCK_SEC_MARGIN) > block.timestamp, "PARTNER SALE: finished" );
        require( arrInfo[INFO_SALE_USER_MAX_IF_WHITELISTED] > 0, "PARTNER SALE: not whitelisted" );

        require( colorIds.length == nums.length, "PARTNER SALE: invalid array sizes" );
        uint256 num;
        for( uint256 i=0; i<nums.length; i++ ){ num += nums[i]; }
        require( arrInfo[INFO_SALE_USER_MINTABLE] >= (arrInfo[INFO_SALE_USER_MINTED]+num), "PARTNER SALE: reached the sale limit" );
        require( arrInfo[INFO_SALE_USER_LIMIT] >= num, "PARTNER SALE: reached the user limit" );

        _checkPayment( msg.sender, arrInfo[INFO_SALE_PRICE]*num, msg.value );

        _PARTNER_map_user_minted[msg.sender] += num;
        for( uint256 i=0; i<colorIds.length; i++ ){
            _mintTokens( msg.sender, colorIds[i], nums[i] );
        }
    }

    //===========================================================
    // [public] getInfo - PUBLIC
    //===========================================================
    function PUBLIC_getInfo( address target, uint256 amountMax, bytes32[] calldata merkleProofMax ) public view returns (uint256[INFO_MAX] memory) {
        uint256[INFO_MAX] memory arrRet;

        if( _PUBLIC_is_suspended ){ arrRet[INFO_SALE_SUSPENDED] = 1; }
        arrRet[INFO_SALE_START] = _PUBLIC_start;
        arrRet[INFO_SALE_END] = _PUBLIC_end;
        arrRet[INFO_SALE_PRICE] = _PUBLIC_price;
        arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED] = _checkMintMaxOfUser( target, amountMax, merkleProofMax );
        arrRet[INFO_SALE_USER_MINTABLE] = arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED];
        arrRet[INFO_SALE_USER_MINTED] = _PUBLIC_map_user_minted[target];
        arrRet[INFO_SALE_USER_LIMIT] = _checkUserLimit( target, arrRet[INFO_SALE_USER_MINTABLE], arrRet[INFO_SALE_USER_MAX_IF_WHITELISTED] );

        return( arrRet );
    }

    //===========================================================
    // [external/onlyOwnerOrManager] write - PUBLIC
    //===========================================================
    function PUBLIC_suspend( bool flag ) external onlyOwnerOrManager { _PUBLIC_is_suspended = flag; }
    function PUBLIC_setStartEnd( uint256 start, uint256 end ) external onlyOwnerOrManager { _PUBLIC_start = start; _PUBLIC_end = end; }
    function PUBLIC_setPrice( uint256 price ) external onlyOwnerOrManager { _PUBLIC_price = price; }

    //===========================================================
    // [external/payable/nonReentrant] mint - PUBLIC
    //===========================================================
    function PUBLIC_mint( uint256[] calldata colorIds, uint256[] calldata nums, uint256 amountMax, bytes32[] calldata merkleProofMax ) external payable nonReentrant {
        require( _total_minted >= _token_reserved, "PUBLIC SALE: reservation not finished" );

        uint256[INFO_MAX] memory arrInfo = PUBLIC_getInfo( msg.sender, amountMax, merkleProofMax );
        require( arrInfo[INFO_SALE_SUSPENDED] == 0, "PUBLIC SALE: suspended" );
        require( arrInfo[INFO_SALE_START] == 0 || arrInfo[INFO_SALE_START] <= (block.timestamp+BLOCK_SEC_MARGIN), "PUBLIC SALE: not opend" );
        require( arrInfo[INFO_SALE_END] == 0 || (arrInfo[INFO_SALE_END]+BLOCK_SEC_MARGIN) > block.timestamp, "PUBLIC SALE: finished" );

        require( colorIds.length == nums.length, "PUBLIC SALE: invalid array sizes" );
        uint256 num;
        for( uint256 i=0; i<nums.length; i++ ){ num += nums[i]; }
        require( arrInfo[INFO_SALE_USER_MINTABLE] >= (arrInfo[INFO_SALE_USER_MINTED]+num), "PUBLIC SALE: reached the sale limit" );
        require( arrInfo[INFO_SALE_USER_LIMIT] >= num, "PUBLIC SALE: reached the user limit" );

        _checkPayment( msg.sender, arrInfo[INFO_SALE_PRICE]*num, msg.value );

        _PUBLIC_map_user_minted[msg.sender] += num;
        for( uint256 i=0; i<colorIds.length; i++ ){
            _mintTokens( msg.sender, colorIds[i], nums[i] );
        }
    }

    //--------------------------------------------------------
    // [internal] _getUserMinted
    //--------------------------------------------------------
    function _getUserMinted( address target ) internal view returns (uint256) {
        uint256 total;
        total += _PRIVATE_map_user_minted[target];
        total += _PARTNER_map_user_minted[target];
        total += _PUBLIC_map_user_minted[target];
        return( total );
    }

    //--------------------------------------------------------
    // [internal] _checkUserLimit
    //--------------------------------------------------------
    function _checkUserLimit( address target, uint256 num, uint256 userMax ) internal view returns (uint256) {
        uint256 total = _getUserMinted( target );
        if( total >= userMax ){
            return( 0 );
        }

        uint256 rest = userMax - total;
        if( num > rest ){
            return( rest );
        }
        return( num );
    }    

    //--------------------------------------------------------
    // [internal] _checkWhitelisted
    //--------------------------------------------------------
    function _checkWhitelisted( bytes32 merkleRoot, address target, uint256 amount, bytes32[] calldata merkleProof ) internal pure returns (bool) {
        bytes32 node = keccak256( abi.encodePacked( target, amount ) );
        if( MerkleProof.verify( merkleProof, merkleRoot, node ) ){
            return( true );
        }
        return( false );
    }

    //--------------------------------------------------------
    // [internal] _checkMintMaxOfUser
    //--------------------------------------------------------
    function _checkMintMaxOfUser( address target, uint256 amountMax, bytes32[] calldata merkleProofMax ) internal view returns (uint256) {
        bytes32 node = keccak256( abi.encodePacked( target, amountMax ) );
        if( MerkleProof.verify( merkleProofMax, _token_max_per_user_merkle_root, node ) ){
            return( amountMax );
        }
        return( _token_max_per_user );
    }

    //--------------------------------------------------------
    // [internal] _checkPayment
    //--------------------------------------------------------
    function _checkPayment( address msgSender, uint256 price, uint256 payment ) internal {
        require( price <= payment, "insufficient value" );

        // refund if overpaymented
        if( price < payment ){
            uint256 change = payment - price;
            address payable target = payable( msgSender );
            Address.sendValue( target, change );
        }
    }

    //--------------------------------------------------------
    // [internal] _mintTokens
    //--------------------------------------------------------
    function _mintTokens( address to, uint256 colorId, uint256 num ) internal {
        require( colorId < COLOR_NUM, "_mintTokens: invalid colorId" );
        require( _token_max >= (_total_minted+num), "_mintTokens: reached the supply range" );
        require( _token_max_per_color >= (_arr_color_minted[colorId]+num), "_mintTokens: reached the color range" );

        uint256 tokenId = _token_id_ofs + colorId*_token_max_per_color + _arr_color_minted[colorId];

        _total_minted += num;
        _arr_color_minted[colorId] += num;
        for( uint256 i=0; i<num; i++ ){
            _token.mintByMinter( to, tokenId+i );
        }
    }

    //--------------------------------------------------------
    // [external/onlyOwnerOrManager] reserveTokens
    //--------------------------------------------------------
    function reserveTokens( uint256 num ) external onlyOwnerOrManager {
        require( _token_reserved >= (_total_minted+num), "reserveTokens: exceeded the reservation range" );

        uint256 colorId = 0;
        while( num > 0 && colorId < COLOR_NUM ){
            if( _arr_token_reserved[colorId] > _arr_color_minted[colorId] ){
                uint256 colorNum = _arr_token_reserved[colorId] - _arr_color_minted[colorId];
                if( num < colorNum ){
                    colorNum = num;
                }

                _mintTokens( owner(), colorId, colorNum );
                num -= colorNum;
            }
            colorId++;
        }
    }