# Dojima, a markets client.
# Copyright (C) 2012-2013  Emery Hemingway
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
import logging
import heapq
 
import otapi
from PyQt4 import QtCore, QtGui
 
import dojima.exchanges
import dojima.exchange
import dojima.data.account
import dojima.data.market
import dojima.data.offers
import dojima.markets
import dojima.model.ot.accounts
import dojima.model.ot.assets
import dojima.model.ot.markets
import dojima.ot.contract
import dojima.ot.request
import dojima.ui.ot.nym
import dojima.ui.ot.offer
import dojima.ui.ot.views
 
import dojima.ui.ot.account
 
objEasy = None
 
logger = logging.getLogger(__name__)
 
OT_BUYING = 0
OT_SELLING = 1
MAX_DEPTH = '256'
 
def saveMarketAccountSettings(server_id, market_id, nym_id, b_ac_id, c_ac_id):
    settings = QtCore.QSettings()
    settings.beginGroup('OT_Servers')
    settings.beginGroup(server_id)
    settings.setValue('nym', nym_id)
    settings.beginGroup('markets')
    settings.beginGroup(market_id)
    settings.setValue('base_account', b_ac_id)
    settings.setValue('counter_account', c_ac_id)
 
 
class OTExchangeProxy(dojima.exchange.ExchangeProxy):
 
    def __init__(self, serverId):
        self.id = serverId
        self.server_id = serverId
        self.exchange_object = None
        self.local_market_map = dict()
        self.remote_market_map = dict()
 
    @property
    def name(self):
        assert self.id
        return otapi.OTAPI_Basic_GetServer_Name(self.server_id)
 
    def getExchangeObject(self):
        if self.exchange_object is None:
            self.exchange_object = OTExchange(self.server_id)
 
        return self.exchange_object
 
    def getPrettyMarketName(self, remote_market_id):
        storable = otapi.QueryObject(otapi.STORED_OBJ_MARKET_LIST,
                                     'markets', self.server_id,
                                     'market_data.bin')
        market_list = otapi.MarketList.ot_dynamic_cast(storable)
        for i in range(market_list.GetMarketDataCount()):
            data = market_list.GetMarketData(i)
            if data.market_id == remote_market_id:
                return QtCore.QCoreApplication.translate('OTExchangeProxy',
                                                         "Scale {}",
                    "The market scale, there should be a note on this somewhere "
                    "around here.").format(data.scale)
 
    def getWizardPage(self, wizard):
        return OTServerWizardPage(self.server_id, wizard)
 
    def refreshMarkets(self):
        storable = otapi.QueryObject(otapi.STORED_OBJ_MARKET_LIST,
                                     'markets', self.id,
                                     'market_data.bin')
        if not storable:
            return
 
        market_list = otapi.MarketList.ot_dynamic_cast(storable)
        for i in range(market_list.GetMarketDataCount()):
            market_data = market_list.GetMarketData(i)
 
            local_base_id = dojima.model.commodities.remote_model.getRemoteToLocalMap(
                market_data.asset_type_id)
            if local_base_id is None: continue
 
            local_counter_id = dojima.model.commodities.remote_model.getRemoteToLocalMap(
                market_data.currency_type_id)
            if local_counter_id is None: continue
 
            local_pair = local_base_id + '_' + local_counter_id
 
            if local_pair in self.local_market_map:
                local_map = self.local_market_map[local_pair]
            else:
                local_map = list()
                self.local_market_map[local_pair] = local_map
 
            if market_data.market_id not in local_map:
                local_map.append(market_data.market_id)
 
            self.remote_market_map[market_data.market_id] = local_pair
            dojima.markets.container.addExchange(self, local_pair,
                                                 local_base_id, local_counter_id)
 
 
