/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright The 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 2
 * 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, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */


#include <kiface_base.h>
#include <env_vars.h>
#include <lib_id.h>
#include <lib_table_lexer.h>
#include <paths.h>
#include <pgm_base.h>
#include <search_stack.h>
#include <settings/kicad_settings.h>
#include <settings/settings_manager.h>
#include <systemdirsappend.h>
#include <wildcards_and_files_ext.h>
#include <design_block_info.h>
#include <design_block_lib_table.h>
#include <design_block.h>

#include <wx/dir.h>
#include <wx/hash.h>
#include <locale_io.h>

#define OPT_SEP '|' ///< options separator character

/// The global design block library table.  This is not dynamically allocated because
/// in a multiple project environment we must keep its address constant (since it is
/// the fallback table for multiple projects).
DESIGN_BLOCK_LIB_TABLE GDesignBlockTable;

/// The global footprint info table.  This is performance-intensive to build so we
/// keep a hash-stamped global version.  Any deviation from the request vs. stored
/// hash will result in it being rebuilt.
DESIGN_BLOCK_LIST_IMPL GDesignBlockList;


using namespace LIB_TABLE_T;


bool DESIGN_BLOCK_LIB_TABLE_ROW::operator==( const DESIGN_BLOCK_LIB_TABLE_ROW& aRow ) const
{
    return LIB_TABLE_ROW::operator==( aRow ) && type == aRow.type;
}


void DESIGN_BLOCK_LIB_TABLE_ROW::SetType( const wxString& aType )
{
    type = DESIGN_BLOCK_IO_MGR::EnumFromStr( aType );

    if( DESIGN_BLOCK_IO_MGR::DESIGN_BLOCK_FILE_T( -1 ) == type )
        type = DESIGN_BLOCK_IO_MGR::KICAD_SEXP;

    plugin.reset();
}


DESIGN_BLOCK_LIB_TABLE::DESIGN_BLOCK_LIB_TABLE( DESIGN_BLOCK_LIB_TABLE* aFallBackTable ) :
        LIB_TABLE( aFallBackTable )
{
    // not copying fall back, simply search aFallBackTable separately
    // if "nickName not found".
}


void DESIGN_BLOCK_LIB_TABLE::Parse( LIB_TABLE_LEXER* in )
{
    T        tok;
    wxString errMsg; // to collect error messages

    // This table may be nested within a larger s-expression, or not.
    // Allow for parser of that optional containing s-epression to have looked ahead.
    if( in->CurTok() != T_design_block_lib_table )
    {
        in->NeedLEFT();

        if( ( tok = in->NextTok() ) != T_design_block_lib_table )
            in->Expecting( T_design_block_lib_table );
    }

    while( ( tok = in->NextTok() ) != T_RIGHT )
    {
        std::unique_ptr<DESIGN_BLOCK_LIB_TABLE_ROW> row =
                std::make_unique<DESIGN_BLOCK_LIB_TABLE_ROW>();

        if( tok == T_EOF )
            in->Expecting( T_RIGHT );

        if( tok != T_LEFT )
            in->Expecting( T_LEFT );

        // in case there is a "row integrity" error, tell where later.
        int lineNum = in->CurLineNumber();
        tok = in->NextTok();

        // Optionally parse the current version number
        if( tok == T_version )
        {
            in->NeedNUMBER( "version" );
            m_version = std::stoi( in->CurText() );
            in->NeedRIGHT();
            continue;
        }

        if( tok != T_lib )
            in->Expecting( T_lib );

        // (name NICKNAME)
        in->NeedLEFT();

        if( ( tok = in->NextTok() ) != T_name )
            in->Expecting( T_name );

        in->NeedSYMBOLorNUMBER();

        row->SetNickName( in->FromUTF8() );

        in->NeedRIGHT();

        // After (name), remaining (lib) elements are order independent, and in
        // some cases optional.
        bool sawType = false;
        bool sawOpts = false;
        bool sawDesc = false;
        bool sawUri = false;
        bool sawDisabled = false;

        while( ( tok = in->NextTok() ) != T_RIGHT )
        {
            if( tok == T_EOF )
                in->Unexpected( T_EOF );

            if( tok != T_LEFT )
                in->Expecting( T_LEFT );

            tok = in->NeedSYMBOLorNUMBER();

            switch( tok )
            {
            case T_uri:
                if( sawUri )
                    in->Duplicate( tok );

                sawUri = true;
                in->NeedSYMBOLorNUMBER();
                row->SetFullURI( in->FromUTF8() );
                break;

            case T_type:
                if( sawType )
                    in->Duplicate( tok );

                sawType = true;
                in->NeedSYMBOLorNUMBER();
                row->SetType( in->FromUTF8() );
                break;

            case T_options:
                if( sawOpts )
                    in->Duplicate( tok );

                sawOpts = true;
                in->NeedSYMBOLorNUMBER();
                row->SetOptions( in->FromUTF8() );
                break;

            case T_descr:
                if( sawDesc )
                    in->Duplicate( tok );

                sawDesc = true;
                in->NeedSYMBOLorNUMBER();
                row->SetDescr( in->FromUTF8() );
                break;

            case T_disabled:
                if( sawDisabled )
                    in->Duplicate( tok );

                sawDisabled = true;
                row->SetEnabled( false );
                break;

            case T_hidden:
                // Hiding design block libraries is not yet supported.  Unclear what path can
                // set this attribute, but clear it on load.
                row->SetVisible();
                break;

            default:
                in->Unexpected( tok );
            }

            in->NeedRIGHT();
        }

        if( !sawType )
            in->Expecting( T_type );

        if( !sawUri )
            in->Expecting( T_uri );

        // All nickNames within this table fragment must be unique, so we do not use doReplace
        // in doInsertRow().  (However a fallBack table can have a conflicting nickName and ours
        // will supercede that one since in FindLib() we search this table before any fall back.)
        wxString nickname = row->GetNickName(); // store it to be able to used it
                                                // after row deletion if an error occurs
        bool           doReplace = false;
        LIB_TABLE_ROW* tmp = row.release();

        if( !doInsertRow( tmp, doReplace ) )
        {
            delete tmp; // The table did not take ownership of the row.

            wxString msg = wxString::Format( _( "Duplicate library nickname '%s' found in "
                                                "design block library table file line %d." ),
                                             nickname, lineNum );

            if( !errMsg.IsEmpty() )
                errMsg << '\n';

            errMsg << msg;
        }
    }

    if( !errMsg.IsEmpty() )
        THROW_IO_ERROR( errMsg );
}


