-- layers for the various elements on the market product tiles
-- controls are layered to ensure correct display order
-- Background layer
ZO_MARKET_TILE_BACKGROUND_LEVEL =   1
ZO_MARKET_TILE_OVERLAY_LEVEL =      9

-- Text/Content layer
ZO_MARKET_TILE_BORDER_LEVEL =       10
ZO_MARKET_TILE_TEXT_LEVEL =         11
ZO_MARKET_TILE_ABOVE_TEXT_LEVEL =   12

-- Top layer
ZO_MARKET_TILE_HIGHLIGHT_LEVEL =    20
-- icons should be on top of the highlight
ZO_MARKET_TILE_ICON_FRAME_LEVEL =   21
ZO_MARKET_TILE_ICON_IMAGE_LEVEL =   22

--account for the fade that we add to the sides of the callout
ZO_MARKET_PRODUCT_CALLOUT_X_OFFSET = 5

ZO_MARKET_PRODUCT_HIGHLIGHT_ANIMATION_DURATION_MS = 255

do
    local MARKET_PRODUCT_COLOR_MAP_UNFOCUSED =
    {
        [MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED] = ZO_MARKET_DIMMED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_PURCHASED] = ZO_MARKET_PRODUCT_PURCHASED_DIMMED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_COMPLETE] = ZO_MARKET_PRODUCT_PURCHASED_DIMMED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_INELIGIBLE] = ZO_MARKET_PRODUCT_INELIGIBLE_DIMMED_COLOR,
    }

    local MARKET_PRODUCT_COLOR_MAP_FOCUSED =
    {
        [MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED] = ZO_MARKET_SELECTED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_PURCHASED] = ZO_MARKET_PRODUCT_PURCHASED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_COMPLETE] = ZO_MARKET_PRODUCT_PURCHASED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_INELIGIBLE] = ZO_MARKET_PRODUCT_INELIGIBLE_COLOR,
    }

    function ZO_MarketClasses_Shared_ApplyTextColorToLabelByState(label, isFocused, purchaseState)
        local colorLookup = isFocused and MARKET_PRODUCT_COLOR_MAP_FOCUSED or MARKET_PRODUCT_COLOR_MAP_UNFOCUSED
        local color = colorLookup[purchaseState]
        label:SetColor(color:UnpackRGB())
    end
end

do
    local UNFOCUSED_COLOR_MAP =
    {
        [MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED] = ZO_MARKET_PRODUCT_ESO_PLUS_DIMMED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_PURCHASED] = ZO_MARKET_PRODUCT_ESO_PLUS_PURCHASED_DIMMED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_COMPLETE] = ZO_MARKET_PRODUCT_ESO_PLUS_PURCHASED_DIMMED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_INELIGIBLE] = ZO_MARKET_PRODUCT_ESO_PLUS_PURCHASED_DIMMED_COLOR,
    }

    local FOCUSED_COLOR_MAP =
    {
        [MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED] = ZO_MARKET_PRODUCT_ESO_PLUS_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_PURCHASED] = ZO_MARKET_PRODUCT_ESO_PLUS_PURCHASED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_COMPLETE] = ZO_MARKET_PRODUCT_ESO_PLUS_PURCHASED_COLOR,
        [MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_INELIGIBLE] = ZO_MARKET_PRODUCT_ESO_PLUS_PURCHASED_COLOR,
    }

    function ZO_MarketClasses_Shared_ApplyEsoPlusColorToLabelByState(label, isFocused, purchaseState)
        local colorLookup = isFocused and FOCUSED_COLOR_MAP or UNFOCUSED_COLOR_MAP
        local color = colorLookup[purchaseState]
        label:SetColor(color:UnpackRGB())
    end
end

--
--[[ MarketProductBase ]]--
--

ZO_MarketProductBase = ZO_Object:Subclass()

function ZO_MarketProductBase:New(...)
    local marketProduct = ZO_Object.New(self)
    marketProduct:Initialize(...)
    return marketProduct
end

function ZO_MarketProductBase:Initialize(control)
    self.textCalloutYOffset = 0
    self.control = control
end

