-------------------------------------------------------------------------------
-- BuyFrame.lua
--
-- Implements the "Buy" tab.
-------------------------------------------------------------------------------

local _
local L = LibStub("AceLocale-3.0"):GetLocale("AuctionLite", false)

-- Constants for display elements.
local BUY_DISPLAY_SIZE = 15;
local ROW_HEIGHT = 21;
local EXPAND_ROWS = 4;

-- Various states for the buy frame.
local BUY_MODE_NONE        = 1;
local BUY_MODE_INTRO       = 2;
local BUY_MODE_SEARCH      = 3;
local BUY_MODE_SCAN        = 4;
local BUY_MODE_DEALS       = 5;
local BUY_MODE_FAVORITES   = 6;
local BUY_MODE_MY_AUCTIONS = 7;

-- Current state of the buy frame.
local BuyMode = BUY_MODE_NONE;

-- Current sorting state.
local SummarySort = {
  sort = "ItemSummary",
  flipped = false,
  justFlipped = false,
  sorted = false,
};
local DetailSort = {
  sort = "BuyoutEach",
  flipped = false,
  justFlipped = false,
  sorted = false,
};

-- Height of expandable frame.
local ExpandHeight = 0;

-- Data to be shown in detail view.
local DetailLink = nil;
local DetailData = {};
local SavedOffset = 0;

-- Save the last selected item.
local DetailLinkPrev = nil;

-- Selected item in detail view and index of last item clicked.
local SelectedItems = {};
local LastClick = nil;

-- Data to be shown in summary view.
local SummaryData = {};
local SummaryDataByLink = {};
local NoResults = false;

-- Info about current purchase for display in expandable frame.
local PurchaseOrder = nil;

-- Information about current search progress.
local StartTime = nil;
local LastTime = nil;
local LastRemaining = nil;
local Progress = nil;
local GetAll = nil;
local Scanning = nil;

-- Stored scan data from the latest full scan.
local ScanData = nil;

-- Links and data for multi-item scan.
local MultiScanCurrent = nil;
local MultiScanOriginal = {};
local MultiScanItems = {};
local MultiScanData = {};

-- Static popup advertising AL's fast scan.
dwStaticPopupDialogs["AL_FAST_SCAN"] = {
  text = L["FAST_SCAN_AD"],
  button1 = L["Enable"],
  button2 = L["Disable"],
  OnAccept = function(self)
    AuctionLite:Print(L["Fast auction scan enabled."]);
    AuctionLite.db.profile.fastScanAd2 = true;
    AuctionLite.db.profile.getAll = true;
    AuctionLite:StartFullScan();
  end,
  OnCancel = function(self)
    AuctionLite:Print(L["Fast auction scan disabled."]);
    AuctionLite.db.profile.fastScanAd2 = true;
    AuctionLite.db.profile.getAll = false;
    AuctionLite:StartFullScan();
  end,
  showAlert = 1,
  timeout = 0,
  exclusive = 1,
  hideOnEscape = 1,
  preferredIndex = 3
};

-- Static popup explaining cancel clicks.
dwStaticPopupDialogs["AL_CANCEL_NOTE"] = {
  text = L["CANCEL_NOTE"],
  button1 = OKAY,
  OnAccept = function(self)
    AuctionLite.db.profile.cancelNote = true;
  end,
  showAlert = 1,
  timeout = 0,
  exclusive = 1,
  hideOnEscape = 1,
  preferredIndex = 3
};

-- Set current item to be shown in detail view, and update dependent data.
function AuctionLite:SetDetailLink(link)
  -- If there's no data to show, then just set the link to nil.
  if link ~= nil and table.getn(SummaryDataByLink[link].data) == 0 then
    link = nil;
  end

  -- If we're leaving the summary view, save our offset.
  if DetailLink == nil then
    SavedOffset = FauxScrollFrame_GetOffset(BuyScrollFrame);
  end

  -- Set the new detail link, if any.
  DetailLink = link;

  local offset;
  if DetailLink ~= nil then
    DetailData = SummaryDataByLink[DetailLink].data;
    offset = 0;
  else
    DetailData = {};
    offset = SavedOffset;
  end

  DetailSort.sorted = false;

  -- Return to our saved offset.
  self:SetScrollFrameOffset(offset);

  SelectedItems = {};
  LastClick = nil;
end

-- Set the data for the scrolling frame.
function AuctionLite:SetBuyData(results)
  SummaryData = {};

  local count = 0;
  local last = nil;
  local foundPrev = false;

  -- Sort everything and assemble the summary data.
  for link, result in pairs(results) do
    table.insert(SummaryData, result);

    result.link = link;
    result.name = self:SplitLink(link);

    local hist = self:GetHistoricalPrice(link);
    if hist ~= nil then
      result.histPrice = hist.price;
    else
      result.histPrice = 0;
    end

    count = count + 1;
    last = link;

    if DetailLinkPrev == link then
      foundPrev = true;
    end
  end

  SummaryDataByLink = results;

  -- Sort our data by name/profit.
  if BuyMode == BUY_MODE_DEALS or
     BuyMode == BUY_MODE_SCAN then
    SummarySort.sort = "Historical";
    SummarySort.flipped = true;
  else
    SummarySort.sort = "ItemSummary";
    SummarySort.flipped = false;
  end

  SummarySort.sorted = false;
  SummarySort.justFlipped = false;

  -- Set up warning markers for undercuts.
  self:SetWarnings();

  -- If we found our last-selected item, then select it again.
  -- If we found only one item, select it.  Otherwise, select nothing.
  local newLink = nil;
  if foundPrev then
    newLink = DetailLinkPrev;
  elseif count == 1 then
    newLink = last;
  end
  DetailLinkPrev = nil;

  -- Reset current query info.
  self:ResetSearch();

  -- Save our data and set our detail link, if we only got one kind of item.
  SummaryDataByLink = results;
  NoResults = (count == 0);
  self:SetDetailLink(newLink);

  -- Clean up the display.
  BuyMessageText:Hide();
  BuyStatus:Hide();

  -- Start a mass buyout, if necessary.
  self:StartMassBuyout();

  -- Repaint.
  self:AuctionFrameBuy_Update();
end

-- Handle results for a full scan.  Make a list of the deals.
function AuctionLite:SetScanData(results)
  ScanData = {};

  -- Search through all scanned items.
  for link, result in pairs(results) do
    local hist = self:GetHistoricalPrice(link);

    -- Find the lowest buyout.
    local min = 0;
    for _, listing in ipairs(result.data) do
      if min == 0 or (0 < listing.buyout and listing.buyout < min) then
        min = listing.buyout;
      end
    end

    -- If it meets a bunch of conditions below, it's considered a deal.
    if min > 0 and hist ~= nil and
       min < hist.price - (10000 * self.db.profile.minProfit) and
       min < hist.price * (1 - self.db.profile.minDiscount) and
       hist.listings / hist.scans > 1.2 then

      result.profit = hist.price - min;
      ScanData[link] = result;
    end
  end

  -- Display our list of deals.
  DetailLinkPrev = nil;
  self:SetBuyData(ScanData, true);
end

