vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:
------------------------------------------------------
-- ReagentCost.lua
------------------------------------------------------
FRC_VERSION = "11200.1";
------------------------------------------------------

FRC_Config = { };
FRC_Config.Enabled = true;
FRC_Config.MinProfitRatio = 0;
FRC_Config.MinProfitMoney = nil;
FRC_Config.AutoLoadPriceSource = nil;

FRC_ReagentLinks = { };

local MIN_SCANS = 35; -- times an item must be seen at auction to be considered a good sample (equates to 100% on our confidence scale)
local MIN_CONFIDENCE = 5; -- cutoff so we don't report items we have little data on as potentially profitable
local MIN_OVERRIDE_CONFIDENCE = 90; -- cutoff for trusting an item's market price versus the price of its components

-- Anti-freeze code borrowed from ReagentInfo (in turn, from Quest-I-On):
-- keeps WoW from locking up if we try to scan the tradeskill window too fast.
FRC_TradeSkillLock = { };
FRC_TradeSkillLock.NeedScan = false;
FRC_TradeSkillLock.Locked = false;
FRC_TradeSkillLock.EventTimer = 0;
FRC_TradeSkillLock.EventCooldown = 0;
FRC_TradeSkillLock.EventCooldownTime = 1;
FRC_CraftLock = { };
FRC_CraftLock.NeedScan = false;
FRC_CraftLock.Locked = false;
FRC_CraftLock.EventTimer = 0;
FRC_CraftLock.EventCooldown = 0;
FRC_CraftLock.EventCooldownTime = 1;

function FRC_CraftFrame_SetSelection(id)
        FRC_Orig_CraftFrame_SetSelection(id);
        
        if ( not id ) then
                return;
        end
        local name, rank, maxRank = GetCraftDisplaySkillLine();
        if not (name) then
                return;
        end
        local craftName, craftSubSpellName, craftType, numAvailable, isExpanded, trainingPointCost, requiredLevel = GetCraftInfo(id);
        if ( trainingPointCost and trainingPointCost > 0 ) then
                return;
        end
        if ( craftType == "header" ) then
                return;
        end

        local costText;
        if (FRC_Config.Enabled) then
                local itemLink = GetCraftItemLink(id);
                if not (itemLink) then
                        itemLink = craftName; -- Some enchanting formulae produce items (runed rods), most don't. Enchants don't link, items do.
                end
                itemLink = FRC_NormalizeLink(itemLink);
                local materialsTotal, confidenceScore = FRC_MaterialsCost(name, itemLink);
                costText = GFWUtils.LtY("(Total cost: ");
                if (materialsTotal == nil) then
                        if (FRC_PriceSource == "Auctioneer" and not IsAddOnLoaded("Auctioneer")) then
                                costText = costText .. GFWUtils.Gray("[Auctioneer not loaded]");
                        else
                                costText = costText .. GFWUtils.Gray("Unknown [insufficient data]");
                        end
                else
                        costText = costText .. GFWUtils.TextGSC(materialsTotal) ..GFWUtils.Gray(" Confidence: "..confidenceScore.."%");
                end
                costText = costText ..GFWUtils.LtY(")");

                CraftReagentLabel:SetText(SPELL_REAGENTS.." "..costText);
                CraftReagentLabel:Show();
        end
        
end

function FRC_TradeSkillFrame_SetSelection(id)
        FRC_Orig_TradeSkillFrame_SetSelection(id);
        
        if ( not id ) then
                return;
        end
        local skillName, skillType, numAvailable, isExpanded = GetTradeSkillInfo(id);
        if ( skillType == "header" ) then
                return;
        end
        local skillLineName, skillLineRank, skillLineMaxRank = GetTradeSkillLine();
        
        local costText;
        if (FRC_Config.Enabled) then
                local link = GetTradeSkillItemLink(id);
                if (link == nil) then return; end
                link = FRC_NormalizeLink(link);
                
                local materialsTotal, confidenceScore = FRC_MaterialsCost(skillLineName, GetTradeSkillItemLink(id));
                costText = GFWUtils.LtY("(Total cost: ");
                if (materialsTotal == nil) then
                        if (FRC_PriceSource == "Auctioneer" and not IsAddOnLoaded("Auctioneer")) then
                                costText = costText .. GFWUtils.Gray("[Auctioneer not loaded]");
                        else
                                costText = costText .. GFWUtils.Gray("Unknown [insufficient data]");
                        end
                else
                        costText = costText .. GFWUtils.TextGSC(materialsTotal) ..GFWUtils.Gray(" Confidence: "..confidenceScore.."%");
                end
                costText = costText ..GFWUtils.LtY(")");

                TradeSkillReagentLabel:SetText(SPELL_REAGENTS.." "..costText);
                TradeSkillReagentLabel:Show();
        end
        
end

function FRC_TradeSkillFrame_Update()
        FRC_Orig_TradeSkillFrame_Update();
        
        FRC_ScanTradeSkill();
end

function FRC_CraftFrame_Update()
        FRC_Orig_CraftFrame_Update();
        
        FRC_ScanCraft();
end

function FRC_OnLoad()
        
        -- Register Slash Commands
        SLASH_FRC1 = "/reagentcost";
        SLASH_FRC2 = "/rc";
        SlashCmdList["FRC"] = function(msg)
                FRC_ChatCommandHandler(msg);
        end
        
        local name, title, notes, enabled, loadable, reason, security = GetAddOnInfo("Auctioneer");
        if (loadable or IsAddOnLoaded("Auctioneer")) then
                FRC_PriceSource = "Auctioneer";
        elseif (KC_Auction ~= nil) then
                FRC_PriceSource = "KC_Items";
        elseif (AuctionMatrix_Version ~= nil) then                      
                FRC_PriceSource = "AuctionMatrix";
    elseif (WOWEcon_Enabled ~= nil) then
                FRC_PriceSource = "WOWEcon_PriceMod";
        end
        
        this:RegisterEvent("CRAFT_SHOW");
        this:RegisterEvent("CRAFT_UPDATE");
        this:RegisterEvent("TRADE_SKILL_SHOW");
        this:RegisterEvent("TRADE_SKILL_UPDATE");
        this:RegisterEvent("ADDON_LOADED");
        this:RegisterEvent("VARIABLES_LOADED");
        
        GFWUtils.Print("Fizzwidget Reagent Cost "..FRC_VERSION.." initialized!");

