# ##### BEGIN GPL LICENSE BLOCK #####
#
#  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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# type: ignore

"""Blender UI integrations for the addon updater.

Implements draw calls, popups, and operators that use the addon_updater.
"""

import os
import traceback
import logging

import bpy
from bpy.app.handlers import persistent

from . import client_lib, utils

bk_logger = logging.getLogger(__name__)


# Safely import the updater.
# Prevents popups for users with invalid python installs e.g. missing libraries
# and will replace with a fake class instead if it fails (so UI draws work).
try:
    from .addon_updater import Updater as updater
except Exception as e:
    bk_logger.error("ERROR INITIALIZING UPDATER")
    bk_logger.error(str(e))
    bk_logger.error("%s", traceback.format_exc())

    class SingletonUpdaterNone(object):
        """Fake, bare minimum fields and functions for the updater object."""

        def __init__(self):
            self.invalid_updater = True  # Used to distinguish bad install.

            self.addon = None
            self.verbose = False
            self.use_print_traces = True
            self.error = None
            self.error_msg = None
            self.async_checking = None

        def clear_state(self):
            self.addon = None
            self.verbose = False
            self.invalid_updater = True
            self.error = None
            self.error_msg = None
            self.async_checking = None

        def run_update(self, force, callback, clean):
            pass

        def check_for_update(self, now):
            pass

    updater = SingletonUpdaterNone()
    updater.error = "Error initializing updater module"
    updater.error_msg = str(e)

# Must declare this before classes are loaded, otherwise the bl_idname's will
# not match and have errors. Must be all lowercase and no spaces! Should also
# be unique among any other addons that could exist (using this updater code),
# to avoid clashes in operator registration.
updater.addon = "blenderkit"


# -----------------------------------------------------------------------------
# Blender version utils
# -----------------------------------------------------------------------------
def make_annotations(cls):
    """Add annotation attribute to fields to avoid Blender 2.8+ warnings"""
    if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80):
        return cls
    if bpy.app.version < (2, 93, 0):
        bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)}
    else:
        bl_props = {
            k: v
            for k, v in cls.__dict__.items()
            if isinstance(v, bpy.props._PropertyDeferred)
        }
    if bl_props:
        if "__annotations__" not in cls.__dict__:
            setattr(cls, "__annotations__", {})

        try:
            annotations = cls.__dict__["__annotations__"]
        except KeyError:
            # Fedora 43 bug workaround #1823
            annotations = getattr(cls, "__annotations__")

        for k, v in bl_props.items():
            annotations[k] = v
            delattr(cls, k)
    return cls


def layout_split(layout, factor=0.0, align=False):
    """Intermediate method for pre and post blender 2.8 split UI function"""
    if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80):
        return layout.split(percentage=factor, align=align)
    return layout.split(factor=factor, align=align)


def get_user_preferences(context=None):
    """Intermediate method for pre and post blender 2.8 grabbing preferences"""
    if not context:
        context = bpy.context
    prefs = None
    if hasattr(context, "user_preferences"):
        prefs = context.user_preferences.addons.get(__package__, None)
    elif hasattr(context, "preferences"):
        prefs = context.preferences.addons.get(__package__, None)
    if prefs:
        return prefs.preferences
    # To make the addon stable and non-exception prone, return None
    # raise Exception("Could not fetch user preferences")
    return None


# -----------------------------------------------------------------------------
# Updater operators
# -----------------------------------------------------------------------------


# Simple popup to prompt use to check for update & offer install if available.
class AddonUpdaterInstallPopup(bpy.types.Operator):
    """Check and install update if available"""

    bl_label = "Update {x} addon".format(x=updater.addon)
    bl_idname = updater.addon + ".updater_install_popup"
    bl_description = "Popup to check and display current updates available"
    bl_options = {"REGISTER", "INTERNAL"}

    # if true, run clean install - ie remove all files before adding new
    # equivalent to deleting the addon and reinstalling, except the
    # updater folder/backup folder remains
    clean_install = bpy.props.BoolProperty(
        name="Clean install",
        description=(
            "If enabled, completely clear the addon's folder before "
            "installing new update, creating a fresh install"
        ),
        default=False,
        options={"HIDDEN"},
    )

    ignore_enum = bpy.props.EnumProperty(
        name="Process update",
        description="Decide to install, ignore, or defer new addon update",
        items=[
            ("install", "Update Now", "Install update now"),
            ("ignore", "Ignore", "Ignore this update to prevent future popups"),
            ("defer", "Defer", "Defer choice till next blender session"),
        ],
        options={"HIDDEN"},
    )

    def check(self, context):
        return True

    def invoke(self, context, event):
        # Skip opening the popup when we already know there is nothing to show.
        if not updater.invalid_updater and updater.update_ready is False:
            updater.print_verbose("No update available; skipping popup")
            return {"CANCELLED"}
        return context.window_manager.invoke_props_dialog(self)

    def draw(self, context):
        layout = self.layout
        if updater.invalid_updater:
            layout.label(text="Updater module error")
            return
        elif updater.update_ready:
            col = layout.column()
            col.scale_y = 0.7
            col.label(
                text="Update {} ready!".format(updater.update_version),
                icon="LOOP_FORWARDS",
            )
            col.label(text="Choose 'Update Now' & press OK to install, ", icon="BLANK1")
            col.label(text="or click outside window to defer", icon="BLANK1")
            row = col.row()
            row.prop(self, "ignore_enum", expand=True)
            col.split()
        elif not updater.update_ready:
            col = layout.column()
            col.scale_y = 0.7
            col.label(text="No updates available")
            col.label(text="Press okay to dismiss dialog")
            # add option to force install
        else:
            # Case: updater.update_ready = None
            # we have not yet checked for the update.
            layout.label(text="Check for update now?")

        # Potentially in future, UI to 'check to select/revert to old version'.

    def execute(self, context):
        # In case of error importing updater.
        if updater.invalid_updater:
            return {"CANCELLED"}

        if updater.manual_only:
            bpy.ops.wm.url_open(url=updater.website)
        elif updater.update_ready:
            # Action based on enum selection.
            if self.ignore_enum == "defer":
                return {"FINISHED"}
            elif self.ignore_enum == "ignore":
                updater.ignore_update()
                return {"FINISHED"}

            res = updater.run_update(
                force=False, callback=post_update_callback, clean=self.clean_install
            )

            # Should return 0, if not something happened.
            if updater.verbose:
                if res == 0:
                    bk_logger.info("Updater returned successful")
                else:
                    bk_logger.info("Updater returned {}, error occurred".format(res))
        elif updater.update_ready is None:
            _ = updater.check_for_update(now=True)

            # Re-launch this dialog.
            atr = AddonUpdaterInstallPopup.bl_idname.split(".")
            getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")
        else:
            updater.print_verbose("Doing nothing, not ready for update")
        return {"FINISHED"}


