--[[
    Finalizing Market:
      Note: Feel free to work on any area and submit
            it as a pull request from your git fork.

      BeniS's Skype: benjiz69

      List:
      * Add offer management:
        - Current Offers
        - Offer History

      * Clean up the interface building
        - Add a new market interface file to handle building?

      * Extend information features
        - Hover over offers for purchase information (balance after transaction, etc)
  ]]

Market = {}

local protocol = runinsandbox('marketprotocol')

marketWindow = nil
mainTabBar = nil
displaysTabBar = nil
offersTabBar = nil
selectionTabBar = nil

marketOffersPanel = nil
browsePanel = nil
overviewPanel = nil
itemOffersPanel = nil
itemDetailsPanel = nil
itemStatsPanel = nil
myOffersPanel = nil
currentOffersPanel = nil
offerHistoryPanel = nil
itemsPanel = nil
selectedOffer = {}
selectedMyOffer = {}

nameLabel = nil
feeLabel = nil
balanceLabel = nil
totalPriceEdit = nil
piecePriceEdit = nil
amountEdit = nil
searchEdit = nil
radioItemSet = nil
selectedItem = nil
offerTypeList = nil
categoryList = nil
subCategoryList = nil
slotFilterList = nil
createOfferButton = nil
buyButton = nil
sellButton = nil
anonymous = nil
filterButtons = {}

buyOfferTable = nil
sellOfferTable = nil
detailsTable = nil
buyStatsTable = nil
sellStatsTable = nil

buyCancelButton = nil
sellCancelButton = nil
buyMyOfferTable = nil
sellMyOfferTable = nil

offerExhaust = {}
marketOffers = {}
marketItems = {}
information = {}
currentItems = {}
lastCreatedOffer = 0
fee = 0
averagePrice = 0

loaded = false

local function isItemValid(item, category, searchFilter)
  if not item or not item.marketData then
    return false
  end

  if not category then
    category = MarketCategory.All
  end
  if item.marketData.category ~= category and category ~= MarketCategory.All then
    return false
  end

  -- filter item
  local slotFilter = false
  if slotFilterList:isEnabled() then
    slotFilter = getMarketSlotFilterId(slotFilterList:getCurrentOption().text)
  end
  local marketData = item.marketData

  local filterVocation = filterButtons[MarketFilters.Vocation]:isChecked()
  local filterLevel = filterButtons[MarketFilters.Level]:isChecked()
  local filterDepot = filterButtons[MarketFilters.Depot]:isChecked()

  if slotFilter then
    if slotFilter ~= 255 and item.thingType:getClothSlot() ~= slotFilter then
      return false
    end
  end
  local player = g_game.getLocalPlayer()
  if filterLevel and marketData.requiredLevel and player:getLevel() < marketData.requiredLevel then
    return false
  end
  if filterVocation and marketData.restrictVocation > 0 then
    local voc = Bit.bit(information.vocation)
    if not Bit.hasBit(marketData.restrictVocation, voc) then
      return false
    end
  end
  if filterDepot and Market.getDepotCount(item.marketData.tradeAs) <= 0 then
    return false
  end
  if searchFilter then
    return marketData.name:lower():find(searchFilter)
  end
  return true
end

local function clearItems()
  currentItems = {}
  Market.refreshItemsWidget()
end

local function clearOffers()
  marketOffers[MarketAction.Buy] = {}
  marketOffers[MarketAction.Sell] = {}
  buyOfferTable:clearData()
  sellOfferTable:clearData()
end

local function clearMyOffers()
  marketOffers[MarketAction.Buy] = {}
  marketOffers[MarketAction.Sell] = {}
  buyMyOfferTable:clearData()
  sellMyOfferTable:clearData()
end

local function clearFilters()
  for _, filter in pairs(filterButtons) do
    if filter and filter:isChecked() ~= filter.default then
      filter:setChecked(filter.default)
    end
  end
end

local function clearFee()
  feeLabel:setText('')
  fee = 20
end

local function refreshTypeList()
  offerTypeList:clearOptions()
  offerTypeList:addOption('Buy')

  if Market.isItemSelected() then
    if Market.getDepotCount(selectedItem.item.marketData.tradeAs) > 0 then
      offerTypeList:addOption('Sell')
    end
  end
end

