
from __future__ import absolute_import
from __future__ import division       # python 2/3 compatibility
from __future__ import print_function # python 2/3 compatibility

from six import iteritems # python 2/3 compatibility
from io import BytesIO # python 2/3 compatibility

from sc2gameLobby import dateFormat
from sc2gameLobby import gameConstants as c

import json
import os
import re
import time


"""
Starcraft 2 patch/update information is copied from the following site(s):
    http://liquipedia.net/starcraft2/Patch_4.2.3 (and links listed on the page)
"""


################################################################################
def addNew(label, version, baseVersion, dataHash="", fixedHash="", replayHash=""):
    """
    Add a new version record to the database to be tracked
    VERSION RECORD EXAMPLE:
        "base-version": 55505, 
        "data-hash": "60718A7CA50D0DF42987A30CF87BCB80", 
        "fixed-hash": "0189B2804E2F6BA4C4591222089E63B2", 
        "label": "3.16", 
        "replay-hash": "B11811B13F0C85C29C5D4597BD4BA5A4", 
        "version": 55505
        """
    baseVersion = int(baseVersion)
    version     = int(version)
    minVersChecks = {"base-version":baseVersion, "version":version}
    if label in handle.ALL_VERS_DATA:
        raise ValueError("given record label (%s) is already defined.  Consider performing update() for this record instead"%(label))
    for vCheckK,vCheckV in iteritems(minVersChecks): # verify no conflicting values
        maxVersion  = min([vData[vCheckK] for vData in handle.ALL_VERS_DATA.values()])
        if vCheckV < c.MIN_VERSION_AI_API:
            raise ValueError("version %s / %s.%s does not support the Starcraft2 API"%(baseVersion, label, version))
        if vCheckV < maxVersion: # base version cannot be smaller than newest value
            raise ValueError("given %s (%d) cannot be smaller than newest known %s (%d)"%(vCheckK, vCheckV, vCheckK, maxVersion))
    uniqueValHeaders = list(c.JSON_HEADERS)
    uniqueValHeaders.remove("base-version")
    record = {"base-version" : baseVersion}
    #print("%15s : %s (%s)"%("base-version", baseVersion, type(baseVersion)))
    for k,v in zip(uniqueValHeaders, [label, version, dataHash, fixedHash, replayHash]): # new attr values must be unique within all handler records
        record[k] = v # convert to dict while checking each param
        #print("%15s : %s (%s)"%(k,v,type(v)))
        if not v: continue # ignore uniqueness requirement if value is unspecified
        if v in [r[k] for r in Handler.ALL_VERS_DATA.values()]:
            raise ValueError("'%s' '%s' is in known values: %s"%(k, v, getattr(handle, k)))
            return
    handle.save(new=record)


################################################################################
#def update(version, newValues):
#    """
#    USAGE EXAMPLES:
#        update(59729, {"fixed-hash":"123"})
#        update("3.19", {"replay-hash":"WHATEVER231"})
#    """
#    if len(newValues) == 0: raise ValueError("must provide data in paramater newValues else cannot update any record")
#    try:
#            int(version)
#            versionKey = "version"
#    except: versionKey = "label"
#    records = handle.search(version) # match record where versionKey value == version
#    if   len(records) > 1: raise ValueError("identified too many records (%d): %s"%(len(records), records))
#    elif len(records) < 1: raise ValueError("failed to identify any records given params: %s"%(records))
#    record = records.pop() # access the single, found record
#    for k,v in iteritems(newValues):   record[k] = v
#    handle._updated = True
#    handle.save()


