import glob, os, shutil

class Operation(object):
    def __init__(self, name, data):
        self.name, self.data = name, data

    def __call__(self, dir = '.', reverse = False):
        try:
            if not reverse:
                self.do(dir)
            else:
                self.do_reverse(dir)
            self._log(True)
        except:
            self._log(False)
            raise

    def _log(self, result):
        if result:
            s = "OK"
        else:
            s = "FAIL"
        print """  (%s) %-4s %s""" % (self.operation, s, self.name)

    def do(self, dir):
        raise NotImplementedError

    def do_reverse(self, dir):
        raise NotImplementedError

class OperationPatch(Operation):
    def __init__(self, name, fopen, data):
        super(OperationPatch, self).__init__(name, data)
        self.fopen = fopen

    def _call(self, dir, extraargs):
        cmdline = "cd %s; patch -p1 -f -s -t --no-backup-if-mismatch %s" % (dir, extraargs)
        f = os.popen(cmdline, 'wb')
        shutil.copyfileobj(self.fopen(), f)
        if f.close():
            raise RuntimeError("Patch failed")

    def patch_push(self, dir):
        self._call(dir, '--fuzz=1')

    def patch_pop(self, dir):
        self._call(dir, '-R')

class OperationPatchPush(OperationPatch):
    operation = '+'

    do = OperationPatch.patch_push
    do_reverse = OperationPatch.patch_pop

class OperationPatchPop(OperationPatch):
    operation = '-'

    do = OperationPatch.patch_pop
    do_reverse = OperationPatch.patch_push

class SubOperation(Operation):
    def _log(self, result):
        if result:
            s = "OK"
        else:
            s = "FAIL"
        print """    %-10s %-4s %s""" % ('(%s)' % self.operation, s, self.name)

class SubOperationFilesRemove(SubOperation):
    operation = "remove"

    def do(self, dir):
        name = os.path.join(dir, self.name)
        for n in glob.iglob(name):
            if os.path.isdir(n):
                shutil.rmtree(n)
            else:
                os.unlink(n)

class SubOperationFilesUnifdef(SubOperation):
    operation = "unifdef"

    def do(self, dir):
        filename = os.path.join(dir, self.name)
        cmdline = "unifdef %s %s" % (filename, ' '.join(self.data))
        f = os.popen(cmdline, 'rb')
        data = f.read()
        ret = f.close()
        if ret is None:
            raise RuntimeError("unifdef of %s removed nothing" % self.name)
        elif ret != 256:
            raise RuntimeError("unifdef failed")
        f1 = file(filename, 'wb')
        f1.write(data)
        f1.close()

class OperationFiles(Operation):
    operation = 'X'

    suboperations = {
        'remove': SubOperationFilesRemove,
        'rm': SubOperationFilesRemove,
        'unifdef': SubOperationFilesUnifdef,
    }

    def __init__(self, name, fopen, data):
        super(OperationFiles, self).__init__(name, data)

        ops = []

        for line in fopen():
            line = line.strip()
            if not line or line[0] == '#':
                continue

            items = line.split()
            operation, filename = items[:2]
            data = items[2:]

            if operation not in self.suboperations:
                raise RuntimeError('Undefined operation "%s" in series %s' % (operation, name))

            ops.append(self.suboperations[operation](filename, data))

        self.ops = ops

    def do(self, dir):
        for i in self.ops:
            i(dir = dir)

class PatchSeries(list):
    operations = {
        '+': OperationPatchPush,
        '-': OperationPatchPop,
        'X': OperationFiles,
    }

    def __init__(self, name, root, fp):
        self.name, self.root = name, root

        from gzip import GzipFile
        from bz2 import BZ2File

        for line in fp:
            line = line.strip()

            if not len(line) or line[0] == '#':
                continue

            items = line.split(' ')
            operation, filename = items[:2]
            data = items[2:]

            if operation in self.operations:
                f = os.path.join(self.root, filename)
                for suffix, cls in (('', file), ('.bz2', BZ2File), ('.gz', GzipFile)):
                    f1 = f + suffix
                    if os.path.exists(f1):
                        # Must copy current bindings into the lambda-function
                        fopen = lambda cls=cls, f1=f1: cls(f1)
                        break
                else:
                    raise RuntimeError("Can't find patch %s for series %s" % (filename, self.name))
            else:
                raise RuntimeError('Undefined operation "%s" in series %s' % (operation, name))

            self.append(self.operations[operation](filename, fopen, data))

    def __call__(self, cond = bool, dir = '.', reverse = False):
        if not reverse:
            l = self
        else:
            l = self[::-1]
        for i in l:
            if cond(i):
                i(dir = dir, reverse = reverse)

    def __repr__(self):
        return '<%s object for %s>' % (self.__class__.__name__, self.name)

class PatchSeriesList(list):
    def __call__(self, cond = bool, reverse = False):
        if not reverse:
            l = self
        else:
            l = self[::-1]
        for i in l:
            if reverse:
                print "--> Try to unapply %s." % i.name
            else:
                print "--> Try to apply %s." % i.name
            i(cond = cond, reverse = reverse)
            if reverse:
                print "--> %s fully unapplied." % i.name
            else:
                print "--> %s fully applied." % i.name

    @classmethod
    def read(cls, home, files):
        ret = cls()
        for i in files:
            try:
                fp = file(os.path.join(home, 'series', i))
                ret.append(PatchSeries(i, home, fp))
            except IOError:
                pass
        return ret

