from __future__ import absolute_import, division, print_function
import locale
import shutil
import os
import sys
from six.moves import range, zip
op = os.path

if sys.hexversion >= 0x03000000:
  unicode = str

def norm_join(*args):
  return op.normpath(op.join(*args))

def abs_real_norm(path):
  return op.normpath(op.realpath(op.abspath(path)))

def abs_norm(path):
  return op.normpath(op.abspath(path))

def posix_relpath(
      path,
      start=".",
      enable_abspath_if_through_root=False):
  # based on relpath() in Python-2.7.2/Lib/posixpath.py
  if not path:
    raise ValueError("no path specified")
  def as_list(ap):
    result = []
    for _ in ap.split("/"):
      if (_): result.append(_)
    return result
  start_list = as_list(op.abspath(start))
  path_abs = op.abspath(path)
  path_list = as_list(path_abs)
  i = len(op.commonprefix([start_list, path_list]))
  if (i == 0 and enable_abspath_if_through_root):
    return path_abs
  rel_list = [".."] * (len(start_list)-i) + path_list[i:]
  if not rel_list:
    return "."
  return op.join(*rel_list)

def nt_is_true_abs(path):
  try: # splitunc is removed in Python 3.7,
    prefix, _ = op.splitunc(path)
    if (bool(prefix)): return True
  except AttributeError:
    pass
  prefix, _ = op.splitdrive(path)
  return bool(prefix)

def nt_relpath(
      path,
      start=".",
      enable_abspath_if_through_root=False):
  # based on relpath() in Python-2.7.2/Lib/ntpath.py
  if not path:
    raise ValueError("no path specified")
  if (enable_abspath_if_through_root
        and not (    nt_is_true_abs(start)
                 and nt_is_true_abs(path))):
    raise RuntimeError(
      "nt_relpath(): both path and start must be absolute paths.")
  start_abs = abs_norm(start)
  path_abs = abs_norm(path)
  def _abspath_split(abs):
    try: # splitunc is removed in Python 3.7
      prefix, rest = op.splitunc(abs)
      is_unc = bool(prefix)
    except AttributeError:
      is_unc = False
    if not is_unc:
      prefix, rest = op.splitdrive(abs)
    rest_list = []
    for _ in rest.split("\\"):
      if _: rest_list.append(_)
    return is_unc, prefix, rest_list
  start_is_unc, start_prefix, start_list = _abspath_split(start_abs)
  path_is_unc, path_prefix, path_list = _abspath_split(path_abs)
  if path_is_unc ^ start_is_unc:
    if (enable_abspath_if_through_root):
      return path_abs
    raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)"
                              % (path, start))
  if path_prefix.lower() != start_prefix.lower():
    if (enable_abspath_if_through_root):
      return path_abs
    if path_is_unc:
      raise ValueError("path is on UNC root %s, start on UNC root %s"
                        % (path_prefix, start_prefix))
    else:
      raise ValueError("path is on drive %s, start on drive %s"
                        % (path_prefix, start_prefix))
  i = 0
  for e1, e2 in zip(start_list, path_list):
    if e1.lower() != e2.lower():
      break
    i += 1
  if (i == 0 and enable_abspath_if_through_root):
    return path_prefix + "\\" + "\\".join(path_list)
  rel_list = [".."] * (len(start_list)-i) + path_list[i:]
  if not rel_list:
    return "."
  return op.join(*rel_list)

if (os.name == "nt"):
  relpath = nt_relpath
else:
  relpath = posix_relpath

def tail_levels(path, number_of_levels):
  return op.join(*path.split(op.sep)[-number_of_levels:])

def create_target_dir(target_file):
  target_dir = op.split(target_file)[0]
  if (not op.isdir(target_dir)):
    os.makedirs(target_dir)

def move_old(path, serial_sep="_", serial_fmt="%03d"):
  if (not op.exists(path)): return
  bns = op.basename(path) + serial_sep
  dn = op.dirname(op.abspath(path))
  max_i = 0
  for ex in os.listdir(dn):
    if (ex.startswith(bns)):
      s = ex[len(bns):]
      try: i = int(s)
      except ValueError: pass
      else: max_i = max(max_i, i)
  nn = op.join(dn, bns + serial_fmt % (max_i+1))
  os.rename(path, nn)

