/*
 * LibrePCB - Professional EDA for everyone!
 * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
 * https://librepcb.org/
 *
 * 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 LIBREPCB_DIRECTORYLOCK_H
#define LIBREPCB_DIRECTORYLOCK_H

/*******************************************************************************
 *  Includes
 ******************************************************************************/
#include "../exceptions.h"
#include "filepath.h"

#include <QtCore>

/*******************************************************************************
 *  Namespace / Forward Declarations
 ******************************************************************************/
namespace librepcb {

/*******************************************************************************
 *  Class DirectoryLock
 ******************************************************************************/

/**
 * @brief This class can be used to implement file-based directory locks
 *
 * Many classes of this project open some directories (workspaces, projects,
 * library elements, ...). But it's very dangerous if a directory is opened
 * multiple times simultaneously (by the same or another instance of the
 * application, maybe even on different computers if the directories are located
 * on a network drive). To avoid such problems, this class provides a mechanism
 * to create directory locks.
 *
 *
 * <b>How such a directory lock works:</b>
 *
 * Let's say that you want to open the directory "/foo/bar/". Then a lock file
 * with the filepath "/foo/bar/.lock" will be created. After closing the
 * directory, the lock file will be removed. So, while the directory (e.g. a
 * project) is open, there will be a lock file in the same directory. If the
 * same or another instance of the application now wants to open the same
 * directory at the same time, the lock file is detected and opening the
 * directory will be denied.
 *
 * The lock file is a simple UTF-8 encoded text file with 5 lines with following
 * values:
 *  -# The full name (first name + last name) of the user which holds the lock
 *  -# The username (logon name) of the user which holds the lock
 *  -# The hostname of the user's computer which holds the lock
 *  -# The process id (PID) of the application instance which holds the lock
 *  -# The process name of the application instance which holds the lock
 *  -# The datetime when the lock file was created/updated (UTC and ISO format!)
 *
 * Example:
 * @code
 * Homer Simpson
 * homer
 * homer-workstation
 * 1234
 * librepcb
 * 2013-04-13T12:43:52Z
 * @endcode
 *
 * The lock file (and especially its content) is also used to detect application
 * crashes. If the application crashes while a directory was locked, the lock
 * file will still exist after the application crashed. Now, if the user
 * tries to open the locked directory again, the content of the lock file will
 * be parsed. If the username and the hostname in the lock file is equal to the
 * current user which tries to get the lock, it's clear that the lock file does
 * NOT exist because the locked directory is already open, but that the
 * application crashed while the directory was locked. If there exists a
 * backup of the locked directory (e.g. project auto-save), this allows to ask
 * the user whether the backup should be restored or not.
 *
 *
 * <b>How to use this class:</b>
 *
 * First, you need to create an instance of this class for the directory you
 * want to protect with a lock. There are two different constructors for this
 * purpose. If you use the default constructor, you need to call #setDirToLock()
 * afterwards. Now you can read the lock status of the specified directory with
 * #getStatus(). With #lock() you can create the lock file, and with #unlock()
 * you can remove the lock file. There are also the two convenience methods
 * #tryLock() and #unlockIfLocked(), just read their documentation for more
 * information.
 *
 * @note    The destructor will automatically call #unlockIfLocked().
 *          This allows a reliable implementation of a directory lock, because
 * you can add a DirectoryLock instance to the attributes of your class which
 * access a directory which should be locked. This will ensure that the lock
 * will be released when your object gets destroyed (RAII). See the code example
 * below.
 *
 * Code Example:
 * @code
 *  class MyDirectoryOpeningClass // a class which opens a directory and needs
 * to lock it
 *  {
 *      public:
 *          MyDirectoryOpeningClass() // constructor
 *              : myLock(FilePath("C:/myDirectory")) // variant 1 to set the
 * filepath
 *          {
 *              myLock.setDirToLock(FilePath("C:/myDirectory")); // variant 2
 *              switch (myLock.getStatus()) // Note: this line can throw an
 * exception!
 *              {
 *                  case Unlocked:
 *                      // No lock exists --> lock the directory now
 *                      myLock.lock(); // Note: this line can throw an
 * exception! break; case Locked:
 *                      // The directory is already locked by another instance!
 *                      throw Exception("Directory is locked!");
 *                      break;
 *                  case StaleLock:
 *                      // The application crashed while the lock was
 * active.
 *                      // Ask the user whether a backup should be restored or
 * not. break; default:
 *                      // Should not happen...
 *                      break;
 *              }
 *              // if you don't care about stale locks, you could just do this
 * instead:
 *              // myLock.tryLock(); // Note: this line can throw an exception!
 *
 *          }
 *          ~MyDirectoryOpeningClass() // destructor
 *          {
 *              // You do not have to (but you could) call
 * myLock.unlockIfLocked(),
 *              // as it will be called automatically in the destructor of
 * myLock.
 *              // try { myLock.unlockIfLocked(); } catch (...) { }
 *          }
 *
 *      private:
 *          DirectoryLock myLock; // an instance, not only a pointer (important
 * for RAII)!
 *  };
 * @endcode
 */
class DirectoryLock final {
  Q_DECLARE_TR_FUNCTIONS(DirectoryLock)

public:
  // Types