class OTServerWizardPage(QtGui.QWizardPage):
 
    def __init__(self, server_id, parent):
        self.server_id = server_id
        super(OTServerWizardPage, self).__init__(parent)
 
    def changeBaseAsset(self, asset_id):
        self.base_asset = asset_id
        self.base_accounts_model.setFilterFixedString(asset_id)
 
        local_uuid = dojima.model.commodities.remote_model.getRemoteToLocalMap(asset_id)
        if local_uuid:
            row = dojima.model.commodities.local_model.getRow(local_uuid)
            self.base_local_combo.setCurrentIndex(row)
 
            self._checkCompleteState()
 
        else:
            if self._is_complete is True:
                self._is_complete = False
                self.completeChanged.emit()
 
    def changeCounterAsset(self, asset_id):
        self.counter_asset = asset_id
        self.counter_accounts_model.setFilterFixedString(asset_id)
 
        local_uuid = dojima.model.commodities.remote_model.getRemoteToLocalMap(asset_id)
        if local_uuid:
            row = dojima.model.commodities.local_model.getRow(local_uuid)
            self.counter_local_combo.setCurrentIndex(row)
 
            self._checkCompleteState()
 
        else:
            if self._is_complete is True:
                self._is_complete = False
                self.completeChanged.emit()
 
    def changeBaseLocal(self, uuid):
        self.base_local = uuid
 
    def changeCounterLocal(self, uuid):
        self.counter_local = uuid
 
    #def changeMarket(self, market_id):
        #print market_id
 
    def changeNym(self, nym_id):
        self.nym_accounts_model.setFilterFixedString(nym_id)
 
    def _checkCompleteState(self):
        was_complete = self._is_complete
        self._is_complete = True
 
        if (self.base_asset is None) or (self.counter_asset is None):
            self._is_complete = False
 
        if (self.base_local_combo.currentIndex() == self.counter_local_combo.currentIndex()):
            self._is_complete = False
 
        if (self.base_accounts_model.rowCount() == 0) or (self.counter_accounts_model.rowCount() == 0):
            self._is_complete = False
 
        if was_complete is not self._is_complete:
            self.completeChanged.emit()
 
    def initializePage(self):
        self.base_asset = None
        self.counter_asset = None
        self._is_complete = False
 
        self.setTitle(otapi.OTAPI_Basic_GetServer_Name(self.server_id))
        self.setSubTitle(
            QtCore.QCoreApplication.translate('OTServerWizardPage',
                "Select accounts to match a new or existing market. "
                "The market list must be refreshed manually, "
                "Also, 'Refresh Markets' must be hit twice when using "
                "an unregistered nym, I'm working on it...",
                "This is the the heading underneath the title on the "
                "OT page in the markets wizard."))
 
        self.markets_model = dojima.model.ot.markets.OTMarketsModel(
            self.server_id)
 
        accounts_model = dojima.model.ot.accounts.OTAccountsServerModel(
            self.server_id)
 
        simple_accounts_model = dojima.model.ot.accounts.OTAccountsProxyModel()
        simple_accounts_model.setSourceModel(accounts_model)
        simple_accounts_model.setFilterRole(QtCore.Qt.UserRole)
        simple_accounts_model.setFilterKeyColumn(accounts_model.TYPE)
        simple_accounts_model.setFilterFixedString('s')
        simple_accounts_model.setDynamicSortFilter(True)
 
        self.nym_accounts_model = dojima.model.ot.accounts.OTAccountsProxyModel()
        self.nym_accounts_model.setSourceModel(simple_accounts_model)
        self.nym_accounts_model.setFilterRole(QtCore.Qt.UserRole)
        self.nym_accounts_model.setFilterKeyColumn(accounts_model.NYM)
        self.nym_accounts_model.setDynamicSortFilter(True)
 
        self.base_accounts_model = dojima.model.ot.accounts.OTAccountsProxyModel()
        self.base_accounts_model.setSourceModel(self.nym_accounts_model)
        self.base_accounts_model.setFilterRole(QtCore.Qt.UserRole)
        self.base_accounts_model.setFilterKeyColumn(accounts_model.ASSET)
        self.base_accounts_model.setDynamicSortFilter(True)
 
        self.counter_accounts_model = dojima.model.ot.accounts.OTAccountsProxyModel()
        self.counter_accounts_model.setSourceModel(self.nym_accounts_model)
        self.counter_accounts_model.setFilterRole(QtCore.Qt.UserRole)
        self.counter_accounts_model.setFilterKeyColumn(accounts_model.ASSET)
        self.counter_accounts_model.setDynamicSortFilter(True)
 
        self.markets_view = dojima.ui.ot.views.MarketTableView()
        self.markets_view.setSelectionBehavior(self.markets_view.SelectRows)
        self.markets_view.setSelectionMode(self.markets_view.SingleSelection)
        self.markets_view.setModel(self.markets_model)
        self.markets_view.setShowGrid(False)
 
        self.nym_combo = dojima.ui.ot.views.NymComboBox()
 
        self.base_account_combo = dojima.ui.ot.views.AccountComboBox(self.base_accounts_model)
        self.counter_account_combo = dojima.ui.ot.views.AccountComboBox(self.counter_accounts_model)
 
        # TODO Perhaps these combos can be replaced with a commodities subclass of QComboBox
        self.base_local_combo = QtGui.QComboBox()
        self.base_local_combo.setModel(dojima.model.commodities.local_model)
        self.counter_local_combo = QtGui.QComboBox()
        self.counter_local_combo.setModel(dojima.model.commodities.local_model)
 
        nym_label = QtGui.QLabel(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Server Nym:",
                                              "The label next to the nym "
                                              "combo box."))
        nym_label.setBuddy(self.nym_combo)
        new_nym_button = QtGui.QPushButton(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "New Nym",
                                              "The button next to the nym"
                                              "combo box."))
        base_account_label = QtGui.QLabel(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Base account:",
                                              "The account of the base asset to "
                                              "use with this market."))
        base_account_label.setBuddy(self.base_account_combo)
        counter_account_label = QtGui.QLabel(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Counter account:",
                                              "The account of the counter "
                                              "currency to use with this "
                                              "market."))
        counter_account_label.setBuddy(self.counter_account_combo)
 
        base_local_label = QtGui.QLabel(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Local base:",
                                              "Label for the locally defined "
                                              "comodity."))
        counter_local_label = QtGui.QLabel(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Local counter:",
                                              "Label for the locally defined "
                                              "comodity."))
 
        new_offer_button = QtGui.QPushButton(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "New Offer",
                                              "Button to pop up the new offer "
                                              "dialog."),
            toolTip=QtCore.QCoreApplication.translate(
                'OTServerWizardPage',
                "Make a new offer, thereby\n"
                "creating a new market.\n"
                "Use this if you want\n"
                "to trade at a new scale.",
                "The tool tip for the 'New Offer' button"))
 
        new_account_button = QtGui.QPushButton(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "New Account",
                                              "Button to pop up the new account "
                                              "dialog."))
 
        new_local_button = QtGui.QPushButton(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "New Commodity"))
 
        self.refresh_markets_button = QtGui.QPushButton(
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Refresh Markets",
                                              "Button to refresh the listed "
                                              "markets on the server."))
 
        button_box = QtGui.QDialogButtonBox()
        button_box.addButton(new_offer_button, button_box.ActionRole)
        button_box.addButton(new_account_button, button_box.ActionRole)
        button_box.addButton(new_local_button, button_box.ActionRole)
        button_box.addButton(self.refresh_markets_button, button_box.ActionRole)
 
        # Layout could use some work, sizes look wrong
        layout = QtGui.QGridLayout()
        layout.addWidget(self.markets_view, 0,0, 1,4)
        layout.addWidget(nym_label, 1,0)
        layout.addWidget(self.nym_combo, 1,1, 1,2)
        layout.addWidget(new_nym_button, 1,3)
        layout.addWidget(base_account_label, 2,0, 1,2)
        layout.addWidget(counter_account_label, 2,2, 1,2)
        layout.addWidget(self.base_account_combo, 3,0, 1,2)
        layout.addWidget(self.counter_account_combo, 3,2, 1,2)
        layout.addWidget(base_local_label, 4,0, 1,2)
        layout.addWidget(counter_local_label, 4,2, 1,2)
        layout.addWidget(self.base_local_combo, 5,0, 1,2)
        layout.addWidget(self.counter_local_combo, 5,2, 1,2)
        layout.addWidget(button_box, 6,0, 1,4)
        self.setLayout(layout)
 
 
        self.markets_view.baseChanged.connect(self.changeBaseAsset)
        self.markets_view.counterChanged.connect(self.changeCounterAsset)
        #self.markets_view.marketChanged.connect(self.changeMarket)
 
        self.base_account_combo.currentIndexChanged.connect(
            self._checkCompleteState)
        self.counter_account_combo.currentIndexChanged.connect(
            self._checkCompleteState)
 
        self.base_local_combo.currentIndexChanged.connect(
            self._checkCompleteState)
        self.counter_local_combo.currentIndexChanged.connect(
            self._checkCompleteState)
 
        new_nym_button.clicked.connect(self.showNewNymDialog)
        new_offer_button.clicked.connect(self.showNewOfferDialog)
        new_account_button.clicked.connect(self.showNewAccountDialog)
        new_local_button.clicked.connect(self.showNewCommodityDialog)
        self.refresh_markets_button.clicked.connect(self.refreshMarkets)
        self.nym_combo.nymIdChanged.connect(self.changeNym)
 
        # select
        self.markets_view.selectRow(0)
        self.nym_combo.currentIndexChanged.emit(0)
 
 
        self.nyms_model = dojima.model.ot.nyms.model
        if self.nyms_model.rowCount() < 1:
            self.refresh_markets_button.setDisabled(True)
 
        self._checkCompleteState()
 
    def isComplete(self):
        return self._is_complete
 
    def isFinalPage(self):
        return True
 
    def refreshMarkets(self):
        pass
        """
        QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
        nym_id = self.nym_combo.nymId
        if otapi.OTAPI_Basic_IsNym_RegisteredAtServer(self.server_id, nym_id) < 1:
            # TODO market fetch immediatly after registering fails
            msg = objEasy.register_nym(self.server_id, nym_id)
            if objEasy.VerifyMessageSuccess(msg) < 1:
                logger.error("Failed to register nym %s at server %s.", nym_id, self.server_id)
                return
            else:
                self.markets_model.refresh(nym_id)
        QtGui.QApplication.restoreOverrideCursor()
        """
 
    def showNewAccountDialog(self):
        dialog = dojima.ui.ot.account.NewAccountDialog(self.server_id, self)
        if dialog.exec_():
            self.nym_accounts_model.refresh()
 
    def showNewCommodityDialog(self):
        contract = None
        if not dojima.model.commodities.remote_model.hasMap(self.base_asset):
            contract = dojima.ot.contract.CurrencyContract(self.base_asset)
            dialog = dojima.ui.edit.commodity.NewCommodityDialog(self,
                                                                 name=contract.getName(),
                                                                 prefix=contract.getSymbol(),
                                                                 suffix=contract.getTLA())
            if dialog.exec_():
                self.base_local_combo.setCurrentIndex(dialog.row)
                dojima.model.commodities.remote_model.map(self.base_asset, dialog.uuid)
 
        elif not dojima.model.commodities.remote_model.hasMap(self.counter_asset):
            contract = dojima.ot.contract.CurrencyContract(self.counter_asset)
            dialog = dojima.ui.edit.commodity.NewCommodityDialog(self,
                                                                 name=contract.getName(),
                                                                 prefix=contract.getSymbol(),
                                                                 suffix=contract.getTLA())
            if dialog.exec_():
                self.counter_local_combo.setCurrentIndex(dialog.row)
                dojima.model.commodities.remote_model.map(self.counter_asset, dialog.uuid)
 
        else:
            dialog = dojima.ui.edit.commodity.NewCommodityDialog(self)
 
    def showNewNymDialog(self):
        dialog = dojima.ui.ot.nym.CreateNymDialog(self)
        if dialog.exec_():
            self.nyms_model.refresh()
            self.refresh_markets_button.setEnabled(True)
 
    def showNewOfferDialog(self):
        dialog = dojima.ui.ot.offer.NewOfferDialog(self.server_id)
        if dialog.exec_():
            self.refreshMarkets()
 
    def validatePage(self):
        nym_id = self.nym_combo.nymId
 
        b_ac_id = self.base_account_combo.getAccountId()
        c_ac_id = self.counter_account_combo.getAccountId()
 
        assert nym_id
        assert b_ac_id
        assert c_ac_id        
 
        remote_base_id = otapi.OTAPI_Basic_GetAccountWallet_AssetTypeID(b_ac_id)
        remote_counter_id = otapi.OTAPI_Basic_GetAccountWallet_AssetTypeID(c_ac_id)
 
        storable = otapi.QueryObject(otapi.STORED_OBJ_MARKET_LIST,
                                     'markets', self.server_id,
                                     'market_data.bin')
        market_list = otapi.MarketList.ot_dynamic_cast(storable)
        market_id = None
        for i in range(market_list.GetMarketDataCount()):
            data = market_list.GetMarketData(i)
            if (data.asset_type_id == remote_base_id and
                data.currency_type_id == remote_counter_id):
                saveMarketAccountSettings(self.server_id, data.market_id,
                                          nym_id, b_ac_id, c_ac_id)
 
        local_base_id = self.base_local_combo.itemData(
            self.base_local_combo.currentIndex(),
            QtCore.Qt.UserRole)
 
        local_counter_id = self.counter_local_combo.itemData(
            self.counter_local_combo.currentIndex(),
            QtCore.Qt.UserRole)
 
        dojima.model.commodities.remote_model.map(remote_base_id, local_base_id)
        dojima.model.commodities.remote_model.map(remote_counter_id, local_counter_id)
        return dojima.model.commodities.remote_model.submit()
 
 
