--[[
	Postman: Building a Better Mailbox
        Bima, aka Tale, aka David C Lawrence <reg+wow-addons@dd.org>
]]--

Postman = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0", 
                                         "AceDB-2.0", "AceHook-2.1",
                                         "AceDebug-2.0")
local L = AceLibrary("AceLocale-2.2"):new("Postman")
local abacus = AceLibrary("Abacus-2.0")

Postman.version = "2.0." .. string.sub("$Revision: 53232 $", 12, -3)
Postman.date = string.sub("$Date: 2007-10-25 16:30:22 -0400 (Thu, 25 Oct 2007) $", 8, 17)

local options = {
    handler = Postman,
    type = "group",
    args = {
        track = {
           order = 1.1,
            name = L["Track"],
            type = "execute",
            desc = L["Show time until mailed items are delivered."],
            func = "ReportTransit"
        },
        autosend = {
            order = 1.2,
            type = "toggle",
            name = L["Autosend"],
            desc = L["Instantly send item when attached with Alt-LeftClick."],
            get = function() return Postman.db.profile.autosend end,
            set = function(v) Postman.db.profile.autosend = v end
        }
    }
}
-- PlayerMenu uses /pm as well.
Postman:RegisterChatCommand({'/postman', '/pman', '/pm'}, options)

-- Convenience variables.
local recips
local me = UnitName("player")
local server = GetRealmName():trim()

-- Initialize from MailTo, if MailTo_List exists.
local initlist = MailToList and MailTo_List[GetCVar("realmName")] or {}
Postman:RegisterDB("PostmanDB")
Postman:RegisterDefaults("profile", { autosend = nil })
Postman:RegisterDefaults("realm", {
	recipients = initlist,
	lastrecip = nil,
	characters = {}
})
-- Why yes, server IS redundant information, sadly so.  Unfortunately it is
-- difficult to parse out server names from the localized AceDB 
-- PLAYER_OF_REALM key that indexes the PostmanDB.chars array, and after
-- pondering using different db types to store the tree, such as account, 
-- this still felt best.  I'll gladly consider other points of view.
Postman:RegisterDefaults("char", { intransit = {}, server = nil })

function Postman:OnInitialize()
    recips = self.db.realm.recipients

    -- Add self to list if not present.
    if not self:RecipFind(me) then
        table.insert(recips, me)
    end

    self.db.realm.characters[UnitName("player")] = true
	self.db.char.server = server
    --self:SetDebugging(true)
end

function Postman:OnEnable()
    self:Hook("SendMailFrame_SendMail", "SendMail", true)
    self:Hook("SendMailFrame_SendeeAutocomplete", "RecipComplete", true)
    self:Hook("ContainerFrameItemButton_OnModifiedClick", "PlaceItem", true)
    self:Hook("InboxFrame_OnClick", "TakeItem", true)

    self:HookScript(SendMailNameEditBox, "OnEditFocusGained", "FillRecip")
    self:HookScript(TradeFrame, "OnShow", "OnTradeShow")

    --- Delay reporting for 10 seconds to get past spammy load messages.
    self:ScheduleEvent(self.ReportDelivery, 10, self)
    self:ScheduleEvent(self.ReportTransit, 10, self)
    -- self:ScheduleEvent(self.ReportExpiry, 10, self)
end

--[[ Postman:Recip* functions are all for the drop down selection box. ]]--

-- The recipients list is an indexed list to make it easier to know its size, 
-- and sort it but LUA doesn't make it easy to look up an element by value.   
-- Could have made it a dictionary to make some things easier, but then the 
-- other things become mildly harder.  Sort of a toss-up.
function Postman:RecipFind(recip)
    for idx, name in pairs(recips) do    
        if name:lower() == recip:lower() then
            return idx
        end
    end
    return nil
end

function Postman:RecipMenuInit()
    -- UIDROPDOWNMENU_MAXBUTTONS=4 -- for testing.

    -- Sanity check.  The recipients list can only grow up to the maximum 
    -- number of lines less one, reserving one for the Add/Remove lines button.
    -- Since you can't send mail to yourself, though, the layer you are
    -- logged in as is not included in the list.

    while #recips > UIDROPDOWNMENU_MAXBUTTONS do
        Postman:Print(L["%s removed; list too long"]:
                      format(table.remove(recips)))
    end

    -- XXXtale why does this not seem to affect the order of the drop down?
    -- the indices reflect a correct order but they don't come out that way.
    table.sort(recips)

    local boxname = SendMailNameEditBox:GetText()
    -- If the filled in name is known, remember its index in our recip list
    local known = Postman:RecipFind(boxname)

    -- Make the list of recipients, skipping self.
    local info
    for idx, name in ipairs(recips) do
        if name ~= me then
            info = { text = name, value = idx, func = Postman.RecipSelect }
            info.checked = idx == known and 1 or nil
            UIDropDownMenu_AddButton(info)
        end
    end

    -- Now give options to add or remove names from the list.  
    if boxname ~= "" then
        info = { notCheckable = 1 }
        if known then
            info.text = L["[Remove %s]"]:format(boxname)
            info.value = known
            info.func = Postman.RecipRemove
        elseif #recips < UIDROPDOWNMENU_MAXBUTTONS then
            info.text = L["[Add %s]"]:format(boxname)
            info.value = boxname
            info.func = Postman.RecipAdd
        else
            -- Suggestions for a better message are welcome.
            info.text = L["Recipient List Full"]
            info.textR = 255
            info.textG = 0
            info.textB = 0
        end
        UIDropDownMenu_AddButton(info) 
    end
