/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
 * @author Jon Evans <jon@craftyjon.com>
 *
 * 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/>.
 */

#ifndef LIBRARY_MANAGER_H
#define LIBRARY_MANAGER_H

#include <future>
#include <memory>
#include <shared_mutex>

#include <kicommon.h>
#include <libraries/library_table.h>
#include <io/io_base.h>


class LIBRARY_MANAGER;
class PROJECT;


/// Status of a library load managed by a library adapter
enum class LOAD_STATUS
{
    INVALID,
    LOADING,
    LOADED,
    LOAD_ERROR
};

/// The overall status of a loaded or loading library
struct KICOMMON_API LIB_STATUS
{
    LOAD_STATUS                  load_status = LOAD_STATUS::INVALID;
    std::optional<LIBRARY_ERROR> error;
};


/// Storage for an actual loaded library (including library content owned by the plugin)
struct KICOMMON_API LIB_DATA
{
    std::unique_ptr<IO_BASE> plugin;
    const LIBRARY_TABLE_ROW* row = nullptr;
    LIB_STATUS               status;

    int                   modify_hash = -1;
    std::vector<wxString> available_fields_cache;
};


/**
 * The interface used by the classes that actually can load IO plugins for the
 * different parts of KiCad and return concrete types (symbols, footprints, etc)
 */
class KICOMMON_API LIBRARY_MANAGER_ADAPTER
{
public:
    /**
     * Constructs a type-specific adapter into the library manager.  The code for these
     * generally resides in app-specific libraries (eeschema/pcbnew for example) rather than
     * being in kicommon.
     *
     * @param aManager should usually be Pgm().GetLibraryManager() except in QA tests
     */
    LIBRARY_MANAGER_ADAPTER( LIBRARY_MANAGER& aManager );

    virtual ~LIBRARY_MANAGER_ADAPTER();

    LIBRARY_MANAGER& Manager() const;

    /// The type of library table this adapter works with
    virtual LIBRARY_TABLE_TYPE Type() const = 0;

    /// Retrieves the global library table for this adapter type
    LIBRARY_TABLE* GlobalTable() const;

    /// Retrieves the project library table for this adapter type, or nullopt if one doesn't exist
    std::optional<LIBRARY_TABLE*> ProjectTable() const;

    std::optional<wxString> FindLibraryByURI( const wxString& aURI ) const;

    /// Returns a list of library nicknames that are available (skips any that failed to load)
    std::vector<wxString> GetLibraryNames() const;

    /**
     * Test for the existence of \a aNickname in the library tables.
     *
     * @param aCheckEnabled if true will only return true for enabled libraries
     * @return true if a library \a aNickname exists in the loaded tables.
     */
    bool HasLibrary( const wxString& aNickname, bool aCheckEnabled = false ) const;

    /// Deletes the given library from disk if it exists; returns true if deleted
    bool DeleteLibrary( const wxString& aNickname );

    std::optional<wxString> GetLibraryDescription( const wxString& aNickname ) const;

    /// Like LIBRARY_MANAGER::Rows but filtered to the LIBRARY_TABLE_TYPE of this adapter
    std::vector<LIBRARY_TABLE_ROW*> Rows( LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH,
                                          bool                aIncludeInvalid = false ) const;

    /// Like LIBRARY_MANAGER::GetRow but filtered to the LIBRARY_TABLE_TYPE of this adapter
    std::optional<LIBRARY_TABLE_ROW*> GetRow( const wxString&     aNickname,
                                              LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH ) const;

    /// Like LIBRARY_MANAGER::FindRowByURI but filtered to the LIBRARY_TABLE_TYPE of this adapter
    std::optional<LIBRARY_TABLE_ROW*> FindRowByURI( const wxString&     aUri,
                                                    LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH ) const;

    /// Notify the adapter that the active project has changed
    virtual void ProjectChanged();

    /// Notify the adapter that the global library tables have changed
    void GlobalTablesChanged( std::initializer_list<LIBRARY_TABLE_TYPE> aChangedTables = {} );

