/*
 * Copyright (C) 2007-2021 Apple Inc. All rights reserved.
 *           (C) 2007, 2008 Nikolas Zimmermann <zimmermann@kde.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "CSSFontSelector.h"

#include "CachedFont.h"
#include "CSSFontFace.h"
#include "CSSFontFaceSource.h"
#include "CSSFontFamily.h"
#include "CSSPrimitiveValue.h"
#include "CSSPropertyNames.h"
#include "CSSSegmentedFontFace.h"
#include "CSSValueKeywords.h"
#include "CSSValueList.h"
#include "CachedResourceLoader.h"
#include "Document.h"
#include "Font.h"
#include "FontCache.h"
#include "FontFace.h"
#include "FontFaceSet.h"
#include "FontSelectorClient.h"
#include "Frame.h"
#include "FrameLoader.h"
#include "Logging.h"
#include "ResourceLoadObserver.h"
#include "RuntimeEnabledFeatures.h"
#include "Settings.h"
#include "StyleProperties.h"
#include "StyleResolver.h"
#include "StyleRule.h"
#include <wtf/Ref.h>
#include <wtf/SetForScope.h>
#include <wtf/text/AtomString.h>

namespace WebCore {

using namespace WebKitFontFamilyNames;

static unsigned fontSelectorId;

Ref<CSSFontSelector> CSSFontSelector::create(ScriptExecutionContext& context)
{
    auto fontSelector = adoptRef(*new CSSFontSelector(context));
    fontSelector->suspendIfNeeded();
    return fontSelector;
}

CSSFontSelector::CSSFontSelector(ScriptExecutionContext& context)
    : ActiveDOMObject(&context)
    , m_context(context)
    , m_cssFontFaceSet(CSSFontFaceSet::create(this))
    , m_fontModifiedObserver([this] { fontModified(); })
    , m_uniqueId(++fontSelectorId)
    , m_version(0)
{
    if (is<Document>(context)) {
        m_fontFamilyNames.reserveInitialCapacity(familyNames->size());
        for (auto& familyName : familyNames.get())
            m_fontFamilyNames.uncheckedConstructAndAppend(familyName);
    } else {
        m_fontFamilyNames.reserveInitialCapacity(familyNamesData->size());
        for (auto& familyName : familyNamesData.get())
            m_fontFamilyNames.uncheckedAppend(familyName);
    }

    FontCache::forCurrentThread().addClient(*this);
    m_cssFontFaceSet->addFontModifiedObserver(m_fontModifiedObserver);
    LOG(Fonts, "CSSFontSelector %p ctor", this);
}

CSSFontSelector::~CSSFontSelector()
{
    LOG(Fonts, "CSSFontSelector %p dtor", this);

    clearFonts();

    if (auto fontCache = FontCache::forCurrentThreadIfNotDestroyed())
        fontCache->removeClient(*this);
}

FontFaceSet* CSSFontSelector::fontFaceSetIfExists()
{
    return m_fontFaceSet.get();
}

FontFaceSet& CSSFontSelector::fontFaceSet()
{
    if (!m_fontFaceSet) {
        ASSERT(m_context);
        m_fontFaceSet = FontFaceSet::create(*m_context, m_cssFontFaceSet.get());
    }

    return *m_fontFaceSet;
}

bool CSSFontSelector::isEmpty() const
{
    return !m_cssFontFaceSet->faceCount();
}

void CSSFontSelector::emptyCaches()
{
    m_cssFontFaceSet->emptyCaches();
}

void CSSFontSelector::buildStarted()
{
    m_buildIsUnderway = true;
    m_cssFontFaceSet->purge();
    ++m_version;

    ASSERT(m_cssConnectionsPossiblyToRemove.isEmpty());
    ASSERT(m_cssConnectionsEncounteredDuringBuild.isEmpty());
    ASSERT(m_stagingArea.isEmpty());
    for (size_t i = 0; i < m_cssFontFaceSet->faceCount(); ++i) {
        CSSFontFace& face = m_cssFontFaceSet.get()[i];
        if (face.cssConnection())
            m_cssConnectionsPossiblyToRemove.add(&face);
    }

    m_paletteMap.clear();
}

void CSSFontSelector::buildCompleted()
{
    if (!m_buildIsUnderway)
        return;

    m_buildIsUnderway = false;

    // Some font faces weren't re-added during the build process.
    for (auto& face : m_cssConnectionsPossiblyToRemove) {
        auto* connection = face->cssConnection();
        ASSERT(connection);
        if (!m_cssConnectionsEncounteredDuringBuild.contains(connection))
            m_cssFontFaceSet->remove(*face);
    }

    for (auto& item : m_stagingArea)
        addFontFaceRule(item.styleRuleFontFace, item.isInitiatingElementInUserAgentShadowTree);
    m_cssConnectionsEncounteredDuringBuild.clear();
    m_stagingArea.clear();
    m_cssConnectionsPossiblyToRemove.clear();
}

void CSSFontSelector::addFontFaceRule(StyleRuleFontFace& fontFaceRule, bool isInitiatingElementInUserAgentShadowTree)
{
    if (m_buildIsUnderway) {
        m_cssConnectionsEncounteredDuringBuild.add(&fontFaceRule);
        m_stagingArea.append({fontFaceRule, isInitiatingElementInUserAgentShadowTree});
        return;
    }

    const StyleProperties& style = fontFaceRule.properties();
    RefPtr<CSSValue> fontFamily = style.getPropertyCSSValue(CSSPropertyFontFamily);
    RefPtr<CSSValue> fontStyle = style.getPropertyCSSValue(CSSPropertyFontStyle);
    RefPtr<CSSValue> fontWeight = style.getPropertyCSSValue(CSSPropertyFontWeight);
    RefPtr<CSSValue> fontStretch = style.getPropertyCSSValue(CSSPropertyFontStretch);
    RefPtr<CSSValue> src = style.getPropertyCSSValue(CSSPropertySrc);
    RefPtr<CSSValue> unicodeRange = style.getPropertyCSSValue(CSSPropertyUnicodeRange);
    RefPtr<CSSValue> featureSettings = style.getPropertyCSSValue(CSSPropertyFontFeatureSettings);
    RefPtr<CSSValue> loadingBehavior = style.getPropertyCSSValue(CSSPropertyFontDisplay);
    if (!is<CSSValueList>(fontFamily) || !is<CSSValueList>(src) || (unicodeRange && !is<CSSValueList>(*unicodeRange)))
        return;

    CSSValueList& familyList = downcast<CSSValueList>(*fontFamily);
    if (!familyList.length())
        return;

    CSSValueList* rangeList = downcast<CSSValueList>(unicodeRange.get());

    CSSValueList& srcList = downcast<CSSValueList>(*src);
    if (!srcList.length())
        return;

    SetForScope creatingFont(m_creatingFont, true);
    auto fontFace = CSSFontFace::create(*this, &fontFaceRule);

    if (!fontFace->setFamilies(*fontFamily))
        return;
    if (fontStyle)
        fontFace->setStyle(*fontStyle);
    if (fontWeight)
        fontFace->setWeight(*fontWeight);
    if (fontStretch)
        fontFace->setStretch(*fontStretch);
    if (rangeList && !fontFace->setUnicodeRange(*rangeList))
        return;
    if (featureSettings)
        fontFace->setFeatureSettings(*featureSettings);
    if (loadingBehavior)
        fontFace->setLoadingBehavior(*loadingBehavior);

    CSSFontFace::appendSources(fontFace, srcList, m_context.get(), isInitiatingElementInUserAgentShadowTree);

    if (RefPtr<CSSFontFace> existingFace = m_cssFontFaceSet->lookUpByCSSConnection(fontFaceRule)) {
        // This adoption is fairly subtle. Script can trigger a purge of m_cssFontFaceSet at any time,
        // which will cause us to just rely on the memory cache to retain the bytes of the file the next
        // time we build up the CSSFontFaceSet. However, when the CSS Font Loading API is involved,
        // the FontFace and FontFaceSet objects need to retain state. We create the new CSSFontFace object
        // while the old one is still in scope so that the memory cache will be forced to retain the bytes
        // of the resource. This means that the CachedFont will temporarily have two clients (until the
        // old CSSFontFace goes out of scope, which should happen at the end of this "if" block). Because
        // the CSSFontFaceSource objects will inspect their CachedFonts, the new CSSFontFace is smart enough
        // to enter the correct state() during the next pump(). This approach of making a new CSSFontFace is
        // simpler than computing and applying a diff of the StyleProperties.
        m_cssFontFaceSet->remove(*existingFace);
        if (auto* existingWrapper = existingFace->existingWrapper())
            existingWrapper->adopt(fontFace.get());
    }

    m_cssFontFaceSet->add(fontFace.get());
    ++m_version;
}

void CSSFontSelector::addFontPaletteValuesRule(StyleRuleFontPaletteValues& fontPaletteValuesRule)
{
    AtomString fontFamily = fontPaletteValuesRule.fontFamily().isNull() ? emptyAtom() : fontPaletteValuesRule.fontFamily();
    AtomString name = fontPaletteValuesRule.name().isNull() ? emptyAtom() : fontPaletteValuesRule.name();
    m_paletteMap.set(std::make_pair(fontFamily, name), fontPaletteValuesRule.fontPaletteValues());

    ++m_version;
}

void CSSFontSelector::registerForInvalidationCallbacks(FontSelectorClient& client)
{
    m_clients.add(&client);
}

void CSSFontSelector::unregisterForInvalidationCallbacks(FontSelectorClient& client)
{
    m_clients.remove(&client);
}

void CSSFontSelector::dispatchInvalidationCallbacks()
{
    ++m_version;

    for (auto& client : copyToVector(m_clients))
        client->fontsNeedUpdate(*this);
}

void CSSFontSelector::opportunisticallyStartFontDataURLLoading(const FontCascadeDescription& description, const AtomString& familyName)
{
    const auto& segmentedFontFace = m_cssFontFaceSet->fontFace(description.fontSelectionRequest(), familyName);
    if (!segmentedFontFace)
        return;
    for (auto& face : segmentedFontFace->constituentFaces())
        face->opportunisticallyStartFontDataURLLoading();
}

void CSSFontSelector::fontLoaded(CSSFontFace&)
{
    dispatchInvalidationCallbacks();
}

void CSSFontSelector::fontModified()
{
    if (!m_creatingFont && !m_buildIsUnderway)
        dispatchInvalidationCallbacks();
}

void CSSFontSelector::updateStyleIfNeeded()
{
    if (is<Document>(m_context))
        downcast<Document>(*m_context).updateStyleIfNeeded();
}

void CSSFontSelector::updateStyleIfNeeded(CSSFontFace&)
{
    updateStyleIfNeeded();
}

void CSSFontSelector::fontCacheInvalidated()
{
    dispatchInvalidationCallbacks();
}

std::optional<AtomString> CSSFontSelector::resolveGenericFamily(const FontDescription& fontDescription, const AtomString& familyName)
{
    auto platformResult = FontDescription::platformResolveGenericFamily(fontDescription.script(), fontDescription.computedLocale(), familyName);
    if (!platformResult.isNull())
        return platformResult;

    if (!m_context)
        return std::nullopt;

    const auto& settings = m_context->settingsValues();

    UScriptCode script = fontDescription.script();
    auto familyNameIndex = m_fontFamilyNames.find(familyName);
    if (familyNameIndex != notFound) {
        if (auto familyString = settings.fontGenericFamilies.fontFamily(static_cast<FamilyNamesIndex>(familyNameIndex), script))
            return AtomString(*familyString);
    }

    return std::nullopt;
}

const FontPaletteValues& CSSFontSelector::lookupFontPaletteValues(const AtomString& familyName, const FontDescription& fontDescription)
{
    static NeverDestroyed<FontPaletteValues> emptyFontPaletteValues;
    if (fontDescription.fontPalette().type != FontPalette::Type::Custom)
        return emptyFontPaletteValues.get();

    const AtomString paletteName = fontDescription.fontPalette().identifier;

    auto iterator = m_paletteMap.find(std::make_pair(familyName, paletteName));
    if (iterator == m_paletteMap.end())
        return emptyFontPaletteValues.get();
    return iterator->value;
}

FontRanges CSSFontSelector::fontRangesForFamily(const FontDescription& fontDescription, const AtomString& familyName)
{
    // If this ASSERT() fires, it usually means you forgot a document.updateStyleIfNeeded() somewhere.
    ASSERT(!m_buildIsUnderway || m_computingRootStyleFontCount);

    // FIXME: The spec (and Firefox) says user specified generic families (sans-serif etc.) should be resolved before the @font-face lookup too.
    bool resolveGenericFamilyFirst = familyName == m_fontFamilyNames.at(FamilyNamesIndex::StandardFamily);

    AtomString familyForLookup = familyName;
    std::optional<FontDescription> overrideFontDescription;
    const FontDescription* fontDescriptionForLookup = &fontDescription;
    auto resolveAndAssignGenericFamily = [&]() {
        if (auto genericFamilyOptional = resolveGenericFamily(fontDescription, familyName))
            familyForLookup = *genericFamilyOptional;
    };

    const auto& fontPaletteValues = lookupFontPaletteValues(familyName, fontDescription);

    if (resolveGenericFamilyFirst)
        resolveAndAssignGenericFamily();
    auto* document = dynamicDowncast<Document>(m_context.get());
    auto* face = m_cssFontFaceSet->fontFace(fontDescriptionForLookup->fontSelectionRequest(), familyForLookup);
    if (face) {
        if (document && RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled())
            ResourceLoadObserver::shared().logFontLoad(*document, familyForLookup.string(), true);
        return face->fontRanges(*fontDescriptionForLookup, fontPaletteValues);
    }

    if (!resolveGenericFamilyFirst)
        resolveAndAssignGenericFamily();
    auto font = FontCache::forCurrentThread().fontForFamily(*fontDescriptionForLookup, familyForLookup, { { }, { }, fontPaletteValues });
    if (document && RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled())
        ResourceLoadObserver::shared().logFontLoad(*document, familyForLookup.string(), !!font);
    return FontRanges { WTFMove(font) };
}

void CSSFontSelector::clearFonts()
{
    m_isStopped = true;
    m_cssFontFaceSet->clear();
    m_clients.clear();
}

size_t CSSFontSelector::fallbackFontCount()
{
    if (m_isStopped)
        return 0;

    return m_context->settingsValues().fontFallbackPrefersPictographs ? 1 : 0;
}

RefPtr<Font> CSSFontSelector::fallbackFontAt(const FontDescription& fontDescription, size_t index)
{
    ASSERT_UNUSED(index, !index);

    if (m_isStopped)
        return nullptr;

    if (!m_context->settingsValues().fontFallbackPrefersPictographs)
        return nullptr;
    auto& pictographFontFamily = m_context->settingsValues().fontGenericFamilies.pictographFontFamily();
    auto font = FontCache::forCurrentThread().fontForFamily(fontDescription, pictographFontFamily);
    if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled() && is<Document>(m_context))
        ResourceLoadObserver::shared().logFontLoad(downcast<Document>(*m_context), pictographFontFamily, !!font);

    return font;
}

}