# User preference check-now operator
class AddonUpdaterCheckNow(bpy.types.Operator):
    bl_label = "Check now for " + updater.addon + " update"
    bl_idname = updater.addon + ".updater_check_now"
    bl_description = "Check now for an update to the {} addon".format(updater.addon)
    bl_options = {"REGISTER", "INTERNAL"}

    def execute(self, context):
        if updater.invalid_updater:
            return {"CANCELLED"}

        if updater.async_checking and updater.error is None:
            # Check already happened.
            # Used here to just avoid constant applying settings below.
            # Ignoring if error, to prevent being stuck on the error screen.
            return {"CANCELLED"}

        # apply the UI settings
        settings = get_user_preferences(context)
        if not settings:
            updater.print_verbose(
                "Could not get {} preferences, update check skipped".format(__package__)
            )
            return {"CANCELLED"}

        updater.set_check_interval(
            enable_prereleases=settings.enable_prereleases,
            enabled=settings.auto_check_update,
            months=settings.updater_interval_months,
            days=settings.updater_interval_days,
            hours=settings.updater_interval_hours,
            minutes=settings.updater_interval_minutes,
        )

        # Input is an optional callback function. This function should take a
        # bool input. If true: update ready, if false: no update ready.
        updater.check_for_update_now(ui_refresh)

        return {"FINISHED"}


class AddonUpdaterUpdateNow(bpy.types.Operator):
    bl_label = "Update " + updater.addon + " addon now"
    bl_idname = updater.addon + ".updater_update_now"
    bl_description = "Update to the latest version of the {x} addon".format(
        x=updater.addon
    )
    bl_options = {"REGISTER", "INTERNAL"}

    # If true, run clean install - ie remove all files before adding new
    # equivalent to deleting the addon and reinstalling, except the updater
    # folder/backup folder remains.
    clean_install = bpy.props.BoolProperty(
        name="Clean install",
        description=(
            "If enabled, completely clear the addon's folder before "
            "installing new update, creating a fresh install"
        ),
        default=False,
        options={"HIDDEN"},
    )

    def execute(self, context):
        # in case of error importing updater
        if updater.invalid_updater:
            return {"CANCELLED"}

        if updater.manual_only:
            bpy.ops.wm.url_open(url=updater.website)
        if updater.update_ready:
            # if it fails, offer to open the website instead
            try:
                res = updater.run_update(
                    force=False, callback=post_update_callback, clean=self.clean_install
                )

                # Should return 0, if not something happened.
                if updater.verbose:
                    if res == 0:
                        bk_logger.info("Updater returned successful")
                    else:
                        bk_logger.info("Updater error response: {}".format(res))
            except Exception as expt:
                updater._error = "Error trying to run update"
                updater._error_msg = str(expt)
                updater.print_trace()
                atr = AddonUpdaterInstallManually.bl_idname.split(".")
                getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")
        elif updater.update_ready is None:
            (update_ready, version, link) = updater.check_for_update(now=True)
            # Re-launch this dialog.
            atr = AddonUpdaterInstallPopup.bl_idname.split(".")
            getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")

        elif not updater.update_ready:
            self.report({"INFO"}, "Nothing to update")
            return {"CANCELLED"}
        else:
            self.report({"ERROR"}, "Encountered a problem while trying to update")
            return {"CANCELLED"}

        return {"FINISHED"}


class AddonUpdaterUpdateTarget(bpy.types.Operator):
    bl_label = updater.addon + " version target"
    bl_idname = updater.addon + ".updater_update_target"
    bl_description = "Install a targeted version of the {x} addon".format(
        x=updater.addon
    )
    bl_options = {"REGISTER", "INTERNAL"}

    def target_version(self, context):
        # In case of error importing updater.
        if updater.invalid_updater:
            ret = []

        ret = []
        i = 0
        for tag in updater.tags:
            ret.append((tag, tag, "Select to install " + tag))
            i += 1
        return ret

    target = bpy.props.EnumProperty(
        name="Target version to install",
        description="Select the version to install",
        items=target_version,
    )

    # If true, run clean install - ie remove all files before adding new
    # equivalent to deleting the addon and reinstalling, except the
    # updater folder/backup folder remains.
    clean_install = bpy.props.BoolProperty(
        name="Clean install",
        description=(
            "If enabled, completely clear the addon's folder before "
            "installing new update, creating a fresh install"
        ),
        default=False,
        options={"HIDDEN"},
    )

    @classmethod
    def poll(cls, context):
        if updater.invalid_updater:
            return False
        return updater.update_ready is not None and len(updater.tags) > 0

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    def draw(self, context):
        layout = self.layout
        if updater.invalid_updater:
            layout.label(text="Updater error")
            return
        split = layout_split(layout, factor=0.5)
        sub_col = split.column()
        sub_col.label(text="Select install version")
        sub_col = split.column()
        sub_col.prop(self, "target", text="")

    def execute(self, context):
        # In case of error importing updater.
        if updater.invalid_updater:
            return {"CANCELLED"}

        res = updater.run_update(
            force=False,
            revert_tag=self.target,
            callback=post_update_callback,
            clean=self.clean_install,
        )

        # Should return 0, if not something happened.
        if res == 0:
            updater.print_verbose("Updater returned successful")
        else:
            updater.print_verbose("Updater returned {}, , error occurred".format(res))
            return {"CANCELLED"}

        return {"FINISHED"}