    /// Notify the adapter that the project library tables are about to be rebuilt.
    /// Mirrors GlobalTablesChanged: aborts any in-progress loads and invalidates
    /// cached LIB_DATA entries so raw LIBRARY_TABLE_ROW pointers do not dangle when
    /// the backing LIBRARY_TABLE objects are destroyed and rebuilt. Entries are
    /// reset in place (rather than erased) so their nicknames continue to mask
    /// same-named global libraries until the project scope is repopulated.
    void ProjectTablesChanged( std::initializer_list<LIBRARY_TABLE_TYPE> aChangedTables = {} );

    /// Complements ProjectTablesChanged by erasing project-scope cache entries
    /// whose nicknames no longer appear in the rebuilt project table. Must be
    /// called AFTER the new project table is loaded; otherwise removed libraries
    /// would be permanently masked by stale sentinels installed during the reset.
    void ProjectTablesReloaded( std::initializer_list<LIBRARY_TABLE_TYPE> aChangedTables = {} );

    void CheckTableRow( LIBRARY_TABLE_ROW& aRow );

    /// Loads all available libraries for this adapter type in the background
    void AsyncLoad();

    virtual std::optional<LIB_STATUS> LoadOne( LIB_DATA* aLib ) = 0;

    /// Returns async load progress between 0.0 and 1.0, or nullopt if load is not in progress
    std::optional<float> AsyncLoadProgress() const;

    void BlockUntilLoaded();

    /// Aborts any async load in progress; blocks until fully done aborting.
    /// This is the public interface to allow LIBRARY_MANAGER to abort loads before project changes.
    void AbortAsyncLoad();

    bool IsLibraryLoaded( const wxString& aNickname );

    /// Returns the status of a loaded library, or nullopt if the library hasn't been loaded (yet)
    std::optional<LIB_STATUS> GetLibraryStatus( const wxString& aNickname ) const;

    /// Returns a list of all library nicknames and their status (even if they failed to load)
    std::vector<std::pair<wxString, LIB_STATUS>> GetLibraryStatuses() const;

    /// Returns all library load errors as newline-separated strings for display
    wxString GetLibraryLoadErrors() const;

    void ReloadLibraryEntry( const wxString& aNickname,
                             LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH );

    /// Synchronously loads the named library to LOADED state. Returns the resulting status,
    /// or nullopt if the library is not found in any table.
    std::optional<LIB_STATUS> LoadLibraryEntry( const wxString& aNickname );

    /// Return true if the given nickname exists and is not a read-only library
    virtual bool IsWritable( const wxString& aNickname ) const;

    /// Creates the library (i.e. saves to disk) for the given row if it exists
    bool CreateLibrary( const wxString& aNickname );

    virtual bool SupportsConfigurationDialog( const wxString& aNickname ) const { return false; }

    virtual void ShowConfigurationDialog( const wxString& aNickname, wxWindow* aParent ) const {};

    virtual std::optional<LIBRARY_ERROR> LibraryError( const wxString& aNickname ) const;

protected:
    virtual std::map<wxString, LIB_DATA>& globalLibs() = 0;
    virtual std::map<wxString, LIB_DATA>& globalLibs() const = 0;
    virtual std::shared_mutex&            globalLibsMutex() = 0;
    virtual std::shared_mutex&            globalLibsMutex() const = 0;

    /// Override in derived class to perform library-specific enumeration.
    /// @param aUri is the pre-resolved library URI (must be resolved on the main thread
    ///             since URI expansion accesses PROJECT data that is not thread-safe).
    virtual void enumerateLibrary( LIB_DATA* aLib, const wxString& aUri ) = 0;

    static wxString getUri( const LIBRARY_TABLE_ROW* aRow );

    std::optional<const LIB_DATA*> fetchIfLoaded( const wxString& aNickname ) const;

    std::optional<LIB_DATA*> fetchIfLoaded( const wxString& aNickname );

    /// Fetches a loaded library, triggering a load of that library if it isn't loaded yet
    LIBRARY_RESULT<LIB_DATA*> loadIfNeeded( const wxString& aNickname );

    LIBRARY_RESULT<LIB_DATA*> loadFromScope( const wxString& aNickname,
                                             LIBRARY_TABLE_SCOPE aScope,
                                             std::map<wxString, LIB_DATA>& aTarget,
                                             std::shared_mutex& aMutex );

    /// Aborts any async load in progress; blocks until fully done aborting
    void abortLoad();

