contract UltramanNFT is ERC721("Ultramannft.art","ULTRAMANNFT"), Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using EnumerableSet for EnumerableSet.AddressSet;

    uint256 internal constant WEI_PRECISION = 10**18;
    address public USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    uint256 internal constant USDT_PRECISION = 10**6;


    // reserve for #1
    uint256 public reserveAdvancedCardIdStart = 1;
    uint256 public reserveAdvancedCardIdEnd = 100;
    uint256 public nextReserveAdvancedCardId = reserveAdvancedCardIdStart;

    // Advanced card tokenId range
    uint256 public advancedCardIdStart = 101;
    uint256 public advancedCardIdEnd = 260;
    uint256 public nextUltramanAdvancedCardId = advancedCardIdStart;
    uint256 public nextMonsterAdvancedCardId = advancedCardIdStart+1;


    // Intermediate card tokenId range
    uint256 public intermediateCardIdStart = 261;
    uint256 public intermediateCardIdEnd= 1160;
    uint256 public nextUltramanIntermediateCardId= intermediateCardIdStart;
    uint256 public nextMonsterIntermediateCardId= intermediateCardIdStart+1;

    // Elementary card tokenId range
    uint256 public elementaryCardIdStart = 1161;
    uint256 public elementaryCardIdEnd = 3560;
    uint256 public nextElementaryCardId = elementaryCardIdStart;

    //  reserve #2
    uint256 public reserveElementaryCardIdStart = 3561;
    uint256 public reserveElementaryCardIdEnd = 8848;
    uint256 public nextReserveElementaryCardId = reserveElementaryCardIdStart;

    EnumerableSet.AddressSet private reserveOfCallers;

    enum CardLevel {
        Padding,
        Elementary,
        Intermediate,
        Advanced
    }

    enum CardType {
        Padding,
        Ultraman,
        Monster
    }

    struct Property {
        // property
        CardType cardType;
        CardLevel level;
        uint256 objectId;
        uint256 forceValue;
        uint256 defenderValue;

        // mine data
        uint256 lastUpdateTime;
        uint256 withdrawValue;
    }


    mapping(uint256 => Property) public cardProperties;
    mapping(CardType => mapping(CardLevel => uint256)) public objects;

    bool public hasSaleStarted = false;
    bool public hasComposeToIntermediateStarted = false;
    bool public hasComposeToAdvancedStarted = false;

    string public METADATA_PROVENANCE_HASH = "";

    address public dev;
    address public lpToken;

    uint256 public composeToIntermediateCardNeedValue = 300 * USDT_PRECISION; // 300 usdt
    uint256 public composeToAdvancedCardNeedValue = 600 * USDT_PRECISION; // 600 usdt

    // mine information
    uint256 public mineStartTime = block.timestamp + 365 days;
    uint256 public mineEndTime = 0;
    uint256 public mineFactor = 10**18;
    IERC20 public untToken;


    constructor(address _untToken,address _dev,string memory _baseURI) public {
        untToken = IERC20(_untToken);
        setBaseURI(_baseURI);
        _initObjects();
        dev = _dev;
    }

    function _initObjects() internal {
        objects[CardType.Ultraman][CardLevel.Advanced] = 2;
        objects[CardType.Monster][CardLevel.Advanced] = 2;

        objects[CardType.Ultraman][CardLevel.Intermediate] = 3;
        objects[CardType.Monster][CardLevel.Intermediate] = 3;

        objects[CardType.Ultraman][CardLevel.Elementary] = 4;
        objects[CardType.Monster][CardLevel.Elementary] = 4;
    }

    function addReserveCaller(address _caller) external onlyOwner {
        require(_caller != address(0), "Not support zero address");
        if(!reserveOfCallers.contains(_caller)) {
            reserveOfCallers.add(_caller);
        }
    }

    function removeReserveCaller(address _caller) external onlyOwner {
        if(reserveOfCallers.contains(_caller)) {
            reserveOfCallers.remove(_caller);
        }
    }



    function setDev(address _dev) public onlyOwner{
        require(_dev != address(0), "Not supported zero address");
        dev = _dev;
    }

    function setLpToken(address _lp) public onlyOwner {
        require(_lp != address(0), "Not supported zero address");
        lpToken = _lp;
    }

    /* Fee collection for any other token */
    function seize(IERC20 token, uint256 amount) external onlyOwner{
        token.safeTransfer(owner(), amount);
    }

    function tokensOfOwner(address _owner) external view returns(uint256[] memory ) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function tokensOfOwnerWithCount(address _owner,uint256 start, uint256 count) external view returns(uint256[] memory ) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256 end = start + count;
            if(tokenCount < end) {
                end = tokenCount;
            }
            if(start >= end) {
                return new uint256[](0);
            }
            count = end - start;
            uint256[] memory result = new uint256[](count);
            uint256 index;
            for (index = start; index < end; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function getCardProperties(uint256 _tokenId) public view returns (uint256 cardType,uint256 cardLevel,uint256 objectId,uint256 forceValue,uint256 defenderValue) {
        Property memory prop = cardProperties[_tokenId];
        cardType = uint256(prop.cardType);
        cardLevel = uint256(prop.level);
        objectId = prop.objectId;
        forceValue = prop.forceValue;
        defenderValue = prop.defenderValue;
    }


    function transferFrom(address from, address to, uint256 tokenId) public virtual override {
        claim(from,tokenId);
        super.transferFrom(from,to,tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
        claim(from,tokenId);
        super.safeTransferFrom(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
        claim(from,tokenId);
        super.safeTransferFrom(from, to, tokenId, _data);
    }

    function calculatePrice() public view returns (uint256) {
        require(nextElementaryCardId <= elementaryCardIdEnd, "Sale has already ended");

        if (nextElementaryCardId > 2660) {
            return 200000000000000000;        // 2661-3560:900,0.20 ETH
        } else if (nextElementaryCardId > 1960) {
            return 160000000000000000;        // 1961-2660:700,0.16 ETH
        } else if (nextElementaryCardId > 1460) {
            return 80000000000000000;         // 1461-1960:500,0.08 ETH
        } else {
            return 40000000000000000;         // 1161-1460:300,0.04 ETH
        }
    }

    function calculatePriceForToken(uint _id) public view returns (uint256) {
        require(_id >= elementaryCardIdStart && _id <= elementaryCardIdEnd, "Sale has already ended");

        if (_id > 2660) {
            return 200000000000000000;        // 2661-3560:900,0.20 ETH
        } else if (_id > 1960) {
            return 160000000000000000;        // 1961-2660:700,0.16 ETH
        } else if (_id > 1460) {
            return 80000000000000000;         // 1461-1960:500,0.08 ETH
        } else {
            return 40000000000000000;         // 1161-1460:300,0.04 ETH
        }
    }

    function mintCard(address owner, uint256 cardId,CardLevel level) internal {
        _safeMint(owner, cardId);
        cardProperties[cardId] = createProperty(cardId,level);
    }

    function createProperty(uint256 cardId,CardLevel level) internal returns (Property memory property){
        // type： Odd for Ultraman, Even for Monster
        property.cardType = (cardId % 2 == 1) ? CardType.Ultraman : CardType.Monster;
        property.level = level;
        property.objectId = (cardId/2) % (objects[property.cardType][property.level]);

        uint k = uint(keccak256(abi.encodePacked(now,msg.sender,cardId,property.cardType,property.level,property.objectId)));

        (uint256 base,uint256 mod) = getForceOrDefenderBaseAndMod(property.level);
        property.forceValue = base + (k % mod);
        property.defenderValue = base + ((k >> 32) % mod);

        property.lastUpdateTime = now;
    }

    function getForceOrDefenderBaseAndMod(CardLevel level) public pure returns (uint256 base, uint256 mod) {
        if(level == CardLevel.Advanced) {
            base = 7;
            mod = 4;
        }else if(level == CardLevel.Intermediate) {
            base = 5;
            mod = 3;
        }else {
            base = 2;
            mod = 3;
        }
    }

    function createElementaryCard(uint256 num) public payable {
        require(hasSaleStarted == true, "Sale has not started");
        require(nextElementaryCardId <= elementaryCardIdEnd, "Sale has already ended");
        require(num > 0 && num <= 10, "You can create minimum 1, maximum 10 Ultramans");
        require(nextElementaryCardId.add(num) <= elementaryCardIdEnd + 1, "Exceeds maximum Ultramans");
        require(msg.value >= calculatePrice().mul(num), "Ether value sent is below the price");

        for (uint256 i = 0; i < num; i++) {
            uint256 cardId = nextElementaryCardId++;
            mintCard(msg.sender,cardId,CardLevel.Elementary);
        }
    }

    // God Mode
    function setProvenanceHash(string memory _hash) public onlyOwner {
        METADATA_PROVENANCE_HASH = _hash;
    }

    function setBaseURI(string memory baseURI) public onlyOwner {
        _setBaseURI(baseURI);
    }

    function startSale() public onlyOwner {
        hasSaleStarted = true;
    }
    function pauseSale() public onlyOwner {
        hasSaleStarted = false;
    }

    function startComposeToIntermediate() public onlyOwner {
        hasComposeToIntermediateStarted = true;
    }
    function pauseComposeToIntermediate() public onlyOwner {
        hasComposeToIntermediateStarted = false;
    }

    function startComposeToAdvanced() public onlyOwner {
        hasComposeToAdvancedStarted = true;
    }
    function pauseComposeToAdvanced() public onlyOwner {
        hasComposeToAdvancedStarted = false;
    }

    function withdrawAll() public payable onlyOwner {
        require(payable(msg.sender).send(address(this).balance));
    }

    function reserveAdvancedCard(address to, uint256 num) public onlyOwner {

        require(to != address(0), "Not support zero address");
        require(nextReserveAdvancedCardId <= reserveAdvancedCardIdEnd, "Reserve advanced card has already ended");
        require(num > 0 && num <= 10, "You can reserve minimum 1, maximum 10 Ultramans");
        require(nextReserveAdvancedCardId.add(num) <= reserveAdvancedCardIdEnd + 1, "Exceeds maximum reserve advanced Ultramans");
        // Reserved for people who helped this project and giveaways
        for (uint256 i = 0; i < num; i++) {
            uint256 cardId = nextReserveAdvancedCardId++;
            mintCard(to,cardId,CardLevel.Advanced);
        }
    }

    function reserveElementaryCard(address to,uint256 num) public {

        require(reserveOfCallers.contains(msg.sender),"Only reserve of caller");
        require(to != address(0), "Not support zero address");
        require(nextReserveElementaryCardId <= reserveElementaryCardIdEnd, "Reserve elementary card has already ended");
        require(num > 0 , "You can reserve minimum 1 card");
        require(nextReserveElementaryCardId.add(num) <= reserveElementaryCardIdEnd + 1, "Exceeds maximum reserve elementary Ultramans");
        // Reserved for people who helped this project and giveaways
        for (uint256 i = 0; i < num; i++) {
            uint256 cardId = nextReserveElementaryCardId++;
            mintCard(to,cardId,CardLevel.Elementary);
        }
    }


    // mine token
    function earned(uint256 _tokenId) public view returns (uint256) {
        if(!_exists(_tokenId)) {
            return 0;
        }
        Property memory prop = cardProperties[_tokenId];
        uint256 _startTime = Math.max(prop.lastUpdateTime, mineStartTime);
        uint256 _endTime = Math.min(block.timestamp,mineEndTime);

        if(_startTime >= _endTime) {
            return 0;
        }

        return prop.forceValue.mul(prop.defenderValue)
                .mul(mineFactor).mul(_endTime.sub(_startTime))
                .div(1 days);
    }

    function setMineStartTime(uint256 time) external onlyOwner {
        mineStartTime = time;
    }

    function setMineEndTime(uint256 time) external onlyOwner {
        mineEndTime = time;
    }

    function setMineFactor(uint256 factor) external onlyOwner {
        mineFactor = factor;
    }

    function claimAll(uint256[] calldata _tokenIds) public {
        uint256 payout = 0;
        for(uint256 i = 0 ; i < _tokenIds.length; i++) {
            payout = payout.add(withdraw(msg.sender,_tokenIds[i]));
        }
        if(payout > 0) {
            untToken.safeTransfer(msg.sender, payout);
        }
    }

    function claim(address owner,uint256 _tokenId) internal {
        uint256 payout = withdraw(owner,_tokenId);
        if(payout > 0) {
            untToken.safeTransfer(owner, payout);
        }
    }

    function withdraw(address owner,uint256 _tokenId) internal returns (uint256) {
        require(ownerOf(_tokenId) == owner,"Not token owner");
        uint256 payout = earned(_tokenId);
        Property memory prop = cardProperties[_tokenId];
        prop.lastUpdateTime = block.timestamp;
        prop.withdrawValue = prop.withdrawValue.add(payout);
        cardProperties[_tokenId] = prop;
        return payout;
    }

    function getTokenPrice() public view returns (uint256) {
        if(lpToken == address (0x0)) {
            return 0;
        }

        IUniswapV2Pair pair = IUniswapV2Pair(lpToken);
        (uint112 _reserve0, uint112 _reserve1,) = pair.getReserves();
        (uint256 value0,uint256 value1) = USDT == pair.token0() ? (uint256(_reserve0),uint256(_reserve1)) : (uint256(_reserve1),uint256(_reserve0));
        uint256 decimals0 = uint256(ERC20(USDT).decimals());
        uint256 decimals1 = uint256(ERC20(address(untToken)).decimals());

        return uint256(10**(18 + decimals1 - decimals0)).mul(value0).div(value1) ;
    }

    function compose(uint256 _tokenId1, uint256 _tokenId2, CardLevel level,uint256 _newId, CardType cardType) internal{
        require(ownerOf(_tokenId1) == msg.sender &&
            ownerOf(_tokenId2) == msg.sender, "Not the owner");
        Property memory prop1 = cardProperties[_tokenId1];
        Property memory prop2 = cardProperties[_tokenId2];
        require(prop1.level == level &&
            prop2.level == level, "Level does not match");
        require(prop1.cardType == cardType &&
            prop2.cardType == cardType, "Card type does not match");
        safeTransferFrom(msg.sender,dev,_tokenId1);
        safeTransferFrom(msg.sender,dev,_tokenId2);
        CardLevel destLevel = CardLevel.Intermediate;
        if(level == CardLevel.Intermediate) {
            destLevel = CardLevel.Advanced;
        }

        mintCard(msg.sender,_newId,destLevel);
    }

    // Compose intermediate cards
    function composeToIntermediateCards(uint256[] memory _tokenIds) external {
        require(hasComposeToIntermediateStarted, "Compose to intermediate card has not started");
        uint256 count = _tokenIds.length / 2;
        require(_tokenIds.length % 2 == 0 && count > 0 && count <= 5, "Number of cards does not match or exceed maximum cards,maximum is 10");
        CardType cardType = cardProperties[_tokenIds[0]].cardType;
        uint256 nextIntermediateCardId = nextUltramanIntermediateCardId;
        if(cardType == CardType.Monster) {
            nextIntermediateCardId = nextMonsterIntermediateCardId;
        }
        require(nextIntermediateCardId.add((count - 1) * 2) <= intermediateCardIdEnd, "No enough intermediate card");
        uint256 _tokenPrice = getTokenPrice();
        uint256 needToken = composeToIntermediateCardNeedValue.
                            mul(WEI_PRECISION).
                            mul(count).
                            div(_tokenPrice);

        untToken.safeTransferFrom(msg.sender,dev,needToken);
        for(uint256 i = 0 ; i < count ;i++) {
            uint256 first = i * 2 ;
            uint256 second = first + 1;
            compose(_tokenIds[first],_tokenIds[second],CardLevel.Elementary,nextIntermediateCardId,cardType);
            nextIntermediateCardId = nextIntermediateCardId + 2;
        }
        if(cardType == CardType.Ultraman) {
            nextUltramanIntermediateCardId = nextIntermediateCardId;
        }else {
            nextMonsterIntermediateCardId = nextIntermediateCardId;
        }
    }


    // Compose advanced cards
    function composeToAdvancedCards(uint256[] memory _tokenIds) external {
        require(hasComposeToAdvancedStarted, "Compose to advance card has not started");
        uint256 count = _tokenIds.length / 2;
        require(_tokenIds.length % 2 == 0 && count > 0 && count <= 5, "Number of cards does not match or exceed maximum cards,maximum is 10");
        CardType cardType = cardProperties[_tokenIds[0]].cardType;
        uint256 nextAdvancedCardId = nextUltramanAdvancedCardId;
        if(cardType == CardType.Monster) {
            nextAdvancedCardId = nextMonsterAdvancedCardId;
        }
        require(nextAdvancedCardId.add((count - 1) * 2) <= advancedCardIdEnd, "No enough advance card");

        uint256 _tokenPrice = getTokenPrice();
        uint256 needToken = composeToAdvancedCardNeedValue.
                                mul(WEI_PRECISION).
                                mul(count).
                                div(_tokenPrice);

        untToken.safeTransferFrom(msg.sender,dev,needToken);
        for(uint256 i = 0 ; i < count ;i++) {
            uint256 first = i * 2 ;
            uint256 second = first + 1;
            compose(_tokenIds[first],_tokenIds[second],CardLevel.Intermediate,nextAdvancedCardId,cardType);
            nextAdvancedCardId = nextAdvancedCardId + 2;
        }
        if(cardType == CardType.Ultraman) {
            nextUltramanAdvancedCardId = nextAdvancedCardId;
        }else {
            nextMonsterAdvancedCardId = nextAdvancedCardId;
        }
    }
}
function seize(IERC20 token, uint256 amount) external onlyOwner{
    token.safeTransfer(owner(), amount);
}

/* Fee collection for any other token */