function ZO_MarketProductBase:SetTextCalloutYOffset(yOffset)
    self.textCalloutYOffset = yOffset
end

function ZO_MarketProductBase:SetControl(control)
    self.control = control
end

function ZO_MarketProductBase:GetControl()
    return self.control
end

function ZO_MarketProductBase:SetMarketProductData(marketProductData)
    self.productData = marketProductData
end

function ZO_MarketProductBase:GetMarketProductData()
    return self.productData
end

function ZO_MarketProductBase:SetParent(parentControl)
    self.control:SetParent(parentControl)
end

function ZO_MarketProductBase:GetId()
    return self.productData:GetId()
end

function ZO_MarketProductBase:GetPresentationIndex()
    return self.productData:GetPresentationIndex()
end

function ZO_MarketProductBase:HasValidPresentationIndex()
    return self.productData:HasValidPresentationIndex()
end

function ZO_MarketProductBase:GetMarketProductDisplayName()
    return self.productData:GetDisplayName()
end

function ZO_MarketProductBase:GetMarketProductPricingByPresentation()
    return self.productData:GetMarketProductPricingByPresentation()
end

function ZO_MarketProductBase:GetMarketProductType()
    return self.productData:GetMarketProductType()
end

function ZO_MarketProductBase:GetNumChildren()
    return self.productData:GetNumChildren()
end

function ZO_MarketProductBase:GetChildMarketProductId(childIndex)
    return self.productData:GetChildMarketProductId(childIndex)
end

function ZO_MarketProductBase:GetStackCount()
    return self.productData:GetStackCount()
end

function ZO_MarketProductBase:GetHidesChildProducts()
    return self.productData:GetHidesChildProducts()
end

function ZO_MarketProductBase:IsGiftable()
    return self.productData:IsGiftable()
end

function ZO_MarketProductBase:GetPurchaseState()
    -- may be overridden by child classes that require additional logic
    return self.productData:GetPurchaseState()
end

function ZO_MarketProductBase:IsPurchaseLocked()
    return self.purchaseState ~= MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED
end

function ZO_MarketProductBase:CanBePurchased()
    return not (self:IsPurchaseLocked() or self:IsHouseCollectible() or self:IsPromo())
end

function ZO_MarketProductBase:IsBundle()
    return self.productData:IsBundle()
end

function ZO_MarketProductBase:IsPromo()
    return self.productData:IsPromo()
end

function ZO_MarketProductBase:IsHouseCollectible()
    return self.productData:IsHouseCollectible()
end

function ZO_MarketProductBase:HasBeenPartiallyPurchased()
    return self.productData:HasBeenPartiallyPurchased()
end

function ZO_MarketProductBase:HasSubscriptionUnlockedAttachments()
    return self.productData:HasSubscriptionUnlockedAttachments()
end

function ZO_MarketProductBase:AreAllCollectiblesUnlocked()
    return self.productData:AreAllCollectiblesUnlocked()
end

function ZO_MarketProductBase:IsLimitedTimeProduct()
    return self.productData:IsLimitedTimeProduct()
end

function ZO_MarketProductBase:GetBackgroundDesaturation(isPurchased)
    return isPurchased and ZO_MARKET_PRODUCT_PURCHASED_DESATURATION or ZO_MARKET_PRODUCT_NOT_PURCHASED_DESATURATION
end