local function addOffer(offer, offerType)
  if not offer then
    return false
  end
  local id = offer:getId()
  local player = offer:getPlayer()
  local amount = offer:getAmount()
  local price = offer:getPrice()
  local timestamp = offer:getTimeStamp()
  local itemName = offer:getItem():getMarketData().name

  buyOfferTable:toggleSorting(false)
  sellOfferTable:toggleSorting(false)

  buyMyOfferTable:toggleSorting(false)
  sellMyOfferTable:toggleSorting(false)

  if amount < 1 then return false end
  if offerType == MarketAction.Buy then
    if offer.warn then
      buyOfferTable:setColumnStyle('OfferTableWarningColumn', true)
    end

    local row = nil
    if offer.var == MarketRequest.MyOffers then
      row = buyMyOfferTable:addRow({
        {text = itemName},
        {text = price*amount},
        {text = price},
        {text = amount},
        {text = string.gsub(os.date('%c', timestamp), " ", "  "), sortvalue = timestamp}
      })
    else
      row = buyOfferTable:addRow({
        {text = player},
        {text = amount},
        {text = price*amount},
        {text = price},
        {text = string.gsub(os.date('%c', timestamp), " ", "  ")}
      })
    end
    row.ref = id

    if offer.warn then
      row:setTooltip(tr('This offer is 25%% below the average market price'))
      buyOfferTable:setColumnStyle('OfferTableColumn', true)
    end
  else
    if offer.warn then
      sellOfferTable:setColumnStyle('OfferTableWarningColumn', true)
    end

    local row = nil
    if offer.var == MarketRequest.MyOffers then
      row = sellMyOfferTable:addRow({
        {text = itemName},
        {text = price*amount},
        {text = price},
        {text = amount},
        {text = string.gsub(os.date('%c', timestamp), " ", "  "), sortvalue = timestamp}
      })
    else
      row = sellOfferTable:addRow({
        {text = player},
        {text = amount},
        {text = price*amount},
        {text = price},
        {text = string.gsub(os.date('%c', timestamp), " ", "  "), sortvalue = timestamp}
      })
    end
    row.ref = id

    if offer.warn then
      row:setTooltip(tr('This offer is 25%% above the average market price'))
      sellOfferTable:setColumnStyle('OfferTableColumn', true)
    end
  end

  buyOfferTable:toggleSorting(false)
  sellOfferTable:toggleSorting(false)
  buyOfferTable:sort()
  sellOfferTable:sort()

  buyMyOfferTable:toggleSorting(false)
  sellMyOfferTable:toggleSorting(false)
  buyMyOfferTable:sort()
  sellMyOfferTable:sort()

  return true
end

local function mergeOffer(offer)
  if not offer then
    return false
  end

  local id = offer:getId()
  local offerType = offer:getType()
  local amount = offer:getAmount()
  local replaced = false

  if offerType == MarketAction.Buy then
    if averagePrice > 0 then
      offer.warn = offer:getPrice() <= averagePrice - math.floor(averagePrice / 4)
    end

    for i = 1, #marketOffers[MarketAction.Buy] do
      local o = marketOffers[MarketAction.Buy][i]
      -- replace existing offer
      if o:isEqual(id) then
        marketOffers[MarketAction.Buy][i] = offer
        replaced = true
      end
    end
    if not replaced then
      table.insert(marketOffers[MarketAction.Buy], offer)
    end
  else
    if averagePrice > 0 then
      offer.warn = offer:getPrice() >= averagePrice + math.floor(averagePrice / 4)
    end

    for i = 1, #marketOffers[MarketAction.Sell] do
      local o = marketOffers[MarketAction.Sell][i]
      -- replace existing offer
      if o:isEqual(id) then
        marketOffers[MarketAction.Sell][i] = offer
        replaced = true
      end
    end
    if not replaced then
      table.insert(marketOffers[MarketAction.Sell], offer)
    end
  end
  return true
end

local function updateOffers(offers)
  if not buyOfferTable or not sellOfferTable then
    return
  end

  balanceLabel:setColor('#bbbbbb')
  selectedOffer[MarketAction.Buy] = nil
  selectedOffer[MarketAction.Sell] = nil

  selectedMyOffer[MarketAction.Buy] = nil
  selectedMyOffer[MarketAction.Sell] = nil

  -- clear existing offer data
  buyOfferTable:clearData()
  buyOfferTable:setSorting(4, TABLE_SORTING_DESC)
  sellOfferTable:clearData()
  sellOfferTable:setSorting(4, TABLE_SORTING_ASC)

  sellButton:setEnabled(false)
  buyButton:setEnabled(false)

  buyCancelButton:setEnabled(false)
  sellCancelButton:setEnabled(false)

  for _, offer in pairs(offers) do
    mergeOffer(offer)
  end
  for type, offers in pairs(marketOffers) do
    for i = 1, #offers do
      addOffer(offers[i], type)
    end
  end
end