bool DESIGN_BLOCK_LIB_TABLE::operator==( const DESIGN_BLOCK_LIB_TABLE& aDesignBlockTable ) const
{
    if( m_rows.size() == aDesignBlockTable.m_rows.size() )
    {
        for( unsigned i = 0; i < m_rows.size(); ++i )
        {
            if( (DESIGN_BLOCK_LIB_TABLE_ROW&) m_rows[i]
                != (DESIGN_BLOCK_LIB_TABLE_ROW&) aDesignBlockTable.m_rows[i] )
            {
                return false;
            }
        }

        return true;
    }

    return false;
}


void DESIGN_BLOCK_LIB_TABLE::Format( OUTPUTFORMATTER* aOutput, int aIndentLevel ) const
{
    aOutput->Print( aIndentLevel, "(design_block_lib_table\n" );
    aOutput->Print( aIndentLevel + 1, "(version %d)\n", m_version );

    for( const LIB_TABLE_ROW& row : m_rows)
        row.Format( aOutput, aIndentLevel + 1 );

    aOutput->Print( aIndentLevel, ")\n" );
}


long long DESIGN_BLOCK_LIB_TABLE::GenerateTimestamp( const wxString* aNickname )
{
    long long hash = 0;

    if( aNickname )
    {
        const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( *aNickname, true );

        wxCHECK( row && row->plugin, hash );

        return row->plugin->GetLibraryTimestamp( row->GetFullURI( true ) )
               + wxHashTable::MakeKey( *aNickname );
    }

    for( const wxString& nickname : GetLogicalLibs() )
    {
        const DESIGN_BLOCK_LIB_TABLE_ROW* row = nullptr;

        try
        {
            row = FindRow( nickname, true );
        }
        catch( ... )
        {
            // Do nothing if not found: just skip.
        }

        wxCHECK2( row && row->plugin, continue );

        hash += row->plugin->GetLibraryTimestamp( row->GetFullURI( true ) )
                + wxHashTable::MakeKey( nickname );
    }

    return hash;
}


void DESIGN_BLOCK_LIB_TABLE::DesignBlockEnumerate( wxArrayString&  aDesignBlockNames, const wxString& aNickname,
                                                   bool aBestEfforts, const LOCALE_IO* aLocale )
{
    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );

    if( !aLocale )
    {
        LOCALE_IO toggle_locale;

        row->plugin->DesignBlockEnumerate( aDesignBlockNames, row->GetFullURI( true ), aBestEfforts,
                                           row->GetProperties() );
    }
    else
    {
        row->plugin->DesignBlockEnumerate( aDesignBlockNames, row->GetFullURI( true ), aBestEfforts,
                                           row->GetProperties() );
    }
}