class _OTRequestObject(object):
 
    def status(self):
        return self.status_msg
 
 
class OTExchange(QtCore.QObject, dojima.exchange.Exchange):
    valueType = int
 
    exchange_error_signal = QtCore.pyqtSignal(str)
    accountChanged = QtCore.pyqtSignal(str)
    requestRequest = QtCore.pyqtSignal(tuple)
 
    balance_proxies = dict()
 
 
    def __init__(self, serverID, parent=None):
        super(OTExchange, self).__init__(parent)
        self.server_id = serverID
 
        self.ready = False
        self.ot_request_manager = dojima.ot.OTServerRequestManager()
 
        self.account_validity_proxies = dict()
        self.ticker_proxies = dict()
        self.ticker_clients = dict()
        self.depth_proxies = dict()
        self.trades_proxies = dict()
        # market_id -> [base_id, counter_id]
        self.assets = dict()
        # market_id -> [base_account_id, counter_account_id]
        self.accounts = dict()
        # market_id -> scale
        self.scales = dict()
 
        self.offers_model = None
        self.offers_proxies_asks = dict()
        self.offers_proxies_bids = dict()
 
        self.base_offers_proxies = dict()
        self.offers_proxies = dict()
 
        storable = otapi.QueryObject(otapi.STORED_OBJ_MARKET_LIST, 'markets',
                                     self.server_id, 'market_data.bin')
        market_list = otapi.MarketList.ot_dynamic_cast(storable)
        for i in range(market_list.GetMarketDataCount()):
            data = market_list.GetMarketData(i)
            market_id = data.market_id
            self.assets[market_id] = (data.asset_type_id, data.currency_type_id)
            self.scales[market_id] = int(data.scale)
 
        settings = QtCore.QSettings()
        settings.beginGroup('OT_Servers')
        settings.beginGroup(self.server_id)
        self.nym_id = settings.value('nym', '')
        settings.beginGroup('markets')
        for market_id in settings.childGroups():
            settings.beginGroup(market_id)
            b_ac_id = settings.value('base_account', '')
            c_ac_id = settings.value('counter_account', '')
            self.accounts[market_id] = [b_ac_id, c_ac_id]
 
        self.ticker_clients = 0
        self.ticker_timer = QtCore.QTimer(self)
        self.ticker_timer.timeout.connect(self.enqueueGetMarketList)
 
    def _cancel_offer(self, order_id, market_id=None):
        search = self.offers_model.findItems(order_id)
        if not search:
            logger.error("could not find order id %s to cancel", order_id)
            return
        row = search[0].row()
        # TODO queuing the account and transaction number but not the nym id
        # could be a problem as the nym may change before the order is cancelled
        account_id = self.offers_model.item(
            row, dojima.data.offers.BASE).text()
 
        self.requestRequest( (0, OTRequestCancelOffer(self.nym_id,
                                                      str(account_id),
                                                      str(order_id)),) )
 
    cancelAskOffer = _cancel_offer
    cancelBidOffer = _cancel_offer
 
    def changeBaseAccount(self, market_id, account_id):
        settings = QtCore.QSettings()
        settings.beginGroup('OT_Servers')
        settings.beginGroup(self.server_id)
        settings.beginGroup('markets')
        settings.beginGroup(market_id)
        settings.setValue('base_account', account_id)
        if market_id in self.accounts:
            self.accounts[market_id][0] = account_id
        else:
            self.accounts[market_id] = [account_id, None]
 
        self.checkAccountValidity(market_id)
 
    def changeCounterAccount(self, market_id, account_id):
        settings = QtCore.QSettings()
        settings.beginGroup('OT_Servers')
        settings.beginGroup(self.server_id)
        settings.beginGroup('markets')
        settings.beginGroup(market_id)
        settings.setValue('counter_account', account_id)
        if market_id in self.accounts:
            self.accounts[market_id][1] = account_id
        else:
            self.accounts[market_id] = [None, account_id]
 
        self.checkAccountValidity(market_id)
 
    def changeNym(self, nym_id):
        self.nym_id = str(nym_id)
        settings = QtCore.QSettings()
        settings.beginGroup('OT_Servers')
        settings.beginGroup(self.server_id)
        settings.setValue('nym', nym_id)
        if otapi.OTAPI_Basic_IsNym_RegisteredAtServer(self.nym_id,
                                                      self.server_id):
            return
 
        """
        msg = objEasy.register_nym(self.server_id, self.nym_id)
        if objEasy.VerifyMessageSuccess(msg) < 1:
            QtGui.QApplication.restoreOverrideCursor()
            QtGui.QMessageBox.error(self,
            QtCore.QCoreApplication.translate('Open Transactions',
                                              "Error registering nym"),
            QtCore.QCoreApplication.translate('Open Transactions'
                                              "Error registering the "
                                              "nym with the server."))
        """
 
    def checkAccountValidity(self, market_id):
        if market_id not in self.account_validity_proxies:
            return
        proxy = self.account_validity_proxies[market_id]
        proxy.accountValidityChanged.emit(
            (None not in self.accounts[market_id]) )
 
    def echoTicker(self, market_id=None):
        self.readMarketList()
 
    def enqueueGetMarketList(self):
        self.get_market_list = True
 
    def getBalanceBaseProxy(self, market_id):
        account_id = self.accounts[market_id][0]
        if account_id not in self.balance_proxies:
            proxy = dojima.data.balance.BalanceProxy(self)
            self.balance_proxies[account_id] = proxy
            return proxy
 
        return self.balance_proxies[account_id]
 
    def getBalanceCounterProxy(self, market_id):
        account_id = self.accounts[str(market_id)][1]
        if account_id not in self.balance_proxies:
            proxy = dojima.data.balance.BalanceProxy(self)
            self.balance_proxies[account_id] = proxy
            return proxy
 
        return self.balance_proxies[account_id]
 
    # TODO getFactors and getPowers will probably change with the OT high API
    def getFactors(self, market_id):
        b_asset_id, c_asset_id = self.assets[market_id]
        b_contract = dojima.ot.contract.CurrencyContract(b_asset_id)
        c_contract = dojima.ot.contract.CurrencyContract(c_asset_id)
        return ( b_contract.getFactor(), c_contract.getFactor(), )
 
    def getOffersModel(self, market_id):
        # what happens here is there is a model that contains all nym offers,
        # that model is filtered by the base account,
        # that model is filtered by the counter account.
 
        if self.offers_model is None:
            self.offers_model = dojima.data.offers.Model()
 
        if market_id in self.offers_proxies:
            return self.offers_proxies[market_id]
 
        bacid, cacid = self.accounts[market_id]
        if bacid in self.base_offers_proxies:
            base_proxy = self.base_offers_proxies[bacid]
        else:
            base_proxy = QtGui.QSortFilterProxyModel()
            base_proxy.setSourceModel(self.offers_model)
            base_proxy.setFilterKeyColumn(dojima.data.offers.BASE)
            base_proxy.setFilterFixedString(bacid)
            base_proxy.setDynamicSortFilter(True)
            self.base_offers_proxies[bacid] = base_proxy
 
        proxy = QtGui.QSortFilterProxyModel()
        proxy.setSourceModel(base_proxy)
        proxy.setFilterKeyColumn(dojima.data.offers.COUNTER)
        proxy.setFilterFixedString(cacid)
        proxy.setDynamicSortFilter(True)
        self.offers_proxies[market_id] = proxy
        return proxy
 
    def getPowers(self, market_id):
        b_asset_id, c_asset_id = self.assets[market_id]
        b_contract = dojima.ot.contract.CurrencyContract(b_asset_id)
        c_contract = dojima.ot.contract.CurrencyContract(c_asset_id)
        return ( b_contract.getPower(), c_contract.getPower(), )
 
    def getRemotePair(self, market_id):
        return self.assets[market_id]
 
    def getScale(self, market_id):
        return int(self.scales[market_id])
 
    def getDepthProxy(self, market_id):
        if market_id not in self.depth_proxies:
            depth_proxy = dojima.data.market.DepthProxy(self, market_id)
            self.depth_proxies[market_id] = depth_proxy
            return depth_proxy
        return self.depth_proxies[market_id]
 
    def getTradesProxy(self, market_id):
        if market_id not in self.trades_proxies:
            trades_proxy = dojima.data.market.TradesProxy(self, market_id)
            self.trades_proxies[market_id] = trades_proxy
            return trades_proxy
        return self.trades_proxies[market_id]
 
    def hasAccount(self, market_id):
        if market_id not in self.accounts:
            return False
        base, counter = self.accounts[market_id]
        if not base or not counter:
            return False
 
        return True
 
 
    def populateMenuBar(self, menu_bar, market_id):
        # Make submenus
        exchange_menu = menu_bar.getExchangeMenu()
        # Maybe the exchange UI stuff should test for depth and trades methods, then add
        # menu actions from that side
        nyms_menu = CurrentNymMenu(
            QtCore.QCoreApplication.translate('Open Transactions', "No nym selected",
                                              "The text that is displayed in "
                                              "the exchange menu until a nym "
                                              "for this exchange server is "
                                              "chosen."),
            exchange_menu)
        exchange_menu.addMenu(nyms_menu)
 
        b_as_id, c_as_id = self.assets[market_id]
        if market_id in self.accounts:
            b_ac_id, c_ac_id = self.accounts[market_id]
        else:
            b_ac_id, c_ac_id = None, None
        account_main_menu = menu_bar.getAccountMenu()
        b_ac_menu = NymAccountMenu(
            QtCore.QCoreApplication.translate('Open Transactions', "Base Account",
                "Title of a submenu to select the account that will hold the "
                "base asset."),
                b_as_id, market_id, self.changeBaseAccount,
                self.nym_id, b_ac_id, account_main_menu)
        c_ac_menu = NymAccountMenu(
            QtCore.QCoreApplication.translate('Open Transactions', "Counter Account",
                "Title of a submenu to select the account that will hold the "
                "counter asset."),
                c_as_id, market_id, self.changeCounterAccount,
                self.nym_id, c_ac_id, account_main_menu)
        account_main_menu.addMenu(b_ac_menu)
        account_main_menu.addMenu(c_ac_menu)
 
        # create actions
        nyms_group = QtGui.QActionGroup(exchange_menu)
        for i in range(otapi.OTAPI_Basic_GetNymCount()):
            nym_id = otapi.OTAPI_Basic_GetNym_ID(i)
            nym_label = otapi.OTAPI_Basic_GetNym_Name(nym_id)
            action = ChangeOTThingAction(nym_id, nym_label, nyms_menu)
            action.setActionGroup(nyms_group)
            nyms_menu.addAction(action)
            action.currentLabelChanged.connect(nyms_menu.changeTitle)
            action.currentIDChanged.connect(c_ac_menu.setNymId)
            action.currentIDChanged.connect(b_ac_menu.setNymId)
            if nym_id == self.nym_id: action.trigger()
            # no sense changing the nym needlessly
            action.currentIDChanged.connect(self.changeNym)
 
    def placeAskLimitOffer(self, amount, price, market_id):
        base_account_id, counter_account_id = self.accounts[market_id]
        request = dojima.ot.request.PlaceMarketOffer(
            self.server_id, self.nym_id, base_account_id, counter_account_id,
            self.scales[market_id], int(amount), int(price),
            OT_SELLING)
        self.ot_request_manager.send(request)
 
    def placeBidLimitOffer(self, market_id, amount, price):
        base_account_id, counter_account_id = self.accounts[market_id]
        request = dojima.ot.request.PlaceMarketOffer(
            self.server_id, self.nym_id, base_account_id, counter_account_id,
            self.scales[market_id], int(amount), int(price),
            OT_BUYING)
        self.ot_request_manager.send(request)
 
    def readDepth(self, market_id):
        proxy = list(self.depth_proxies.items())
        storable = otapi.QueryObject(otapi.STORED_OBJ_OFFER_LIST_MARKET, 'markets',
                                     self.server_id, 'offers',
                                     market_id + '.bin')
        if not storable: return
        offers = otapi.OfferListMarket.ot_dynamic_cast(storable)
 
        asks = list()
        for i in range(offers.GetAskDataCount()):
            offer = offers.GetAskData(i)
            asks.append(( int(offer.price_per_scale),
                          int(offer.available_assets),))
 
        bids = list()
        for i in range(offers.GetBidDataCount()):
            offer = offers.GetBidData(i)
            bids.append(( int(offer.price_per_scale),
                          int(offer.available_assets),))
 
        proxy.processDepth(asks, bids)
 
    def readMarketList(self):
        storable = otapi.QueryObject(otapi.STORED_OBJ_MARKET_LIST,
                                     'markets', self.server_id,
                                     'market_data.bin')
        if not storable: return
        market_list = otapi.MarketList.ot_dynamic_cast(storable)
        for i in range(market_list.GetMarketDataCount()):
            data = market_list.GetMarketData(i)
            if data.market_id in self.ticker_proxies:
                proxy = self.ticker_proxies[data.market_id]
                proxy.ask_signal.emit(int(data.current_ask))
                proxy.last_signal.emit(int(data.last_sale_price))
                proxy.bid_signal.emit(int(data.current_bid))
 
    def readNymOffers(self, nym_id):
        storable = otapi.QueryObject(otapi.STORED_OBJ_OFFER_LIST_NYM, 'nyms',
                                     self.server_id, 'offers',
                                     nym_id + '.bin')
        if not storable: return
        offers = otapi.OfferListNym.ot_dynamic_cast(storable)
        self.offers_model.clear()
 
        for row in range(offers.GetOfferDataNymCount()):
            offer = offers.GetOfferDataNym(row)
 
            # Offer ID
            item = QtGui.QStandardItem(offer.transaction_id)
            self.offers_model.setItem(row, dojima.data.offers.ID, item)
 
            # Offer price
            value = ( int(offer.price_per_scale)
                      * int(offer.minimum_increment)
                      * int(offer.scale) )
 
            item = QtGui.QStandardItem()
            item.setData(value, QtCore.Qt.UserRole)
            self.offers_model.setItem(row, dojima.data.offers.PRICE, item)
 
            # Offer outstanding
            value = ( int(offer.total_assets)
                      - int(offer.finished_so_far) )
            item = QtGui.QStandardItem()
            item.setData(value, QtCore.Qt.UserRole)
            self.offers_model.setItem(row, dojima.data.offers.OUTSTANDING, item)
 
            # Offer type
            if offer.selling:
                item = QtGui.QStandardItem(dojima.data.offers.ASK)
            else:
                item = QtGui.QStandardItem(dojima.data.offers.BID)
            self.offers_model.setItem(row, dojima.data.offers.TYPE, item)
 
            # Offer base account
            self.offers_model.setItem(row, dojima.data.offers.BASE,
                                      QtGui.QStandardItem(
                                          offer.asset_acct_id))
            # Offer counter account
            self.offers_model.setItem(row, dojima.data.offers.COUNTER,
                                      QtGui.QStandardItem(
                                          offer.currency_acct_id))
 
    def readTrades(self, market_id):
        proxy = self.trades_proxies.items[market_id]
        storable = otapi.QueryObject(otapi.STORED_OBJ_TRADE_LIST_MARKET,
                                     "markets", self.server_id,
                                     "recent", market_id + ".bin")
        trades = otapi.TradeListMarket.ot_dynamic_cast(storable)
        if not trades: return
 
        epochs, prices, amounts = list(), list(), list()
        for i in range(trades.GetTradeDataMarketCount()):
            trade = trades.GetTradeDataMarket(i)
            epochs.append( int(trade.date))
            prices.append( float(trade.price))
            amounts.append( float(trade.amount_sold))
 
        proxy.processTrades(epochs, prices, amounts)
 
    def refresh(self, market_id):
        self.refreshBalance(market_id)
        self.refreshOffers(market_id)
 
    def refreshBalance(self, market_id):
        for account_id in self.accounts[market_id]:
            request = dojima.ot.request.Account(self.server_id, self.nym_id,
                                                account_id)
            self.ot_request_manager.send(request)
 
    def refreshDepth(self, market_id):
        # TODO take offer depth (amount of offers) into account
        self.requestRequest.emit( (3, OTRequestDepth(self.nym_id, market_id),) )
 
    def refreshOffers(self, market_id=None):
        request = dojima.ot.request.NymOffers(self.server_id, self.nym_id)
        self.ot_request_manager.send(request)
 
    def refreshTrades(self, market_id):
        self.requestRequest.emit( (3, OTRequestTrades(self.nym_id, market_id),) )
 
    def currentScale(self, market_id):
        return self.scales[market_id]
 
    def setDefaultAccounts(self, marketId):
        settings = QtCore.QSettings()
        settings.beginGroup('OT-defaults')
        settings.beginGroup(self.server_id)
        settings.setValue('nym', self.nym_id)
 
        b_ac_id, c_ac_id = self.accounts[marketId]
        saveMarketAccountSettings(self.server_id, marketId, self.nym_id, b_ac_id, c_ac_id)
 
    def setTickerStreamState(self, state, market_id):
        if state is True:
            self.startTickerStream(market_id)
            return
        self.stopTickerStream(market_id)
 
    def startTickerStream(self, market_id=None):
        if self.ticker_clients == 0:
            logger.debug("starting ticker stream for %s", self.server_id)
            self.ticker_timer.start(16384)
 
        self.ticker_clients += 1
 
    def stopTickerStream(self, market_id=None):
        if self.ticker_clients == 1:
            logger.debug("stopping ticker stream for %s", self.server_id)
            self.ticker_timer.stop()
 
        self.ticker_clients -= 1
        assert self.ticker_clients >= 0
 
    def supportedScales(self, market_id):
        # this is confusing because when the scale changes, the market changes,
        # but the market_id is used to find the base and counter asset IDs
        # for parsing the markets
        basid, casid = self.assets[market_id]
        market_scales = list()
 
        storable = otapi.QueryObject(otapi.STORED_OBJ_MARKET_LIST,
                                     'markets', self.server_id,
                                     'market_data.bin')
        market_list = otapi.MarketList.ot_dynamic_cast(storable)
        for i in range(market_list.GetMarketDataCount()):
            data = market_list.GetMarketData(i)
            if (data.asset_type_id == basid and
                data.currency_type_id == casid):
                market_scales.append( (data.market_id, int(data.scale),) )
        return market_scales
 
 
