/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2020 Jon Evans <jon@craftyjon.com>
 * Copyright (C) 2020 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 <regex>
#include <wx/debug.h>
#include <wx/filename.h>
#include <wx/stdpaths.h>
#include <wx/utils.h>

#include <build_version.h>
#include <confirm.h>
#include <dialogs/dialog_migrate_settings.h>
#include <gestfich.h>
#include <macros.h>
#include <settings/app_settings.h>
#include <settings/common_settings.h>
#include <settings/settings_manager.h>
#include <settings/color_settings.h>


/**
 * Flag to enable settings tracing
 * @ingroup trace_env_vars
 */
const char* traceSettings = "SETTINGS";


SETTINGS_MANAGER::SETTINGS_MANAGER() :
        m_common_settings( nullptr ),
        m_migration_source()
{
    // Check if the settings directory already exists, and if not, perform a migration if possible
    if( !MigrateIfNeeded() )
    {
        m_ok = false;
        return;
    }

    m_ok = true;

    // create the common settings shared by all applications.  Not loaded immediately
    m_common_settings =
            static_cast<COMMON_SETTINGS*>( RegisterSettings( new COMMON_SETTINGS, false ) );

    loadAllColorSettings();
}

SETTINGS_MANAGER::~SETTINGS_MANAGER()
{
    m_settings.clear();
    m_color_settings.clear();
}


JSON_SETTINGS* SETTINGS_MANAGER::RegisterSettings( JSON_SETTINGS* aSettings, bool aLoadNow )
{
    std::unique_ptr<JSON_SETTINGS> ptr( aSettings );

    wxLogTrace( traceSettings, "Registered new settings object %s", ptr->GetFilename() );

    if( aLoadNow )
        ptr->LoadFromFile( GetPathForSettingsFile( ptr.get() ) );

    m_settings.push_back( std::move( ptr ) );
    return m_settings.back().get();
}


void SETTINGS_MANAGER::Load()
{
    // TODO(JE) We should check for dirty settings here and write them if so, because
    // Load() could be called late in the application lifecycle

    for( auto&& settings : m_settings )
        settings->LoadFromFile( GetPathForSettingsFile( settings.get() ) );
}


void SETTINGS_MANAGER::Load( JSON_SETTINGS* aSettings )
{
    auto it = std::find_if( m_settings.begin(), m_settings.end(),
                            [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
                            {
                                return aPtr.get() == aSettings;
                            } );

    if( it != m_settings.end() )
        ( *it )->LoadFromFile( GetPathForSettingsFile( it->get() ) );
}


void SETTINGS_MANAGER::Save()
{
    for( auto&& settings : m_settings )
    {
        // Never automatically save color settings, caller should use SaveColorSettings
        if( dynamic_cast<COLOR_SETTINGS*>( settings.get() ) )
            continue;

        settings->SaveToFile( GetPathForSettingsFile( settings.get() ) );
    }
}


void SETTINGS_MANAGER::Save( JSON_SETTINGS* aSettings )
{
    auto it = std::find_if( m_settings.begin(), m_settings.end(),
                            [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
                            {
                                return aPtr.get() == aSettings;
                            } );

    if( it != m_settings.end() )
    {
        wxLogTrace( traceSettings, "Saving %s", ( *it )->GetFilename() );
        ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) );
    }
}


void SETTINGS_MANAGER::FlushAndRelease( JSON_SETTINGS* aSettings )
{
    auto it = std::find_if( m_settings.begin(), m_settings.end(),
                            [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
                            {
                                return aPtr.get() == aSettings;
                            } );

    if( it != m_settings.end() )
    {
        wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFilename() );
        ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) );
        m_settings.erase( it );
    }
}


COLOR_SETTINGS* SETTINGS_MANAGER::GetColorSettings( const wxString& aName )
{
    if( m_color_settings.count( aName ) )
        return m_color_settings.at( aName );

    COLOR_SETTINGS* ret = nullptr;

    if( !aName.empty() )
        ret = loadColorSettingsByName( aName );

    // This had better work
    if( !ret )
        ret = m_color_settings.at( "user" );

    return ret;
}


