/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2021 Ola Rinta-Koski
 * Copyright (C) 2023 CERN (www.cern.ch)
 * Copyright (C) 2021-2022 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


#include <font/fontconfig.h>
#include <wx/log.h>
#include <trace_helpers.h>
#include <string_utils.h>
#include <macros.h>
#include <cstdint>

#ifdef __WIN32__
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

using namespace fontconfig;

static FONTCONFIG* g_config = nullptr;
static bool        g_fcInitSuccess = false;

/**
 * A simple wrapper to avoid exporing fontconfig in the header
 */
struct fontconfig::FONTCONFIG_PAT
{
    FcPattern* pat;
};


wxString FONTCONFIG::Version()
{
    return wxString::Format( "%d.%d.%d", FC_MAJOR, FC_MINOR, FC_REVISION );
}


FONTCONFIG::FONTCONFIG()
{
};


/**
 * This is simply a wrapper to call FcInit() with SEH for Windows
 * SEH on Windows can only be used in functions without objects that might be unwinded
 * (basically objects with destructors)
 * For example, new FONTCONFIG() in Fontconfig() is creating a object with a destructor
 * that *might* need to be unwinded. MSVC catches this and throws a compile error
 */
static void bootstrapFc()
{
#if defined( _MSC_VER )
    __try
    {
#endif
        FcInit();
        g_fcInitSuccess = true;
#if defined( _MSC_VER )
    }
    __except( GetExceptionCode() == STATUS_IN_PAGE_ERROR ? EXCEPTION_EXECUTE_HANDLER
                                                         : EXCEPTION_CONTINUE_SEARCH )
    {
        g_fcInitSuccess = false;
        // We have documented cases that fontconfig while trying to cache fonts
        // ends up using freetype to try and get font info
        // freetype itself reads fonts through memory mapping instead of normal file APIs
        // there are crashes reading fonts sometimes as a result that return STATUS_IN_PAGE_ERROR
    }
#endif
}


FONTCONFIG* Fontconfig()
{
    if( !g_config )
    {
        bootstrapFc();
        g_config = new FONTCONFIG();
    }

    return g_config;
}


bool FONTCONFIG::isLanguageMatch( const wxString& aSearchLang, const wxString& aSupportedLang )
{
    if( aSearchLang.Lower() == aSupportedLang.Lower() )
        return true;

    if( aSupportedLang.empty() )
        return false;

    if( aSearchLang.empty() )
        return false;

    wxArrayString supportedLangBits;
    wxStringSplit( aSupportedLang.Lower(), supportedLangBits, wxS( '-' ) );

    wxArrayString searhcLangBits;
    wxStringSplit( aSearchLang.Lower(), searhcLangBits, wxS( '-' ) );

    // if either side of the comparison have only one section, then its a broad match but fine
    // i.e. the haystack is declaring broad support or the search language is broad
    if( searhcLangBits.size() == 1 || supportedLangBits.size() == 1 )
    {
        return searhcLangBits[0] == supportedLangBits[0];
    }

    // the full two part comparison should have passed the initial shortcut

    return false;
}


std::string FONTCONFIG::getFcString( FONTCONFIG_PAT& aPat, const char* aObj, int aIdx )
{
    FcChar8*    str;
    std::string res;

    if( FcPatternGetString( aPat.pat, aObj, aIdx, &str ) == FcResultMatch )
        res = std::string( reinterpret_cast<char*>( str ) );

    return res;
}


void FONTCONFIG::getAllFamilyStrings( FONTCONFIG_PAT&                               aPat,
                                      std::unordered_map<std::string, std::string>& aFamStringMap )
{
    std::string famLang;
    std::string fam;

    int langIdx = 0;
    do
    {
        famLang = getFcString( aPat, FC_FAMILYLANG, langIdx );

        if( famLang.empty() && langIdx != 0 )
            break;
        else
        {
            fam = getFcString( aPat, FC_FAMILY, langIdx );
            aFamStringMap.insert_or_assign( famLang, fam );
        }
    } while( langIdx++ < std::numeric_limits<
                     int8_t>::max() ); //arbitrary to avoid getting stuck for any reason
}


std::string FONTCONFIG::getFamilyStringByLang( FONTCONFIG_PAT& aPat, const wxString& aDesiredLang )
{
    std::unordered_map<std::string, std::string> famStrings;
    getAllFamilyStrings( aPat, famStrings );

    if( famStrings.empty() )
        return "";

    for( auto const& [key, val] : famStrings )
    {
        if( isLanguageMatch( aDesiredLang, From_UTF8( key.c_str() ) ) )
        {
            return val;
        }
    }

    // fall back to the first and maybe only available name
    // most fonts by review don't even bother declaring more than one font family name
    // and they don't even bother declare the language tag either, they just leave it blank
    return famStrings.begin()->second;
}