end

function Postman:RecipMenuShow()
    this.tooltip = L["Select Recipient"]
    UIDropDownMenu_Initialize(this:GetParent(), self.RecipMenuInit)
    UIDropDownMenu_SetAnchor(0, 0, this:GetParent(), "TOPRIGHT", 
                             this:GetName(), "BOTTOMRIGHT")
end

function Postman:RecipSelect()
    local recip = recips[this.value]
    SendMailNameEditBox:SetText(recip)
    SendMailSubjectEditBox:SetFocus()
end

function Postman:RecipAdd()
    table.insert(recips, this.value)
    table.sort(recips)
end

function Postman:RecipRemove()
    table.remove(recips, this.value)
end

-- Autocompletion of recipient names.
-- Probably should be cached somehow to allow fast lookup.
function Postman:RecipComplete()
    -- First look in our memorized recipients list, then amongst friends,
    -- then amongst guild mates.
    local input = this:GetText()

    for _, name in pairs(recips) do
        if self:NameMatch(input, name) then
            return
        end
    end 

    -- Use Blizzard's autocomplete for friends and guildmates.
    self.hooks["SendMailFrame_SendeeAutocomplete"]()
end

function Postman:NameMatch(input, name)
    if name and name:lower():find(input:lower(), 1, true) == 1 then
        this:SetText(name)
        this:HighlightText(strlen(input), -1)
        return true
    end
    return false
end

-- Fills in empty recipient field with the last recipient, called
-- as SendMailNameEditBox OnEditFocusGained handler.
function Postman:FillRecip(editbox)
    if editbox:GetText() == "" and self.db.realm.lastrecip and 
       self.db.realm.lastrecip ~= me then

       editbox:SetText(self.db.realm.lastrecip)
        -- Most of the time folks are just mailing items around, with
        -- Subject autofilled and no body, so might as well just clear
        -- the focus back out to the outer UI.
        editbox:ClearFocus()
    else
        -- Highlight whatever (if anything) is there for possible replacement.
        editbox:HighlightText(0,-1)
    end

    -- Call original script.
    self.hooks[editbox].OnEditFocusGained()
end

function Postman:SendMail()
	-- Check if we are trying to send to ourself, and if so, just pass the function to the original
	-- and return, so as to avoid tracking something that wasn't sent.
	if (SendMailNameEditBox:GetText() == UnitName("player")) then
	    self.hooks["SendMailFrame_SendMail"]()
		return
	end

	-- Remember to whom we're sending.
    self.db.realm.lastrecip = SendMailNameEditBox:GetText()
    -- Clear focus so it is regained when frame is reset.
    SendMailNameEditBox:ClearFocus()

    -- Item mail takes a mininum of one hour, but occasionally just a 
    -- little longer.  Wait just a little longer to help to head off
    -- complaints of "Postman said it was delivered, but it wasn't!"
	local wait = 60 * 62
	
	-- This only triggers if there is an item attached
    -- Second value is item texture, not needed.
    local name, _, count = GetSendMailItem()
	-- Check if we are sending money, and if so, track it as well
	local amount = MoneyInputFrame_GetCopper(SendMailMoney)
	-- Stores the name and value of the attachments, as well as the itemlink
	local item, money, link, attach
    if name then
		_, link = GetItemInfo(name)
		if count > 1 then
			item = count .. "x" .. link .. " "
			attach = item
        else
			item = "1x" .. link .. " "
			attach = item
		end
		
		if amount > 0 then
			money = formatmoney(amount)
			if SendMailCODButton:GetChecked() then
				attach = attach .. " for " .. money .. " (COD)"
			else
				attach = attach .. " and " .. money
			end
		end
		-- Check if we are sending to another character on the same account and realm
		-- This only works if you have logged into that character with this addon enabled,
		-- so that that character's name is in the db
		-- If we are not sending to another character on the same account and realm,
		-- then dsve transit info to report delivery.. otherwise do nothing so that
		-- neither transit nor delivery gets reported
		if not (self.db.realm.characters[self.db.realm.lastrecip]) then
			table.insert(self.db.char.intransit, {
				to = self.db.realm.lastrecip,
				n = attach,
				t = time() + wait 
			})
		end
	elseif amount > 0 then
		money = formatmoney(amount)
		attach = money
		if not (self.db.realm.characters[self.db.realm.lastrecip]) then
			table.insert(self.db.char.intransit, {
				to = self.db.realm.lastrecip,
				n = attach,
				t = time() + wait 
			})
		end
	end
	
	if not self.nextevent then
		self.nextevent = 
			self:ScheduleEvent(self.ReportDelivery, wait, self)
	end
	
    -- Call original script.
    self.hooks["SendMailFrame_SendMail"]()