end

function FRC_OnUpdate(elapsed)
        -- If it's been more than a second since our last tradeskill update,
        -- we can allow the event to process again.
        FRC_TradeSkillLock.EventTimer = FRC_TradeSkillLock.EventTimer + elapsed;
        if (FRC_TradeSkillLock.Locked) then
                FRC_TradeSkillLock.EventCooldown = FRC_TradeSkillLock.EventCooldown + elapsed;
                if (FRC_TradeSkillLock.EventCooldown > FRC_TradeSkillLock.EventCooldownTime) then

                        FRC_TradeSkillLock.EventCooldown = 0;
                        FRC_TradeSkillLock.Locked = false;
                end
        end
        FRC_CraftLock.EventTimer = FRC_CraftLock.EventTimer + elapsed;
        if (FRC_CraftLock.Locked) then
                FRC_CraftLock.EventCooldown = FRC_CraftLock.EventCooldown + elapsed;
                if (FRC_CraftLock.EventCooldown > FRC_CraftLock.EventCooldownTime) then

                        FRC_CraftLock.EventCooldown = 0;
                        FRC_CraftLock.Locked = false;
                end
        end
        
        if (FRC_TradeSkillLock.NeedScan) then
                FRC_TradeSkillLock.NeedScan = false;
                FRC_ScanTradeSkill();
        end
        if (FRC_CraftLock.NeedScan) then
                FRC_CraftLock.NeedScan = false;
                FRC_ScanCraft();
        end
end

