-------------------------------------------------------------------------------
-- Query.lua
--
-- Queries the auction house.
-------------------------------------------------------------------------------

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

-- Maximum number of bytes in the first argument of QueryAuctionItems().
local MAX_QUERY_BYTES = 63;

-- Maximum number of retries if we get duplicate pages.
local MAX_RETRIES = 3;

-- State of our current auction query.
local QUERY_STATE_SEND    = 1; -- ready to request a new page
local QUERY_STATE_WAIT    = 2; -- waiting for results of previous request
local QUERY_STATE_APPROVE = 3; -- waiting for approval of a purchase

-- Time to wait (in seconds) after incomplete results are returned.
local QUERY_DELAY = 5;

-- Info about current AH query.
local Query = nil;

-- Is the current call to QueryAuctionItems ours?
local OurQuery = false;

-- Start an auction query.
function AuctionLite:StartQuery(newQuery)
  if Query ~= nil and Query.state == QUERY_STATE_APPROVE then
    self:CancelQuery();
  end
  if Query == nil then
    Query = newQuery;
    Query.state = QUERY_STATE_SEND;
    Query.page = 0;
    Query.retries = 0;
    Query.data = {};
    return true;
  else
    return false;
  end
end

-- Cancel an auction query.
function AuctionLite:CancelQuery()
  if Query ~= nil then
    if Query.state == QUERY_STATE_APPROVE then
      assert(Query.found ~= nil);
      Query.found = nil;
      if Query.finish ~= nil then
        Query.finish(true);
      end
    end
    self:QueryEnd();
  end
end

-- Cancel our queries if we see somebody else interfere.
function AuctionLite:QueryAuctionItems_Hook()
  if not OurQuery then
    self:CancelQuery();
  end
end

