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

#include "kicad_git_common.h"

#include <wx/filename.h>
#include <wx/log.h>
#include <map>
#include <vector>

KIGIT_COMMON::KIGIT_COMMON( git_repository* aRepo ) :
        m_repo( aRepo ), m_connType( GIT_CONN_TYPE::GIT_CONN_LOCAL ), m_testedTypes( 0 )
{}

KIGIT_COMMON::~KIGIT_COMMON()
{}

git_repository* KIGIT_COMMON::GetRepo() const
{
    return m_repo;
}

wxString KIGIT_COMMON::GetCurrentBranchName() const
{
    git_reference* head = nullptr;

    int retval = git_repository_head( &head, m_repo );

    if( retval && retval != GIT_EUNBORNBRANCH && retval != GIT_ENOTFOUND )
        return wxEmptyString;

    git_reference *branch;

    if( git_reference_resolve( &branch, head ) )
    {
        git_reference_free( head );
        return wxEmptyString;
    }

    git_reference_free( head );
    const char* branchName = "";

    if( git_branch_name( &branchName, branch ) )
    {
        git_reference_free( branch );
        return wxEmptyString;
    }

    git_reference_free( branch );

    return branchName;
}


std::vector<wxString> KIGIT_COMMON::GetBranchNames() const
{
    std::vector<wxString> branchNames;
    std::map<git_time_t, wxString> branchNamesMap;
    wxString firstName;

    git_branch_iterator* branchIterator = nullptr;

    if( git_branch_iterator_new( &branchIterator, m_repo, GIT_BRANCH_LOCAL ) )
        return branchNames;

    git_reference* branchReference = nullptr;
    git_branch_t branchType;

    while( git_branch_next( &branchReference, &branchType, branchIterator ) != GIT_ITEROVER )
    {
        const char* branchName = "";

        if( git_branch_name( &branchName, branchReference ) )
            continue;

        const git_oid* commitId = git_reference_target( branchReference );

        git_commit* commit = nullptr;

        if( git_commit_lookup( &commit, m_repo, commitId ) )
            continue;

        git_time_t commitTime = git_commit_time( commit );

        if( git_branch_is_head( branchReference ) )
            firstName = branchName;
        else
            branchNamesMap.emplace( commitTime, branchName );

        git_commit_free( commit );
        git_reference_free( branchReference );
    }

    git_branch_iterator_free( branchIterator );

    // Add the current branch to the top of the list
    if( !firstName.IsEmpty() )
        branchNames.push_back( firstName );

    // Add the remaining branches in order from newest to oldest
    for( auto rit = branchNamesMap.rbegin(); rit != branchNamesMap.rend(); ++rit )
        branchNames.push_back( rit->second );

    return branchNames;
}


std::vector<wxString> KIGIT_COMMON::GetProjectDirs()
{
    std::vector<wxString> projDirs;

    git_oid oid;
    git_commit* commit;
    git_tree *tree;

    if( git_reference_name_to_id( &oid, m_repo, "HEAD" ) != GIT_OK )
    {
        wxLogError( "An error occurred: %s", git_error_last()->message );
        return projDirs;
    }

    if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
    {
        wxLogError( "An error occurred: %s", git_error_last()->message );
        return projDirs;
    }

    if( git_commit_tree( &tree, commit ) != GIT_OK )
    {
        wxLogError( "An error occurred: %s", git_error_last()->message );
        return projDirs;
    }

    // Define callback
    git_tree_walk(
            tree, GIT_TREEWALK_PRE,
            []( const char* root, const git_tree_entry* entry, void* payload )
            {
                std::vector<wxString>* prjs = static_cast<std::vector<wxString>*>( payload );
                wxFileName             root_fn( git_tree_entry_name( entry ) );

                root_fn.SetPath( root );

                if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB
                    && ( ( root_fn.GetExt() == "kicad_pro" ) || ( root_fn.GetExt() == "pro" ) ) )
                {
                    prjs->push_back( root_fn.GetFullPath() );
                }

                return 0; // continue walking
            },
            &projDirs );

    git_tree_free( tree );
    git_commit_free( commit );

    std::sort( projDirs.begin(), projDirs.end(),
               []( const wxString& a, const wxString& b )
               {
                    int a_freq = a.Freq( wxFileName::GetPathSeparator() );
                    int b_freq = b.Freq( wxFileName::GetPathSeparator() );

                    if( a_freq == b_freq )
                        return a < b;
                    else
                        return a_freq < b_freq;

               } );

    return projDirs;
}