function FRC_OnEvent(event)
        
        if (event == "ADDON_LOADED" and (arg1 == "Blizzard_CraftUI" or IsAddOnLoaded("Blizzard_CraftUI"))) then
                if (FRC_Orig_CraftFrame_SetSelection == nil) then
                        -- Overrides for displaying info in CraftFrame
                        FRC_Orig_CraftFrame_SetSelection = CraftFrame_SetSelection;
                        CraftFrame_SetSelection = FRC_CraftFrame_SetSelection;

                        -- And for scanning, since it looks like doing it in event handlers is crashy/unreliable now.
                        FRC_Orig_CraftFrame_Update = CraftFrame_Update;
                        CraftFrame_Update = FRC_CraftFrame_Update;

                        GFWUtils.Print("ReagentCost CraftFrame hooks installed.");
                end
        end

        if (event == "ADDON_LOADED" and (arg1 == "Blizzard_TradeSkillUI" or IsAddOnLoaded("Blizzard_TradeSkillUI"))) then
                if (FRC_Orig_TradeSkillFrame_SetSelection == nil) then
                        -- Overrides for displaying info in TradeSkillFrame
                        FRC_Orig_TradeSkillFrame_SetSelection = TradeSkillFrame_SetSelection;
                        TradeSkillFrame_SetSelection = FRC_TradeSkillFrame_SetSelection;

                        -- And for scanning, since it looks like doing it in event handlers is crashy/unreliable now.
                        FRC_Orig_TradeSkillFrame_Update = TradeSkillFrame_Update;
                        TradeSkillFrame_Update = FRC_TradeSkillFrame_Update;
                
                        GFWUtils.Print("ReagentCost TradeSkillFrame hooks installed.");
                end
        end

        if ( event == "VARIABLES_LOADED" or event == "ADDON_LOADED" ) then

                local name, title, notes, enabled, loadable, reason, security = GetAddOnInfo("Auctioneer");
                if ((loadable or IsAddOnLoaded("Auctioneer")) and FRC_PriceSource == nil) then
                        FRC_PriceSource = "Auctioneer";
                end
                if (AUCTIONEER_VERSION ~= nil) then
                        local _, _, major, minor = string.find(AUCTIONEER_VERSION, "^(%d+)%.(%d+)");
                        major, minor = tonumber(major), tonumber(minor);
                        if (major ~= nil and major >= 3 and minor ~= nil and minor >= 1) then
                                FRC_PriceSource = "Auctioneer";
                        end
                end
                return;
        end
        
        if ( event == "TRADE_SKILL_SHOW" or event == "CRAFT_SHOW" and FRC_Config.Enabled) then
        
                if (event == "CRAFT_SHOW" and GetCraftDisplaySkillLine() == nil) then
                        -- Beast Training uses the CraftFrame; we can tell when it's up because it doesn't have a skill-level bar.
                        -- We don't have anything to do in that case, so let's not try loading Auctioneer and stuff.
                        return;         
                end
                
                local name, title, notes, enabled, loadable, reason, security = GetAddOnInfo("Auctioneer");
                if ((loadable or IsAddOnLoaded("Auctioneer")) and FRC_PriceSource == nil) then
                        FRC_PriceSource = "Auctioneer";
                end
                if ( FRC_PriceSource == "Auctioneer" and FRC_Config.AutoLoadPriceSource) then
                        if (not IsAddOnLoaded("Auctioneer")) then
                                local loaded, reason = LoadAddOn("Auctioneer");
                                if (not loaded) then
                                        GFWUtils.Print("Can't load Auctioneer: "..reason);
                                        return;
                                end
                        end
                end
                if ( FRC_PriceSource == nil) then
                        GFWUtils.Print("ReagentCost: missing required dependency. Can't find Auctioneer, KC_Items, or AuctionMatrix.");
                        return;
                end
        end

        if ( event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_UPDATE" ) then

                FRC_ScanTradeSkill();
                
        elseif ( event == "CRAFT_SHOW" or event == "CRAFT_UPDATE" ) then
        
                FRC_ScanCraft();
                
        end

end

function FRC_ChatCommandHandler(msg)

        if (FRC_PriceSource == nil) then
                GFWUtils.Print("Fizzwidget Reagent Cost is installed but non-functional; can't find Auctioneer, KC_Items (with auction module), or AuctionMatrix.");
                return;
        end

        -- Print Help
        if ( msg == "help" ) or ( msg == "" ) then
                GFWUtils.Print("Fizzwidget Reagent Cost "..FRC_VERSION..":");
                GFWUtils.Print("/reagentcost (or /rc) <command>");
                GFWUtils.Print("- "..GFWUtils.Hilite("help").." - Print this helplist.");
                GFWUtils.Print("- "..GFWUtils.Hilite("status").." - Check current settings.");
                GFWUtils.Print("- "..GFWUtils.Hilite("reset").." - Reset to default settings.");
                GFWUtils.Print("- "..GFWUtils.Hilite("on").." | "..GFWUtils.Hilite("off").." - Toggle displaying info in tradeskill windows.");
                if (FRC_PriceSource == "Auctioneer") then
                        GFWUtils.Print("- "..GFWUtils.Hilite("autoload on").." | "..GFWUtils.Hilite("off").." - Control whether to automatically load Auctioneer when showing tradeskill windows.");
                end
                GFWUtils.Print("- "..GFWUtils.Hilite("report [<skillname>]").." - Output a list of the most profitable tradeskill items you can make. (Or only those produced through <skillname>.)");
                GFWUtils.Print("- "..GFWUtils.Hilite("minprofit <number>").." - When reporting, only show items whose estimated profit is <number> or greater. (In copper, so 1g == 10000.)");
                GFWUtils.Print("- "..GFWUtils.Hilite("minprofit <number>%").." - When reporting, only show items whose estimated profit exceeds its cost of materials by <number> percent or more.");
                return;
        end

        if (msg == "version") then
                GFWUtils.Print("Fizzwidget Reagent Cost "..FRC_VERSION);
                return;
        end
                
        -- Check Status
        if ( msg == "status" ) then
                if (FRC_Config.Enabled) then
                        GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("is").." displaying materials cost in tradeskill windows.");
                        if (FRC_Config.AutoLoadPriceSource) then
                                GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("will").." automatically load Auctioneer to show prices in tradeskill windows.");
                        else
                                GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("will not").." automatically load Auctioneer; prices will not be shown in tradeskill windows until Auctioner is loaded some other way.");
                        end
                else
                        GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("is not").." displaying materials cost in tradeskill windows.");
                end
                if (FRC_Config.MinProfitMoney == nil) then
                        GFWUtils.Print("Reports will only include items whose estimated profit exceeds materials cost by "..GFWUtils.Hilite(FRC_Config.MinProfitRatio.."%").." or more.");
                else
                        GFWUtils.Print("Reports will only include items whose estimated profit is "..GFWUtils.TextGSC(FRC_Config.MinProfitMoney).." or greater.");
                end
                return;
        end

        -- Reset Variables
        if ( msg == "reset" ) then
                FRC_Config.Enabled = true;
                FRC_Config.MinProfitRatio = 0;
                FRC_Config.MinProfitMoney = nil;
                GFWUtils.Print("Reagent Cost configuration reset.");
                FRC_ChatCommandHandler("status");
                return;
        end
        
        -- Turn trade info gathering on
        if ( msg == "on" ) then
                FRC_Config.Enabled = true;
                GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("is").." displaying materials cost in tradeskill windows.");
                return;
        end
        
        -- Turn trade info gathering Off
        if ( msg == "off" ) then
                FRC_Config.Enabled = false;
                GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("is not").." displaying materials cost in tradeskill windows.");
                return;
        end

        if ( msg == "autoload on" ) then
                FRC_Config.AutoLoadPriceSource = true;
                GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("will").." automatically load Auctioneer to show prices in tradeskill windows.");
                return;
        end
        if ( msg == "autoload off" ) then
                FRC_Config.AutoLoadPriceSource = nil;
                GFWUtils.Print("Reagent Cost "..GFWUtils.Hilite("will not").." automatically load Auctioneer; prices will not be shown in tradeskill windows until Auctioner is loaded some other way.");
                return;
        end

        local _, _, cmd, args = string.find(msg, "(%w+) *(.*)");
        if ( cmd == "minprofit" ) then

                local _, _, number, isPercent = string.find(msg, "minprofit (-*%d+)(%%*)");
                if (number == nil) then
                        GFWUtils.Print("Usage: "..GFWUtils.Hilite("/rc minprofit <number>[%]"));
                        return;
                end
                if (isPercent == "%") then
                        FRC_Config.MinProfitRatio = tonumber(number);
                        FRC_Config.MinProfitMoney = nil;
                        GFWUtils.Print("Reports will only include items whose estimated profit exceeds materials cost by "..GFWUtils.Hilite(FRC_Config.MinProfitRatio.."%").." or more.");
                else
                        FRC_Config.MinProfitRatio = nil;
                        FRC_Config.MinProfitMoney = tonumber(number);
                        GFWUtils.Print("Reports will only include items whose estimated profit is "..GFWUtils.TextGSC(FRC_Config.MinProfitMoney).." or greater.");
                end
                return;
        end

        if ( ( cmd == "reagents" or cmd == "report" ) and FRC_PriceSource == "Auctioneer") then
                if (not IsAddOnLoaded("Auctioneer")) then
                        local loaded, reason = LoadAddOn("Auctioneer");
                        if (not loaded) then
                                GFWUtils.Print("Can't load Auctioneer: "..reason);
                                return;
                        end
                end
        end

        if ( cmd == "reagents" or cmd == "report" ) then
                
                -- check second arg
                local _, _, arg1, moreArgs = string.find(args, "(%w+) *(.*)");
                local scope = "toon";
                if (arg1 == "all") then
                        scope = "realm";
                        args = moreArgs;
                elseif (arg1 == "allrealms") then
                        scope = "all";
                        args = moreArgs;
                end
                
                -- parse skill names from args
                local mySkills = { };
                if (args ~= nil and args ~= "") then
                        for word in string.gfind(args, "[^%s]+") do
                                local niceWord = string.upper(string.sub(word, 1, 1))..string.sub(word, 2);
                                table.insert(mySkills, niceWord);
                        end
                end
                
                -- if no args, use the skills this character knows
                if (table.getn(mySkills) == 0) then
                        for skillIndex = 1, GetNumSkillLines() do
                                local skillName, _, _, _, _, _, _, isAbandonable = GetSkillLineInfo(skillIndex);
                                if (isAbandonable) then
                                        table.insert(mySkills, skillName);
                                end
                        end
                end
                
                local printList;
                if (cmd == "report") then
                        printList = FRC_ReportForSkill;
                elseif (cmd == "reagents") then
                        printList = FRC_ListAllReagents;
                end
                for _, skillName in mySkills do
                        printList(skillName, scope);
                end
                
                return;
        end
        
        _, _, itemLink = string.find(msg, "(|c%x+|Hitem:%d+:%d+:%d+:%d+|h%[.-%]|h|r)");
        if (itemLink ~= nil and itemLink ~= "") then
                itemLink = FRC_NormalizeLink(itemLink);
                if (not IsAddOnLoaded("Auctioneer")) then
                        local loaded, reason = LoadAddOn("Auctioneer");
                        if (not loaded) then
                                GFWUtils.Print("Can't load Auctioneer: "..reason);
                                return;
                        end
                end
                local found = false;
                for skillName, skillTable in FRC_ReagentLinks do
                        if (skillTable[itemLink] ~= nil) then
                                for recipe, reagentList in skillTable[itemLink] do
                                        if (string.find(itemLink, "%["..recipe.."%]")) then
                                                GFWUtils.Print(itemLink.." ("..skillName.."):");
                                        else
                                                GFWUtils.Print(itemLink.." ("..skillName.." - "..recipe.."):");
                                        end
                                        found = true;
                                        for _, reagentInfo in reagentList do
                                                if (type(reagentInfo) == "table") then
                                                        local price, confidence, isAdjusted = FRC_AdjustedCost(skillName, reagentInfo.link);                                            
                                                        local adjustedText, confidenceText;
                                                        if (isAdjusted) then
                                                                adjustedText = "(based on component prices)";
                                                        else
                                                                adjustedText = "";
                                                        end
                                                        if (confidence < 0) then
                                                                confidenceText = "from vendor";
                                                        else
                                                                confidenceText = confidence.."%"
                                                        end
                                                        if (price ~= nil) then
                                                                GFWUtils.Print(GFWUtils.Hilite(reagentInfo.count.."x ")..reagentInfo.link..": "..GFWUtils.TextGSC(price * reagentInfo.count)..GFWUtils.Gray(" ("..confidenceText..") ")..adjustedText);
                                                        else
                                                                GFWUtils.Print(GFWUtils.Hilite(reagentInfo.count.."x ")..reagentInfo.link..": No price data");
                                                        end
                                                end
                                        end
                                
                                        local itemPrice, itemConfidence = FRC_TypicalItemPrice(itemLink);
                                        local materialsCost, matsConfidence = FRC_MaterialsCostForRecipe(skillName, itemLink, recipe);
                                        local profit = itemPrice - materialsCost;
                                        local profitText;
                                        if (profit > 0) then
                                                profitText = "profit ".. GFWUtils.TextGSC(profit);
                                        elseif (profit == 0) then
                                                profitText = GFWUtils.Hilite("(break-even)");
                                        else
                                                profitText = GFWUtils.Red("loss ").. GFWUtils.TextGSC(math.abs(profit));
                                        end
                                        if (materialsCost ~= nil) then
                                                GFWUtils.Print("Total materials: "..GFWUtils.TextGSC(materialsCost)..GFWUtils.Gray("("..matsConfidence..")"));
                                        else
                                                GFWUtils.Print("Total materials: data not available for one or more reagents");
                                        end
                                        if (itemPrice ~= nil) then
                                                GFWUtils.Print("Auction price: "..GFWUtils.TextGSC(itemPrice)..GFWUtils.Gray("("..itemConfidence..")").."; "..profitText);
                                        else
                                                GFWUtils.Print("Auction price: data not available");
                                        end
                                end
                        end
                end
                if (not found) then
                        GFWUtils.Print(itemLink.." not found in tradeskill data.");
                end
                return;
        end

        
        -- If we get down to here, we got bad input.
        FRC_ChatCommandHandler("help");