FONTCONFIG::FF_RESULT FONTCONFIG::FindFont( const wxString &aFontName, wxString &aFontFile,
                                            int& aFaceIndex, bool aBold, bool aItalic )
{
    FF_RESULT retval = FF_RESULT::FF_ERROR;

    if( !g_fcInitSuccess )
        return retval;

    wxString qualifiedFontName = aFontName;

    wxScopedCharBuffer const fcBuffer = qualifiedFontName.ToUTF8();

    FcPattern* pat = FcPatternCreate();

    if( aBold )
        FcPatternAddString( pat, FC_STYLE, (const FcChar8*) "Bold" );

    if( aItalic )
        FcPatternAddString( pat, FC_STYLE, (const FcChar8*) "Italic" );

    FcPatternAddString( pat, FC_FAMILY, (FcChar8*) fcBuffer.data() );

    FcConfigSubstitute( nullptr, pat, FcMatchPattern );
    FcDefaultSubstitute( pat );

    FcResult   r = FcResultNoMatch;
    FcPattern* font = FcFontMatch( nullptr, pat, &r );

    wxString fontName;

    if( font )
    {
        FcChar8* file = nullptr;

        if( FcPatternGetString( font, FC_FILE, 0, &file ) == FcResultMatch )
        {
            aFontFile = wxString::FromUTF8( (char*) file );
            aFaceIndex = 0;

            wxString styleStr;
            FcChar8* family = nullptr;
            FcChar8* style = nullptr;

            retval = FF_RESULT::FF_SUBSTITUTE;

            std::unordered_map<std::string, std::string> famStrings;
            FONTCONFIG_PAT                               patHolder{ font };

            getAllFamilyStrings( patHolder, famStrings );

            if( FcPatternGetString( font, FC_FAMILY, 0, &family ) == FcResultMatch )
            {
                FcPatternGetInteger( font, FC_INDEX, 0, &aFaceIndex );

                fontName = wxString::FromUTF8( (char*) family );

                if( FcPatternGetString( font, FC_STYLE, 0, &style ) == FcResultMatch )
                {
                    styleStr = wxString::FromUTF8( (char*) style );

                    if( !styleStr.IsEmpty() )
                    {
                        styleStr.Replace( ' ', ':' );
                        fontName += ':' + styleStr;
                    }
                }

                bool has_bold = false;
                bool has_ital = false;
                wxString lower_style = styleStr.Lower();

                if( lower_style.Contains( wxS( "thin" ) )
                         || lower_style.Contains( wxS( "light" ) )   // also cataches ultralight and extralight
                         || lower_style.Contains( wxS( "regular" ) )
                         || lower_style.Contains( wxS( "roman" ) )
                         || lower_style.Contains( wxS( "book" ) ) )
                {
                    has_bold = false;
                }
                else if( lower_style.Contains( wxS( "medium" ) )
                         || lower_style.Contains( wxS( "semibold" ) )
                         || lower_style.Contains( wxS( "demibold" ) ) )
                {
                    has_bold = aBold;
                }
                else if( lower_style.Contains( wxS( "bold" ) )       // also catches ultrabold
                         || lower_style.Contains( wxS( "heavy" ) )
                         || lower_style.Contains( wxS( "black" ) )   // also catches extrablack
                         || lower_style.Contains( wxS( "thick" ) )
                         || lower_style.Contains( wxS( "dark" ) ) )
                {
                    has_bold = true;
                }

                if( lower_style.Contains( wxS( "italic" ) )
                        || lower_style.Contains( wxS( "oblique" ) )
                        || lower_style.Contains( wxS( "slant" ) ) )
                {
                    has_ital = true;
                }

                for( auto const& [key, val] : famStrings )
                {
                    wxString searchFont;
                    searchFont = wxString::FromUTF8( (char*) val.data() );

                    if( searchFont.Lower().StartsWith( aFontName.Lower() ) )
                    {
                        if( ( aBold && !has_bold ) && ( aItalic && !has_ital ) )
                            retval = FF_RESULT::FF_MISSING_BOLD_ITAL;
                        else if( aBold && !has_bold )
                            retval = FF_RESULT::FF_MISSING_BOLD;
                        else if( aItalic && !has_ital )
                            retval = FF_RESULT::FF_MISSING_ITAL;
                        else if( ( aBold != has_bold ) || ( aItalic != has_ital ) )
                            retval = FF_RESULT::FF_SUBSTITUTE;
                        else
                            retval = FF_RESULT::FF_OK;

                        break;
                    }
                }
            }
        }

        FcPatternDestroy( font );
    }

    if( retval == FF_RESULT::FF_ERROR )
    {
        wxLogWarning( _( "Error loading font '%s'." ), qualifiedFontName );
    }
    else if( retval == FF_RESULT::FF_SUBSTITUTE )
    {
        fontName.Replace( ':', ' ' );
        wxLogWarning( _( "Font '%s' not found; substituting '%s'." ), qualifiedFontName, fontName );
    }

    FcPatternDestroy( pat );
    return retval;
}