local function updateDetails(itemId, descriptions, purchaseStats, saleStats)
  if not selectedItem then
    return
  end

  -- update item details
  detailsTable:clearData()
  for k, desc in pairs(descriptions) do
    local columns = {
      {text = getMarketDescriptionName(desc[1])..':'},
      {text = desc[2]}
    }
    detailsTable:addRow(columns)
  end

  -- update sale item statistics
  sellStatsTable:clearData()
  if table.empty(saleStats) then
    sellStatsTable:addRow({{text = 'No information'}})
  else
    local offerAmount = 0
    local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0
    for _, stat in pairs(saleStats) do
      if not stat:isNull() then
        offerAmount = offerAmount + 1
        transactions = transactions + stat:getTransactions()
        totalPrice = totalPrice + stat:getTotalPrice()
        local newHigh = stat:getHighestPrice()
        if newHigh > highestPrice then
          highestPrice = newHigh
        end
        local newLow = stat:getLowestPrice()
        -- ?? getting '0xffffffff' result from lowest price in 9.60 cipsoft
        if (lowestPrice == 0 or newLow < lowestPrice) and newLow ~= 0xffffffff then
          lowestPrice = newLow
        end
      end
    end

    if offerAmount >= 5 and transactions >= 10 then
      averagePrice = math.round(totalPrice / transactions)
    else
      averagePrice = 0
    end

    sellStatsTable:addRow({{text = 'Total Transations:'}, {text = transactions}})
    sellStatsTable:addRow({{text = 'Highest Price:'}, {text = highestPrice}})

    if totalPrice > 0 and transactions > 0 then
      sellStatsTable:addRow({{text = 'Average Price:'},
        {text = math.floor(totalPrice/transactions)}})
    else
      sellStatsTable:addRow({{text = 'Average Price:'}, {text = 0}})
    end

    sellStatsTable:addRow({{text = 'Lowest Price:'}, {text = lowestPrice}})
  end

  -- update buy item statistics
  buyStatsTable:clearData()
  if table.empty(purchaseStats) then
    buyStatsTable:addRow({{text = 'No information'}})
  else
    local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0
    for _, stat in pairs(purchaseStats) do
      if not stat:isNull() then
        transactions = transactions + stat:getTransactions()
        totalPrice = totalPrice + stat:getTotalPrice()
        local newHigh = stat:getHighestPrice()
        if newHigh > highestPrice then
          highestPrice = newHigh
        end
        local newLow = stat:getLowestPrice()
        -- ?? getting '0xffffffff' result from lowest price in 9.60 cipsoft
        if (lowestPrice == 0 or newLow < lowestPrice) and newLow ~= 0xffffffff then
          lowestPrice = newLow
        end
      end
    end

    buyStatsTable:addRow({{text = 'Total Transations:'},{text = transactions}})
    buyStatsTable:addRow({{text = 'Highest Price:'}, {text = highestPrice}})

    if totalPrice > 0 and transactions > 0 then
      buyStatsTable:addRow({{text = 'Average Price:'},
        {text = math.floor(totalPrice/transactions)}})
    else
      buyStatsTable:addRow({{text = 'Average Price:'}, {text = 0}})
    end

    buyStatsTable:addRow({{text = 'Lowest Price:'}, {text = lowestPrice}})
  end
end

local function updateSelectedItem(widget)
  selectedItem.item = widget.item
  selectedItem.ref = widget

  Market.resetCreateOffer()
  if Market.isItemSelected() then
    selectedItem:setItem(selectedItem.item.displayItem)
    nameLabel:setText(selectedItem.item.marketData.name)
    clearOffers()

    Market.enableCreateOffer(true) -- update offer types
    MarketProtocol.sendMarketBrowse(selectedItem.item.marketData.tradeAs) -- send browsed msg
  else
    Market.clearSelectedItem()
  end
end

local function updateBalance(balance)
  local balance = tonumber(balance)
  if not balance then
    return
  end

  if balance < 0 then balance = 0 end
  information.balance = balance

  balanceLabel:setText('Balance: '..balance..' gold')
  balanceLabel:resizeToText()
end

local function updateFee(price, amount)
  fee = math.ceil(price / 100 * amount)
  if fee < 20 then
    fee = 20
  elseif fee > 1000 then
    fee = 1000
  end
  feeLabel:setText('Fee: '..fee)
  feeLabel:resizeToText()
end

local function destroyAmountWindow()
  if amountWindow then
    amountWindow:destroy()
    amountWindow = nil
  end
end

local function cancelMyOffer(actionType)
  local offer = selectedMyOffer[actionType]
  MarketProtocol.sendMarketCancelOffer(offer:getTimeStamp(), offer:getCounter())
  Market.refreshMyOffers()
end