################################################################################
class Handler(object):
    """NOTE: the data-hash field is required to launch stand-alone versions of the game"""
    ############################################################################
    ALL_VERS_DATA = None
    ############################################################################
    def __init__(self):
        self.load()
    ############################################################################
    #def __getattr__(self, key):
    #    """get all record values of a given key"""
    #    return sorted([v[key] for v in Handler.ALL_VERS_DATA.values()])
    ############################################################################
    def __len__(self):
        """the number of known version records"""
        return len(Handler.ALL_VERS_DATA)
    ############################################################################
    @property
    def mostRecent(self):
        records = iteritems(Handler.ALL_VERS_DATA)
        try: 
            label,record = max(records)
            return record
        except ValueError: return None # no versions are installed
    ############################################################################
    #def get(self, key, versFilter=""):
    #    """get a values of key where records match versFilter"""
    #    for v in self.search(versFilter): print(v)
    #    return [v[key] for v in self.search(versFilter)]
    ############################################################################
    def load(self):
        """load ALL_VERS_DATA from disk"""
        basepath = os.path.dirname(os.path.abspath(__file__))
        filename = os.sep.join([basepath, c.FOLDER_JSON, c.FILE_GAME_VERSIONS])
        Handler.ALL_VERS_DATA = {} # reset known data; do not retain defunct information
        with open(filename, "r") as f:
            data = json.loads( f.read() )
        self.update(data)
        self._updated = False
        #for v,record in iteritems(Handler.ALL_VERS_DATA):
        #    print(type(v), v)
            #for k,v in iteritems(record):    
    ############################################################################
    def save(self, new=None, timeout=2):
        """write ALL_VERS_DATA to disk in 'pretty' format"""
        if new: self.update(new) # allow two operations (update + save) with a single command
        if not self._updated: return # nothing to do
        thisPkg = os.path.dirname(__file__)
        filename = os.path.join(thisPkg, c.FOLDER_JSON, c.FILE_GAME_VERSIONS)
        fParts = c.FILE_GAME_VERSIONS.split('.')
        newFile = os.path.join(thisPkg, c.FOLDER_JSON, "%s_%s.%s"%(fParts[0], dateFormat.now(), fParts[1]))
        if not os.path.isfile(newFile):
            #fParts = c.FILE_GAME_VERSIONS.split('.')
            #newFile = "%s%s%s_%s.%s"%(c.FOLDER_JSON, os.sep, fParts[0], dateFormat.now(), fParts[1])
            #if not os.path.isfile(newFile):
            #print(filename)
            #print(newFile)
            os.rename(filename, newFile) # backup existing version file
        recordKeys = [(record["version"], record) for record in Handler.ALL_VERS_DATA.values()]
        data = [r for k,r in sorted(recordKeys)] # i.e. get values sorted by version key
        start = time.time()
        while time.time()-start < timeout: # allow multiple retries if multiple processes fight over the version file
            try:
                with open(filename, "wb") as f:
                    f.write(str.encode(json.dumps(data, indent=4, sort_keys=True))) # python3 requires encoding str => bytes to write to file
                self._updated = False
                return
            except IOError: pass # continue waiting for file to be available
        raise # after timeout, prior exception is what matters
    ############################################################################
    def search(self, *args, **kwargs):
        """match all records that have any args in any key/field that also match
        key/value requirements specified in kwargs"""
        ret = []
        for record in Handler.ALL_VERS_DATA.values():
            matchArgs = list(kwargs.keys())
            for k,v in iteritems(kwargs): # restrict records based on key-value match requirement
                try:
                    if record[k] != v: break # a non-matching requirement means this record doesn't match
                except: break # record doesn't have required key 'k'
                matchArgs.remove(k)
            if matchArgs: continue # didn't match all required kwargs
            matchArgs = list(args)
            for k,v in iteritems(record): # find any record with a <value> in it
                if k in matchArgs: matchArgs.remove(k)
                if v in matchArgs: matchArgs.remove(v)
            if matchArgs: continue # didn't match all required args
            ret.append(record)
        return ret
    ############################################################################
    #def search(self, searchVal, keyOnly=None, **filters):
    #    """identify all version records where searchVal exists (can be a regex)"""
    #    ########################################################################
    #    def checkValues(vData, regex):
    #        for k,v in iteritems(vData):
    #            #if k == "base-version": continue # ignore matching this key; expect it will match 'version' key where applicable
    #            if re.search(regex, str(v)): return True
    #        return False
    #    ########################################################################
    #    _tmp = re.compile("")
    #    if type(searchVal) == type(_tmp):
    #          regex = searchVal
    #    else: regex = re.compile(str(searchVal))
    #    ret = {}
    #    if keyOnly!=False: # search all keys (only)
    #        for vers in Handler.ALL_VERS_DATA.values():
    #            if any([re.search(regex, str(k)) for k in vers.keys()]):
    #                ret[vers["label"]] = vers
    #    if keyOnly!=True: # search all values (only)
    #        for vers in Handler.ALL_VERS_DATA.values():
    #            if checkValues(vers, regex):   ret[vers["label"]] = vers
    #    return ret.values()
    ############################################################################
    def update(self, data):
        """update known data with with newly provided data"""
        if not isinstance(data, list): data = [data] # otherwise no conversion is necessary
        master = Handler.ALL_VERS_DATA
        for record in data:
            #print(record)
            for k,v in iteritems(record): # ensure record contents aretyped appropriately
                try:                record[k] = int(v)
                except ValueError:  record[k] = v
            try: label = record["label"] # verify this record has the required 'label' key
            except KeyError:
                raise ValueError("Must provide a valid label argument.  Given:%s%s"%(\
                    os.linesep, ("%s  "%(os.linesep)).join(
                        ["%15s:%s"%(k,v) for k,v in iteritems(kwargs)]
                    )))
            try:    masterLabel = master[label] # identify the already existing record that matches this to-be-updated record, if any
            except KeyError: # master hasn't been defined yet
                master[label] = record
                self._updated = True # a new record should also be saved
                continue
            for k,v in iteritems(record): # determine whether master needs to be updated
                try:
                    if masterLabel[k] == v:  continue # whether an entry in the record needs to be updated (doesn't match)
                except KeyError:             pass # this condition means that k is a new key, so the record must be updated
                self._updated = True
                try:    master[label].update(record) # index each record by its label
                except KeyError:             break


################################################################################
handle = Handler()
#newRecord = {
#    "fixed-hash": "", 
#    "replay-hash": "", 
#    "data-hash": "", 
#    "label": "4.2.4", 
#    "base-version": 63454, 
#    "version": 64128
#}
#handle.save(new=newRecord)