def move_old_create_new_directory(path, serial_sep="_", serial_fmt="%03d"):
  move_old(path=path, serial_sep=serial_sep, serial_fmt=serial_fmt)
  os.makedirs(path)

def canonical_path(file_name, effective_current_working_directory=None):
  if not isinstance(file_name, (bytes, unicode)):
    file_name = abs(file_name)
  if (not op.isabs(file_name)):
    if (effective_current_working_directory is None):
      effective_current_working_directory = os.getcwd()
    file_name = op.join(effective_current_working_directory, file_name)
  return op.normpath(file_name)

def is_same_canoncial_file(file_names):
  assert len(file_names) == 2
  if (file_names[0] == file_names[1]): return True
  if (hasattr(op, "samefile")):
    return op.samefile(file_names[0], file_names[1])
  return False

def is_same_file(file_names, effective_current_working_directory=None):
  return is_same_canoncial_file(
    [canonical_path(file_name, effective_current_working_directory)
      for file_name in file_names])

def full_command_path(command, search_first=[], search_last=[]):
  dirs = search_first + os.environ["PATH"].split(os.pathsep) + search_last
  for path in dirs:
    path_command = op.join(path, command)
    if (op.exists(path_command)):
      return abs_norm(path=path_command)
  return None

class directory(object):

  def __init__(self, path):
    self.path = path

  def get(self, name, must_exist=True):
    assert name is not None
    result = op.join(self.path, name)
    if (must_exist and not op.exists(result)):
      from libtbx.str_utils import show_string
      raise RuntimeError("No such file or directory: %s" % show_string(result))
    return result

  def sub_directory(self, name, must_exist=True):
    result = directory(self.get(name))
    if (must_exist and not op.isdir(result.path)):
      from libtbx.str_utils import show_string
      raise RuntimeError("Not a directory: %s" % show_string(result.path))
    return result

def walk_source_tree(top, arg=None):
  def visitor(result, dirname, names):
    names_keep = []
    for name in names:
      path = op.join(dirname, name)
      if (not op.isdir(path)):
        if (not name.endswith(".pyc")):
          result.append(path)
        continue
      def is_file_in_subdir(name):
        return op.isfile(op.join(path, name))
      if (   (name == "CVS" and is_file_in_subdir("Entries"))
          or (name == ".svn"
                and (   is_file_in_subdir("README.txt")
                     or is_file_in_subdir("entries")))):
        continue
      names_keep.append(name)
    if (len(names_keep) != len(names)):
      del names[:]
      names.extend(names_keep)
  result = []
  if sys.version_info.major == 3:
    for root, dirs, files in os.walk(top):
      visitor(
          result=result,
          dirname=root,
          names=files)
  else:
    op.walk(top, visitor, result)
  return result

def random_new_directory_name(prefix="tmp_dir_", number_of_hex_code_digits=8):
  from libtbx.utils import random_hex_code
  for i_trial in range(10**6):
    name = prefix + random_hex_code(number_of_digits=number_of_hex_code_digits)
    if (not op.exists(name)):
      return name
  else:
    raise AssertionError

def makedirs(path):
  """
  Wrapper for os.makedirs that catches OSError and re-raises as Sorry.
  """
  try :
    os.makedirs(path)
  except OSError as e :
    from libtbx.utils import Sorry
    raise Sorry(str(e))

def makedirs_race(
      path=None,
      max_trials=10,
      delay_if_exists=0.001,
      delay_after_exception=0.5):
  if (path is None):
    path = random_new_directory_name()
  import time
  for i_trial in range(max_trials):
    if (op.exists(path)):
      if (delay_if_exists is not None):
        # in case the OS needs time to finalize makedirs from another process
        time.sleep(delay_if_exists)
      break
    try:
      os.makedirs(path)
    except Exception:
      if (delay_after_exception is not None):
        time.sleep(delay_after_exception)
  if (not op.isdir(path)):
    raise RuntimeError("makedirs_race(%s) failure." % path)
  return path