local function openAmountWindow(callback, actionType, actionText)
  if not Market.isOfferSelected(actionType) then
    return
  end

  amountWindow = g_ui.createWidget('AmountWindow', rootWidget)
  amountWindow:lock()

  local offer = selectedOffer[actionType]
  local item = offer:getItem()

  local maximum = offer:getAmount()
  if actionType == MarketAction.Sell then
    local depot = Market.getDepotCount(item:getId())
    if maximum > depot then
      maximum = depot
    end
  else
    maximum = math.min(maximum, math.floor(information.balance / offer:getPrice()))
  end

  if item:isStackable() then
    maximum = math.min(maximum, MarketMaxAmountStackable)
  else
    maximum = math.min(maximum, MarketMaxAmount)
  end

  local itembox = amountWindow:getChildById('item')
  itembox:setItemId(item:getId())

  local scrollbar = amountWindow:getChildById('amountScrollBar')
  scrollbar:setText(offer:getPrice()..'gp')

  scrollbar.onValueChange = function(widget, value)
    widget:setText((value*offer:getPrice())..'gp')
    itembox:setText(value)
  end

  scrollbar:setRange(1, maximum)
  scrollbar:setValue(1)

  local okButton = amountWindow:getChildById('buttonOk')
  if actionText then
    okButton:setText(actionText)
  end

  local okFunc = function()
    local counter = offer:getCounter()
    local timestamp = offer:getTimeStamp()
    callback(scrollbar:getValue(), timestamp, counter)
    destroyAmountWindow()
  end

  local cancelButton = amountWindow:getChildById('buttonCancel')
  local cancelFunc = function()
    destroyAmountWindow()
  end

  amountWindow.onEnter = okFunc
  amountWindow.onEscape = cancelFunc

  okButton.onClick = okFunc
  cancelButton.onClick = cancelFunc
end

local function onSelectSellOffer(table, selectedRow, previousSelectedRow)
  updateBalance()
  for _, offer in pairs(marketOffers[MarketAction.Sell]) do
    if offer:isEqual(selectedRow.ref) then
      selectedOffer[MarketAction.Buy] = offer
    end
  end

  local offer = selectedOffer[MarketAction.Buy]
  if offer then
    local price = offer:getPrice()
    if price > information.balance then
      balanceLabel:setColor('#b22222') -- red
      buyButton:setEnabled(false)
    else
      local slice = (information.balance / 2)
      if (price/slice) * 100 <= 40 then
        color = '#008b00' -- green
      elseif (price/slice) * 100 <= 70 then
        color = '#eec900' -- yellow
      else
        color = '#ee9a00' -- orange
      end
      balanceLabel:setColor(color)
      buyButton:setEnabled(true)
    end
  end
end

local function onSelectBuyOffer(table, selectedRow, previousSelectedRow)
  updateBalance()
  for _, offer in pairs(marketOffers[MarketAction.Buy]) do
    if offer:isEqual(selectedRow.ref) then
      selectedOffer[MarketAction.Sell] = offer
      if Market.getDepotCount(offer:getItem():getId()) > 0 then
        sellButton:setEnabled(true)
      else
        sellButton:setEnabled(false)
      end
    end
  end
end

local function onSelectMyBuyOffer(table, selectedRow, previousSelectedRow)
  for _, offer in pairs(marketOffers[MarketAction.Buy]) do
    if offer:isEqual(selectedRow.ref) then
      selectedMyOffer[MarketAction.Buy] = offer
      buyCancelButton:setEnabled(true)
    end
  end
end

local function onSelectMySellOffer(table, selectedRow, previousSelectedRow)
  for _, offer in pairs(marketOffers[MarketAction.Sell]) do
    if offer:isEqual(selectedRow.ref) then
      selectedMyOffer[MarketAction.Sell] = offer
      sellCancelButton:setEnabled(true)
    end
  end
end

local function onChangeCategory(combobox, option)
  local id = getMarketCategoryId(option)
  if id == MarketCategory.MetaWeapons then
    -- enable and load weapons filter/items
    subCategoryList:setEnabled(true)
    slotFilterList:setEnabled(true)
    local subId = getMarketCategoryId(subCategoryList:getCurrentOption().text)
    Market.loadMarketItems(subId)
  else
    subCategoryList:setEnabled(false)
    slotFilterList:setEnabled(false)
    Market.loadMarketItems(id) -- load standard filter
  end
end

local function onChangeSubCategory(combobox, option)
  Market.loadMarketItems(getMarketCategoryId(option))
  slotFilterList:clearOptions()

  local subId = getMarketCategoryId(subCategoryList:getCurrentOption().text)
  local slots = MarketCategoryWeapons[subId].slots
  for _, slot in pairs(slots) do
    if table.haskey(MarketSlotFilters, slot) then
      slotFilterList:addOption(MarketSlotFilters[slot])
    end
  end
  slotFilterList:setEnabled(true)
end

local function onChangeSlotFilter(combobox, option)
  Market.updateCurrentItems()
end

local function onChangeOfferType(combobox, option)
  local item = selectedItem.item
  local maximum = item.thingType:isStackable() and MarketMaxAmountStackable or MarketMaxAmount

  if option == 'Sell' then
    maximum = math.min(maximum, Market.getDepotCount(item.marketData.tradeAs))
    amountEdit:setMaximum(maximum)
  else
    amountEdit:setMaximum(maximum)
  end
end

local function onTotalPriceChange()
  local amount = amountEdit:getValue()
  local totalPrice = totalPriceEdit:getValue()
  local piecePrice = math.floor(totalPrice/amount)

  piecePriceEdit:setValue(piecePrice, true)
  if Market.isItemSelected() then
    updateFee(piecePrice, amount)
  end