std::pair<std::set<wxString>,std::set<wxString>> KIGIT_COMMON::GetDifferentFiles() const
{
    auto get_modified_files = [&]( git_oid* from_oid, git_oid* to_oid ) -> std::set<wxString>
    {
        std::set<wxString> modified_set;
        git_revwalk* walker = nullptr;

        if( git_revwalk_new( &walker, m_repo ) != GIT_OK )
            return modified_set;

        if( ( git_revwalk_push( walker, from_oid ) != GIT_OK )
            || ( git_revwalk_hide( walker, to_oid ) != GIT_OK ) )
        {
            git_revwalk_free( walker );
            return modified_set;
        }

        git_oid     oid;
        git_commit* commit;

        // iterate over all local commits not in remote
        while( git_revwalk_next( &oid, walker ) == GIT_OK )
        {
            if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
                continue;

            git_tree *tree, *parent_tree = nullptr;
            if( git_commit_tree( &tree, commit ) != GIT_OK )
            {
                git_commit_free( commit );
                continue;
            }

            // get parent commit tree to diff against
            if( !git_commit_parentcount( commit ) )
            {
                git_tree_free( tree );
                git_commit_free( commit );
                continue;
            }


            git_commit* parent;
            if( git_commit_parent( &parent, commit, 0 ) != GIT_OK )
            {
                git_tree_free( tree );
                git_commit_free( commit );
                continue;
            }


            if( git_commit_tree( &parent_tree, parent ) != GIT_OK )
            {
                git_tree_free( tree );
                git_commit_free( commit );
                git_commit_free( parent );
                continue;
            }


            git_diff*        diff;
            git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT;

            if( git_diff_tree_to_tree( &diff, m_repo, parent_tree, tree, &diff_opts ) == GIT_OK )
            {
                size_t num_deltas = git_diff_num_deltas( diff );

                for( size_t i = 0; i < num_deltas; ++i )
                {
                    const git_diff_delta* delta = git_diff_get_delta( diff, i );
                    modified_set.insert( delta->new_file.path );
                }

                git_diff_free( diff );
            }

            git_tree_free( parent_tree );
            git_commit_free( parent );
            git_tree_free( tree );
            git_commit_free( commit );
        }

        git_revwalk_free( walker );

        return modified_set;
    };

    std::pair<std::set<wxString>,std::set<wxString>> modified_files;

    if( !m_repo )
        return modified_files;

    git_reference* head = nullptr;
    git_reference* remote_head = nullptr;

    if( git_repository_head( &head, m_repo ) != GIT_OK )
        return modified_files;

    if( git_branch_upstream( &remote_head, head ) != GIT_OK )
    {
        git_reference_free( head );
        return modified_files;
    }

    git_oid head_oid = *git_reference_target( head );
    git_oid remote_oid = *git_reference_target( remote_head );

    git_reference_free( head );
    git_reference_free( remote_head );

    modified_files.first = get_modified_files( &head_oid, &remote_oid );
    modified_files.second = get_modified_files( &remote_oid, &head_oid );

    return modified_files;
}


bool KIGIT_COMMON::HasLocalCommits() const
{
    if( !m_repo )
        return false;

    git_reference* head = nullptr;
    git_reference* remote_head = nullptr;

    if( git_repository_head( &head, m_repo ) != GIT_OK )
        return false;

    if( git_branch_upstream( &remote_head, head ) != GIT_OK )
    {
        git_reference_free( head );
        return false;
    }

    git_oid head_oid = *git_reference_target( head );
    git_oid remote_oid = *git_reference_target( remote_head );

    git_reference_free( head );
    git_reference_free( remote_head );

    git_revwalk* walker = nullptr;

    if( git_revwalk_new( &walker, m_repo ) != GIT_OK )
        return false;

    if( ( git_revwalk_push( walker, &head_oid ) != GIT_OK )
        || ( git_revwalk_hide( walker, &remote_oid ) != GIT_OK ) )
    {
        git_revwalk_free( walker );
        return false;
    }

    git_oid oid;

    // If we can't walk to the next commit, then we are at or behind the remote
    if( git_revwalk_next( &oid, walker ) != GIT_OK )
    {
        git_revwalk_free( walker );
        return false;
    }

    git_revwalk_free( walker );
    return true;
}


bool KIGIT_COMMON::HasPushAndPullRemote() const
{
    git_remote* remote = nullptr;

    if( git_remote_lookup( &remote, m_repo, "origin" ) != GIT_OK )
    {
        return false;
    }

    // Get the URLs associated with the remote
    const char* fetch_url = git_remote_url( remote );
    const char* push_url = git_remote_pushurl( remote );

    // If no push URL is set, libgit2 defaults to using the fetch URL for pushing
    if( !push_url )
    {
        push_url = fetch_url;
    }

    // Clean up the remote object
    git_remote_free( remote );

    // Check if both URLs are valid (i.e., not NULL)
    return fetch_url && push_url;
}


extern "C" int fetchhead_foreach_cb( const char*, const char*,
                                     const git_oid* aOID, unsigned int aIsMerge, void* aPayload )
{
    if( aIsMerge )
        git_oid_cpy( (git_oid*) aPayload, aOID );

    return 0;
}


extern "C" void clone_progress_cb( const char* aStr, size_t aLen, size_t aTotal, void* data )
{
    KIGIT_COMMON* parent = (KIGIT_COMMON*) data;

    wxString progressMessage( aStr );
    parent->UpdateProgress( aLen, aTotal, progressMessage );
}