end

function FRC_ListAllReagents(skillName, scope)
        local itemsTable = FRC_ReagentLinks[skillName];
        if (itemsTable == nil) then
                if (ReagentData == nil) then
                        GFWUtils.Print("Nothing for "..GFWUtils.Hilite(skillName)..".");
                elseif (ReagentData['reversegathering'][skillName] ~= nil) then
                        -- do nothing; don't want to barf errors about gathering skills...
                elseif (ReagentData['reverseprofessions'][skillName] ~= nil) then
                        GFWUtils.Print("ReagentCost doesn't have information on "..GFWUtils.Hilite(skillName)..". Please open your "..GFWUtils.Hilite(skillName).." window before requesting a report.");
                else
                        GFWUtils.Print(GFWUtils.Hilite(skillName).." is not a known profession.");
                end
        else
                local realm = GetRealmName();
                local player = UnitName("player");
                for anItem, recipesTable in itemsTable do
                        for recipe, reagentList in recipesTable do
                                local known;
                                if (scope == "toon") then
                                        if (FRC_KnownRecipes and FRC_KnownRecipes[realm] and FRC_KnownRecipes[realm][player]) then
                                                known = GFWTable.KeyOf(FRC_KnownRecipes[realm][player], anItem);
                                        end
                                elseif (scope == "realm") then
                                        if (FRC_KnownRecipes and FRC_KnownRecipes[realm]) then
                                                for player, items in FRC_KnownRecipes[realm] do
                                                        if (GFWTable.KeyOf(FRC_KnownRecipes[realm][player], anItem)) then
                                                                known = true;
                                                                break;
                                                        end
                                                end                             
                                        end
                                else
                                        known = true;
                                end                             
                
                                if (known) then
                                        local itemString;
                                        if (string.find(anItem, "%["..recipe.."%]")) then
                                                itemString = anItem..": ";
                                        else
                                                itemString = anItem.." ("..recipe.."): ";
                                        end
                                        for _, aReagent in reagentsTable do
                                                itemString = itemString .. aReagent.count .. "x" .. aReagent.link .. ", ";
                                        end
                                        itemString = string.gsub(itemString, ", $", "");
                                        GFWUtils.Print(itemString);
                                end
                        end
                end
        end
end