const DESIGN_BLOCK_LIB_TABLE_ROW* DESIGN_BLOCK_LIB_TABLE::FindRow( const wxString& aNickname,
                                                                   bool            aCheckIfEnabled )
{
    DESIGN_BLOCK_LIB_TABLE_ROW* row =
            static_cast<DESIGN_BLOCK_LIB_TABLE_ROW*>( findRow( aNickname, aCheckIfEnabled ) );

    if( !row )
    {
        THROW_IO_ERROR( wxString::Format( _( "design-block-lib-table files contain no library "
                                             "named '%s'." ),
                                          aNickname ) );
    }

    if( !row->plugin )
        row->setPlugin( DESIGN_BLOCK_IO_MGR::FindPlugin( row->type ) );

    return row;
}


static void setLibNickname( DESIGN_BLOCK* aModule, const wxString& aNickname,
                            const wxString& aDesignBlockName )
{
    // The library cannot know its own name, because it might have been renamed or moved.
    // Therefore design blocks cannot know their own library nickname when residing in
    // a design block library.
    // Only at this API layer can we tell the design block about its actual library nickname.
    if( aModule )
    {
        // remove "const"-ness, I really do want to set nickname without
        // having to copy the LIB_ID and its two strings, twice each.
        LIB_ID& dbid = (LIB_ID&) aModule->GetLibId();

        // Catch any misbehaving plugin, which should be setting internal design block name
        // properly:
        wxASSERT( aDesignBlockName == dbid.GetLibItemName().wx_str() );

        // and clearing nickname
        wxASSERT( !dbid.GetLibNickname().size() );

        dbid.SetLibNickname( aNickname );
    }
}


const DESIGN_BLOCK*
DESIGN_BLOCK_LIB_TABLE::GetEnumeratedDesignBlock( const wxString& aNickname,
                                                  const wxString& aDesignBlockName,
                                                  const LOCALE_IO* aLocale )
{
    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );

    if( !aLocale )
    {
        LOCALE_IO toggle_locale;

        return row->plugin->GetEnumeratedDesignBlock( row->GetFullURI( true ), aDesignBlockName,
                                                      row->GetProperties() );
    }
    else
    {
        return row->plugin->GetEnumeratedDesignBlock( row->GetFullURI( true ), aDesignBlockName,
                                                      row->GetProperties() );
    }
}


bool DESIGN_BLOCK_LIB_TABLE::DesignBlockExists( const wxString& aNickname,
                                                const wxString& aDesignBlockName )
{
    // NOT THREAD-SAFE!  LOCALE_IO is global!

    LOCALE_IO toggle_locale;

    try
    {
        const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
        wxASSERT( row->plugin );

        return row->plugin->DesignBlockExists( row->GetFullURI( true ), aDesignBlockName,
                                               row->GetProperties() );
    }
    catch( ... )
    {
        return false;
    }
}


DESIGN_BLOCK* DESIGN_BLOCK_LIB_TABLE::DesignBlockLoad( const wxString& aNickname,
                                                       const wxString& aDesignBlockName,
                                                       bool            aKeepUUID )
{
    // NOT THREAD-SAFE!  LOCALE_IO is global!

    LOCALE_IO toggle_locale;

    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );

    DESIGN_BLOCK* ret = row->plugin->DesignBlockLoad( row->GetFullURI( true ), aDesignBlockName,
                                                      aKeepUUID, row->GetProperties() );

    setLibNickname( ret, row->GetNickName(), aDesignBlockName );

    return ret;
}


DESIGN_BLOCK_LIB_TABLE::SAVE_T
DESIGN_BLOCK_LIB_TABLE::DesignBlockSave( const wxString&     aNickname,
                                         const DESIGN_BLOCK* aDesignBlock, bool aOverwrite )
{
    // NOT THREAD-SAFE!  LOCALE_IO is global!

    LOCALE_IO toggle_locale;

    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );

    if( !aOverwrite )
    {
        // Try loading the design block to see if it already exists, caller wants overwrite
        // protection, which is atypical, not the default.

        wxString DesignBlockname = aDesignBlock->GetLibId().GetLibItemName();

        std::unique_ptr<DESIGN_BLOCK> design_block( row->plugin->DesignBlockLoad(
                row->GetFullURI( true ), DesignBlockname, row->GetProperties() ) );

        if( design_block )
            return SAVE_SKIPPED;
    }

    row->plugin->DesignBlockSave( row->GetFullURI( true ), aDesignBlock, row->GetProperties() );

    return SAVE_OK;
}