class CurrentNymMenu(QtGui.QMenu):
 
    template = QtCore.QCoreApplication.translate('Open Transactions', "Current nym: %1",
                                                 "%1 will be replaced with the "
                                                 "currently selected nym label.")
 
    def changeTitle(self, label):
        self.setTitle(self.template.format(label))
 
 
class ChangeOTThingAction(QtGui.QAction):
 
    currentIDChanged = QtCore.pyqtSignal(str)
    currentLabelChanged = QtCore.pyqtSignal(str)
 
    def __init__(self, ot_id, label, parent):
        super(ChangeOTThingAction, self).__init__(label, parent,
                                                  checkable=True)
        self.id = ot_id
        self.label = label
        self.triggered.connect(self.thingChanged)
 
    def thingChanged(self, toggled):
        self.currentIDChanged.emit(self.id)
        self.currentLabelChanged.emit(self.label)
 
 
class ChangeAccountAction(QtGui.QAction):
 
    currentLabelChanged = QtCore.pyqtSignal(str)
 
    def __init__(self, label, account_id, market_id,
                 change_account_method, parent=None):
        super(ChangeAccountAction, self).__init__(label, parent,
                                                  checkable=True)
        self.label = label
        self.account_id = account_id
        self.market_id = market_id
        self.changeExchangeAccount = change_account_method
        self.triggered.connect(self.accountChanged)
 
    def accountChanged(self, toggled):
        assert toggled
        self.changeExchangeAccount(self.market_id, self.account_id)
        self.currentLabelChanged.emit(self.label)
 
 
