contract EtheremonTrade is EtheremonEnum, BasicAccessControl, SafeMath {
    
    uint8 constant public GEN0_NO = 24;

    struct MonsterClassAcc {
        uint32 classId;
        uint256 price;
        uint256 returnPrice;
        uint32 total;
        bool catchable;
    }

    struct MonsterObjAcc {
        uint64 monsterId;
        uint32 classId;
        address trainer;
        string name;
        uint32 exp;
        uint32 createIndex;
        uint32 lastClaimIndex;
        uint createTime;
    }
    
    // Gen0 has return price & no longer can be caught when this contract is deployed
    struct Gen0Config {
        uint32 classId;
        uint256 originalPrice;
        uint256 returnPrice;
        uint32 total; // total caught (not count those from eggs)
    }
    
    struct BorrowItem {
        uint index;
        address owner;
        address borrower;
        uint price;
        bool lent;
        uint releaseTime;
        uint createTime;
    }
    
    // data contract
    address public dataContract;
    address public battleContract;
    address public tradingMonDataContract;
    address public monsterNFTContract;
    
    mapping(uint32 => Gen0Config) public gen0Config;
    
    // trading fee
    uint16 public tradingFeePercentage = 3;
    
    // event
    event EventPlaceSellOrder(address indexed seller, uint objId, uint price);
    event EventRemoveSellOrder(address indexed seller, uint objId);
    event EventCompleteSellOrder(address indexed seller, address indexed buyer, uint objId, uint price);
    event EventOfferBorrowingItem(address indexed lender, uint objId, uint price, uint releaseTime);
    event EventRemoveOfferBorrowingItem(address indexed lender, uint objId);
    event EventAcceptBorrowItem(address indexed lender, address indexed borrower, uint objId, uint price);
    event EventGetBackItem(address indexed lender, address indexed borrower, uint objId);
    
    // constructor
    function EtheremonTrade(address _dataContract, address _battleContract, address _tradingMonDataContract, address _monsterNFTContract) public {
        dataContract = _dataContract;
        battleContract = _battleContract;
        tradingMonDataContract = _tradingMonDataContract;
        monsterNFTContract = _monsterNFTContract;
    }
    
     // admin & moderators
    function setOriginalPriceGen0() onlyModerators public {
        gen0Config[1] = Gen0Config(1, 0.3 ether, 0.003 ether, 374);
        gen0Config[2] = Gen0Config(2, 0.3 ether, 0.003 ether, 408);
        gen0Config[3] = Gen0Config(3, 0.3 ether, 0.003 ether, 373);
        gen0Config[4] = Gen0Config(4, 0.2 ether, 0.002 ether, 437);
        gen0Config[5] = Gen0Config(5, 0.1 ether, 0.001 ether, 497);
        gen0Config[6] = Gen0Config(6, 0.3 ether, 0.003 ether, 380); 
        gen0Config[7] = Gen0Config(7, 0.2 ether, 0.002 ether, 345);
        gen0Config[8] = Gen0Config(8, 0.1 ether, 0.001 ether, 518); 
        gen0Config[9] = Gen0Config(9, 0.1 ether, 0.001 ether, 447);
        gen0Config[10] = Gen0Config(10, 0.2 ether, 0.002 ether, 380); 
        gen0Config[11] = Gen0Config(11, 0.2 ether, 0.002 ether, 354);
        gen0Config[12] = Gen0Config(12, 0.2 ether, 0.002 ether, 346);
        gen0Config[13] = Gen0Config(13, 0.2 ether, 0.002 ether, 351); 
        gen0Config[14] = Gen0Config(14, 0.2 ether, 0.002 ether, 338);
        gen0Config[15] = Gen0Config(15, 0.2 ether, 0.002 ether, 341);
        gen0Config[16] = Gen0Config(16, 0.35 ether, 0.0035 ether, 384);
        gen0Config[17] = Gen0Config(17, 1 ether, 0.01 ether, 305); 
        gen0Config[18] = Gen0Config(18, 0.1 ether, 0.001 ether, 427);
        gen0Config[19] = Gen0Config(19, 1 ether, 0.01 ether, 304);
        gen0Config[20] = Gen0Config(20, 0.4 ether, 0.05 ether, 82);
        gen0Config[21] = Gen0Config(21, 1, 1, 123);
        gen0Config[22] = Gen0Config(22, 0.2 ether, 0.001 ether, 468);
        gen0Config[23] = Gen0Config(23, 0.5 ether, 0.0025 ether, 302);
        gen0Config[24] = Gen0Config(24, 1 ether, 0.005 ether, 195);
    }
    
    function setContract(address _dataContract, address _battleContract, address _tradingMonDataContract, address _monsterNFTContract) onlyModerators public {
        dataContract = _dataContract;
        battleContract = _battleContract;
        tradingMonDataContract = _tradingMonDataContract;
        monsterNFTContract = _monsterNFTContract;
    }
    
    function updateConfig(uint16 _fee) onlyModerators public {
        tradingFeePercentage = _fee;
    }
    
    function withdrawEther(address _sendTo, uint _amount) onlyModerators public {
        // no user money is kept in this contract, only trasaction fee
        if (_amount > this.balance) {
            revert();
        }
        _sendTo.transfer(_amount);
    }

    function _triggerNFTEvent(address _from, address _to, uint _objId) internal {
        EtheremonMonsterNFTInterface monsterNFT = EtheremonMonsterNFTInterface(monsterNFTContract);
        monsterNFT.triggerTransferEvent(_from, _to, _objId);
    }
    
    // public
    function placeSellOrder(uint _objId, uint _price) isActive external {
        if (_price == 0)
            revert();
        
        // not on borrowing
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        if (monTradeData.isOnBorrow(_objId))
            revert();

        // not on battle 
        EtheremonBattleInterface battle = EtheremonBattleInterface(battleContract);
        if (battle.isOnBattle(uint64(_objId)))
            revert();
        
        // check ownership
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        MonsterObjAcc memory obj;
        (obj.monsterId, obj.classId, obj.trainer, obj.exp, obj.createIndex, obj.lastClaimIndex, obj.createTime) = data.getMonsterObj(uint64(_objId));

        if (obj.trainer != msg.sender) {
            revert();
        }
        
        monTradeData.addSellingItem(_objId, _price, block.timestamp);
        EventPlaceSellOrder(msg.sender, _objId, _price);
    }
    
    function removeSellOrder(uint _objId) isActive external {
        // check ownership
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        MonsterObjAcc memory obj;
        (obj.monsterId, obj.classId, obj.trainer, obj.exp, obj.createIndex, obj.lastClaimIndex, obj.createTime) = data.getMonsterObj(uint64(_objId));

        if (obj.trainer != msg.sender) {
            revert();
        }
        
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        monTradeData.removeSellingItem(_objId);
        
        EventRemoveSellOrder(msg.sender, _objId);
    }
    
    function buyItem(uint _objId) isActive external payable {
        // check item is valid to sell
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        uint requestPrice = monTradeData.getSellPrice(_objId);
        if (requestPrice == 0 || msg.value != requestPrice) {
            revert();
        }

        // check obj
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        MonsterObjAcc memory obj;
        (obj.monsterId, obj.classId, obj.trainer, obj.exp, obj.createIndex, obj.lastClaimIndex, obj.createTime) = data.getMonsterObj(uint64(_objId));
        // can not buy from yourself
        if (obj.monsterId == 0 || obj.trainer == msg.sender) {
            revert();
        }
        
        EtheremonMonsterNFTInterface monsterNFT = EtheremonMonsterNFTInterface(monsterNFTContract);

        uint fee = requestPrice * tradingFeePercentage / 100;
        monTradeData.removeSellingItem(_objId);
        
        // transfer owner
        data.removeMonsterIdMapping(obj.trainer, obj.monsterId);
        data.addMonsterIdMapping(msg.sender, obj.monsterId);
        monsterNFT.triggerTransferEvent(obj.trainer, msg.sender, _objId);
        
        // transfer money
        obj.trainer.transfer(safeSubtract(requestPrice, fee));
        
        EventCompleteSellOrder(obj.trainer, msg.sender, _objId, requestPrice);
    }
    
    function offerBorrowingItem(uint _objId, uint _price, uint _releaseTime) isActive external {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        if (monTradeData.isOnSell(_objId) || monTradeData.isOnLent(_objId)) revert();

         // check ownership
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        MonsterObjAcc memory obj;
        (obj.monsterId, obj.classId, obj.trainer, obj.exp, obj.createIndex, obj.lastClaimIndex, obj.createTime) = data.getMonsterObj(uint64(_objId));

        if (obj.trainer != msg.sender) {
            revert();
        }

        // not on battle 
        EtheremonBattleInterface battle = EtheremonBattleInterface(battleContract);
        if (battle.isOnBattle(obj.monsterId))
            revert();
        
        monTradeData.addBorrowingItem(msg.sender, _objId, _price, address(0), false, _releaseTime, block.timestamp);
        EventOfferBorrowingItem(msg.sender, _objId, _price, _releaseTime);
    }
    
    function removeBorrowingOfferItem(uint _objId) isActive external {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        address owner;
        bool lent;
        (owner, lent) = monTradeData.getBorrowBasicInfo(_objId);
        if (owner != msg.sender || lent == true)
            revert();
        
        monTradeData.removeBorrowingItem(_objId);
        EventRemoveOfferBorrowingItem(msg.sender, _objId);
    }
    
    function borrowItem(uint _objId) isActive external payable {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        BorrowItem memory borrowItem;
        (borrowItem.index, borrowItem.owner, borrowItem.borrower, borrowItem.price, borrowItem.lent, borrowItem.createTime, borrowItem.releaseTime) = monTradeData.getBorrowInfo(_objId);
        if (borrowItem.index == 0 || borrowItem.lent == true) revert();
        if (borrowItem.owner == msg.sender) revert(); // can not borrow from yourself
        if (borrowItem.price != msg.value)
            revert();

        // check obj
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        MonsterObjAcc memory obj;
        (obj.monsterId, obj.classId, obj.trainer, obj.exp, obj.createIndex, obj.lastClaimIndex, obj.createTime) = data.getMonsterObj(uint64(_objId));
        if (obj.trainer != borrowItem.owner) {
            revert();
        }
        
        // update borrow data
        monTradeData.addBorrowingItem(borrowItem.owner, _objId, borrowItem.price, msg.sender, true, (borrowItem.releaseTime + block.timestamp), borrowItem.createTime);
        
        data.removeMonsterIdMapping(obj.trainer, obj.monsterId);
        data.addMonsterIdMapping(msg.sender, obj.monsterId);
        _triggerNFTEvent(obj.trainer, msg.sender, _objId);
        
        obj.trainer.transfer(safeSubtract(borrowItem.price, borrowItem.price * tradingFeePercentage / 100));
        monTradeData.addItemLendingList(obj.trainer, _objId);
        EventAcceptBorrowItem(obj.trainer, msg.sender, _objId, borrowItem.price);
    }
    
    function getBackLendingItem(uint64 _objId) isActive external {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        BorrowItem memory borrowItem;
        (borrowItem.index, borrowItem.owner, borrowItem.borrower, borrowItem.price, borrowItem.lent, borrowItem.createTime, borrowItem.releaseTime) = monTradeData.getBorrowInfo(_objId);
        
        if (borrowItem.index == 0)
            revert();
        if (borrowItem.lent == false)
            revert();
        if (borrowItem.releaseTime > block.timestamp)
            revert();
        
        if (msg.sender != borrowItem.owner)
            revert();
        
        monTradeData.removeBorrowingItem(_objId);
        
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        data.removeMonsterIdMapping(borrowItem.borrower, _objId);
        data.addMonsterIdMapping(msg.sender, _objId);
        EtheremonMonsterNFTInterface monsterNFT = EtheremonMonsterNFTInterface(monsterNFTContract);
        monsterNFT.triggerTransferEvent(borrowItem.borrower, msg.sender, _objId);
        
        monTradeData.removeItemLendingList(msg.sender, _objId);
        EventGetBackItem(msg.sender, borrowItem.borrower, _objId);
    }
    
    // read access
    function getObjInfoWithBp(uint64 _objId) constant public returns(address owner, uint32 classId, uint32 exp, uint32 createIndex, uint bp) {
        EtheremonDataBase data = EtheremonDataBase(dataContract);
        MonsterObjAcc memory obj;
        (obj.monsterId, classId, owner, exp, createIndex, obj.lastClaimIndex, obj.createTime) = data.getMonsterObj(_objId);
        EtheremonMonsterNFTInterface monsterNFT = EtheremonMonsterNFTInterface(monsterNFTContract);
        bp = monsterNFT.getMonsterCP(_objId);
    }
    
    function getTotalSellingMonsters() constant external returns(uint) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        return monTradeData.getTotalSellingItem();
    }
    
    function getTotalBorrowingMonsters() constant external returns(uint) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        return monTradeData.getTotalBorrowingItem();
    }

    function getSellingItem(uint _index) constant external returns(uint objId, uint32 classId, uint32 exp, uint bp, address trainer, uint32 createIndex, uint256 price, uint createTime) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        (objId, price, createTime) = monTradeData.getSellingInfoByIndex(_index);
        if (objId > 0) {
            (trainer, classId, exp, createIndex, bp) = getObjInfoWithBp(uint64(objId));
        }
    }
    
    function getSellingItemByObjId(uint64 _objId) constant external returns(uint32 classId, uint32 exp, uint bp, address trainer, uint32 createIndex, uint256 price, uint createTime) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        uint index;
        (index, price, createTime) = monTradeData.getSellInfo(_objId);
        if (price > 0) {
            (trainer, classId, exp, createIndex, bp) = getObjInfoWithBp(_objId);
        }
    }

    function getBorrowingItem(uint _index) constant external returns(uint objId, address owner, address borrower, 
        uint256 price, bool lent, uint createTime, uint releaseTime) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);    
        (objId, owner, borrower, price, lent, createTime, releaseTime) = monTradeData.getBorrowInfoByIndex(_index);
    }
    
    function getBorrowingItemByObjId(uint64 _objId) constant external returns(uint index, address owner, address borrower, 
        uint256 price, bool lent, uint createTime, uint releaseTime) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);    
        (index, owner, borrower, price, lent, createTime, releaseTime) = monTradeData.getBorrowInfo(_objId);
    }
    
    
    function getLendingItemLength(address _trainer) constant external returns(uint) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);    
        return monTradeData.getTotalLendingItem(_trainer);
    }
    
    function getLendingItemInfo(address _trainer, uint _index) constant external returns(uint objId, address owner, address borrower, 
        uint256 price, bool lent, uint createTime, uint releaseTime) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        (objId, owner, borrower, price, lent, createTime, releaseTime) = monTradeData.getLendingInfo(_trainer, _index);
    }
    
    function getTradingInfo(uint _objId) constant external returns(address owner, address borrower, uint256 sellingPrice, uint256 lendingPrice, bool lent, uint releaseTime) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        (sellingPrice, lendingPrice, lent, releaseTime, owner, borrower) = monTradeData.getTradingInfo(_objId);
    }
    
    function isOnTrading(uint _objId) constant external returns(bool) {
        EtheremonTradeData monTradeData = EtheremonTradeData(tradingMonDataContract);
        return monTradeData.isOnTrade(_objId);
    }
}
function setOriginalPriceGen0() onlyModerators public {
    gen0Config[1] = Gen0Config(1, 0.3 ether, 0.003 ether, 374);
    gen0Config[2] = Gen0Config(2, 0.3 ether, 0.003 ether, 408);
    gen0Config[3] = Gen0Config(3, 0.3 ether, 0.003 ether, 373);
    gen0Config[4] = Gen0Config(4, 0.2 ether, 0.002 ether, 437);
    gen0Config[5] = Gen0Config(5, 0.1 ether, 0.001 ether, 497);
    gen0Config[6] = Gen0Config(6, 0.3 ether, 0.003 ether, 380); 
    gen0Config[7] = Gen0Config(7, 0.2 ether, 0.002 ether, 345);
    gen0Config[8] = Gen0Config(8, 0.1 ether, 0.001 ether, 518); 
    gen0Config[9] = Gen0Config(9, 0.1 ether, 0.001 ether, 447);
    gen0Config[10] = Gen0Config(10, 0.2 ether, 0.002 ether, 380); 
    gen0Config[11] = Gen0Config(11, 0.2 ether, 0.002 ether, 354);
    gen0Config[12] = Gen0Config(12, 0.2 ether, 0.002 ether, 346);
    gen0Config[13] = Gen0Config(13, 0.2 ether, 0.002 ether, 351); 
    gen0Config[14] = Gen0Config(14, 0.2 ether, 0.002 ether, 338);
    gen0Config[15] = Gen0Config(15, 0.2 ether, 0.002 ether, 341);
    gen0Config[16] = Gen0Config(16, 0.35 ether, 0.0035 ether, 384);
    gen0Config[17] = Gen0Config(17, 1 ether, 0.01 ether, 305); 
    gen0Config[18] = Gen0Config(18, 0.1 ether, 0.001 ether, 427);
    gen0Config[19] = Gen0Config(19, 1 ether, 0.01 ether, 304);
    gen0Config[20] = Gen0Config(20, 0.4 ether, 0.05 ether, 82);
    gen0Config[21] = Gen0Config(21, 1, 1, 123);
    gen0Config[22] = Gen0Config(22, 0.2 ether, 0.001 ether, 468);
    gen0Config[23] = Gen0Config(23, 0.5 ether, 0.0025 ether, 302);
    gen0Config[24] = Gen0Config(24, 1 ether, 0.005 ether, 195);
}

// admin & moderators