void DESIGN_BLOCK_LIB_TABLE::DesignBlockDelete( const wxString& aNickname,
                                                const wxString& aDesignBlockName )

{
    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );
    return row->plugin->DesignBlockDelete( row->GetFullURI( true ), aDesignBlockName,
                                           row->GetProperties() );
}


bool DESIGN_BLOCK_LIB_TABLE::IsDesignBlockLibWritable( const wxString& aNickname )
{
    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );
    return row->plugin->IsLibraryWritable( row->GetFullURI( true ) );
}


void DESIGN_BLOCK_LIB_TABLE::DesignBlockLibDelete( const wxString& aNickname )
{
    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );
    row->plugin->DeleteLibrary( row->GetFullURI( true ), row->GetProperties() );
}


void DESIGN_BLOCK_LIB_TABLE::DesignBlockLibCreate( const wxString& aNickname )
{
    const DESIGN_BLOCK_LIB_TABLE_ROW* row = FindRow( aNickname, true );
    wxASSERT( row->plugin );
    row->plugin->CreateLibrary( row->GetFullURI( true ), row->GetProperties() );
}


DESIGN_BLOCK*
DESIGN_BLOCK_LIB_TABLE::DesignBlockLoadWithOptionalNickname( const LIB_ID& aDesignBlockId,
                                                             bool          aKeepUUID )
{
    wxString nickname = aDesignBlockId.GetLibNickname();
    wxString DesignBlockname = aDesignBlockId.GetLibItemName();

    if( nickname.size() )
    {
        return DesignBlockLoad( nickname, DesignBlockname, aKeepUUID );
    }

    // nickname is empty, sequentially search (alphabetically) all libs/nicks for first match:
    else
    {
        // Search each library going through libraries alphabetically.
        for( const wxString& library : GetLogicalLibs() )
        {
            // DesignBlockLoad() returns NULL on not found, does not throw exception
            // unless there's an IO_ERROR.
            DESIGN_BLOCK* ret = DesignBlockLoad( library, DesignBlockname, aKeepUUID );

            if( ret )
                return ret;
        }

        return nullptr;
    }
}


const wxString DESIGN_BLOCK_LIB_TABLE::GlobalPathEnvVariableName()
{
    return ENV_VAR::GetVersionedEnvVarName( wxS( "DESIGN_BLOCK_DIR" ) );
}


class PCM_DESIGN_BLOCK_LIB_TRAVERSER final : public wxDirTraverser
{
public:
    explicit PCM_DESIGN_BLOCK_LIB_TRAVERSER( const wxString& aPath, DESIGN_BLOCK_LIB_TABLE& aTable,
                                             const wxString& aPrefix ) :
            m_lib_table( aTable ),
            m_path_prefix( aPath ),
            m_lib_prefix( aPrefix )
    {
        wxFileName f( aPath, wxS( "" ) );
        m_prefix_dir_count = f.GetDirCount();
    }

    wxDirTraverseResult OnFile( const wxString& aFilePath ) override { return wxDIR_CONTINUE; }

    wxDirTraverseResult OnDir( const wxString& dirPath ) override
    {
        wxFileName dir = wxFileName::DirName( dirPath );

        // consider a directory to be a lib if it's name ends with the design block lib dir
        // extension it is under $KICADn_3RD_PARTY/design_blocks/<pkgid>/ i.e. has nested
        // level of at least +3.
        if( dirPath.EndsWith( wxString::Format( wxS( ".%s" ), FILEEXT::KiCadDesignBlockLibPathExtension ) )
            && dir.GetDirCount() >= m_prefix_dir_count + 3 )
        {
            wxString versionedPath;
            versionedPath.Printf( wxS( "${%s}" ), ENV_VAR::GetVersionedEnvVarName( wxS( "3RD_PARTY" ) ) );

            wxArrayString parts = dir.GetDirs();
            parts.RemoveAt( 0, m_prefix_dir_count );
            parts.Insert( versionedPath, 0 );

            wxString libPath = wxJoin( parts, '/' );

            if( !m_lib_table.HasLibraryWithPath( libPath ) )
            {
                wxString name = parts.Last().substr( 0, parts.Last().length() - 7 );
                wxString nickname;
                nickname.Printf( wxS( "%s%s" ),
                                 m_lib_prefix,
                                 name );

                if( m_lib_table.HasLibrary( nickname ) )
                {
                    int increment = 1;

                    do
                    {
                        nickname.Printf( wxS( "%s%s_%d" ),
                                         m_lib_prefix,
                                         name,
                                         increment++ );
                    } while( m_lib_table.HasLibrary( nickname ) );
                }

                m_lib_table.InsertRow( new DESIGN_BLOCK_LIB_TABLE_ROW( nickname, libPath, wxT( "KiCad" ),
                                                                       wxEmptyString,
                                                                       _( "Added by Plugin and Content Manager" ) ) );
            }
        }

        return wxDIR_CONTINUE;
    }

private:
    DESIGN_BLOCK_LIB_TABLE& m_lib_table;
    wxString                m_path_prefix;
    wxString                m_lib_prefix;
    size_t                  m_prefix_dir_count;
};


