# The Craftr build system
# Copyright (C) 2017  Niklas Rosenstein
#
# 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/>.

from fnmatch import fnmatch
from nr.types.recordclass import recordclass

import zipfile


class Archive(object):
  '''
  Helper class to build and a list of files for an archive
  and then create that archive from that list. If no *name* is
  specified, it is derived from the *prefix*. The *format* must
  be ``'zip'`` for now.
  '''

  File = recordclass.new('File', 'name arc_name')

  def __init__(self, filename=None, base_dir=None, prefix=None, format='zip', files=None):
    assert format == 'zip', "format must be 'zip' for now"
    if not filename:
      if not prefix:
        raise TypeError('neither name nor prefix argument specified')
      filename = prefix + '.' + format
    if prefix and not prefix.endswith('/'):
      prefix += '/'
    self._files = []
    self._base_dir = base_dir or session.module.project_dir
    self._prefix = prefix
    self.filename = filename
    try:
      self._assigned_name = gtn()
    except ValueErorr as exc:
      self._assigned_name = None
    if files:
      for fn in files:
        self.add(fn)

  def add(self, name, rel_dir=None, arc_name=None, parts=None):
    ''' Add a file, directory or `Target` to the archive file list.
    If *parts* is specified, it must be a number which specifies how
    many parts of the arc name are kept from the right.
    .. note:: *name* can be a filename, the path to a directory,
      a glob pattern or list. Note that a directory will be globbed
      for its contents and will then be added recursively. A glob
      pattern that yields a directory path will add that directory. '''

    if arc_name and parts:
      raise TypeError('arc_name conflicts with parts')

    def _raise_arc():
      if arc_name:
        raise TypeError('arc_name can only be specified for a single file')

    if not rel_dir:
      rel_dir = self._base_dir
    if isinstance(name, (tuple, list)):
      _raise_arc()
      #[self.add(path.abspath(x), rel_dir, None, parts) for x in name]
      [self.add(x) for x in name]
    elif isinstance(name, str):
      if not path.isabs(name):
        name = path.join(self._base_dir, name)
      # xxx: make sure *name* is a subpath of *rel_dir*
      if path.isglob(name):
        _raise_arc()
        self.add(path.glob(name), rel_dir, None, parts)
      elif path.isdir(name):
        _raise_arc()
        self.add(path.glob(path.join(name, '*')), rel_dir, None, parts)
      else:
        name = path.get_long_path_name(name)
        if parts:
          assert not arc_name
          path_parts = path.split_parts(name)
          arc_name = path.sep.join(path_parts[-parts:])
        if not arc_name:
          arc_name = path.rel(name, rel_dir)
          if path.sep != '/':
            arc_name = arc_name.replace(path.sep, '/')
          # Make sure the arc_name contains no par-dirs.
          # xxx: Issues with other platforms here that don't use .. ?
          while arc_name.startswith('..'):
            arc_name = arc_name[3:]
        if self._prefix:
          arc_name = self._prefix + arc_name
        name = path.get_long_path_name(name)
        file = Archive.File(name, arc_name)
        self._files.append(file)
    else:
      raise TypeError('name must be str/list/tuple')

    return self

  def exclude(self, filter):
    ''' Remove all files in the Archive's file list that match the
    specified *filter*. The filter can be a string, in which case it
    is applied with :func:`fnmatch` or a function which accepts a single
    argument (the filename). '''

    if isinstance(filter, str):
      def wrapper(pattern):
        return lambda x: fnmatch(x, pattern)
      filter = wrapper(filter)

    self._files = [file for file in self._files if not filter(file.name)]

  def rename(self, old_arcname, new_arcname):
    ''' Rename the *old_arcname* to *new_arcname*. This will take
    folders into account. '''

    old_arcname_dir = old_arcname
    if not old_arcname.endswith('/'):
      old_arcname_dir += '/'
    new_arcname_dir = new_arcname
    if not new_arcname_dir.endswith('/'):
      new_arcname_dir += '/'

    for file in self._files:
      if file.arc_name == old_arcname:
        assert new_arcname
        file.arc_name = new_arcname
      elif file.arc_name.startswith(old_arcname_dir):
        file.arc_name = file.arc_name[len(old_arcname_dir):]
        if new_arcname:
          file.arc_name = new_arcname_dir + file.arc_name

  def save(self):
    ''' Save the archive. '''

    zf = zipfile.ZipFile(self.filename, 'w')
    try:
      for file in self._files:
        zf.write(file.name, file.arc_name)
    except:
      zf.close()
      path.remove(self.filename, silent=True)
      raise
    else:
      zf.close()

  @staticmethod
  def make_target(filename=None, base_dir=None, prefix=None, format='zip',
                  files=None, explicit=True, name=None):
    # Just to get the actual filename.
    archive = Archive(filename, base_dir, prefix, format, files)
    return gentask(archive.save, [], inputs=[f.name for f in archive._files],
      outputs=[archive.filename], explicit=explicit, name=gtn(name, 'archive'))


exports = Archive