################################################################################
class Version(object):
    """Represent a single version of the game settings"""
    ############################################################################
    def __init__(self, versionVal=None):
        if versionVal: # get specific version
            if isinstance(versionVal, list): versionVal = max(versionVal)
            records = handle.search(versionVal)
            if   len(records) > 1: raise ValueError("identified too many records (%d): %s"%(len(records), records))
            elif len(records) < 1: raise ValueError("first collect and update version information for version: %s"%(versionVal))
            record = records.pop()
        else: # select most recent version
            record = handle.mostRecent
        for k in c.JSON_HEADERS: # convert hyphenated keys into code-compatible keys
            oldK = k
            wordBoundaries = re.search("(-\w)", k)
            if wordBoundaries:
                for wb in wordBoundaries.groups():
                    newWb = wb.upper().strip('-')
                    k = re.sub(wb, newWb, k)
            setattr(self, k, record[oldK])
        while self.label.count(".") < 2: self.label += ".0" # ensure a 3-field versioning scheme
    ############################################################################
    def __str__(self):  return self.__repr__()
    def __repr__(self):
        return "%s.%s"%(self.label, self.version)
    ############################################################################
    def __getitem__(self, key):
        return getattr(self, key)
    ############################################################################
    def toTuple(self, typeCast=int):
        return [typeCast(v) for v in self.label.split('.')]
    ############################################################################
    def toFilename(self):
        return re.sub("\.", "_", str(self.label))


02293f2d28c29f4a7bd51e38d1178a99558396bc
<mask token>


class Handler(object):
    <mask token>
    <mask token>

    def __init__(self):
        self.load()
    <mask token>
    <mask token>

    def load(self):
        """load ALL_VERS_DATA from disk"""
        basepath = os.path.dirname(os.path.abspath(__file__))
        filename = os.sep.join([basepath, c.FOLDER_JSON, c.FILE_GAME_VERSIONS])
        Handler.ALL_VERS_DATA = {}
        with open(filename, 'r') as f:
            data = json.loads(f.read())
        self.update(data)
        self._updated = False

    def save(self, new=None, timeout=2):
        """write ALL_VERS_DATA to disk in 'pretty' format"""
        if new:
            self.update(new)
        if not self._updated:
            return
        thisPkg = os.path.dirname(__file__)
        filename = os.path.join(thisPkg, c.FOLDER_JSON, c.FILE_GAME_VERSIONS)
        fParts = c.FILE_GAME_VERSIONS.split('.')
        newFile = os.path.join(thisPkg, c.FOLDER_JSON, '%s_%s.%s' % (fParts
            [0], dateFormat.now(), fParts[1]))
        if not os.path.isfile(newFile):
            os.rename(filename, newFile)
        recordKeys = [(record['version'], record) for record in Handler.
            ALL_VERS_DATA.values()]
        data = [r for k, r in sorted(recordKeys)]
        start = time.time()
        while time.time() - start < timeout:
            try:
                with open(filename, 'wb') as f:
                    f.write(str.encode(json.dumps(data, indent=4, sort_keys
                        =True)))
                self._updated = False
                return
            except IOError:
                pass
        raise
    <mask token>

    def update(self, data):
        """update known data with with newly provided data"""
        if not isinstance(data, list):
            data = [data]
        master = Handler.ALL_VERS_DATA
        for record in data:
            for k, v in iteritems(record):
                try:
                    record[k] = int(v)
                except ValueError:
                    record[k] = v
            try:
                label = record['label']
            except KeyError:
                raise ValueError(
                    'Must provide a valid label argument.  Given:%s%s' % (
                    os.linesep, ('%s  ' % os.linesep).join([('%15s:%s' % (k,
                    v)) for k, v in iteritems(kwargs)])))
            try:
                masterLabel = master[label]
            except KeyError:
                master[label] = record
                self._updated = True
                continue
            for k, v in iteritems(record):
                try:
                    if masterLabel[k] == v:
                        continue
                except KeyError:
                    pass
                self._updated = True
                try:
                    master[label].update(record)
                except KeyError:
                    break


<mask token>


class Version(object):
    """Represent a single version of the game settings"""

    def __init__(self, versionVal=None):
        if versionVal:
            if isinstance(versionVal, list):
                versionVal = max(versionVal)
            records = handle.search(versionVal)
            if len(records) > 1:
                raise ValueError('identified too many records (%d): %s' % (
                    len(records), records))
            elif len(records) < 1:
                raise ValueError(
                    'first collect and update version information for version: %s'
                     % versionVal)
            record = records.pop()
        else:
            record = handle.mostRecent
        for k in c.JSON_HEADERS:
            oldK = k
            wordBoundaries = re.search('(-\\w)', k)
            if wordBoundaries:
                for wb in wordBoundaries.groups():
                    newWb = wb.upper().strip('-')
                    k = re.sub(wb, newWb, k)
            setattr(self, k, record[oldK])
        while self.label.count('.') < 2:
            self.label += '.0'

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return '%s.%s' % (self.label, self.version)

    def __getitem__(self, key):
        return getattr(self, key)

    def toTuple(self, typeCast=int):
        return [typeCast(v) for v in self.label.split('.')]

    def toFilename(self):
        return re.sub('\\.', '_', str(self.label))

<mask token>