class AddonUpdaterInstallManually(bpy.types.Operator):
    """As a fallback, direct the user to download the addon manually"""

    bl_label = "Install update manually"
    bl_idname = updater.addon + ".updater_install_manually"
    bl_description = "Proceed to manually install update"
    bl_options = {"REGISTER", "INTERNAL"}

    error = bpy.props.StringProperty(
        name="Error Occurred", default="", options={"HIDDEN"}
    )

    def invoke(self, context, event):
        return context.window_manager.invoke_popup(self)

    def draw(self, context):
        layout = self.layout

        if updater.invalid_updater:
            layout.label(text="Updater error")
            return

        # Display error if a prior automated install failed.
        if self.error != "":
            col = layout.column()
            col.scale_y = 0.7
            col.label(text="There was an issue trying to auto-install", icon="ERROR")
            col.label(text="Press the download button below and install", icon="BLANK1")
            col.label(text="the zip file like a normal addon.", icon="BLANK1")
        else:
            col = layout.column()
            col.scale_y = 0.7
            col.label(text="Install the addon manually")
            col.label(text="Press the download button below and install")
            col.label(text="the zip file like a normal addon.")

        # If check hasn't happened, i.e. accidentally called this menu,
        # allow to check here.

        row = layout.row()

        if updater.update_link is not None:
            row.operator("wm.url_open", text="Direct download").url = (
                updater.update_link
            )
        else:
            row.operator("wm.url_open", text="(failed to retrieve direct download)")
            row.enabled = False

            if updater.website is not None:
                row = layout.row()
                ops = row.operator("wm.url_open", text="Open website")
                ops.url = updater.website
            else:
                row = layout.row()
                row.label(text="See source website to download the update")

    def execute(self, context):
        return {"FINISHED"}


class AddonUpdaterUpdatedSuccessful(bpy.types.Operator):
    """Addon in place, popup telling user it completed or what went wrong"""

    bl_label = "Installation Report"
    bl_idname = updater.addon + ".updater_update_successful"
    bl_description = "Update installation response"
    bl_options = {"REGISTER", "INTERNAL", "UNDO"}

    error = bpy.props.StringProperty(
        name="Error Occurred", default="", options={"HIDDEN"}
    )

    def invoke(self, context, event):
        # we need to restart Blendkit-Client after update
        try:
            client_lib.shutdown_client()
        except:
            pass
        return context.window_manager.invoke_props_popup(self, event)

    def draw(self, context):
        layout = self.layout

        if updater.invalid_updater:
            layout.label(text="Updater error")
            return

        saved = updater.json
        if self.error != "":
            col = layout.column()
            col.scale_y = 0.7
            col.label(text="Error occurred, did not install", icon="ERROR")
            if updater.error_msg:
                msg = updater.error_msg
            else:
                msg = self.error
            col.label(text=str(msg), icon="BLANK1")
            rw = col.row()
            rw.scale_y = 2
            rw.operator(
                "wm.url_open", text="Click for manual download.", icon="BLANK1"
            ).url = updater.website
        elif not updater.auto_reload_post_update:
            # Tell user to restart blender after an update/restore!
            if "just_restored" in saved and saved["just_restored"]:
                col = layout.column()
                col.label(text="Addon restored", icon="RECOVER_LAST")
                alert_row = col.row()
                alert_row.alert = True
                alert_row.operator(
                    "wm.quit_blender", text="Restart blender to reload", icon="BLANK1"
                )
                updater.json_reset_restore()
            else:
                col = layout.column()
                col.label(text="Addon successfully installed", icon="FILE_TICK")
                alert_row = col.row()
                alert_row.alert = True
                alert_row.operator(
                    "wm.quit_blender", text="Restart blender to reload", icon="BLANK1"
                )

        else:
            # reload addon, but still recommend they restart blender
            if "just_restored" in saved and saved["just_restored"]:
                col = layout.column()
                col.scale_y = 0.7
                col.label(text="Addon restored", icon="RECOVER_LAST")
                col.label(
                    text="Consider restarting blender to fully reload.", icon="BLANK1"
                )
                updater.json_reset_restore()
            else:
                col = layout.column()
                col.scale_y = 0.7
                col.label(text="Addon successfully installed", icon="FILE_TICK")
                col.label(
                    text="Consider restarting blender to fully reload.", icon="BLANK1"
                )

    def execute(self, context):
        return {"FINISHED"}


class AddonUpdaterRestoreBackup(bpy.types.Operator):
    """Restore addon from backup"""

    bl_label = "Restore backup"
    bl_idname = updater.addon + ".updater_restore_backup"
    bl_description = "Restore addon from backup"
    bl_options = {"REGISTER", "INTERNAL"}

    @classmethod
    def poll(cls, context):
        try:
            return os.path.isdir(os.path.join(updater.stage_path, "backup"))
        except:
            return False

    def execute(self, context):
        # in case of error importing updater
        if updater.invalid_updater:
            return {"CANCELLED"}
        updater.restore_backup()
        return {"FINISHED"}


class AddonUpdaterIgnore(bpy.types.Operator):
    """Ignore update to prevent future popups"""

    bl_label = "Ignore update"
    bl_idname = updater.addon + ".updater_ignore"
    bl_description = "Ignore update to prevent future popups"
    bl_options = {"REGISTER", "INTERNAL"}

    @classmethod
    def poll(cls, context):
        if updater.invalid_updater:
            return False
        elif updater.update_ready:
            return True
        else:
            return False

    def execute(self, context):
        # in case of error importing updater
        if updater.invalid_updater:
            return {"CANCELLED"}
        updater.ignore_update()
        self.report({"INFO"}, "Open addon preferences for updater options")
        return {"FINISHED"}