function ZO_MarketProductBase:LayoutCostAndText()
    -- setup the callouts for new, on sale, and LTO
    self:SetupCalloutsDisplay()

    -- layout the price labels
    self:SetupPricingDisplay()

    local control = self.control
    control.cost:ClearAnchors()
    control.textCallout:ClearAnchors()
    control.esoPlusCost:ClearAnchors()

    local hasNormalCost = self:HasCost()
    if hasNormalCost then
        if self:IsOnSale() and not self:IsFree() then
            control.cost:SetAnchor(BOTTOMLEFT, control.previousCost, BOTTOMRIGHT, 10)
            control.textCallout:SetAnchor(BOTTOMLEFT, control.previousCost, TOPLEFT, ZO_MARKET_PRODUCT_CALLOUT_X_OFFSET - 2, self.textCalloutYOffset) -- x offset to account for strikethrough
        else
            control.cost:SetAnchor(BOTTOMLEFT, control, BOTTOMLEFT, 10, -10)
            control.textCallout:SetAnchor(BOTTOMLEFT, control.cost, TOPLEFT, ZO_MARKET_PRODUCT_CALLOUT_X_OFFSET, self.textCalloutYOffset)
        end
    else
        -- if we don't have a normal cost, then we need to anchor the callout to the ESO Plus cost
        -- there should never be a case where a market product has neither a normal cost nor an ESO Plus cost (because then it would be unpurchasable)
        control.textCallout:SetAnchor(BOTTOMLEFT, control.esoPlusCost, TOPLEFT, ZO_MARKET_PRODUCT_CALLOUT_X_OFFSET, self.textCalloutYOffset)
    end

    local hasEsoPlusCost = self:HasEsoPlusCost()
    if hasEsoPlusCost then
        if hasNormalCost then
            control.esoPlusCost:SetAnchor(BOTTOMLEFT, control.cost, BOTTOMRIGHT, 10)
        else
            control.esoPlusCost:SetAnchor(BOTTOMLEFT, control, BOTTOMLEFT, 10, -10)
        end
    end

    self:SetupBundleDisplay()

    self:SetupPurchaseLabelDisplay()
    self:SetupEsoPlusDealLabelDisplay()

    ZO_MarketClasses_Shared_ApplyTextColorToLabelByState(control.title, self:IsFocused(), self.purchaseState)
end

function ZO_MarketProductBase:ShouldShowCallouts()
    local purchaseable = self.purchaseState == MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED
    local purchasedAndNew = self.productData:IsNew() and not (purchaseable or self:IsLimitedTimeProduct() or self:IsOnSale())
    return self:IsPromo() or not purchasedAndNew
end

function ZO_MarketProductBase:GetMarketProductListingsForHouseTemplate(houseTemplateId, displayGroup)
    return { GetActiveMarketProductListingsForHouseTemplate(houseTemplateId, displayGroup) }
end

function ZO_MarketProductBase:GetDefaultHouseTemplatePricingInfo()
    return ZO_MarketProduct_GetDefaultHousingTemplatePricingInfo(self:GetId(), function(...) return self:GetMarketProductListingsForHouseTemplate(...) end)
end

function ZO_MarketProductBase:SetupCalloutsDisplay()
    local hideCallouts = true

    -- setup the callouts for new, on sale, and LTO
    if self:ShouldShowCallouts() then
        local calloutUpdateHandler = nil
        if self:IsLimitedTimeProduct() then
            hideCallouts = false
            self:UpdateLTORemainingTimeCalloutText()
            calloutUpdateHandler = function() self:UpdateLTORemainingTimeCalloutText() end
        elseif self:IsOnSale() then
            hideCallouts = false
            local onSaleMarketProductId = self.defaultHouseTemplateMarketProductId or self:GetId()
            self:UpdateSaleRemainingTimeCalloutText(self.discountPercent, onSaleMarketProductId)
            if GetMarketProductSaleTimeLeftInSeconds(onSaleMarketProductId) > 0 then
                calloutUpdateHandler = function() self:UpdateSaleRemainingTimeCalloutText(self.discountPercent, onSaleMarketProductId) end
            end
        elseif self.productData:IsNew() then
            hideCallouts = false
            self.control.textCallout:SetText(GetString(SI_MARKET_TILE_CALLOUT_NEW))
            self.control.textCallout:SetModifyTextType(MODIFY_TEXT_TYPE_UPPERCASE)
        end

        self.control.textCallout:SetHandler("OnUpdate", calloutUpdateHandler)
    end

    self.control.textCallout:SetHidden(hideCallouts)
end