function FRC_ReportForSkill(skillName, scope)
        local knownItems = 0;
        local reliableItems = 0;
        local shownItems = 0;
        local itemsTable = FRC_ReagentLinks[skillName];
        
        if (itemsTable == nil) then
                if (ReagentData == nil) then
                        GFWUtils.Print("Nothing for "..GFWUtils.Hilite(skillName)..".");
                elseif (ReagentData['reversegathering'][skillName] ~= nil) then
                        -- do nothing; don't want to barf errors about gathering skills...
                        if (skillName == ReagentData['gathering']['mining']) then
                                -- ...except for Mining, which is also a production skill as far as we're concerned.
                                GFWUtils.Print("ReagentCost doesn't have information on "..GFWUtils.Hilite(skillName)..". Please open your "..GFWUtils.Hilite(skillName).." window before requesting a report.");
                        end
                elseif (ReagentData['reverseprofessions'][skillName] ~= nil) then
                        GFWUtils.Print("ReagentCost doesn't have information on "..GFWUtils.Hilite(skillName)..". Please open your "..GFWUtils.Hilite(skillName).." window before requesting a report.");
                else
                        GFWUtils.Print(GFWUtils.Hilite(skillName).." is not a known profession.");
                end
                return;
        end
        
        local reportTable = { }; -- separate report for each skill

        -- first, build a table that includes current Auctioneer prices for composite items
        local realm = GetRealmName();
        local player = UnitName("player");
        for anItem in itemsTable do
                local known;
                if (scope == "toon") then
                        if (FRC_KnownRecipes and FRC_KnownRecipes[realm] and FRC_KnownRecipes[realm][player]) then
                                known = GFWTable.KeyOf(FRC_KnownRecipes[realm][player], anItem);
                        end
                elseif (scope == "realm") then
                        if (FRC_KnownRecipes and FRC_KnownRecipes[realm]) then
                                for player, items in FRC_KnownRecipes[realm] do
                                        if (GFWTable.KeyOf(FRC_KnownRecipes[realm][player], anItem)) then
                                                known = true;
                                                break;
                                        end
                                end                             
                        end
                else
                        known = true;
                end                             

                if (known) then
                        -- parse out a link so that we ignore non-auctionable craft recipes (i.e. enchants)
                        _, _, itemLink = string.find(anItem, "(|c%x+|Hitem:%d+:%d+:%d+:%d+|h%[.-%]|h|r)");
                        if (itemLink ~= nil and itemLink ~= "") then
                                itemLink = FRC_NormalizeLink(itemLink);
                                for recipe in FRC_ReagentLinks[skillName][itemLink] do
                                        knownItems = knownItems + 1;
                                        local itemPrice, itemConfidence = FRC_TypicalItemPrice(itemLink);
                                        local materialsCost, matsConfidence = FRC_MaterialsCostForRecipe(skillName, itemLink, recipe);
                
                                        if (itemConfidence == nil) then itemConfidence = 0; end
                                        if (matsConfidence == nil) then matsConfidence = 0; end
                
                                        if (itemConfidence >= MIN_CONFIDENCE and matsConfidence >= MIN_CONFIDENCE) then
                                                reliableItems = reliableItems + 1;
                                                local profit = itemPrice - materialsCost;
                                                table.insert(reportTable, {link=itemLink, recipe=recipe, matsCost=materialsCost, matsConf=matsConfidence, itemPrice=itemPrice, itemConf=itemConfidence, profit=profit});
                                        end
                                end
                        end
                end
        end
        

        if (knownItems == 0) then 
                GFWUtils.Print("ReagentCost doesn't have information on "..GFWUtils.Hilite(skillName)..". Please open your "..GFWUtils.Hilite(skillName).." window before requesting a report.");
                return;
        end
        
        if (reliableItems == 0) then 
                GFWUtils.Print("None of the "..GFWUtils.Hilite(knownItems).." items you can make with "..GFWUtils.Hilite(skillName).." have reliable auction price data. (They may not be tradeable.)");
                return;
        end
        
        GFWUtils.Print("Most profitable recipes for "..GFWUtils.Hilite(skillName)..":");

        if (reliableItems > 1) then
                table.sort(reportTable, FRC_SortProfit);
        end
        
        -- and report those that meet our minimum requirements
        for _, reportInfo in reportTable do
                if (FRC_Config.MinProfitRatio and (reportInfo.profit / reportInfo.matsCost * 100) >= FRC_Config.MinProfitRatio) then
                        shownItems = shownItems + 1;
                        FRC_PrintReportLine(reportInfo);
                elseif (FRC_Config.MinProfitMoney and reportInfo.profit >= FRC_Config.MinProfitMoney) then
                        shownItems = shownItems + 1;
                        FRC_PrintReportLine(reportInfo);
                end
        end
        GFWUtils.Print(GFWUtils.Hilite(knownItems).." recipes known, "..GFWUtils.Hilite(reliableItems).." with auction data, "..GFWUtils.Hilite(shownItems).." above profit threshold.");

end

function FRC_ScanTradeSkill()
        if (not TradeSkillFrame or not TradeSkillFrame:IsVisible() or FRC_TradeSkillLock.Locked) then return; end
        -- This prevents further update events from being handled if we're already processing one.
        -- This is done to prevent the game from freezing under certain conditions.
        FRC_TradeSkillLock.Locked = true;

        local skillLineName, skillLineRank, skillLineMaxRank = GetTradeSkillLine();
        if not (skillLineName) then
                FRC_TradeSkillLock.NeedScan = true;
                return; -- apparently sometimes we're called too early, this is nil, and all hell breaks loose.
        end
        if (FRC_ReagentLinks == nil) then
                FRC_ReagentLinks = { };
        end
        if (FRC_ReagentLinks[skillLineName] == nil) then
                FRC_ReagentLinks[skillLineName] = { };
        end

        local realm = GetRealmName();
        local player = UnitName("player");
        if (FRC_KnownRecipes == nil) then
                FRC_KnownRecipes = {};
        end
        if (FRC_KnownRecipes[realm] == nil) then
                FRC_KnownRecipes[realm] = {};
        end
        FRC_KnownRecipes[realm][player] = {};
        for id = GetNumTradeSkills(), 1, -1 do 
                -- loop from the bottom up, since the reagents we make for compound items are usually below the recipes that need them
                local skillName, skillType, numAvailable, isExpanded = GetTradeSkillInfo(id);
                if ( skillType ~= "header" ) then                               
                        local itemLink = GetTradeSkillItemLink(id);
                        if (itemLink == nil) then
                                FRC_TradeSkillLock.NeedScan = true;
                        else
                                table.insert(FRC_KnownRecipes[realm][player], itemLink);
                                
                                FRC_ReagentLinks[skillLineName][itemLink] = { };
                                FRC_ReagentLinks[skillLineName][itemLink][skillName] = { };
                                for i=1, GetTradeSkillNumReagents(id), 1 do
                                        local link = GetTradeSkillReagentItemLink(id, i);
                                        if (link == nil) then
                                                FRC_ReagentLinks[skillLineName][itemLink][skillName] = nil;
                                                FRC_TradeSkillLock.NeedScan = true;
                                                break;
                                        else
                                                local reagentName, reagentTexture, reagentCount, playerReagentCount = GetTradeSkillReagentInfo(id, i);
                                                table.insert(FRC_ReagentLinks[skillLineName][itemLink][skillName], {link=link, count=reagentCount});
                                        end
                                end
                        end
                end
        end