end

local function onPiecePriceChange()
  local amount = amountEdit:getValue()
  local totalPrice = totalPriceEdit:getValue()
  local piecePrice = piecePriceEdit:getValue()

  totalPriceEdit:setValue(piecePrice*amount, true)
  if Market.isItemSelected() then
    updateFee(piecePrice, amount)
  end
end

local function onAmountChange()
  local amount = amountEdit:getValue()
  local piecePrice = piecePriceEdit:getValue()
  local totalPrice = piecePrice * amount

  totalPriceEdit:setValue(piecePrice*amount, true)
  if Market.isItemSelected() then
    updateFee(piecePrice, amount)
  end
end

local function onMarketMessage(messageMode, message)
  Market.displayMessage(message)
end

local function initMarketItems()
  for c = MarketCategory.First, MarketCategory.Last do
    marketItems[c] = {}
  end

  -- save a list of items which are already added
  local itemSet = {}

  -- populate all market items
  local types = g_things.findThingTypeByAttr(ThingAttrMarket, 0)
  for i = 1, #types do
    local itemType = types[i]

    local item = Item.create(itemType:getId())
    if item then
      local marketData = itemType:getMarketData()
      if not table.empty(marketData) and not itemSet[marketData.tradeAs] then
          -- Some items use a different sprite in Market
          item:setId(marketData.showAs)

          -- create new marketItem block
          local marketItem = {
            displayItem = item,
            thingType = itemType,
            marketData = marketData
          }

          -- add new market item
          table.insert(marketItems[marketData.category], marketItem)
          itemSet[marketData.tradeAs] = true
      end
    end
  end
end