  /**
   * @brief The return type of #getStatus()
   */
  enum class LockStatus {
    Unlocked,  ///< the directory is not locked (lock file does not exist)
    Locked,  ///< the directory is locked by another application instance
    StaleLock  ///< the directory is locked by a crashed application instance
  };

  // Constructors / Destructor

  /**
   * @brief The default constructor
   *
   * @warning     If you use this constructor, you need to call #setDirToLock()
   *              afterwards (before calling any other method of this class)!
   */
  DirectoryLock() noexcept;

  /**
   * @brief Copy constructor
   *
   * @param other     The object to copy
   */
  DirectoryLock(const DirectoryLock& other) = delete;

  /**
   * @brief A constructor which will call #setDirToLock()
   *
   * @param dir       See #setDirToLock()
   */
  explicit DirectoryLock(const FilePath& dir) noexcept;

  /**
   * @brief The destructor (this may also unlock the locked file)
   *
   * @note    The destructor will also try to unlock the directory if it was
   * locked with this object.
   */
  ~DirectoryLock() noexcept;

  // Setters

  /**
   * @brief Specify the directory for which you need the lock
   *
   * @param dir      The filepath to the directory to lock
   *
   * @warning This method must not be called when this object already holds a
   * lock!
   */
  void setDirToLock(const FilePath& dir) noexcept;

  // Getters

  /**
   * @brief Get the filepath of the directory to lock (passed by
   * #setDirToLock())
   *
   * @return The filepath to the directory to lock (invalid if no filepath was
   * set)
   */
  const FilePath& getDirToLock() const noexcept { return mDirToLock; }

  /**
   * @brief Get the filepath of the lock file (NOT the directory to lock!)
   *
   * @return The filepath to the lock file (invalid if no valid filepath was
   * set)
   */
  const FilePath& getLockFilepath() const noexcept { return mLockFilePath; }

  /**
   * @brief Get the lock status of the specified directory
   *
   * @return  The current lock status (see #LockStatus)
   *
   * @throw   Exception on error (e.g. invalid filepath, no access rights, ...)
   */
  LockStatus getStatus() const;

  // General Methods

  /**
   * @brief Lock the specified directory if not already locked
   *
   * This is a save method to get a lock without the need for first reading the
   * lock status with #getStatus(). Depending on the lock status, this method
   * does following:
   * - Unlocked:  Set "wasStale = false" and get the lock (calling #lock())
   * - StaleLock: Set "wasStale = true" and get the lock (calling #lock())
   * - Locked:    Throw exception (something like "Directory already locked")
   *
   * @param wasStale  This variable will be set to true if there was a stale
   * lock, and to false if not (if a valid pointer was passed).
   *
   * @throw   Exception on error (e.g. already locked, no access rights, ...)
   */
  void tryLock(bool* wasStale = nullptr);

  /**
   * @brief Unlock the specified directory if it was locked by this object
   *
   * If the specified directory is locked by this object, this method calls
   * #unlock(). Otherwise this method does nothing.
   *
   * @return  True if the lock has been released by this object, false
   * otherwise.
   *
   * @throw   Exception on error (e.g. invalid filepath, no access rights, ...)
   */
  bool unlockIfLocked();

  /**
   * @brief Lock the specified directory (create/update the lock file)
   *
   * @warning This method will always overwrite an already existing lock file,
   * even if it was created by another application instance! So: Always check
   *          the lock status first with #getStatus(), or use #tryLock()
   * instead!
   *
   * @throw   Exception on error (e.g. invalid filepath, no access rights, ...)
   */
  void lock();

  /**
   * @brief Unlock the specified directory (remove the lock file)
   *
   * @warning This method will always remove an existing lock file, even if it
   * was created by another application instance! So: Always check the lock
   *          status first with #getStatus(), or use #unlockIfLocked() instead!
   *
   * @throw   Exception on error (e.g. invalid filepath, no access rights, ...)
   */
  void unlock();

  // Operator Overloadings
  DirectoryLock& operator=(const DirectoryLock& rhs) = delete;

private:  // Data
  /**
   * @brief The filepath to the directory to lock (passed by #setDirToLock())
   */
  FilePath mDirToLock;

  /**
   * @brief The filepath to the lock file
   *
   * Example: If the filepath "/foo/bar" was passed to #setDirToLock(),
   *          this attribute will have the value "/foo/bar/.lock".
   */
  FilePath mLockFilePath;

  /**
   * @brief This attribute defines if the lock is active by this object
   *
   * If #lock() was called successfully, mLockedByThisObject is set to true.
   * If #unlock() was called successfully, mLockedByThisObject is set to false.
   *
   * In other words: This attribute is true while this object has the ownership
   * over the lock file (between calling #lock() and #unlock()).
   *
   * The only goal of this attrubute is to decide whether the destructor should
   * remove the lock or not. If the destructor is called while this attribute is
   * true, the destructor will call #unlock() to remove the file lock.
   */
  bool mLockedByThisObject;
};

/*******************************************************************************
 *  End of File
 ******************************************************************************/

}  // namespace librepcb

#endif  // LIBREPCB_DIRECTORYLOCK_H