class Handler(object):
    <mask token>
    <mask token>

    def __init__(self):
        self.load()

    def __len__(self):
        """the number of known version records"""
        return len(Handler.ALL_VERS_DATA)

    @property
    def mostRecent(self):
        records = iteritems(Handler.ALL_VERS_DATA)
        try:
            label, record = max(records)
            return record
        except ValueError:
            return None

    def load(self):
        """load ALL_VERS_DATA from disk"""
        basepath = os.path.dirname(os.path.abspath(__file__))
        filename = os.sep.join([basepath, c.FOLDER_JSON, c.FILE_GAME_VERSIONS])
        Handler.ALL_VERS_DATA = {}
        with open(filename, 'r') as f:
            data = json.loads(f.read())
        self.update(data)
        self._updated = False

    def save(self, new=None, timeout=2):
        """write ALL_VERS_DATA to disk in 'pretty' format"""
        if new:
            self.update(new)
        if not self._updated:
            return
        thisPkg = os.path.dirname(__file__)
        filename = os.path.join(thisPkg, c.FOLDER_JSON, c.FILE_GAME_VERSIONS)
        fParts = c.FILE_GAME_VERSIONS.split('.')
        newFile = os.path.join(thisPkg, c.FOLDER_JSON, '%s_%s.%s' % (fParts
            [0], dateFormat.now(), fParts[1]))
        if not os.path.isfile(newFile):
            os.rename(filename, newFile)
        recordKeys = [(record['version'], record) for record in Handler.
            ALL_VERS_DATA.values()]
        data = [r for k, r in sorted(recordKeys)]
        start = time.time()
        while time.time() - start < timeout:
            try:
                with open(filename, 'wb') as f:
                    f.write(str.encode(json.dumps(data, indent=4, sort_keys
                        =True)))
                self._updated = False
                return
            except IOError:
                pass
        raise

    def search(self, *args, **kwargs):
        """match all records that have any args in any key/field that also match
        key/value requirements specified in kwargs"""
        ret = []
        for record in Handler.ALL_VERS_DATA.values():
            matchArgs = list(kwargs.keys())
            for k, v in iteritems(kwargs):
                try:
                    if record[k] != v:
                        break
                except:
                    break
                matchArgs.remove(k)
            if matchArgs:
                continue
            matchArgs = list(args)
            for k, v in iteritems(record):
                if k in matchArgs:
                    matchArgs.remove(k)
                if v in matchArgs:
                    matchArgs.remove(v)
            if matchArgs:
                continue
            ret.append(record)
        return ret

    def update(self, data):
        """update known data with with newly provided data"""
        if not isinstance(data, list):
            data = [data]
        master = Handler.ALL_VERS_DATA
        for record in data:
            for k, v in iteritems(record):
                try:
                    record[k] = int(v)
                except ValueError:
                    record[k] = v
            try:
                label = record['label']
            except KeyError:
                raise ValueError(
                    'Must provide a valid label argument.  Given:%s%s' % (
                    os.linesep, ('%s  ' % os.linesep).join([('%15s:%s' % (k,
                    v)) for k, v in iteritems(kwargs)])))
            try:
                masterLabel = master[label]
            except KeyError:
                master[label] = record
                self._updated = True
                continue
            for k, v in iteritems(record):
                try:
                    if masterLabel[k] == v:
                        continue
                except KeyError:
                    pass
                self._updated = True
                try:
                    master[label].update(record)
                except KeyError:
                    break


<mask token>


class Version(object):
    """Represent a single version of the game settings"""

    def __init__(self, versionVal=None):
        if versionVal:
            if isinstance(versionVal, list):
                versionVal = max(versionVal)
            records = handle.search(versionVal)
            if len(records) > 1:
                raise ValueError('identified too many records (%d): %s' % (
                    len(records), records))
            elif len(records) < 1:
                raise ValueError(
                    'first collect and update version information for version: %s'
                     % versionVal)
            record = records.pop()
        else:
            record = handle.mostRecent
        for k in c.JSON_HEADERS:
            oldK = k
            wordBoundaries = re.search('(-\\w)', k)
            if wordBoundaries:
                for wb in wordBoundaries.groups():
                    newWb = wb.upper().strip('-')
                    k = re.sub(wb, newWb, k)
            setattr(self, k, record[oldK])
        while self.label.count('.') < 2:
            self.label += '.0'

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return '%s.%s' % (self.label, self.version)

    def __getitem__(self, key):
        return getattr(self, key)

    def toTuple(self, typeCast=int):
        return [typeCast(v) for v in self.label.split('.')]

    def toFilename(self):
        return re.sub('\\.', '_', str(self.label))

<mask token>