class AddonUpdaterEndBackground(bpy.types.Operator):
    """Stop checking for update in the background"""

    bl_label = "End background check"
    bl_idname = updater.addon + ".end_background_check"
    bl_description = "Stop checking for update in the background"
    bl_options = {"REGISTER", "INTERNAL"}

    def execute(self, context):
        # in case of error importing updater
        if updater.invalid_updater:
            return {"CANCELLED"}
        updater.stop_async_check_update()
        return {"FINISHED"}


# -----------------------------------------------------------------------------
# Handler related, to create popups
# -----------------------------------------------------------------------------


# global vars used to prevent duplicate popup handlers
ran_auto_check_install_popup = False
ran_update_success_popup = False

# global var for preventing successive calls
ran_background_check = False


@persistent
def updater_run_success_popup_handler(scene):
    global ran_update_success_popup
    ran_update_success_popup = True

    # in case of error importing updater
    if updater.invalid_updater:
        return

    try:
        if "scene_update_post" in dir(bpy.app.handlers):
            bpy.app.handlers.scene_update_post.remove(updater_run_success_popup_handler)
        else:
            bpy.app.handlers.depsgraph_update_post.remove(
                updater_run_success_popup_handler
            )
    except:
        pass

    atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".")
    getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")


@persistent
def updater_run_install_popup_handler(scene):
    global ran_auto_check_install_popup
    ran_auto_check_install_popup = True
    updater.print_verbose("Running the install popup handler.")

    # in case of error importing updater
    if updater.invalid_updater:
        return

    try:
        if "scene_update_post" in dir(bpy.app.handlers):
            bpy.app.handlers.scene_update_post.remove(updater_run_install_popup_handler)
        else:
            bpy.app.handlers.depsgraph_update_post.remove(
                updater_run_install_popup_handler
            )
    except Exception as e:
        bk_logger.error("%s", e)
        pass

    if "ignore" in updater.json and updater.json["ignore"]:
        return  # Don't do popup if ignore pressed.
    elif "version_text" in updater.json and updater.json["version_text"].get("version"):
        version = updater.json["version_text"]["version"]
        ver_tuple = updater.version_tuple_from_text(version)

        if ver_tuple < updater.current_version:
            # User probably manually installed to get the up to date addon
            # in here. Clear out the update flag using this function.
            updater.print_verbose(
                "{} updater: appears user updated, clearing flag".format(updater.addon)
            )
            updater.json_reset_restore()
            return
    atr = AddonUpdaterInstallPopup.bl_idname.split(".")
    getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")


def background_update_callback(update_ready):
    """Passed into the updater, background thread updater"""
    global ran_auto_check_install_popup
    updater.print_verbose("Running background update callback")

    # In case of error importing updater.
    if updater.invalid_updater:
        return
    if not updater.show_popups:
        return
    if not update_ready:
        return

    # See if we need add to the update handler to trigger the popup.
    handlers = []
    if "scene_update_post" in dir(bpy.app.handlers):  # 2.7x
        handlers = bpy.app.handlers.scene_update_post
    else:  # 2.8+
        handlers = bpy.app.handlers.depsgraph_update_post
    in_handles = updater_run_install_popup_handler in handlers

    if in_handles or ran_auto_check_install_popup:
        return

    if "scene_update_post" in dir(bpy.app.handlers):  # 2.7x
        bpy.app.handlers.scene_update_post.append(updater_run_install_popup_handler)
    else:  # 2.8+
        bpy.app.handlers.depsgraph_update_post.append(updater_run_install_popup_handler)
    ran_auto_check_install_popup = True
    updater.print_verbose("Attempted popup prompt")


def post_update_callback(module_name, res=None):
    """Callback for once the run_update function has completed.

    Only makes sense to use this if "auto_reload_post_update" == False,
    i.e. don't auto-restart the addon.

    Arguments:
        module_name: returns the module name from updater, but unused here.
        res: If an error occurred, this is the detail string.
    """

    # In case of error importing updater.
    if updater.invalid_updater:
        return

    if res is None:
        # This is the same code as in conditional at the end of the register
        # function, ie if "auto_reload_post_update" == True, skip code.
        updater.print_verbose(
            "{} updater: Running post update callback".format(updater.addon)
        )

        atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".")
        getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")
        global ran_update_success_popup
        ran_update_success_popup = True
    else:
        # Some kind of error occurred and it was unable to install, offer
        # manual download instead.
        atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".")
        getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT", error=res)
    return


def ui_refresh(update_status):
    """Redraw the ui once an async thread has completed"""
    for windowManager in bpy.data.window_managers:
        for window in windowManager.windows:
            for area in window.screen.areas:
                area.tag_redraw()


def check_for_update_background():
    """Function for asynchronous background check.

    *Could* be called on register, but would be bad practice as the bare
    minimum code should run at the moment of registration (addon ticked).
    """
    if updater.invalid_updater:
        return
    global ran_background_check
    if ran_background_check:
        # Global var ensures check only happens once.
        return
    elif updater.update_ready is not None or updater.async_checking:
        # Check already happened.
        # Used here to just avoid constant applying settings below.
        return

    # Apply the UI settings.
    settings = get_user_preferences(bpy.context)
    if not settings:
        return
    updater.set_check_interval(
        enable_prereleases=settings.enable_prereleases,
        enabled=settings.auto_check_update,
        months=settings.updater_interval_months,
        days=settings.updater_interval_days,
        hours=settings.updater_interval_hours,
        minutes=settings.updater_interval_minutes,
    )

    # Input is an optional callback function. This function should take a bool
    # input, if true: update ready, if false: no update ready.
    updater.check_for_update_async(background_update_callback)
    ran_background_check = True