-- Helper that finds user's listings.  Return value indicates whether we've
-- been undercut.  (If that's all you care about, omit the last three args.)
function AuctionLite:FindMyListings(listings, list, name, onlyUndercut)
  local listingsCopy = {};

  -- Make a copy of listings.
  for a, b in pairs(listings) do
    listingsCopy[a] = b;
  end

  -- Sort by per-item buyout price.
  table.sort(listingsCopy,
    function(a, b) return a.buyout / a.count < b.buyout / b.count end);

  -- Iterate over listings.
  local undercut = false;
  local foundOther = false;
  local listing;
  for _, listing in ipairs(listingsCopy) do
    if listing.buyout > 0 then
      if listing.owner == UnitName("player") then
        if foundOther then
          undercut = true;
          if list == nil then
            break;
          end
        end
        if list ~= nil then
          if foundOther or not onlyUndercut then
            listing.name = name;
            table.insert(list, listing);
          end
        end
      else
        foundOther = true;
      end
    end
  end

  return undercut;
end

-- Set undercut warnings for all auctions.
function AuctionLite:SetWarnings()
  if BuyMode == BUY_MODE_MY_AUCTIONS then
    for link, result in pairs(SummaryDataByLink) do
      -- Sort by per-item buyout price.
      table.sort(result.data,
        function(a, b) return a.buyout / a.count < b.buyout / b.count end);

      -- Invalidate the current sort.
      DetailSort.sorted = false;

      -- Find the first listing with non-zero buyout.  If it's not ours,
      -- we've been undercut.
      result.warning = self:FindMyListings(result.data);
    end
  end
end

-- Determine whether the selected items are biddable/buyable.
function AuctionLite:GetSelectionStatus()
  local biddable = true;
  local buyable = true;
  local cancellable = true;
  local found = false;

  -- Helper to determine whether we have any listings.
  local findMine = function(data)
    for _, listing in ipairs(data) do
      if listing.owner == UnitName("player") then
        return true;
      end
    end
    return false;
  end

  if DetailLink ~= nil then
    -- Check all selected items.
    for listing, _ in pairs(SelectedItems) do
      -- We can't bid/buy our own auctions.
      if listing.owner == UnitName("player") then
        biddable = false;
        buyable = false;
      else
        cancellable = false;
      end
      -- To buy, we must have a buyout price listed.
      if listing.buyout == 0 then
        buyable = false;
      end
      -- We must find at least one selected item.
      found = true;
    end

    -- Nothing's selected, so check all items.
    if not found then
      cancellable = findMine(DetailData);
    end
  elseif SummaryDataByLink ~= nil then
    -- We're at the summary screen, so check all items.
    cancellable = false;
    for link, result in pairs(SummaryDataByLink) do
      if findMine(result.data) then
        cancellable = true;
        break;
      end
    end
  else
    cancellable = false;
  end

  return (found and biddable), (found and buyable), cancellable;
end

-- Cancel the selected items.
function AuctionLite:CancelItems(onlyUndercut, link)
  -- List of items to be cancelled.
  local list = {};

  -- If we didn't specify data, try using DetailData.
  if link == nil then
    link = DetailLink;
  end

  -- Now find the items to cancel.
  if link ~= nil then
    local data = SummaryDataByLink[link].data;
    local name = self:SplitLink(link);

    -- Add information about each selected item.
    local i;
    for listing, _ in pairs(SelectedItems) do
      assert(listing.owner == UnitName("player"));
      listing.name = name;
      table.insert(list, listing);
    end

    -- If we don't have any selected items, cancel all.
    if table.getn(list) == 0 then
      self:FindMyListings(data, list, name, onlyUndercut);
    end
  else
    -- We're at the summary screen, so do this for all items.
    for link, result in pairs(SummaryDataByLink) do
      local name = self:SplitLink(link);
      self:FindMyListings(result.data, list, name, onlyUndercut);
    end
  end

  -- If we found some items to cancel, do it.
  if table.getn(list) > 0 then
    self:CancelAuctions(list);
  end
end

-- Update the display now that we've finished cancelling.
function AuctionLite:CancelComplete()
  -- Go through all listings and remove the ones that were cancelled.
  for link, summary in pairs(SummaryDataByLink) do
    local data = summary.data;
    local i = table.getn(data);
    while i > 0 do
      local listing = data[i];
      if listing.cancelled then
        table.remove(data, i);
        summary.itemsAll = summary.itemsAll - listing.count;
        summary.itemsMine = summary.itemsMine - listing.count;
        summary.listingsAll = summary.listingsAll - 1;
        summary.listingsMine = summary.listingsMine - 1;
        SelectedItems[listing] = nil;
      end
      i = i - 1;
    end
  end

  -- Reevaluate "warning" status.
  self:SetWarnings();

  -- Cleanup.
  if table.getn(DetailData) == 0 then
    self:SetDetailLink(nil);
  end

  -- Update the display.
  self:AuctionFrameBuy_Update();
end

-- Called after a search query ends in order to start a mass buyout.
function AuctionLite:StartMassBuyout()
  -- See if the user requested a specific quantity.
  local requested = BuyQuantity:GetNumber();
  if DetailLink ~= nil and table.getn(DetailData) > 0 and requested > 0 then
    -- Clear our selected items.  (Should already be done, but what the hey.)
    SelectedItems = {};

    -- Make a list of all items from other sellers with nonzero buyout.
    -- Also figure out how many items we have available in all.
    local available = {};
    local cheapest = nil;
    local cheapestOwned = nil;
    local total = 0;
    local listing;
    for _, listing in ipairs(DetailData) do
      if listing.buyout > 0 then
        local price = listing.buyout / listing.count;
        if listing.owner == UnitName("player") then
          -- Remember the cheapest auction that we own.
          if cheapestOwned == nil or price < cheapestOwned then
            cheapestOwned = price;
          end
        else
          -- Remember the cheapest auction that we don't own.
          if cheapest == nil or price < cheapest then
            cheapest = price;
          end
          -- Keep track of all other auctions with buyouts.
          table.insert(available, listing);
          total = total + listing.count;
        end
      end
    end

    -- Are there enough items for sale to satisfy the request?
    if total <= requested then
      -- Nope.  Just take everything we can.
      local listing;
      for _, listing in ipairs(available) do
        SelectedItems[listing] = true;
      end
    else
      -- Yep.  So now we have to figure out what to buy.  We use an
      -- adaptation of the standard dynamic programming algorithm for
      -- 0-1 knapsack.

      -- The state for this algorithm is as follows:
      --   results: For any n, what's the best price we can get for exactly
      --            n items, and what listings do we have to buy to do so?
      --   indices: A list of valid indices for results, in descending order.
      -- We initialize these with 0, our base case.
      local results = { [0] = { price = 0 } };
      local indices = { 0 };

      -- Determine the maximum number of items we want to consider buying.
      -- This is the number requested plus the maximum stack size available
      -- in the AH.  We don't need to consider anything large than this,
      -- because if we could fill the user's order for a larger count, then
      -- we must be able to remove one listing to fill the user's order for
      -- less money overall.
      local maxCount = 0;
      local listing;
      for _, listing in ipairs(available) do
        if listing.count > maxCount then
          maxCount = listing.count;
        end
      end
      maxCount = maxCount + requested;

      -- Sort by count, which improves the speed of the algorithm below.
      table.sort(available, function(a, b) return a.count > b.count end);

      -- Save our start time.
      local start = time();

      -- Consider each listing in turn.  At each step, the results table
      -- will have the best results for the set of listings we've considered
      -- so far.
      local timeout = false;
      local listing;
      for _, listing in ipairs(available) do
        -- If we've taken more than 1-2 seconds, bail.
        if start + 1 < time() then
          timeout = true;
          break;
        end
        -- Iterate over all counts where we have results so far, in
        -- descending order so that we can update in place.
        local count;
        for _, count in ipairs(indices) do
          local info = results[count];
          local newCount = count + listing.count;
          assert(info ~= nil);
          -- Add this listing to the best result at this stack size.
          -- Do we care about the resulting set of items?
          if newCount < maxCount then
            -- If this is the first option at this new stack size, or if it's
            -- cheaper than our previous best effort, update the data.
            local newInfo = results[newCount];
            local newPrice = info.price + listing.buyout;
            if newInfo == nil or newPrice < newInfo.price then
              -- The new data consists of the current result's listings plus
              -- the listing we just added.
              results[newCount] = {
                price = newPrice,
                listing = listing,
                info = info,
              };
            end
            -- If we added a new index to the results table, add it to our
            -- list of indices too, and make sure it's still sorted.
            if newInfo == nil then
              table.insert(indices, newCount);
              table.sort(indices, function(a, b) return a > b end);
            end
          end
        end
      end

      if timeout then
        -- We hit a timeout, so just do things the fast way.
        -- First sort by per-item buyout.
        local sort = function(a, b)
          return a.buyout / a.count < b.buyout / b.count;
        end
        table.sort(available, sort);

        -- Get the cheapest items until the order is filled.
        local selected = {};
        local count = 0;
        local i = 1;
        while i < table.getn(available) and count < requested do
          local listing = available[i];
          table.insert(selected, listing);
          count = count + listing.count;
          i = i + 1;
        end

        -- Go over the selected items in reverse order and remove any
        -- that aren't actually necessary.
        local i = table.getn(selected);
        while i > 0 do
          local listing = selected[i];
          if count - listing.count >= requested then
            table.remove(selected, i);
            count = count - listing.count;
          end
          i = i - 1;
        end

        -- Update the selected items list as appropriate.
        local listing;
        for _, listing in ipairs(selected) do
          SelectedItems[listing] = true;
        end
      else
        -- We've computed our best options for all stack sizes.

        -- Figure out the resale value for this item.  We use the lesser
        -- of the cheapest price and the historical price.
        local resale = cheapest - 1;
        local hist = self:GetHistoricalPrice(DetailLink);
        if hist ~= nil and hist.price > 0 and hist.price < resale then
          resale = hist.price;
        end

        -- Now find the cheapest way to buy the requested number of items.
        local bestCount = nil;
        local bestInfo = nil;
        local count;
        local info;
        for count, info in pairs(results) do
          if count >= requested then
            -- If the user wants us to, adjust for resale price.
            if self.db.profile.considerResale then
              info.price = info.price - (resale * (count - requested));
            end
            -- Is this the best option we've seen?
            if bestInfo == nil or info.price < bestInfo.price or
               (info.price == bestInfo.price and count > bestCount) then
              bestCount = count;
              bestInfo = info;
            end
          end
        end

        -- Update SelectedItems with the final results.  We should always
        -- find a result here, but we'll be defensive anyway.
        while bestInfo ~= nil and bestInfo.listing ~= nil do
          SelectedItems[bestInfo.listing] = true;
          bestInfo = bestInfo.info;
        end
      end

      -- Sort by selectedness in order to pull selected items to the
      -- top, and mark the list as unsorted so that they will be sorted
      -- properly (and stably) on the next update.
      local sort = function(a, b)
        return SelectedItems[b] and not SelectedItems[a];
      end
      table.sort(DetailData, sort);
      DetailSort.sorted = false;
    end

    -- Do we have any listings for this item?
    if cheapestOwned ~= nil then
      -- Find the worst price we're paying.
      local worstBought = nil;
      local listing;
      for listing, _ in pairs(SelectedItems) do
        local price = listing.buyout / listing.count;
        if worstBought == nil or worstBought < price then
          worstBought = price;
        end
      end

      -- If it's above the price for our cheapest listing, warn the user.
      if cheapestOwned < worstBought then
        self:Print(L["|cffff0000[Warning]|r Skipping your own auctions.  " ..
                     "You might want to cancel them instead."]);
      end
    end

    -- Now create our buyout order.
    self:CreateOrder(true, requested);
  end
end

-- Create a purchase order based on the current selection.  The first
-- argument indicates whether we're bidding or buying, and the second
-- argument (optional) indicates the actual number of items the user wants.
function AuctionLite:CreateOrder(isBuyout, requested)
  if DetailLink ~= nil then
    -- Create purchase order object to be filled out.
    local order = { list = {}, name = self:SplitLink(DetailLink),
                    price = 0, spent = 0, count = 0, isBuyout = isBuyout,
                    itemsBought = 0, listingsBought = 0,
                    itemsNotFound = 0, listingsNotFound = 0 };

    -- Add information about each selected item.
    local i;
    for listing, _ in pairs(SelectedItems) do
      assert(listing.owner ~= UnitName("player"));

      local price;
      if isBuyout then
        price = listing.buyout;
      else
        price = listing.bid;
      end

      table.insert(order.list, listing);
      order.count = order.count + listing.count;
      order.price = order.price + price;
    end

    -- If we found any selected items and we have enough money, proceed.
    if order.price > GetMoney() then
      self:Print(L["|cffff0000[Error]|r Insufficient funds."]);
    elseif order.count > 0 then
      -- If the second argument wasn't specified, the user wants exactly
      -- the number of items selected.
      if requested == nil then
        requested = order.count;
      end

      -- If we overshot, figure out how much we can resell the excess for.
      if order.count > requested then
        order.resell = order.count - requested;

        local price = SummaryDataByLink[DetailLink].price;
        order.resellPrice = math.floor(order.resell * price);

        order.netPrice = order.price - order.resellPrice;
      end

      -- Get a historical comparison.
      local hist = self:GetHistoricalPrice(DetailLink);
      if hist ~= nil then
        order.histPrice = math.floor(requested * hist.price);
      else
        order.histPrice = 0;
      end

      -- Save the purchase order and start buying.
      PurchaseOrder = order;
      self:ContinuePurchase();
    end
  end
end

-- Place an order for the next item in line.
function AuctionLite:ContinuePurchase()
  local order = PurchaseOrder;
  if order ~= nil then
    assert(order.current == nil);

    local n = table.getn(order.list);
    if n > 0 then
      -- Find the first item to purchase in the current detail list.
      -- TODO: Make this more efficient by just sorting order.list.
      self:ApplyDetailSort();
      local index = nil;
      local i;
      local displayListing;
      for _, displayListing in ipairs(DetailData) do
        local purchaseListing;
        for i, purchaseListing in ipairs(order.list) do
          if displayListing == purchaseListing then
            index = i;
            break;
          end
        end
        if index ~= nil then
          break;
        end
      end

      -- The purchase items should be a subset of the main listing.
      assert(index ~= nil);

      order.current = table.remove(order.list, index);

      local query = {
        name = order.name,
        wait = true,
        exact = true,
        listing = order.current,
        isBuyout = order.isBuyout,
        finish = function(cancelled)
          AuctionLite:PurchaseComplete(cancelled);
        end,
      };

      -- TODO: Make this work.
      -- self:ScrollToListing(order.current);

      -- TODO: What if this returns false?
      self:StartQuery(query);
    else
      self:EndPurchase();
    end
  end
end

-- Finish a purchase; show the result and clean up.
function AuctionLite:EndPurchase(cancelled)
  assert(PurchaseOrder ~= nil);
  assert(PurchaseOrder.current == nil);

  local order = PurchaseOrder;

  -- Tally up remaining items.
  local listing;
  for _, listing in ipairs(order.list) do
    self:UpdatePurchaseStats(listing);
  end

  -- Print a summary.
  if not cancelled or order.listingsBought > 0 then
    if order.isBuyout then
      self:Print(L["Bought %dx %s (%d |4listing:listings; at %s)."]:
                 format(order.itemsBought, order.name, order.listingsBought,
                        self:PrintMoney(order.spent)));
    else
      self:Print(L["Bid on %dx %s (%d |4listing:listings; at %s)."]:
                 format(order.itemsBought, order.name, order.listingsBought,
                        self:PrintMoney(order.spent)));
    end

    if order.itemsNotFound > 0 then
      self:Print(L["Note: %d |4listing:listings; of %d |4item was:items were; not purchased."]:
                 format(order.listingsNotFound, order.itemsNotFound));
    end
  end

  PurchaseOrder = nil;
end

-- The query system needs us to approve purchases.
function AuctionLite:RequestApproval()
  -- Mark ourselves as waiting for approval.
  if PurchaseOrder ~= nil and PurchaseOrder.current ~= nil then
    PurchaseOrder.needsApproval = true;
  end

  -- Update the display.
  self:AuctionFrameBuy_Update();
end

-- The query system no longer needs approval.
function AuctionLite:CancelRequestApproval()
  -- Mark ourselves as not waiting for approval.
  if PurchaseOrder ~= nil then
    PurchaseOrder.needsApproval = nil;
  end

  -- Update the display.
  self:AuctionFrameBuy_Update();
end

-- Update purchase stats for an item.
function AuctionLite:UpdatePurchaseStats(listing)
  local order = PurchaseOrder;
  if order ~= nil then
    if listing.purchased then
      order.itemsBought = order.itemsBought + listing.count;
      order.listingsBought = order.listingsBought + 1;
      if order.isBuyout then
        order.spent = order.spent + listing.buyout;
      else
        order.spent = order.spent + listing.bid;
      end
    else
      order.itemsNotFound = order.itemsNotFound + listing.count;
      order.listingsNotFound = order.listingsNotFound + 1;
    end
  end
end

-- Notification that a purchase has completed.
function AuctionLite:PurchaseComplete(cancelled)
  local order = PurchaseOrder;

  -- Update the purchase order.
  if order ~= nil then
    -- Update our current purchase.
    if order.current ~= nil then
      self:UpdatePurchaseStats(order.current);
      order.current = nil;
    end

    -- Update our display according to the purchase.
    -- TODO: Make this more efficient, now that we only buy one item at a time.
    if DetailLink ~= nil then
      local summary = SummaryDataByLink[DetailLink];
      local i = table.getn(DetailData);
      while i > 0 do
        local listing = DetailData[i];
        if listing.purchased then
          if PurchaseOrder.isBuyout then
            -- If we bought an item, remove it.
            table.remove(DetailData, i);
            summary.itemsAll = summary.itemsAll - listing.count;
            summary.listingsAll = summary.listingsAll - 1;
            SelectedItems[listing] = nil;
          else
            -- If we bid on an item, update the minimum bid.
            local increment = math.floor(listing.bid / 100) * 5;
            listing.purchased = false;
            listing.bidder = 1;
            listing.bid = listing.bid + increment;
            if listing.bid > listing.buyout and listing.buyout > 0 then
              listing.bid = listing.buyout;
            end
          end
        end
        i = i - 1;
      end

      if table.getn(DetailData) == 0 then
        self:SetDetailLink(nil);
      end
    end

    -- Continue with further purchases if relevant.
    if not cancelled then
      self:ContinuePurchase();
    else
      self:EndPurchase(true);
    end
  end

  -- Update the display.
  self:AuctionFrameBuy_Update();
end

-- Update search progress display.
function AuctionLite:UpdateProgressSearch(pct, getAll, scan)
  -- If we got a progress update, record it.
  if pct ~= nil then
    Progress = pct;
  end

  -- Record a start time if we don't have one already.
  if StartTime == nil then
    StartTime = math.floor(time());
  end

  -- Figure out whether we're actually doing getAll and/or scanning.
  if GetAll == nil and getAll ~= nil then
    GetAll = getAll;
  end
  if Scanning == nil and scan ~= nil then
    Scanning = scan;
  end

  -- Update the display every second.
  local currentTime = math.floor(time());
  if LastTime == nil or currentTime > LastTime then
    LastTime = currentTime;

    -- If we have some data, compute the time remaining.
    local elapsed = currentTime - StartTime;
    if elapsed > 0 and Progress > 0 then
      -- In order to reduce jitter in the estimate, do the following:
      -- 1. Update once every two seconds at first.
      -- 2. Average each estimate with the previous one.
      -- 3. Ignore estimates that exceed the previous by less than 10%.
      if Progress > 15 or (elapsed % 2) == 0 then
        local remaining = math.floor((100 * elapsed / Progress) - elapsed);

        if LastRemaining ~= nil then
          remaining = math.floor((remaining + LastRemaining) / 2);

          if remaining > LastRemaining and remaining < LastRemaining * 1.1 then
            remaining = LastRemaining;
          end
        end

        LastRemaining = remaining;

        BuyRemainingData:SetText(self:PrintTime(remaining));
      end
    else
      BuyRemainingData:SetText("---");
    end

    -- Update the percentage.
    if GetAll then
      BuyStatusText:SetText(L["Scanning..."]);
      BuyStatusData:SetText("");
    else
      if Scanning then
        BuyStatusText:SetText(L["Scanning:"]);
      else
        BuyStatusText:SetText(L["Searching:"]);
      end
      BuyStatusData:SetText(tostring(Progress) .. "%");
    end

    -- Update the elapsed time and show the whole pane.
    BuyElapsedData:SetText(self:PrintTime(elapsed));
    BuyStatus:Show();
  end
end

-- Show our progress for the favorites scan.  We assume each scan takes
-- roughly the same amount of time, and then split each segment accordingly.
function AuctionLite:UpdateProgressMulti(pct)
  local numDone = 0;
  for _, _ in pairs(MultiScanData) do
    numDone = numDone + 1;
  end

  local numItems = 0;
  for _, _ in pairs(MultiScanItems) do
    numItems = numItems + 1;
  end

  local overall = math.floor(((100 * numDone) + pct) / numItems);
  self:UpdateProgressSearch(overall);
end

-- Take the next step in a multi-item scan.  If we need to scan for another
-- item, do it; if there are no items left, display our results.
function AuctionLite:MultiScan(items)
  -- Are we starting a new multi-item scan?
  local first = false;
  if items ~= nil then
    MultiScanOriginal = items;
    MultiScanItems = {};
    MultiScanData = {};
    for item, _ in pairs(items) do
      MultiScanItems[item] = true;
    end
    first = true;
  end

  -- Find an unscanned favorite.
  MultiScanCurrent = nil;
  local item;
  for item, _ in pairs(MultiScanItems) do
    if MultiScanItems[item] then
      MultiScanCurrent = item;
      break;
    end
  end

  if MultiScanCurrent ~= nil then
    -- Start the scan.
    local query = {
      wait = true,
      exact = true,
      update = function(pct) AuctionLite:UpdateProgressMulti(pct) end,
      finish = function(data) AuctionLite:SetMultiScanData(data) end,
    };

    if self:IsLink(MultiScanCurrent) then
      query.link = MultiScanCurrent;
    else
      query.name = MultiScanCurrent;
    end

    if self:StartQuery(query) and first then
      DetailLinkPrev = nil;
      self:ClearBuyFrame(true);
    end
  else
    -- Show our results.
    self:SetBuyData(MultiScanData);
    MultiScanData = {};
  end
end

-- Get the results for a multi-item scan.
function AuctionLite:SetMultiScanData(data)
  -- Gather all relevant results returned by the search.
  local found = false;
  for link, results in pairs(data) do
    local item = self:IsFavorite(link, MultiScanItems);

    if item then
      -- We were looking for this one.  Save the data and mark it as found.
      MultiScanItems[link] = false;
      MultiScanItems[item] = false;
      MultiScanData[link] = results;

      -- We now have a link to use, so swap it in if we just had a name before.
      if MultiScanOriginal[item] then
        MultiScanOriginal[item] = nil;
        MultiScanOriginal[link] = true;
      end
    end

    -- Did we find the actual search term?
    if MultiScanCurrent == link or MultiScanCurrent == item then
      found = true;
    end
  end

  -- We didn't find the item we were looking for, so add a fake entry to
  -- the list so that it shows up with zero results.
  if not found then
    local link;
    if self:IsLink(MultiScanCurrent) then
      link = MultiScanCurrent;
    else
      link = "|cffffffff|Hitem:0:0:0:0:0:0:0:0|h[" ..
             MultiScanCurrent ..
             "]|h|r";
    end

    MultiScanItems[MultiScanCurrent] = false;
    MultiScanData[link] = {
      link = link,
      data = {},
      price = 0,
      itemsAll = 0,
      itemsMine = 0,
      listingsAll = 0,
      listingsMine = 0,
    };
  end

  -- Continue the scan.
  self:MultiScan();
end

-- Sort the detail view.
function AuctionLite:ApplyDetailSort()
  local info = DetailSort;
  local data = DetailData;

  local cmp;
  if info.sort == "Item" then
    cmp = function(a, b) return a.count < b.count end;
  elseif info.sort == "BidEach" then
    cmp = function(a, b) return a.bid / a.count < b.bid / b.count end;
  elseif info.sort == "BidAll" then
    cmp = function(a, b) return a.bid < b.bid end;
  elseif info.sort == "BuyoutEach" then
    cmp = function(a, b) return a.buyout / a.count < b.buyout / b.count end;
  elseif info.sort == "BuyoutAll" then
    cmp = function(a, b) return a.buyout < b.buyout end;
  else
    assert(false);
  end

  self:ApplySort(info, data, cmp);
end

-- Sort the summary view.
function AuctionLite:ApplySummarySort()
  local info = SummarySort;
  local data = SummaryData;

  local cmp;
  if info.sort == "ItemSummary" then
    cmp = function(a, b)
      local aFav = (self:IsFavorite(a.link) ~= nil);
      local bFav = (self:IsFavorite(b.link) ~= nil);
      if aFav == bFav then
        return a.name < b.name;
      else
        return aFav;
      end
    end
  elseif info.sort == "Listings" then
    cmp = function(a, b) return a.listingsAll < b.listingsAll end;
  elseif info.sort == "Items" then
    cmp = function(a, b) return a.itemsAll < b.itemsAll end;
  elseif info.sort == "Market" then
    cmp = function(a, b) return a.price < b.price end;
  elseif info.sort == "Historical" then
    if BuyMode == BUY_MODE_DEALS or
       BuyMode == BUY_MODE_SCAN then
      cmp = function(a, b) return a.profit < b.profit end;
    else
      cmp = function(a, b) return a.histPrice < b.histPrice end;
    end
  else
    assert(false);
  end

  self:ApplySort(info, data, cmp);
end

-- Set a new sort type for the detail view.
function AuctionLite:DetailSortButton_OnClick(sort)
  assert(sort == "Item" or sort == "BidEach" or sort == "BidAll" or
         sort == "BuyoutEach" or sort == "BuyoutAll");

  self:SortButton_OnClick(DetailSort, sort);
  self:AuctionFrameBuy_Update();
end

-- Set a new sort type for the summary view.
function AuctionLite:SummarySortButton_OnClick(sort)
  assert(sort == "ItemSummary" or sort == "Listings" or sort == "Items" or
         sort == "Market" or sort == "Historical");

  self:SortButton_OnClick(SummarySort, sort);
  self:AuctionFrameBuy_Update();
end

-- Return a sorted list of favorites list names.
function AuctionLite:GetFavoritesLists()
  local names = {};

  for name, list in pairs(self.db.profile.favorites) do
    table.insert(names, name);
  end
  table.sort(names);

  return names;
end

-- Toggle the favorites flag for this item.
function AuctionLite:FavoritesButton_OnClick(id)
  local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);
  local link = SummaryData[offset + id].link;

  local toggle = function(list)
    -- Pop open any fake links.
    local item;
    local name, _, id = self:SplitLink(link);
    if id == 0 then
      item = name;
    else
      item = link;
    end

    -- Toggle the favorite.
    if list[item] == nil then
      list[item] = true;
    else
      list[item] = nil;
    end
    self:AuctionFrameBuy_Update();
  end

  local single = self:GetSingleFavoritesList();
  if single ~= nil then
    -- Just toggle the list.
    toggle(single);
  else
    -- Make a more detailed menu showing the available lists.
    if BuyFavoritesDropDown == nil then
      CreateFrame("Frame", "BuyFavoritesDropDown", UIParent,
                  "Lib_UIDropDownMenuTemplate");
    end

    BuyFavoritesDropDown.displayMode = "MENU";
    BuyFavoritesDropDown.initialize = function(menu)
      local info = Lib_UIDropDownMenu_CreateInfo();
      info.text = L["Member Of"];
      info.isTitle = true;
      Lib_UIDropDownMenu_AddButton(info);

      for _, name in ipairs(self:GetFavoritesLists()) do
        local list = self.db.profile.favorites[name];
        local info = Lib_UIDropDownMenu_CreateInfo();
        info.text = name;
        info.func = function() toggle(list) end;
        info.checked = (self:IsFavorite(link, list) ~= nil);
        Lib_UIDropDownMenu_AddButton(info);
      end
    end

    ToggleDropDownMenu(1, nil, BuyFavoritesDropDown, "cursor");
  end