class Handler(object):
    """NOTE: the data-hash field is required to launch stand-alone versions of the game"""
    ALL_VERS_DATA = None

    def __init__(self):
        self.load()

    def __len__(self):
        """the number of known version records"""
        return len(Handler.ALL_VERS_DATA)

    @property
    def mostRecent(self):
        records = iteritems(Handler.ALL_VERS_DATA)
        try:
            label, record = max(records)
            return record
        except ValueError:
            return None

    def load(self):
        """load ALL_VERS_DATA from disk"""
        basepath = os.path.dirname(os.path.abspath(__file__))
        filename = os.sep.join([basepath, c.FOLDER_JSON, c.FILE_GAME_VERSIONS])
        Handler.ALL_VERS_DATA = {}
        with open(filename, 'r') as f:
            data = json.loads(f.read())
        self.update(data)
        self._updated = False

    def save(self, new=None, timeout=2):
        """write ALL_VERS_DATA to disk in 'pretty' format"""
        if new:
            self.update(new)
        if not self._updated:
            return
        thisPkg = os.path.dirname(__file__)
        filename = os.path.join(thisPkg, c.FOLDER_JSON, c.FILE_GAME_VERSIONS)
        fParts = c.FILE_GAME_VERSIONS.split('.')
        newFile = os.path.join(thisPkg, c.FOLDER_JSON, '%s_%s.%s' % (fParts
            [0], dateFormat.now(), fParts[1]))
        if not os.path.isfile(newFile):
            os.rename(filename, newFile)
        recordKeys = [(record['version'], record) for record in Handler.
            ALL_VERS_DATA.values()]
        data = [r for k, r in sorted(recordKeys)]
        start = time.time()
        while time.time() - start < timeout:
            try:
                with open(filename, 'wb') as f:
                    f.write(str.encode(json.dumps(data, indent=4, sort_keys
                        =True)))
                self._updated = False
                return
            except IOError:
                pass
        raise

    def search(self, *args, **kwargs):
        """match all records that have any args in any key/field that also match
        key/value requirements specified in kwargs"""
        ret = []
        for record in Handler.ALL_VERS_DATA.values():
            matchArgs = list(kwargs.keys())
            for k, v in iteritems(kwargs):
                try:
                    if record[k] != v:
                        break
                except:
                    break
                matchArgs.remove(k)
            if matchArgs:
                continue
            matchArgs = list(args)
            for k, v in iteritems(record):
                if k in matchArgs:
                    matchArgs.remove(k)
                if v in matchArgs:
                    matchArgs.remove(v)
            if matchArgs:
                continue
            ret.append(record)
        return ret

    def update(self, data):
        """update known data with with newly provided data"""
        if not isinstance(data, list):
            data = [data]
        master = Handler.ALL_VERS_DATA
        for record in data:
            for k, v in iteritems(record):
                try:
                    record[k] = int(v)
                except ValueError:
                    record[k] = v
            try:
                label = record['label']
            except KeyError:
                raise ValueError(
                    'Must provide a valid label argument.  Given:%s%s' % (
                    os.linesep, ('%s  ' % os.linesep).join([('%15s:%s' % (k,
                    v)) for k, v in iteritems(kwargs)])))
            try:
                masterLabel = master[label]
            except KeyError:
                master[label] = record
                self._updated = True
                continue
            for k, v in iteritems(record):
                try:
                    if masterLabel[k] == v:
                        continue
                except KeyError:
                    pass
                self._updated = True
                try:
                    master[label].update(record)
                except KeyError:
                    break


<mask token>


class Version(object):
    """Represent a single version of the game settings"""

    def __init__(self, versionVal=None):
        if versionVal:
            if isinstance(versionVal, list):
                versionVal = max(versionVal)
            records = handle.search(versionVal)
            if len(records) > 1:
                raise ValueError('identified too many records (%d): %s' % (
                    len(records), records))
            elif len(records) < 1:
                raise ValueError(
                    'first collect and update version information for version: %s'
                     % versionVal)
            record = records.pop()
        else:
            record = handle.mostRecent
        for k in c.JSON_HEADERS:
            oldK = k
            wordBoundaries = re.search('(-\\w)', k)
            if wordBoundaries:
                for wb in wordBoundaries.groups():
                    newWb = wb.upper().strip('-')
                    k = re.sub(wb, newWb, k)
            setattr(self, k, record[oldK])
        while self.label.count('.') < 2:
            self.label += '.0'

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return '%s.%s' % (self.label, self.version)

    def __getitem__(self, key):
        return getattr(self, key)

    def toTuple(self, typeCast=int):
        return [typeCast(v) for v in self.label.split('.')]

    def toFilename(self):
        return re.sub('\\.', '_', str(self.label))

<mask token>


def addNew(label, version, baseVersion, dataHash='', fixedHash='',
    replayHash=''):
    """
    Add a new version record to the database to be tracked
    VERSION RECORD EXAMPLE:
        "base-version": 55505, 
        "data-hash": "60718A7CA50D0DF42987A30CF87BCB80", 
        "fixed-hash": "0189B2804E2F6BA4C4591222089E63B2", 
        "label": "3.16", 
        "replay-hash": "B11811B13F0C85C29C5D4597BD4BA5A4", 
        "version": 55505
        """
    baseVersion = int(baseVersion)
    version = int(version)
    minVersChecks = {'base-version': baseVersion, 'version': version}
    if label in handle.ALL_VERS_DATA:
        raise ValueError(
            'given record label (%s) is already defined.  Consider performing update() for this record instead'
             % label)
    for vCheckK, vCheckV in iteritems(minVersChecks):
        maxVersion = min([vData[vCheckK] for vData in handle.ALL_VERS_DATA.
            values()])
        if vCheckV < c.MIN_VERSION_AI_API:
            raise ValueError(
                'version %s / %s.%s does not support the Starcraft2 API' %
                (baseVersion, label, version))
        if vCheckV < maxVersion:
            raise ValueError(
                'given %s (%d) cannot be smaller than newest known %s (%d)' %
                (vCheckK, vCheckV, vCheckK, maxVersion))
    uniqueValHeaders = list(c.JSON_HEADERS)
    uniqueValHeaders.remove('base-version')
    record = {'base-version': baseVersion}
    for k, v in zip(uniqueValHeaders, [label, version, dataHash, fixedHash,
        replayHash]):
        record[k] = v
        if not v:
            continue
        if v in [r[k] for r in Handler.ALL_VERS_DATA.values()]:
            raise ValueError("'%s' '%s' is in known values: %s" % (k, v,
                getattr(handle, k)))
            return
    handle.save(new=record)


