import glob
import logging
import os
from collections import OrderedDict
from textwrap import dedent

from .action import Action
from .util import run_script
from ..util import is_installed, get_installed_build


class InstallAction(Action):
    def __init__(self, build, script, config, from_binary_archives=False):
        name = "install" if not from_binary_archives else "install from binary archives"
        super().__init__(name, build, script, config)
        self.from_binary_archives = from_binary_archives

    def _run(self, show_output=False, args=None):
        environment = self.config.global_env()
        tmp_root = environment["TMP_ROOT"]
        orchestra_root = environment['ORCHESTRA_ROOT']

        logging.info("Preparing temporary root directory")
        self._prepare_tmproot()
        pre_file_list = self._index_directory(tmp_root + orchestra_root, strip_prefix=tmp_root + orchestra_root)

        if self.from_binary_archives:
            self._install_from_binary_archives()
        else:
            self._install(show_output)
            self._post_install(show_output)

        post_file_list = self._index_directory(tmp_root + orchestra_root, strip_prefix=tmp_root + orchestra_root)
        new_files = [f for f in post_file_list if f not in pre_file_list]

        archive_name = self.build.binary_archive_filename
        archive_path = os.path.join(self.environment["BINARY_ARCHIVES"], archive_name)
        if args.create_binary_archives and not os.path.exists(archive_path):
            logging.info("Creating binary archive")
            self._create_binary_archive()

        if not args.no_merge:
            self._uninstall_currently_installed_build(show_output)

            logging.info("Merging installation into Orchestra root directory")
            self._merge(show_output)

            # Write file index
            os.makedirs(self.config.component_index_dir(), exist_ok=True)
            installed_component_path = self.config.component_index_path(self.build.component.name)
            with open(installed_component_path, "w") as f:
                f.truncate(0)
                f.write(self.build.qualified_name + "\n")
                f.write("\n".join(new_files))

    def _is_satisfied(self):
        return is_installed(self.config, self.build.component.name, wanted_build=self.build.name)

    def _prepare_tmproot(self):
        script = dedent("""
            rm -rf "$TMP_ROOT"
            mkdir -p "$TMP_ROOT"
            mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/include"
            mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/lib64"{,/include,/pkgconfig}
            test -e "${TMP_ROOT}${ORCHESTRA_ROOT}/lib" || ln -s lib64 "${TMP_ROOT}${ORCHESTRA_ROOT}/lib"
            test -L "${TMP_ROOT}${ORCHESTRA_ROOT}/lib"
            mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/bin"
            mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/usr/"{lib,include}
            mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/share/"{info,doc,man}
            touch "${TMP_ROOT}${ORCHESTRA_ROOT}/share/info/dir"
            mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/libexec"
            """)
        run_script(script, environment=self.environment)

    def _install(self, show_output):
        logging.info("Executing install script")
        run_script(self.script, show_output=show_output, environment=self.environment)

    def _post_install(self, show_output):
        # TODO: maybe this should be put into the configuration and not in Orchestra itself
        logging.info("Converting hardlinks to symbolic")
        self._hard_to_symbolic(show_output)

        # TODO: maybe this should be put into the configuration and not in Orchestra itself
        logging.info("Fixing RPATHs")
        self._fix_rpath(show_output)

        # TODO: this should be put into the configuration and not in Orchestra itself
        logging.info("Replacing NDEBUG preprocessor statements")
        self._replace_ndebug(True, show_output)

    def _hard_to_symbolic(self, show_output):
        hard_to_symbolic = """hard-to-symbolic.py "${TMP_ROOT}${ORCHESTRA_ROOT}" """
        run_script(hard_to_symbolic, show_output=show_output, environment=self.environment)

    def _fix_rpath(self, show_output):
        fix_rpath_script = dedent(f"""
            cd "$TMP_ROOT$ORCHESTRA_ROOT"
            # Fix rpath
            find . -type f -executable | while read EXECUTABLE; do
                if head -c 4 "$EXECUTABLE" | grep '^.ELF' > /dev/null &&
                        file "$EXECUTABLE" | grep x86-64 | grep -E '(shared|dynamic)' > /dev/null;
                then
                    REPLACE='$'ORIGIN/$(realpath --relative-to="$(dirname "$EXECUTABLE")" ".")
                    echo "Setting rpath of $EXECUTABLE to $REPLACE"
                    elf-replace-dynstr.py "$EXECUTABLE" "$RPATH_PLACEHOLDER" "$REPLACE" /
                    elf-replace-dynstr.py "$EXECUTABLE" "$ORCHESTRA_ROOT" "$REPLACE" /
                fi
            done
            """)
        run_script(fix_rpath_script, show_output=show_output, environment=self.environment)

    def _replace_ndebug(self, enable_debugging, show_output):
        debug, ndebug = ("1", "0") if enable_debugging else ("0", "1")
        patch_ndebug_script = dedent(rf"""
            cd "$TMP_ROOT$ORCHESTRA_ROOT"
            find include/ -name "*.h" \
                -exec \
                    sed -i \
                    -e 's|^\s*#\s*ifndef\s\+NDEBUG|#if {debug}|' \
                    -e 's|^\s*#\s*ifdef\s\+NDEBUG|#if {ndebug}|' \
                    -e 's|^\(\s*#\s*if\s\+.*\)!defined(NDEBUG)|\1{debug}|' \
                    -e 's|^\(\s*#\s*if\s\+.*\)defined(NDEBUG)|\1{ndebug}|' \
                    {"{}"} ';'
            """)
        run_script(patch_ndebug_script, show_output=show_output, environment=self.environment)

    def _uninstall_currently_installed_build(self, show_output):
        installed_build = get_installed_build(self.build.component.name, self.config)

        if installed_build is None:
            return

        logging.info("Uninstalling previously installed build")
        uninstall(self.build.component.name, self.config)

    def _merge(self, show_output):
        copy_command = f'cp -farl "$TMP_ROOT/$ORCHESTRA_ROOT/." "$ORCHESTRA_ROOT"'
        run_script(copy_command, show_output=show_output, environment=self.environment)

    def _create_binary_archive(self):
        archive_name = self.build.binary_archive_filename
        script = dedent(f"""
            mkdir -p "$BINARY_ARCHIVES"
            cd "$TMP_ROOT$ORCHESTRA_ROOT"
            tar caf "$BINARY_ARCHIVES/{archive_name}" --owner=0 --group=0 "."
            """)
        run_script(script, show_output=True, environment=self.environment)

    def _install_from_binary_archives(self):
        archives_dir = self.environment["BINARY_ARCHIVES"]
        archive_filepath = os.path.join(archives_dir, self.build.binary_archive_filename)
        if not os.path.exists(archive_filepath):
            raise Exception("Binary archive not found!")

        script = dedent(f"""
            mkdir -p "$TMP_ROOT$ORCHESTRA_ROOT"
            cd "$TMP_ROOT$ORCHESTRA_ROOT"
            tar xaf "{archive_filepath}"
            """)
        run_script(script, environment=self.environment)

    @staticmethod
    def _index_directory(dirpath, strip_prefix=None):
        paths = list(glob.glob(f"{dirpath}/**", recursive=True))
        if strip_prefix:
            paths = [remove_prefix(p, strip_prefix) for p in paths]
        return paths

    @property
    def environment(self) -> OrderedDict:
        env = super().environment
        env["DESTDIR"] = env["TMP_ROOT"]
        return env

    def _implicit_dependencies(self):
        if self.from_binary_archives:
            return set()
        else:
            return {self.build.configure}