local function initInterface()
  -- TODO: clean this up
  -- setup main tabs
  mainTabBar = marketWindow:getChildById('mainTabBar')
  mainTabBar:setContentWidget(marketWindow:getChildById('mainTabContent'))

  -- setup 'Market Offer' section tabs
  marketOffersPanel = g_ui.loadUI('ui/marketoffers')
  mainTabBar:addTab(tr('Market Offers'), marketOffersPanel)

  selectionTabBar = marketOffersPanel:getChildById('leftTabBar')
  selectionTabBar:setContentWidget(marketOffersPanel:getChildById('leftTabContent'))

  browsePanel = g_ui.loadUI('ui/marketoffers/browse')
  selectionTabBar:addTab(tr('Browse'), browsePanel)

  -- Currently not used
  -- "Reserved for more functionality later"
  --overviewPanel = g_ui.loadUI('ui/marketoffers/overview')
  --selectionTabBar:addTab(tr('Overview'), overviewPanel)

  displaysTabBar = marketOffersPanel:getChildById('rightTabBar')
  displaysTabBar:setContentWidget(marketOffersPanel:getChildById('rightTabContent'))

  itemStatsPanel = g_ui.loadUI('ui/marketoffers/itemstats')
  displaysTabBar:addTab(tr('Statistics'), itemStatsPanel)

  itemDetailsPanel = g_ui.loadUI('ui/marketoffers/itemdetails')
  displaysTabBar:addTab(tr('Details'), itemDetailsPanel)

  itemOffersPanel = g_ui.loadUI('ui/marketoffers/itemoffers')
  displaysTabBar:addTab(tr('Offers'), itemOffersPanel)
  displaysTabBar:selectTab(displaysTabBar:getTab(tr('Offers')))

  -- setup 'My Offer' section tabs
  myOffersPanel = g_ui.loadUI('ui/myoffers')
  mainTabBar:addTab(tr('My Offers'), myOffersPanel)

  offersTabBar = myOffersPanel:getChildById('offersTabBar')
  offersTabBar:setContentWidget(myOffersPanel:getChildById('offersTabContent'))

  currentOffersPanel = g_ui.loadUI('ui/myoffers/currentoffers')
  offersTabBar:addTab(tr('Current Offers'), currentOffersPanel)

  offerHistoryPanel = g_ui.loadUI('ui/myoffers/offerhistory')
  offersTabBar:addTab(tr('Offer History'), offerHistoryPanel)

  balanceLabel = marketWindow:getChildById('balanceLabel')

  -- setup offers
  buyButton = itemOffersPanel:getChildById('buyButton')
  buyButton.onClick = function() openAmountWindow(Market.acceptMarketOffer, MarketAction.Buy, 'Buy') end

  sellButton = itemOffersPanel:getChildById('sellButton')
  sellButton.onClick = function() openAmountWindow(Market.acceptMarketOffer, MarketAction.Sell, 'Sell') end

  -- setup selected item
  nameLabel = marketOffersPanel:getChildById('nameLabel')
  selectedItem = marketOffersPanel:getChildById('selectedItem')

  -- setup create new offer
  totalPriceEdit = marketOffersPanel:getChildById('totalPriceEdit')
  piecePriceEdit = marketOffersPanel:getChildById('piecePriceEdit')
  amountEdit = marketOffersPanel:getChildById('amountEdit')
  feeLabel = marketOffersPanel:getChildById('feeLabel')
  totalPriceEdit.onValueChange = onTotalPriceChange
  piecePriceEdit.onValueChange = onPiecePriceChange
  amountEdit.onValueChange = onAmountChange

  offerTypeList = marketOffersPanel:getChildById('offerTypeComboBox')
  offerTypeList.onOptionChange = onChangeOfferType

  anonymous = marketOffersPanel:getChildById('anonymousCheckBox')
  createOfferButton = marketOffersPanel:getChildById('createOfferButton')
  createOfferButton.onClick = Market.createNewOffer
  Market.enableCreateOffer(false)

  -- setup filters
  filterButtons[MarketFilters.Vocation] = browsePanel:getChildById('filterVocation')
  filterButtons[MarketFilters.Level] = browsePanel:getChildById('filterLevel')
  filterButtons[MarketFilters.Depot] = browsePanel:getChildById('filterDepot')
  filterButtons[MarketFilters.SearchAll] = browsePanel:getChildById('filterSearchAll')

  -- set filter default values
  clearFilters()

  -- hook filters
  for _, filter in pairs(filterButtons) do
    filter.onCheckChange = Market.updateCurrentItems
  end

  searchEdit = browsePanel:getChildById('searchEdit')
  categoryList = browsePanel:getChildById('categoryComboBox')
  subCategoryList = browsePanel:getChildById('subCategoryComboBox')
  slotFilterList = browsePanel:getChildById('slotComboBox')

  slotFilterList:addOption(MarketSlotFilters[255])
  slotFilterList:setEnabled(false)

  for i = MarketCategory.First, MarketCategory.Last do
    if i >= MarketCategory.Ammunition and i <= MarketCategory.WandsRods then
      subCategoryList:addOption(getMarketCategoryName(i))
    else
      categoryList:addOption(getMarketCategoryName(i))
    end
  end
  categoryList:addOption(getMarketCategoryName(255)) -- meta weapons
  categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First))
  subCategoryList:setEnabled(false)

  -- hook item filters
  categoryList.onOptionChange = onChangeCategory
  subCategoryList.onOptionChange = onChangeSubCategory
  slotFilterList.onOptionChange = onChangeSlotFilter

  -- setup tables
  buyOfferTable = itemOffersPanel:recursiveGetChildById('buyingTable')
  sellOfferTable = itemOffersPanel:recursiveGetChildById('sellingTable')
  detailsTable = itemDetailsPanel:recursiveGetChildById('detailsTable')
  buyStatsTable = itemStatsPanel:recursiveGetChildById('buyStatsTable')
  sellStatsTable = itemStatsPanel:recursiveGetChildById('sellStatsTable')
  buyOfferTable.onSelectionChange = onSelectBuyOffer
  sellOfferTable.onSelectionChange = onSelectSellOffer

  -- setup my offers
  buyMyOfferTable = currentOffersPanel:recursiveGetChildById('myBuyingTable')
  sellMyOfferTable = currentOffersPanel:recursiveGetChildById('mySellingTable')
  buyMyOfferTable.onSelectionChange = onSelectMyBuyOffer
  sellMyOfferTable.onSelectionChange = onSelectMySellOffer

  buyCancelButton = currentOffersPanel:getChildById('buyCancelButton')
  buyCancelButton.onClick = function() cancelMyOffer(MarketAction.Buy) end

  sellCancelButton = currentOffersPanel:getChildById('sellCancelButton')
  sellCancelButton.onClick = function() cancelMyOffer(MarketAction.Sell) end


  buyStatsTable:setColumnWidth({120, 270})
  sellStatsTable:setColumnWidth({120, 270})
  detailsTable:setColumnWidth({80, 330})

  buyOfferTable:setSorting(4, TABLE_SORTING_DESC)
  sellOfferTable:setSorting(4, TABLE_SORTING_ASC)

  buyMyOfferTable:setSorting(3, TABLE_SORTING_DESC)
  sellMyOfferTable:setSorting(3, TABLE_SORTING_DESC)
end

function init()
  g_ui.importStyle('market')
  g_ui.importStyle('ui/general/markettabs')
  g_ui.importStyle('ui/general/marketbuttons')
  g_ui.importStyle('ui/general/marketcombobox')
  g_ui.importStyle('ui/general/amountwindow')

  offerExhaust[MarketAction.Sell] = 10
  offerExhaust[MarketAction.Buy] = 20

  registerMessageMode(MessageModes.Market, onMarketMessage)

  protocol.initProtocol()
  connect(g_game, { onGameEnd = Market.reset })
  connect(g_game, { onGameEnd = Market.close })
  marketWindow = g_ui.createWidget('MarketWindow', rootWidget)
  marketWindow:hide()

  initInterface() -- build interface
end

