abstract contract CollectCode is ERC721, Ownable
{
    using SafeMath for uint256;

    struct Config {
        string seriesCode;
        uint256 initialSupply;
        uint256 maxSupply;
        uint256 initialPrice;
        uint8 gridSize;
    }
    Config internal config_;

    struct State {
        bool isReleased;            // Token Zero was minted
        uint256 mintedCount;        // (ignores Token Zero)
        uint256 builtCount;         // (ignores Token Zero)
        uint256 notBuiltCount;      // (ignores Token Zero)
        uint256 currentSupply;      // permitted to mint
        uint256 availableSupply;    // not minted
        uint256 maxBuyout;          // not minted
        bool isAvailable;           // availableSupply > 0
    }
    State internal state_;

    struct TokenInfo {
        address owner;
        bool youOwnIt;
        bool isBuilt;
        address builder;
        uint256 sequenceNumber;
        uint256[] sequenceTokens;
    }

    mapping (uint256 => bytes) internal _colors;
    mapping (uint256 => uint256) internal _sequenceNumber;
    mapping (uint256 => address) internal _builder;

    constructor()
    {
        state_ = State(
            false,  // isReleased
            0,      // mintedCount
            0,      // builtCount
            0,      // notBuiltCount
            0,      // currentSupply
            0,      // availableSupply
            0,      // maxBuyout
            false   // isAvailable
        );
    }

    //
    // public actions
    function giftCode(address to) onlyOwner public returns (uint256)
    {
        require(!state_.isReleased, "CC:AlreadyIssued");
        require(to == owner(), "CC:NotOwner");
        //require(isOwner(), "CC:NotOwner"); // Ownable takes care
        return mintCode_( to, 1, true );
    }
    function buyCode(address to, uint256 quantity, bool build) public payable returns (uint256)
    {
        require(state_.isReleased, "CC: Unreleased");
        require(msg.value == calculatePriceForQuantity(quantity), "CC:BadValue");
        return mintCode_( to, quantity, build );
    }
    function buildCode(uint256 tokenId) public
    {
        require(_exists(tokenId), "CC:BadTokenId");
        require(_colors[tokenId].length == 0, "CC:AlreadyBuilt");
        require(msg.sender == ownerOf(tokenId), "CC:NotOwner");
        buildCode_( msg.sender, tokenId, 0 );
    }
    function withdraw() onlyOwner public {
        msg.sender.transfer(address(this).balance);
    }

    //
    // public getters
    function getConfig() public view returns (Config memory)
    {
        return config_;
    }
    function getState() public view returns (State memory)
    {
        return state_;
    }
    function getTokenInfo(address from, uint256 tokenId) public view returns (TokenInfo memory)
    {
        require(_exists(tokenId), "CC:BadTokenId");
        uint256[] memory sequenceTokens;
        if(_sequenceNumber[tokenId] > 0) {
            // find this token's complete sequence
            uint256 tokenCount = _sequenceNumber[tokenId];
            for(uint256 t = tokenId+1 ; _sequenceNumber[t] > 0 && _sequenceNumber[t] > _sequenceNumber[tokenId] ; ++t)
                tokenCount++;
            sequenceTokens = new uint256[](tokenCount);
            for(uint256 i = 0 ; i < tokenCount ; ++i)
                sequenceTokens[i] = tokenId-_sequenceNumber[tokenId]+1+i;
        }
        return TokenInfo(
            ownerOf(tokenId),               // owner address
            (from == ownerOf(tokenId)),     // sender owns it
            (_colors[tokenId].length > 0),  // is built
            _builder[tokenId],              // user who built the token
            _sequenceNumber[tokenId],       // build sequential number
            sequenceTokens                  // sequence of tokens
        );
    }
    function calculatePriceForQuantity(uint256 quantity) public view returns (uint256)
    {
        uint256 price = 0;
        for(uint256 i = 1 ; i <= quantity ; i++)
            price += state_.mintedCount.add(i) * config_.initialPrice * 10000000000000000; // 1 ETH=1000000000000000000
        return price;
    }
    function getPrices() public view returns (uint256[] memory)
    {
        uint256 quantity = 0;
        if(totalSupply() > 0 && totalSupply() < config_.maxSupply)
            quantity = Utils.max_uint256(state_.maxBuyout, 1);
        uint256[] memory prices = new uint[](quantity);
        for(uint256 i = 1 ; i <= quantity ; i++)
            prices[i-1] = calculatePriceForQuantity(i);
        return prices;
    }
    function getOwnedTokens(address from) public view returns (uint256[] memory)
    {
        uint256[] memory tokenIds = new uint[](balanceOf(from));
        for(uint256 i = 0 ; i < tokenIds.length ; i++)
            tokenIds[i] = tokenOfOwnerByIndex(from, i);
        return tokenIds;
    }

    //
    // Privates
    //

    function mintCode_(address to, uint256 quantity, bool build) internal returns (uint256)
    {
        require(quantity > 0, "CC:BadQuantity");

        if( state_.isReleased )
        {
            require(state_.mintedCount < config_.maxSupply, "CC:SoldOut");
            require(state_.mintedCount < state_.currentSupply, "CC:Unavailable");
            require(state_.mintedCount.add(quantity) <= state_.currentSupply, "CC:TooMany");
            require(quantity <= state_.maxBuyout, "CC:TooMany");
        }

        for(uint256 i = 0 ; i < quantity ; i++)
        {
            uint256 newTokenId = !state_.isReleased ? 0 : state_.mintedCount.add(1);
            _mint( to, newTokenId );

            // update contract state
            state_.isReleased = true;
            state_.mintedCount = newTokenId;
            if(newTokenId > 0) {
                state_.notBuiltCount++;
            }
            
            if( build ) {
                buildCode_(to, newTokenId, quantity == 1 ? 0 : (i+1));
            } else {
                calculateSupply_();
            }
        }

        return quantity;
    }

    function buildCode_(address to, uint256 tokenId, uint256 sequenceNumber) internal
    {
        _builder[tokenId] = to;
        _sequenceNumber[tokenId] = sequenceNumber;
        _colors[tokenId] = Utils.reduceColors(
            bytes20(address(this)),   // seed 1: contract address
            bytes20(to),              // seed 2: owner address
            Utils.getBlockSeed(),     // seed 3: block hash
            uint8(config_.gridSize*2), tokenId*config_.gridSize);

        // increase built count for supply (ignore Token Zero)
        if(tokenId > 0)
        {
            state_.notBuiltCount--;
            state_.builtCount++;
        }
        calculateSupply_();
    }

    function calculateSupply_() internal
    {
        if(state_.mintedCount < config_.initialSupply) {
            // Initial supply must go first
            state_.currentSupply = config_.initialSupply;
        }
        else {
            // Release 10% minus not build tokens
            uint256 surplus = Utils.percent_uint256(config_.maxSupply, 10);
            state_.currentSupply = state_.mintedCount   // minted
                + surplus                               // add 10%
                - Utils.clamp_uint256(state_.notBuiltCount, 0, surplus);    // minus not built
            if(state_.currentSupply > config_.maxSupply)
                state_.currentSupply = config_.maxSupply;
        }
        state_.availableSupply = state_.currentSupply - state_.mintedCount;
        state_.maxBuyout = Utils.min_uint256( state_.availableSupply, Utils.percent_uint256(config_.maxSupply, 5) );
        state_.isAvailable = (state_.availableSupply > 0);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "CC:BadTokenId");
        return string(abi.encodePacked(
            "https://collect-code.com/api/token/", config_.seriesCode,
            "/", Utils.utoa(uint(tokenId)),
            "/metadata?v=1&pixels=", (_colors[tokenId].length > 0 ? Utils.convertToHexString(Utils.makePixels(config_.gridSize, _colors[tokenId])) : '') ));
    }
}
function getConfig() public view returns (Config memory)
{
    return config_;
}

//
// public getters