def check_for_update_nonthreaded(self, context):
    """Can be placed in front of other operators to launch when pressed"""
    if updater.invalid_updater:
        return

    # Only check if it's ready, ie after the time interval specified should
    # be the async wrapper call here.
    settings = get_user_preferences(bpy.context)
    if not settings:
        if updater.verbose:
            bk_logger.info(
                "Could not get %s preferences, update check skipped", __package__
            )
        return
    updater.set_check_interval(
        enable_prereleases=settings.enable_prereleases,
        enabled=settings.auto_check_update,
        months=settings.updater_interval_months,
        days=settings.updater_interval_days,
        hours=settings.updater_interval_hours,
        minutes=settings.updater_interval_minutes,
    )

    (update_ready, version, link) = updater.check_for_update(now=False)
    if update_ready:
        atr = AddonUpdaterInstallPopup.bl_idname.split(".")
        getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT")
    else:
        updater.print_verbose("No update ready")
        self.report({"INFO"}, "No update ready")


def show_reload_popup():
    """For use in register only, to show popup after re-enabling the addon.

    Must be enabled by developer.
    """
    if updater.invalid_updater:
        return
    saved_state = updater.json
    global ran_update_success_popup

    has_state = saved_state is not None
    just_updated = "just_updated" in saved_state
    updated_info = saved_state["just_updated"]

    if not (has_state and just_updated and updated_info):
        return

    updater.json_reset_postupdate()  # So this only runs once.

    # No handlers in this case.
    if not updater.auto_reload_post_update:
        return

    # See if we need add to the update handler to trigger the popup.
    handlers = []
    if "scene_update_post" in dir(bpy.app.handlers):  # 2.7x
        handlers = bpy.app.handlers.scene_update_post
    else:  # 2.8+
        handlers = bpy.app.handlers.depsgraph_update_post
    in_handles = updater_run_success_popup_handler in handlers

    if in_handles or ran_update_success_popup:
        return

    if "scene_update_post" in dir(bpy.app.handlers):  # 2.7x
        bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler)
    else:  # 2.8+
        bpy.app.handlers.depsgraph_update_post.append(updater_run_success_popup_handler)
    ran_update_success_popup = True


# -----------------------------------------------------------------------------
# Example UI integrations
# -----------------------------------------------------------------------------
def update_notice_box_ui(self, context):
    """Update notice draw, to add to the end or beginning of a panel.

    After a check for update has occurred, this function will draw a box
    saying an update is ready, and give a button for: update now, open website,
    or ignore popup. Ideal to be placed at the end / beginning of a panel.
    """

    if updater.invalid_updater:
        return

    saved_state = updater.json
    if not updater.auto_reload_post_update:
        if "just_updated" in saved_state and saved_state["just_updated"]:
            layout = self.layout
            box = layout.box()
            col = box.column()
            alert_row = col.row()
            alert_row.alert = True
            alert_row.operator("wm.quit_blender", text="Restart blender", icon="ERROR")
            col.label(text="to complete update")
            return

    # If user pressed ignore, don't draw the box.
    if "ignore" in updater.json and updater.json["ignore"]:
        return
    if not updater.update_ready:
        return

    layout = self.layout
    box = layout.box()
    col = box.column(align=True)
    col.alert = True
    col.label(text="Update ready!", icon="ERROR")
    col.alert = False
    col.separator()
    row = col.row(align=True)
    split = row.split(align=True)
    colL = split.column(align=True)
    colL.scale_y = 1.5
    colL.operator(AddonUpdaterIgnore.bl_idname, icon="X", text="Ignore")
    colR = split.column(align=True)
    colR.scale_y = 1.5
    if not updater.manual_only:
        colR.operator(
            AddonUpdaterUpdateNow.bl_idname, text="Update", icon="LOOP_FORWARDS"
        )
        col.operator("wm.url_open", text="Open website").url = updater.website
        # ops = col.operator("wm.url_open",text="Direct download")
        # ops.url=updater.update_link
        col.operator(AddonUpdaterInstallManually.bl_idname, text="Install manually")
    else:
        # ops = col.operator("wm.url_open", text="Direct download")
        # ops.url=updater.update_link
        col.operator("wm.url_open", text="Get it now").url = updater.website