COLOR_SETTINGS* SETTINGS_MANAGER::loadColorSettingsByName( const wxString& aName )
{
    wxLogTrace( traceSettings, "Attempting to load color theme %s", aName );

    wxFileName fn( GetColorSettingsPath(), aName, "json" );

    if( !fn.IsOk() || !fn.Exists() )
    {
        wxLogTrace( traceSettings, "Theme file %s.json not found, falling back to user", aName );
        return nullptr;
    }

    auto cs = static_cast<COLOR_SETTINGS*>(
            RegisterSettings( new COLOR_SETTINGS( aName.ToStdString() ) ) );

    if( cs->GetFilename() != aName.ToStdString() )
    {
        wxLogTrace( traceSettings, "Warning: stored filename is actually %s, ", cs->GetFilename() );
    }

    m_color_settings[aName] = cs;

    return cs;
}


class COLOR_SETTINGS_LOADER : public wxDirTraverser
{
private:
    std::function<void( const wxString& )> m_action;

public:
    explicit COLOR_SETTINGS_LOADER( std::function<void( const wxString& )> aAction )
            : m_action( std::move( aAction ) )
    {
    }

    wxDirTraverseResult OnFile( const wxString& aFilePath ) override
    {
        wxFileName file( aFilePath );

        if( file.GetExt() != "json" )
            return wxDIR_CONTINUE;

        if( file.GetName() == "user" )
            return wxDIR_CONTINUE;

        m_action( file.GetName() );

        return wxDIR_CONTINUE;
    }

    wxDirTraverseResult OnDir( const wxString& dirPath ) override
    {
        return wxDIR_IGNORE;
    }
};


void SETTINGS_MANAGER::registerColorSettings( const wxString& aFilename )
{
    m_color_settings[aFilename] = static_cast<COLOR_SETTINGS*>(
            RegisterSettings( new COLOR_SETTINGS( aFilename.ToStdString() ) ) );
}


COLOR_SETTINGS* SETTINGS_MANAGER::AddNewColorSettings( const wxString& aFilename )
{
    wxString filename = aFilename;

    if( filename.EndsWith( wxT( ".json" ) ) )
        filename = filename.BeforeLast( '.' );

    registerColorSettings( filename );
    return m_color_settings[filename];
}


void SETTINGS_MANAGER::loadAllColorSettings()
{
    // Create the default color settings
    registerColorSettings( "user" );

    // Search for and load any other settings
    COLOR_SETTINGS_LOADER loader( [&]( const wxString& aFilename )
                                  {
                                      registerColorSettings( aFilename );
                                  } );

    wxDir colors_dir( GetColorSettingsPath() );

    if( colors_dir.IsOpened() )
        colors_dir.Traverse( loader );
}


void SETTINGS_MANAGER::ReloadColorSettings()
{
    m_color_settings.clear();
    loadAllColorSettings();
}


void SETTINGS_MANAGER::SaveColorSettings( COLOR_SETTINGS* aSettings, const std::string& aNamespace )
{
    // The passed settings should already be managed
    wxASSERT( std::find_if( m_color_settings.begin(), m_color_settings.end(),
                            [aSettings] ( const std::pair<wxString, COLOR_SETTINGS*>& el )
                            {
                                return el.second->GetFilename() == aSettings->GetFilename();
                            }
                            ) != m_color_settings.end() );

    nlohmann::json::json_pointer ptr = JSON_SETTINGS::PointerFromString( aNamespace );

    aSettings->Store();

    wxASSERT( aSettings->contains( ptr ) );

    wxLogTrace( traceSettings, "Saving color scheme %s, preserving %s", aSettings->GetFilename(),
            aNamespace );

    nlohmann::json backup = aSettings->at( ptr );
    std::string path = GetColorSettingsPath();

    aSettings->LoadFromFile( path );

    ( *aSettings )[ptr].update( backup );
    aSettings->Load();

    aSettings->SaveToFile( path );
}


std::string SETTINGS_MANAGER::GetPathForSettingsFile( JSON_SETTINGS* aSettings )
{
    wxASSERT( aSettings );

    switch( aSettings->GetLocation() )
    {
    case SETTINGS_LOC::USER:
        return GetUserSettingsPath();

    case SETTINGS_LOC::PROJECT:
        // TODO(JE)
        return "";

    case SETTINGS_LOC::COLORS:
        return GetColorSettingsPath();

    default:
        wxASSERT_MSG( false, "Unknown settings location!" );
    }

    return "";
}


