import sublime

import os
import re
import codecs
import textwrap
from zipfile import ZipFile

from threading import Thread

from os.path import join, dirname, normpath, relpath
from importlib import __import__ as do_import


### ---------------------------------------------------------------------------


# These name the bootstrap package that we use to give Sublime access to our
# resources and commands, and the base name of the file within that package
# that is responsible for loading the commands into Sublime when the package
# is loaded.
bootstrap_pkg = "1_my_package"
bootloader = "bootstrap"


### ---------------------------------------------------------------------------


def log(msg, *args, dialog=False, error=False, **kwargs):
    """
    Generate a message to the console and optionally as either a message or
    error dialog. The message will be formatted and dedented before being
    displayed, and will be prefixed with its origin.
    """
    msg = textwrap.dedent(msg.format(*args, **kwargs)).strip()

    if error:
        print("my_package error:")
        return sublime.error_message(msg)

    for line in msg.splitlines():
        print("my_package: {msg}".format(msg=line))

    if dialog:
        sublime.message_dialog(msg)


### ---------------------------------------------------------------------------


class BootstrapThread(Thread):
    """
    Spawns a background thread that will create or update the my_package
    bootstrap package.
    """
    def __init__(self):
        super().__init__()
        self.settings = sublime.load_settings("Preferences.sublime-settings")


    def enable_package(self, reenable_resources):
        """
        Enables the system bootstrap package (if it exists) by ensuring that
        it is not in the list of ignored packages and then restoring any
        resources that were unloaded back to the views that were using them.
        """
        ignored_packages = self.settings.get("ignored_packages", [])

        if bootstrap_pkg in ignored_packages:
            ignored_packages.remove(bootstrap_pkg)
            self.settings.set("ignored_packages", ignored_packages)

        # Enable resources after a short delay to ensure that Sublime has had a
        # change to re-index them.
        if reenable_resources:
            sublime.set_timeout_async(lambda: self.enable_resources())


    def disable_package(self):
        """
        Disables the system bootstrap package (if it exists) by ensuring that
        none of the resources that it provides are currently in use and then
        adding it to the list of ignored packages so that Sublime will unload
        it.
        """
        self.disable_resources()

        ignored_packages = self.settings.get("ignored_packages", [])
        if bootstrap_pkg not in ignored_packages:
            ignored_packages.append(bootstrap_pkg)
            self.settings.set("ignored_packages", ignored_packages)


    def enable_resources(self):
        """
        Enables all resources being provided by the system boostrap package by
        restoring the state that was saved when the resources were disabled.
        """
        for window in sublime.windows():
            for view in window.views():
                s = view.settings()
                old_syntax = s.get("_mp_boot_syntax", None)
                if old_syntax is not None:
                    s.set("syntax", old_syntax)
                    s.erase("_mp_boot_syntax")


    def disable_resources(self):
        """
        Disables all resources being provided by the system bootstrap package
        by saving the state of items that are using them and then reverting
        them to temporary defaults.
        """
        prefix = "Packages/{pkg}/".format(pkg=bootstrap_pkg)

        # TODO if the package also contains a custom color scheme, this should
        # also temporarily reset the color scheme back to defaults and then
        # restore them later.
        for window in sublime.windows():
            for view in window.views():
                s = view.settings()
                syntax = s.get("syntax")
                if syntax.startswith(prefix):
                    s.set("_mp_boot_syntax", syntax)
                    s.set("syntax", "Packages/Text/Plain text.tmLanguage")


    def create_boot_loader(self, stub_loader_name):
        """
        Given the name of a file containing a stub system bootstrap loader,
        return the body of a loader that contains the version number of the
        core dependency.
        """
        try:
            from package_bootstrap import version as ver_info

            with codecs.open(stub_loader_name, 'r', 'utf-8') as file:
                content = file.read()

            return re.sub(r"^__core_version_tuple\s+=\s+\(.*\)$",
                           "__core_version_tuple = {version}".
                              format(version=str(ver_info())),
                          content,
                          count=1,
                          flags=re.MULTILINE)
        except:
            log("Bootstrap error: Unable to create bootloader")
            raise


    def create_bootstrap_package(self, package, res_path):
        """
        Perform the task of actually creating the system bootstrap package from
        files in the given resource folder into the provided package.
        """
        try:
            success = True
            boot_file = "{file}.py".format(file=bootloader)

            with ZipFile(package, 'w') as zFile:
                for (path, dirs, files) in os.walk(res_path):
                    rPath = relpath(path, res_path) if path != res_path else ""

                    for file in files:
                        real_file = join(res_path, path, file)
                        archive_file = join(rPath, file)

                        if archive_file.endswith(".sublime-ignored"):
                            archive_file = archive_file[:-len(".sublime-ignored")]

                        if archive_file == boot_file:
                            content = self.create_boot_loader(real_file)
                            zFile.writestr(archive_file, content)
                        else:
                            zFile.write(real_file, archive_file)

        except Exception as err:
            success = False
            log("Bootstrap error: {reason}", reason=str(err))
            if os.path.exists(package):
                os.remove(package)

        return success


    def run(self):
        """
        Creates or updates the system bootstrap package by packaging up the
        contents of the resource directory.
        """
        self.disable_package()

        res_path = normpath(join(dirname(__file__), "..", bootstrap_pkg))
        package = join(sublime.installed_packages_path(), bootstrap_pkg +
                            ".sublime-package")

        prefix = os.path.commonprefix([res_path, package])
        log("Bootstraping {path} to {pkg}",
            path=res_path[len(prefix):],
            pkg=package[len(prefix):])

        pkg_existed = os.path.isfile(package)
        success = self.create_bootstrap_package(package, res_path)

        self.enable_package(success)

        if not success:
            return log(
                """
                An error was encountered while updating my_package.

                Please check the console to see what went wrong.
                my_package will not be available until the problem
                is resolved.
                """, error=True)

        if pkg_existed:
            log(
                """
                my_package has been updated!

                In order to complete the update, restart Sublime
                Text.
                """, dialog=True)
        else:
            log(
                """
                my_package has been installed!
                """, dialog=True)


### ---------------------------------------------------------------------------