def update_settings_ui(self, context, element=None):
    """Preferences - for drawing with full width inside user preferences

    A function that can be run inside user preferences panel for prefs UI.
    Place inside UI draw using:
        addon_updater_ops.update_settings_ui(self, context)
    or by:
        addon_updater_ops.update_settings_ui(context)
    """

    # Element is a UI element, such as layout, a row, column, or box.
    if element is None:
        element = self.layout
    box = element.box()

    # In case of error importing updater.
    if updater.invalid_updater:
        box.label(text="Error initializing updater code:")
        box.label(text=updater.error_msg)
        return
    settings = get_user_preferences(context)
    if not settings:
        box.label(text="Error getting updater preferences", icon="ERROR")
        return

    # auto-update settings
    box.label(text="Updater Settings")
    row = box.row()

    # special case to tell user to restart blender, if set that way
    if not updater.auto_reload_post_update:
        saved_state = updater.json
        if "just_updated" in saved_state and saved_state["just_updated"]:
            row.alert = True
            row.operator(
                "wm.quit_blender",
                text="Restart blender to complete update",
                icon="ERROR",
            )
            return

    split = layout_split(row, factor=0.4)
    sub_col = split.column()
    sub_col.prop(settings, "auto_check_update")
    sub_col.prop(settings, "enable_prereleases")
    sub_col = split.column()

    if not settings.auto_check_update:
        sub_col.enabled = False
    sub_row = sub_col.row()
    sub_row.label(text="Interval between checks")
    sub_row = sub_col.row(align=True)
    check_col = sub_row.column(align=True)
    check_col.prop(settings, "updater_interval_months")
    check_col = sub_row.column(align=True)
    check_col.prop(settings, "updater_interval_days")
    check_col = sub_row.column(align=True)

    # Consider un-commenting for local dev (e.g. to set shorter intervals)
    # check_col.prop(settings,"updater_interval_hours")
    # check_col = sub_row.column(align=True)
    # check_col.prop(settings,"updater_interval_minutes")

    # Checking / managing updates.
    row = box.row()
    col = row.column()
    if updater.error is not None:
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.scale_y = 2
        if "ssl" in updater.error_msg.lower():
            split.enabled = True
            split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error)
        else:
            split.enabled = False
            split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error)
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    elif updater.update_ready is None and not updater.async_checking:
        col.scale_y = 2
        col.operator(AddonUpdaterCheckNow.bl_idname)
    elif updater.update_ready is None:  # async is running
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.enabled = False
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...")
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X")

    elif (
        updater.include_branches
        and len(updater.tags) == len(updater.include_branch_list)
        and not updater.manual_only
    ):
        # No releases found, but still show the appropriate branch.
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.scale_y = 2
        update_now_txt = "Update directly to {}".format(updater.include_branch_list[0])
        split.operator(AddonUpdaterUpdateNow.bl_idname, text=update_now_txt)
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    elif updater.update_ready and not updater.manual_only:
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(
            AddonUpdaterUpdateNow.bl_idname,
            text="Update now to " + str(updater.update_version),
        )
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    elif updater.update_ready and updater.manual_only:
        col.scale_y = 2
        dl_now_txt = "Download " + str(updater.update_version)
        col.operator("wm.url_open", text=dl_now_txt).url = updater.website
    else:  # i.e. that updater.update_ready == False.
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.enabled = False
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date")
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    if not updater.manual_only:
        col = row.column(align=True)
        if updater.include_branches and len(updater.include_branch_list) > 0:
            branch = updater.include_branch_list[0]
            col.operator(
                AddonUpdaterUpdateTarget.bl_idname,
                text="Install {} / old version".format(branch),
            )
        else:
            col.operator(
                AddonUpdaterUpdateTarget.bl_idname, text="(Re)install addon version"
            )
        last_date = "none found"
        backup_path = os.path.join(updater.stage_path, "backup")
        if "backup_date" in updater.json and os.path.isdir(backup_path):
            if updater.json["backup_date"] == "":
                last_date = "Date not found"
            else:
                last_date = updater.json["backup_date"]
        backup_text = "Restore addon backup ({})".format(last_date)
        col.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text)

    row = box.row()
    row.scale_y = 0.7
    last_check = updater.json["last_check"]
    if updater.error is not None and updater.error_msg is not None:
        row.label(text=updater.error_msg)
    elif last_check:
        last_check = last_check[0 : last_check.index(".")]
        row.label(text="Last update check: " + last_check)
    else:
        row.label(text="Last update check: Never")

    version_row = box.row()
    version_row.label(
        text=f"Blendkit v{utils.get_addon_version()} · Blender {bpy.app.version_string}",
        icon="INFO",
    )
    version_row.operator(
        "wm.blenderkit_copy_environment_info", text="Copy Info", icon="COPYDOWN"
    )


def update_settings_ui_condensed(self, context, element=None):
    """Preferences - Condensed drawing within preferences.

    Alternate draw for user preferences or other places, does not draw a box.
    """

    # Element is a UI element, such as layout, a row, column, or box.
    if element is None:
        element = self.layout
    row = element.row()

    # In case of error importing updater.
    if updater.invalid_updater:
        row.label(text="Error initializing updater code:")
        row.label(text=updater.error_msg)
        return
    settings = get_user_preferences(context)
    if not settings:
        row.label(text="Error getting updater preferences", icon="ERROR")
        return

    # Special case to tell user to restart blender, if set that way.
    if not updater.auto_reload_post_update:
        saved_state = updater.json
        if "just_updated" in saved_state and saved_state["just_updated"]:
            row.alert = True  # mark red
            row.operator(
                "wm.quit_blender",
                text="Restart blender to complete update",
                icon="ERROR",
            )
            return

    col = row.column()
    if updater.error is not None:
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.scale_y = 2
        if "ssl" in updater.error_msg.lower():
            split.enabled = True
            split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error)
        else:
            split.enabled = False
            split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error)
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    elif updater.update_ready is None and not updater.async_checking:
        col.scale_y = 2
        col.operator(AddonUpdaterCheckNow.bl_idname)
    elif updater.update_ready is None:  # Async is running.
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.enabled = False
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...")
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X")

    elif (
        updater.include_branches
        and len(updater.tags) == len(updater.include_branch_list)
        and not updater.manual_only
    ):
        # No releases found, but still show the appropriate branch.
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.scale_y = 2
        now_txt = "Update directly to " + str(updater.include_branch_list[0])
        split.operator(AddonUpdaterUpdateNow.bl_idname, text=now_txt)
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    elif updater.update_ready and not updater.manual_only:
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(
            AddonUpdaterUpdateNow.bl_idname,
            text="Update now to " + str(updater.update_version),
        )
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    elif updater.update_ready and updater.manual_only:
        col.scale_y = 2
        dl_txt = "Download " + str(updater.update_version)
        col.operator("wm.url_open", text=dl_txt).url = updater.website
    else:  # i.e. that updater.update_ready == False.
        sub_col = col.row(align=True)
        sub_col.scale_y = 1
        split = sub_col.split(align=True)
        split.enabled = False
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date")
        split = sub_col.split(align=True)
        split.scale_y = 2
        split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH")

    row = element.row()
    row.prop(settings, "auto_check_update")

    row = element.row()
    row.scale_y = 0.7
    last_check = updater.json["last_check"]
    if updater.error is not None and updater.error_msg is not None:
        row.label(text=updater.error_msg)
    elif last_check != "" and last_check is not None:
        last_check = last_check[0 : last_check.index(".")]
        row.label(text="Last check: " + last_check)
    else:
        row.label(text="Last check: Never")


