/*
 * 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 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
 */

/**
 * File locking utilities
 * @file lockfile.h
 */

#ifndef INCLUDE__LOCK_FILE_H_
#define INCLUDE__LOCK_FILE_H_

#include <wx/wx.h>
#include <wx/file.h>
#include <wx/filefn.h>
#include <wx/log.h>
#include <wx/filename.h>
#include <nlohmann/json.hpp>

#define LCK "KICAD_LOCKING"

class LOCKFILE
{
public:
    LOCKFILE( const wxString &filename, bool aRemoveOnRelease = true ) :
            m_originalFile( filename ), m_fileCreated( false ), m_status( false ),
            m_removeOnRelease( aRemoveOnRelease ), m_errorMsg( "" )
    {
        if( filename.IsEmpty() )
            return;

        wxLogTrace( LCK, "Trying to lock %s", filename );
        wxFileName fn( filename );
        fn.SetName( "~" + fn.GetName() );
        fn.SetExt( fn.GetExt() + ".lck" );

        if( !fn.IsDirWritable() )
        {
            wxLogTrace( LCK, "File is not writable: %s", filename );
            m_status = true;
            m_removeOnRelease = false;
            return;
        }

        m_lockFilename = fn.GetFullPath();

        wxFile file;
        try
        {
            bool lock_success = false;
            bool rw_success = false;

            {
                wxLogNull suppressExpectedErrorMessages;

                lock_success = file.Open( m_lockFilename, wxFile::write_excl );

                if( !lock_success )
                    rw_success = file.Open( m_lockFilename, wxFile::read );
            }

            if( lock_success )
            {
                // Lock file doesn't exist, create one
                m_fileCreated = true;
                m_status = true;
                m_username = wxGetUserId();
                m_hostname = wxGetHostName();
                nlohmann::json j;
                j["username"] = std::string( m_username.mb_str() );
                j["hostname"] = std::string( m_hostname.mb_str() );
                std::string lock_info = j.dump();
                file.Write( lock_info );
                file.Close();
                wxLogTrace( LCK, "Locked %s", filename );
            }
            else if( rw_success )
            {
                // Lock file already exists, read the details
                wxString lock_info;
                file.ReadAll( &lock_info );
                nlohmann::json j = nlohmann::json::parse( std::string( lock_info.mb_str() ) );
                m_username = wxString( j["username"].get<std::string>() );
                m_hostname = wxString( j["hostname"].get<std::string>() );
                file.Close();
                m_errorMsg = _( "Lock file already exists" );
                wxLogTrace( LCK, "Existing Lock for %s", filename );
            }
            else
            {
                throw std::runtime_error( "Failed to open lock file" );
            }
        }
        catch( std::exception& e )
        {
            wxLogError( "Got an error trying to lock %s: %s", filename, e.what() );

            // Delete lock file if it was created above but we threw an exception somehow
            if( m_fileCreated )
            {
                wxRemoveFile( m_lockFilename );
                m_fileCreated = false; // Reset the flag since file has been deleted manually
            }

            m_errorMsg = _( "Failed to access lock file" );
            m_status = false;
        }
    }

    ~LOCKFILE()
    {
        UnlockFile();
    }

    /**
     * Unlocks and removes the file from the filesystem as long as we still own it.
     */
    void UnlockFile()
    {
        wxLogTrace( LCK, "Unlocking %s", m_lockFilename );

        // Delete lock file only if the file was created in the constructor and if the file contains the correct user and host names
        if( m_fileCreated && checkUserAndHost() )
        {
            if( m_removeOnRelease )
                wxRemoveFile( m_lockFilename );

            m_fileCreated = false; // Reset the flag since file has been deleted manually
            m_status = false;
            m_errorMsg = wxEmptyString;
        }
    }

    /**
     * Forces the lock, overwriting the data that existed already
     * @return True if we successfully overrode the lock
     */
    bool OverrideLock( bool aRemoveOnRelease = true )
    {
        wxLogTrace( LCK, "Overriding lock on %s", m_lockFilename );

        if( !m_fileCreated )
        {
            try
            {
                wxFile file;
                bool success = false;

                {
                    wxLogNull suppressExpectedErrorMessages;
                    success = file.Open( m_lockFilename, wxFile::write );
                }

                if( success )
                {
                    m_username = wxGetUserId();
                    m_hostname = wxGetHostName();
                    nlohmann::json j;
                    j["username"] = std::string( m_username.mb_str() );
                    j["hostname"] = std::string( m_hostname.mb_str() );
                    std::string lock_info = j.dump();
                    file.Write( lock_info );
                    file.Close();
                    m_fileCreated = true;
                    m_status = true;
                    m_removeOnRelease = aRemoveOnRelease;
                    m_errorMsg = wxEmptyString;
                    wxLogTrace( LCK, "Successfully overrode lock on %s", m_lockFilename );
                    return true;
                }

                return false;
            }
            catch( std::exception& e )
            {
                wxLogError( "Got exception trying to override lock on %s: %s",
                        m_lockFilename, e.what() );

                return false;
            }
        }
        else
        {
            wxLogTrace( LCK, "Upgraded lock on %s to delete on release", m_lockFilename );
            m_removeOnRelease = aRemoveOnRelease;
        }

        return true;
    }

    bool IsLockedByMe()
    {
        return m_username == wxGetUserId() && m_hostname == wxGetHostName();
    }

    /**
     * @return Current username.  If we own the lock, this is us.  Otherwise, this is the user that does own it
     */
    wxString GetUsername(){ return m_username; }

    /**
     * @return Current hostname.  If we own the lock this is our computer.  Otherwise, this is the computer that does
     */
    wxString GetHostname(){ return m_hostname; }

    /**
     * @return Last error message generated
     */
    wxString GetErrorMsg(){ return m_errorMsg; }

    bool Locked() const
    {
        return m_fileCreated;
    }

    bool Valid() const
    {
        return m_status;
    }

    explicit operator bool() const
    {
        return m_status;
    }

private:
    wxString m_originalFile;
    wxString m_lockFilename;
    wxString m_username;
    wxString m_hostname;
    bool m_fileCreated;
    bool m_status;
    bool m_removeOnRelease;
    wxString m_errorMsg;

    bool checkUserAndHost()
    {
        wxFileName fileName( m_lockFilename );

        if( !fileName.FileExists() )
        {
            wxLogTrace
            ( LCK, "File does not exist: %s", m_lockFilename );
            return false;
        }

        wxFile file;
        try
        {
            if( file.Open( m_lockFilename, wxFile::read ) )
            {
                wxString lock_info;
                file.ReadAll( &lock_info );
                nlohmann::json j = nlohmann::json::parse( std::string( lock_info.mb_str() ) );
                if( m_username == wxString( j["username"].get<std::string>() )
                        && m_hostname == wxString( j["hostname"].get<std::string>() ) )
                {
                    wxLogTrace
                        ( LCK, "User and host match for lock %s", m_lockFilename );
                    return true;
                }
            }
        }
        catch( std::exception &e )
        {
            wxLogError
                ( "Got exception trying to check user/host for lock on %s: %s", m_lockFilename,
                    e.what() );
        }

        wxLogTrace
            ( LCK, "User and host DID NOT match for lock %s", m_lockFilename );
        return false;
    }
};


#endif  // INCLUDE__LOCK_FILE_H_