end

-- Handles clicks on the buttons in the "Buy" scroll frame.
function AuctionLite:BuyButton_OnClick(id, button)
  if button == "LeftButton" then
    local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);

    if DetailLink ~= nil then
      -- We're in detail view, so select the item.

      -- Unless we're holding control, this is a new selection.
      if not IsControlKeyDown() then
        SelectedItems = {};
      end

      if LastClick ~= nil and IsShiftKeyDown() then
        -- Shift is down and we have a previous click.
        -- Add all items in this range to the selection.
        local lower = offset + id;
        local upper = offset + id;

        if LastClick < offset + id then
          lower = LastClick;
        else
          upper = LastClick;
        end

        local i;
        for i = lower, upper do
          SelectedItems[DetailData[i]] = true;
        end
      else
        -- No shift, or first click; add only the current item to the selection.
        -- If control is down, toggle the item.
        LastClick = offset + id;

        local listing = DetailData[offset + id];
        if IsControlKeyDown() and SelectedItems[listing] then
          SelectedItems[listing] = nil;
        else
          SelectedItems[listing] = true;
        end
      end
    else
      -- We're in summary view, so switch to detail view.
      self:SetDetailLink(SummaryData[offset + id].link);
      self:StartMassBuyout();
    end

    self:AuctionFrameBuy_Update();
  elseif button == "RightButton" then
    local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);

    if DetailLink == nil then
      local link = SummaryData[offset + id].link;
      local menuList = {
        {
          text = L["Cancel Undercut Auctions"],
          func = function() AuctionLite:CancelItems(true, link) end,
        },
        {
          text = L["Cancel All Auctions"],
          func = function() AuctionLite:CancelItems(false, link) end,
        },
      };

      if BuyContextDropDown == nil then
        CreateFrame("Frame", "BuyContextDropDown", UIParent,
                    "Lib_UIDropDownMenuTemplate");
      end

      EasyMenu(menuList, BuyContextDropDown, "cursor", 0, 0, "MENU");
    end
  end