class InstallAnyBuildAction(Action):
    def __init__(self, build, config):
        installed_build_name = get_installed_build(build.component.name, config)
        if installed_build_name:
            chosen_build = build.component.builds[installed_build_name]
        else:
            chosen_build = build
        super().__init__("install any", chosen_build, None, config)
        self._original_build = build

    def _implicit_dependencies(self):
        return {self.build.install}

    def _run(self, show_output=False, args=None):
        return

    def is_satisfied(self, recursively=False, already_checked=None):
        return self.build.install.is_satisfied(recursively=recursively, already_checked=already_checked)

    def _is_satisfied(self):
        raise NotImplementedError("This method should not be called!")

    @property
    def name_for_graph(self):
        if self.build == self._original_build:
            return f"install {self.build.component.name} (prefer {self._original_build.name})"
        else:
            return f"install {self.build.component.name} (prefer {self._original_build.name}, chosen {self.build.name})"

    @property
    def name_for_components(self):
        return f"{self._original_build.component.name}~{self._original_build.name}"


def remove_prefix(string, prefix):
    if string.startswith(prefix):
        return string[len(prefix):]
    else:
        return string[:]


def uninstall(component_name, config):
    index_path = config.component_index_path(component_name)
    with open(index_path) as f:
        f.readline()
        paths = f.readlines()

    # Ensure depth first visit by reverse-sorting
    paths.sort(reverse=True)
    paths = [path.strip() for path in paths]

    for path in paths:
        path = path.lstrip("/")
        path_to_delete = os.path.join(config.global_env()['ORCHESTRA_ROOT'], path)
        if os.path.isfile(path_to_delete) or os.path.islink(path_to_delete):
            logging.debug(f"Deleting {path_to_delete}")
            os.remove(path_to_delete)
        elif os.path.isdir(path_to_delete):
            if os.listdir(path_to_delete):
                logging.debug(f"Not removing directory {path_to_delete} as it is not empty")
            else:
                logging.debug(f"Deleting directory {path_to_delete}")
                os.rmdir(path_to_delete)

    logging.debug(f"Deleting index file {index_path}")
    os.remove(index_path)