class Handler(object):
    """NOTE: the data-hash field is required to launch stand-alone versions of the game"""
    ALL_VERS_DATA = None

    def __init__(self):
        self.load()

    def __len__(self):
        """the number of known version records"""
        return len(Handler.ALL_VERS_DATA)

    @property
    def mostRecent(self):
        records = iteritems(Handler.ALL_VERS_DATA)
        try:
            label, record = max(records)
            return record
        except ValueError:
            return None

    def load(self):
        """load ALL_VERS_DATA from disk"""
        basepath = os.path.dirname(os.path.abspath(__file__))
        filename = os.sep.join([basepath, c.FOLDER_JSON, c.FILE_GAME_VERSIONS])
        Handler.ALL_VERS_DATA = {}
        with open(filename, 'r') as f:
            data = json.loads(f.read())
        self.update(data)
        self._updated = False

    def save(self, new=None, timeout=2):
        """write ALL_VERS_DATA to disk in 'pretty' format"""
        if new:
            self.update(new)
        if not self._updated:
            return
        thisPkg = os.path.dirname(__file__)
        filename = os.path.join(thisPkg, c.FOLDER_JSON, c.FILE_GAME_VERSIONS)
        fParts = c.FILE_GAME_VERSIONS.split('.')
        newFile = os.path.join(thisPkg, c.FOLDER_JSON, '%s_%s.%s' % (fParts
            [0], dateFormat.now(), fParts[1]))
        if not os.path.isfile(newFile):
            os.rename(filename, newFile)
        recordKeys = [(record['version'], record) for record in Handler.
            ALL_VERS_DATA.values()]
        data = [r for k, r in sorted(recordKeys)]
        start = time.time()
        while time.time() - start < timeout:
            try:
                with open(filename, 'wb') as f:
                    f.write(str.encode(json.dumps(data, indent=4, sort_keys
                        =True)))
                self._updated = False
                return
            except IOError:
                pass
        raise

    def search(self, *args, **kwargs):
        """match all records that have any args in any key/field that also match
        key/value requirements specified in kwargs"""
        ret = []
        for record in Handler.ALL_VERS_DATA.values():
            matchArgs = list(kwargs.keys())
            for k, v in iteritems(kwargs):
                try:
                    if record[k] != v:
                        break
                except:
                    break
                matchArgs.remove(k)
            if matchArgs:
                continue
            matchArgs = list(args)
            for k, v in iteritems(record):
                if k in matchArgs:
                    matchArgs.remove(k)
                if v in matchArgs:
                    matchArgs.remove(v)
            if matchArgs:
                continue
            ret.append(record)
        return ret

    def update(self, data):
        """update known data with with newly provided data"""
        if not isinstance(data, list):
            data = [data]
        master = Handler.ALL_VERS_DATA
        for record in data:
            for k, v in iteritems(record):
                try:
                    record[k] = int(v)
                except ValueError:
                    record[k] = v
            try:
                label = record['label']
            except KeyError:
                raise ValueError(
                    'Must provide a valid label argument.  Given:%s%s' % (
                    os.linesep, ('%s  ' % os.linesep).join([('%15s:%s' % (k,
                    v)) for k, v in iteritems(kwargs)])))
            try:
                masterLabel = master[label]
            except KeyError:
                master[label] = record
                self._updated = True
                continue
            for k, v in iteritems(record):
                try:
                    if masterLabel[k] == v:
                        continue
                except KeyError:
                    pass
                self._updated = True
                try:
                    master[label].update(record)
                except KeyError:
                    break


handle = Handler()


class Version(object):
    """Represent a single version of the game settings"""

    def __init__(self, versionVal=None):
        if versionVal:
            if isinstance(versionVal, list):
                versionVal = max(versionVal)
            records = handle.search(versionVal)
            if len(records) > 1:
                raise ValueError('identified too many records (%d): %s' % (
                    len(records), records))
            elif len(records) < 1:
                raise ValueError(
                    'first collect and update version information for version: %s'
                     % versionVal)
            record = records.pop()
        else:
            record = handle.mostRecent
        for k in c.JSON_HEADERS:
            oldK = k
            wordBoundaries = re.search('(-\\w)', k)
            if wordBoundaries:
                for wb in wordBoundaries.groups():
                    newWb = wb.upper().strip('-')
                    k = re.sub(wb, newWb, k)
            setattr(self, k, record[oldK])
        while self.label.count('.') < 2:
            self.label += '.0'

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return '%s.%s' % (self.label, self.version)

    def __getitem__(self, key):
        return getattr(self, key)

    def toTuple(self, typeCast=int):
        return [typeCast(v) for v in self.label.split('.')]

    def toFilename(self):
        return re.sub('\\.', '_', str(self.label))


from __future__ import absolute_import
from __future__ import division       # python 2/3 compatibility
from __future__ import print_function # python 2/3 compatibility

from six import iteritems # python 2/3 compatibility
from io import BytesIO # python 2/3 compatibility

from sc2gameLobby import dateFormat
from sc2gameLobby import gameConstants as c

import json
import os
import re
import time


"""
Starcraft 2 patch/update information is copied from the following site(s):
    http://liquipedia.net/starcraft2/Patch_4.2.3 (and links listed on the page)
"""