class MIGRATION_TRAVERSER : public wxDirTraverser
{
private:
    wxString m_src;
    wxString m_dest;
    wxString m_errors;

public:
    MIGRATION_TRAVERSER( const wxString& aSrcDir, const wxString& aDestDir ) :
            m_src( aSrcDir ), m_dest( aDestDir )
    {
    }

    wxString GetErrors() { return m_errors; }

    virtual wxDirTraverseResult OnFile( const wxString& aSrcFilePath ) override
    {
        wxFileName file( aSrcFilePath );
        wxString path = file.GetPath();

        path.Replace( m_src, m_dest, false );
        file.SetPath( path );

        wxLogTrace( traceSettings, "Copying %s to %s", aSrcFilePath, file.GetFullPath() );

        // For now, just copy everything
        CopyFile( aSrcFilePath, file.GetFullPath(), m_errors );

        return wxDIR_CONTINUE;
    }

    virtual wxDirTraverseResult OnDir( const wxString& dirPath ) override
    {
        wxFileName dir( dirPath );

        // Whitelist of directories to migrate
        if( dir.GetName() == "colors" ||
            dir.GetName() == "3d" )
        {

            wxString path = dir.GetPath();

            path.Replace( m_src, m_dest, false );
            dir.SetPath( path );

            wxMkdir( dir.GetFullPath() );

            return wxDIR_CONTINUE;
        }
        else
        {
            return wxDIR_IGNORE;
        }
    }
};


bool SETTINGS_MANAGER::MigrateIfNeeded()
{
    wxFileName path( GetUserSettingsPath(), "" );
    wxLogTrace( traceSettings, "Using settings path %s", path.GetFullPath() );

    if( path.DirExists() )
    {
        wxFileName common = path;
        common.SetName( "kicad_common" );
        common.SetExt( "json" );

        if( common.Exists() )
        {
            wxLogTrace( traceSettings, "Path exists and has a kicad_common, continuing!" );
            return true;
        }
    }

    // Now we have an empty path, let's figure out what to put in it
    DIALOG_MIGRATE_SETTINGS dlg( this );

    if( dlg.ShowModal() != wxID_OK )
    {
        wxLogTrace( traceSettings, "Migration dialog canceled; exiting" );
        return false;
    }

    if( !path.DirExists() )
    {
        wxLogTrace( traceSettings, "Path didn't exist; creating it" );
        path.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
    }

    if( m_migration_source.IsEmpty() )
    {
        wxLogTrace( traceSettings, "No migration source given; starting with defaults" );
        return true;
    }

    MIGRATION_TRAVERSER traverser( m_migration_source, path.GetFullPath() );
    wxDir source_dir( m_migration_source );

    source_dir.Traverse( traverser );

    if( !traverser.GetErrors().empty() )
        DisplayErrorMessage( nullptr, traverser.GetErrors() );

    return true;
}


bool SETTINGS_MANAGER::GetPreviousVersionPaths( std::vector<wxString>* aPaths )
{
    wxASSERT( aPaths );

    aPaths->clear();

    wxDir dir;
    std::vector<wxFileName> base_paths;

    base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false ), "" ) );

    // If the env override is set, also check the default paths
    if( wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), nullptr ) )
        base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false, false ), "" ) );

    wxString subdir;
    std::string mine = GetSettingsVersion();

    auto check_dir = [&] ( const wxString& aSubDir )
    {
        // Only older versions are valid for migration
        if( compareVersions( aSubDir.ToStdString(), mine ) <= 0 )
        {
            wxString sub_path = dir.GetNameWithSep() + aSubDir;

            if( IsSettingsPathValid( sub_path ) )
            {
                aPaths->push_back( sub_path );
                wxLogTrace( traceSettings, "GetPreviousVersionName: %s is valid", sub_path );
            }
        }
    };

    for( auto base_path : base_paths )
    {
        if( !dir.Open( base_path.GetFullPath() ) )
        {
            wxLogTrace( traceSettings, "GetPreviousVersionName: could not open base path %s",
                    base_path.GetFullPath() );
            continue;
        }

        wxLogTrace( traceSettings, "GetPreviousVersionName: checking base path %s",
            base_path.GetFullPath() );

        if( dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS ) )
        {
            if( subdir != mine )
                check_dir( subdir );

            while( dir.GetNext( &subdir ) )
            {
                if( subdir != mine )
                    check_dir( subdir );
            }
        }

        // If we didn't find one yet, check for legacy settings without a version directory
        if( IsSettingsPathValid( dir.GetNameWithSep() ) )
        {
            wxLogTrace( traceSettings,
                    "GetPreviousVersionName: root path %s is valid", dir.GetName() );
            aPaths->push_back( dir.GetName() );
        }
    }

    return aPaths->size() > 0;
}