    /// Aborts pending loads and resets every project-scope cache entry in place
    /// (plugin and row cleared, status returned to INVALID) while preserving the
    /// nickname keys. Keeping the keys acts as a sentinel: fetchIfLoaded() finds
    /// the entry, sees it is no longer LOADED, and returns nullopt instead of
    /// falling through to globalLibs(), which is what preserves project-over-
    /// global shadowing across a project table reload. Shared between
    /// ProjectChanged() and ProjectTablesChanged() so both hooks stay in sync.
    void resetProjectCache();

    /// Creates a concrete plugin for the given row
    virtual LIBRARY_RESULT<IO_BASE*> createPlugin( const LIBRARY_TABLE_ROW* row ) = 0;

    virtual IO_BASE* plugin( const LIB_DATA* aRow ) = 0;

    LIBRARY_MANAGER& m_manager;

    // The actual library content is held in an associated IO plugin
    std::map<wxString, LIB_DATA> m_libraries;

    mutable std::shared_mutex m_librariesMutex;

    std::atomic_bool               m_abort;
    std::vector<std::future<void>> m_futures;

    std::atomic<size_t> m_loadCount{ 0 };
    std::atomic<size_t> m_loadTotal{ 0 };
    std::mutex          m_loadMutex;
};


class KICOMMON_API LIBRARY_MANAGER
{
public:
    LIBRARY_MANAGER();

    ~LIBRARY_MANAGER();

    LIBRARY_MANAGER( const LIBRARY_MANAGER& ) = delete;
    LIBRARY_MANAGER& operator=( const LIBRARY_MANAGER& ) = delete;

    static wxString DefaultGlobalTablePath( LIBRARY_TABLE_TYPE aType );

    static wxString StockTablePath( LIBRARY_TABLE_TYPE aType );

    static bool IsTableValid( const wxString& aPath );

    /// @return true if all required global tables are present on disk and valid
    static bool GlobalTablesValid();

    /// @return a list of global tables that are not valid (or an empty list if GlobalTablesValid() returns true)
    static std::vector<LIBRARY_TABLE_TYPE> InvalidGlobalTables();

    static bool CreateGlobalTable( LIBRARY_TABLE_TYPE aType, bool aPopulateDefaultLibraries );

    /// (Re)loads the global library tables in the given list, or all tables if no list is given
    void LoadGlobalTables( std::initializer_list<LIBRARY_TABLE_TYPE> aTablesToLoad = {} );

    /// (Re)loads the project library tables in the given list, or all tables if no list is given
    void LoadProjectTables( std::initializer_list<LIBRARY_TABLE_TYPE> aTablesToLoad = {} );

    void ReloadTables( LIBRARY_TABLE_SCOPE aScope, std::initializer_list<LIBRARY_TABLE_TYPE> aTablesToLoad = {} );

    /// Notify all adapters that the project has changed
    void ProjectChanged();

    /// Abort any async library loading operations in progress. This should be called before
    /// modifying the project list to prevent race conditions with background threads that
    /// access Prj().
    void AbortAsyncLoads();

    void RegisterAdapter( LIBRARY_TABLE_TYPE aType,
                          std::unique_ptr<LIBRARY_MANAGER_ADAPTER>&& aAdapter );

    bool RemoveAdapter( LIBRARY_TABLE_TYPE aType, LIBRARY_MANAGER_ADAPTER* aAdapter );

    std::optional<LIBRARY_MANAGER_ADAPTER*> Adapter( LIBRARY_TABLE_TYPE aType ) const;

    /**
     * Retrieves a given table; creating a new empty project table if a valid project is
     * loaded and the given table type doesn't exist in the project.
     * @param aType determines which type of table to return
     * @param aScope determines whether to return a global or project table
     * @return the given table if it exists (which should be the case unless a project table is
     *         requested and there is no valid project loaded)
     */
    std::optional<LIBRARY_TABLE*> Table( LIBRARY_TABLE_TYPE aType,
                                         LIBRARY_TABLE_SCOPE aScope );

    /**
     * Returns a flattened list of libraries of the given type
     * @param aType determines which type of libraries to return (symbol, footprint, ...)
     * @param aIncludeInvalid will include the nicknames of libraries even if they could not be
     *                        loaded for some reason (file not found, etc)
     * @return a list of library nicknames (the first part of a LIB_ID)
     */
    std::vector<LIBRARY_TABLE_ROW*> Rows( LIBRARY_TABLE_TYPE aType,
                                          LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH,
                                          bool aIncludeInvalid = false ) const;

