# ##### 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 #####


"""
See documentation for usage
https://github.com/CGCookie/blender-addon-updater

"""

import urllib.request
import urllib
import os
import json
import zipfile
import shutil
import asyncio # for async processing
import threading
import time
from datetime import datetime,timedelta

# blender imports, used in limited cases
import bpy
import addon_utils

# -----------------------------------------------------------------------------
# Define error messages/notices & hard coded globals
# -----------------------------------------------------------------------------

DEFAULT_API_URL = "https://api.github.com" # plausibly could be some other system
DEFAULT_TIMEOUT = 10
DEFAULT_PER_PAGE = 30


# -----------------------------------------------------------------------------
# The main class
# -----------------------------------------------------------------------------

class Singleton_updater(object):
    """
    This is the singleton class to reference a copy from,
    it is the shared module level class
    """
    
    def __init__(self):
        """
        #UPDATE
        :param user: string # name of the user owning the repository
        :param repo: string # name of the repository
        :param api_url: string # should just be the github api link
        :param timeout: integer # request timeout
        :param current_version: tuple # typically 3 values meaning the version #
        """

        self._user = None
        self._repo = None
        self._website = None
        self._api_url = DEFAULT_API_URL
        self._current_version = None
        self._tags = []
        self._tag_latest = None
        self._tag_names = []
        self._latest_release = None
        self._include_branches = False
        self._include_branch_list = ['master']
        self._include_branch_autocheck = False
        self._manual_only = False
        self._version_min_update = None
        self._version_max_update = None

        # by default, backup current addon if new is being loaded
        self._backup_current = True

        # by default, enable/disable the addon.. but less safe.
        self._auto_reload_post_update = False

        self._check_interval_enable = False
        self._check_interval_months = 0
        self._check_interval_days = 7
        self._check_interval_hours = 0
        self._check_interval_minutes = 0

        # runtime variables, initial conditions
        self._verbose = False
        self._fake_install = False
        self._async_checking = False # only true when async daemon started
        self._update_ready = None
        self._update_link = None
        self._update_version = None
        self._source_zip = None
        self._check_thread = None
        self._skip_tag = None

        # get from module data
        self._addon = __package__.lower()
        self._addon_package = __package__ # must not change
        self._updater_path = os.path.join(os.path.dirname(__file__),
                            self._addon+"_updater")
        self._addon_root = os.path.dirname(__file__)
        self._json = {}
        self._error = None
        self._error_msg = None
        self._prefiltered_tag_count = 0

        # to verify a valid import, in place of placeholder import
        self.invalidupdater = False


    # -------------------------------------------------------------------------
    # Getters and setters
    # -------------------------------------------------------------------------

    @property
    def addon(self):
        return self._addon
    @addon.setter
    def addon(self, value):
        self._addon = str(value)

    @property
    def verbose(self):
        return self._verbose
    @verbose.setter
    def verbose(self, value):
        try:
            self._verbose = bool(value)
            if self._verbose == True:
                print(self._addon+" updater verbose is enabled")
        except:
            raise ValueError("Verbose must be a boolean value")

    @property
    def include_branches(self):
        return self._include_branches
    @include_branches.setter
    def include_branches(self, value):
        try:
            self._include_branches = bool(value)
        except:
            raise ValueError("include_branches must be a boolean value")

    @property
    def include_branch_list(self):
        return self._include_branch_list
    @include_branch_list.setter
    def include_branch_list(self, value):
        try:
            if value is None:
                self._include_branch_list = ['master']
            elif type(value) != type(['master']):
                raise ValueError("include_branch_list should be a list of valid branches")
            else:
                self._include_branch_list = value
        except:
            raise ValueError("include_branch_list should be a list of valid branches")

    # not currently used
    @property
    def include_branch_autocheck(self):
        return self._include_branch_autocheck
    @include_branch_autocheck.setter
    def include_branch_autocheck(self, value):
        try:
            self._include_branch_autocheck = bool(value)
        except:
            raise ValueError("include_branch_autocheck must be a boolean value")


    @property
    def manual_only(self):
        return self._manual_only
    @manual_only.setter
    def manual_only(self, value):
        try:
            self._manual_only = bool(value)
        except:
            raise ValueError("manual_only must be a boolean value")

    @property
    def auto_reload_post_update(self):
        return self._auto_reload_post_update
    @auto_reload_post_update.setter
    def auto_reload_post_update(self, value):
        try:
            self._auto_reload_post_update = bool(value)
        except:
            raise ValueError("Must be a boolean value")

    @property
    def fake_install(self):
        return self._fake_install
    @fake_install.setter
    def fake_install(self, value):
        if type(value) != type(False):
            raise ValueError("fake_install must be a boolean value")
        self._fake_install = bool(value)
            
    @property
    def user(self):
        return self._user
    @user.setter
    def user(self, value):
        try:
            self._user = str(value)
        except:
            raise ValueError("User must be a string value")

    @property
    def json(self):
        if self._json == {}:
            self.set_updater_json()
        return self._json

    @property
    def repo(self):
        return self._repo
    @repo.setter
    def repo(self, value):
        try:
            self._repo = str(value)
        except:
            raise ValueError("User must be a string")

    @property
    def website(self):
        return self._website
    @website.setter
    def website(self, value):
        if self.check_is_url(value) == False:
            raise ValueError("Not a valid URL: " + value)
        self._website = value

    @property
    def async_checking(self):
        return self._async_checking

    @property
    def api_url(self):
        return self._api_url
    @api_url.setter
    def api_url(self, value):
        if self.check_is_url(value) == False:
            raise ValueError("Not a valid URL: " + value)
        self._api_url = value

    @property
    def stage_path(self):
        return self._updater_path
    @stage_path.setter
    def stage_path(self, value):
        if value is None:
            if self._verbose:print("Aborting assigning stage_path, it's null")
            return
        elif value is not None and not os.path.exists(value):
            try:
                os.makedirs(value)
            except:
                if self._verbose:print("Error trying to staging path")
                return
        self._updater_path = value


    @property
    def tags(self):
        if self._tags == []:
            return []
        tag_names = []
        for tag in self._tags:
            tag_names.append(tag["name"])

        return tag_names

    @property
    def tag_latest(self):
        if self._tag_latest is None:
            return None
        return self._tag_latest["name"]

    @property
    def latest_release(self):
        if self._releases_latest is None:
            return None
        return self._latest_release

    @property
    def current_version(self):
        return self._current_version

    @property
    def update_ready(self):
        return self._update_ready

    @property
    def update_version(self):
        return self._update_version

    @property
    def update_link(self):
        return self._update_link

    @current_version.setter
    def current_version(self,tuple_values):
        if type(tuple_values) is not tuple:
            raise ValueError(\
            "Not a tuple! current_version must be a tuple of integers")
        for i in tuple_values:
            if type(i) is not int:
                raise ValueError(\
                "Not an integer! current_version must be a tuple of integers")
        self._current_version = tuple_values

    def set_check_interval(self,enable=False,months=0,days=14,hours=0,minutes=0):
        # enabled = False, default initially will not check against frequency
        # if enabled, default is then 2 weeks

        if type(enable) is not bool:
            raise ValueError("Enable must be a boolean value")
        if type(months) is not int:
            raise ValueError("Months must be an integer value")
        if type(days) is not int:
            raise ValueError("Days must be an integer value")
        if type(hours) is not int:
            raise ValueError("Hours must be an integer value")
        if type(minutes) is not int:
            raise ValueError("Minutes must be an integer value")

        if enable==False:
            self._check_interval_enable = False
        else:
            self._check_interval_enable = True
        
        self._check_interval_months = months
        self._check_interval_days = days
        self._check_interval_hours = hours
        self._check_interval_minutes = minutes

    @property
    def check_interval(self):
        return (self._check_interval_enable,
                self._check_interval_months,
                self._check_interval_days,
                self._check_interval_hours,
                self._check_interval_minutes)

    @property
    def error(self):
        return self._error

    @property
    def error_msg(self):
        return self._error_msg

    @property
    def version_min_update(self):
        return self._version_min_update
    @version_min_update.setter
    def version_min_update(self, value):
        if value is None:
            self._version_min_update = None
            return
        if type(value) != type((1,2,3)):
            raise ValueError("Version minimum must be a tuple")
        else:
            # potentially check entries are integers
            self._version_min_update = value


    @property
    def version_max_update(self):
        return self._version_max_update
    @version_max_update.setter
    def version_max_update(self, value):
        if value is None:
            self._version_max_update = None
            return
        if type(value) != type((1,2,3)):
            raise ValueError("Version maximum must be a tuple")
        else:
            # potentially check entries are integers
            self._version_max_update = value



    # -------------------------------------------------------------------------
    # Parameter validation related functions
    # -------------------------------------------------------------------------


    def check_is_url(self,url):
        if not ("http://" in url or "https://" in url):
            return False
        if "." not in url:
            return False
        return True

    def get_tag_names(self):
        tag_names = []
        self.get_tags(self)
        for tag in self._tags:
            tag_names.append(tag["name"])
        return tag_names

    # declare how the class gets printed

    def __repr__(self):
        return "<Module updater from {a}>".format(a=__file__)

    def __str__(self):
        return "Updater, with user: {a}, repository: {b}, url: {c}".format(
                        a=self._user,
                        b=self._repo, c=self.form_repo_url())


    # -------------------------------------------------------------------------
    # API-related functions
    # -------------------------------------------------------------------------

    def form_repo_url(self):
        return self._api_url+"/repos/"+self.user+"/"+self.repo


    def get_tags(self):
        request = "/repos/"+self.user+"/"+self.repo+"/tags"
        if self.verbose:print("Getting tags from server")

        # get all tags, internet call
        all_tags = self.get_api(request)
        self._prefiltered_tag_count = len(all_tags)

        # pre-process to skip tags
        if self.skip_tag is not None:
            self._tags = [tg for tg in all_tags if self.skip_tag(tg)==False]
        else:
            self._tags = all_tags

        # get additional branches too, if needed, and place in front
        # does NO checking here whether branch is valid
        if self._include_branches == True:
            temp_branches = self._include_branch_list.copy()
            temp_branches.reverse()
            for branch in temp_branches:
                request = self._api_url +"/repos/" \
                        +self.user+"/"+self.repo+"/zipball/"+branch
                include = {
                    "name":branch.title(),
                    "zipball_url":request
                }
                self._tags = [include] + self._tags # append to front

        if self._tags is None:
            # some error occurred
            self._tag_latest = None
            self._tags = []
            return
        elif self._prefiltered_tag_count == 0 and self._include_branches == False:
            self._tag_latest = None
            self._error = "No releases found"
            self._error_msg = "No releases or tags found on this repository"
            if self.verbose:print("No releases or tags found on this repository")
        elif self._prefiltered_tag_count == 0 and self._include_branches == True:
            self._tag_latest = self._tags[0]
            if self.verbose:
                branch = self._include_branch_list[0]
                print("{} branch found, no releases".format(branch),self._tags[0])
        elif len(self._tags) == 0 and self._prefiltered_tag_count > 0:
            self._tag_latest = None
            self._error = "No releases available"
            self._error_msg = "No versions found within compatible version range"
            if self.verbose:print("No versions found within compatible version range")
        else:
            if self._include_branches == False:
                self._tag_latest = self._tags[0]
                if self.verbose:print("Most recent tag found:",self._tags[0])
            else:
                # don't return branch if in list
                n = len(self._include_branch_list)
                self._tag_latest = self._tags[n] # guarenteed at least len()=n+1
                if self.verbose:print("Most recent tag found:",self._tags[n])


    # all API calls to base url
    def get_api_raw(self, url):
        request = urllib.request.Request(self._api_url + url)
        try:
            result = urllib.request.urlopen(request)
        except urllib.error.HTTPError as e:
            self._error = "HTTP error"
            self._error_msg = str(e.code)
            self._update_ready = None
        except urllib.error.URLError as e:
            self._error = "URL error, check internet connection"
            self._error_msg = str(e.reason)
            self._update_ready = None
            return None
        else:
            result_string = result.read()
            result.close()
            return result_string.decode()
        # if we didn't get here, return or raise something else
        
        
    # result of all api calls, decoded into json format
    def get_api(self, url):
        # return the json version
        get = None
        get = self.get_api_raw(url) # this can fail by self-created error raising
        if get is not None:
            return json.JSONDecoder().decode( get )
        else:
            return None


    # create a working directory and download the new files
    def stage_repository(self, url):

        # first make/clear the staging folder
        # ensure the folder is always "clean"
        local = os.path.join(self._updater_path,"update_staging")
        error = None

        if self._verbose:print("Preparing staging folder for download:\n",local)
        if os.path.isdir(local) == True:
            try:
                shutil.rmtree(local)
                os.makedirs(local)
            except:
                error = "failed to remove existing staging directory"
        else:
            try:
                os.makedirs(local)
            except:
                error = "failed to make staging directory"
        
        if error is not None:
            if self._verbose: print("Error: Aborting update, "+error)
            raise ValueError("Aborting update, "+error)

        if self._backup_current==True:
            self.create_backup()
        if self._verbose:print("Now retrieving the new source zip")

        self._source_zip = os.path.join(local,"source.zip")
        
        if self._verbose:print("Starting download update zip")
        try:
            urllib.request.urlretrieve(url, self._source_zip)
        except Exception as e:
            self._error = "Error retreiving download, bad link?"
            self._error_msg = "Error: {}".format(e)
            if self._verbose:
                print("Error retreiving download, bad link?")
                print("Error: {}".format(e))
            return
        if self._verbose:print("Successfully downloaded update zip")

    def create_backup(self):
        if self._verbose:print("Backing up current addon folder")
        local = os.path.join(self._updater_path,"backup")
        tempdest = os.path.join(self._addon_root,
                        os.pardir,
                        self._addon+"_updater_backup_temp")

        if os.path.isdir(local) == True:
            shutil.rmtree(local)
        if self._verbose:print("Backup destination path: ",local)

        # make the copy
        shutil.copytree(self._addon_root,tempdest)
        shutil.move(tempdest,local)

        # save the date for future ref
        now = datetime.now()
        self._json["backup_date"] = "{m}-{d}-{yr}".format(
                m=now.strftime("%B"),d=now.day,yr=now.year)
        self.save_updater_json()

    def restore_backup(self):
        if self._verbose:print("Restoring backup")

        if self._verbose:print("Backing up current addon folder")
        backuploc = os.path.join(self._updater_path,"backup")
        tempdest = os.path.join(self._addon_root,
                        os.pardir,
                        self._addon+"_updater_backup_temp")
        tempdest = os.path.abspath(tempdest)

        # make the copy
        shutil.move(backuploc,tempdest)
        shutil.rmtree(self._addon_root)
        os.rename(tempdest,self._addon_root)

        self._json["backup_date"] = ""
        self._json["just_restored"] = True
        self._json["just_updated"] = True
        self.save_updater_json()

        self.reload_addon()

    def upack_staged_zip(self):

        if os.path.isfile(self._source_zip) == False:
            if self._verbose:print("Error, update zip not found")
            return -1

        # clear the existing source folder in case previous files remain
        try:
            shutil.rmtree( os.path.join(self._updater_path,"source") )
            os.makedirs( os.path.join(self._updater_path,"source") )
            if self._verbose:print("Source folder cleared and recreated")
        except:
            pass
        

        if self.verbose:print("Begin extracting source")
        if zipfile.is_zipfile(self._source_zip):
            with zipfile.ZipFile(self._source_zip) as zf:
                # extractall is no longer a security hazard
                zf.extractall(os.path.join(self._updater_path,"source"))
        else:
            if self._verbose:
                print("Not a zip file, future add support for just .py files")
            raise ValueError("Resulting file is not a zip")
        if self.verbose:print("Extracted source")

        # either directly in root of zip, or one folder level deep
        unpath = os.path.join(self._updater_path,"source")
        if os.path.isfile(os.path.join(unpath,"__init__.py")) == False:
            dirlist = os.listdir(unpath)
            if len(dirlist)>0:
                unpath = os.path.join(unpath,dirlist[0])

            if os.path.isfile(os.path.join(unpath,"__init__.py")) == False:
                if self._verbose:
                    print("not a valid addon found")
                    print("Paths:")
                    print(dirlist)

                raise ValueError("__init__ file not found in new source")

        # now commence merging in the two locations:
        origpath = os.path.dirname(__file__) # verify, is __file__ always valid?

        self.deepMergeDirectory(origpath,unpath)
        
        # now save the json state
        #  Change to True, to trigger the handler on other side
        #  if allowing reloading within same blender instance
        self._json["just_updated"] = True
        self.save_updater_json()
        self.reload_addon()
        self._update_ready = False


    # merge folder 'merger' into folder 'base' without deleting existing
    def deepMergeDirectory(self,base,merger):
        if not os.path.exists(base):
            if self._verbose:print("Base path does not exist")
            return -1
        elif not os.path.exists(merger):
            if self._verbose:print("Merger path does not exist")
            return -1

        # this should have better error handling
        # and also avoid the addon dir
        # Could also do error handling outside this function
        for path, dirs, files in os.walk(merger):
            relPath = os.path.relpath(path, merger)
            destPath = os.path.join(base, relPath)
            if not os.path.exists(destPath):
                os.makedirs(destPath)
            for file in files:
                destFile = os.path.join(destPath, file)
                if os.path.isfile(destFile):
                    os.remove(destFile)
                srcFile = os.path.join(path, file)
                os.rename(srcFile, destFile)
    

    def reload_addon(self):
        # if post_update false, skip this function
        # else, unload/reload addon & trigger popup
        if self._auto_reload_post_update == False:
            print("Restart blender to reload addon and complete update")
            return


        if self._verbose:print("Reloading addon...")
        addon_utils.modules(refresh=True)
        bpy.utils.refresh_script_paths()

        # not allowed in restricted context, such as register module
        # toggle to refresh
        bpy.ops.wm.addon_disable(module=self._addon_package)
        bpy.ops.wm.addon_refresh()
        bpy.ops.wm.addon_enable(module=self._addon_package)


    # -------------------------------------------------------------------------
    # Other non-api functions and setups
    # -------------------------------------------------------------------------

    def clear_state(self):
        self._update_ready = None
        self._update_link = None
        self._update_version = None
        self._source_zip = None
        self._error = None
        self._error_msg = None

    def version_tuple_from_text(self,text):

        if text is None: return ()

        # should go through string and remove all non-integers,
        # and for any given break split into a different section

        segments = []
        tmp = ''
        for l in str(text):
            if l.isdigit()==False:
                if len(tmp)>0:
                    segments.append(int(tmp))
                    tmp = ''
            else:
                tmp+=l
        if len(tmp)>0:
            segments.append(int(tmp))

        if len(segments)==0:
            if self._verbose:print("No version strings found text: ",text)
            if self._include_branches == False:
                return ()
            else:
                return (text)
        return tuple(segments)

    # called for running check in a background thread
    def check_for_update_async(self, callback=None):

        if self._json is not None and "update_ready" in self._json:
            if self._json["update_ready"] == True:
                self._update_ready = True
                self._update_link = self._json["version_text"]["link"]
                self._update_version = str(self._json["version_text"]["version"])
                # cached update
                callback(True)
                return

        # do the check
        if self._check_interval_enable == False:
            return
        elif self._async_checking == True:
            if self._verbose:print("Skipping async check, already started")
            return # already running the bg thread
        elif self._update_ready is None:
            self.start_async_check_update(False, callback)

    def check_for_update_now(self, callback=None):

        self._error = None
        self._error_msg = None

        if self._verbose:
            print("Check update pressed, first getting current status")
        if self._async_checking == True:
            if self._verbose:print("Skipping async check, already started")
            return # already running the bg thread
        elif self._update_ready is None:
            self.start_async_check_update(True, callback)
        else:
            self._update_ready = None
            self.start_async_check_update(True, callback)


    # this function is not async, will always return in sequential fashion
    # but should have a parent which calls it in another thread
    def check_for_update(self, now=False):
        if self._verbose:print("Checking for update function")

        # clear the errors if any
        self._error = None
        self._error_msg = None

        # avoid running again in, just return past result if found
        # but if force now check, then still do it
        if self._update_ready is not None and now == False:
            return (self._update_ready,self._update_version,self._update_link)

        if self._current_version is None:
            raise ValueError("current_version not yet defined")
        if self._repo is None:
            raise ValueError("repo not yet defined")
        if self._user is None:
            raise ValueError("username not yet defined")

        self.set_updater_json() # self._json

        if now == False and self.past_interval_timestamp()==False:
            if self.verbose:
                print("Aborting check for updated, check interval not reached")
            return (False, None, None)
        
        # check if using tags or releases
        # note that if called the first time, this will pull tags from online
        if self._fake_install == True:
            if self._verbose:
                print("fake_install = True, setting fake version as ready")
            self._update_ready = True
            self._update_version = "(999,999,999)"
            self._update_link = "http://127.0.0.1"
            
            return (self._update_ready, self._update_version, self._update_link)
        
        # primary internet call
        self.get_tags() # sets self._tags and self._tag_latest

        self._json["last_check"] = str(datetime.now())
        self.save_updater_json()

        # can be () or ('master') in addition to branchs, and version tag
        new_version = self.version_tuple_from_text(self.tag_latest)

        if len(self._tags)==0:
            self._update_ready = False
            self._update_version = None
            self._update_link = None
            return (False, None, None)
        elif self._include_branches == False:
            link = self._tags[0]["zipball_url"] # potentially other sources
        else:
            n = len(self._include_branch_list)
            if len(self._tags)==n:
                # effectively means no tags found on repo
                # so provide the first one as default
                link = self._tags[0]["zipball_url"] # potentially other sources
            else:
                link = self._tags[n]["zipball_url"] # potentially other sources
        
        if new_version == ():
            self._update_ready = False
            self._update_version = None
            self._update_link = None
            return (False, None, None)
        elif str(new_version).lower() in self._include_branch_list:
            # handle situation where master/whichever branch is included
            # however, this code effectively is not triggered now
            # as new_version will only be tag names, not branch names
            if self._include_branch_autocheck == False:
                # don't offer update as ready,
                # but set the link for the default
                # branch for installing
                self._update_ready = True
                self._update_version = new_version
                self._update_link = link
                self.save_updater_json()
                return (True, new_version, link)
            else:
                raise ValueError("include_branch_autocheck: NOT YET DEVELOPED")
                # bypass releases and look at timestamp of last update
                # from a branch compared to now, see if commit values
                # match or not.

        else:
            # situation where branches not included

            if new_version > self._current_version:

                self._update_ready = True
                self._update_version = new_version
                self._update_link = link
                self.save_updater_json()
                return (True, new_version, link)

        # elif new_version != self._current_version:
        # 	self._update_ready = False
        # 	self._update_version = new_version
        # 	self._update_link = link
        # 	self.save_updater_json()
        # 	return (True, new_version, link)

        # if no update, set ready to False from None
        self._update_ready = False
        self._update_version = None
        self._update_link = None
        return (False, None, None)

    def set_tag(self,name):
        tg = None
        for tag in self._tags:
            if name == tag["name"]:
                tg = tag
                break
        if tg is None:
            raise ValueError("Version tag not found: "+revert_tag)
        new_version = self.version_tuple_from_text(self.tag_latest)
        self._update_version = new_version
        self._update_link = tg["zipball_url"]


    def run_update(self,force=False,revert_tag=None,clean=False,callback=None):
        # revert_tag: could e.g. get from drop down list
        # different versions of the addon to revert back to
        # clean: not used, but in future could use to totally refresh addon
        self._json["update_ready"] = False
        self._json["ignore"] = False # clear ignore flag
        self._json["version_text"] = {}

        if revert_tag is not None:
            self.set_tag(revert_tag)
            self._update_ready = True

        # clear the errors if any
        self._error = None
        self._error_msg = None

        if self.verbose:print("Running update")

        if self._fake_install == True:
            # change to True, to trigger the reload/"update installed" handler
            if self._verbose:
                print("fake_install=True")
                print("Just reloading and running any handler triggers")
            self._json["just_updated"] = True
            self.save_updater_json()
            if self._backup_current == True:
                self.create_backup()
            self.reload_addon()
            self._update_ready = False

        elif force==False:
            if self._update_ready != True:
                if self.verbose:print("Update stopped, new version not ready")
                return 1 # stopped
            elif self._update_link is None:
                # this shouldn't happen if update is ready
                if self.verbose:print("Update stopped, update link unavailable")
                return 1 # stopped

            if self.verbose and revert_tag is None:
                print("Staging update")
            elif self.verbose:
                print("Staging install")
            self.stage_repository(self._update_link)
            self.upack_staged_zip()

        else:
            if self._update_link is None:
                return # stopped, no link - run check update first or set tag
            if self.verbose:print("Forcing update")
            # first do a check
            if self._update_link is None:
                if self.verbose:print("Update stopped, could not get link")
                return
            self.stage_repository(self._update_link)
            self.upack_staged_zip()
            # would need to compare against other versions held in tags

        # run the front-end's callback if provided
        if callback is not None:callback()

        # return something meaningful, 0 means it worked
        return 0


    def past_interval_timestamp(self):
        if self._check_interval_enable == False:
            return True # ie this exact feature is disabled
        
        if "last_check" not in self._json or self._json["last_check"] == "":
            return True
        else:
            now = datetime.now()
            last_check = datetime.strptime(self._json["last_check"],
                                        "%Y-%m-%d %H:%M:%S.%f")
            next_check = last_check
            offset = timedelta(
                days=self._check_interval_days + 30*self._check_interval_months,
                hours=self._check_interval_hours,
                minutes=self._check_interval_minutes
                )

            delta = (now - offset) - last_check
            if delta.total_seconds() > 0:
                if self._verbose:
                    print("{} Updater: Time to check for updates!".format(self._addon))
                return True
            else:
                if self._verbose:
                    print("{} Updater: Determined it's not yet time to check for updates".format(self._addon))
                return False


    def set_updater_json(self):
        if self._updater_path is None:
            raise ValueError("updater_path is not defined")
        elif os.path.isdir(self._updater_path) == False:
            os.makedirs(self._updater_path)

        jpath = os.path.join(self._updater_path,"updater_status.json")
        if os.path.isfile(jpath):
            with open(jpath) as data_file:
                self._json = json.load(data_file)
                if self._verbose:print("{} Updater: Read in json settings from file".format(self._addon))
        else:
            # set data structure
            self._json = {
                "last_check":"",
                "backup_date":"",
                "update_ready":False,
                "ignore":False,
                "just_restored":False,
                "just_updated":False,
                "version_text":{}
            }
            self.save_updater_json()


    def save_updater_json(self):

        # first save the state
        if self._update_ready == True:
            if type(self._update_version) == type((0,0,0)):
                self._json["update_ready"] = True
                self._json["version_text"]["link"]=self._update_link
                self._json["version_text"]["version"]=self._update_version
            else:
                self._json["update_ready"] = False
                self._json["version_text"] = {}
        else:
            self._json["update_ready"] = False
            self._json["version_text"] = {}

        jpath = os.path.join(self._updater_path,"updater_status.json")
        outf = open(jpath,'w')
        data_out = json.dumps(self._json,indent=4)
        outf.write(data_out)
        outf.close()
        if self._verbose:
            print(self._addon+": Wrote out updater json settings to file, with the contents:")
            print(self._json)

    def json_reset_postupdate(self):
        self._json["just_updated"] = False
        self._json["update_ready"] = False
        self._json["version_text"] = {}
        self.save_updater_json()
    def json_reset_restore(self):
        self._json["just_restored"] = False
        self._json["update_ready"] = False
        self._json["version_text"] = {}
        self.save_updater_json()
        self._update_ready = None # reset so you could check update again

    def ignore_update(self):
        self._json["ignore"] = True
        self.save_updater_json()

    # -------------------------------------------------------------------------
    # ASYNC stuff
    # -------------------------------------------------------------------------

    def start_async_check_update(self, now=False,callback=None):
        if self._async_checking == True:
            return
        if self._verbose: print("{} updater: Starting background checking thread".format(self._addon))
        check_thread = threading.Thread(target=self.async_check_update,
                                        args=(now,callback,))
        check_thread.daemon = True
        self._check_thread = check_thread
        check_thread.start()
        
        return True

    def async_check_update(self, now, callback=None):
        self._async_checking = True
        if self._verbose:print("{} BG thread: Checking for update now in background".format(self._addon))
        # time.sleep(3) # to test background, in case internet too fast to tell
        # try:
        self.check_for_update(now=now)
        # except Exception as exception:
        # 	print("Checking for update error:")
        # 	print(exception)
        # 	self._update_ready = False
        # 	self._update_version = None
        # 	self._update_link = None
        # 	self._error = "Error occurred"
        # 	self._error_msg = "Encountered an error while checking for updates"

        if self._verbose:
            print("{} BG thread: Finished checking for update, doing callback".format(self._addon))
        if callback is not None:callback(self._update_ready)
        self._async_checking = False
        self._check_thread = None


    def stop_async_check_update(self):
        if self._check_thread is not None:
            try:
                if self._verbose:print("Thread will end in normal course.")
                # however, "There is no direct kill method on a thread object."
                # better to let it run its course
                #self._check_thread.stop()
            except:
                pass
        self._async_checking = False
        self._error = None
        self._error_msg = None




# -----------------------------------------------------------------------------
# The module-shared class instance,
# should be what's imported to other files
# -----------------------------------------------------------------------------

Updater = Singleton_updater()