/*
 * 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 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 <text_eval/text_eval_vcs.h>
#include <git/project_git_utils.h>
#include <git/git_backend.h>
#include <git/kicad_git_common.h>
#include <git/kicad_git_memory.h>
#include <string_utils.h>
#include <wx/string.h>
#include <wx/arrstr.h> // REQUIRED for wxString vector export on MSVC
#include <map>

namespace TEXT_EVAL_VCS
{
// Per-thread override that anchors repo-scoped queries to a specific path (for example the
// loaded project directory). When empty, repo discovery falls back to the process cwd.
namespace
{
    thread_local wxString tl_contextPath;
}


void SetContextPath( const wxString& aPath )
{
    tl_contextPath = aPath;
}


wxString GetContextPath()
{
    return tl_contextPath.IsEmpty() ? wxString( wxT( "." ) ) : tl_contextPath;
}


CONTEXT_PATH_SCOPE::CONTEXT_PATH_SCOPE( const wxString& aPath ) :
        m_previous( tl_contextPath )
{
    tl_contextPath = aPath;
}


CONTEXT_PATH_SCOPE::~CONTEXT_PATH_SCOPE()
{
    tl_contextPath = m_previous;
}


// Private implementation details
namespace
{
    // Resolve the effective path for repo discovery. Inputs of "." are replaced with the
    // current context path (which itself falls back to ".").
    wxString ResolveEffectivePath( const std::string& aPath )
    {
        if( aPath.empty() || aPath == "." )
            return GetContextPath();

        return wxString::FromUTF8( aPath );
    }


    git_repository* OpenRepo( const std::string& aPath )
    {
        if( !GetGitBackend() )
            return nullptr;

        const wxString effective = ResolveEffectivePath( aPath );
        return KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( TO_UTF8( effective ) );
    }

    void CloseRepo( git_repository* aRepo )
    {
        if( aRepo )
            git_repository_free( aRepo );
    }

    git_oid MakeZeroOid()
    {
        git_oid oid;
        git_oid_fromstrn( &oid, "0000000000000000000000000000000000000000", 40 );
        return oid;
    }

    git_oid GetFileCommit( git_repository* aRepo, const std::string& aPath )
    {
        if( !aRepo )
            return MakeZeroOid();

        // Get HEAD commit
        git_oid head_oid;

        if( git_reference_name_to_id( &head_oid, aRepo, "HEAD" ) != 0 )
            return MakeZeroOid();

        // For repo-level query (empty or "."), just return HEAD
        if( aPath.empty() || aPath == "." )
            return head_oid;

        // For file-specific query, walk history to find last commit that touched this file
        git_revwalk* walker = nullptr;

        if( git_revwalk_new( &walker, aRepo ) != 0 )
            return MakeZeroOid();

        git_revwalk_sorting( walker, GIT_SORT_TIME );
        git_revwalk_push( walker, &head_oid );

        // Walk through commits to find when the file was last modified
        git_oid result = MakeZeroOid();
        git_oid commit_oid;
        git_oid prev_blob_oid = MakeZeroOid();
        bool    first_commit = true;

        while( git_revwalk_next( &commit_oid, walker ) == 0 )
        {
            git_commit* commit = nullptr;

            if( git_commit_lookup( &commit, aRepo, &commit_oid ) != 0 )
                continue;

            // Get the tree for this commit
            git_tree* tree = nullptr;

            if( git_commit_tree( &tree, commit ) == 0 )
            {
                // Try to find the file in this tree
                git_tree_entry* entry = nullptr;

                if( git_tree_entry_bypath( &entry, tree, aPath.c_str() ) == 0 )
                {
                    const git_oid* blob_oid = git_tree_entry_id( entry );

                    if( first_commit )
                    {
                        // First time we see this file, remember its blob ID
                        git_oid_cpy( &prev_blob_oid, blob_oid );
                        git_oid_cpy( &result, &commit_oid );
                        first_commit = false;
                    }
                    else if( git_oid_cmp( blob_oid, &prev_blob_oid ) != 0 )
                    {
                        // File content changed - previous commit is where it changed
                        git_tree_entry_free( entry );
                        git_tree_free( tree );
                        git_commit_free( commit );
                        break;
                    }
                    else
                    {
                        // File unchanged, keep looking
                        git_oid_cpy( &result, &commit_oid );
                    }

                    git_tree_entry_free( entry );
                }
                else if( !first_commit )
                {
                    // File doesn't exist in this commit, but existed before
                    // So the previous commit is where it was added/last modified
                    git_tree_free( tree );
                    git_commit_free( commit );
                    break;
                }

                git_tree_free( tree );
            }

            git_commit_free( commit );
        }

        git_revwalk_free( walker );
        return result;
    }

    struct DescribeInfo
    {
        std::string tag;
        int         distance;
    };

    DescribeInfo GetDescribeInfo( const std::string& aMatch, bool aAnyTags )
    {
        git_repository* repo = OpenRepo( "." );

        if( !repo )
            return { std::string(), 0 };

        git_oid head_oid;

        if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
        {
            CloseRepo( repo );
            return { std::string(), 0 };
        }

        git_strarray tag_names;

        if( git_tag_list_match( &tag_names, aMatch.empty() ? "*" : aMatch.c_str(), repo ) != 0 )
        {
            CloseRepo( repo );
            return { std::string(), 0 };
        }

        // Build map of commit OID -> tag name upfront
        std::map<git_oid, std::string, decltype(
                [](const git_oid& a, const git_oid& b)
                {
                    return git_oid_cmp(&a, &b) < 0;
                } )> commit_to_tag;

        for( size_t i = 0; i < tag_names.count; ++i )
        {
            git_object* tag_obj = nullptr;

            if( git_revparse_single( &tag_obj, repo, tag_names.strings[i] ) == 0 )
            {
                git_object_t type = git_object_type( tag_obj );

                if( type == GIT_OBJECT_TAG )
                {
                    git_object* target = nullptr;

                    if( git_tag_peel( &target, (git_tag*) tag_obj ) == 0 )
                    {
                        commit_to_tag[*git_object_id( target )] = tag_names.strings[i];
                        git_object_free( target );
                    }
                }
                else if( aAnyTags && type == GIT_OBJECT_COMMIT )
                {
                    commit_to_tag[*git_object_id( tag_obj )] = tag_names.strings[i];
                }

                git_object_free( tag_obj );
            }
        }

        git_strarray_dispose( &tag_names );

        git_revwalk* walker = nullptr;

        if( git_revwalk_new( &walker, repo ) != 0 )
        {
            CloseRepo( repo );
            return { std::string(), 0 };
        }

        git_revwalk_sorting( walker, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME );
        git_revwalk_push( walker, &head_oid );

        DescribeInfo result{ std::string(), 0 };
        int          distance = 0;
        git_oid      commit_oid;

        while( git_revwalk_next( &commit_oid, walker ) == 0 )
        {
            auto it = commit_to_tag.find( commit_oid );

            if( it != commit_to_tag.end() )
            {
                result.tag = it->second;
                result.distance = distance;
                break;
            }

            distance++;
        }

        git_revwalk_free( walker );
        CloseRepo( repo );
        return result;
    }

    std::string GetCommitSignatureField( const std::string& aPath, bool aUseCommitter, bool aGetEmail )
    {
        git_repository* repo = OpenRepo( aPath );

        if( !repo )
            return std::string();

        git_oid oid = GetFileCommit( repo, aPath );

        if( git_oid_is_zero( &oid ) )
        {
            CloseRepo( repo );
            return std::string();
        }

        git_commit* commit = nullptr;
        std::string result;

        if( git_commit_lookup( &commit, repo, &oid ) == 0 )
        {
            const git_signature* sig = aUseCommitter ? git_commit_committer( commit ) : git_commit_author( commit );

            if( sig )
            {
                const char* field = aGetEmail ? sig->email : sig->name;

                if( field )
                    result = field;
            }

            git_commit_free( commit );
        }

        CloseRepo( repo );
        return result;
    }

} // anonymous namespace


std::string GetCommitHash( const std::string& aPath, int aLength )
{
    git_repository* repo = OpenRepo( aPath );

    if( !repo )
        return std::string();

    git_oid oid = GetFileCommit( repo, aPath );

    if( git_oid_is_zero( &oid ) )
    {
        CloseRepo( repo );
        return std::string();
    }

    int  length = std::max( 4, std::min( aLength, GIT_OID_HEXSZ ) );
    char hash[GIT_OID_HEXSZ + 1];
    git_oid_tostr( hash, length + 1, &oid );

    CloseRepo( repo );
    return hash;
}


std::string GetNearestTag( const std::string& aMatch, bool aAnyTags )
{
    return GetDescribeInfo( aMatch, aAnyTags ).tag;
}


int GetDistanceFromTag( const std::string& aMatch, bool aAnyTags )
{
    return GetDescribeInfo( aMatch, aAnyTags ).distance;
}


bool IsDirty( bool aIncludeUntracked )
{
    git_repository* repo = OpenRepo( "." );

    if( !repo )
        return false;

    git_status_list*   status = nullptr;
    git_status_options statusOpts;
    git_status_options_init( &statusOpts, GIT_STATUS_OPTIONS_VERSION );

    statusOpts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
    statusOpts.flags = aIncludeUntracked ? GIT_STATUS_OPT_INCLUDE_UNTRACKED : GIT_STATUS_OPT_EXCLUDE_SUBMODULES;

    bool isDirty = false;

    if( git_status_list_new( &status, repo, &statusOpts ) == 0 )
    {
        isDirty = git_status_list_entrycount( status ) > 0;
        git_status_list_free( status );
    }

    CloseRepo( repo );
    return isDirty;
}


std::string GetAuthor( const std::string& aPath )
{
    return GetCommitSignatureField( aPath, false, false );
}


std::string GetAuthorEmail( const std::string& aPath )
{
    return GetCommitSignatureField( aPath, false, true );
}


std::string GetCommitter( const std::string& aPath )
{
    return GetCommitSignatureField( aPath, true, false );
}


std::string GetCommitterEmail( const std::string& aPath )
{
    return GetCommitSignatureField( aPath, true, true );
}


std::string GetBranch()
{
    git_repository* repo = OpenRepo( "." );

    if( !repo )
        return std::string();

    KIGIT_COMMON common( repo );
    wxString     branchName = common.GetCurrentBranchName();

    CloseRepo( repo );
    return branchName.ToStdString();
}


int64_t GetCommitTimestamp( const std::string& aPath )
{
    git_repository* repo = OpenRepo( aPath );

    if( !repo )
        return 0;

    git_oid oid = GetFileCommit( repo, aPath );

    if( git_oid_is_zero( &oid ) )
    {
        CloseRepo( repo );
        return 0;
    }

    git_commit* commit = nullptr;
    int64_t     timestamp = 0;

    if( git_commit_lookup( &commit, repo, &oid ) == 0 )
    {
        timestamp = static_cast<int64_t>( git_commit_time( commit ) );
        git_commit_free( commit );
    }

    CloseRepo( repo );
    return timestamp;
}


std::string GetCommitDate( const std::string& aPath )
{
    int64_t timestamp = GetCommitTimestamp( aPath );
    return timestamp > 0 ? std::to_string( timestamp ) : std::string();
}

} // namespace TEXT_EVAL_VCS