end

-- Mouse has entered a row in the scrolling frame.
function AuctionLite:BuyButton_OnEnter(widget)
  -- Get our index into the current display data.
  local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);
  local id = widget:GetID();

  -- Get a link and count for the item we're currently hovering over.
  -- The "shift" is used to move the tooltip to the right in detail view
  -- so that it doesn't obscure item quantities.
  local link = nil;
  local count = 1;
  local shift = 0;

  if DetailLink ~= nil then
    local item = DetailData[offset + id];
    if item ~= nil then
      link = DetailLink;
      count = item.count;
      shift = BuyButton1DetailName:GetLeft() - BuyButton1DetailCount:GetLeft();
    end
    shift = shift + 200;
  else
    link = SummaryData[offset + id].link;
    shift = shift + 250;
  end

  -- If we have an item, show the tooltip.
  self:SetAuctionLiteTooltip(widget, shift, link, count);
end

-- Mouse has left a row in the scrolling frame.
function AuctionLite:BuyButton_OnLeave(widget)
  GameTooltip:Hide();
  BattlePetTooltip:Hide();
end

-- Click the advanced menu button.
function AuctionLite:BuyAdvancedButton_OnClick()
  BuyAdvancedDropDown.displayMode = "MENU";
  BuyAdvancedDropDown.initialize = function(menu, level)
    Lib_UIDropDownMenu_SetWidth(BuyAdvancedDropDown, 140);

    if level == 1 then
      local info = Lib_UIDropDownMenu_CreateInfo();
      info.text = L["Show Deals"];
      info.func = function() AuctionLite:AuctionFrameBuy_Deals() end;
      Lib_UIDropDownMenu_AddButton(info, level);

      local info = Lib_UIDropDownMenu_CreateInfo();
      info.text = L["Show Favorites"];
      local single = self:GetSingleFavoritesList();
      if single == nil then
        info.hasArrow = true;
      else
        info.func = function()
          BuyMode = BUY_MODE_FAVORITES;
          self:MultiScan(single);
        end
      end
      Lib_UIDropDownMenu_AddButton(info, level);

      local info = Lib_UIDropDownMenu_CreateInfo();
      info.text = L["Show My Auctions"];
      info.func = function()
        BuyMode = BUY_MODE_MY_AUCTIONS;
        self:MultiScan(self:GetMyAuctionLinks());
      end
      Lib_UIDropDownMenu_AddButton(info, level);

      local info = Lib_UIDropDownMenu_CreateInfo();
      info.text = L["Configure AuctionLite"];
      info.func = function()
        InterfaceOptionsFrame_OpenToCategory(AuctionLite.optionFrames.tooltips);
        InterfaceOptionsFrame_OpenToCategory(self.optionFrames.main);
      end
      Lib_UIDropDownMenu_AddButton(info, level);
    elseif level == 2 then
      for _, name in ipairs(self:GetFavoritesLists()) do
        local favs = self.db.profile.favorites[name];
        local info = Lib_UIDropDownMenu_CreateInfo();
        info.text = name;
        info.func = function()
          BuyMode = BUY_MODE_FAVORITES;
          self:MultiScan(favs);
          CloseDropDownMenus();
        end
        Lib_UIDropDownMenu_AddButton(info, level);
      end
    end
  end

  ToggleDropDownMenu(1, nil, BuyAdvancedDropDown);