do
    local TEXT_CALLOUT_BACKGROUND_ALPHA = 0.9
    function ZO_MarketProductBase:ApplyCalloutColor(textCalloutBackgroundColor, textCalloutTextColor)
        if textCalloutBackgroundColor then
            local r, g, b = textCalloutBackgroundColor:UnpackRGB()
            self.control.leftCalloutBackground:SetColor(r, g, b, TEXT_CALLOUT_BACKGROUND_ALPHA)
            self.control.rightCalloutBackground:SetColor(r, g, b, TEXT_CALLOUT_BACKGROUND_ALPHA)
            self.control.centerCalloutBackground:SetColor(r, g, b, TEXT_CALLOUT_BACKGROUND_ALPHA)
            self.control.textCallout:SetColor(textCalloutTextColor:UnpackRGB())
        end
    end
end

function ZO_MarketProductBase:SetupPricingDisplay()
    local control = self.control
    if self:HasCost() then
        -- layout the previous cost
        if self:IsOnSale() and not self:IsFree() then
            local formattedAmount = zo_strformat(SI_NUMBER_FORMAT, ZO_CommaDelimitNumber(self.cost))
            local strikethroughAmountString = zo_strikethroughTextFormat(formattedAmount)
            control.previousCost:SetText(strikethroughAmountString)
            control.previousCost:SetHidden(false)
        else
            control.previousCost:SetHidden(true)
        end

        -- layout the current cost
        if not self:IsFree() then
            local priceFormat = "%s %s"
            if self.hasMultiplePriceOptions then
                priceFormat = "%s+ %s"
            end

            local INHERIT_ICON_COLOR = true
            local CURRENCY_ICON_SIZE = "100%"
            local currencyIcon = ZO_Currency_GetPlatformFormattedCurrencyIcon(ZO_Currency_MarketCurrencyToUICurrency(self.currencyType), CURRENCY_ICON_SIZE, INHERIT_ICON_COLOR)
            local currencyString = string.format(priceFormat, zo_strformat(SI_NUMBER_FORMAT, ZO_CommaDelimitNumber(self.costAfterDiscount)), currencyIcon)
            control.cost:SetText(currencyString)
        else
            control.cost:SetText(GetString(SI_MARKET_FREE_LABEL))
        end

        ZO_MarketClasses_Shared_ApplyTextColorToLabelByState(control.cost, self:IsFocused(), self.purchaseState)
        control.cost:SetHidden(self:IsPromo())
    else
        control.cost:SetHidden(true)
        control.previousCost:SetHidden(true)
    end

    if self:HasEsoPlusCost() then
        control.esoPlusCost:SetHidden(false)

        if not self:IsFreeForEsoPlus() then
            local INHERIT_ICON_COLOR = true
            local CURRENCY_ICON_SIZE = "100%"
            local currencyIcon = ZO_Currency_GetPlatformFormattedCurrencyIcon(ZO_Currency_MarketCurrencyToUICurrency(self.currencyType), CURRENCY_ICON_SIZE, INHERIT_ICON_COLOR)

            -- the keyboard Crown Gem icon is purple, so we shouldn't try to recolor it with gold colors
            if self.currencyType == MKCT_CROWN_GEMS and not IsInGamepadPreferredMode() then
                local iconColor = self:CanBePurchased() and ZO_MARKET_SELECTED_COLOR or ZO_MARKET_PRODUCT_PURCHASED_COLOR
                currencyIcon = iconColor:Colorize(currencyIcon)
            end

            local currencyString = string.format("%s %s", zo_strformat(SI_NUMBER_FORMAT, ZO_CommaDelimitNumber(self.esoPlusCost)), currencyIcon)
            control.esoPlusCost:SetText(currencyString)
        else
            control.esoPlusCost:SetText(GetString(SI_MARKET_FREE_LABEL))
        end

        ZO_MarketClasses_Shared_ApplyEsoPlusColorToLabelByState(control.esoPlusCost, self:IsFocused(), self.purchaseState)
    else
        control.esoPlusCost:SetHidden(true)
    end
end