-- Called periodically to check whether a new query should be sent.
function AuctionLite:QueryUpdate()
  -- Find out whether we can send queries.
  local canSend, canGetAll = CanSendAuctionQuery("list");
  if canSend and Query ~= nil and Query.state == QUERY_STATE_SEND then
    -- Determine the query string.
    local name = nil;
    if Query.name ~= nil then
      name = Query.name;
    elseif Query.link ~= nil then
      name = self:SplitLink(Query.link);
    end

    -- Did we get a reasonable query?  We need a name, and if it's a getAll
    -- query, it should be on the first page with no shopping list.
    if name ~= nil and
       (not Query.getAll or (Query.page == 0 and Query.listing == nil)) then

      -- Truncate to avoid disconnects.
      name = self:Truncate(name, MAX_QUERY_BYTES);

      -- Was getAll requested, and can we actually use it?
      local getAll = false;
      if Query.getAll then
        if canGetAll then
          getAll = true;
        else
          Query.getAll = false;
          self:Print(L["|cffffd000[Note]|r " ..
                       "Fast auction scans can only be used once every " ..
                       "15 minutes. Using a slow scan for now."]);
        end
      end

      -- Submit the query.
      OurQuery = true;
      QueryAuctionItems(name, 0, 0, 0, 0, 0, Query.page, 0, 0, getAll);
      OurQuery = false;

      -- Wait for our result.
      self:QueryWait();

      -- If this is the first query, notify the caller (mostly so that
      -- they know whether we're doing getAll or not).
      if Query.update ~= nil and Query.page == 0 then
        Query.update(0, getAll);
      end
    else
      self:CancelQuery();
    end
  end

  -- Are we waiting for a more detailed update?  If so, check to see
  -- whether we've timed out.
  if Query ~= nil and Query.state == QUERY_STATE_WAIT and
     Query.time ~= nil and Query.time + QUERY_DELAY < time() then
    Query.time = nil;
    self:QueryNewData();
  end
end

-- Wait for purchase approval.
function AuctionLite:QueryRequestApproval()
  assert(Query ~= nil and Query.state == QUERY_STATE_WAIT and
         Query.found ~= nil);
  Query.state = QUERY_STATE_APPROVE;
end

-- Wait for incoming data.
function AuctionLite:QueryWait()
  assert(Query ~= nil and
         (Query.state == QUERY_STATE_SEND or
          Query.state == QUERY_STATE_APPROVE));
  Query.state = QUERY_STATE_WAIT;
end

-- Get the next page.
function AuctionLite:QueryNext()
  assert(Query ~= nil and
         (Query.state == QUERY_STATE_WAIT or
          Query.state == QUERY_STATE_APPROVE));
  Query.state = QUERY_STATE_SEND;
  Query.page = Query.page + 1;
  Query.retries = 0;
end

-- Get the current page again.
function AuctionLite:QueryCurrent()
  assert(Query ~= nil and Query.state == QUERY_STATE_WAIT);
  Query.state = QUERY_STATE_SEND;
end

-- End the current query.
function AuctionLite:QueryEnd()
  assert(Query ~= nil);
  Query = nil;
end

-- Is there currently a query pending?
function AuctionLite:QueryInProgress()
  return (Query ~= nil and Query.state ~= QUERY_STATE_APPROVE);
end

-- Compute the average and standard deviation of the points in data.
function AuctionLite:ComputeStats(data)
  local count = 0;
  local sum = 0;
  local sumSquared = 0;

  for _, listing in ipairs(data) do
    if listing.keep then
      count = count + listing.count;
      sum = sum + listing.price * listing.count;
      sumSquared = sumSquared + (listing.price ^ 2) * listing.count;
    end
  end

  local avg = 0;
  local stddev = 0;

  if count ~= 0 then
    avg = sum / count;
    stddev = math.max(0, sumSquared / count - (sum ^ 2 / count ^ 2)) ^ 0.5;
  end
  
  return avg, stddev;
end

-- Analyze an AH query result.
function AuctionLite:AnalyzeData(rawData)
  local results = {};
  local itemData = {};
  local i;

  -- Split up our data into tables for each item.
  for _, entry in ipairs(rawData) do
    local link = entry.link;
    local count = entry.count;
    local bid = entry.bid;
    local buyout = entry.buyout
    local owner = entry.owner;
    local bidder = entry.highBidder;

    if link ~= nil then
      local price = buyout / count;
      if price <= 0 then
        price = bid / count;
      end

      local keep = owner ~= UnitName("player") and buyout > 0;

      local listing = { bid = bid, buyout = buyout,
                        price = price, count = count,
                        owner = owner, bidder = bidder, keep = keep };

      if itemData[link] == nil then
        itemData[link] = {};
      end

      table.insert(itemData[link], listing);
    end
  end

  -- Process each data set.
  local link, data;
  for link, data in pairs(itemData) do 
    local done = false;

    -- Discard any points that are more than 2 SDs away from the mean.
    -- Repeat until no such points exist.
    while not done do
      done = true;
      local avg, stddev = self:ComputeStats(data);
      for _, listing in ipairs(data) do
        if listing.keep and math.abs(listing.price - avg) > 2.5 * stddev then
          listing.keep = false;
          done = false;
        end
      end
    end

    -- We've converged.  Compute our min price and other stats.
    local result = { price = 1000000000, items = 0, listings = 0,
                     itemsMine = 0, listingsMine = 0,
                     itemsAll = 0, listingsAll = 0 };
    local setPrice = false;

    for _, listing in ipairs(data) do
      if listing.keep then
        result.items = result.items + listing.count;
        result.listings = result.listings + 1;
        if listing.price < result.price then
          result.price = listing.price;
          setPrice = true;
        end
      end
      if listing.owner == UnitName("player") then
        result.itemsMine = result.itemsMine + listing.count;
        result.listingsMine = result.listingsMine + 1;
      end
      result.itemsAll = result.itemsAll + listing.count;
      result.listingsAll = result.listingsAll + 1;
    end

    -- If we kept no data (e.g., all auctions are ours), pick the first
    -- price.  By construction of itemData, there is at least one entry.
    if not setPrice then
      result.price = data[1].price;
      result.priceIsMine = true;
    end

    result.data = data;
    results[link] = result;
  end

  return results;
end

-- Approve purchase of a pending item.
function AuctionLite:QueryApprove()
  assert(Query ~= nil);
  assert(Query.state == QUERY_STATE_APPROVE);
  assert(Query.found ~= nil);

  -- Place the request bid or buyout.
  local price;
  if Query.isBuyout then
    price = Query.found.buyout;
  else
    price = Query.found.bid;
  end
  if price <= GetMoney() then
    PlaceAuctionBid("list", Query.found.index, price);
    self:IgnoreMessage(ERR_AUCTION_BID_PLACED);
    if Query.isBuyout then
      self:IgnoreMessage(ERR_AUCTION_WON_S:format(Query.name));
    end
    Query.listing.purchased = true;
  end

  -- Clean up.
  Query.found = nil;

  -- End the query.
  local oldQuery = Query;
  self:QueryEnd();
  if oldQuery.finish ~= nil then
    oldQuery.finish();
  end
end

-- Cancel the current purchase and restart the approval process.
function AuctionLite:RestartApproval()
  assert(Query ~= nil);
  assert(Query.state == QUERY_STATE_APPROVE);
  assert(Query.found ~= nil);

  -- Forget the found item.
  Query.found = nil;

  -- Tell the "Buy" frame.
  self:CancelRequestApproval();

  -- Go back to the waiting state.
  self:QueryWait();
end

-- We've got new data.
function AuctionLite:QueryNewData()
  assert(Query ~= nil);
  assert(Query.state == QUERY_STATE_WAIT);

  -- We've completed one of our own queries.
  local seen = Query.page * NUM_AUCTION_ITEMS_PER_PAGE + Query.batch;

  -- If we're running a getAll query, we'd better have seen everything.
  assert(not Query.getAll or seen == Query.total);

  -- Update status.
  local pct = 0;
  if Query.total > 0 then
    pct = math.floor(seen * 100 / Query.total);
  end
  if Query.update ~= nil then
    Query.update(pct, Query.getAll);
  end

  -- Handle the new data based on the kind of query.
  if Query.listing == nil then
    -- This is a search query, not a purchase.
    if seen < Query.total then
      -- Request the next page.
      self:QueryNext();
    else
      local oldQuery = Query;
      -- We're done.  End the query and return the results.
      self:QueryEnd();
      -- Update our price info.
      local results = self:AnalyzeData(oldQuery.data);
      for link, result in pairs(results) do 
        self:UpdateHistoricalPrice(link, result);
      end
      -- Notify our caller.
      if oldQuery.finish ~= nil then
        oldQuery.finish(results, oldQuery.link);
      end
    end
  else
    assert(not Query.getAll);
    assert(Query.found == nil);

    -- See if we've found the auction we're looking for.
    local i;
    for i = 1, Query.batch do
      local listing = Query.data[Query.page * NUM_AUCTION_ITEMS_PER_PAGE + i];
      if self:MatchListing(Query.name, Query.listing, listing) then
        Query.found = listing;
        Query.found.index = i;
        break;
      end
    end

    -- If we found something, request approval.
    -- Otherwise, get the next page or end the query.
    if Query.found ~= nil then
      self:RequestApproval();
      self:QueryRequestApproval();
    elseif seen < Query.total then
      self:QueryNext();
    else
      local oldQuery = Query;
      self:QueryEnd();
      if oldQuery.finish ~= nil then
        oldQuery.finish();
      end
    end
  end
end

-- Handle a completed auction query.
function AuctionLite:AUCTION_ITEM_LIST_UPDATE()
  -- If we were waiting for approval of a purchase, and the list of items
  -- changed from underneath us, we need to find the item again.
  if Query ~= nil and Query.state == QUERY_STATE_APPROVE then
    self:RestartApproval();
  end
  -- Now handle the data for real.
  if Query ~= nil and Query.state == QUERY_STATE_WAIT then
    Query.batch, Query.total = GetNumAuctionItems("list");

    -- Workaround for Blizzard bug in getAll queries that can cause it
    -- to return bogus values for Query.total.
    if Query.getAll and Query.total ~= Query.batch then
      Query.total = Query.batch;
    end

    local incomplete = 0;
    local i;

    for i = 1, Query.batch do
      local listing = self:GetListing("list", i);

      -- Sometimes we get incomplete records.  Is this one of them?
      if listing.owner == nil then
        incomplete = incomplete + 1;
      end

      -- Record the data.
      Query.data[Query.page * NUM_AUCTION_ITEMS_PER_PAGE + i] = listing;
    end

    local duplicate =
      Query.page > 0 and
      self:MatchPages(Query.data, Query.page - 1, Query.page);

    -- If we got a duplicate record, request the current one again.
    -- If it's an incomplete record, wait.
    -- Otherwise, process the data.
    if duplicate and Query.retries < MAX_RETRIES then
      Query.retries = Query.retries + 1;
      self:QueryCurrent();
    elseif Query.wait and incomplete > 0 then
      Query.time = time();
    else
      Query.time = nil;
      self:QueryNewData();
    end
  end
end
