# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import contextlib
import os

LOCK_EX = None  # Exclusive lock
LOCK_SH = None  # Shared lock
LOCK_NB = None  # Non-blocking (LockException is raised if resource is locked)


class LockException(Exception):
  pass


# pylint: disable=import-error
# pylint: disable=wrong-import-position
if os.name == 'nt':
  import win32con
  import win32file
  import pywintypes
  LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
  LOCK_SH = 0  # the default
  LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
  _OVERLAPPED = pywintypes.OVERLAPPED()
elif os.name == 'posix':
  import fcntl
  LOCK_EX = fcntl.LOCK_EX
  LOCK_SH = fcntl.LOCK_SH
  LOCK_NB = fcntl.LOCK_NB
# pylint: enable=import-error
# pylint: enable=wrong-import-position


@contextlib.contextmanager
def FileLock(target_file, flags):
  """ Lock the target file. Similar to AcquireFileLock but allow user to write:
        with FileLock(f, LOCK_EX):
           ...do stuff on file f without worrying about race condition
    Args: see AcquireFileLock's documentation.
  """
  AcquireFileLock(target_file, flags)
  try:
    yield
  finally:
    ReleaseFileLock(target_file)


def AcquireFileLock(target_file, flags):
  """ Lock the target file. Note that if |target_file| is closed, the lock is
    automatically released.
  Args:
    target_file: file handle of the file to acquire lock.
    flags: can be any of the type LOCK_EX, LOCK_SH, LOCK_NB, or a bitwise
      OR combination of flags.
  """
  assert flags in (
      LOCK_EX, LOCK_SH, LOCK_NB, LOCK_EX | LOCK_NB, LOCK_SH | LOCK_NB)
  if os.name == 'nt':
    _LockImplWin(target_file, flags)
  elif os.name == 'posix':
    _LockImplPosix(target_file, flags)
  else:
    raise NotImplementedError('%s is not supported' % os.name)


def ReleaseFileLock(target_file):
  """ Unlock the target file.
  Args:
    target_file: file handle of the file to release the lock.
  """
  if os.name == 'nt':
    _UnlockImplWin(target_file)
  elif os.name == 'posix':
    _UnlockImplPosix(target_file)
  else:
    raise NotImplementedError('%s is not supported' % os.name)

# These implementations are based on
# http://code.activestate.com/recipes/65203/

def _LockImplWin(target_file, flags):
  hfile = win32file._get_osfhandle(target_file.fileno())
  try:
    win32file.LockFileEx(hfile, flags, 0, -0x10000, _OVERLAPPED)
  except pywintypes.error as exc_value:
    if exc_value[0] == 33:
      raise LockException('Error trying acquiring lock of %s: %s' %
                          (target_file.name, exc_value[2]))
    else:
      raise


def _UnlockImplWin(target_file):
  hfile = win32file._get_osfhandle(target_file.fileno())
  try:
    win32file.UnlockFileEx(hfile, 0, -0x10000, _OVERLAPPED)
  except pywintypes.error as exc_value:
    if exc_value[0] == 158:
      # error: (158, 'UnlockFileEx', 'The segment is already unlocked.')
      # To match the 'posix' implementation, silently ignore this error
      pass
    else:
      # Q:  Are there exceptions/codes we should be dealing with here?
      raise


def _LockImplPosix(target_file, flags):
  try:
    fcntl.flock(target_file.fileno(), flags)
  except IOError as exc_value:
    if exc_value[0] == 11 or exc_value[0] == 35:
      raise LockException('Error trying acquiring lock of %s: %s' %
                          (target_file.name, exc_value[1]))
    else:
      raise


def _UnlockImplPosix(target_file):
  fcntl.flock(target_file.fileno(), fcntl.LOCK_UN)