function ZO_MarketProductBase:SetupPurchaseLabelDisplay()
    local purchaseLabelControl = self.control.purchaseLabelControl
    local canBePurchased = self:CanBePurchased()
    if not canBePurchased then
        local purchasedString
        if self:IsPromo() then
            purchasedString = ""
        elseif self.purchaseState == MARKET_PRODUCT_PURCHASE_STATE_NOT_PURCHASED and self:IsHouseCollectible() then
            purchasedString = ""
        elseif self.purchaseState == MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_COMPLETE then
            local errorStringId = GetMarketProductCompleteErrorStringId(self:GetId())
            purchasedString = GetErrorString(errorStringId)
        elseif self.purchaseState == MARKET_PRODUCT_PURCHASE_STATE_INSTANT_UNLOCK_INELIGIBLE then
            purchasedString = GetString(SI_MARKET_INSTANT_UNLOCK_INELIGIBLE_LABEL)
        else
            purchasedString = GetString(SI_MARKET_PURCHASED_LABEL)
        end

        purchaseLabelControl:SetText(purchasedString)
        ZO_MarketClasses_Shared_ApplyTextColorToLabelByState(purchaseLabelControl, self:IsFocused(), self.purchaseState)

        self:AnchorPurchaseLabel()
    end

    purchaseLabelControl:SetHidden(canBePurchased)
end

function ZO_MarketProductBase:AnchorPurchaseLabel()
    self:AnchorLabelBetweenBundleIndicatorAndCost(self.control.purchaseLabelControl)
end

function ZO_MarketProductBase:AnchorLabelBetweenBundleIndicatorAndCost(label)
    -- anchor the purchase control to the bottom right of the tile
    -- if this is a bundle, account for the bundle indicators in the bottom right
    label:ClearAnchors()

    local control = self.control
    if self:IsBundle() then
        if self.productData:GetNumBundledProducts() > 1 then
            label:SetAnchor(BOTTOMRIGHT, control.bundledProductsItemsLabel, BOTTOMLEFT, -10, 0)
        else
            label:SetAnchor(BOTTOMRIGHT, control.numBundledProductsLabel, BOTTOMRIGHT, 0, -2)
        end
    else
        label:SetAnchor(BOTTOMRIGHT, control, BOTTOMRIGHT, -10, -10)
    end

    -- anchor the left side to the right-most cost label
    if self:HasEsoPlusCost() then
        label:SetAnchor(BOTTOMLEFT, control.esoPlusCost, BOTTOMRIGHT, 10, 0, ANCHOR_CONSTRAINS_X)
    elseif self:HasCost() then
        label:SetAnchor(BOTTOMLEFT, control.cost, BOTTOMRIGHT, 10, 0, ANCHOR_CONSTRAINS_X)
    else
        -- we shouldn't hit this case
        label:SetAnchor(BOTTOMLEFT, control, BOTTOMLEFT, 10, 0, ANCHOR_CONSTRAINS_X)
    end
end

function ZO_MarketProductBase:SetupEsoPlusDealLabelDisplay()
    local esoPlusDealLabelControl = self.control.esoPlusDealLabelControl
    local shouldShowDealLabel = self:CanBePurchased() and self:HasEsoPlusCost()
    if shouldShowDealLabel then
        local formattedIcon = self:GetEsoPlusIcon()
        local text
        if self:HasCost() then
            text = GetString(SI_MARKET_ESO_PLUS_DEAL_LABEL)
        else
            text = GetString(SI_MARKET_ESO_PLUS_EXCLUSIVE_LABEL)
        end
        local esoPlusDealString = string.format("%s%s", formattedIcon, text)
        esoPlusDealLabelControl:SetText(esoPlusDealString)
        ZO_MarketClasses_Shared_ApplyEsoPlusColorToLabelByState(esoPlusDealLabelControl, self:IsFocused(), self.purchaseState)

        self:AnchorEsoPlusDealLabel()
    end

    esoPlusDealLabelControl:SetHidden(not shouldShowDealLabel)
end

function ZO_MarketProductBase:AnchorEsoPlusDealLabel()
    self:AnchorLabelBetweenBundleIndicatorAndCost(self.control.esoPlusDealLabelControl)
end

function ZO_MarketProductBase:SetupBundleDisplay()
    local isBundle = self:IsBundle()
    if isBundle then
        local numBundledProducts = self.productData:GetNumBundledProducts()
        self.control.numBundledProductsLabel:SetText(numBundledProducts)
        -- hide the label if the result is 0 or 1 (which means either an empty bundle or what should be a single product...)
        local hideBundledProductsLabel = numBundledProducts <= 1
        self.control.numBundledProductsLabel:SetHidden(hideBundledProductsLabel)

        self:AnchorBundledProductsLabel()
    else
        self.control.numBundledProductsLabel:SetHidden(true)
    end

    self.control.bundleIndicator:SetHidden(not isBundle)