bool SETTINGS_MANAGER::IsSettingsPathValid( const wxString& aPath )
{
    wxFileName test( aPath, "kicad_common" );
    return test.Exists();
}


std::string SETTINGS_MANAGER::GetColorSettingsPath()
{
    wxFileName path;

    path.AssignDir( GetUserSettingsPath() );
    path.AppendDir( "colors" );

    return path.GetPath().ToStdString();
}


std::string SETTINGS_MANAGER::GetUserSettingsPath()
{
    static std::string user_settings_path;

    if( user_settings_path.empty() )
        user_settings_path = calculateUserSettingsPath();

    return user_settings_path;
}


std::string SETTINGS_MANAGER::calculateUserSettingsPath( bool aIncludeVer, bool aUseEnv )
{
    wxFileName cfgpath;

    // http://docs.wxwidgets.org/3.0/classwx_standard_paths.html#a7c7cf595d94d29147360d031647476b0
    cfgpath.AssignDir( wxStandardPaths::Get().GetUserConfigDir() );

    // GetUserConfigDir() does not default to ~/.config which is the current standard
    // configuration file location on Linux.  This has been fixed in later versions of wxWidgets.
#if !defined( __WXMSW__ ) && !defined( __WXMAC__ )
    wxArrayString dirs = cfgpath.GetDirs();

    if( dirs.Last() != ".config" )
        cfgpath.AppendDir( ".config" );
#endif

    wxString envstr;

    // This shouldn't cause any issues on Windows or MacOS.
    if( wxGetEnv( wxT( "XDG_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() )
    {
        // Override the assignment above with XDG_CONFIG_HOME
        cfgpath.AssignDir( envstr );
    }

    cfgpath.AppendDir( TO_STR( KICAD_CONFIG_DIR ) );

    // Use KICAD_CONFIG_HOME to allow the user to force a specific configuration path.
    if( aUseEnv && wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() )
    {
        // Override the assignment above with KICAD_CONFIG_HOME
        cfgpath.AssignDir( envstr );
    }

    if( aIncludeVer )
        cfgpath.AppendDir( GetSettingsVersion() );

    return cfgpath.GetPath().ToStdString();
}


std::string SETTINGS_MANAGER::GetSettingsVersion()
{
    // CMake computes the major.minor string for us.
    return GetMajorMinorVersion().ToStdString();
}


int SETTINGS_MANAGER::compareVersions( const std::string& aFirst, const std::string& aSecond )
{
    int a_maj = 0;
    int a_min = 0;
    int b_maj = 0;
    int b_min = 0;

    if( !extractVersion( aFirst, &a_maj, &a_min ) || !extractVersion( aSecond, &b_maj, &b_min ) )
    {
        wxLogTrace( traceSettings, "compareSettingsVersions: bad input (%s, %s)", aFirst, aSecond );
        return -1;
    }

    if( a_maj < b_maj )
    {
        return -1;
    }
    else if( a_maj > b_maj )
    {
        return 1;
    }
    else
    {
        if( a_min < b_min )
        {
            return -1;
        }
        else if( a_min > b_min )
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }
}


bool SETTINGS_MANAGER::extractVersion( const std::string& aVersionString, int* aMajor, int* aMinor )
{
    std::regex  re_version( "(\\d+)\\.(\\d+)" );
    std::smatch match;

    if( std::regex_match( aVersionString, match, re_version ) )
    {
        try
        {
            *aMajor = std::stoi( match[1].str() );
            *aMinor = std::stoi( match[2].str() );
        }
        catch( ... )
        {
            return false;
        }

        return true;
    }

    return false;
}
