contract CryptoSurprise
{
    using SetLibrary for SetLibrary.Set;
    
    ////////////////////////////////////
    ////// CONSTANTS
    
    uint256 constant public BAG_TRANSFER_FEE = 0.05 ether;
    uint256 constant public BAG_TRANSFER_MINIMUM_AMOUNT_OF_BUYS = 4;
    
    
    ////////////////////////////////////
    ////// STATE VARIABLES
    
    struct BagType
    {
        // Constants
        string name;
        
        uint256 startPrice;
        uint256 priceMultiplierPerBuy; // For example, 2 000 000 means 100% increase. (100% = doubling every buy)
        
        uint256 startCommission; // 0 to 1 000 000, for example 100 000 means 10%
        uint256 commissionIncrementPerBuy;
        uint256 maximumCommission;
        
        uint256 supplyHardCap;
        
        // Variables
        uint256 currentSupply;
    }
    
    struct Bag
    {
        // Constants
        uint256 bagTypeIndex;
        
        // Variables
        uint256 amountOfBuys;
        address owner;
        uint256 commission; // 0 to 1 000 000, for example 100 000 means 10%
        uint256 price;
        
        uint256 availableTimestamp;
    }
    
    // Variable that remembers the current owner
    address public owner;
    BagType[] public bagTypes;
    Bag[] public bags;
    
    mapping(address => uint256) public addressToTotalEtherSpent;
    mapping(address => uint256) public addressToTotalPurchasesMade;
    mapping(address => SetLibrary.Set) private ownerToBagIndices;
    address[] public allParticipants;
    
    
    ////////////////////////////////////
    ////// PLAYER FUNCTIONS
    
    function buyBag(uint256 _bagIndex) external payable
    {
        // Make sure that the bag exists
        require(_bagIndex < bags.length);
        
        // Reference the bag data and bag type data
        Bag storage bag = bags[_bagIndex];
        BagType storage bagType = bagTypes[bag.bagTypeIndex];
        
        // Make sure the bag is already available
        require(now >= bag.availableTimestamp);
        
        // Make sure the caller payed at least the current price
        require(msg.value >= bag.price);
        uint256 refund = msg.value - bag.price;
        
        // Remember who the previous owner was
        address previousOwner = bag.owner;
        
        // Set the buyer as the new owner
        bag.owner = msg.sender;
        
        // Calculate the previous and next price
        uint256 previousPrice = bag.price * 1000000 / bagType.priceMultiplierPerBuy;
        uint256 nextPrice = bag.price * bagType.priceMultiplierPerBuy / 1000000;
        
        // Calculate how much the previous owner should get:
        uint256 previousOwnerReward;
        
        // If this is the first buy: the full current price
        if (bag.amountOfBuys == 0)
        {
            previousOwnerReward = bag.price;
        }
        
        // otherwise: previous price + the commission
        else
        {
            previousOwnerReward = bag.price * bag.commission / 1000000;
            //previousOwnerReward = previousPrice + previousPrice * bag.commission / 1000000;
        }
        
        // Set the new price of the bag
        bag.price = nextPrice;
        
        // Increment the amountOfBuys counter
        bag.amountOfBuys++;
        
        // If this is NOT the first buy of this bag:
        if (bag.amountOfBuys > 1)
        {
            // Increase the commission up to the maximum
            if (bag.commission < bagType.maximumCommission)
            {
                uint256 newCommission = bag.commission + bagType.commissionIncrementPerBuy;
                
                if (newCommission >= bagType.maximumCommission)
                {
                    bag.commission = bagType.maximumCommission;
                }
                else 
                {
                    bag.commission = newCommission;
                }
            }
        }
        
        // Record statistics
        if (addressToTotalPurchasesMade[msg.sender] == 0)
        {
            allParticipants.push(msg.sender);
        }
        addressToTotalEtherSpent[msg.sender] += msg.value;
        addressToTotalPurchasesMade[msg.sender]++;
        
        // Transfer the reward to the previous owner. If the previous owner is
        // the CryptoSurprise smart contract itself, we don't need to perform any
        // transfer because the contract already has it.
        if (previousOwner != address(this))
        {
            previousOwner.transfer(previousOwnerReward);
        }
        
        if (refund > 0)
        {
            msg.sender.transfer(refund);
        }
    }
    
    function transferBag(address _newOwner, uint256 _bagIndex) public payable
    {
        // Require payment
        require(msg.value == BAG_TRANSFER_FEE);
        
        // Perform the transfer
        _transferBag(msg.sender, _newOwner, _bagIndex);
    }
    
    
    ////////////////////////////////////
    ////// OWNER FUNCTIONS
    
    // Constructor function
    function CryptoSurprise() public
    {
        owner = msg.sender;
        
        bagTypes.push(BagType({
            name: "Blue",
            
            startPrice: 0.04 ether,
            priceMultiplierPerBuy: 1300000, // 130%
            
            startCommission: 850000, // 85%
            commissionIncrementPerBuy: 5000, // 0.5 %-point
            maximumCommission: 900000, // 90%
            
            supplyHardCap: 600,
            
            currentSupply: 0
        }));
		bagTypes.push(BagType({
            name: "Red",
            
            startPrice: 0.03 ether,
            priceMultiplierPerBuy: 1330000, // 133%
            
            startCommission: 870000, // 87%
            commissionIncrementPerBuy: 5000, // 0.5 %-point
            maximumCommission: 920000, // 92%
            
            supplyHardCap: 300,
            
            currentSupply: 0
        }));
		bagTypes.push(BagType({
            name: "Green",
            
            startPrice: 0.02 ether,
            priceMultiplierPerBuy: 1360000, // 136%
            
            startCommission: 890000, // 89%
            commissionIncrementPerBuy: 5000, // 0.5 %-point
            maximumCommission: 940000, // 94%
            
            supplyHardCap: 150,
            
            currentSupply: 0
        }));
		bagTypes.push(BagType({
            name: "Black",
            
            startPrice: 0.1 ether,
            priceMultiplierPerBuy: 1450000, // 145%
            
            startCommission: 920000, // 92%
            commissionIncrementPerBuy: 10000, // 1 %-point
            maximumCommission: 960000, // 96%
            
            supplyHardCap: 50,
            
            currentSupply: 0
        }));
		bagTypes.push(BagType({
            name: "Pink",
            
            startPrice: 1 ether,
            priceMultiplierPerBuy: 1500000, // 150%
            
            startCommission: 940000, // 94%
            commissionIncrementPerBuy: 10000, // 1 %-point
            maximumCommission: 980000, // 98%
            
            supplyHardCap: 10,
            
            currentSupply: 0
        }));
		bagTypes.push(BagType({
            name: "White",
            
            startPrice: 10 ether,
            priceMultiplierPerBuy: 1500000, // 150%
            
            startCommission: 970000, // 97%
            commissionIncrementPerBuy: 10000, // 1 %-point
            maximumCommission: 990000, // 99%
            
            supplyHardCap: 1,
            
            currentSupply: 0
        }));
    }
    
    // Function that allows the current owner to transfer ownership
    function transferOwnership(address _newOwner) external
    {
        require(msg.sender == owner);
        owner = _newOwner;
    }
    
    // Only the owner can deposit ETH by sending it directly to the contract
    function () payable external
    {
        require(msg.sender == owner);
    }
    
    // Function that allows the current owner to withdraw any amount
    // of ETH from the contract
    function withdrawEther(uint256 amount) external
    {
        require(msg.sender == owner);
        owner.transfer(amount);
    }
    
    function addBag(uint256 _bagTypeIndex) external
    {
        addBagAndGift(_bagTypeIndex, address(this));
    }
    function addBagDelayed(uint256 _bagTypeIndex, uint256 _delaySeconds) external
    {
        addBagAndGiftAtTime(_bagTypeIndex, address(this), now + _delaySeconds);
    }
    
    function addBagAndGift(uint256 _bagTypeIndex, address _firstOwner) public
    {
        addBagAndGiftAtTime(_bagTypeIndex, _firstOwner, now);
    }
    function addBagAndGiftAtTime(uint256 _bagTypeIndex, address _firstOwner, uint256 _timestamp) public
    {
        require(msg.sender == owner);
        
        require(_bagTypeIndex < bagTypes.length);
        
        BagType storage bagType = bagTypes[_bagTypeIndex];
        
        require(bagType.currentSupply < bagType.supplyHardCap);
        
        bags.push(Bag({
            bagTypeIndex: _bagTypeIndex,
            
            amountOfBuys: 0,
            owner: _firstOwner,
            commission: bagType.startCommission,
            price: bagType.startPrice,
            
            availableTimestamp: _timestamp
        }));
        
        bagType.currentSupply++;
    }
    

    
    ////////////////////////////////////
    ////// INTERNAL FUNCTIONS
    
    function _transferBag(address _from, address _to, uint256 _bagIndex) internal
    {
        // Make sure that the bag exists
        require(_bagIndex < bags.length);
        
        // Bag may not be transferred before it has been bought x times
        require(bags[_bagIndex].amountOfBuys >= BAG_TRANSFER_MINIMUM_AMOUNT_OF_BUYS);
        
        // Make sure that the sender is the current owner of the bag
        require(bags[_bagIndex].owner == _from);
        
        // Set the new owner
        bags[_bagIndex].owner = _to;
        ownerToBagIndices[_from].remove(_bagIndex);
        ownerToBagIndices[_to].add(_bagIndex);
        
        // Trigger blockchain event
        Transfer(_from, _to, _bagIndex);
    }
    
    
    ////////////////////////////////////
    ////// VIEW FUNCTIONS FOR USER INTERFACE
    
    function amountOfBags() external view returns (uint256)
    {
        return bags.length;
    }
    function amountOfBagTypes() external view returns (uint256)
    {
        return bagTypes.length;
    }
    function amountOfParticipants() external view returns (uint256)
    {
        return allParticipants.length;
    }
    
    
    ////////////////////////////////////
    ////// ERC721 NON FUNGIBLE TOKEN INTERFACE
    
    event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
    
    function name() external pure returns (string)
    {
        return "Bags";
    }
    
    function symbol() external pure returns (string)
    {
        return "BAG";
    }
    
    function totalSupply() external view returns (uint256)
    {
        return bags.length;
    }
    
    function balanceOf(address _owner) external view returns (uint256)
    {
        return ownerToBagIndices[_owner].size();
    }
    
    function ownerOf(uint256 _bagIndex) external view returns (address)
    {
        require(_bagIndex < bags.length);
        
        return bags[_bagIndex].owner;
    }
    mapping(address => mapping(address => mapping(uint256 => bool))) private ownerToAddressToBagIndexAllowed;
    function approve(address _to, uint256 _bagIndex) external
    {
        require(_bagIndex < bags.length);
        
        require(msg.sender == bags[_bagIndex].owner);
        
        ownerToAddressToBagIndexAllowed[msg.sender][_to][_bagIndex] = true;
    }
    
    function takeOwnership(uint256 _bagIndex) external
    {
        require(_bagIndex < bags.length);
        
        address previousOwner = bags[_bagIndex].owner;
        
        require(ownerToAddressToBagIndexAllowed[previousOwner][msg.sender][_bagIndex] == true);
        
        ownerToAddressToBagIndexAllowed[previousOwner][msg.sender][_bagIndex] = false;
        
        _transferBag(previousOwner, msg.sender, _bagIndex);
    }
    
    function transfer(address _to, uint256 _bagIndex) external
    {
        transferBag(_to, _bagIndex);
    }
    
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256)
    {
        require(_index < ownerToBagIndices[_owner].size());
        
        return ownerToBagIndices[_owner].values[_index];
    }
}
////////////////////////////////////
////// CRYPTO SURPRISE
////// https://cryptosurprise.me
////////////////////////////////////
function _transferBag(address _from, address _to, uint256 _bagIndex) internal
{
    // Make sure that the bag exists
    require(_bagIndex < bags.length);
        
    // Bag may not be transferred before it has been bought x times
    require(bags[_bagIndex].amountOfBuys >= BAG_TRANSFER_MINIMUM_AMOUNT_OF_BUYS);
        
    // Make sure that the sender is the current owner of the bag
    require(bags[_bagIndex].owner == _from);
        
    // Set the new owner
    bags[_bagIndex].owner = _to;
    ownerToBagIndices[_from].remove(_bagIndex);
    ownerToBagIndices[_to].add(_bagIndex);
        
    // Trigger blockchain event
    Transfer(_from, _to, _bagIndex);
}

////////////////////////////////////
////// INTERNAL FUNCTIONS