end

function ZO_MarketProductBase:AnchorBundledProductsLabel()
    -- optional override
end

function ZO_MarketProductBase:UpdateLTORemainingTimeCalloutText()
    local remainingTime = self.productData:GetLTOTimeLeftInSeconds()
    -- if there is no time left remaining then the MarketProduct is not limited time
    -- because when a limited time product reaches 0 it is removed from the store
    if remainingTime > 0 then
        if remainingTime >= ZO_ONE_DAY_IN_SECONDS then
            self.control.textCallout:SetModifyTextType(MODIFY_TEXT_TYPE_UPPERCASE)
            self.control.textCallout:SetText(zo_strformat(SI_TIME_DURATION_LEFT, ZO_FormatTime(remainingTime, TIME_FORMAT_STYLE_SHOW_LARGEST_UNIT_DESCRIPTIVE, TIME_FORMAT_PRECISION_SECONDS)))
        else
            self.control.textCallout:SetModifyTextType(MODIFY_TEXT_TYPE_NONE)
            self.control.textCallout:SetText(zo_strformat(SI_TIME_DURATION_LEFT, ZO_FormatTimeLargestTwo(remainingTime, TIME_FORMAT_STYLE_DESCRIPTIVE_MINIMAL)))
        end
    end
end

function ZO_MarketProductBase:UpdateSaleRemainingTimeCalloutText(discountPercent, marketProductId)
    local discountPercentText = zo_strformat(SI_MARKET_DISCOUNT_PRICE_PERCENT_FORMAT, discountPercent)
    local remainingTime = GetMarketProductSaleTimeLeftInSeconds(marketProductId)
    -- if there is no time left remaining then the MarketProduct is not limited time
    -- because when a limited time product reaches 0 it is removed from the store
    if remainingTime > 0 and remainingTime <= ZO_ONE_MONTH_IN_SECONDS then
        local remainingTimeText
        if remainingTime >= ZO_ONE_DAY_IN_SECONDS then
            self.control.textCallout:SetModifyTextType(MODIFY_TEXT_TYPE_UPPERCASE)
            remainingTimeText = ZO_FormatTime(remainingTime, TIME_FORMAT_STYLE_SHOW_LARGEST_UNIT_DESCRIPTIVE, TIME_FORMAT_PRECISION_SECONDS)
        else
            self.control.textCallout:SetModifyTextType(MODIFY_TEXT_TYPE_NONE)
            remainingTimeText = ZO_FormatTimeLargestTwo(remainingTime, TIME_FORMAT_STYLE_DESCRIPTIVE_MINIMAL)
        end
        local calloutText = string.format("%s %s", discountPercentText, remainingTimeText)
        self.control.textCallout:SetText(calloutText)
    else
        self.control.textCallout:SetText(discountPercentText)
    end
end

function ZO_MarketProductBase:Show(marketProductData)
    -- this is currently a safeguard for the announcement tiles which set their
    -- product data up ahead of time and don't pass in data on show
    if marketProductData then
        self.productData = marketProductData
    end

    self:UpdatingPricingInformation()

    self.purchaseState = self:GetPurchaseState()

    local name = self.productData:GetDisplayName()
    self:SetTitle(name)

    self:PerformLayout()

    self:LayoutCostAndText()

    self:LayoutBackground()

    self.control:SetHidden(false)
end

function ZO_MarketProductBase:LayoutBackground()
    local background = self:GetBackground()
    self.control.background:SetTexture(background)
    self.hasBackground = background ~= ZO_NO_TEXTURE_FILE
    self.control.background:SetHidden(not self.hasBackground)
end