function terminate()
  Market.close()

  unregisterMessageMode(MessageModes.Market, onMarketMessage)

  protocol.terminateProtocol()
  disconnect(g_game, { onGameEnd = Market.reset })
  disconnect(g_game, { onGameEnd = Market.close })

  destroyAmountWindow()
  marketWindow:destroy()

  Market = nil
end

function Market.reset()
  balanceLabel:setColor('#bbbbbb')
  categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First))
  searchEdit:setText('')
  clearFilters()
  clearMyOffers()
  if not table.empty(information) then
    Market.updateCurrentItems()
  end
end

function Market.displayMessage(message)
  if marketWindow:isHidden() then return end

  local infoBox = displayInfoBox(tr('Market Error'), message)
  infoBox:lock()
end

function Market.clearSelectedItem()
  if Market.isItemSelected() then
    Market.resetCreateOffer(true)
    offerTypeList:clearOptions()
    offerTypeList:setText('Please Select')
    offerTypeList:setEnabled(false)

    clearOffers()
    radioItemSet:selectWidget(nil)
    nameLabel:setText('No item selected.')
    selectedItem:setItem(nil)
    selectedItem.item = nil
    selectedItem.ref:setChecked(false)
    selectedItem.ref = nil

    detailsTable:clearData()
    buyStatsTable:clearData()
    sellStatsTable:clearData()

    Market.enableCreateOffer(false)
  end
end

function Market.isItemSelected()
  return selectedItem and selectedItem.item
end

function Market.isOfferSelected(type)
  return selectedOffer[type] and not selectedOffer[type]:isNull()
end

function Market.getDepotCount(itemId)
  return information.depotItems[itemId] or 0
end

function Market.enableCreateOffer(enable)
  offerTypeList:setEnabled(enable)
  totalPriceEdit:setEnabled(enable)
  piecePriceEdit:setEnabled(enable)
  amountEdit:setEnabled(enable)
  anonymous:setEnabled(enable)
  createOfferButton:setEnabled(enable)

  local prevAmountButton = marketOffersPanel:recursiveGetChildById('prevAmountButton')
  local nextAmountButton = marketOffersPanel:recursiveGetChildById('nextAmountButton')

  prevAmountButton:setEnabled(enable)
  nextAmountButton:setEnabled(enable)
end

function Market.close(notify)
  if notify == nil then notify = true end
  if not marketWindow:isHidden() then
    marketWindow:hide()
    marketWindow:unlock()
    Market.clearSelectedItem()
    Market.reset()
    if notify then
      MarketProtocol.sendMarketLeave()
    end
  end
end

function Market.incrementAmount()
  amountEdit:setValue(amountEdit:getValue() + 1)
end

function Market.decrementAmount()
  amountEdit:setValue(amountEdit:getValue() - 1)
end

function Market.updateCurrentItems()
  local id = getMarketCategoryId(categoryList:getCurrentOption().text)
  if id == MarketCategory.MetaWeapons then
    id = getMarketCategoryId(subCategoryList:getCurrentOption().text)
  end
  Market.loadMarketItems(id)
end

function Market.resetCreateOffer(resetFee)
  piecePriceEdit:setValue(1)
  totalPriceEdit:setValue(1)
  amountEdit:setValue(1)
  refreshTypeList()

  if resetFee then
    clearFee()
  else
    updateFee(0, 0)
  end
end

function Market.refreshItemsWidget(selectItem)
  local selectItem = selectItem or 0
  itemsPanel = browsePanel:recursiveGetChildById('itemsPanel')

  local layout = itemsPanel:getLayout()
  layout:disableUpdates()

  Market.clearSelectedItem()
  itemsPanel:destroyChildren()

  if radioItemSet then
    radioItemSet:destroy()
  end
  radioItemSet = UIRadioGroup.create()

  local select = nil
  for i = 1, #currentItems do
    local item = currentItems[i]
    local itemBox = g_ui.createWidget('MarketItemBox', itemsPanel)
    itemBox.onCheckChange = Market.onItemBoxChecked
    itemBox.item = item

    if selectItem > 0 and item.marketData.tradeAs == selectItem then
      select = itemBox
      selectItem = 0
    end

    local itemWidget = itemBox:getChildById('item')
    itemWidget:setItem(item.displayItem)

    local amount = Market.getDepotCount(item.marketData.tradeAs)
    if amount > 0 then
      itemWidget:setText(amount)
      itemBox:setTooltip('You have '.. amount ..' in your depot.')
    end

    radioItemSet:addWidget(itemBox)
  end

  if select then
    radioItemSet:selectWidget(select, false)
  end

  layout:enableUpdates()
  layout:update()
end

function Market.refreshOffers()
  if Market.isItemSelected() then
    Market.onItemBoxChecked(selectedItem.ref)
  else
    Market.refreshMyOffers()
  end
end

function Market.refreshMyOffers()
  clearMyOffers()
  MarketProtocol.sendMarketBrowseMyOffers()
end