class path_mixin(object):

  def __truediv__(self, path):
    return self.__div__(path)

  def isdir(self):
    return op.isdir(abs(self))

  def isfile(self):
    return op.isfile(abs(self))

  def exists(self):
    return op.exists(abs(self))

  def open(self, *args, **kwds):
    return open(abs(self), *args, **kwds)

  def makedirs(self):
    os.makedirs(abs(self))

  def remove(self):
    if self.exists(): os.remove(abs(self))
    assert not self.exists()

  def remove_tree(self):
    from distutils.dir_util import remove_tree
    if self.isdir():
      remove_tree(abs(self))
    else:
      self.remove()

  def listdir(self):
    return os.listdir(abs(self))

  def chmod(self, *args, **kwds):
    return os.chmod(abs(self), *args, **kwds)

  def access(self, *args, **kwds):
    return os.access(abs(self), *args, **kwds)

  def basename(self):
    return op.basename(abs(self))

  def ext(self):
    return op.splitext(self.basename())[1]

  def split(self):
    return (self.dirname(), self.basename())

  def samefile(self, other):
    if isinstance(other, (bytes, unicode)):
      return op.samefile(abs(self), other)
    else:
      return op.samefile(abs(self), abs(other))

  def is_relocatable(self):
    return isinstance(self, relocatable_path)

  def sh_value(self, anchor_var="LIBTBX_BUILD"):
    if (self.is_relocatable()):
      return op.join("$%s" % anchor_var, self.relocatable)
    return abs(self)

  def bat_value(self, anchor_var="LIBTBX_BUILD"):
    if (self.is_relocatable()):
      return op.join("%%%s%%" % anchor_var, self.relocatable)
    return abs(self)


class absolute_path(path_mixin):

  def __init__(self, path, case_sensitive=False):
    assert op.isabs(path)
    if not case_sensitive:
      path = op.normcase(path)
    path = op.normpath(path)
    self._path = path

  def reset(self, new_path, case_sensitive=False):
    self._path = absolute_path(new_path, case_sensitive)._path

  def __div__(self, other):
    return absolute_path(op.join(self._path, other))

  def __abs__(self):
    return self._path

  def __add__(self, ext):
    return absolute_path(self._path + ext)

  def __repr__(self):
    return 'absolute_path("%s")' % self._path

  def dirname(self):
    return absolute_path(op.dirname(self._path))


class relocatable_path(path_mixin):
  def __init__(self, anchor, relocatable, resolve_symlinks=True):
    assert isinstance(anchor, absolute_path)
    self._anchor = anchor
    if op.isabs(relocatable):
      path = abs(absolute_path(relocatable))
      start = abs(self._anchor)
      if resolve_symlinks:
        path = op.realpath(path)
        start = op.realpath(start)
      relocatable = relpath(
        path=path,
        start=start,
        enable_abspath_if_through_root=True)
    self.relocatable = relocatable

  def anchor(self):
    return self._anchor
  anchor = property(anchor)

  def __div__(self, path):
    return relocatable_path(self._anchor, op.join(self.relocatable, path))

  def __idiv__(self, path):
    self.relocatable = op.join(self.relocatable, path)
    return self

  def __add__(self, ext):
    return relocatable_path(self._anchor, self.relocatable + ext)

  def self_or_abs_if(self, flag):
    if flag:
      return abs(self)
    else:
      return self

  def __abs__(self):
    return op.abspath(op.join(abs(self.anchor), self.relocatable))

  def __repr__(self):
    return 'relocatable_path(anchor="%s", relocatable="%s")' % (
      abs(self.anchor), self.relocatable)

  def dirname(self):
    assert self.relocatable
    return relocatable_path(self._anchor, op.dirname(self.relocatable))

  def basename(self):
    return op.basename(self.relocatable)

  def normcase(self):
    return relocatable_path(self._anchor, op.normcase(self.relocatable))

  def __eq__(self, other):
    return (    self._anchor == other._anchor
            and self.relocatable == other.relocatable)

  # From the Python 2.7 documentation:
  #   There are no implied relationships among the comparison operators. The
  #   truth of x==y does not imply that x!=y is false. Accordingly, when
  #   defining __eq__(), one should also define __ne__() so that the operators
  #   will behave as expected.
  def __ne__(self, other):
    return not self == other

  def __hash__(self):
    return hash((self._anchor, self.relocatable))

