pragma solidity ^0.4.25;

/**
 * 
 * World War Goo - Competitive Idle Game
 * 
 * https://ethergoo.io
 * 
 */

interface ERC721 {
    function totalSupply() external view returns (uint256 tokens);
    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function exists(uint256 tokenId) external view returns (bool tokenExists);
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address approvee);

    function transferFrom(address from, address to, uint256 tokenId) external;
    function tokensOf(address owner) external view returns (uint256[] tokens);
    //function tokenByIndex(uint256 index) external view returns (uint256 token);

    // Events
    event Transfer(address from, address to, uint256 tokenId);
    event Approval(address owner, address approved, uint256 tokenId);
}


interface ERC721Receiver {
    function onERC721Received(address operator, address from, uint256 tokenId, bytes data) external returns(bytes4);
}

contract Inventory is ERC721 {

    Units constant units = Units(0xf936aa9e1f22c915abf4a66a5a6e94eb8716ba5e);

    string public constant name = "Goo Item";
    string public constant symbol = "GOOITEM";

    mapping(address => mapping(uint256 => uint256)) public unitEquippedItems; // address -> unitId -> tokenId
    mapping(uint256 => Item) public itemList;

    // ERC721 stuff
    mapping(uint256 => address) public tokenOwner;
    mapping(uint256 => address) public tokenApprovals;
    mapping(address => uint256[]) public ownedTokens;
    mapping(uint256 => uint256) public ownedTokensIndex;
    mapping(uint256 => uint256) public tokenItems; // tokenId -> ItemId
    mapping(address => bool) operator;

    // Offset by one (so token id starts from 1)
    uint256 nextTokenId = 1;
    uint256 tokensBurnt = 1;

    struct Item {
        uint256 itemId;
        uint256 unitId;
        uint256 rarity;
        uint32[8] upgradeGains;
    }

    address owner; // Minor management

    constructor() public {
        owner = msg.sender;
    }

    function setOperator(address gameContract, bool isOperator) external {
        require(msg.sender == owner);
        operator[gameContract] = isOperator;
    }

    function totalSupply() external view returns (uint256) {
        return nextTokenId - tokensBurnt;
    }

    function balanceOf(address player) public view returns (uint256) {
        return ownedTokens[player].length;
    }

    function ownerOf(uint256 tokenId) external view returns (address) {
        return tokenOwner[tokenId];
    }

    function exists(uint256 tokenId) external view returns (bool) {
        return tokenOwner[tokenId] != address(0);
    }

    function approve(address to, uint256 tokenId) external {
        require(msg.sender == tokenOwner[tokenId]);
        tokenApprovals[tokenId] = to;
        emit Approval(msg.sender, to, tokenId);
    }

    function getApproved(uint256 tokenId) external view returns (address) {
        return tokenApprovals[tokenId];
    }

    function tokensOf(address player) external view returns (uint256[] tokens) {
         return ownedTokens[player];
    }

    function itemsOf(address player) external view returns (uint256[], uint256[]) {
        uint256 unequippedItemsCount = 0; // TODO better way?
        uint256 tokensLength = ownedTokens[player].length;
        for (uint256 i = 0; i < tokensLength; i++) {
            if (tokenOwner[ownedTokens[player][i]] == player) {
                unequippedItemsCount++;
            }
        }

        uint256[] memory tokensOwned = new uint256[](unequippedItemsCount);
        uint256 j = 0;
        for (i = 0; i < tokensLength; i++) {
            uint256 tokenId = ownedTokens[player][i];
            if (tokenOwner[tokenId] == player) { // Unequipped items only
                tokensOwned[j] = tokenId;
                j++;
            }
        }

        uint256[] memory itemIdsOwned = new uint256[](unequippedItemsCount);
        for (i = 0; i < unequippedItemsCount; i++) {
            itemIdsOwned[i] = tokenItems[tokensOwned[i]];
        }

        return (tokensOwned, itemIdsOwned);
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        require(tokenApprovals[tokenId] == msg.sender || tokenOwner[tokenId] == msg.sender || operator[msg.sender]);
        require(tokenOwner[tokenId] == from);

        removeTokenFrom(from, tokenId);
        addTokenTo(to, tokenId);

        delete tokenApprovals[tokenId]; // Clear approval
        emit Transfer(from, to, tokenId);
    }
    
    function safeTransferFrom(address from, address to, uint256 tokenId) public {
        safeTransferFrom(from, to, tokenId, "");
    }
    
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {
        transferFrom(from, to, tokenId);
        checkERC721Recieved(from, to, tokenId, data);
    }
    
    function checkERC721Recieved(address from, address to, uint256 tokenId, bytes memory data) internal {
        uint256 size;
        assembly { size := extcodesize(to) }
        if (size > 0) { // Recipient is contract so must confirm recipt
            bytes4 successfullyRecieved = ERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data);
            require(successfullyRecieved == bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")));
        }
    }

    function removeTokenFrom(address from, uint256 tokenId) internal {
        require(tokenOwner[tokenId] == from);
        tokenOwner[tokenId] = address(0);

        uint256 tokenIndex = ownedTokensIndex[tokenId];
        uint256 lastTokenIndex = SafeMath.sub(ownedTokens[from].length, 1);
        uint256 lastToken = ownedTokens[from][lastTokenIndex];

        ownedTokens[from][tokenIndex] = lastToken;
        ownedTokens[from][lastTokenIndex] = 0;

        ownedTokens[from].length--;
        ownedTokensIndex[tokenId] = 0;
        ownedTokensIndex[lastToken] = tokenIndex;
    }

    function addTokenTo(address to, uint256 tokenId) internal {
        require(tokenOwner[tokenId] == address(0));
        tokenOwner[tokenId] = to;

        ownedTokensIndex[tokenId] = ownedTokens[to].length;
        ownedTokens[to].push(tokenId);
    }
    
    function burn(uint256 tokenId) external {
        address itemOwner = tokenOwner[tokenId];
        require(itemOwner == msg.sender || operator[msg.sender]);
        
        removeTokenFrom(itemOwner, tokenId);
        delete tokenApprovals[tokenId]; // Clear approval
        delete tokenItems[tokenId]; // Delete token-item
        emit Transfer(itemOwner, address(0), tokenId);
        tokensBurnt++;
    }

    function mintItem(uint256 itemId, address player) external {
        require(operator[msg.sender]);
        require(validItem(itemId));

        uint256 tokenId = nextTokenId; // Start from id 1
        tokenItems[tokenId] = itemId;
        addTokenTo(player, tokenId);
        emit Transfer(address(0), player, tokenId);
        nextTokenId++;
    }

    function getEquippedItemId(address player, uint256 unitId) external view returns (uint256) {
        return tokenItems[unitEquippedItems[player][unitId]];
    }

    function equipSingle(uint256 tokenId) public {
        require(tokenOwner[tokenId] == msg.sender);
        uint256 itemId = tokenItems[tokenId];
        uint256 unitId = itemList[itemId].unitId;

        // Remove item from user
        tokenOwner[tokenId] = 0;
        delete tokenApprovals[tokenId]; // Clear approval

        uint256 existingEquipment = unitEquippedItems[msg.sender][unitId];
        uint32[8] memory newItemGains = itemList[itemId].upgradeGains;
        
        if (existingEquipment == 0) {
            // Grant buff to unit
            units.increaseUpgradesExternal(msg.sender, unitId, newItemGains[0], newItemGains[1], newItemGains[2], newItemGains[3], newItemGains[4], newItemGains[5], newItemGains[6], newItemGains[7]);
        } else if (existingEquipment != tokenId) {
            uint256 existingItemId = tokenItems[existingEquipment];

            // Grant buff to unit
            units.swapUpgradesExternal(msg.sender, unitId, newItemGains, itemList[existingItemId].upgradeGains);

            // Return old item to user
            tokenOwner[existingEquipment] = msg.sender;
        }

        // Finally equip token (item)
        unitEquippedItems[msg.sender][unitId] = tokenId;
    }

    function unequipSingle(uint256 unitId) public {
        require(unitEquippedItems[msg.sender][unitId] > 0);

        uint256 tokenId = unitEquippedItems[msg.sender][unitId];
        require(tokenOwner[tokenId] == 0);

        uint256 itemId = tokenItems[tokenId];
        uint32[8] memory existingItemGains = itemList[itemId].upgradeGains;
        units.decreaseUpgradesExternal(msg.sender, unitId, existingItemGains[0], existingItemGains[1], existingItemGains[2], existingItemGains[3], existingItemGains[4], existingItemGains[5], existingItemGains[6], existingItemGains[7]);

        // Finally return item
        tokenOwner[tokenId] = msg.sender;
        unitEquippedItems[msg.sender][unitId] = 0;
    }

    function equipMultipleTokens(uint256[] tokens) external {
        for (uint256 i = 0; i < tokens.length; ++i) {
            equipSingle(tokens[i]);
        }
    }

    function unequipMultipleUnits(uint256[] unitIds) external {
        for (uint256 i = 0; i < unitIds.length; ++i) {
            unequipSingle(unitIds[i]);
        }
    }

    function addItem(uint256 itemId, uint256 unitId, uint256 rarity, uint32[8] upgradeGains) external {
        require(operator[msg.sender]);
        itemList[itemId] = Item(itemId, unitId, rarity, upgradeGains);
    }

    function validItem(uint256 itemId) internal constant returns (bool) {
        return itemList[itemId].itemId == itemId;
    }
    
    function getItemRarity(uint256 itemId) external view returns (uint256) {
        return itemList[itemId].rarity;
    }
}


contract Units {
    function increaseUpgradesExternal(address player, uint256 unitId, uint32 prodIncrease, uint32 prodMultiplier, uint32 attackIncrease, uint32 attackMultiplier, uint32 defenseIncrease, uint32 defenseMultiplier, uint32 lootingIncrease, uint32 lootingMultiplier) external;
    function decreaseUpgradesExternal(address player, uint256 unitId, uint32 prodIncrease, uint32 prodMultiplier, uint32 attackIncrease, uint32 attackMultiplier, uint32 defenseIncrease, uint32 defenseMultiplier, uint32 lootingIncrease, uint32 lootingMultiplier) external;
    function swapUpgradesExternal(address player, uint256 unitId, uint32[8] upgradeGains, uint32[8] upgradeLosses) external;
}



library SafeMath {

  /**
  * @dev Multiplies two numbers, throws on overflow.
  */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  /**
  * @dev Integer division of two numbers, truncating the quotient.
  */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  /**
  * @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
  */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  /**
  * @dev Adds two numbers, throws on overflow.
  */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}