    /**
     *
     * @param aType determines which type of libraries to return (symbol, footprint, ...)
     * @param aScope determines whether to search project, global, or both library tables
     * @param aNickname is the library nickname to retrieve
     * @return the row, or a nullopt if it does not exist
     */
    std::optional<LIBRARY_TABLE_ROW*> GetRow( LIBRARY_TABLE_TYPE aType, const wxString &aNickname,
                                              LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH );

    std::optional<LIBRARY_TABLE_ROW*> FindRowByURI( LIBRARY_TABLE_TYPE aType, const wxString &aUri,
                                                    LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH ) const;

    void ReloadLibraryEntry( LIBRARY_TABLE_TYPE aType, const wxString& aNickname,
                             LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH );

    /// Synchronously loads the named library to LOADED state for the given type.
    /// Returns the resulting status, or nullopt if the library is not found.
    std::optional<LIB_STATUS> LoadLibraryEntry( LIBRARY_TABLE_TYPE aType,
                                                const wxString& aNickname );

    void LoadProjectTables( const wxString& aProjectPath,
                            std::initializer_list<LIBRARY_TABLE_TYPE> aTablesToLoad = {} );

    /**
     * Return the full location specifying URI for the LIB, either in original UI form or
     * in environment variable expanded form.
     *
     * @param aType determines which tables will be searched for the library
     * @param aNickname is the library to look up
     * @param aSubstituted Tells if caller wanted the substituted form, else not.
     * @return the URI for the given library, or nullopt if the nickname is not a valid library
     */
    std::optional<wxString> GetFullURI( LIBRARY_TABLE_TYPE aType, const wxString& aNickname,
                                        bool aSubstituted = false );

    static wxString GetFullURI( const LIBRARY_TABLE_ROW* aRow, bool aSubstituted = false );

    static wxString ExpandURI( const wxString& aShortURI, const PROJECT& aProject );

    static bool UrisAreEquivalent( const wxString& aURI1, const wxString& aURI2 );

    /**
     * Return true if a library table row was added by the Plugin and Content Manager.
     *
     * PCM-managed rows are identified by the unexpanded URI template referencing the
     * versioned 3RD_PARTY env var (as produced by PCM_LIB_TRAVERSER). Matching on the
     * URI template, rather than on the expanded absolute path, prevents false positives
     * when a user library uses a different env var whose expanded path is a descendant
     * of the 3RD_PARTY directory (e.g. KICAD_USER_LIB pointing inside KICAD10_3RD_PARTY).
     */
    static bool IsPcmManagedRow( const LIBRARY_TABLE_ROW& aRow );

private:
    void loadTables( const wxString& aTablePath, LIBRARY_TABLE_SCOPE aScope,
                     std::vector<LIBRARY_TABLE_TYPE> aTablesToLoad = {} );

    void loadNestedTables( LIBRARY_TABLE& aTable );

    static wxString tableFileName( LIBRARY_TABLE_TYPE aType );

    void createEmptyTable( LIBRARY_TABLE_TYPE aType, LIBRARY_TABLE_SCOPE aScope );

    std::map<LIBRARY_TABLE_TYPE, std::unique_ptr<LIBRARY_TABLE>> m_tables;

    /// Map of full URI to table object for tables that are referenced by global or project tables
    std::map<wxString, std::unique_ptr<LIBRARY_TABLE>> m_childTables;

    // TODO: support multiple projects
    std::map<LIBRARY_TABLE_TYPE, std::unique_ptr<LIBRARY_TABLE>> m_projectTables;

    std::map<LIBRARY_TABLE_TYPE, std::unique_ptr<LIBRARY_MANAGER_ADAPTER>> m_adapters;

    mutable std::mutex m_adaptersMutex;

    typedef std::tuple<LIBRARY_TABLE_TYPE, LIBRARY_TABLE_SCOPE, wxString> ROW_CACHE_KEY;

    std::map<ROW_CACHE_KEY, LIBRARY_TABLE_ROW*> m_rowCache;
    mutable std::mutex                          m_rowCacheMutex;
};

#endif //LIBRARY_MANAGER_H