end

-- Returns to the summary page.
function AuctionLite:BuySummaryButton_OnClick()
  if PurchaseOrder ~= nil then
    self:CancelQuery();
    self:ResetSearch();
  end

  self:SetDetailLink(nil);

  self:AuctionFrameBuy_Update();
end

-- Approve a pending purchase.
function AuctionLite:BuyApproveButton_OnClick()
  PurchaseOrder.needsApproval = nil;
  self:QueryApprove();
  self:AuctionFrameBuy_Update();
end

-- Cancel a pending purchase.
function AuctionLite:BuyCancelPurchaseButton_OnClick()
  PurchaseOrder.needsApproval = nil;
  self:CancelQuery();
  self:ResetSearch();
  self:AuctionFrameBuy_Update();
end

-- Cancel an in-progress search.
function AuctionLite:BuyCancelSearchButton_OnClick()
  self:CancelQuery();
  self:ResetSearch();
  self:AuctionFrameBuy_Update();
end

-- Bid on the currently-selected item.
function AuctionLite:BuyBidButton_OnClick()
  if PurchaseOrder ~= nil then
    self:CancelQuery();
  end
  self:CreateOrder(false);
  self:AuctionFrameBuy_Update();
end

-- Buy out the currently-selected item.
function AuctionLite:BuyBuyoutButton_OnClick()
  if PurchaseOrder ~= nil then
    self:CancelQuery();
  end
  self:CreateOrder(true);
  self:AuctionFrameBuy_Update();