################################################################################
def addNew(label, version, baseVersion, dataHash="", fixedHash="", replayHash=""):
    """
    Add a new version record to the database to be tracked
    VERSION RECORD EXAMPLE:
        "base-version": 55505, 
        "data-hash": "60718A7CA50D0DF42987A30CF87BCB80", 
        "fixed-hash": "0189B2804E2F6BA4C4591222089E63B2", 
        "label": "3.16", 
        "replay-hash": "B11811B13F0C85C29C5D4597BD4BA5A4", 
        "version": 55505
        """
    baseVersion = int(baseVersion)
    version     = int(version)
    minVersChecks = {"base-version":baseVersion, "version":version}
    if label in handle.ALL_VERS_DATA:
        raise ValueError("given record label (%s) is already defined.  Consider performing update() for this record instead"%(label))
    for vCheckK,vCheckV in iteritems(minVersChecks): # verify no conflicting values
        maxVersion  = min([vData[vCheckK] for vData in handle.ALL_VERS_DATA.values()])
        if vCheckV < c.MIN_VERSION_AI_API:
            raise ValueError("version %s / %s.%s does not support the Starcraft2 API"%(baseVersion, label, version))
        if vCheckV < maxVersion: # base version cannot be smaller than newest value
            raise ValueError("given %s (%d) cannot be smaller than newest known %s (%d)"%(vCheckK, vCheckV, vCheckK, maxVersion))
    uniqueValHeaders = list(c.JSON_HEADERS)
    uniqueValHeaders.remove("base-version")
    record = {"base-version" : baseVersion}
    #print("%15s : %s (%s)"%("base-version", baseVersion, type(baseVersion)))
    for k,v in zip(uniqueValHeaders, [label, version, dataHash, fixedHash, replayHash]): # new attr values must be unique within all handler records
        record[k] = v # convert to dict while checking each param
        #print("%15s : %s (%s)"%(k,v,type(v)))
        if not v: continue # ignore uniqueness requirement if value is unspecified
        if v in [r[k] for r in Handler.ALL_VERS_DATA.values()]:
            raise ValueError("'%s' '%s' is in known values: %s"%(k, v, getattr(handle, k)))
            return
    handle.save(new=record)


################################################################################
#def update(version, newValues):
#    """
#    USAGE EXAMPLES:
#        update(59729, {"fixed-hash":"123"})
#        update("3.19", {"replay-hash":"WHATEVER231"})
#    """
#    if len(newValues) == 0: raise ValueError("must provide data in paramater newValues else cannot update any record")
#    try:
#            int(version)
#            versionKey = "version"
#    except: versionKey = "label"
#    records = handle.search(version) # match record where versionKey value == version
#    if   len(records) > 1: raise ValueError("identified too many records (%d): %s"%(len(records), records))
#    elif len(records) < 1: raise ValueError("failed to identify any records given params: %s"%(records))
#    record = records.pop() # access the single, found record
#    for k,v in iteritems(newValues):   record[k] = v
#    handle._updated = True
#    handle.save()