class NymAccountMenu(QtGui.QMenu):
 
 
    template = QtCore.QCoreApplication.translate('OTExchange',
                                                 "Current account: {}",
                                                 "{} will be replaced with the "
                                                 "currently selected account "
                                                 "label.")
 
    def __init__(self, title, asset_id, market_id,
                 change_account_method,
                 current_nym_id, current_account_id, parent):
        super(NymAccountMenu, self).__init__(title, parent)
        self.asset_id = asset_id
        self.market_id = market_id
        self.change_account_method = change_account_method
        if current_nym_id: self.setNymId(current_nym_id)
        if current_account_id:
            for action in self.actions():
                if action.account_id == current_account_id:
                    action.trigger()
 
    def changeTitle(self, label):
        self.setTitle(self.template.format(label))
 
    def setNymId(self, nym_id):
        self.clear()
        action_group = QtGui.QActionGroup(self)
        actions = list()
        for i in range(otapi.OTAPI_Basic_GetAccountCount()):
            account_id = otapi.OTAPI_Basic_GetAccountWallet_ID(i)
            if otapi.OTAPI_Basic_GetAccountWallet_NymID(account_id) != nym_id:
                continue
            if (otapi.OTAPI_Basic_GetAccountWallet_AssetTypeID(account_id)
                != self.asset_id):
                continue
 
            if otapi.OTAPI_Basic_GetAccountWallet_Type(account_id) == 'issuer':
                continue
 
            account_label =  otapi.OTAPI_Basic_GetAccountWallet_Name(account_id)
            action = ChangeAccountAction(account_label,
                                         account_id, self.market_id,
                                         self.change_account_method, self)
            action.setActionGroup(action_group)
            action.currentLabelChanged.connect(self.changeTitle)
            self.addAction(action)
            actions.append(action)
 
        if len(actions) == 1:
            actions[0].trigger()
 
 