function Market.loadMarketItems(category)
  clearItems()

  -- check search filter
  local searchFilter = searchEdit:getText()
  if searchFilter and searchFilter:len() > 2 then
    if filterButtons[MarketFilters.SearchAll]:isChecked() then
      category = MarketCategory.All
    end
  end

  if category == MarketCategory.All then
    -- loop all categories
    for category = MarketCategory.First, MarketCategory.Last do
      for i = 1, #marketItems[category] do
        local item = marketItems[category][i]
        if isItemValid(item, category, searchFilter) then

          table.insert(currentItems, item)
        end
      end
    end
  else
    -- loop specific category
    for i = 1, #marketItems[category] do
      local item = marketItems[category][i]
      if isItemValid(item, category, searchFilter) then
        table.insert(currentItems, item)
      end
    end
  end

  Market.refreshItemsWidget()
end

function Market.createNewOffer()
  local type = offerTypeList:getCurrentOption().text
  if type == 'Sell' then
    type = MarketAction.Sell
  else
    type = MarketAction.Buy
  end

  if not Market.isItemSelected() then
    return
  end

  local spriteId = selectedItem.item.marketData.tradeAs

  local piecePrice = piecePriceEdit:getValue()
  local amount = amountEdit:getValue()
  local anonymous = anonymous:isChecked() and 1 or 0

  -- error checking
  local errorMsg = ''
  if type == MarketAction.Buy then
    if information.balance < ((piecePrice * amount) + fee) then
      errorMsg = errorMsg..'Not enough balance to create this offer.\n'
    end
  elseif type == MarketAction.Sell then
    if information.balance < fee then
      errorMsg = errorMsg..'Not enough balance to create this offer.\n'
    end
    if Market.getDepotCount(spriteId) < amount then
      errorMsg = errorMsg..'Not enough items in your depot to create this offer.\n'
    end
  end

  if piecePrice > piecePriceEdit.maximum then
    errorMsg = errorMsg..'Price is too high.\n'
  elseif piecePrice < piecePriceEdit.minimum then
    errorMsg = errorMsg..'Price is too low.\n'
  end

  if amount > amountEdit.maximum then
    errorMsg = errorMsg..'Amount is too high.\n'
  elseif amount < amountEdit.minimum then
    errorMsg = errorMsg..'Amount is too low.\n'
  end

  if amount * piecePrice > MarketMaxPrice then
    errorMsg = errorMsg..'Total price is too high.\n'
  end

  if information.totalOffers >= MarketMaxOffers then
    errorMsg = errorMsg..'You cannot create more offers.\n'
  end

  local timeCheck = os.time() - lastCreatedOffer
  if timeCheck < offerExhaust[type] then
    local waitTime = math.ceil(offerExhaust[type] - timeCheck)
    errorMsg = errorMsg..'You must wait '.. waitTime ..' seconds before creating a new offer.\n'
  end

  if errorMsg ~= '' then
    Market.displayMessage(errorMsg)
    return
  end

  MarketProtocol.sendMarketCreateOffer(type, spriteId, amount, piecePrice, anonymous)
  lastCreatedOffer = os.time()
  Market.resetCreateOffer()
end

function Market.acceptMarketOffer(amount, timestamp, counter)
  if timestamp > 0 and amount > 0 then
    MarketProtocol.sendMarketAcceptOffer(timestamp, counter, amount)
    Market.refreshOffers()
  end
end

function Market.onItemBoxChecked(widget)
  if widget:isChecked() then
    updateSelectedItem(widget)
  end
end

-- protocol callback functions

function Market.onMarketEnter(depotItems, offers, balance, vocation)
  if not loaded then
    initMarketItems()
    loaded = true
  end

  updateBalance(balance)
  averagePrice = 0

  information.totalOffers = offers
  local player = g_game.getLocalPlayer()
  if player then
    information.player = player
  end
  if vocation == -1 then
    if player then
      information.vocation = player:getVocation()
    end
  else
    -- vocation must be compatible with < 950
    information.vocation = vocation
  end

  -- set list of depot items
  information.depotItems = depotItems

  -- update the items widget to match depot items
  if Market.isItemSelected() then
    local spriteId = selectedItem.item.marketData.tradeAs
    MarketProtocol.silent(true) -- disable protocol messages
    Market.refreshItemsWidget(spriteId)
    MarketProtocol.silent(false) -- enable protocol messages
  else
    Market.refreshItemsWidget()
  end

  if table.empty(currentItems) then
    Market.loadMarketItems(MarketCategory.First)
  end

  if g_game.isOnline() then
    marketWindow:lock()
    marketWindow:show()
  end
end

function Market.onMarketLeave()
  Market.close(false)
end

function Market.onMarketDetail(itemId, descriptions, purchaseStats, saleStats)
  updateDetails(itemId, descriptions, purchaseStats, saleStats)
end

function Market.onMarketBrowse(offers)
  updateOffers(offers)
end