def skip_tag_function(self, tag):
    """A global function for tag skipping.

    A way to filter which tags are displayed, e.g. to limit downgrading too
    long ago.

    Args:
        self: The instance of the singleton addon update.
        tag: the text content of a tag from the repo, e.g. "v1.2.3".

    Returns:
        bool: True to skip this tag name (ie don't allow for downloading this
            version), or False if the tag is allowed.
    """

    # In case of error importing updater.
    if self.invalid_updater:
        return False

    # ---- write any custom code here, return true to disallow version ---- #
    #
    # # Filter out e.g. if 'beta' is in name of release
    if get_user_preferences().enable_prereleases is False:
        if "alpha" in tag["name"].lower():
            return True
        if "beta" in tag["name"].lower():
            return True
        if "rc" in tag["name"].lower():
            return True
    # ---- write any custom code above, return true to disallow version --- #

    if self.include_branches:
        for branch in self.include_branch_list:
            if tag["name"].lower() == branch:
                return False

    # Function converting string to tuple, ignoring e.g. leading 'v'.
    # Be aware that this strips out other text that you might otherwise
    # want to be kept and accounted for when checking tags (e.g. v1.1a vs 1.1b)
    tupled = self.version_tuple_from_text(tag["name"])
    if not isinstance(tupled, tuple):
        return True

    # Select the min tag version - change tuple accordingly.
    if self.version_min_update is not None:
        if tupled < self.version_min_update:
            return True  # Skip if current version below this.

    # Select the max tag version.
    if self.version_max_update is not None:
        if tupled >= self.version_max_update:
            return True  # Skip if current version at or above this.

    # In all other cases, allow showing the tag for updating/reverting.
    # To simply and always show all tags, this return False could be moved
    # to the start of the function definition so all tags are allowed.
    return False


def select_link_function(self, tag):
    """Only customize if trying to leverage "attachments" in *GitHub* releases.

    A way to select from one or multiple attached downloadable files from the
    server, instead of downloading the default release/tag source code.
    """

    # -- Default, universal case (and is the only option for GitLab/Bitbucket)
    link = tag["zipball_url"]

    # -- Example: select the first (or only) asset instead source code --
    if "assets" in tag and "browser_download_url" in tag["assets"][0]:
        link = tag["assets"][0]["browser_download_url"]

    # -- Example: select asset based on OS, where multiple builds exist --
    # # not tested/no error checking, modify to fit your own needs!
    # # assume each release has three attached builds:
    # #		release_windows.zip, release_OSX.zip, release_linux.zip
    # # This also would logically not be used with "branches" enabled
    # if platform.system() == "Darwin": # ie OSX
    # 	link = [asset for asset in tag["assets"] if 'OSX' in asset][0]
    # elif platform.system() == "Windows":
    # 	link = [asset for asset in tag["assets"] if 'windows' in asset][0]
    # elif platform.system() == "Linux":
    # 	link = [asset for asset in tag["assets"] if 'linux' in asset][0]

    return link


# -----------------------------------------------------------------------------
# Register, should be run in the register module itself
# -----------------------------------------------------------------------------
classes = (
    AddonUpdaterInstallPopup,
    AddonUpdaterCheckNow,
    AddonUpdaterUpdateNow,
    AddonUpdaterUpdateTarget,
    AddonUpdaterInstallManually,
    AddonUpdaterUpdatedSuccessful,
    AddonUpdaterRestoreBackup,
    AddonUpdaterIgnore,
    AddonUpdaterEndBackground,
)