end

-- Cancel the currently-selected item.
function AuctionLite:BuyCancelAuctionButton_OnClick()
  self:CancelItems(IsControlKeyDown());
  self:AuctionFrameBuy_Update();
end

-- Show the cancel tooltip.
function AuctionLite:BuyCancelAuctionButton_OnEnter(widget)
  GameTooltip:SetOwner(widget, "ANCHOR_TOPLEFT");
  GameTooltip:SetText(L["CANCEL_TOOLTIP"]);
end

-- Hide the cancel tooltip.
function AuctionLite:BuyCancelAuctionButton_OnLeave(widget)
  GameTooltip:Hide();
  BattlePetTooltip:Hide();
end

-- Starts a full scan of the auction house.
function AuctionLite:StartFullScan()
  BuyMode = BUY_MODE_SCAN;

  if not self.db.profile.fastScanAd2 then
    dwStaticPopup_Show("AL_FAST_SCAN");
  else
    local query = {
      name = "",
      getAll = self.db.profile.getAll,
      update = function(pct, all)
        AuctionLite:UpdateProgressSearch(pct, all, true);
      end,
      finish = function(data, link)
        AuctionLite:SetScanData(data);
      end,
    };

    if self:StartQuery(query) then
      DetailLinkPrev = nil;
      self:ClearBuyFrame(true);
    end
  end
end

-- List current deals.  If we haven't done a full scan, do it now.
function AuctionLite:AuctionFrameBuy_Deals()
  BuyMode = BUY_MODE_DEALS;

  if ScanData ~= nil then
    DetailLinkPrev = nil;
    self:SetScanData(ScanData);
  else
    self:StartFullScan();
  end
end

-- Submit a search query.
function AuctionLite:AuctionFrameBuy_Search()
  BuyMode = BUY_MODE_SEARCH;

  local query = {
    name = BuyName:GetText(),
    wait = true,
    exact = IsShiftKeyDown(),
    update = function(pct) AuctionLite:UpdateProgressSearch(pct) end,
    finish = function(data) AuctionLite:SetBuyData(data) end,
  };

  if self:StartQuery(query) then
    DetailLinkPrev = DetailLink;
    self:ClearBuyFrame(true);
  end
end

-- Adjust frame buttons for repaint.
function AuctionLite:AuctionFrameBuy_OnUpdate()
  local canSend = CanSendAuctionQuery("list") and not self:QueryInProgress();
  local biddable, buyable, cancellable = self:GetSelectionStatus();

  if canSend and BuyName:GetText() ~= "" then
    BuySearchButton:Enable();
  else
    BuySearchButton:Disable();
  end

  if canSend then
    BuyScanButton:Enable();
  else
    BuyScanButton:Disable();
  end

  if canSend and biddable then
    BuyBidButton:Enable();
  else
    BuyBidButton:Disable();
  end

  if canSend and buyable then
    BuyBuyoutButton:Enable();
  else
    BuyBuyoutButton:Disable();
  end

  if cancellable then
    BuyCancelAuctionButton:Enable();
  else
    BuyCancelAuctionButton:Disable();
  end

  if StartTime ~= nil then
    self:UpdateProgressSearch();
  end
end

-- Adjust the scroll bar to a specified offset.
function AuctionLite:SetScrollFrameOffset(offset)
  -- We have to set the inner scroll bar manually, because otherwise the
  -- two values will become inconsistent.  We also set the max values to
  -- make sure that the value we're setting is not ignored.
  FauxScrollFrame_SetOffset(BuyScrollFrame, offset);
  BuyScrollFrameScrollBar:SetMinMaxValues(0, offset * BuyButton1:GetHeight());
  BuyScrollFrameScrollBar:SetValue(offset * BuyButton1:GetHeight());
end

-- Scroll the frame to a particular listing.
function AuctionLite:ScrollToListing(listing)
  if DetailLink ~= nil then
    -- Get the index of the item we're looking for.
    local i;
    local current;
    local found = nil;
    for i, current in ipairs(DetailData) do
      if current == listing then
        found = i;
        break;
      end
    end

    if found ~= nil then
      local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);
      local displaySize = BUY_DISPLAY_SIZE - 2;

      if found < offset or found >= displaySize then
        local newOffset = math.min(0, found - (displaySize / 2));
        self:SetScrollFrameOffset(newOffset);
      end
    end
  end
end

-- Update the scroll frame with either the detail view or summary view.
function AuctionLite:AuctionFrameBuy_Update()
  -- First clear everything.
  local i;
  for i = 1, BUY_DISPLAY_SIZE do
    local buttonName = "BuyButton" .. i;

    local button = _G[buttonName];
    local buttonDetail = _G[buttonName .. "Detail"];
    local buttonSummary = _G[buttonName .. "Summary"];

    button:Hide();
    buttonDetail:Hide();
    buttonSummary:Hide();
  end

  BuyHeader:Hide();
  BuySummaryHeader:Hide();

  BuyStatus:Hide();

  -- If we have no items, say so.
  BuyMessageText:Show();
  if BuyMode == BUY_MODE_INTRO then
    BuyMessageText:SetText(L["Enter item name and click \"Search\""]);
  elseif BuyMode == BUY_MODE_SEARCH and NoResults then
    BuyMessageText:SetText(L["No items found"]);
  elseif BuyMode == BUY_MODE_SCAN and NoResults then
    BuyMessageText:SetText(L["Scan complete.  Try again later to find deals!"]);
  elseif BuyMode == BUY_MODE_DEALS and NoResults then
    BuyMessageText:SetText(L["No deals found"]);
  elseif BuyMode == BUY_MODE_FAVORITES and NoResults then
    BuyMessageText:SetText(L["No items found"]);
  elseif BuyMode == BUY_MODE_MY_AUCTIONS and NoResults then
    BuyMessageText:SetText(L["No current auctions"]);
  else
    BuyMessageText:Hide();
  end

  -- Update the expandable header.
  self:AuctionFrameBuy_UpdateExpand();

  -- Use detail view if we've chosen an item, or summary view otherwise.
  if DetailLink ~= nil then
    self:AuctionFrameBuy_UpdateDetail();
  else
    self:AuctionFrameBuy_UpdateSummary();
  end
