contract DungeonTraining is DungeonChallenge {

    /**
     * @dev The HeroTrained event is fired when user finished a training.
     */
    event HeroTrained(uint timestamp, address indexed playerAddress, uint indexed dungeonId, uint indexed heroId, uint heroGenes, uint floorNumber, uint floorGenes, bool success, uint newHeroGenes);


    /* ======== GAME SETTINGS ======== */

    /**
     * @dev The actual fee contribution required to call trainX() is calculated by this feeMultiplier,
     *  times the dungeon difficulty, times X. The payment is accumulated to the dungeon rewards,
     *  and a large proportion will be claimed by whoever successfully challenged the floor.
     *  1 finney = 0.001 ether
     */
    uint public trainingFeeMultiplier = 2 finney;

    /**
     * @dev The discounted training fee multiplier to be used in the preparation period.
     * 1000 szabo = 0.001 ether
     */
    uint public preparationPeriodTrainingFeeMultiplier = 1800 szabo;

    /**
     * @dev The actual fee contribution required to call trainEquipment() is calculated by this feeMultiplier,
     *  times the dungeon difficulty, times X. The payment is accumulated to the dungeon rewards,
     *  and a large proportion will be claimed by whoever successfully challenged the floor.
     *  (No preparation period discount on equipment training.)
     *  1000 szabo = 0.001 ether
     */
    uint public equipmentTrainingFeeMultiplier = 500 szabo;

    /**
     * @dev The external function to call when a hero train with a dungeon,
     *  it determines whether whether a training is successfully, and the resulting genes.
     *  Will generate a DungeonChallenged event.
     */
    function train1(uint _dungeonId, uint _heroId) whenNotPaused dungeonCanTrain(_dungeonId) heroAllowedToTrain(_heroId) external payable {
        _train(_dungeonId, _heroId, 0, 1);
    }

    function train2(uint _dungeonId, uint _heroId) whenNotPaused dungeonCanTrain(_dungeonId) heroAllowedToTrain(_heroId) external payable {
        _train(_dungeonId, _heroId, 0, 2);
    }

    function train3(uint _dungeonId, uint _heroId) whenNotPaused dungeonCanTrain(_dungeonId) heroAllowedToTrain(_heroId) external payable {
        _train(_dungeonId, _heroId, 0, 3);
    }

    /**
     * @dev The external function to call when a hero train a particular equipment with a dungeon,
     *  it determines whether whether a training is successfully, and the resulting genes.
     *  Will generate a DungeonChallenged event.
     *  _equipmentIndex is the index of equipment: 0 is train all attributes, including equipments and stats.
     *  1: weapon | 2: shield | 3: armor | 4: shoe | 5: helmet | 6: gloves | 7: belt | 8: shawl
     */
    function trainEquipment(uint _dungeonId, uint _heroId, uint _equipmentIndex) whenNotPaused dungeonCanTrain(_dungeonId) heroAllowedToTrain(_heroId) external payable {
        require(_equipmentIndex <= 8);

        _train(_dungeonId, _heroId, _equipmentIndex, 1);
    }

    /**
     * @dev An internal function of a hero train with dungeon,
     *  it determines whether whether a training is successfully, and the resulting genes.
     *  Will generate a DungeonChallenged event.
     */
    function _train(uint _dungeonId, uint _heroId, uint _equipmentIndex, uint _trainingTimes) private {
        // Get the dungeon details from the token contract.
        uint creationTime;
        uint difficulty;
        uint floorNumber;
        uint rewards;
        uint seedGenes;
        uint floorGenes;
        (creationTime,,difficulty,,floorNumber,,rewards,seedGenes,floorGenes) = dungeonTokenContract.dungeons(_dungeonId);

        // Check for _trainingTimes abnormality, we probably won't have any feature that train a hero 10 times with a single call.
        require(_trainingTimes < 10);

        // Checks for payment, any exceeding funds will be transferred back to the player.
        uint requiredFee;

        if (_equipmentIndex > 0) { // train specific equipments
            requiredFee = difficulty * equipmentTrainingFeeMultiplier * _trainingTimes;
        } else if (now < creationTime + dungeonPreparationTime) { // train all attributes, preparation period
            requiredFee = difficulty * preparationPeriodTrainingFeeMultiplier * _trainingTimes;
        } else { // train all attributes, normal period
            requiredFee = difficulty * trainingFeeMultiplier * _trainingTimes;
        }

        require(msg.value >= requiredFee);

        // Get the hero gene, or claim first hero.
        uint heroGenes;
        (_heroId, heroGenes) = _getHeroGenesOrClaimFirstHero(_heroId);

        // ** STORAGE UPDATE **
        // Increment the accumulated rewards for the dungeon.
        dungeonTokenContract.addDungeonRewards(_dungeonId, requiredFee);

        // Calculate any excess funds and make it available to be withdrawed by the player.
        asyncSend(msg.sender, msg.value - requiredFee);

        // Split the _train function into multiple parts because of stack too deep error.
        _trainPart2(_dungeonId, _heroId, heroGenes, _equipmentIndex, _trainingTimes);
    }

    /**
     * Split the _train function into multiple parts because of Stack Too Deep error.
     */
    function _trainPart2(uint _dungeonId, uint _heroId, uint _heroGenes, uint _equipmentIndex, uint _trainingTimes) private {
        // Get the dungeon details from the token contract.
        uint floorNumber;
        uint floorGenes;
        (,,,, floorNumber,,,, floorGenes) = dungeonTokenContract.dungeons(_dungeonId);

        // Determine if the hero training is successful or not, and the resulting genes.
        uint heroPower = _getHeroPower(_heroGenes, _dungeonId);

        uint newHeroGenes = _heroGenes;
        uint newHeroPower = heroPower;

        // Train the hero multiple times according to _trainingTimes,
        // each time if the resulting power is larger, update new hero power.
        for (uint i = 0; i < _trainingTimes; i++) {
            // Call the external closed source secret function that determines the resulting hero "genes".
            uint tmpHeroGenes = trainingScienceContract.mixGenes(newHeroGenes, floorGenes, _equipmentIndex);

            uint tmpHeroPower = _getHeroPower(tmpHeroGenes, _dungeonId);

            if (tmpHeroPower > newHeroPower) {
                newHeroGenes = tmpHeroGenes;
                newHeroPower = tmpHeroPower;
            }
        }

        // Prevent reduced power.
        if (newHeroPower > heroPower) {
            // ** STORAGE UPDATE **
            // Set the upgraded hero genes.
            heroTokenContract.setHeroGenes(_heroId, newHeroGenes);
        }

        // Emit the HeroTrained event.
        HeroTrained(now, msg.sender, _dungeonId, _heroId, _heroGenes, floorNumber, floorGenes, newHeroPower > heroPower, newHeroGenes);
    }


    /* ======== SETTER FUNCTIONS ======== */

    /// @dev Updates the fee contribution multiplier required for calling trainX().
    function setTrainingFeeMultiplier(uint _newTrainingFeeMultiplier) onlyOwner external {
        trainingFeeMultiplier = _newTrainingFeeMultiplier;
    }

    /// @dev Updates the fee contribution multiplier for preparation period required for calling trainX().
    function setPreparationPeriodTrainingFeeMultiplier(uint _newPreparationPeriodTrainingFeeMultiplier) onlyOwner external {
        preparationPeriodTrainingFeeMultiplier = _newPreparationPeriodTrainingFeeMultiplier;
    }

    /// @dev Updates the fee contribution multiplier required for calling trainEquipment().
    function setEquipmentTrainingFeeMultiplier(uint _newEquipmentTrainingFeeMultiplier) onlyOwner external {
        equipmentTrainingFeeMultiplier = _newEquipmentTrainingFeeMultiplier;
    }


    /* ======== MODIFIERS ======== */

    /**
     * @dev Throws if dungeon status do not allow training, also check for dungeon existence.
     *  Also check if the user is in the dungeon.
     */
    modifier dungeonCanTrain(uint _dungeonId) {
        require(_dungeonId < dungeonTokenContract.totalSupply());
        uint status;
        (,status,,,,,,,) = dungeonTokenContract.dungeons(_dungeonId);
        require(status == 0 || status == 3);

        // Also check if the user is in the dungeon.
        require(playerToDungeonID[msg.sender] == _dungeonId);
        _;
    }

    /**
     * @dev Throws if player does not own the hero.
     *  Unless the player does not have any hero yet, which will auto claim one during first challenge / train.
     */
    modifier heroAllowedToTrain(uint _heroId) {
        if (heroTokenContract.balanceOf(msg.sender) > 0) {
            // You can only train with your own hero.
            require(heroTokenContract.ownerOf(_heroId) == msg.sender);
        }
        _;
    }


}
function setPreparationPeriodTrainingFeeMultiplier(uint _newPreparationPeriodTrainingFeeMultiplier) onlyOwner external {
    preparationPeriodTrainingFeeMultiplier = _newPreparationPeriodTrainingFeeMultiplier;
}

/// @dev Updates the fee contribution multiplier for preparation period required for calling trainX().