end

function Postman:PlaceItem(button)
    -- Don't even bother doing anything if this isn't an Alt-Left click,
    -- or if already holding an item.
    if not (button == "LeftButton" and IsAltKeyDown() and
            not CursorHasItem()) then
        self.hooks["ContainerFrameItemButton_OnModifiedClick"](button)
        return
    end

    local bag = this:GetParent():GetID();
    local slot = this:GetID();

    -- First, see if we're sending mail.
    if SendMailFrame:IsVisible() then
        PickupContainerItem(bag, slot)
        ClickSendMailItemButton()
        if self.db.profile.autosend then
            SendMailMailButton:Click()
        end
        return

    -- XXXtale this is where mass mail adding will go.

    -- Listing an auction, just for UI consistency.
    elseif AuctionFrameAuctions and AuctionFrameAuctions:IsVisible() then
        PickupContainerItem(bag, slot)
        ClickAuctionSellItemButton()
        return

    -- Add to existing trade.
    elseif TradeFrame:IsVisible() then
        for i = 1, 6 do
            if not GetTradePlayerItemLink(i) then
                PickupContainerItem(bag, slot)
                ClickTradeButton(i)
                return
            end
        end

    -- Initiate trade.
    elseif UnitExists("target") and UnitIsFriend("player", "target") and
           UnitIsPlayer("target") and CheckInteractDistance("target", 2) then
        InitiateTrade("target")
        self.trade = { UnitName("target"), bag, slot }
        -- Postman.trade only has a validity period of 2 seconds; if it
        -- doesn't get reset to nil by use within that time, force it to nil.
        self:ScheduleEvent(function() self.trade = nil end, 2)
        return
    end
end

function Postman:OnTradeShow(object)
    self.hooks[object].OnShow()

    if self.trade and self.trade[1] == UnitName("NPC") and
       not CursorHasItem() then
        PickupContainerItem(self.trade[2], self.trade[3])
        ClickTradeButton(1)
    end
    self.trade = nil
end

