contract GachaMachineAzuki is Ownable, ERC1155Holder {
    using SafeERC20 for IERC20;
    using SafeMath for uint256;
    using Address for address;
    using EnumerableSet for EnumerableSet.UintSet;
    using EnumerableSet for EnumerableSet.AddressSet;

    /**
     * @dev Game round
     */
    struct Round {
        uint256 id; // request id.
        address player; // address of player.
        RoundStatus status; // status of the round.
        uint256 times; // how many times of this round;
        uint256 totalTimes; // total time of an account.
        uint256[20] cards; // Prize card ot this round.
    }
    enum RoundStatus { Pending, Finished } // status of this round
    mapping(address => Round) public gameRounds;
    uint256 public currentRoundIdCount; //until now, the total round of this Gamemachine.
    uint256 public totalRoundCount;

    /***********************************
     * @dev Configuration of this GameMachine
     ***********************************/
    uint256 public machineId;
    string public machineTitle;
    string public machineDescription;
    string public machineUri;
    uint256 public machineType = 1;
    bool public maintaining = true;
    bool public banned = false;

    // This is a set which contains cradID
    EnumerableSet.UintSet private _cardsSet;
    // This mapping contains cardId => amount
    mapping(uint256 => uint256) public amountWithId;
    mapping(uint256 => uint256) public mintedCardAmountWithId;
    // Prize pool with a random number to cardId
    mapping(uint256 => uint256) private _prizePool;
    // The amount of cards in this machine.
    uint256 public cardAmount;

    uint256 private _salt;
    uint256 public shuffleCount = 10;

    uint256 public totalBurned = 0;

    /*******************************
     * somehting about token
     *******************************/
    uint256 public currencyBurnRate = 50;
    uint256 public forArtistRate = 40;
    // Currency of the game machine, like DOKI, AZUKI.
    IERC20 public currencyToken;
    // the price of playing one time.
    uint256 public playOncePrice;
    // ERC1155 Token
    IMomijiToken public momijiToken;

    EnumerableSet.AddressSet private _devAccountSet;

    address public administrator;

    event AddCardNotMinted(uint256 cardId, uint256 amount, uint256 cardAmount);
    event AddCardMinted(uint256 cardId, uint256 amount, uint256 cardAmount);
    event RemoveCard(uint256 card, uint256 removeAmount, uint256 cardAmount);
    event RunMachineSuccessfully(address account, uint256 times, uint256 playFee);
    event ChangePlayOncePrice(uint256 newPrice);
    event LockMachine(bool locked);
    event BanMachine(bool banned);

    constructor(uint256 _machineId,
                string memory _machineTitle,
                IMomijiToken _momijiToken,
                IERC20 _currencyToken) {
        machineId = _machineId;
        machineTitle = _machineTitle;
        machineDescription = _machineTitle;
        momijiToken = _momijiToken;
        currencyToken = _currencyToken;
        administrator = owner();
        _salt = uint256(keccak256(abi.encodePacked(_momijiToken, _currencyToken, block.timestamp))).mod(10000);
    }

    /**
     * @dev add cards which have not been minted into machine.
     * @param cardId. Card id you want to add.
     * @param amount. How many cards you want to add.
     */
    function addCardNotMintedWithAmount(uint256 cardId, uint256 amount) public onlyOwner unbanned {
        require((momijiToken.tokenQuantityWithId(cardId) + amount) <= momijiToken.tokenMaxQuantityWithId(cardId), "You add too much.");
        require(momijiToken.creators(cardId) == msg.sender, "You are not the creator of this Card.");
        _cardsSet.add(cardId);
        for (uint256 i = 0; i < amount; i ++) {
            _prizePool[cardAmount + i] = cardId;
        }
        amountWithId[cardId] = amountWithId[cardId].add(amount);
        momijiToken.removeMintManuallyQuantity(cardId, amount);
        cardAmount = cardAmount.add(amount);
        emit AddCardNotMinted(cardId, amount, cardAmount);
    }

    /**
     * @dev Add cards which have been minted, and your owned cards
     * @param cardId. Card id you want to add.
     * @param amount. How many cards you want to add.
     */
    function addCardMintedWithAmount(uint256 cardId, uint256 amount) public onlyOwner unbanned {
        require(momijiToken.balanceOf(msg.sender, cardId) >= amount, "You don't have enough Cards");
        momijiToken.safeTransferFrom(msg.sender, address(this), cardId, amount, "Add Card");
        _cardsSet.add(cardId);
        mintedCardAmountWithId[cardId] = mintedCardAmountWithId[cardId].add(amount);
        amountWithId[cardId] = amountWithId[cardId].add(amount);
        for (uint256 i = 0; i < amount; i ++) {
            _prizePool[cardAmount + i] = cardId;
        }
        cardAmount = cardAmount.add(amount);
        emit AddCardMinted(cardId, amount, cardAmount);
    }

    function runMachine(uint256 userProvidedSeed, uint256 times) public onlyHuman unbanned {
        require(!maintaining, "This machine is under maintenance");
        require(!banned, "This machine is banned.");
        require(cardAmount > 0, "There is no card in this machine anymore.");
        require(times > 0, "Times can not be 0");
        require(times <= 20, "Over times.");
        require(times <= cardAmount, "You play too many times.");
        _createARound(times);
        // get random seed with userProvidedSeed and address of sender.
        uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, msg.sender)));
        _shufflePrizePool(seed);

        for (uint256 i = 0; i < times; i ++) {
            // get randomResult with seed and salt, then mod cardAmount.
            uint256 randomResult = _getRandomNumebr(seed, _salt, cardAmount);
            // update random salt.
            _salt = ((randomResult + cardAmount + _salt) * (i + 1) * block.timestamp).mod(cardAmount) + 1;
            // transfer the cards.
            uint256 result = (randomResult * _salt).mod(cardAmount);
            _updateRound(result, i);
        }

        totalRoundCount = totalRoundCount.add(times);
        uint256 playFee = playOncePrice.mul(times);
        _transferAndBurnToken(playFee);
        _distributePrize();

        emit RunMachineSuccessfully(msg.sender, times, playFee);
    }

    /**
     * @param amount how much token will be needed and will be burned.
     */
    function _transferAndBurnToken(uint256 amount) private {
        _burnCurrencyTokenBalance();
        // 1. burn token
        uint256 burnAmount = amount.mul(currencyBurnRate).div(100);
        // 2. transfer token from use to this machine
        currencyToken.transferFrom(msg.sender, address(this), burnAmount);

        uint256 forArtistAmount = amount.mul(forArtistRate).div(100);
        currencyToken.transferFrom(msg.sender, owner(), burnAmount);

        // 3. tansfer token remaining to dev account.
        uint256 remainingAmount = amount.sub(burnAmount).sub(forArtistAmount);
        uint256 devAccountAmount = _devAccountSet.length();
        uint256 transferAmount = remainingAmount.div(devAccountAmount);

        for (uint256 i = 0; i < devAccountAmount; i ++) {
            address toAddress = _devAccountSet.at(i);
            currencyToken.safeTransferFrom(msg.sender, toAddress, transferAmount);
        }
    }

    function _distributePrize() private {
        for (uint i = 0; i < gameRounds[msg.sender].times; i ++) {
            uint256 cardId = gameRounds[msg.sender].cards[i];
            require(amountWithId[cardId] > 0, "No enough cards of this kind in the Mchine.");
            require(_calculateLastQuantityWithId(cardId) > 0, "Can not mint more cards of this kind.");

            if (mintedCardAmountWithId[cardId] > 0) {
                momijiToken.safeTransferFrom(address(this), msg.sender, cardId, 1, '');
                mintedCardAmountWithId[cardId] = mintedCardAmountWithId[cardId].sub(1);
            } else {
                momijiToken.mint(cardId, msg.sender, 1, "Minted by Gacha Machine.");
            }

            amountWithId[cardId] = amountWithId[cardId].sub(1);
            if (amountWithId[cardId] == 0) {
                _removeCardId(cardId);
            }
        }
        gameRounds[msg.sender].status = RoundStatus.Finished;
    }

    function _updateRound(uint256 randomResult, uint256 rand) private {
        uint256 cardId = _prizePool[randomResult];
        _prizePool[randomResult] = _prizePool[cardAmount - 1];
        cardAmount = cardAmount.sub(1);
        gameRounds[msg.sender].cards[rand] = cardId;
    }

    function _getRandomNumebr(uint256 seed, uint256 salt, uint256 mod) view private returns(uint256) {
        return uint256(keccak256(abi.encode(block.timestamp, block.difficulty, block.coinbase, block.gaslimit, seed, block.number))).mod(mod).add(seed).add(salt);
    }

    function _calculateLastQuantityWithId(uint256 cardId) view private returns(uint256) {
        return momijiToken.tokenMaxQuantityWithId(cardId)
                          .sub(momijiToken.tokenQuantityWithId(cardId));
    }

    function _createARound(uint256 times) private {
        gameRounds[msg.sender].id = currentRoundIdCount + 1;
        gameRounds[msg.sender].player = msg.sender;
        gameRounds[msg.sender].status = RoundStatus.Pending;
        gameRounds[msg.sender].times = times;
        gameRounds[msg.sender].totalTimes = gameRounds[msg.sender].totalTimes.add(times);
        currentRoundIdCount = currentRoundIdCount.add(1);
    }

    function _burnCurrencyTokenBalance() private {
        uint256 balance = currencyToken.balanceOf(address(this));
        if (balance > 0) {
            currencyToken.burn(balance);
            totalBurned = totalBurned.add(balance);
        }
    }

    // shuffle the prize pool again.
    function _shufflePrizePool(uint256 seed) private {
        for (uint256 i = 0; i < shuffleCount; i++) {
            uint256 randomResult = _getRandomNumebr(seed, _salt, cardAmount);
            _salt = ((randomResult + cardAmount + _salt) * (i + 1) * block.timestamp).mod(cardAmount);
            _swapPrize(i, _salt);
        }
    }

    function _swapPrize(uint256 a, uint256 b) private {
        uint256 temp = _prizePool[a];
        _prizePool[a] = _prizePool[b];
        _prizePool[b] = temp;
    }

    function _removeCardId(uint256 _cardId) private {
        _cardsSet.remove(_cardId);
    }

    function cardIdCount() view public returns(uint256) {
        return _cardsSet.length();
    }

    function cardIdWithIndex(uint256 index) view public returns(uint256) {
        return _cardsSet.at(index);
    }

    function changeShuffleCount(uint256 _shuffleCount) public onlyAdministrator {
        shuffleCount = _shuffleCount;
    }

    function changePlayOncePrice(uint256 newPrice) public onlyOwner {
        playOncePrice = newPrice;
        emit ChangePlayOncePrice(newPrice);
    }

    function getCardId(address account, uint256 at) view public returns(uint256) {
        return gameRounds[account].cards[at];
    }

    function unlockMachine() public onlyOwner {
        maintaining = false;
        emit LockMachine(false);
    }

    function lockMachine() public onlyOwner {
        maintaining = true;
        emit LockMachine(true);
    }

    function addDevAccount(address payable account) public onlyAdministrator {
        _devAccountSet.add(account);
    }

    function removeDevAccount(address payable account) public onlyAdministrator {
        _devAccountSet.remove(account);
    }

    function getDevAccount(uint256 index) public returns(address) {
        return _devAccountSet.at(index);
    }

    function devAccountLength() view public returns(uint256) {
        return _devAccountSet.length();
    }

    function changeBurnRate(uint256 rate) public onlyAdministrator {
        currencyBurnRate = rate;
    }

    function changeForArtistRate(uint256 rate) public onlyAdministrator {
        forArtistRate = rate;
    }

    function transferAdministrator(address account) public onlyAdministrator {
        require(account != address(0), "Ownable: new owner is the zero address");
        administrator = account;
    }

    function transferOwnership(address newOwner) public override onlyAdministrator {
        super.transferOwnership(newOwner);
    }

    function banThisMachine() public onlyAdministrator {
        banned = true;
    }

    function unbanThisMachine() public onlyAdministrator {
        banned = false;
    }

    function changeMachineTitle(string memory title) public onlyOwner {
        machineTitle = title;
    }

    function changeMachineDescription(string memory description) public onlyOwner {
        machineDescription = description;
    }

    function changeMachineUri(string memory newUri) public onlyOwner {
        machineUri = newUri;
    }

    function abandonMachine() public {
        require(msg.sender == administrator || msg.sender == owner(), "Only for administrator.");
        maintaining = true;
        banned = true;

        for (uint256 i = 0; i < cardIdCount(); i ++) {
            uint256 cardId = cardIdWithIndex(i);
            if (mintedCardAmountWithId[cardId] > 0) {
                momijiToken.safeTransferFrom(address(this), owner(), cardId, mintedCardAmountWithId[cardId], "Reset Machine");
                amountWithId[cardId] = amountWithId[cardId].sub(mintedCardAmountWithId[cardId]);
                cardAmount = cardAmount.sub(mintedCardAmountWithId[cardId]);
                mintedCardAmountWithId[cardId] = 0;
            }
            cardAmount = cardAmount.sub(amountWithId[cardId]);
            momijiToken.addMintManuallyQuantity(cardId, amountWithId[cardId]);
            amountWithId[cardId] = 0;
        }
    }

    /**
     * Modifiers
     */
    modifier onlyHuman() {
        require(!address(msg.sender).isContract() && tx.origin == msg.sender, "Only for human.");
        _;
    }

    modifier onlyAdministrator() {
        require(address(msg.sender) == administrator, "Only for administrator.");
        _;
    }

    modifier unbanned() {
        require(!banned, "This machine is banned.");
        _;
    }
}
/**
 Author: DokiDoki Dev: Kaki
 Machine with AZUKI. Machine version 1
 */
function addCardNotMintedWithAmount(uint256 cardId, uint256 amount) public onlyOwner unbanned {
        require((momijiToken.tokenQuantityWithId(cardId) + amount) <= momijiToken.tokenMaxQuantityWithId(cardId), "You add too much.");
        require(momijiToken.creators(cardId) == msg.sender, "You are not the creator of this Card.");
        _cardsSet.add(cardId);
        for (uint256 i = 0; i < amount; i ++) {
            _prizePool[cardAmount + i] = cardId;
        }
        amountWithId[cardId] = amountWithId[cardId].add(amount);
        momijiToken.removeMintManuallyQuantity(cardId, amount);
        cardAmount = cardAmount.add(amount);
        emit AddCardNotMinted(cardId, amount, cardAmount);
    }
/**
 * @dev add cards which have not been minted into machine.
 * @param cardId. Card id you want to add.
 * @param amount. How many cards you want to add.
 */