from __future__ import with_statement

import logging
import os

from .diffing import Differ
from .util import relpath_parts


logger = logging.getLogger("abl.installer")


class Action(object):

    ACTION_PREFIX = None

    def __init__(self, new_dir, full_path):
        self.new_dir = new_dir
        self.full_path = full_path

    def path_parts(self):
        parts = []
        path = self.full_path
        while path != self.new_dir:
            parts.append(path.basename())
            path = path.directory()
        return list(reversed(parts))

    def make_and_get_basedir(self, dest):
        basedir = dest / self.ACTION_PREFIX

        if not basedir.exists():
            basedir.mkdir()
        assert basedir.isdir()
        return basedir

    def perform(self, dest):
        basedir = self.make_and_get_basedir(dest)
        path_parts = self.path_parts()
        for dir_ in path_parts[:-1]:
            basedir /= dir_
            if not basedir.exists():
                basedir.mkdir()

        destpath = basedir / path_parts[-1]
        destpath = self.create_delta_file(self.full_path, destpath)
        # preserve the mode of the source-file
        mode = self.full_path.info(followlinks=False).mode
        destpath.info({"mode": mode}, followlinks=False)

    def create_delta_file(self, sourcepath, destpath):
        if sourcepath.islink():
            sourcepath.readlink().symlink(destpath)
        else:
            with destpath.open("wb") as outf:
                with sourcepath.open("rb") as inf:
                    outf.write(inf.read())
        return destpath


class UpdateAction(Action):

    ACTION_PREFIX = "updated"

    def __init__(self, differ, *args, **kwargs):
        super(UpdateAction, self).__init__(*args, **kwargs)
        self.differ = differ

    def create_delta_file(self, sourcepath, destpath):
        return self.differ.delta_file_into(destpath)


class CreateAction(Action):

    ACTION_PREFIX = "created"


class CreateDirAction(CreateAction):
    def perform(self, dest):
        basedir = self.make_and_get_basedir(dest)
        path_parts = self.path_parts()
        for dir_ in path_parts[:-1]:
            basedir /= dir_
        dest_dir = basedir / path_parts[-1]
        dest_dir.makedirs()


class DeleteAction(Action):

    ACTION_PREFIX = "deleted"

    def perform(self, dest):
        basedir = self.make_and_get_basedir(dest)
        path_parts = self.path_parts()
        for dir_ in path_parts[:-1]:
            basedir /= dir_
            if not basedir.exists():
                basedir.mkdir()

        # just create an empty file to signify deletion
        with (basedir / path_parts[-1]).open("wb") as outf:
            pass


class DeltaBuilder(object):
    def __init__(self, old, new):
        assert old.isdir() and new.isdir(), "both old and new need to be directories"
        self.old, self.new = old, new
        logger.debug("DeltaBuilder old: %r new: %r", old, new)

    def compute_action_list(self, use_xdelta):
        actions = []

        def actions_for_directories(old, new):
            old_files = {}
            new_files = {}
            old_dirs = {}
            new_dirs = {}

            def is_file_or_link(uri):
                return uri.isfile() or uri.islink()

            if old is not None:
                for old_file_name in old.listdir():
                    if is_file_or_link(old / old_file_name):
                        old_files[old_file_name] = old / old_file_name
                    elif (old / old_file_name).isdir():
                        old_dirs[old_file_name] = old / old_file_name

            for new_file_name in new.listdir():
                if is_file_or_link(new / new_file_name):
                    new_files[new_file_name] = new / new_file_name
                elif (new / new_file_name).isdir():
                    new_dirs[new_file_name] = new / new_file_name

            # add create actions for files that are only
            # in new
            for new_file_name in set(new_files.keys()) - set(old_files.keys()):
                logger.debug("creating new file: %s", new_file_name)
                actions.append(CreateAction(self.new, new_files[new_file_name]))

            # add update actions for files that exist in both
            for updated_file_name in set(new_files.keys()) & set(old_files.keys()):
                logger.debug("updating file: %s", updated_file_name)
                old_f, new_f = (old / updated_file_name), (new / updated_file_name)
                differ = Differ.select_differ(old_f, new_f, use_xdelta)
                if differ.are_different():
                    actions.append(
                        UpdateAction(differ, self.new, new_files[updated_file_name])
                    )

            # add delete actions for files that exist only in old
            for deleted_file_name in set(old_files.keys()) - set(new_files.keys()):
                logger.debug("deleting file: %s", deleted_file_name)
                actions.append(DeleteAction(self.old, old_files[deleted_file_name]))

            # now process dirs that need creation
            for new_dir_name in set(new_dirs.keys()) - set(old_dirs.keys()):
                logger.debug("creating directory: %s", new_dir_name)
                new_dir = new_dirs[new_dir_name]
                actions.append(CreateDirAction(self.new, new_dir))
                actions_for_directories(None, new_dir)

            # now process dirs that are to be deleted.
            # these will actually result just in a DeleteAction
            for deleted_dir_name in set(old_dirs.keys()) - set(new_dirs.keys()):
                logger.debug("deleting directory: %s", deleted_dir_name)
                actions.append(DeleteAction(self.old, old_dirs[deleted_dir_name]))

            # directories in both are processed recursively
            for common_dir in set(new_dirs.keys()) & set(old_dirs.keys()):
                logger.debug("descending into %s", common_dir)
                actions_for_directories(old / common_dir, new / common_dir)

        actions_for_directories(self.old, self.new)

        return actions

    def produce_delta(self, dest, use_xdelta=False):
        for action in self.compute_action_list(use_xdelta):
            action.perform(dest)
