// Contract: Token
// Target function: function saleInfo() external view returns (uint256[12] memory)
to`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {}
}


//import "./IPresaleInfo.sol";
//-----------------------------------------------------------------------
// 事前販売情報：インターフェイス
//-----------------------------------------------------------------------
interface IPresaleInfo {
    //----------------------------------------
    // 登録確認されているか？
    //----------------------------------------
    function isRegistered( address user ) external view returns (bool);
}


//import "./LibString.sol";
library LibString {
    function toString(uint256 value) internal pure returns (string memory) {

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

}


//-------------------
// トークン
//-------------------
contract Token is Ownable, ERC721{
    //----------------------------------------------
    // Events
    //----------------------------------------------
    event TokenStake( address indexed holder, uint256 indexed tokenId );
    event TokenUnstake( address indexed holder, uint256 indexed tokenId );

    //----------------------------------------------
    // 定数
    //----------------------------------------------
    string constant private TOKEN_NAME = "Comical Girl";
    string constant private TOKEN_SYMBOL = "CG";

    uint256 constant private PUBLIC_SALE_MAX_SHOT = 7;
    uint256 constant private PRE_SALE_MAX_SHOT = 2;
    uint256 constant private PRE_SALE_LIMIT = 2;
 
    uint256 constant private ID_PHASE_GA  = 0;
    uint256 constant private ID_PHASE_1ST = 1;
    uint256 constant private ID_PHASE_2ND = 2;
    uint256 constant private ID_PHASE_3RD = 3;
    uint256 constant private ID_PHASE_MAX = 4;

    //----------------------------------------------
    // 管理データ
    //----------------------------------------------
    // 売り上げを引き出せる人
    address private _accountant;

    // 排出管理
    uint256[ID_PHASE_MAX] private _arr_id_offset;
    uint256[ID_PHASE_MAX] private _arr_num_mintable;
    uint256[ID_PHASE_MAX] private _arr_num_minted;

    // metadata参照先
    string[ID_PHASE_MAX] private _arr_base_url;
    mapping( uint256 => string ) private _repaired_urls;

    // 販売関連
    uint256 private _sale_phase;

    uint256 private _sale_price;
    uint256 private _sale_start;
    uint256 private _sale_end;
    bool private _sale_suspended;

    uint256 private _presale_price;
    uint256 private _presale_start;
    uint256 private _presale_end;
    IPresaleInfo[] private _presale_infos;
    mapping( address => uint256 ) private _address_to_presale_minted_1st;
    mapping( address => uint256 ) private _address_to_presale_minted_2nd;
    mapping( address => uint256 ) private _address_to_presale_minted_3rd;
    bool private _presale_suspended;

    // ステーク関連
    bool private _stake_suspended;
    bool private _unstake_suspended;
    mapping( uint256 => address ) private _token_to_stake_holder;

    //--------------------------------------------
    // コンストラクタ
    //--------------------------------------------
    constructor() Ownable() ERC721( TOKEN_NAME, TOKEN_SYMBOL ){
        // お金を引き出せる人
        _accountant = 0xda1B1849EeF51f1F5EfF49dA1220ebb89a5ef2FF;   // 本番

        // id_offset
        _arr_id_offset[ID_PHASE_GA] = 1;
        _arr_id_offset[ID_PHASE_1ST] = 113;
        _arr_id_offset[ID_PHASE_2ND] = 1668;
        _arr_id_offset[ID_PHASE_3RD] = 4223;

        // num_mintable
        _arr_num_mintable[ID_PHASE_GA] = 112;
        _arr_num_mintable[ID_PHASE_1ST] = 1555;
        _arr_num_mintable[ID_PHASE_2ND] = 2555;
        _arr_num_mintable[ID_PHASE_3RD] = 3555;

        // ステーク関連（リリース時はオフにしておく）
        _stake_suspended = true;
        _unstake_suspended = true;
    }

    //---------------------------------------------------
    // [public] 一般販売が利用可能か？
    //---------------------------------------------------
    function isSaleAvailable() public view returns (bool) {
        // フェーズが無効
        if( _sale_phase < ID_PHASE_1ST || _sale_phase > ID_PHASE_3RD ){
            return( false );
        }

        // 一時停止中
        if( _sale_suspended ){
            return( false );
        }

        // 開始前
        if( _sale_start > block.timestamp ){
            return( false );
        }

        // 終了：_sale_endが０の場合は無期限
        if( _sale_end != 0 && _sale_end <= block.timestamp ){
            return( false );
        }

        // 利用可能
        return( true );
    }

    //--------------------------------------------
    // [external] 一般販売
    //--------------------------------------------    
    function mintTokens( uint256 num ) external payable {
        // 購入可能か？
        require( isSaleAvailable(), "sale: not available" );

        // 試行回数は有効か？
        require( num > 0 && num <= PUBLIC_SALE_MAX_SHOT, "sale: invalid num" );

        // 残りはあるか？
        require( _arr_num_mintable[_sale_phase] >= (_arr_num_minted[_sale_phase]+num), "sale: remaining not enough" );

        // 入金額は有効か？
        uint256 amount = _sale_price * num;
        require( amount <= msg.value , "sale: insufficient value" );
        
        //--------------------------
        // ここまできたらチェック完了
        //--------------------------

        for( uint256 i=0; i<num; i++ ){
            _safeMint( msg.sender, _arr_num_minted[_sale_phase] + _arr_id_offset[_sale_phase] );
            _arr_num_minted[_sale_phase]++;
        }
    }

    //---------------------------------------------------
    // [public] 事前販売が利用可能か？
    //---------------------------------------------------
    function isPresaleAvailable() public view returns (bool) {
        // フェーズが無効
        if( _sale_phase < ID_PHASE_1ST || _sale_phase > ID_PHASE_3RD ){
            return( false );
        }

        // 一時停止中
        if( _presale_suspended ){
            return( false );
        }

        // 開始前
        if( _presale_start > block.timestamp ){
            return( false );
        }

        // 終了：_presale_endが０の場合は無期限
        if( _presale_end != 0 && _presale_end <= block.timestamp ){
            return( false );
        }

        // 利用可能
        return( true );
    }

    //---------------------------------------------------
    // [public] 事前登録済みのユーザーか？
    //---------------------------------------------------
    function isRegisteredUser( address user ) public view returns (bool) {
        for( uint256 i=0; i<_presale_infos.length; i++ ){
            if( _presale_infos[i].isRegistered( user ) ){
                return( true );
            }
        }

        return( false );
    }

    //--------------------------------------------
    // [external] 事前販売
    //--------------------------------------------    
    function mintTokensForPresale( uint256 num ) external payable {
        // 購入可能か？
        require( isPresaleAvailable(), "pre-sale: not available" );

        // 事前登録に登録しているか？
        require( isRegisteredUser(msg.sender), "pre-sale: not registered" );

        // 試行回数は有効か？
        require( num > 0 && num <= PRE_SALE_MAX_SHOT, "pre-sale: invalid num" );

        // 発行上限未満か？
        uint256 minted = PRE_SALE_LIMIT;
        if( _sale_phase == ID_PHASE_1ST){ minted = _address_to_presale_minted_1st[msg.sender]; }
        else if( _sale_phase == ID_PHASE_2ND){ minted = _address_to_presale_minted_2nd[msg.sender]; }
        else if( _sale_phase == ID_PHASE_3RD){ minted = _address_to_presale_minted_3rd[msg.sender]; }

        require( PRE_SALE_LIMIT >= (minted+num), "pre-sale: limitation" );

        // 残りはあるか？
        require( _arr_num_mintable[_sale_phase] >= (_arr_num_minted[_sale_phase]+num), "pre-sale: remaining not enough" );

        // 入金額は有効か？
        uint256 amount = _presale_price * num;
        require( amount <= msg.value , "pre-sale: insufficient value" );
        
        //--------------------------
        // ここまできたらチェック完了
        //--------------------------

        for( uint256 i=0; i<num; i++ ){
            _safeMint( msg.sender, _arr_num_minted[_sale_phase] + _arr_id_offset[_sale_phase] );
            _arr_num_minted[_sale_phase]++;
        }

        if( _sale_phase == ID_PHASE_1ST){ _address_to_presale_minted_1st[msg.sender] += num; }
        else if( _sale_phase == ID_PHASE_2ND){ _address_to_presale_minted_2nd[msg.sender] += num; }
        else if( _sale_phase == ID_PHASE_3RD){ _address_to_presale_minted_3rd[msg.sender] += num; }
    }

    //--------------------------------------------
    // [external/onlyOwner] トークンのgiveaway
    //--------------------------------------------    
    function giveawayTokens( address[] calldata users ) external onlyOwner {
        // 発行上限未満か？
        require( users.length > 0 && _arr_num_mintable[ID_PHASE_GA] >= (_arr_num_minted[ID_PHASE_GA]+users.length), "giveaway: invalid length" );

        // 用心
        for( uint256 i=0; i<users.length; i++ ){
            require( users[i] != address(0), "giveaway: invalid user" );
        }

        //--------------------------
        // ここまできたらチェック完了
        //--------------------------

        for( uint256 i=0; i<users.length; i++ ){
            _safeMint( users[i], _arr_num_minted[ID_PHASE_GA] + _arr_id_offset[ID_PHASE_GA] );
            _arr_num_minted[ID_PHASE_GA]++;
        }
    }

    //--------------------------------------------------------
    // [external] 引き出せるアカウントの確認
    //--------------------------------------------------------
    function accountant() external view returns (address) {
        return( _accountant );
    }

    //--------------------------------------------------------
    // [external/onlyOwner] 引き出せるアカウントの設定
    //--------------------------------------------------------
    function setaAccountant( address target ) external onlyOwner {
        _accountant = target;
    }

    //--------------------------------------------------------
    // [external] 各種確認
    //--------------------------------------------------------
    function idOffsetAt( uint256 id ) external view returns (uint256) { return( _arr_id_offset[id] ); }
    function numMintableAt( uint256 id ) external view returns (uint256) { return( _arr_num_mintable[id] ); }
    function numMintedAt( uint256 id ) external view returns (uint256) { return( _arr_num_minted[id] ); }

    //--------------------------------------------------------
    // [external] 発行可能数の確認
    //--------------------------------------------------------
    function totalMintable() external view returns (uint256) {
        uint256 mintable = 0;
        for( uint256 i=0; i<ID_PHASE_MAX; i++ ){
            mintable += _arr_num_mintable[i];
        }
        return( mintable );
    }

    //--------------------------------------------------------
    // [external] 発行数の確認
    //--------------------------------------------------------
    function totalMinted() external view returns (uint256) {
        uint256 minted = 0;
        for( uint256 i=0; i<ID_PHASE_MAX; i++ ){
            minted += _arr_num_minted[i];
        }
        return( minted );
    }

    //--------------------------------------------------------
    // [external] _base_url の確認
    //--------------------------------------------------------
    function baseUrlAt( uint256 at ) external view returns (string memory) {
        return( _arr_base_url[at] );
    }

    //--------------------------------------------------------
    // [external/onlyOwner] _base_url の設定
    //--------------------------------------------------------
    function setBaseUrlAt( uint256 at, string calldata url ) external onlyOwner {
        _arr_base_url[at] = url;
    }

    //---------------------------------------------------
    // [public] 修復されたURLの確認
    //---------------------------------------------------
    function repairedUrl( uint256 tokenId ) public view returns (string memory) {
        require( _exists( tokenId ), "nonexistent token" );

        return( _repaired_urls[tokenId] );
    }

    //---------------------------------------------------
    // [external/onlyOwner] メタデータのURLの修復
    //---------------------------------------------------
    function repairUrl( uint256 tokenId, string calldata url ) external onlyOwner {
        require( _exists( tokenId ), "nonexistent token" );

        _repaired_urls[tokenId] = url;
    }

    //----------------------------------------------
    // [public] tokenURI
    //----------------------------------------------
    function tokenURI( uint256 tokenId ) public view override returns (string memory) {
        require( _exists( tokenId ), "nonexistent token" );

        // 修復データがあれば
        string memory url = repairedUrl( tokenId );
        if( bytes(url).length > 0 ){
            return( url );
        }

        uint256 at = ID_PHASE_MAX;
        for( uint256 i=0; i<ID_PHASE_MAX; i++ ){
            if( tokenId >= _arr_id_offset[i] && tokenId < (_arr_id_offset[i]+_arr_num_mintable[i]) ){
                at = i;
                break;
            }
        }

        require( at < ID_PHASE_MAX, "invalid phase" );

        // ここまできたらリリース時のメタを返す
        string memory strId = LibString.toString( tokenId );
        return( string( abi.encodePacked( _arr_base_url[at], strId ) ) );
    }

    //---------------------------------------------------
    // [external/onlyOwner] 販売設定
    //---------------------------------------------------
    function setSale( uint256 phase, uint256 saleP, uint256 saleF, uint256 saleT, uint256 presaleP, uint256 presaleF, uint256 presaleT, address[] calldata arrInfo ) external onlyOwner {
        _sale_phase = phase;

        _sale_price = saleP;
        _sale_start = saleF;
        _sale_end = saleT;

        _presale_price = presaleP;
        _presale_start = presaleF;
        _presale_end = presaleT;

        delete _presale_infos;
        for( uint256 i=0; i<arrInfo.length; i++ ){
            _presale_infos.push( IPresaleInfo( arrInfo[i] ) );
        }
    }

    //---------------------------------------------------
    // [external] 設定確認
    //---------------------------------------------------
    function salePhase() external view returns (uint256) { return(_sale_phase); }
    function salePrice() external view returns (uint256) { return(_sale_price); }
    function saleStart() external view returns (uint256) { return( _sale_start ); }
    function saleEnd() external view returns (uint256) { return( _sale_end ); }
    function presalePrice() external view returns (uint256) { return(_presale_price); }
    function presaleStart() external view returns (uint256) { return( _presale_start ); }
    function presaleEnd() external view returns (uint256) { return( _presale_end ); }
    function presaleInfoAt( uint256 at ) external view returns (address) {
        if( at < _presale_infos.length ){
            return( address(_presale_infos[at]) );
        }
        return( address(0) );
    }
    function presaleMintedAt( uint256 at, address user ) external view returns (uint256) {
        if( at == ID_PHASE_1ST){ return( _address_to_presale_minted_1st[user] ); }
        if( at == ID_PHASE_2ND){ return( _address_to_presale_minted_2nd[user] ); }
        if( at == ID_PHASE_3RD){ return( _address_to_presale_minted_3rd[user] ); }
        return( 0 );
    }