def register(bl_info):
    """Registering the operators in this module"""
    # Safer failure in case of issue loading module.
    if updater.error:
        bk_logger.error("Exiting updater registration, %s", updater.error)
        return
    updater.clear_state()  # Clear internal vars, avoids reloading oddities.

    # Confirm your updater "engine" (Github is default if not specified).
    updater.engine = "Github"
    # updater.engine = "GitLab"
    # updater.engine = "Bitbucket"

    # If using private repository, indicate the token here.
    # Must be set after assigning the engine.
    # **WARNING** Depending on the engine, this token can act like a password!!
    # Only provide a token if the project is *non-public*, see readme for
    # other considerations and suggestions from a security standpoint.
    updater.private_token = None  # "tokenstring"

    # Choose your own username, must match website (not needed for GitLab).
    updater.user = "blenderkit"

    # Choose your own repository, must match git name for GitHUb and Bitbucket,
    # for GitLab use project ID (numbers only).
    updater.repo = "blenderkit"

    # updater.addon = # define at top of module, MUST be done first

    # Website for manual addon download, optional but recommended to set.
    updater.website = "https://www.blendkit.com/get-blendkit/"

    # Addon subfolder path.
    # "sample/path/to/addon"
    # default is "" or None, meaning root
    updater.subfolder_path = ""

    # Used to check/compare versions.
    updater.current_version = bl_info["version"]

    # Optional, to hard-set update frequency, use this here - however, this
    # demo has this set via UI properties.
    # updater.set_check_interval(enabled=False, months=0, days=0, hours=0, minutes=2)

    # Optional, consider turning off for production or allow as an option
    # This will print out additional debugging info to the console
    updater.verbose = True  # make False for production default

    # Optional, customize where the addon updater processing subfolder is,
    # essentially a staging folder used by the updater on its own
    # Needs to be within the same folder as the addon itself
    # Need to supply a full, absolute path to folder
    # updater.updater_path = # set path of updater folder, by default:
    # 			/addons/{__package__}/{__package__}_updater

    # Auto create a backup of the addon when installing other versions.
    updater.backup_current = True  # True by default

    # Sample ignore patterns for when creating backup of current during update.
    updater.backup_ignore_patterns = ["__pycache__"]
    # Alternate example patterns:
    # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"]

    # Patterns for files to actively overwrite if found in new update file and
    # are also found in the currently installed addon. Note that by default
    # (ie if set to []), updates are installed in the same way as blender:
    # .py files are replaced, but other file types (e.g. json, txt, blend)
    # will NOT be overwritten if already present in current install. Thus
    # if you want to automatically update resources/non py files, add them as a
    # part of the pattern list below so they will always be overwritten by an
    # update. If a pattern file is not found in new update, no action is taken
    # NOTE: This does NOT delete anything proactively, rather only defines what
    # is allowed to be overwritten during an update execution.
    updater.overwrite_patterns = ["*"]
    # updater.overwrite_patterns = []
    # other examples:
    # ["*"] means ALL files/folders will be overwritten by update, was the
    #    behavior pre updater v1.0.4.
    # [] or ["*.py","*.pyc"] matches default blender behavior, ie same effect
    #    if user installs update manually without deleting the existing addon
    #    first e.g. if existing install and update both have a resource.blend
    #    file, the existing installed one will remain.
    # ["some.py"] means if some.py is found in addon update, it will overwrite
    #    any existing some.py in current addon install, if any.
    # ["*.json"] means all json files found in addon update will overwrite
    #    those of same name in current install.
    # ["*.png","README.md","LICENSE.txt"] means the readme, license, and all
    #    pngs will be overwritten by update.

    # Patterns for files to actively remove prior to running update.
    # Useful if wanting to remove old code due to changes in filenames
    # that otherwise would accumulate. Note: this runs after taking
    # a backup (if enabled) but before placing in new update. If the same
    # file name removed exists in the update, then it acts as if pattern
    # is placed in the overwrite_patterns property. Note this is effectively
    # ignored if clean=True in the run_update method.
    updater.remove_pre_update_patterns = ["*"]
    # Note setting ["*"] here is equivalent to always running updates with
    # clean = True in the run_update method, ie the equivalent of a fresh,
    # new install. This would also delete any resources or user-made/modified
    # files setting ["__pycache__"] ensures the pycache folder always removed.
    # The configuration of ["*.py", "*.pyc"] is a safe option as this
    # will ensure no old python files/caches remain in event different addon
    # versions have different filenames or structures.

    # Allow branches like 'master' as an option to update to, regardless
    # of release or version.
    # Default behavior: releases will still be used for auto check (popup),
    # but the user has the option from user preferences to directly
    # update to the master branch or any other branches specified using
    # the "install {branch}/older version" operator.
    updater.include_branches = False

    # (GitHub only) This options allows using "releases" instead of "tags",
    # which enables pulling down release logs/notes, as well as installs update
    # from release-attached zips (instead of the auto-packaged code generated
    # with a release/tag). Setting has no impact on BitBucket or GitLab repos.
    updater.use_releases = True
    # Note: Releases always have a tag, but a tag may not always be a release.
    # Therefore, setting True above will filter out any non-annotated tags.
    # Note 2: Using this option will also display (and filter by) the release
    # name instead of the tag name, bear this in mind given the
    # skip_tag_function filtering above.

    # Populate if using "include_branches" option above.
    # Note: updater.include_branch_list defaults to ['master'] branch if set to
    # none. Example targeting another multiple branches allowed to pull from:
    # updater.include_branch_list = ['master', 'dev']
    updater.include_branch_list = None  # None is the equivalent = ['master']

    # Only allow manual install, thus prompting the user to open
    # the addon's web page to download, specifically: updater.website
    # Useful if only wanting to get notification of updates but not
    # directly install.
    updater.manual_only = False

    # Used for development only, "pretend" to install an update to test
    # reloading conditions.
    updater.fake_install = False  # Set to true to test callback/reloading.

    # Show popups, ie if auto-check for update is enabled or a previous
    # check for update in user preferences found a new version, show a popup
    # (at most once per blender session, and it provides an option to ignore
    # for future sessions); default behavior is set to True.
    updater.show_popups = True
    # note: if set to false, there will still be an "update ready" box drawn
    # using the `update_notice_box_ui` panel function.

    # Override with a custom function on what tags
    # to skip showing for updater; see code for function above.
    # Set the min and max versions allowed to install.
    # Optional, default None
    # min install (>=) will install this and higher
    updater.version_min_update = (0, 0, 0)
    # updater.version_min_update = None  # None or default for no minimum.

    # Max install (<) will install strictly anything lower than this version
    # number, useful to limit the max version a given user can install (e.g.
    # if support for a future version of blender is going away, and you don't
    # want users to be prompted to install a non-functioning addon)
    # updater.version_max_update = (9,9,9)
    updater.version_max_update = None  # None or default for no max.

    # Function defined above, customize as appropriate per repository
    updater.skip_tag = skip_tag_function  # min and max used in this function

    # Function defined above, optionally customize as needed per repository.
    updater.select_link = select_link_function

    # Recommended false to encourage blender restarts on update completion
    # Setting this option to True is NOT as stable as false (could cause
    # blender crashes).
    updater.auto_reload_post_update = False

    # The register line items for all operators/panels.
    # If using bpy.utils.register_module(__name__) to register elsewhere
    # in the addon, delete these lines (also from unregister).
    for cls in classes:
        # Apply annotations to remove Blender 2.8+ warnings, no effect on 2.7
        make_annotations(cls)
        # Comment out this line if using bpy.utils.register_module(__name__)
        bpy.utils.register_class(cls)

    # Special situation: we just updated the addon, show a popup to tell the
    # user it worked. Could enclosed in try/catch in case other issues arise.
    show_reload_popup()


def unregister():
    for cls in reversed(classes):
        # Comment out this line if using bpy.utils.unregister_module(__name__).
        bpy.utils.unregister_class(cls)

    # Clear global vars since they may persist if not restarting blender.
    updater.clear_state()  # Clear internal vars, avoids reloading oddities.

    global ran_auto_check_install_popup
    ran_auto_check_install_popup = False

    global ran_update_success_popup
    ran_update_success_popup = False

    global ran_background_check
    ran_background_check = False
