contract Controller is Inherit, Strings {

    using SafeMath for uint; 

    struct Champ {
        uint id; //same as position in Champ[]
        uint attackPower;
        uint defencePower;
        uint cooldownTime; //how long does it take to be ready attack again
        uint readyTime; //if is smaller than block.timestamp champ is ready to fight
        uint winCount;
        uint lossCount;
        uint position; //position in leaderboard. subtract 1 and you got position in leaderboard[]
        uint price; //selling price
        uint withdrawCooldown; //if you one of the 800 best champs and withdrawCooldown is less as block.timestamp then you get ETH reward
        uint eq_sword; 
        uint eq_shield; 
        uint eq_helmet; 
        bool forSale; //is champ for sale?
    }

    struct Item {
        uint id;
        uint8 itemType; // 1 - Sword | 2 - Shield | 3 - Helmet
        uint8 itemRarity; // 1 - Common | 2 - Uncommon | 3 - Rare | 4 - Epic | 5 - Legendery | 6 - Forged
        uint attackPower;
        uint defencePower;
        uint cooldownReduction;
        uint price;
        uint onChampId; //can not be used to decide if item is on champ, because champ's id can be 0, 'bool onChamp' solves it.
        bool onChamp; 
        bool forSale; //is item for sale?
    }

    EC champsEC;
    EC itemsEC;
     
    /// @notice People are allowed to withdraw only if min. balance (0.01 gwei) is reached
    modifier contractMinBalanceReached(){
        uint pendingWithdrawal = core.pendingWithdrawal();
        require( (address(core).balance).sub(pendingWithdrawal) > 1000000 );
        _;
    }
    
    modifier onlyApprovedOrOwnerOfToken(uint _id, address _msgsender, bool _isTokenChamp) 
    {
        require(core.onlyApprovedOrOwnerOfToken(_id, _msgsender, _isTokenChamp));
        _;
    }
    

    /// @notice Gets champ's reward in wei
    function getChampReward(uint _position) public view returns(uint) 
    {
        if(_position <= 800){
            //percentageMultipier = 10,000
            //maxReward = 2000 = .2% * percentageMultipier
            //subtractPerPosition = 2 = .0002% * percentageMultipier
            //2000 - (2 * (_position - 1))
            uint rewardPercentage = uint(2000).sub(2 * (_position - 1));

            //available funds are all funds - already pending
            uint availableWithdrawal = address(coreAddress).balance.sub(core.pendingWithdrawal());

            //calculate reward for champ's position
            //1000000 = percentageMultipier * 100
            return availableWithdrawal / 1000000 * rewardPercentage;
        }else{
            return uint(0);
        }
    }

    function setChampEC(address _address) public onlyOwner {
        champsEC = EC(_address);
    }


    function setItemsEC(address _address) public onlyOwner {
        itemsEC = EC(_address);
    }

    function changeChampsName(uint _champId, string _name, address _msgsender) external 
    onlyApprovedOrOwnerOfToken(_champId, _msgsender, true)
    onlyCore
    {
        core.setChampsName(_champId, _name);
    }

    /// @dev Move champ reward to pending withdrawal to his wallet. 
    function withdrawChamp(uint _id, address _msgsender) external 
    onlyApprovedOrOwnerOfToken(_id, _msgsender, true) 
    contractMinBalanceReached  
    onlyCore 
    {
        Champ memory champ = _getChamp(_id);
        require(champ.position <= 800); 
        require(champ.withdrawCooldown < block.timestamp); //isChampWithdrawReady

        champ.withdrawCooldown = block.timestamp + 1 days; //one withdrawal 1 per day
        _updateChamp(champ); //update core storage

        core.addWithdrawal(_msgsender, getChampReward(champ.position));
    }
    

    /// @dev Is called from from Attack function after the winner is already chosen. Updates abilities, champ's stats and swaps positions.
    function _attackCompleted(Champ memory _winnerChamp, Champ memory _defeatedChamp, uint _pointsGiven) private 
    {
        /*
         * Updates abilities after fight
         */
        //winner abilities update
        _winnerChamp.attackPower += _pointsGiven; //increase attack power
        _winnerChamp.defencePower += _pointsGiven; //max point that was given - already given to AP
                
        //defeated champ's abilities update
        //checks for not cross minimal AP & DP points
        //_defeatedChamp.attackPower = _subAttack(_defeatedChamp.attackPower, _pointsGiven); //decrease attack power
        _defeatedChamp.attackPower = (_defeatedChamp.attackPower <= _pointsGiven + 2) ? 2 : _defeatedChamp.attackPower - _pointsGiven; //Subtracts ability points. Helps to not cross minimal attack ability points -> 2

        //_defeatedChamp.defencePower = _subDefence(_defeatedChamp.defencePower, _pointsGiven); // decrease defence power
        _defeatedChamp.defencePower = (_defeatedChamp.defencePower <= _pointsGiven) ? 1 : _defeatedChamp.defencePower - _pointsGiven; //Subtracts ability points. Helps to not cross minimal defence ability points -> 1


        /*
         * Update champs' wins and losses
         */
        _winnerChamp.winCount++;
        _defeatedChamp.lossCount++;
            

        /*
         * Swap positions
         */
        if(_winnerChamp.position > _defeatedChamp.position) { //require loser to has better (lower) postion than attacker
            uint winnerPosition = _winnerChamp.position;
            uint loserPosition = _defeatedChamp.position;
        
            _defeatedChamp.position = winnerPosition;
            _winnerChamp.position = loserPosition;
        }

        _updateChamp(_winnerChamp);
        _updateChamp(_defeatedChamp);
    }


    /*
     * External
     */
    function attack(uint _champId, uint _targetId, address _msgsender) external 
    onlyApprovedOrOwnerOfToken(_champId, _msgsender, true) 
    onlyCore 
    {
        Champ memory myChamp = _getChamp(_champId); 
        Champ memory enemyChamp = _getChamp(_targetId); 
        
        require (myChamp.readyTime <= block.timestamp); /// Is champ ready to fight again?
        require(_champId != _targetId); /// Prevents from self-attack
        require(core.tokenToOwner(true, _targetId) != address(0)); /// Checks if champ does exist
    
        uint pointsGiven; //total points that will be divided between AP and DP
        uint myChampAttackPower;  
        uint enemyChampDefencePower; 
        uint myChampCooldownReduction;
        
        (myChampAttackPower,,myChampCooldownReduction) = core.getChampStats(_champId);
        (,enemyChampDefencePower,) = core.getChampStats(_targetId);


        //if attacker's AP is more than target's DP then attacker wins
        if (myChampAttackPower > enemyChampDefencePower) {
            
            //this should demotivate players from farming on way weaker champs than they are
            //the bigger difference between AP & DP is, the reward is smaller
            if(myChampAttackPower - enemyChampDefencePower < 5){
                pointsGiven = 6; //big experience - 6 ability points
            }else if(myChampAttackPower - enemyChampDefencePower < 10){
                pointsGiven = 4; //medium experience - 4 ability points
            }else{
                pointsGiven = 2; //small experience - 2 ability point to random ability (attack power or defence power)
            }
            
            _attackCompleted(myChamp, enemyChamp, pointsGiven/2);

        } else {
            
            //1 ability point to random ability (attack power or defence power)
            pointsGiven = 2;

            _attackCompleted(enemyChamp, myChamp, pointsGiven/2);
             
        }
        
        //Trigger cooldown for attacker
        myChamp.readyTime = uint(block.timestamp + myChamp.cooldownTime - myChampCooldownReduction);

        _updateChamp(myChamp);

    }

     function _cancelChampSale(Champ memory _champ) private 
     {
        //cancel champ's sale
        //no need waste gas to overwrite his price.
        _champ.forSale = false;

        /*
        uint champsForSaleCount = core.champsForSaleCount() - 1;
        core.setTokensForSaleCount(champsForSaleCount, true);
        */

        _updateChamp(_champ);
     }
     

    function _transferChamp(address _from, address _to, uint _champId) private onlyCore
    {
        Champ memory champ = _getChamp(_champId);

        //ifChampForSaleThenCancelSale
        if(champ.forSale){
             _cancelChampSale(champ);
        }

        core.clearTokenApproval(_from, _champId, true);

        //transfer champ
        (,uint toChampsCount,,) = core.addressInfo(_to); 
        (,uint fromChampsCount,,) = core.addressInfo(_from);

        core.updateAddressInfo(_to,0,false,toChampsCount + 1,true,0,false,"",false);
        core.updateAddressInfo(_from,0,false,fromChampsCount - 1,true,0,false,"",false);

        core.setTokenToOwner(_champId, _to, true);

        champsEC.emitTransfer(_from,_to,_champId);

        //transfer items
        if(champ.eq_sword != 0) { _transferItem(_from, _to, champ.eq_sword); }
        if(champ.eq_shield != 0) { _transferItem(_from, _to, champ.eq_shield); }
        if(champ.eq_helmet != 0) { _transferItem(_from, _to, champ.eq_helmet); }
    }


    function transferToken(address _from, address _to, uint _id, bool _isTokenChamp) external
    onlyCore{
        if(_isTokenChamp){
            _transferChamp(_from, _to, _id);
        }else{
            _transferItem(_from, _to, _id);
        }
    }

    function cancelTokenSale(uint _id, address _msgsender, bool _isTokenChamp) public 
      onlyApprovedOrOwnerOfToken(_id, _msgsender, _isTokenChamp)
      onlyCore 
    {
        if(_isTokenChamp){
            Champ memory champ = _getChamp(_id);
            require(champ.forSale); //champIsForSale
            _cancelChampSale(champ);
        }else{
            Item memory item = _getItem(_id);
          require(item.forSale);
           _cancelItemSale(item);
        }
    }

    /// @dev Address _from is msg.sender
    function giveToken(address _to, uint _id, address _msgsender, bool _isTokenChamp) external 
      onlyApprovedOrOwnerOfToken(_id, _msgsender, _isTokenChamp)
      onlyCore 
    {
        if(_isTokenChamp){
            _transferChamp(core.tokenToOwner(true,_id), _to, _id);
        }else{
             _transferItem(core.tokenToOwner(false,_id), _to, _id);
        }
    }


    function setTokenForSale(uint _id, uint _price, address _msgsender, bool _isTokenChamp) external 
      onlyApprovedOrOwnerOfToken(_id, _msgsender, _isTokenChamp) 
      onlyCore 
    {
        if(_isTokenChamp){
            Champ memory champ = _getChamp(_id);
            require(champ.forSale == false); //champIsNotForSale
            champ.forSale = true;
            champ.price = _price;
            _updateChamp(champ);
            
            /*
            uint champsForSaleCount = core.champsForSaleCount() + 1;
            core.setTokensForSaleCount(champsForSaleCount,true);
            */
        }else{
            Item memory item = _getItem(_id);
            require(item.forSale == false);
            item.forSale = true;
            item.price = _price;
            _updateItem(item);
            
            /*
            uint itemsForSaleCount = core.itemsForSaleCount() + 1;
            core.setTokensForSaleCount(itemsForSaleCount,false);
            */
        }

    }

    function _updateChamp(Champ memory champ) private 
    {
        core.updateChamp(champ.id, champ.attackPower, champ.defencePower, champ.cooldownTime, champ.readyTime, champ.winCount, champ.lossCount, champ.position, champ.price, champ.withdrawCooldown, champ.eq_sword, champ.eq_shield, champ.eq_helmet, champ.forSale);
    }

    function _updateItem(Item memory item) private
    {
        core.updateItem(item.id, item.itemType, item.itemRarity, item.attackPower, item.defencePower, item.cooldownReduction,item.price, item.onChampId, item.onChamp, item.forSale);
    }
    
    function _getChamp(uint _champId) private view returns (Champ)
    {
        Champ memory champ;
        
        //CompilerError: Stack too deep, try removing local variables.
        (champ.id, champ.attackPower, champ.defencePower, champ.cooldownTime, champ.readyTime, champ.winCount, champ.lossCount, champ.position,,,,,,) = core.champs(_champId);
        (,,,,,,,,champ.price, champ.withdrawCooldown, champ.eq_sword, champ.eq_shield, champ.eq_helmet, champ.forSale) = core.champs(_champId);
        
        return champ;
    }
    
    function _getItem(uint _itemId) private view returns (Item)
    {
        Item memory item;
        
        //CompilerError: Stack too deep, try removing local variables.
        (item.id, item.itemType, item.itemRarity, item.attackPower, item.defencePower, item.cooldownReduction,,,,) = core.items(_itemId);
        (,,,,,,item.price, item.onChampId, item.onChamp, item.forSale) = core.items(_itemId);
        
        return item;
    }

    function getTokenURIs(uint _id, bool _isTokenChamp) public pure returns(string)
    {
        if(_isTokenChamp){
            return strConcat('https://mccapi.patrikmojzis.com/champ.php?id=', uint2str(_id));
        }else{
            return strConcat('https://mccapi.patrikmojzis.com/item.php?id=', uint2str(_id));
        }
    }


    function _takeOffItem(uint _champId, uint8 _type) private
    {
        uint itemId;
        Champ memory champ = _getChamp(_champId);
        if(_type == 1){
            itemId = champ.eq_sword; //Get item ID
            if (itemId > 0) { //0 = nothing
                champ.eq_sword = 0; //take off sword
            }
        }
        if(_type == 2){
            itemId = champ.eq_shield; //Get item ID
            if(itemId > 0) {//0 = nothing
                champ.eq_shield = 0; //take off shield
            }
        }
        if(_type == 3){
            itemId = champ.eq_helmet; //Get item ID
            if(itemId > 0) { //0 = nothing
                champ.eq_helmet = 0; //take off 
            }
        }
        if(itemId > 0){
            Item memory item = _getItem(itemId);
            item.onChamp = false;
            _updateItem(item);
        }
    }

    function takeOffItem(uint _champId, uint8 _type, address _msgsender) public 
    onlyApprovedOrOwnerOfToken(_champId, _msgsender, true) 
    onlyCore
    {
            _takeOffItem(_champId, _type);
    }

    function putOn(uint _champId, uint _itemId, address _msgsender) external 
        onlyApprovedOrOwnerOfToken(_champId, _msgsender, true) 
        onlyApprovedOrOwnerOfToken(_itemId, _msgsender, false) 
        onlyCore 
        {
            Champ memory champ = _getChamp(_champId);
            Item memory item = _getItem(_itemId);

            //checks if items is on some other champ
            if(item.onChamp){
                _takeOffItem(item.onChampId, item.itemType); //take off from champ
            }

            item.onChamp = true; //item is on champ
            item.onChampId = _champId; //champ's id

            //put on
            if(item.itemType == 1){
                //take off actual sword 
                if(champ.eq_sword > 0){
                    _takeOffItem(champ.id, 1);
                }
                champ.eq_sword = _itemId; //put on sword
            }
            if(item.itemType == 2){
                //take off actual shield 
                if(champ.eq_shield > 0){
                    _takeOffItem(champ.id, 2);
                }
                champ.eq_shield = _itemId; //put on shield
            }
            if(item.itemType == 3){
                //take off actual helmet 
                if(champ.eq_helmet > 0){
                    _takeOffItem(champ.id, 3);
                }
                champ.eq_helmet = _itemId; //put on helmet
            }

            _updateChamp(champ);
            _updateItem(item);
    }


    function _cancelItemSale(Item memory item) private {
      //No need to overwrite item's price
      item.forSale = false;
      
      /*
      uint itemsForSaleCount = core.itemsForSaleCount() - 1;
      core.setTokensForSaleCount(itemsForSaleCount, false);
      */

      _updateItem(item);
    }

    function _transferItem(address _from, address _to, uint _itemID) private 
    {
        Item memory item = _getItem(_itemID);

        if(item.forSale){
              _cancelItemSale(item);
        }

        //take off      
        if(item.onChamp && _to != core.tokenToOwner(true, item.onChampId)){
          _takeOffItem(item.onChampId, item.itemType);
        }

        core.clearTokenApproval(_from, _itemID, false);

        //transfer item
        (,,uint toItemsCount,) = core.addressInfo(_to);
        (,,uint fromItemsCount,) = core.addressInfo(_from);

        core.updateAddressInfo(_to,0,false,0,false,toItemsCount + 1,true,"",false);
        core.updateAddressInfo(_from,0,false,0,false,fromItemsCount - 1,true,"",false);
        
        core.setTokenToOwner(_itemID, _to,false);

        itemsEC.emitTransfer(_from,_to,_itemID);
    }

    function forgeItems(uint _parentItemID, uint _childItemID, address _msgsender) external 
    onlyApprovedOrOwnerOfToken(_parentItemID, _msgsender, false) 
    onlyApprovedOrOwnerOfToken(_childItemID, _msgsender, false) 
    onlyCore
    {
        //checks if items are not the same
        require(_parentItemID != _childItemID);
        
        Item memory parentItem = _getItem(_parentItemID);
        Item memory childItem = _getItem(_childItemID);
        
        //if Item For Sale Then Cancel Sale
        if(parentItem.forSale){
          _cancelItemSale(parentItem);
        }
        
        //if Item For Sale Then Cancel Sale
        if(childItem.forSale){
          _cancelItemSale(childItem);
        }

        //take child item off, because child item will be burned
        if(childItem.onChamp){
            _takeOffItem(childItem.onChampId, childItem.itemType);
        }

        //update parent item
        parentItem.attackPower = (parentItem.attackPower > childItem.attackPower) ? parentItem.attackPower : childItem.attackPower;
        parentItem.defencePower = (parentItem.defencePower > childItem.defencePower) ? parentItem.defencePower : childItem.defencePower;
        parentItem.cooldownReduction = (parentItem.cooldownReduction > childItem.cooldownReduction) ? parentItem.cooldownReduction : childItem.cooldownReduction;
        parentItem.itemRarity = uint8(6);

        _updateItem(parentItem);

        //burn child item
        _transferItem(core.tokenToOwner(false,_childItemID), address(0), _childItemID);

    }


}
//author Patrik Mojzis
function _attackCompleted(Champ memory _winnerChamp, Champ memory _defeatedChamp, uint _pointsGiven) private 
{
    /*
     * Updates abilities after fight
     */
    //winner abilities update
    _winnerChamp.attackPower += _pointsGiven; //increase attack power
    _winnerChamp.defencePower += _pointsGiven; //max point that was given - already given to AP
                
    //defeated champ's abilities update
    //checks for not cross minimal AP & DP points
    //_defeatedChamp.attackPower = _subAttack(_defeatedChamp.attackPower, _pointsGiven); //decrease attack power
    _defeatedChamp.attackPower = (_defeatedChamp.attackPower <= _pointsGiven + 2) ? 2 : _defeatedChamp.attackPower - _pointsGiven; //Subtracts ability points. Helps to not cross minimal attack ability points -> 2

    //_defeatedChamp.defencePower = _subDefence(_defeatedChamp.defencePower, _pointsGiven); // decrease defence power
    _defeatedChamp.defencePower = (_defeatedChamp.defencePower <= _pointsGiven) ? 1 : _defeatedChamp.defencePower - _pointsGiven; //Subtracts ability points. Helps to not cross minimal defence ability points -> 1


    /*
     * Update champs' wins and losses
     */
    _winnerChamp.winCount++;
    _defeatedChamp.lossCount++;
            

    /*
     * Swap positions
     */
    if(_winnerChamp.position > _defeatedChamp.position) { //require loser to has better (lower) postion than attacker
        uint winnerPosition = _winnerChamp.position;
        uint loserPosition = _defeatedChamp.position;
        
        _defeatedChamp.position = winnerPosition;
        _winnerChamp.position = loserPosition;
    }

    _updateChamp(_winnerChamp);
    _updateChamp(_defeatedChamp);
}

/// @dev Is called from from Attack function after the winner is already chosen. Updates abilities, champ's stats and swaps positions.