end

function FRC_ScanCraft()
        if (not CraftFrame or not CraftFrame:IsVisible() or FRC_CraftLock.Locked) then return; end
        -- This prevents further update events from being handled if we're already processing one.
        -- This is done to prevent the game from freezing under certain conditions.
        FRC_CraftLock.Locked = true;

        -- This is used only for Enchanting
        local skillLineName, rank, maxRank = GetCraftDisplaySkillLine();
        if not (skillLineName) then
                return; -- Hunters' Beast Training also uses the CraftFrame, but doesn't have a SkillLine.
        end
        if (FRC_ReagentLinks == nil) then
                FRC_ReagentLinks = { };
        end
        if (FRC_ReagentLinks[skillLineName] == nil) then
                FRC_ReagentLinks[skillLineName] = { };
        end

        local realm = GetRealmName();
        local player = UnitName("player");
        if (FRC_KnownRecipes == nil) then
                FRC_KnownRecipes = {};
        end
        if (FRC_KnownRecipes[realm] == nil) then
                FRC_KnownRecipes[realm] = {};
        end
        FRC_KnownRecipes[realm][player] = {};
        for id = GetNumCrafts(), 1, -1 do
                if ( craftType ~= "header" ) then
                        craftName, craftSubSpellName, craftType, numAvailable, isExpanded, trainingPointCost, requiredLevel = GetCraftInfo(id);
                        local itemLink = GetCraftItemLink(id);
                        if (itemLink == nil) then
                                itemLink = craftName; -- may be an item, may be a (currently unlinkable) enchant
                        end
                        if (itemLink == nil) then
                                FRC_TradeSkillLock.NeedScan = true;
                        else
                                table.insert(FRC_KnownRecipes[realm][player], itemLink);
                                
                                FRC_ReagentLinks[skillLineName][itemLink] = { };
                                FRC_ReagentLinks[skillLineName][itemLink][craftName] = { };
                                for i=1, GetCraftNumReagents(id), 1 do
                                        local link = GetCraftReagentItemLink(id, i);
                                        if (link == nil) then
                                                FRC_ReagentLinks[skillLineName][itemLink][craftName] = nil;
                                                FRC_CraftLock.NeedScan = true;
                                                break;
                                        else
                                                local reagentName, reagentTexture, reagentCount, playerReagentCount = GetCraftReagentInfo(id, i);
                                                table.insert(FRC_ReagentLinks[skillLineName][itemLink][craftName], {link=link, count=reagentCount});
                                        end
                                end
                        end
                end
        end
end

function FRC_SortProfit(a, b)
        -- sort by ratio or actual amount based on which we're using as cutoff
        if (FRC_Config.MinProfitRatio ~= nil) then
                return (a.profit / a.matsCost) > (b.profit / b.matsCost);
        else
                return a.profit > b.profit;
        end
end

function FRC_PrintReportLine(reportInfo)
        local reportLine;
        if (string.find(reportInfo.link, "%["..reportInfo.recipe.."%]")) then
                reportLine = reportInfo.link..": ";
        else
                reportLine = reportInfo.link.." ("..reportInfo.recipe.."): ";
        end
        reportLine = reportLine .."materials cost ".. GFWUtils.TextGSC(reportInfo.matsCost) ..GFWUtils.Gray(" ("..reportInfo.matsConf.."%)")..", "
        reportLine = reportLine .."auction price ".. GFWUtils.TextGSC(reportInfo.itemPrice) ..GFWUtils.Gray(" ("..reportInfo.itemConf.."%)")..", "
        if (reportInfo.profit >= 0) then
                reportLine = reportLine .."profit ".. GFWUtils.TextGSC(reportInfo.profit);
        else
                reportLine = reportLine ..GFWUtils.Red("loss ").. GFWUtils.TextGSC(reportInfo.profit);
        end
        GFWUtils.Print(reportLine);
end

function FRC_AdjustedCost(skillName, itemLink)

        local itemPrice, itemConfidence = FRC_TypicalItemPrice(itemLink);
        if (FRC_RecursiveItems == nil) then
                FRC_RecursiveItems = {};
        end
        if (GFWTable.KeyOf(FRC_RecursiveItems, itemLink)) then
                -- avoid infinite recursion
                FRC_RecursiveItems = nil;
                return itemPrice, itemConfidence, false;
        else
                table.insert(FRC_RecursiveItems, itemLink);
        end
        
        -- don't calculate sub-reagent prices for the likes of alchemical transumutes
        -- (recipes that take one reagent also produced by the same skill and produce one other such reagent)
        if (FRC_ReagentLinks[skillName] and FRC_ReagentLinks[skillName][itemLink]) then
                for recipe, reagentsList in FRC_ReagentLinks[skillName][itemLink] do
                        if (table.getn(reagentsList) == 1 ) then
                                local reagentInfo = reagentsList[1];
                                if (reagentInfo.count == 1 and FRC_ReagentLinks[skillName][reagentInfo.link]) then
                                        return itemPrice, itemConfidence, false;
                                end
                        end
                end
        end
        
        -- for all other recipes, calculate total cost of reagents which might be produced by the same skill,
        -- and use that amount if it's more reliable. 
        -- (e.g. engineering parts -> base reagents, bolts of cloth -> pieces of cloth)
        local subReagentsPrice, subReagentsConfidence = FRC_MaterialsCost(skillName, itemLink);
        if (subReagentsPrice and subReagentsConfidence) then
                if (not (itemPrice and itemConfidence)) then
                        return subReagentsPrice, subReagentsConfidence, true;
                end
                if (subReagentsConfidence >= itemConfidence and itemConfidence < MIN_OVERRIDE_CONFIDENCE and subReagentsPrice < itemPrice) then
                        return subReagentsPrice, subReagentsConfidence, true;
                end
        end
        return itemPrice, itemConfidence, false;
