contract Presale is PresaleInterface, PausableOperators, IERC1155TokenReceiver
{
    struct RewardToken1155
    {
        uint tokenId;
        uint count;
    }

    struct RewardNFT
    {
        uint128 nftKind;
        uint128 tokenIndex;
    }

    struct RewardCutie
    {
        uint genome;
        uint16 generation;
    }

    uint32 constant RATE_SIGN = 0;
    uint32 constant NATIVE = 1;

    struct Lot
    {
        RewardToken1155[] rewardsToken1155; // stackable
        uint128[] rewardsNftMint; // stackable
        RewardNFT[] rewardsNftFixed; // non stackable - one element per lot
        RewardCutie[] rewardsCutie; // stackable
        uint128 price;
        uint128 leftCount;
        uint128 priceMul;
        uint128 priceAdd;
        uint32 expireTime;
        uint32 lotKind;
        address priceInToken;
    }

    mapping (uint32 => Lot) public lots;

    mapping (address => uint) public referrers;

    BlockchainCutiesERC1155Interface public token1155;
    CutieGeneratorInterface public cutieGenerator;
    TokenRegistryInterface public tokenRegistry;
    address public signerAddress;

    event Bid(address indexed purchaser, uint32 indexed lotId, uint value, address indexed token);
    event BidReferrer(address indexed purchaser, uint32 indexed lotId, uint value, address token, address indexed referrer);
    event LotChange(uint32 indexed lotId);

    function setToken1155(BlockchainCutiesERC1155Interface _token1155) onlyOwner public
    {
        token1155 = _token1155;
    }

    function setCutieGenerator(CutieGeneratorInterface _cutieGenerator) onlyOwner public
    {
        cutieGenerator = _cutieGenerator;
    }

    function setTokenRegistry(TokenRegistryInterface _tokenRegistry) onlyOwner public
    {
        tokenRegistry = _tokenRegistry;
    }

    function setup(
        BlockchainCutiesERC1155Interface _token1155,
        CutieGeneratorInterface _cutieGenerator,
        TokenRegistryInterface _tokenRegistry) onlyOwner external
    {
        setToken1155(_token1155);
        setCutieGenerator(_cutieGenerator);
        setTokenRegistry(_tokenRegistry);
    }

    function setLot(uint32 lotId, uint128 price, uint128 count, uint32 expireTime, uint128 priceMul, uint128 priceAdd, uint32 lotKind) external onlyOperator
    {
        delete lots[lotId];
        Lot storage lot = lots[lotId];
        lot.price = price;
        lot.leftCount = count;
        lot.expireTime = expireTime;
        lot.priceMul = priceMul;
        lot.priceAdd = priceAdd;
        lot.lotKind = lotKind;
        lot.priceInToken = address(0x0);
        emit LotChange(lotId);
    }

    function setLotWithToken(uint32 lotId, uint128 price, uint128 count, uint32 expireTime, uint128 priceMul, uint128 priceAdd, uint32 lotKind, address priceInToken) external onlyOperator
    {
        delete lots[lotId];
        Lot storage lot = lots[lotId];
        lot.price = price;
        lot.leftCount = count;
        lot.expireTime = expireTime;
        lot.priceMul = priceMul;
        lot.priceAdd = priceAdd;
        lot.lotKind = lotKind;
        lot.priceInToken = priceInToken;
        emit LotChange(lotId);
    }

    function setLotLeftCount(uint32 lotId, uint128 count) external onlyOperator
    {
        Lot storage lot = lots[lotId];
        lot.leftCount = count;
        emit LotChange(lotId);
    }

    function setExpireTime(uint32 lotId, uint32 expireTime) external onlyOperator
    {
        Lot storage lot = lots[lotId];
        lot.expireTime = expireTime;
        emit LotChange(lotId);
    }

    function setPrice(uint32 lotId, uint128 price) external onlyOperator
    {
        lots[lotId].price = price;
        emit LotChange(lotId);
    }

    function deleteLot(uint32 lotId) external onlyOperator
    {
        delete lots[lotId];
        emit LotChange(lotId);
    }

    function addRewardToken1155(uint32 lotId, uint tokenId, uint count) external onlyOperator
    {
        lots[lotId].rewardsToken1155.push(RewardToken1155(tokenId, count));
        emit LotChange(lotId);
    }

    function setRewardToken1155(uint32 lotId, uint tokenId, uint count) external onlyOperator
    {
        delete lots[lotId].rewardsToken1155;
        lots[lotId].rewardsToken1155.push(RewardToken1155(tokenId, count));
        emit LotChange(lotId);
    }

    function setRewardNftFixed(uint32 lotId, uint128 nftType, uint128 tokenIndex) external onlyOperator
    {
        delete lots[lotId].rewardsNftFixed;
        lots[lotId].rewardsNftFixed.push(RewardNFT(nftType, tokenIndex));
        emit LotChange(lotId);
    }

    function addRewardNftFixed(uint32 lotId, uint128 nftType, uint128 tokenIndex) external onlyOperator
    {
        lots[lotId].rewardsNftFixed.push(RewardNFT(nftType, tokenIndex));
        emit LotChange(lotId);
    }

    function addRewardNftFixedBulk(uint32 lotId, uint128 nftType, uint128[] tokenIndex) external onlyOperator
    {
        for (uint i = 0; i < tokenIndex.length; i++)
        {
            lots[lotId].rewardsNftFixed.push(RewardNFT(nftType, tokenIndex[i]));
        }
        emit LotChange(lotId);
    }

    function addRewardNftMint(uint32 lotId, uint128 nftType) external onlyOperator
    {
        lots[lotId].rewardsNftMint.push(nftType);
        emit LotChange(lotId);
    }

    function setRewardNftMint(uint32 lotId, uint128 nftType) external onlyOperator
    {
        delete lots[lotId].rewardsNftMint;
        lots[lotId].rewardsNftMint.push(nftType);
        emit LotChange(lotId);
    }

    function addRewardCutie(uint32 lotId, uint genome, uint16 generation) external onlyOperator
    {
        lots[lotId].rewardsCutie.push(RewardCutie(genome, generation));
        emit LotChange(lotId);
    }

    function setRewardCutie(uint32 lotId, uint genome, uint16 generation) external onlyOperator
    {
        delete lots[lotId].rewardsCutie;
        lots[lotId].rewardsCutie.push(RewardCutie(genome, generation));
        emit LotChange(lotId);
    }

    function isAvailable(uint32 lotId) public view returns (bool)
    {
        Lot storage lot = lots[lotId];
        return
            lot.leftCount > 0 && lot.expireTime >= now;
    }

    function getLot(uint32 lotId) external view returns (
        uint256 price,
        uint256 left,
        uint256 expireTime,
        uint256 lotKind
    )
    {
        Lot storage p = lots[lotId];
        price = p.price;
        left = p.leftCount;
        expireTime = p.expireTime;
        lotKind = p.lotKind;
    }

    function getLot2(uint32 lotId) external view returns (
        uint256 price,
        uint256 left,
        uint256 expireTime,
        uint256 lotKind,
        address priceInToken
    )
    {
        Lot storage p = lots[lotId];
        price = p.price;
        left = p.leftCount;
        expireTime = p.expireTime;
        lotKind = p.lotKind;
        priceInToken = p.priceInToken;
    }

    function getLotRewards(uint32 lotId) external view returns (
            uint256[5] memory rewardsToken1155tokenId,
            uint256[5] memory rewardsToken1155count,
            uint256[5] memory rewardsNFTMintNftKind,
            uint256[5] memory rewardsNFTFixedKind,
            uint256[5] memory rewardsNFTFixedIndex,
            uint256[5] memory rewardsCutieGenome,
            uint256[5] memory rewardsCutieGeneration
        )
    {
        Lot storage p = lots[lotId];
        uint i;
        for (i = 0; i < p.rewardsToken1155.length; i++)
        {
            if (i >= 5) break;
            rewardsToken1155tokenId[i] = p.rewardsToken1155[i].tokenId;
            rewardsToken1155count[i] = p.rewardsToken1155[i].count;
        }
        for (i = 0; i < p.rewardsNftMint.length; i++)
        {
            if (i >= 5) break;
            rewardsNFTMintNftKind[i] = p.rewardsNftMint[i];
        }
        for (i = 0; i < p.rewardsNftFixed.length; i++)
        {
            if (i >= 5) break;
            rewardsNFTFixedKind[i] = p.rewardsNftFixed[i].nftKind;
            rewardsNFTFixedIndex[i] = p.rewardsNftFixed[i].tokenIndex;
        }
        for (i = 0; i < p.rewardsCutie.length; i++)
        {
            if (i >= 5) break;
            rewardsCutieGenome[i] = p.rewardsCutie[i].genome;
            rewardsCutieGeneration[i] = p.rewardsCutie[i].generation;
        }
    }

    function getLotNftFixedRewards(uint32 lotId) external view returns (
        uint256 rewardsNFTFixedKind,
        uint256 rewardsNFTFixedIndex
    )
    {
        Lot storage p = lots[lotId];

        if (p.rewardsNftFixed.length > 0)
        {
            rewardsNFTFixedKind = p.rewardsNftFixed[p.rewardsNftFixed.length-1].nftKind;
            rewardsNFTFixedIndex = p.rewardsNftFixed[p.rewardsNftFixed.length-1].tokenIndex;
        }
    }

    function getLotToken1155Rewards(uint32 lotId) external view returns (
        uint256[10] memory rewardsToken1155tokenId,
        uint256[10] memory rewardsToken1155count
    )
    {
        Lot storage p = lots[lotId];
        for (uint i = 0; i < p.rewardsToken1155.length; i++)
        {
            if (i >= 10) break;
            rewardsToken1155tokenId[i] = p.rewardsToken1155[i].tokenId;
            rewardsToken1155count[i] = p.rewardsToken1155[i].count;
        }
    }

    function getLotCutieRewards(uint32 lotId) external view returns (
        uint256[10] memory rewardsCutieGenome,
        uint256[10] memory rewardsCutieGeneration
    )
    {
        Lot storage p = lots[lotId];
        for (uint i = 0; i < p.rewardsCutie.length; i++)
        {
            if (i >= 10) break;
            rewardsCutieGenome[i] = p.rewardsCutie[i].genome;
            rewardsCutieGeneration[i] = p.rewardsCutie[i].generation;
        }
    }

    function getLotNftMintRewards(uint32 lotId) external view returns (
        uint256[10] memory rewardsNFTMintNftKind
    )
    {
        Lot storage p = lots[lotId];
        for (uint i = 0; i < p.rewardsNftMint.length; i++)
        {
            if (i >= 10) break;
            rewardsNFTMintNftKind[i] = p.rewardsNftMint[i];
        }
    }

    function getLotToken1155RewardByIndex(uint32 lotId, uint index) external view returns (
        uint256 rewardsToken1155tokenId,
        uint256 rewardsToken1155count
    )
    {
        Lot storage p = lots[lotId];
        rewardsToken1155tokenId = p.rewardsToken1155[index].tokenId;
        rewardsToken1155count = p.rewardsToken1155[index].count;
    }

    function getLotCutieRewardByIndex(uint32 lotId, uint index) external view returns (
        uint256 rewardsCutieGenome,
        uint256 rewardsCutieGeneration
    )
    {
        Lot storage p = lots[lotId];
        rewardsCutieGenome = p.rewardsCutie[index].genome;
        rewardsCutieGeneration = p.rewardsCutie[index].generation;
    }

    function getLotNftMintRewardByIndex(uint32 lotId, uint index) external view returns (
        uint256 rewardsNFTMintNftKind
    )
    {
        Lot storage p = lots[lotId];
        rewardsNFTMintNftKind = p.rewardsNftMint[index];
    }

    function getLotToken1155RewardCount(uint32 lotId) external view returns (uint)
    {
        return lots[lotId].rewardsToken1155.length;
    }
    function getLotCutieRewardCount(uint32 lotId) external view returns (uint)
    {
        return lots[lotId].rewardsCutie.length;
    }
    function getLotNftMintRewardCount(uint32 lotId) external view returns (uint)
    {
        return lots[lotId].rewardsNftMint.length;
    }

    function deleteRewards(uint32 lotId) external onlyOwner
    {
        delete lots[lotId].rewardsToken1155;
        delete lots[lotId].rewardsNftMint;
        delete lots[lotId].rewardsNftFixed;
        delete lots[lotId].rewardsCutie;
        emit LotChange(lotId);
    }

    function bidWithPlugin(uint32 lotId, address purchaser, uint valueForEvent, address tokenForEvent) external payable onlyOperator
    {
        _bid(lotId, purchaser, valueForEvent, tokenForEvent, address(0x0));
    }

    function bidWithPluginReferrer(uint32 lotId, address purchaser, uint valueForEvent, address tokenForEvent, address referrer) external payable onlyOperator
    {
        _bid(lotId, purchaser, valueForEvent, tokenForEvent, referrer);
    }

    function _bid(uint32 lotId, address purchaser, uint valueForEvent, address tokenForEvent, address referrer) internal whenNotPaused
    {
        Lot storage p = lots[lotId];
        require(isAvailable(lotId), "Lot is not available");

        if (referrer == address(0x0))
        {
            emit BidReferrer(purchaser, lotId, valueForEvent, tokenForEvent, referrer);
        }
        else
        {
            emit Bid(purchaser, lotId, valueForEvent, tokenForEvent);
        }

        p.leftCount--;
        p.price += uint128(uint256(p.price)*p.priceMul / 1000000);
        p.price += p.priceAdd;

        issueRewards(p, purchaser);

        if (referrers[referrer] > 0)
        {
            uint referrerValue = valueForEvent * referrers[referrer] / 100;
            referrer.transfer(referrerValue);
        }
    }

    function issueRewards(Lot storage p, address purchaser) internal
    {
        uint i;
        for (i = 0; i < p.rewardsToken1155.length; i++)
        {
            mintToken1155(purchaser, p.rewardsToken1155[i]);
        }
        if (p.rewardsNftFixed.length > 0)
        {
            transferNFT(purchaser, p.rewardsNftFixed[p.rewardsNftFixed.length-1]);
            p.rewardsNftFixed.length--;
        }
        for (i = 0; i < p.rewardsNftMint.length; i++)
        {
            mintNFT(purchaser, p.rewardsNftMint[i]);
        }
        for (i = 0; i < p.rewardsCutie.length; i++)
        {
            mintCutie(purchaser, p.rewardsCutie[i]);
        }
    }

    function mintToken1155(address purchaser, RewardToken1155 storage reward) internal
    {
        token1155.mintFungibleSingle(reward.tokenId, purchaser, reward.count);
    }

    function mintNFT(address purchaser, uint128 nftKind) internal
    {
        token1155.mintNonFungibleSingleShort(nftKind, purchaser);
    }

    function transferNFT(address purchaser, RewardNFT storage reward) internal
    {
        uint tokenId = (uint256(reward.nftKind) << 128) | (1 << 255) | reward.tokenIndex;
        token1155.safeTransferFrom(address(this), purchaser, tokenId, 1, "");
    }

    function mintCutie(address purchaser, RewardCutie storage reward) internal
    {
        cutieGenerator.generateSingle(reward.genome, reward.generation, purchaser);
    }

    function destroyContract() external onlyOwner {
        require(address(this).balance == 0);
        selfdestruct(msg.sender);
    }

    /// @dev Reject all Ether
    function() external payable {
        revert();
    }

    /// @dev The balance transfer to project owners
    function withdrawEthFromBalance(uint value) external onlyOwner
    {
        uint256 total = address(this).balance;
        if (total > value)
        {
            total = value;
        }

        msg.sender.transfer(total);
    }

    function bidNative(uint32 lotId, address referrer) external payable
    {
        Lot storage lot = lots[lotId];
        require(lot.price <= msg.value, "Not enough value provided");
        require(lot.lotKind == NATIVE, "Lot kind should be NATIVE");
        require(lot.priceInToken == address(0x0), "Price in token is not supported");

        _bid(lotId, msg.sender, msg.value, address(0x0), referrer);
    }

    // https://github.com/BitGuildPlatform/Documentation/blob/master/README.md#2-required-game-smart-contract-changes
    // Function that is called when trying to use Token for payments from approveAndCall
    function receiveApproval(address _sender, uint256, address _tokenContract, bytes _extraData) external
    {
        uint32 lotId = getLotId(_extraData);
        _bidNativeWithToken(lotId, address(0x0), ERC20(_tokenContract), _sender);
    }

    function getLotId(bytes _extraData) pure internal returns (uint32)
    {
        require(_extraData.length == 4); // 32 bits

        return
            uint32(_extraData[0]) +
            uint32(_extraData[1]) * 0x100 +
            uint32(_extraData[2]) * 0x10000 +
            uint32(_extraData[3]) * 0x100000;
    }

    function bidNativeWithToken(uint32 lotId, address referrer, ERC20 paymentToken) external
    {
        _bidNativeWithToken(lotId, referrer, paymentToken, msg.sender);
    }

    function _bidNativeWithToken(uint32 lotId, address referrer, ERC20 paymentToken, address sender) internal
    {
        Lot storage lot = lots[lotId];
        require(lot.lotKind == NATIVE, "Lot kind should be NATIVE");
        require(isTokenAllowed(paymentToken), "Token is not allowed");

        uint priceInTokens;

        // do not convert if price is specified in same token as payment token
        if (lot.priceInToken == address(paymentToken))
        {
            priceInTokens = lot.price;
        }
        // convert from one token to another
        else if (lot.priceInToken != address(0x0))
        {
            uint priceInWei = convertPriceFromTokensToWei(ERC20(lot.priceInToken), lot.price);
            priceInTokens = convertPriceFromWeiToTokens(paymentToken, priceInWei);
        }
        else // price in ETH
        {
            uint priceInWei2 = lot.price;
            priceInTokens = convertPriceFromWeiToTokens(paymentToken, priceInWei2);
        }
        require(paymentToken.transferFrom(sender, address(tokenRegistry), priceInTokens), "Can't request tokens");
        tokenRegistry.onTokensReceived(paymentToken, priceInTokens);

        _bid(lotId, sender, priceInTokens, address(paymentToken), referrer);
    }

    function bid(uint32 lotId, uint rate, uint expireAt, uint8 _v, bytes32 _r, bytes32 _s) external payable
    {
        bidReferrer(lotId, rate, expireAt, _v, _r, _s, address(0x0));
    }

    function bidWithToken(uint32 lotId, uint rate, uint expireAt, ERC20 paymentToken, uint8 _v, bytes32 _r, bytes32 _s, address referrer) external
    {
        Lot storage lot = lots[lotId];
        require(lot.lotKind == RATE_SIGN, "Lot kind should be RATE_SIGN");

        require(isValidSignature(rate, expireAt, _v, _r, _s), "Signature is not valid");
        require(expireAt >= now, "Rate sign is expired");
        require(isTokenAllowed(paymentToken), "Token is not allowed");

        uint priceInTokens;
        // do not convert if price is specified in same token as payment token
        if (lot.priceInToken == address(paymentToken))
        {
            priceInTokens = lot.price;
        }
        // convert from one token to another
        else if (lot.priceInToken != address(0x0))
        {
            uint priceInWei = rate * lot.price;
            priceInTokens = convertPriceFromWeiToTokens(paymentToken, priceInWei);
        }
        else // price in ETH
        {
            uint priceInWei2 = rate * lot.price;
            priceInTokens = convertPriceFromWeiToTokens(paymentToken, priceInWei2);
        }

        require(paymentToken.transferFrom(msg.sender, address(tokenRegistry), priceInTokens), "Can't request tokens");
        tokenRegistry.onTokensReceived(paymentToken, priceInTokens);

        _bid(lotId, msg.sender, priceInTokens, address(paymentToken), referrer);
    }

    function bidReferrer(uint32 lotId, uint rate, uint expireAt, uint8 _v, bytes32 _r, bytes32 _s, address referrer) public payable
    {
        Lot storage lot = lots[lotId];
        require(lot.lotKind == RATE_SIGN, "Lot kind should be RATE_SIGN");

        require(isValidSignature(rate, expireAt, _v, _r, _s));
        require(expireAt >= now, "Rate sign is expired");

        uint priceInWei = rate * lot.price;
        require(priceInWei <= msg.value, "Not enough value provided");

        _bid(lotId, msg.sender, priceInWei, address(0x0), referrer);
    }

    function setSigner(address _newSigner) public onlyOwner {
        signerAddress = _newSigner;
    }

    function isValidSignature(uint rate, uint expireAt, uint8 _v, bytes32 _r, bytes32 _s) public view returns (bool)
    {
        return getSigner(rate, expireAt, _v, _r, _s) == signerAddress;
    }

    function getSigner(uint rate, uint expireAt, uint8 _v, bytes32 _r, bytes32 _s) public pure returns (address)
    {
        bytes32 msgHash = hashArguments(rate, expireAt);
        return ecrecover(msgHash, _v, _r, _s);
    }

    /// @dev Common function to be used also in backend
    function hashArguments(uint rate, uint expireAt) public pure returns (bytes32 msgHash)
    {
        msgHash = keccak256(abi.encode(rate, expireAt));
    }

    function isERC1155TokenReceiver() external view returns (bytes4) {
        return bytes4(keccak256("isERC1155TokenReceiver()"));
    }

    function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external returns(bytes4)
    {
        return bytes4(keccak256("acrequcept_batch_erc1155_tokens()"));
    }

    function onERC1155Received(address, address, uint256, uint256, bytes) external returns(bytes4)
    {
        return bytes4(keccak256("accept_erc1155_tokens()"));
    }

    // 100 means 100%
    function setReferrer(address _address, uint _percent) external onlyOwner
    {
        require(_percent < 100);
        referrers[_address] = _percent;
    }

    function removeReferrer(address _address) external onlyOwner
    {
        delete referrers[_address];
    }

    function decreaseCount(uint32 lotId) external onlyOperator
    {
        Lot storage p = lots[lotId];
        if (p.leftCount > 0)
        {
            p.leftCount--;
        }

        emit LotChange(lotId);
    }

    function isTokenAllowed(ERC20 token) view public returns (bool)
    {
        address[] memory arg = new address[](1);
        arg[0] = address(token);
        return tokenRegistry.areAllTokensAllowed(arg);
    }

    function convertPriceFromWeiToTokens(ERC20 token, uint valueInWei) view public returns (uint)
    {
        return uint(tokenRegistry.getPriceInToken(token, uint128(valueInWei)));
    }

    function convertPriceFromTokensToWei(ERC20 token, uint valueInTokens) view public returns (uint)
    {
        return tokenRegistry.convertPriceFromTokensToWei(token, valueInTokens);
    }
}
/// @title BlockchainCuties Presale
/// @author https://BlockChainArchitect.io
function setReferrer(address _address, uint _percent) external onlyOwner
{
    require(_percent < 100);
    referrers[_address] = _percent;
}

// 100 means 100%