void FONTCONFIG::ListFonts( std::vector<std::string>& aFonts, const std::string& aDesiredLang )
{
    if( !g_fcInitSuccess )
        return;

    // be sure to cache bust if the language changed
    if( m_fontInfoCache.empty() || m_fontCacheLastLang != aDesiredLang )
    {
        FcPattern*   pat = FcPatternCreate();
        FcObjectSet* os = FcObjectSetBuild( FC_FAMILY, FC_FAMILYLANG, FC_STYLE, FC_LANG, FC_FILE,
                                            FC_OUTLINE, nullptr );
        FcFontSet*   fs = FcFontList( nullptr, pat, os );

        for( int i = 0; fs && i < fs->nfont; ++i )
        {
            FcPattern* font = fs->fonts[i];
            FcChar8*   file;
            FcChar8*   style;
            FcLangSet* langSet;
            FcBool     outline;

            if( FcPatternGetString( font, FC_FILE, 0, &file ) == FcResultMatch
                && FcPatternGetString( font, FC_STYLE, 0, &style ) == FcResultMatch
                && FcPatternGetLangSet( font, FC_LANG, 0, &langSet ) == FcResultMatch
                && FcPatternGetBool( font, FC_OUTLINE, 0, &outline ) == FcResultMatch )
            {
                if( !outline )
                    continue;

                FONTCONFIG_PAT patHolder{ font };
                std::string    theFamily =
                        getFamilyStringByLang( patHolder, From_UTF8( aDesiredLang.c_str() ) );

#ifdef __WXMAC__
                // On Mac (at least) some of the font names are in their own language.  If
                // the OS doesn't support this language then we get a bunch of garbage names
                // in the font menu.
                //
                // GTK, on the other hand, doesn't appear to support wxLocale::IsAvailable(),
                // so we can't run these checks.

                FcStrSet*  langStrSet = FcLangSetGetLangs( langSet );
                FcStrList* langStrList = FcStrListCreate( langStrSet );
                FcChar8*   langStr = FcStrListNext( langStrList );
                bool langSupported = false;

                if( !langStr )
                {
                    // Symbol fonts (Wingdings, etc.) have no language
                    langSupported = true;
                }
                else while( langStr )
                {
                    wxString langWxStr( reinterpret_cast<char *>( langStr ) );
                    const wxLanguageInfo* langInfo = wxLocale::FindLanguageInfo( langWxStr );

                    if( langInfo && wxLocale::IsAvailable( langInfo->Language ) )
                    {
                        langSupported = true;
                        break;
                    }
                    else
                    {
                        wxLogTrace( traceFonts, wxS( "Font '%s' language '%s' not supported by OS." ),
                                    theFamily, langWxStr );
                    }

                    langStr = FcStrListNext( langStrList );
                }

                FcStrListDone( langStrList );
                FcStrSetDestroy( langStrSet );

                if( !langSupported )
                    continue;
#endif

                std::string theFile( reinterpret_cast<char *>( file ) );
                std::string theStyle( reinterpret_cast<char *>( style ) );
                FONTINFO    fontInfo( std::move( theFile ), std::move( theStyle ), theFamily );

                if( theFamily.length() > 0 && theFamily.front() == '.' )
                    continue;

                std::map<std::string, FONTINFO>::iterator it = m_fontInfoCache.find( theFamily );

                if( it == m_fontInfoCache.end() )
                    m_fontInfoCache.emplace( theFamily, fontInfo );
                else
                    it->second.Children().push_back( fontInfo );
            }
        }

        if( fs )
            FcFontSetDestroy( fs );

        m_fontCacheLastLang = aDesiredLang;
    }

    for( const std::pair<const std::string, FONTINFO>& entry : m_fontInfoCache )
        aFonts.push_back( entry.second.Family() );
}