class OTRequestDepth(object):
 
    status_msg = QtCore.QCoreApplication.translate(
        'Open Transactions',
        "Requesting standing offers...")
 
    def __init__(self, nym_id, market_id):
        self.nym_id = nym_id
        self.market_id
 
    def send(self, server_id):
        msg = objEasy.get_market_offers(server_id, self.nym_id,
                                        self.market_id, MAX_DEPTH)
        if objEasy.VerifyMessageSuccess(msg) < 1:
            logger.error("server %s: failed to request standing offers at "
                         "depth of %s", server_id, MAX_DEPTH)
 
 
class OTRequestMarketList(object):
 
    status_msg = QtCore.QCoreApplication.translate(
        'Open Transactions',
        "Requesting server's markets list...")
 
    def __init__(self, nym_id):
        self.nym_id = nym_id
 
    def send(self, server_id):
        msg = objEasy.get_market_list(server_id, self.nym_id)
 
        if objEasy.VerifyMessageSuccess(msg) < 1:
            logger.error("server %s: market list request failed", server_id)
 
 
def parse_servers():
    for i in range(otapi.OTAPI_Basic_GetServerCount()):
        server_id = otapi.OTAPI_Basic_GetServer_ID(i)
 
        if server_id in dojima.exchanges.container:
            continue
 
        exchange_proxy = OTExchangeProxy(server_id)
        dojima.exchanges.container.addExchange(exchange_proxy)
 
parse_servers()