class clean_out_directory(object):
  """
  Utility for cleaning out Phenix (etc.) project folders, which tend to
  accumulate large amounts of temporary files and other large objects (CCP4
  maps, .geo files, etc.) which we may not want to keep around forever.
  """
  def __init__(self,
      path_name,
      delete_kin_files=True,
      delete_geo_files=True,
      delete_map_files=True,
      delete_temp_dirs=True,
      delete_probe_files=True):
    self.path_name = path_name
    self.n_files = self.n_dirs = self.n_bytes = 0
    self.file_paths = []
    self.dir_paths = []
    for dirname, dirnames, filenames in os.walk(path_name):
      base_dir = os.path.basename(dirname)
      base_dir_name = base_dir.split("_")[0]
      # phenix-specific stuff
      if (base_dir_name in ["AutoSol","AutoBuild","AutoMR","LigandFit",
                            "StructureComparison",]):
        if ("TEMP0" in dirnames) and delete_temp_dirs :
          dirnames.remove("TEMP0")
          full_path = os.path.join(dirname, "TEMP0")
          self.delete_directory(full_path)
      elif (base_dir_name in ["Refine"]):
        if (".comm" in dirnames) and delete_temp_dirs and delete_probe_files :
          dirnames.remove(".comm")
          full_path = os.path.join(dirname, ".comm")
          self.delete_directory(full_path)
      elif (base_dir_name in ["FFT", "SuperposeMaps"]):
        continue
      for file_name in filenames :
        full_path = os.path.join(dirname, file_name)
        if ((file_name.endswith(".kin") and delete_kin_files) or
            (file_name.endswith(".geo") and delete_geo_files) or
            (file_name.endswith(".ccp4") and delete_map_files) or
            (file_name.endswith(".xplor") and delete_map_files) or
            (file_name == "probe.txt" and delete_probe_files)):
          self.delete_file(full_path)

  def delete_file(self, file_name):
    if not os.path.isfile(file_name) : return
    self.n_bytes += os.path.getsize(file_name)
    self.file_paths.append(file_name)
    self.n_files += 1

  def delete_directory(self, dir_name):
    self.n_bytes += directory_size(dir_name)
    self.dir_paths.append(dir_name)
    self.n_dirs += 1

  @property
  def n_total(self):
    return self.n_dirs + self.n_files

  def run(self, out=sys.stdout):
    self.show(out=out)
    print("Deleting all selected files and directories...", file=out)
    for file_name in self.file_paths :
      os.remove(file_name)
    for dir_name in self.dir_paths :
      shutil.rmtree(dir_name)

  def get_freed_space(self):
    locale.setlocale(locale.LC_ALL, 'C')
    n_kb = self.n_bytes / 1000
    if (n_kb > 10000):
      return locale.format_string("%.1f", n_kb / 1000, grouping=True) + " MB"
    else :
      return locale.format_string("%.1f", n_kb, grouping=True) + " KB"

  def show(self, out=sys.stdout):
    if (self.n_dirs > 0):
      print("The following %d directories will deleted:" % \
        self.n_dirs, file=out)
      for dir_name in sorted(self.dir_paths):
        print("  %s" % dir_name, file=out)
    if (self.n_files > 0):
      print("The following %d files will be deleted:" % \
        self.n_files, file=out)
      for file_name in sorted(self.file_paths):
        print("  %s" % file_name, file=out)
    if (self.n_bytes > 0):
      print("%s of disk space will be freed." % self.get_freed_space(), file=out)

# http://stackoverflow.com/questions/1392413/calculating-a-directory-size-using-python
def directory_size(path):
  total_size = 0
  seen = set()
  for dirpath, dirnames, filenames in os.walk(path):
    for f in filenames:
      fp = os.path.join(dirpath, f)
      try:
        stat = os.stat(fp)
      except OSError:
        continue
      if stat.st_ino in seen:
        continue
      seen.add(stat.st_ino)
      total_size += stat.st_size
  return total_size  # size in bytes