function ZO_MarketProductBase:UpdatingPricingInformation()
    local currencyType, cost, costAfterDiscount, discountPercent, esoPlusCost = self:GetMarketProductPricingByPresentation()
    if self:IsHouseCollectible() then
        local houseCurrencyType, houseCost, houseCostAfterDiscount, houseDiscountPercent, houseEsoPlusCost, defaultHouseTemplateMarketProductId, hasMultiplePriceOptions = self:GetDefaultHouseTemplatePricingInfo()
        if houseCurrencyType ~= MKCT_NONE then
            currencyType = houseCurrencyType
            cost = houseCost
            costAfterDiscount = houseCostAfterDiscount
            discountPercent = houseDiscountPercent
            esoPlusCost = houseEsoPlusCost
            self.hasMultiplePriceOptions = hasMultiplePriceOptions
            self.defaultHouseTemplateMarketProductId = defaultHouseTemplateMarketProductId
        else
            self.hasMultiplePriceOptions = false
            self.defaultHouseTemplateMarketProductId = nil
        end
    else
        self.hasMultiplePriceOptions = false
        self.defaultHouseTemplateMarketProductId = nil
    end

    self.currencyType = currencyType
    self.cost = cost
    self.costAfterDiscount = costAfterDiscount
    self.discountPercent = discountPercent
    self.esoPlusCost = esoPlusCost
end

function ZO_MarketProductBase:HasCost()
    return self.cost ~= nil
end

function ZO_MarketProductBase:HasEsoPlusCost()
    return self.esoPlusCost ~= nil
end

function ZO_MarketProductBase:IsFree()
    return self:HasCost() and self.costAfterDiscount == 0 or false
end

function ZO_MarketProductBase:IsOnSale()
    return self.discountPercent > 0 and not self:IsPromo()
end

function ZO_MarketProductBase:IsFreeForEsoPlus()
    return self:HasEsoPlusCost() and self.esoPlusCost == 0 or false
end

function ZO_MarketProductBase:ClearPricingInformation()
    self.currencyType = MKCT_NONE
    self.cost = nil
    self.costAfterDiscount = nil
    self.discountPercent = 0
    self.esoPlusCost = nil
    self.hasMultiplePriceOptions = false
    self.defaultHouseTemplateMarketProductId = nil
end

function ZO_MarketProductBase:Reset()
    self.control:SetHidden(true)
    self.control.textCallout:SetHidden(true)
    -- Clear the background's texture so that it can be cleared at zero references
    self.control.background:SetTexture("")
    self.productData = nil
    self:ClearPricingInformation()
end

function ZO_MarketProductBase:Refresh()
    self:Show(self.productData)
end

function ZO_MarketProductBase:SetTitle(title)
    self.control.title:SetText(zo_strformat(SI_MARKET_PRODUCT_NAME_FORMATTER, title))
end

-- MarketProduct Preview functions

function ZO_MarketProductBase:GetMarketProductPreviewType()
    return self.productData:GetMarketProductPreviewType()
end

function ZO_MarketProductBase:HasPreview()
    --Houses are special because as far as C is concerned they aren't previewable, but Lua allows for a roundabout preview with jumping to the house
    return CanPreviewMarketProduct(self:GetId()) or self:GetMarketProductPreviewType() == ZO_MARKET_PREVIEW_TYPE_HOUSE
end

function ZO_MarketProductBase:GetNumPreviewVariations()
    return self.productData:GetNumPreviewVariations()
end

-- functions to be overridden

function ZO_MarketProductBase:IsFocused()
    -- optional overridde, default behavior
    return true
end

function ZO_MarketProductBase:PerformLayout()
    -- to be overridden
end

function ZO_MarketProductBase:GetBackground()
    -- to be overridden, default behavior
    return nil
end

function ZO_MarketProductBase:GetEsoPlusIcon()
    -- to be overridden, default behavior
    return nil
end

function ZO_MarketProduct_IsHouseCollectible(marketProductId)
    if GetMarketProductType(marketProductId) == MARKET_PRODUCT_TYPE_COLLECTIBLE then
        local collectibleType = select(4, GetMarketProductCollectibleInfo(marketProductId))
        if collectibleType == COLLECTIBLE_CATEGORY_TYPE_HOUSE then
            return true
        end
    end

    return false
end

-- global functions