bool DESIGN_BLOCK_LIB_TABLE::LoadGlobalTable( DESIGN_BLOCK_LIB_TABLE& aTable )
{
    bool       tableExists = true;
    wxFileName fn = GetGlobalTableFileName();

    if( !fn.FileExists() )
    {
        tableExists = false;

        if( !wxFileName::DirExists( fn.GetPath() )
            && !wxFileName::Mkdir( fn.GetPath(), 0x777, wxPATH_MKDIR_FULL ) )
        {
            THROW_IO_ERROR( wxString::Format( _( "Cannot create global library table path '%s'." ),
                                              fn.GetPath() ) );
        }

        // Attempt to copy the default global file table from the KiCad
        // template folder to the user's home configuration path.
        SEARCH_STACK ss;

        SystemDirsAppend( &ss );

        const ENV_VAR_MAP&      envVars = Pgm().GetLocalEnvVariables();
        std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( envVars, wxT( "TEMPLATE_DIR" ) );

        if( v && !v->IsEmpty() )
            ss.AddPaths( *v, 0 );

        wxString fileName = ss.FindValidPath( FILEEXT::DesignBlockLibraryTableFileName );

        // The fallback is to create an empty global design block table for the user to populate.
        if( fileName.IsEmpty() || !::wxCopyFile( fileName, fn.GetFullPath(), false ) )
        {
            DESIGN_BLOCK_LIB_TABLE emptyTable;

            emptyTable.Save( fn.GetFullPath() );
        }
    }

    aTable.clear();
    aTable.Load( fn.GetFullPath() );

    SETTINGS_MANAGER& mgr = Pgm().GetSettingsManager();
    KICAD_SETTINGS*   settings = mgr.GetAppSettings<KICAD_SETTINGS>( "kicad" );

    const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
    wxString           packagesPath;

    if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
        packagesPath = *v;

    if( settings->m_PcmLibAutoAdd )
    {
        // Scan for libraries in PCM packages directory

        wxFileName d( packagesPath, wxS( "" ) );
        d.AppendDir( wxS( "design_blocks" ) );

        if( d.DirExists() )
        {
            PCM_DESIGN_BLOCK_LIB_TRAVERSER traverser( packagesPath, aTable, settings->m_PcmLibPrefix );
            wxDir                          dir( d.GetPath() );

            dir.Traverse( traverser );
        }
    }

    if( settings->m_PcmLibAutoRemove )
    {
        // Remove PCM libraries that no longer exist
        std::vector<wxString> to_remove;

        for( size_t i = 0; i < aTable.GetCount(); i++ )
        {
            LIB_TABLE_ROW& row = aTable.At( i );
            wxString       path = row.GetFullURI( true );

            if( path.StartsWith( packagesPath ) && !wxDir::Exists( path ) )
                to_remove.push_back( row.GetNickName() );
        }

        for( const wxString& nickName : to_remove )
            aTable.RemoveRow( aTable.FindRow( nickName ) );
    }

    return tableExists;
}


DESIGN_BLOCK_LIB_TABLE& DESIGN_BLOCK_LIB_TABLE::GetGlobalLibTable()
{
    return GDesignBlockTable;
}


DESIGN_BLOCK_LIST_IMPL& DESIGN_BLOCK_LIB_TABLE::GetGlobalList()
{
    return GDesignBlockList;
}


wxString DESIGN_BLOCK_LIB_TABLE::GetGlobalTableFileName()
{
    wxFileName fn;

    fn.SetPath( PATHS::GetUserSettingsPath() );
    fn.SetName( FILEEXT::DesignBlockLibraryTableFileName );

    return fn.GetFullPath();
}