end

function FRC_MaterialsCost(skillName, itemLink)
        if (FRC_ReagentLinks[skillName] == nil) then
                return nil, nil;
        end
        if (FRC_ReagentLinks[skillName][itemLink] == nil) then
                return nil, nil;
        end

        local pricesPerRecipe = {};
        for recipe in FRC_ReagentLinks[skillName][itemLink] do
                if (type(recipe) == "string") then
                        local cost, confidence = FRC_MaterialsCostForRecipe(skillName, itemLink, recipe);
                        if (cost) then
                                table.insert(pricesPerRecipe, {cost=cost, confidence=confidence});
                        end
                end
        end
        if (table.getn(pricesPerRecipe) == 0) then
                return nil, nil;
        end
        
        local sortCost = function(a,b)
                return a.cost < b.cost;
        end
        local sortConfidence = function(a,b)
                return a.confidence > b.confidence;
        end
        table.sort(pricesPerRecipe, sortConfidence);
        table.sort(pricesPerRecipe, sortCost);
        
        return pricesPerRecipe[1].cost, pricesPerRecipe[1].confidence;
        
end

function FRC_MaterialsCostForRecipe(skillName, itemLink, recipeName)
        local materialsTotal = 0;
        local totalConfidence = 0;
        local numAuctionReagents = 0;
                
        if (FRC_ReagentLinks[skillName] == nil) then
                return nil, nil;
        end
        if (FRC_ReagentLinks[skillName][itemLink] == nil) then
                return nil, nil;
        end
        if (FRC_ReagentLinks[skillName][itemLink][recipeName] == nil) then
                return nil, nil;
        end
        
        for _, reagentInfo in FRC_ReagentLinks[skillName][itemLink][recipeName] do
                local price, confidence = FRC_AdjustedCost(skillName, reagentInfo.link)
                if (price == nil) then
                        return nil, nil; -- if any of the reagents is missing price info, we can't calculate a total.
                end
                materialsTotal = materialsTotal + (price * reagentInfo.count);
                if (confidence >= 0) then
                        totalConfidence = totalConfidence + confidence;
                        numAuctionReagents = numAuctionReagents + 1;
                end             
        end
        local confidenceScore = math.floor((totalConfidence / numAuctionReagents) * 100) / 100;
        
        return materialsTotal, confidenceScore;

end

function FRC_TypicalItemPrice(itemLink)
        if (FRC_PriceSource == "Auctioneer") then
                if (not IsAddOnLoaded("Auctioneer")) then
                        if (FRC_Config.AutoLoadPriceSource) then
                                local loaded, reason = LoadAddOn("Auctioneer");
                                if (not loaded) then
                                        GFWUtils.Print("Can't load Auctioneer: "..reason);
                                        return nil;
                                end
                        else
                                return nil;
                        end
                end     
                return FRC_AuctioneerItemPrice(itemLink);
        elseif (FRC_PriceSource == "KC_Items") then
                return FRC_KCItemPrice(itemLink);
        elseif (FRC_PriceSource == "AuctionMatrix") then
                return FRC_AuctionMatrixItemPrice(itemLink);
    elseif (FRC_PriceSource == "WOWEcon_PriceMod") then
                return FRC_WOWEcon_PriceModItemPrice(itemLink);
        else
                return nil; 
        end
end

function FRC_AuctioneerItemPrice(itemLink)
        local getUsableMedian = Auctioneer_GetUsableMedian;
        local getHistoricalMedian = Auctioneer_GetItemHistoricalMedianBuyout;
        local getVendorSellPrice = Auctioneer_GetVendorSellPrice;
        if (Auctioneer and Auctioneer.Statistic) then
                getUsableMedian = Auctioneer.Statistic.GetUsableMedian;
                getHistoricalMedian = Auctioneer.Statistic.GetItemHistoricalMedianBuyout;
        end
        if (Auctioneer and Auctioneer.API) then
                getVendorSellPrice = Auctioneer.API.GetVendorSellPrice;
        end
        if (not (getUsableMedian and getHistoricalMedian)) then
                GFWUtils.PrintOnce(GFWUtils.Red("ReagentCost error:").." missing expected Auctioneer API; can't calculate item prices.", 5);
                return nil, nil;
        end
        
        local _, _, itemID, randomProp, enchant = string.find(itemLink, "item:(%d+):(%d+):(%d+):%d+");
        local itemKey = itemID..":"..(randomProp or 0)..":"..(enchant or 0);    
        local medianPrice, medianCount = getUsableMedian(itemKey);
        if (medianPrice == nil) then
                medianPrice, medianCount = getHistoricalMedian(itemKey);
        end
        if (medianCount == nil) then medianCount = 0 end
        
        itemID = tonumber(itemID) or 0;
        local buyFromVendorPrice = 0;
        local sellToVendorPrice = 0;
        if (FRC_VendorPrices[itemID]) then
                buyFromVendorPrice = FRC_VendorPrices[itemID].b;
                sellToVendorPrice = FRC_VendorPrices[itemID].s;
        end
        if (sellToVendorPrice == 0) then
                if (getVendorSellPrice) then
                        sellToVendorPrice = getVendorSellPrice(itemID) or 0;
                elseif (Auctioneer_BasePrices ~= nil and Auctioneer_BasePrices[itemID] ~= nil and Auctioneer_BasePrices[itemID].s ~= nil) then
                        sellToVendorPrice = Auctioneer_BasePrices[itemID].s or 0;
                end
        end
                
        if (buyFromVendorPrice > 0) then
                return buyFromVendorPrice, -1; -- FRC_VendorPrices lists only the primarily-vendor-bought tradeskill items
        elseif (medianCount == 0 or medianPrice == nil) then
                return sellToVendorPrice * 3, 0; -- generally a good guess for auction price if we don't have real auction data
        else
                return medianPrice, math.floor((math.min(medianCount, MIN_SCANS) / MIN_SCANS) * 1000) / 10; 
        end
