contract CardRedeemerV2 is Ownable {
    using SafeMath for uint256;

    RMU public ropeMaker;
    Hope public hope;

    struct Pack {
        uint256[] cardIdList;
        uint256 price;
        uint256 redeemed;
    }

    struct Card {
        uint256 maxSupply;
        uint256 supply;
    }

    struct PackCardData {
        uint256 cardId;
        uint256 cardsLeft;
    }

    uint256[] public packIdList;
    mapping (uint256 => Pack) public packs;
    mapping (uint256 => Card) public cards;

    event Redeemed(address indexed _user, uint256 indexed _packId, uint256 indexed _cardId);

    constructor(RMU _ropeMakerAddr, Hope _hopeAddr) public {
        ropeMaker = _ropeMakerAddr;
        hope = _hopeAddr;
    }

    modifier onlyEOA() {
        require(msg.sender == tx.origin, "Not eoa");
        _;
    }

    /////
    /////
    /////

    // Returns the list of cardIds which are part of a pack
    function getCardIdListOfPack(uint256 _packId) external view returns(uint256[] memory) {
        return packs[_packId].cardIdList;
    }

    function _getPackCardData(uint256 _packId) internal view returns(PackCardData[] memory) {
        uint256[] memory _cardIdList = packs[_packId].cardIdList;
        PackCardData[] memory result = new PackCardData[](_cardIdList.length);

        for (uint256 i = 0; i < _cardIdList.length; ++i) {
            uint256 _cardId = _cardIdList[i];
            uint256 maxSupply = cards[_cardId].maxSupply;
            uint256 supply = cards[_cardId].supply;
            uint256 cardsLeft = maxSupply.sub(supply);
            result[i] = PackCardData(_cardId, cardsLeft);
        }

        return result;
    }

    // Returns amount of packs left
    function _getPacksLeft(PackCardData[] memory _data) internal pure returns(uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < _data.length; ++i) {
            total = total.add(_data[i].cardsLeft);
        }

        return total;
    }

    /////////
    /////////
    /////////

    // Returns amount of packs left
    function getPacksLeft(uint256 _packId) public view returns(uint256) {
        uint256[] memory _cardIdList = packs[_packId].cardIdList;

        uint256 total = 0;
        for (uint256 i = 0; i < _cardIdList.length; ++i) {
            uint256 _cardId = _cardIdList[i];
            uint256 cardsLeft = ropeMaker.maxSupply(_cardId).sub(ropeMaker.totalSupply(_cardId));

            total = total.add(cardsLeft);
        }

        return total;
    }

    // Returns probability to get each card (Value must be divided by 1e5)
    function getPackProbabilities(uint256 _packId) public view returns(uint256[] memory) {
        uint256 packsLeft = getPacksLeft(_packId);
        uint256[] memory _cardIdList = packs[_packId].cardIdList;

        uint256[] memory proba = new uint256[](_cardIdList.length);
        for (uint256 i = 0; i < _cardIdList.length; ++i) {
            uint256 _cardId = _cardIdList[i];
            uint256 cardsLeft = ropeMaker.maxSupply(_cardId).sub(ropeMaker.totalSupply(_cardId));

            proba[i] = cardsLeft.mul(1e5).div(packsLeft);
        }

        return proba;
    }

    function getTotalRedeemed() public view returns(uint256) {
        uint256 totalRedeemed = 0;

        for (uint256 i = 0; i < packIdList.length; ++i) {
            totalRedeemed = totalRedeemed.add(packs[packIdList[i]].redeemed);
        }

        return totalRedeemed;
    }

    function getCardsLeft(uint256 _packId) external view returns(uint256[] memory) {
        uint256[] memory _cardIdList = packs[_packId].cardIdList;
        uint256[] memory result = new uint256[](_cardIdList.length);

        for (uint256 i = 0; i < _cardIdList.length; ++i) {
            uint256 _cardId = _cardIdList[i];
            uint256 cardsLeft = ropeMaker.maxSupply(_cardId).sub(ropeMaker.totalSupply(_cardId));
            result[i] = cardsLeft;
        }

        return result;
    }

    /////
    /////
    /////

    function addPack(uint256 _packId, uint256[] memory _cardIdList, uint256 _price) public onlyOwner {
        require(_cardIdList.length > 0, "CardIdList cannot be empty");
        require(_price > 0, "Price cannot be 0");

        updateCardsData(_cardIdList);

        if (_isInArray(_packId, packIdList) == false) {
            packIdList.push(_packId);
        }

        packs[_packId] = Pack(_cardIdList, _price, 0);
    }

    // We need to call this function, if we ever mint manually some cards included in a pack
    function updateCardsData(uint256[] memory _cardIdList) public onlyOwner {
        for (uint256 i = 0; i < _cardIdList.length; ++i) {
            uint256 _cardId = _cardIdList[i];
            cards[_cardId] = Card(ropeMaker.maxSupply(_cardId), ropeMaker.totalSupply(_cardId));
        }
    }

    function removePack(uint256 _packId) public onlyOwner {
        delete packs[_packId].cardIdList;
        packs[_packId].price = 0;
    }

    //

    // Redeem a random card from a pack (Not callable by contract, to prevent exploits on RNG)
    function redeem(uint256 _packId) public onlyEOA {
        Pack storage pack = packs[_packId];
        require(pack.price > 0, "Pack does not exist");
        require(hope.balanceOf(msg.sender) >= pack.price, "Not enough hope for pack");

        PackCardData[] memory data = _getPackCardData(_packId);

        uint256 packsLeft = _getPacksLeft(data);
        require(packsLeft > 0, "Pack sold out");

        hope.burn(msg.sender, pack.price);
        pack.redeemed = pack.redeemed.add(1);

        uint256 rng = _rng(getTotalRedeemed()) % packsLeft;


        uint256 cardIdWon = 0;
        uint256 cumul = 0;
        for (uint256 i = 0; i < data.length; ++i) {
            uint256 cardId = data[i].cardId;

            cumul = cumul.add(data[i].cardsLeft);
            if (rng < cumul) {
                cardIdWon = cardId;
                cards[cardId].supply = cards[cardId].supply.add(1);
                break;
            }
        }

        require(cardIdWon != 0, "Error during card redeeming RNG");

        ropeMaker.mint(msg.sender, cardIdWon, 1, "");
        emit Redeemed(msg.sender, _packId, cardIdWon);
    }

    //////////////
    // Internal //
    //////////////

    // Utility function to check if a value is inside an array
    function _isInArray(uint256 _value, uint256[] memory _array) internal pure returns(bool) {
        uint256 length = _array.length;
        for (uint256 i = 0; i < length; ++i) {
            if (_array[i] == _value) {
                return true;
            }
        }

        return false;
    }

    // This is a pseudo random function, but considering the fact that redeem function is not callable by contract,
    // and the fact that Hope is not transferable, this should be enough to protect us from an attack
    // I would only expect a miner to be able to exploit this, and the attack cost would not be worth it in our case
    function _rng(uint256 _seed) internal view returns(uint256) {
        return uint256(keccak256(abi.encodePacked((block.timestamp).add(_seed).add
        (block.difficulty).add
        ((uint256(keccak256(abi.encodePacked(block.coinbase)))) /
            block.timestamp).add
        (block.gaslimit).add
        ((uint256(keccak256(abi.encodePacked(msg.sender)))) /
            block.timestamp).add
            (block.number)
            )));
    }
}
function _rng(uint256 _seed) internal view returns(uint256) {
    return uint256(keccak256(abi.encodePacked((block.timestamp).add(_seed).add
    (block.difficulty).add
    ((uint256(keccak256(abi.encodePacked(block.coinbase)))) /
        block.timestamp).add
    (block.gaslimit).add
    ((uint256(keccak256(abi.encodePacked(msg.sender)))) /
        block.timestamp).add
        (block.number)
        )));
}

// This is a pseudo random function, but considering the fact that redeem function is not callable by contract,
// and the fact that Hope is not transferable, this should be enough to protect us from an attack
// I would only expect a miner to be able to exploit this, and the attack cost would not be worth it in our case