end

-- Update the expandable frame at the top of the scroll frame.
function AuctionLite:AuctionFrameBuy_UpdateExpand()
  -- Figure out how big a window to make.
  local order = PurchaseOrder;
  if order == nil then
    ExpandHeight = 0;
  elseif order.resell == nil then
    ExpandHeight = 2;
  else
    ExpandHeight = 4;
  end

  -- Set the height of the expandable window.  It's always one higher than
  -- the number of rows because WoW gets confused if it goes to zero;
  -- other frame offsets are computed appropriately.
  BuyExpand:SetHeight((ExpandHeight + 1) * ROW_HEIGHT);

  -- Show rows as appropriate.
  local i;
  for i = 1, EXPAND_ROWS do
    local prefix = "BuyExpand" .. i;
    local text = _G[prefix .. "Text"];
    local money = _G[prefix .. "MoneyFrame"];

    if i <= ExpandHeight then
      text:Show();
      money:Show();
    else
      text:Hide();
      money:Hide();
    end
  end

  -- Populate the expandable frame with appropriate data from the order.
  local order = PurchaseOrder;
  if order ~= nil then
    if order.resell == nil then
      if order.isBuyout then
        BuyExpand1Text:SetText(L["Buyout cost for %d:"]:format(order.count));
      else
        BuyExpand1Text:SetText(L["Bid cost for %d:"]:format(order.count));
      end
      MoneyFrame_Update(BuyExpand1MoneyFrame, order.price);

      BuyExpand2Text:SetText(L["Historical price for %d:"]:format(order.count));
      MoneyFrame_Update(BuyExpand2MoneyFrame, order.histPrice);
    else
      if order.isBuyout then
        BuyExpand1Text:SetText(L["Buyout cost for %d:"]:format(order.count));
      else
        BuyExpand1Text:SetText(L["Bid cost for %d:"]:format(order.count));
      end
      MoneyFrame_Update(BuyExpand1MoneyFrame, order.price);

      BuyExpand2Text:SetText(L["Resell %d:"]:format(order.resell));
      MoneyFrame_Update(BuyExpand2MoneyFrame, order.resellPrice);
      self:MakeNegative("BuyExpand2MoneyFrame");

      BuyExpand3Text:SetText(
        L["Net cost for %d:"]:format(order.count - order.resell));
      if order.netPrice < 0 then
        MoneyFrame_Update(BuyExpand3MoneyFrame, - order.netPrice);
        self:MakeNegative("BuyExpand3MoneyFrame");
      else
        MoneyFrame_Update(BuyExpand3MoneyFrame, order.netPrice);
      end

      BuyExpand4Text:SetText(
        L["Historical price for %d:"]:format(order.count - order.resell));
      MoneyFrame_Update(BuyExpand4MoneyFrame, order.histPrice);
    end

    local current = order.listingsBought + order.listingsNotFound + 1;
    local total = current + table.getn(order.list);
    BuyBatchText:SetText(L["Listing %d of %d"]:format(current, total));
  else
    BuyBatchText:SetText("");
  end

  -- Show/hide and enable/disable approval buttons.
  if ExpandHeight > 0 then
    BuyApproveButton:Show();
    BuyCancelPurchaseButton:Show();

    if PurchaseOrder ~= nil and PurchaseOrder.needsApproval then
      BuyApproveButton:Enable();
      BuyCancelPurchaseButton:Enable();
    else
      BuyApproveButton:Disable();
      BuyCancelPurchaseButton:Disable();
    end
  else
    BuyApproveButton:Hide();
    BuyCancelPurchaseButton:Hide();
  end
end

-- Update the scroll frame with the detail view.
function AuctionLite:AuctionFrameBuy_UpdateDetail()
  if not DetailSort.sorted then
    self:ApplyDetailSort();
  end

  local sort;
  for _, sort in ipairs({ "Item", "BidEach", "BidAll",
                          "BuyoutEach", "BuyoutAll" }) do
    self:UpdateSortArrow("Buy", sort, DetailSort.sort, DetailSort.flipped);
  end

  local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);
  local displaySize = BUY_DISPLAY_SIZE - ExpandHeight;

  local _, _, _, _, enchant, jewel1, jewel2, jewel3, jewel4 =
    self:SplitLink(DetailLink);

  local showPlus = enchant ~= 0 or
                   jewel1 ~= 0 or jewel2 ~= 0 or
                   jewel3 ~= 0 or jewel4 ~= 0;

  local i;
  for i = 1, displaySize do
    local item = DetailData[offset + i];
    if item ~= nil then
      local buttonName = "BuyButton" .. i;
      local button = _G[buttonName];

      local warning = _G[buttonName .. "Warning"];

      local buttonDetailName = buttonName .. "Detail";
      local buttonDetail     = _G[buttonDetailName];

      local countText        = _G[buttonDetailName .. "Count"];
      local nameText         = _G[buttonDetailName .. "Name"];
      local plusText         = _G[buttonDetailName .. "Plus"];
      local bidEachFrame     = _G[buttonDetailName .. "BidEachFrame"];
      local bidFrame         = _G[buttonDetailName .. "BidFrame"];
      local buyoutEachFrame  = _G[buttonDetailName .. "BuyoutEachFrame"];
      local buyoutFrame      = _G[buttonDetailName .. "BuyoutFrame"];

      local name, color = self:SplitLink(DetailLink);

      local countColor;
      local nameColor;
      if item.owner == UnitName("player") then
        countColor = "ffffff00";
        nameColor = "ffffff00";
      else
        countColor = "ffffffff";
        nameColor = color;
      end

      countText:SetText("|c" .. countColor .. item.count .. "x|r");

      nameText:SetText("|c" .. nameColor .. name .. "|r");

      if showPlus then
        plusText:SetPoint("LEFT", nameText, "LEFT",
                          nameText:GetStringWidth(), 0);
        plusText:Show();
      else
        plusText:Hide();
      end

      MoneyFrame_Update(bidEachFrame, math.floor(item.bid / item.count));
      bidEachFrame:SetAlpha(0.5);
      if item.bidder then
        SetMoneyFrameColor(buttonDetailName .. "BidEachFrame", "yellow");
      else
        SetMoneyFrameColor(buttonDetailName .. "BidEachFrame", "white");
      end

      MoneyFrame_Update(bidFrame, math.floor(item.bid));
      bidFrame:SetAlpha(0.5);
      if item.bidder then
        SetMoneyFrameColor(buttonDetailName .. "BidFrame", "yellow");
      else
        SetMoneyFrameColor(buttonDetailName .. "BidFrame", "white");
      end

      if item.buyout > 0 then
        MoneyFrame_Update(buyoutEachFrame, math.floor(item.buyout / item.count));
        buyoutEachFrame:Show();

        MoneyFrame_Update(buyoutFrame, math.floor(item.buyout));
        buyoutFrame:Show();
      else
        buyoutEachFrame:Hide();
        buyoutFrame:Hide();
      end

      if SelectedItems[item] then
        button:LockHighlight();
      else
        button:UnlockHighlight();
      end

      if PurchaseOrder ~= nil and PurchaseOrder.current == item then
        warning:SetAlpha(0.4);
      else
        warning:SetAlpha(0);
      end

      buttonDetail:Show();
      button:Show();
    end
  end

  FauxScrollFrame_Update(BuyScrollFrame, table.getn(DetailData),
                         displaySize, ROW_HEIGHT);

  if table.getn(DetailData) > 0 then
    BuyHeader:Show();
  end