function Postman:TakeItem(idx)
    self:Debug("TakeItem: enter")

    -- Courtesy of WoWWiki:
    -- packageIcon, stationeryIcon, sender, subject, money, CODAmount, 
    --    daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply 
    -- = GetInboxHeaderInfo(index);
    local _, _, from, _, money, cod, expires, hasitem, wasread, _, _, _ =
        GetInboxHeaderInfo(idx)

    if arg1 ~= "RightButton" or 
       IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then
        self:Debug("TakeItem: calling original OnClick and returning")
        self.hooks["InboxFrame_OnClick"](idx)
        return
    end

    -- For inexplicable reasons, GetInboxText() nils this:GetChecked()
    -- the very first time a message is read, which screws with being able
    -- to use the original InboxFrame_OnClick handler.
    local checked = this:GetChecked()

    -- XXXtale todo: report auction summary
    local body, _, _, invoice = GetInboxText(idx)

    this:SetChecked(checked)
    self:Debug(format("hasitem is %s, money=%d, invoice is %s, body is '%s'",
                      hasitem and "true" or "false", money, 
                      invoice and "true" or "false", body or "nil"))

    -- If there is a message with no items or money, show it. 
    -- By design, these messages will never be autodeleted.
    --
    -- For reasons that escape me, just relying on body not being nil fails.
    -- When a message is first read using GetInboxText(), body is nil no matter
    -- whether there really is a body or not.  Blizzard uses a some sort of
    -- magical nil value that gets filled in later.  I tested this by ripping
    -- OpenMail_Update out of MailFrame.lua and discovering that bodyText is
    -- nil even when OpenMailBodyText:SetText(bodyText) is called there.
    -- After the very first time a message is loaded in the game, things work
    -- fine for all subsequent times, even after relogging.  So weird.
    -- I'm not sure what happens after server restarts.
    -- 
    -- Using "not wasread" means a message with only a subject and no 
    -- body/items/money that hadn't yet been read won't be autodeleted, 
    -- but that seems to be reasonable behaviour.
    if not hasitem and money == 0 and not wasread or (body and body ~= "") then
        self:Debug("TakeItem: showing message via old OnClick and returning")
        self.hooks["InboxFrame_OnClick"](idx)
        return
    end

    if cod and cod > 0 then
        if cod > GetMoney() then
            StaticPopup_Show("COD_ALERT")
        else
            -- In theory I could just keep the message closed, but it
            -- seems more likely to causes issues with Blizzard's code.
            -- Also use only this part of Blizzard's InboxFrame_OnClick
            -- rather than calling it in case the window is already open.
            InboxFrame.openMailID = idx
            OpenMail_Update()
            ShowUIPanel(OpenMailFrame)
            PlaySound("igSpellBookOpen")
            StaticPopup_Show("COD_CONFIRMATION")
        end
        return
    end                

    local took = ""
    -- Taking money apparently *needs* to come before taking items
    -- because taking the item first seems to require some sort of server
    -- sync that doesn't allow the money taking to go through.
    if money > 0 then
        took = abacus:FormatMoneyFull(money, true)
        TakeInboxMoney(idx)
    end

    -- Can't take money and item without some delay, so this block
    -- only executes if there is no money.
    if hasitem and money == 0 then
        -- name, texture, count, quality, canuse
        local name, _, count, quality = GetInboxItem(idx)
        TakeInboxItem(idx)
        -- r, g, b, hex
        _, _, _, hex = GetItemQualityColor(quality)
        if count > 1 then
          took = count .. " "

        end
        took = took .. hex .. name .. "|r"
    end

    if not body or body == "" then
        self:Debug("TakeItem: want to delete message")
        -- This doesn't seem to work after taking money, for reasons I
        -- can only vaguely guess are related to syncing with the server.
        -- Before deleting, attempt to ensure that money and an item are
        -- no longer attached.
        _, _, _, _, money, _, _, hasitem = GetInboxHeaderInfo(idx)
        if not hasitem and money == 0 then
            self:Debug("TakeItem: no item or money, deleting message")
            DeleteInboxItem(idx)
        end
    end

    if took ~= "" then
        self:Print(L["Received %s from %s"]:format(took, from or UNKNOWN))
    end
end

-- "pofr" = formated PLAYER_OF_REALM string from AceDB
local function fromstring(pofr, onserver)
    local s = pofr:gsub(" .*", "")
    if onserver ~= server then
        s = s .. "(" .. onserver .. ")"
    end
    return s
end

--T ake money amount in copper and convert to gold silver copper
function formatmoney(money)
	local copper, silver, gold, formatted
	if strlen(money) <= 2 then
		copper = money
		formatted = copper .. "c"
	elseif strlen(money) <= 4 then
		copper = strsub(money,-2)
		silver = strsub(money,-4,-3)
		if copper == 0 then
			formatted = silver .. "s"
		else
			formatted = silver .. "s " .. copper .. "c"
		end
	else
		copper = strsub(money,-2)
		silver = strsub(money,-4,-3)
		gold = strsub(money,1,-5)
		if silver == 0 then
			if copper == 0 then
				formatted = gold .. "g"
			else
				formatted = gold .. "g " .. copper .. "c"
			end
		else
			if copper == 0 then
				formatted = gold .. "g " .. silver .. "s"
			else
				formatted = gold .. "g " .. silver .. "s " .. copper .. "c"
			end
		end
	end
	
	return formatted
end

function Postman:ReportDelivery()
    local now = time()
    local next
    local rangbell
    local reported = {}

    for pofr, char in pairs(PostmanDB.chars) do
        local from = fromstring(pofr, char.server)
		
        for idx, msg in ipairs(char.intransit or {}) do
            if msg.t > now then
				if not next or msg.t < next then
					next = msg.t
				end
			else
                self:Print(L["%s, %s to %s, delivered."]:
                           format(msg.n or "?", from, msg.to))
                if not rangbell then
                    PlaySound("AuctionWindowOpen")
                    rangbell = true
                end
                table.insert(reported, idx)
			end
        end
		
        -- Remove the processed messages.  
		for k, v in pairs(reported) do
			table.remove(char.intransit or {}, v - (k - 1))
		end
		
		if next then
	        self.nextevent = 
				self:ScheduleEvent(self.ReportDelivery, next - now, self)
		end
	end
end

function Postman:ReportTransit()
    local now = time()
    
    for pofr, char in pairs(PostmanDB.chars) do
        local from = fromstring(pofr, char.server)
        for idx, msg in ipairs(char.intransit or {}) do
            if (msg.t - now) / 60 + 1 >= 0 then
				self:Print(L["%s, %s to %s, due in %dm."]:
						   format(msg.n or "?", from, msg.to, 
								  (msg.t - now) / 60 + 1))
			end
        end
    end
end