extern "C" int progress_cb( const char* str, int len, void* data )
{
    KIGIT_COMMON* parent = (KIGIT_COMMON*) data;

    wxString progressMessage( str, len );
    parent->UpdateProgress( 0, 0, progressMessage );

    return 0;
}

extern "C" int transfer_progress_cb( const git_transfer_progress* aStats, void* aPayload )
{
    KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload;
    wxString      progressMessage = wxString::Format( _( "Received %u of %u objects" ),
                                                      aStats->received_objects, aStats->total_objects );

    parent->UpdateProgress( aStats->received_objects, aStats->total_objects, progressMessage );

    return 0;
}

extern "C" int update_cb( const char* aRefname, const git_oid* aFirst, const git_oid* aSecond,
                          void* aPayload )
{
    constexpr int cstring_len = 8;
    char          a_str[cstring_len + 1];
    char          b_str[cstring_len + 1];

    KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload;
    wxString      status;

    git_oid_tostr( b_str, cstring_len, aSecond );

#if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 )
    if( !git_oid_is_zero( aFirst ) )
#else
    if( !git_oid_iszero( aFirst ) )
#endif
    {
        git_oid_tostr( a_str, cstring_len, aFirst );
        status = wxString::Format( _( "* [updated] %s..%s %s" ), a_str, b_str, aRefname );
    }
    else
    {
        status = wxString::Format( _( "* [new] %s %s" ), b_str, aRefname );
    }

    parent->UpdateProgress( 0, 0, status );

    return 0;
}


extern "C" int push_transfer_progress_cb( unsigned int aCurrent, unsigned int aTotal, size_t aBytes,
                                          void* aPayload )
{
    int64_t           progress = 100;
    KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload;

    if( aTotal != 0 )
    {
        progress = ( aCurrent * 100 ) / aTotal;
    }

    wxString progressMessage = wxString::Format( _( "Writing objects: %d%% (%d/%d), %d bytes" ),
                                                 progress, aCurrent, aTotal, aBytes );
    parent->UpdateProgress( aCurrent, aTotal, progressMessage );

    return 0;
}


extern "C" int push_update_reference_cb( const char* aRefname, const char* aStatus, void* aPayload )
{
    KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload;
    wxString      status( aStatus );

    if( !status.IsEmpty() )
    {
        wxString statusMessage = wxString::Format( _( "* [rejected] %s (%s)" ), aRefname, aStatus );
        parent->UpdateProgress( 0, 0, statusMessage );
    }
    else
    {
        wxString statusMessage = wxString::Format( _( "[updated] %s" ), aRefname );
        parent->UpdateProgress( 0, 0, statusMessage );
    }

    return 0;
}


extern "C" int credentials_cb( git_cred** aOut, const char* aUrl, const char* aUsername,
                                unsigned int aAllowedTypes, void* aPayload )
{
    KIGIT_COMMON* parent = static_cast<KIGIT_COMMON*>( aPayload );

    if( parent->GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL )
        return GIT_PASSTHROUGH;

    if( aAllowedTypes & GIT_CREDTYPE_USERNAME
        && !( parent->TestedTypes() & GIT_CREDTYPE_USERNAME ) )
    {
        wxString username = parent->GetUsername().Trim().Trim( false );
        git_cred_username_new( aOut, username.ToStdString().c_str() );
        parent->TestedTypes() |= GIT_CREDTYPE_USERNAME;
    }
    else if( parent->GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS
                && ( aAllowedTypes & GIT_CREDTYPE_USERPASS_PLAINTEXT )
                && !( parent->TestedTypes() & GIT_CREDTYPE_USERPASS_PLAINTEXT ) )
    {
        wxString username = parent->GetUsername().Trim().Trim( false );
        wxString password = parent->GetPassword().Trim().Trim( false );

        git_cred_userpass_plaintext_new( aOut, username.ToStdString().c_str(),
                                            password.ToStdString().c_str() );
        parent->TestedTypes() |= GIT_CREDTYPE_USERPASS_PLAINTEXT;
    }
    else if( parent->GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH
                && ( aAllowedTypes & GIT_CREDTYPE_SSH_KEY )
                && !( parent->TestedTypes() & GIT_CREDTYPE_SSH_KEY ) )
    {
        // SSH key authentication
        wxString sshKey = parent->GetSSHKey();
        wxString sshPubKey = sshKey + ".pub";
        wxString username = parent->GetUsername().Trim().Trim( false );
        wxString password = parent->GetPassword().Trim().Trim( false );

        git_cred_ssh_key_new( aOut, username.ToStdString().c_str(),
                                sshPubKey.ToStdString().c_str(),
                                sshKey.ToStdString().c_str(),
                                password.ToStdString().c_str() );
        parent->TestedTypes() |= GIT_CREDTYPE_SSH_KEY;
    }
    else
    {
        return GIT_PASSTHROUGH;
    }

    return GIT_OK;
};
