--[[
    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
myCurrentOffersTab = nil
myOfferHistoryTab = nil
offerHistoryPanel = nil
itemsPanel = nil
selectedOffer = {}
selectedMyOffer = {}
reperaTokenOffers = {}
selectedReperaTokenOffer = nil
reperaTokenBalance = 0

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
currencyList = 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
myOfferHistoryTabel = nil

offerExhaust = {}
marketOffers = {}
marketItems = {}
marketItemNames = {}
information = {}
currentItems = {}
lastCreatedOffer = 0
fee = 0
averagePrice = 0
tibiaCoins = 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 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()
  myOfferHistoryTabel: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 = marketItemNames[offer:getItem():getId()]
  if not itemName then
    itemName = offer:getItem():getMarketData().name
  end

  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 = comma_value(price*amount), sortvalue = price*amount},
        {text = comma_value(price), sortvalue = price},
        {text = amount},
        {text = string.gsub(os.date('%H:%M %d/%m/%y', timestamp), " ", "  "), sortvalue = timestamp}
      })
    else
      row = buyOfferTable:addRow({
        {text = player},
        {text = amount},
        {text = comma_value(price*amount), sortvalue = price*amount},
        {text = comma_value(price), sortvalue = price},
        {text = string.gsub(os.date('%H:%M %d/%m/%y', 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 = comma_value(price*amount), sortvalue = price*amount},
        {text = comma_value(price), sortvalue = price},
        {text = amount},
        {text = string.gsub(os.date('%H:%M %d/%m/%y', timestamp), " ", "  "), sortvalue = timestamp}
      })
    else
      row = sellOfferTable:addRow({
        {text = player},
        {text = amount},
        {text = comma_value(price*amount), sortvalue = price*amount},
        {text = comma_value(price), sortvalue = price},
        {text = string.gsub(os.date('%H:%M %d/%m/%y', 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 appendReperaTokenOffers()
  if not sellOfferTable or not selectedItem or not selectedItem.item then
    return
  end

  local displayItem = selectedItem.item.displayItem
  local selectedClientId = displayItem and displayItem:getId() or 0
  local localPlayer = g_game.getLocalPlayer()
  local localName = localPlayer and localPlayer:getName() or ''

  for _, offer in ipairs(reperaTokenOffers or {}) do
    if tonumber(offer.currency) == 1 and tonumber(offer.clientId) == selectedClientId and tonumber(offer.amount) > 0 then
      local totalPrice = tonumber(offer.price) * tonumber(offer.amount)
      local row = sellOfferTable:addRow({
        {text = offer.seller},
        {text = offer.amount},
        {text = comma_value(totalPrice) .. ' Tokens', sortvalue = -1000000000000 + totalPrice},
        {text = comma_value(offer.price) .. ' Tokens', sortvalue = -1000000000000 + tonumber(offer.price)},
        {text = 'Repera Token offer'}
      })
      row.ref = {'repera-token', offer.id}
      row.reperaTokenOffer = offer
      if offer.seller == localName then
        row:setTooltip('Your Repera Token offer - select it to cancel.')
      else
        row:setTooltip('Repera Token offer - select it and buy with Repera Tokens.')
      end
    end
  end

  sellOfferTable:sort()
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
  selectedReperaTokenOffer = 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)
  buyButton:setText('Buy Now')

  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
  appendReperaTokenOffers()
end

local function updateHistoryOffers(offers)
  myOfferHistoryTabel:toggleSorting(false)
  for _, offer in ipairs(offers) do
    local offerType = offer:getType()
    local id = offer:getId()
    local player = offer:getPlayer()
    local amount = offer:getAmount()
    local price = offer:getPrice()
    local timestamp = offer:getTimeStamp()
    local itemName = marketItemNames[offer:getItem():getId()]
    if not itemName then
      itemName = offer:getItem():getMarketData().name
    end
        
    local offerTypeName = "?"
    if offerType == MarketAction.Buy then
      offerTypeName = "Buy"
    elseif offerType == MarketAction.Sell then
      offerTypeName = "Sell"
    end
    
    local row = myOfferHistoryTabel:addRow({
      {text = offerTypeName},
      {text = itemName},
      {text = comma_value(price*amount), sortvalue = price*amount},
      {text = comma_value(price), sortvalue = price},
      {text = amount},
      {text = string.gsub(os.date('%H:%M %d/%m/%y', timestamp), " ", "  "), sortvalue = timestamp}
    })  
  end
  myOfferHistoryTabel:toggleSorting(false)
  myOfferHistoryTabel:sort()
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: '.. comma_value(balance) ..' gold')
  balanceLabel:resizeToText()
end

local function updateFee(price, amount)
  local currency = currencyList and currencyList:getCurrentOption().data or 0
  if currency == 1 then
    fee = price * amount * 500
    feeLabel:setText('Listing fee from bank: '.. comma_value(fee) .. ' GP (500 GP/token)')
  else
    fee = math.ceil(price * amount * 0.10)
    feeLabel:setText('Listing fee from bank: '.. comma_value(fee) .. ' GP (10%)')
  end
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(comma_value(offer:getPrice()) ..'gp')

  scrollbar.onValueChange = function(widget, value)
    widget:setText(comma_value(value*offer:getPrice())..'gp')
    itembox:setText(comma_value(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 openReperaTokenAmountWindow()
  local offer = selectedReperaTokenOffer
  if not offer then
    return
  end

  local player = g_game.getLocalPlayer()
  if player and offer.seller == player:getName() then
    modules.game_repera_market.cancelOffer(offer.id)
    selectedReperaTokenOffer = nil
    return
  end

  local price = math.max(1, tonumber(offer.price) or 1)
  local maximum = math.min(tonumber(offer.amount) or 1, math.floor(reperaTokenBalance / price))
  if maximum < 1 then
    Market.displayMessage('You do not have enough Repera Tokens for this offer.')
    return
  end

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

  local itembox = amountWindow:getChildById('item')
  itembox:setItemId(tonumber(offer.clientId) or 0)

  local scrollbar = amountWindow:getChildById('amountScrollBar')
  scrollbar.onValueChange = function(widget, value)
    widget:setText(comma_value(value * price) .. ' Tokens')
    itembox:setText(comma_value(value))
  end
  scrollbar:setRange(1, maximum)
  scrollbar:setValue(1)
  scrollbar:setText(comma_value(price) .. ' Tokens')

  local okButton = amountWindow:getChildById('buttonOk')
  okButton:setText('Buy for Tokens')
  local okFunc = function()
    modules.game_repera_market.buyOffer(offer.id, scrollbar:getValue())
    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()
  selectedReperaTokenOffer = selectedRow and selectedRow.reperaTokenOffer or nil
  if selectedReperaTokenOffer then
    selectedOffer[MarketAction.Buy] = nil
    local player = g_game.getLocalPlayer()
    local ownOffer = player and selectedReperaTokenOffer.seller == player:getName()
    buyButton:setText(ownOffer and 'Cancel Offer' or 'Buy for Tokens')
    buyButton:setEnabled(true)
    return
  end

  buyButton:setText('Buy Now')
  if not selectedRow then
    buyButton:setEnabled(false)
    return
  end
  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

-- Protocol 8 clients receive only a very small/legacy market category set from
-- the server.  Repera has many newer/custom items, so put them in useful native
-- Market categories without changing their DAT/SPR or server item ids.
local function inferReperaMarketCategory(entry, thingType)
  local name = string.lower(entry.name or '')
  local slot = thingType and thingType:getClothSlot() or 0

  if slot == InventorySlotHead then return MarketCategory.HelmetsHats end
  if slot == InventorySlotBody then return MarketCategory.Armors end
  if slot == InventorySlotLeg then return MarketCategory.Legs end
  if slot == InventorySlotFeet then return MarketCategory.Boots end
  if slot == InventorySlotNeck then return MarketCategory.Amulets end
  if slot == InventorySlotFinger then return MarketCategory.Rings end
  if slot == InventorySlotAmmo then return MarketCategory.Ammunition end
  if slot == InventorySlotBack then return MarketCategory.Containers end

  local function has(words)
    for _, word in ipairs(words) do
      if string.find(name, word, 1, true) then return true end
    end
    return false
  end

  if has({'shield', 'spellbook'}) then return MarketCategory.Shields end
  if has({'helmet', 'hat', 'hood', 'mask', 'crown', 'tiara'}) then return MarketCategory.HelmetsHats end
  if has({'armor', 'armour', 'plate', 'robe', 'jacket', 'coat', 'tunic'}) then return MarketCategory.Armors end
  if has({'legs', 'trousers', 'kilt', 'skirt'}) then return MarketCategory.Legs end
  if has({'boots', 'shoes', 'sandals', 'slippers'}) then return MarketCategory.Boots end
  if has({'amulet', 'necklace', 'scarf', 'collar'}) then return MarketCategory.Amulets end
  if has({'ring'}) then return MarketCategory.Rings end
  if has({'gold coin', 'platinum coin', 'crystal coin', 'repera token', 'rift stone', 'gem', 'ruby', 'sapphire', 'emerald', 'diamond', 'pearl'}) then return MarketCategory.Valuables end
  if has({'arrow', 'bolt', 'spear', 'javelin', 'throwing star', 'throwing stone', 'small stone', 'quiver'}) then return MarketCategory.Ammunition end
  if has({'backpack', 'bag', 'box', 'chest', 'parcel', 'basket'}) then return MarketCategory.Containers end
  if has({'potion', 'flask', 'vial'}) then return MarketCategory.Potions end
  if has({'rune'}) then return MarketCategory.Runes end
  if has({'meat', 'ham', 'bread', 'cheese', 'fish', 'apple', 'banana', 'orange', 'grape', 'mushroom', 'cookie', 'cake'}) then return MarketCategory.Food end
  if has({'bar', 'ingot', 'ore', 'dust', 'powder', 'essence', 'fragment', 'shard', 'crystal', 'thread', 'cloth', 'leather', 'hide', 'fur', 'feather', 'scale', 'bone', 'skull', 'claw', 'fang', 'tooth', 'horn', 'wing', 'tail', 'eye', 'heart', 'shell', 'wood', 'plank'}) then
    return MarketCategory.CreatureProducs
  end
  if has({'axe', 'hatchet'}) then return MarketCategory.Axes end
  if has({'club', 'mace', 'hammer', 'maul'}) then return MarketCategory.Clubs end
  if has({'bow', 'crossbow'}) then return MarketCategory.DistanceWeapons end
  if has({'sword', 'blade', 'sabre', 'saber', 'scimitar', 'dagger', 'knife'}) then return MarketCategory.Swords end
  if has({'wand', 'rod'}) then return MarketCategory.WandsRods end
  if has({'rope', 'shovel', 'pick', 'machete', 'scythe', 'fishing rod', 'tool'}) then return MarketCategory.Tools end

  return entry.category
end

-- Keep the native Market focused on objects a player can genuinely trade and
-- use. Older 8.0 data marks many map decorations as generic market items.
local reperaDeniedMarketWords = {
  'corpse','dead ','remains','chair','table','bed','bench','sofa','armchair','stool',
  'bookcase','bookshelf','cabinet','drawer','wardrobe','dresser','wall','floor','roof',
  'door','window','gate','fence','statue','pillar','column','tapestry','painting','banner',
  'street lamp','lamp post','counter','oven','furnace','chimney','depot','inbox','market',
  'mailbox','trash holder','teleport','locker','blackboard','sign','torch bearer','candelabrum',
  'chandelier','trough','barrel','crate','drawers','piano','rock','stone wall','mountain',
  'water','lava','swamp','grass','dirt','sand','snow','ice wall','tree','bush','plant',
  'flower pot','carpet','rug','curtain','railing','stairs','ladder','bridge','fountain',
  'monument','gravestone','tombstone','coffin','sarcophagus','armor rack','armour rack',
  'weapon rack','rack kit','hammock','pillow','candle','dustbin','steering wheel','wooden bars',
  'wooden decoration','wooden doll','wooden flute','wooden spoon','wooden whistle','wooden trash',
  'gemmed lamp','gemmed book','gem holder','flower bowl','skull candle','surprise bag',
  'premium scroll','blank scroll','burnt scroll'
}

local reperaAllowedMarketWords = {
  'helmet','hat','hood','mask','crown','tiara','armor','armour','plate','robe','jacket','coat',
  'tunic','legs','trousers','kilt','skirt','boots','shoes','sandals','slippers','shield',
  'spellbook','amulet','necklace','scarf','collar','ring','sword','blade','sabre','saber',
  'scimitar','dagger','knife','axe','hatchet','club','mace','hammer','maul','bow','crossbow',
  'wand','rod','spear','javelin','throwing star','throwing stone','small stone','arrow','bolt',
  'quiver','backpack',' bag','potion','flask','vial','rune','meat','ham','bread','cheese',
  'fish','apple','banana','orange','grape','mushroom','cookie','cake','rope','shovel','pick',
  'machete','scythe','fishing rod','gold coin','platinum coin','crystal coin','repera token',
  'rift stone','gem','ruby','sapphire','emerald','diamond','pearl','bar','ingot','ore','dust',
  'powder','essence','fragment','shard','crystal','thread','cloth','leather','hide','fur',
  'feather','scale','bone','skull','claw','fang','tooth','horn','wing','tail','eye','heart',
  'shell','wood','plank','scales','bones','claws','fangs','teeth','horns','wings','tails',
  'eyes','hearts','shells','planks','bars','relic','talisman','recipe','blueprint','riftforge','reinforcement',
  'upgrade','scroll','reward casket'
}

local function reperaNameHasAny(name, words)
  local normalized = ' ' .. string.gsub(name, '[^%w]+', ' ') .. ' '
  for _, word in ipairs(words) do
    local normalizedWord = string.gsub(word, '[^%w]+', ' ')
    if string.find(normalized, ' ' .. normalizedWord .. ' ', 1, true) then return true end
  end
  return false
end

local function isReperaPlayerMarketEntry(entry, thingType)
  local name = string.lower(entry.name or '')
  if name == '' or reperaNameHasAny(name, reperaDeniedMarketWords) then return false end
  if thingType and thingType:getClothSlot() ~= 0 then return true end
  return reperaNameHasAny(name, reperaAllowedMarketWords)
end

local function initMarketItems(items)
  for c = MarketCategory.First, MarketCategory.Last do
    marketItems[c] = {}
  end
  marketItemNames = {}
  
  -- save a list of items which are already added
  local itemSet = {}
  
  -- parse items send by server
  if items then
    for _, entry in ipairs(items) do
      local item = Item.create(entry.id)
      local thingType = g_things.getThingType(entry.id, ThingCategoryItem)
      if item and thingType and not marketItemNames[entry.id] and isReperaPlayerMarketEntry(entry, thingType) then
        local category = inferReperaMarketCategory(entry, thingType)
        -- create new marketItem block
        local marketItem = {
          displayItem = item,
          thingType = thingType,
          marketData = {
            name = entry.name,
            category = category,
            requiredLevel = 0,
            restrictVocation = 0,
            showAs = entry.id,
            tradeAs = entry.id
          }
        }
     
        -- add new market item
        if marketItems[category] ~= nil then
          table.insert(marketItems[category], marketItem)
          marketItemNames[entry.id] = entry.name
        end
      end
    end
    Market.updateCategories()
    return
  end

  -- 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
          if marketItems[marketData.category] ~= nil then
            table.insert(marketItems[marketData.category], marketItem)
            itemSet[marketData.tradeAs] = true
          end
      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')
  local myOffersTab = mainTabBar:addTab(tr('My Offers'), myOffersPanel)

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

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

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

  balanceLabel = marketWindow:getChildById('balanceLabel')

  mainTabBar.onTabChange = function(widget, tab)
    if tab == myOffersTab then
      local ctab = offersTabBar:getCurrentTab()
      if ctab == myCurrentOffersTab then
        Market.refreshMyOffers()    
      elseif ctab == myOfferHistoryTab then
        Market.refreshMyOffersHistory()
      end      
    else
      Market.refreshOffers()
    end
  end
  
  offersTabBar.onTabChange = function(widget, tab)
    if tab == myCurrentOffersTab then
      Market.refreshMyOffers()    
    elseif tab == myOfferHistoryTab then
      Market.refreshMyOffersHistory()
    end
  end

  -- setup offers
  buyButton = itemOffersPanel:getChildById('buyButton')
  buyButton.onClick = function()
    if selectedReperaTokenOffer then
      openReperaTokenAmountWindow()
    else
      openAmountWindow(Market.acceptMarketOffer, MarketAction.Buy, 'Buy')
    end
  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
  currencyList = marketOffersPanel:getChildById('currencyComboBox')
  currencyList:addOption('Gold (GP)', 0)
  currencyList:addOption('Repera Tokens', 1)
  currencyList.onOptionChange = function(_, option)
    if option and option.data == 1 then
      offerTypeList:clearOptions()
      offerTypeList:addOption('Sell')
      offerTypeList:setCurrentOption('Sell')
      offerTypeList:setEnabled(false)
    else
      offerTypeList:setEnabled(true)
      refreshTypeList()
    end
    updateFee(piecePriceEdit:getValue(), amountEdit:getValue())
  end
  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)
  
  Market.updateCategories()
  
  -- 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')
  myOfferHistoryTabel = offerHistoryPanel:recursiveGetChildById('myHistoryTable')
  
  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)
  myOfferHistoryTabel:setSorting(6, TABLE_SORTING_DESC)
end

function init()
  g_logger.info('[ReperaMarket] native Market module loaded')
  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 })
  connect(g_game, { onGameStart = Market.updateCategories })
  connect(g_game, { onCoinBalance = Market.onCoinBalance })
  
  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 })
  disconnect(g_game, { onGameStart = Market.updateCategories })
  disconnect(g_game, { onCoinBalance = Market.onCoinBalance })

  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.updateCategories()
  categoryList:clearOptions()
  subCategoryList:clearOptions()
  
  local categories = {}
  local addedCategories = {}
  for _, c in ipairs(g_things.getMarketCategories()) do
    table.insert(categories, getMarketCategoryName(c) or "Unknown")
    addedCategories[c] = true
  end
  for c, items in ipairs(marketItems) do
    if #items > 0 and not addedCategories[c] then
      table.insert(categories, getMarketCategoryName(c) or "Unknown")      
      addedCategories[c] = true
    end
  end
  
  table.sort(categories)
  for _, c in ipairs(categories) do
      categoryList:addOption(c)
  end
  
  for i = MarketCategory.Ammunition, MarketCategory.WandsRods do
    subCategoryList:addOption(getMarketCategoryName(i))
  end
  
  categoryList:addOption(getMarketCategoryName(255)) -- meta weapons
  categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First))
  subCategoryList:setEnabled(false)
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)
  if not information.depotItems then
    return 0
  end
  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()
    modules.game_interface.getRootPanel():focus()
    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()
  if not categoryList or not categoryList:getCurrentOption() then 
    return 
  end
  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(comma_value(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
    local ctab = offersTabBar:getCurrentTab()
    if ctab == myCurrentOffersTab then
      Market.refreshMyOffers()    
    elseif ctab == myOfferHistoryTab then
      Market.refreshMyOffersHistory()
    end
  end
end

function Market.setReperaTokenOffers(offers, accountTokens, depotTokens)
  reperaTokenOffers = offers or {}
  reperaTokenBalance = (tonumber(accountTokens) or 0) + (tonumber(depotTokens) or 0)
  if Market.isItemSelected() then
    Market.refreshOffers()
  end
end

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

function Market.refreshMyOffersHistory()
  clearMyOffers()
  MarketProtocol.sendMarketBrowseMyHistory()
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 not marketItems[category] and category ~= MarketCategory.All then
    return
  end

  if category == MarketCategory.All then
    -- loop all categories
    for category = MarketCategory.First, MarketCategory.Last do
      if marketItems[category] then
        for i = 1, #marketItems[category] do
          local item = marketItems[category][i]
          if isItemValid(item, category, searchFilter) then
            table.insert(currentItems, item)
          end
        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
  local currency = currencyList and currencyList:getCurrentOption().data or 0

  -- Token offers use Repera's safe depot/account settlement, but are created
  -- from this native Market form. There is deliberately no second Market UI.
  if currency == 1 then
    -- The native combo can remain on Buy after changing the selected item.
    -- Token listings are sell-only, so normalize the action automatically.
    type = MarketAction.Sell
    if Market.getDepotCount(spriteId) < amount then
      Market.displayMessage('Not enough items in your depot to create this offer.')
      return
    end
    if amount < 1 or piecePrice < 1 then
      Market.displayMessage('Amount and token price must be greater than zero.')
      return
    end
    if amount > amountEdit.maximum or piecePrice > piecePriceEdit.maximum or amount * piecePrice > MarketMaxPrice then
      Market.displayMessage('The token offer exceeds the Market limits.')
      return
    end
    if information.totalOffers >= MarketMaxOffers then
      Market.displayMessage('You cannot create more offers.')
      return
    end
    local tokenTimeCheck = os.time() - lastCreatedOffer
    if tokenTimeCheck < offerExhaust[MarketAction.Sell] then
      Market.displayMessage('You must wait ' .. math.ceil(offerExhaust[MarketAction.Sell] - tokenTimeCheck) .. ' seconds before creating a new offer.')
      return
    end
    if information.balance < fee then
      Market.displayMessage('Not enough gold in your bank to pay the listing fee.')
      return
    end
    local clientId = selectedItem.item.displayItem and selectedItem.item.displayItem:getId() or 0
    modules.game_repera_market.createOffer(spriteId, amount, piecePrice, clientId,
      selectedItem.item.marketData and selectedItem.item.marketData.name or '')
    lastCreatedOffer = os.time()
    Market.resetCreateOffer()
    return
  end

  -- 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, items)
  if not loaded or (items and #items > 0) then
    initMarketItems(items)
    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

  for i = 1, #marketItems[MarketCategory.TibiaCoins] do
    local item = marketItems[MarketCategory.TibiaCoins][i].displayItem
    depotItems[item:getId()] = tibiaCoins
  end
  
  -- 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()
    if modules.game_repera_market then
      modules.game_repera_market.refresh()
    end
  end
end

function Market.onMarketLeave()
  selectedReperaTokenOffer = nil
  Market.close(false)
end

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

function Market.onMarketBrowse(offers, offersType)    
  if offersType == MarketRequest.MyHistory then
    updateHistoryOffers(offers)
  else
    updateOffers(offers)  
  end
end

function Market.onCoinBalance(coins, transferableCoins)
  tibiaCoins = coins
  if not marketItems[MarketCategory.TibiaCoins] then return end
  for i = 1, #marketItems[MarketCategory.TibiaCoins] do
    local item = marketItems[MarketCategory.TibiaCoins][i].displayItem
    information.depotItems[item:getId()] = tibiaCoins
  end  
end
