contract SkinMix is SkinBase {

    // Mix formula
    MixFormulaInterface public mixFormula;


    // Pre-paid ether for synthesization, will be returned to user if the synthesization failed (minus gas).
    uint256 public prePaidFee = 150000 * 5000000000; // (15w gas * 5 gwei)

    // Events
    event MixStart(address account, uint256 skinAId, uint256 skinBId);
    event AutoMix(address account, uint256 skinAId, uint256 skinBId, uint64 cooldownEndTime);
    event MixSuccess(address account, uint256 skinId, uint256 skinAId, uint256 skinBId);

    // Set mix formula contract address 
    function setMixFormulaAddress(address mixFormulaAddress) external onlyCOO {
        mixFormula = MixFormulaInterface(mixFormulaAddress);
    }

    // setPrePaidFee: set advance amount, only owner can call this
    function setPrePaidFee(uint256 newPrePaidFee) external onlyCOO {
        prePaidFee = newPrePaidFee;
    }

    // _isCooldownReady: check whether cooldown period has been passed
    function _isCooldownReady(uint256 skinAId, uint256 skinBId) private view returns (bool) {
        return (skins[skinAId].cooldownEndTime <= uint64(now)) && (skins[skinBId].cooldownEndTime <= uint64(now));
    }

    // _isNotMixing: check whether two skins are in another mixing process
    function _isNotMixing(uint256 skinAId, uint256 skinBId) private view returns (bool) {
        return (skins[skinAId].mixingWithId == 0) && (skins[skinBId].mixingWithId == 0);
    }

    // _setCooldownTime: set new cooldown time
    function _setCooldownEndTime(uint256 skinAId, uint256 skinBId) private {
        uint256 end = now + 5 minutes;
        // uint256 end = now;
        skins[skinAId].cooldownEndTime = uint64(end);
        skins[skinBId].cooldownEndTime = uint64(end);
    }

    // _isValidSkin: whether an account can mix using these skins
    // Make sure two things:
    // 1. these two skins do exist
    // 2. this account owns these skins
    function _isValidSkin(address account, uint256 skinAId, uint256 skinBId) private view returns (bool) {
        // Make sure those two skins belongs to this account
        if (skinAId == skinBId) {
            return false;
        }
        if ((skinAId == 0) || (skinBId == 0)) {
            return false;
        }
        if ((skinAId >= nextSkinId) || (skinBId >= nextSkinId)) {
            return false;
        }
        return (skinIdToOwner[skinAId] == account) && (skinIdToOwner[skinBId] == account);
    }

    // _isNotOnSale: whether a skin is not on sale
    function _isNotOnSale(uint256 skinId) private view returns (bool) {
        return (isOnSale[skinId] == false);
    }

    // mix  
    function mix(uint256 skinAId, uint256 skinBId) public whenNotPaused {

        // Check whether skins are valid
        require(_isValidSkin(msg.sender, skinAId, skinBId));

        // Check whether skins are neither on sale
        require(_isNotOnSale(skinAId) && _isNotOnSale(skinBId));

        // Check cooldown
        require(_isCooldownReady(skinAId, skinBId));

        // Check these skins are not in another process
        require(_isNotMixing(skinAId, skinBId));

        // Set new cooldown time
        _setCooldownEndTime(skinAId, skinBId);

        // Mark skins as in mixing
        skins[skinAId].mixingWithId = uint64(skinBId);
        skins[skinBId].mixingWithId = uint64(skinAId);

        // Emit MixStart event
        MixStart(msg.sender, skinAId, skinBId);
    }

    // Mixing auto
    function mixAuto(uint256 skinAId, uint256 skinBId) public payable whenNotPaused {
        require(msg.value >= prePaidFee);

        mix(skinAId, skinBId);

        Skin storage skin = skins[skinAId];

        AutoMix(msg.sender, skinAId, skinBId, skin.cooldownEndTime);
    }

    // Get mixing result, return the resulted skin id
    function getMixingResult(uint256 skinAId, uint256 skinBId) public whenNotPaused {
        // Check these two skins belongs to the same account
        address account = skinIdToOwner[skinAId];
        require(account == skinIdToOwner[skinBId]);

        // Check these two skins are in the same mixing process
        Skin storage skinA = skins[skinAId];
        Skin storage skinB = skins[skinBId];
        require(skinA.mixingWithId == uint64(skinBId));
        require(skinB.mixingWithId == uint64(skinAId));

        // Check cooldown
        require(_isCooldownReady(skinAId, skinBId));

        // Create new skin
        uint128 newSkinAppearance = mixFormula.calcNewSkinAppearance(skinA.appearance, skinB.appearance);
        Skin memory newSkin = Skin({appearance: newSkinAppearance, cooldownEndTime: uint64(now), mixingWithId: 0});
        skins[nextSkinId] = newSkin;
        skinIdToOwner[nextSkinId] = account;
        isOnSale[nextSkinId] = false;
        nextSkinId++;

        // Clear old skins
        skinA.mixingWithId = 0;
        skinB.mixingWithId = 0;

        // In order to distinguish created skins in minting with destroyed skins
        // skinIdToOwner[skinAId] = owner;
        // skinIdToOwner[skinBId] = owner;
        delete skinIdToOwner[skinAId];
        delete skinIdToOwner[skinBId];
        // require(numSkinOfAccounts[account] >= 2);
        numSkinOfAccounts[account] -= 1;

        MixSuccess(account, nextSkinId - 1, skinAId, skinBId);
    }
}
function getMixingResult(uint256 skinAId, uint256 skinBId) public whenNotPaused {
    // Check these two skins belongs to the same account
    address account = skinIdToOwner[skinAId];
    require(account == skinIdToOwner[skinBId]);

    // Check these two skins are in the same mixing process
    Skin storage skinA = skins[skinAId];
    Skin storage skinB = skins[skinBId];
    require(skinA.mixingWithId == uint64(skinBId));
    require(skinB.mixingWithId == uint64(skinAId));

    // Check cooldown
    require(_isCooldownReady(skinAId, skinBId));

    // Create new skin
    uint128 newSkinAppearance = mixFormula.calcNewSkinAppearance(skinA.appearance, skinB.appearance);
    Skin memory newSkin = Skin({appearance: newSkinAppearance, cooldownEndTime: uint64(now), mixingWithId: 0});
    skins[nextSkinId] = newSkin;
    skinIdToOwner[nextSkinId] = account;
    isOnSale[nextSkinId] = false;
    nextSkinId++;

    // Clear old skins
    skinA.mixingWithId = 0;
    skinB.mixingWithId = 0;

    // In order to distinguish created skins in minting with destroyed skins
    // skinIdToOwner[skinAId] = owner;
    // skinIdToOwner[skinBId] = owner;
    delete skinIdToOwner[skinAId];
    delete skinIdToOwner[skinBId];
    // require(numSkinOfAccounts[account] >= 2);
    numSkinOfAccounts[account] -= 1;

    MixSuccess(account, nextSkinId - 1, skinAId, skinBId);
}

// Get mixing result, return the resulted skin id