end

-- Update the scroll frame with the summary view.
function AuctionLite:AuctionFrameBuy_UpdateSummary()
  if not SummarySort.sorted then
    self:ApplySummarySort();
  end

  local sort;
  for _, sort in ipairs({ "ItemSummary", "Listings", "Items",
                          "Market", "Historical" }) do
    self:UpdateSortArrow("Buy", sort, SummarySort.sort, SummarySort.flipped);
  end

  local offset = FauxScrollFrame_GetOffset(BuyScrollFrame);
  local displaySize = BUY_DISPLAY_SIZE - ExpandHeight;

  local i;
  for i = 1, displaySize do
    local item = SummaryData[offset + i];
    if item ~= nil then
      local buttonName = "BuyButton" .. i;
      local button = _G[buttonName];

      local warning = _G[buttonName .. "Warning"];

      local buttonSummaryName = buttonName .. "Summary";
      local buttonSummary     = _G[buttonSummaryName];

      local starButton        = _G[buttonSummaryName .. "StarButton"];
      local nameText          = _G[buttonSummaryName .. "Name"];
      local plusText          = _G[buttonSummaryName .. "Plus"];
      local listingsText      = _G[buttonSummaryName .. "Listings"];
      local itemsText         = _G[buttonSummaryName .. "Items"];
      local marketFrame       = _G[buttonSummaryName .. "MarketPriceFrame"];
      local histFrame         = _G[buttonSummaryName .. "HistPriceFrame"];

      if self:IsFavorite(item.link) then
        starButton:GetNormalTexture():SetAlpha(1.0);
      else
        starButton:GetNormalTexture():SetAlpha(0.1);
      end

      local name, color, _, _, enchant, jewel1, jewel2, jewel3, jewel4 =
        self:SplitLink(item.link);

      local countStr = function(mine, all)
        local prefix;
        if mine > 0 and self.db.profile.countMyListings then
          prefix = "(" .. mine .. ") ";
        else
          prefix = "";
        end
        return prefix .. "|cffffffff" .. all .. "|r";
      end

      nameText:SetText("|c" .. color .. name .. "|r");
      listingsText:SetText(countStr(item.listingsMine, item.listingsAll));
      itemsText:SetText(countStr(item.itemsMine, item.itemsAll));

      if item.price ~= nil and item.price > 0 then
        MoneyFrame_Update(marketFrame, math.floor(item.price));
        marketFrame:Show();
      else
        marketFrame:Hide();
      end

      local moreData;
      if BuyMode == BUY_MODE_DEALS or
         BuyMode == BUY_MODE_SCAN then
        moreData = item.profit;
      else
        moreData = item.histPrice;
      end

      if moreData ~= nil and moreData > 0 then
        MoneyFrame_Update(histFrame, math.floor(moreData));
        histFrame:Show();
      else
        histFrame:Hide();
      end

      if enchant ~= 0 or
         jewel1 ~= 0 or jewel2 ~= 0 or
         jewel3 ~= 0 or jewel4 ~= 0 then
        plusText:SetPoint("LEFT", nameText, "LEFT",
                          nameText:GetStringWidth(), 0);
        plusText:Show();
      else
        plusText:Hide();
      end

      button:UnlockHighlight();

      if item.warning then
        warning:SetAlpha(0.4);
      else
        warning:SetAlpha(0);
      end

      buttonSummary:Show();
      button:Show();
    end
  end

  FauxScrollFrame_Update(BuyScrollFrame, table.getn(SummaryData),
                         displaySize, ROW_HEIGHT);

  if table.getn(SummaryData) > 0 then
    BuySummaryHeader:Show();
    if BuyMode == BUY_MODE_DEALS or
       BuyMode == BUY_MODE_SCAN then
      self:UpdateSortButton("Buy", "Historical", L["Potential Profit"]);
    else
      self:UpdateSortButton("Buy", "Historical", L["Historical Price"]);
    end
  end
end

-- Loads a name into the input field and searches.
function AuctionLite:NameClickBuy(name)
  BuyName:SetText(name);
  BuyQuantity:SetFocus();
  AuctionLite:AuctionFrameBuy_Search();
end

-- Handle bag item clicks by searching for the item.
function AuctionLite:BagClickBuy(container, slot)
  local link = GetContainerItemLink(container, slot);
  if link ~= nil then
    local name = self:SplitLink(link);
    self:NameClickBuy(name);
  end
end

-- Resets all info about an in-progress search.
function AuctionLite:ResetSearch()
  StartTime = nil;
  LastTime = nil;
  LastRemaining = nil;
  Progress = nil;
  GetAll = nil;
  Scanning = nil;
end

-- Clean up the "Buy" tab.
function AuctionLite:ClearBuyFrame(partial)
  ExpandHeight = 0;

  if not partial then
    BuyMode = BUY_MODE_INTRO;

    SummarySort = {
      sort = "ItemSummary",
      flipped = false,
      justFlipped = false,
      sorted = false,
    };

    DetailSort = {
      sort = "BuyoutEach",
      flipped = false,
      justFlipped = false,
      sorted = false,
    };
  end

  DetailLink = nil;
  DetailData = {};

  if not partial then
    DetailLinkPrev = nil;
  end

  SelectedItems = {};
  LastClick = nil;

  SummaryData = {};
  SummaryDataByLink = {};
  NoResults = false;

  PurchaseOrder = nil;

  self:ResetSearch();

  if not partial then
    MultiScanCurrent = nil;
    MultiScanOriginal = {};
    MultiScanItems = {};
  end
  MultiScanData = {};

  if not partial then
    BuyName:SetText("");
    BuyQuantity:SetText("");
    BuyName:SetFocus();
  end

  BuyMessageText:Hide();
  BuyStatus:Hide();

  FauxScrollFrame_SetOffset(BuyScrollFrame, 0);
  BuyScrollFrameScrollBar:SetValue(0);

  self:AuctionFrameBuy_Update();
end

-- Create the "Buy" tab.
function AuctionLite:CreateBuyFrame()
  -- Create our tab.
  local index = self:CreateTab(L["AuctionLite - Buy"], AuctionFrameBuy);

  -- Set all localizable strings in the UI.
  BuyTitle:SetText(L["AuctionLite - Buy"]);
  BuyNameText:SetText(L["Name"]);
  BuyQuantityText:SetText(L["Qty"]);
  BuyStatusText:SetText(L["Searching:"]);
  BuyElapsedText:SetText(L["Time Elapsed:"]);
  BuyRemainingText:SetText(L["Time Remaining:"]);
  BuyCancelSearchButton:SetText(L["Cancel"]);
  BuyApproveButton:SetText(L["Approve"]);
  BuyCancelPurchaseButton:SetText(L["Cancel"]);
  BuyAdvancedText:SetText(L["Advanced"]);
  BuyScanButton:SetText(L["Full Scan"]);
  BuySearchButton:SetText(L["Search"]);
  BuyCancelAuctionButton:SetText(L["Cancel"]);

  -- Set button text and adjust arrow icons accordingly.
  BuyItemButton:SetText(L["Item"]);
  BuyItemSummaryButton:SetText(L["Item Summary"]);

  self:UpdateSortButton("Buy", "BidEach", L["Bid Per Item"]);
  self:UpdateSortButton("Buy", "BidAll", L["Bid Total"]);
  self:UpdateSortButton("Buy", "BuyoutEach", L["Buyout Per Item"]);
  self:UpdateSortButton("Buy", "BuyoutAll", L["Buyout Total"]);

  self:UpdateSortButton("Buy", "Historical", L["Historical Price"]);
  self:UpdateSortButton("Buy", "Market", L["Market Price"]);
  self:UpdateSortButton("Buy", "Items", L["Items"]);
  self:UpdateSortButton("Buy", "Listings", L["Listings"]);

  -- Make sure it's pristine.
  self:ClearBuyFrame();

  return index;
end