function ZO_MarketProduct_GetDefaultHousingTemplatePricingInfo(marketProductId, getMarketProductListingsCallback)
    local houseCurrencyType = MKCT_NONE
    local houseCost = nil
    local houseCostAfterDiscount = nil
    local houseDiscountPercent = 0
    local houseEsoPlusCost = nil
    local defaultHouseTemplateMarketProductId = nil
    local hasMultiplePriceOptions = false
    if ZO_MarketProduct_IsHouseCollectible(marketProductId) then
        local marketProductHouseId = GetMarketProductHouseId(marketProductId)

        -- Iterate through all known house template defs and verify they have an equivalent market product.
        -- Only those with market products will be considered in determining the number of pricing options.
        local numHouseTemplates = GetNumHouseTemplatesForHouse(marketProductHouseId)
        local numEnabledTemplates = 0
        for index = 1, numHouseTemplates do
            local houseTemplateId = GetHouseTemplateIdByIndexForHouse(marketProductHouseId, index)
            local marketProductListings = getMarketProductListingsCallback(houseTemplateId, MARKET_DISPLAY_GROUP_CROWN_STORE)
            if #marketProductListings > 0 then
                numEnabledTemplates = numEnabledTemplates + 1
            end
        end

        hasMultiplePriceOptions = numEnabledTemplates > 1

        local defaultHouseTemplateId = GetDefaultHouseTemplateIdForHouse(marketProductHouseId)
        local defaultMarketProductListings = getMarketProductListingsCallback(defaultHouseTemplateId, MARKET_DISPLAY_GROUP_CROWN_STORE)
        -- There could be multiple listings per template, such as one for each currency type or for each category it should appear in,
        -- or just multiple market products for the same template
        local PRODUCT_LISTINGS_FOR_HOUSE_TEMPLATE_STRIDE = 2
        for i = 1, #defaultMarketProductListings, PRODUCT_LISTINGS_FOR_HOUSE_TEMPLATE_STRIDE do
            local houseTemplateMarketProductId = defaultMarketProductListings[i]
            local presentationIndex = defaultMarketProductListings[i + 1]

            local currencyType, cost, costAfterDiscount, discountPercent, esoPlusCost = GetMarketProductPricingByPresentation(houseTemplateMarketProductId, presentationIndex)

            if not houseCostAfterDiscount or costAfterDiscount < houseCostAfterDiscount then
                houseCurrencyType = currencyType
                houseCost = cost
                houseCostAfterDiscount = costAfterDiscount
                houseDiscountPercent = discountPercent
                houseEsoPlusCost = esoPlusCost
                defaultHouseTemplateMarketProductId = houseTemplateMarketProductId
            end
        end
    end

    return houseCurrencyType, houseCost, houseCostAfterDiscount, houseDiscountPercent, houseEsoPlusCost, defaultHouseTemplateMarketProductId, hasMultiplePriceOptions
end

--
--[[ XML Handlers ]]--
--

function ZO_MarketProductBase_OnInitialized(control)
    control.title = control:GetNamedChild("Title")
    control.background = control:GetNamedChild("Background")
    control.cost = control:GetNamedChild("Cost")
    control.previousCost = control:GetNamedChild("PreviousCost")
    control.esoPlusCost = control:GetNamedChild("EsoPlusCost")
    control.purchaseLabelControl = control:GetNamedChild("Purchased")
    control.esoPlusDealLabelControl = control:GetNamedChild("EsoPlusDeal")
    control.textCallout = control:GetNamedChild("TextCallout")
    control.textCalloutBackground = control.textCallout:GetNamedChild("Background")
    if control.textCalloutBackground then
        control.leftCalloutBackground = control.textCalloutBackground:GetNamedChild("Left")
        control.rightCalloutBackground = control.textCalloutBackground:GetNamedChild("Right")
        control.centerCalloutBackground = control.textCalloutBackground:GetNamedChild("Center")
    end
    control.numBundledProductsLabel = control:GetNamedChild("BundledProducts")
    control.bundledProductsItemsLabel = control:GetNamedChild("BundledProductsLabel")
    control.bundleIndicator = control:GetNamedChild("BundleIndicator")
end