end

function FRC_KCItemPrice(itemLink)
        local itemCode = KC_Common:GetCode(itemLink);
        local seen, avgstack, min, bidseen, bid, buyseen, buy = KC_Auction:GetItemData(itemCode);
        local _, _, itemID  = string.find(itemLink, ".Hitem:(%d+):%d+:%d+:%d+.h%[[^]]+%].h");
        itemID = tonumber(itemID) or 0;
        
        local buyFromVendorPrice = 0;
        local sellToVendorPrice = 0;
        if (FRC_VendorPrices[itemID]) then
                buyFromVendorPrice = FRC_VendorPrices[itemID].b;
                sellToVendorPrice = FRC_VendorPrices[itemID].s;
        end
        if (sellToVendorPrice == 0 and KC_SellValue ~= nil) then
                sellToVendorPrice = (KC_Common:GetItemPrices(itemCode) or 0);
        end
        
        --DevTools_Dump({itemLink=itemLink, itemID=itemID, buy=buy, buyseen=buyseen, buyFromVendorPrice=buyFromVendorPrice, sellToVendorPrice=sellToVendorPrice});

        if (buyFromVendorPrice ~= nil and buyFromVendorPrice > 0) then
                return buyFromVendorPrice, -1; -- FRC_VendorPrices lists only the primarily-vendor-bought tradeskill items
        elseif (buy ~= nil and buy > 0) then
                return buy, math.floor((math.min(buyseen, MIN_SCANS) / MIN_SCANS) * 1000) / 10;
        elseif (sellToVendorPrice ~= nil and sellToVendorPrice > 0) then
                return sellToVendorPrice * 3, 0; -- generally a good guess for auction price if we don't have real auction data
        else
                GFWUtils.DebugLog(itemLink.." not found in KC_Auction or vendor-reagent prices list");
                return nil, 0;
        end
end

function FRC_AuctionMatrixItemPrice(itemLink)
        local _, _, itemID, itemName  = string.find(itemLink, ".Hitem:(%d+):%d+:%d+:%d+.h%[([^]]+)%].h");
        local buyFromVendorPrice = 0;
        local sellToVendorPrice = 0;
        itemID = tonumber(itemID) or 0;
        if (FRC_VendorPrices[itemID]) then
                buyFromVendorPrice = FRC_VendorPrices[itemID].b;
                sellToVendorPrice = FRC_VendorPrices[itemID].s;
        end
                
        local buyout, times, storeStack;
        if (itemName ~= nil and itemName ~= "" and AMDB[itemName]) then
                buyout = tonumber(AM_GetMedian(itemName, "abuyout"));
                if (buyout == nil) then
                        buyout = tonumber(AuctionMatrix_GetData(itemName, "abuyout"));
                end
                times = tonumber(AuctionMatrix_GetData(itemName, "times"));
                storeStack = tonumber(AuctionMatrix_GetData(itemName, "stack"));
                if (sellToVendorPrice == 0) then
                        sellToVendorPrice = tonumber(AuctionMatrix_GetData(itemName, "vendor"));
                end
        end

        --DevTools_Dump({itemLink=itemLink, buyout=buyout, times=times, buyFromVendorPrice=buyFromVendorPrice, sellToVendorPrice=sellToVendorPrice});
                
        if (buyFromVendorPrice ~= nil and buyFromVendorPrice > 0) then
                return buyFromVendorPrice, -1; -- FRC_VendorPrices lists only the primarily-vendor-bought tradeskill items
        elseif (buyout ~= nil and times ~= nil and buyout > 0) then
                local buyoutForOne = buyout;
                if (storeStack ~= nil and storeStack > 0) then
                        buyoutForOne = math.floor(buyout/storeStack);
                end
                return buyoutForOne, math.floor((math.min(times, MIN_SCANS) / MIN_SCANS) * 1000) / 10;
        elseif (sellToVendorPrice ~= nil and sellToVendorPrice > 0) then
                return sellToVendorPrice * 3, 0; -- generally a good guess for auction price if we don't have real auction data
        end
        
        GFWUtils.DebugLog(itemLink.." not found in AuctionMatrix or vendor-reagent prices list");
        return nil, 0;
end

function FRC_WOWEcon_PriceModItemPrice(itemLink)
    local medianPrice, medianCount, serverData = WOWEcon_GetAuctionPrice_ByLink(itemLink);
    if (medianCount == nil) then
                medianCount = 0;
        end
        
        local _, _, itemID  = string.find(itemLink, ".Hitem:(%d+):%d+:%d+:%d+.h%[[^]]+%].h");
        itemID = tonumber(itemID) or 0;
                        
        local buyFromVendorPrice = 0;
        local sellToVendorPrice = 0;
        if (FRC_VendorPrices[itemID]) then
                buyFromVendorPrice = FRC_VendorPrices[itemID].b;
                sellToVendorPrice = FRC_VendorPrices[itemID].s;
        end
                        
        if (sellToVendorPrice == 0) then
                sellToVendorPrice = WOWEcon_GetVendorPrice_ByLink(itemLink);
        end
                        
        if (sellToVendorPrice == nil) then sellToVendorPrice = 0 end
                        
        if (buyFromVendorPrice > 0) then
                return buyFromVendorPrice, -1; -- FRC_VendorPrices lists only the primarily-vendor-bought tradeskill items
        elseif (medianCount == 0 or medianPrice == nil) then
                return sellToVendorPrice * 3, 0; -- generally a good guess for auction price if we don't have real auction data
        else
                return medianPrice, math.floor((math.min(medianCount, MIN_SCANS) / MIN_SCANS) * 1000) / 10;
        end
end

function FRC_NormalizeLink(link)
        -- we don't care about variations in random-property items, enchants, or unique IDs...
        -- discarding them lets us use the link as both a printable link and a reliable index key.
        return string.gsub(link, "|c(%x+)|Hitem:(%d+):%d+:%d+:%d+|h%[(.-)%]|h|r", "|c%1|Hitem:%2:0:0:0|h[%3]|h|r");
end