################################################################################
class Handler(object):
    """NOTE: the data-hash field is required to launch stand-alone versions of the game"""
    ############################################################################
    ALL_VERS_DATA = None
    ############################################################################
    def __init__(self):
        self.load()
    ############################################################################
    #def __getattr__(self, key):
    #    """get all record values of a given key"""
    #    return sorted([v[key] for v in Handler.ALL_VERS_DATA.values()])
    ############################################################################
    def __len__(self):
        """the number of known version records"""
        return len(Handler.ALL_VERS_DATA)
    ############################################################################
    @property
    def mostRecent(self):
        records = iteritems(Handler.ALL_VERS_DATA)
        try: 
            label,record = max(records)
            return record
        except ValueError: return None # no versions are installed
    ############################################################################
    #def get(self, key, versFilter=""):
    #    """get a values of key where records match versFilter"""
    #    for v in self.search(versFilter): print(v)
    #    return [v[key] for v in self.search(versFilter)]
    ############################################################################
    def load(self):
        """load ALL_VERS_DATA from disk"""
        basepath = os.path.dirname(os.path.abspath(__file__))
        filename = os.sep.join([basepath, c.FOLDER_JSON, c.FILE_GAME_VERSIONS])
        Handler.ALL_VERS_DATA = {} # reset known data; do not retain defunct information
        with open(filename, "r") as f:
            data = json.loads( f.read() )
        self.update(data)
        self._updated = False
        #for v,record in iteritems(Handler.ALL_VERS_DATA):
        #    print(type(v), v)
            #for k,v in iteritems(record):    
    ############################################################################
    def save(self, new=None, timeout=2):
        """write ALL_VERS_DATA to disk in 'pretty' format"""
        if new: self.update(new) # allow two operations (update + save) with a single command
        if not self._updated: return # nothing to do
        thisPkg = os.path.dirname(__file__)
        filename = os.path.join(thisPkg, c.FOLDER_JSON, c.FILE_GAME_VERSIONS)
        fParts = c.FILE_GAME_VERSIONS.split('.')
        newFile = os.path.join(thisPkg, c.FOLDER_JSON, "%s_%s.%s"%(fParts[0], dateFormat.now(), fParts[1]))
        if not os.path.isfile(newFile):
            #fParts = c.FILE_GAME_VERSIONS.split('.')
            #newFile = "%s%s%s_%s.%s"%(c.FOLDER_JSON, os.sep, fParts[0], dateFormat.now(), fParts[1])
            #if not os.path.isfile(newFile):
            #print(filename)
            #print(newFile)
            os.rename(filename, newFile) # backup existing version file
        recordKeys = [(record["version"], record) for record in Handler.ALL_VERS_DATA.values()]
        data = [r for k,r in sorted(recordKeys)] # i.e. get values sorted by version key
        start = time.time()
        while time.time()-start < timeout: # allow multiple retries if multiple processes fight over the version file
            try:
                with open(filename, "wb") as f:
                    f.write(str.encode(json.dumps(data, indent=4, sort_keys=True))) # python3 requires encoding str => bytes to write to file
                self._updated = False
                return
            except IOError: pass # continue waiting for file to be available
        raise # after timeout, prior exception is what matters
    ############################################################################
    def search(self, *args, **kwargs):
        """match all records that have any args in any key/field that also match
        key/value requirements specified in kwargs"""
        ret = []
        for record in Handler.ALL_VERS_DATA.values():
            matchArgs = list(kwargs.keys())
            for k,v in iteritems(kwargs): # restrict records based on key-value match requirement
                try:
                    if record[k] != v: break # a non-matching requirement means this record doesn't match
                except: break # record doesn't have required key 'k'
                matchArgs.remove(k)
            if matchArgs: continue # didn't match all required kwargs
            matchArgs = list(args)
            for k,v in iteritems(record): # find any record with a <value> in it
                if k in matchArgs: matchArgs.remove(k)
                if v in matchArgs: matchArgs.remove(v)
            if matchArgs: continue # didn't match all required args
            ret.append(record)
        return ret
    ############################################################################
    #def search(self, searchVal, keyOnly=None, **filters):
    #    """identify all version records where searchVal exists (can be a regex)"""
    #    ########################################################################
    #    def checkValues(vData, regex):
    #        for k,v in iteritems(vData):
    #            #if k == "base-version": continue # ignore matching this key; expect it will match 'version' key where applicable
    #            if re.search(regex, str(v)): return True
    #        return False
    #    ########################################################################
    #    _tmp = re.compile("")
    #    if type(searchVal) == type(_tmp):
    #          regex = searchVal
    #    else: regex = re.compile(str(searchVal))
    #    ret = {}
    #    if keyOnly!=False: # search all keys (only)
    #        for vers in Handler.ALL_VERS_DATA.values():
    #            if any([re.search(regex, str(k)) for k in vers.keys()]):
    #                ret[vers["label"]] = vers
    #    if keyOnly!=True: # search all values (only)
    #        for vers in Handler.ALL_VERS_DATA.values():
    #            if checkValues(vers, regex):   ret[vers["label"]] = vers
    #    return ret.values()
    ############################################################################
    def update(self, data):
        """update known data with with newly provided data"""
        if not isinstance(data, list): data = [data] # otherwise no conversion is necessary
        master = Handler.ALL_VERS_DATA
        for record in data:
            #print(record)
            for k,v in iteritems(record): # ensure record contents aretyped appropriately
                try:                record[k] = int(v)
                except ValueError:  record[k] = v
            try: label = record["label"] # verify this record has the required 'label' key
            except KeyError:
                raise ValueError("Must provide a valid label argument.  Given:%s%s"%(\
                    os.linesep, ("%s  "%(os.linesep)).join(
                        ["%15s:%s"%(k,v) for k,v in iteritems(kwargs)]
                    )))
            try:    masterLabel = master[label] # identify the already existing record that matches this to-be-updated record, if any
            except KeyError: # master hasn't been defined yet
                master[label] = record
                self._updated = True # a new record should also be saved
                continue
            for k,v in iteritems(record): # determine whether master needs to be updated
                try:
                    if masterLabel[k] == v:  continue # whether an entry in the record needs to be updated (doesn't match)
                except KeyError:             pass # this condition means that k is a new key, so the record must be updated
                self._updated = True
                try:    master[label].update(record) # index each record by its label
                except KeyError:             break


################################################################################
handle = Handler()
#newRecord = {
#    "fixed-hash": "", 
#    "replay-hash": "", 
#    "data-hash": "", 
#    "label": "4.2.4", 
#    "base-version": 63454, 
#    "version": 64128
#}
#handle.save(new=newRecord)


################################################################################
class Version(object):
    """Represent a single version of the game settings"""
    ############################################################################
    def __init__(self, versionVal=None):
        if versionVal: # get specific version
            if isinstance(versionVal, list): versionVal = max(versionVal)
            records = handle.search(versionVal)
            if   len(records) > 1: raise ValueError("identified too many records (%d): %s"%(len(records), records))
            elif len(records) < 1: raise ValueError("first collect and update version information for version: %s"%(versionVal))
            record = records.pop()
        else: # select most recent version
            record = handle.mostRecent
        for k in c.JSON_HEADERS: # convert hyphenated keys into code-compatible keys
            oldK = k
            wordBoundaries = re.search("(-\w)", k)
            if wordBoundaries:
                for wb in wordBoundaries.groups():
                    newWb = wb.upper().strip('-')
                    k = re.sub(wb, newWb, k)
            setattr(self, k, record[oldK])
        while self.label.count(".") < 2: self.label += ".0" # ensure a 3-field versioning scheme
    ############################################################################
    def __str__(self):  return self.__repr__()
    def __repr__(self):
        return "%s.%s"%(self.label, self.version)
    ############################################################################
    def __getitem__(self, key):
        return getattr(self, key)
    ############################################################################
    def toTuple(self, typeCast=int):
        return [typeCast(v) for v in self.label.split('.')]
    ############################################################################
    def toFilename(self):
        return re.sub("\.", "_", str(self.label))

