#!/usr/bin/env python3

"""
@modified: April 25, 2025
@author: Dale Hamel
@author: Brian Fristensky
@contact: frist@cc.umanitoba.ca
The purpose of this class is to Provide support for the various actions that are repeated by several different python scripts
"""
import os
import sys
import tarfile
import shutil


class Birchmod:
    """
    Purpose:
    This class provides common safety features to scripts,
    as well as making user interactions more intuitive.
    In particular, this class is intended to provide
    user-readable information when an error occurs
    """

    def __init__(self, prog, use):
        """Initializes birchmod to contain the program usage and program name"""
        self.PROGRAM = prog
        self.USAGE = use

    def getHomeDir(self):
        """return the location of the user's home directory"""
        from os.path import expanduser
        return expanduser("~")

    def getEmailAddr(self):
        """return user's email address
        This is currently a bit of a hack. We need to find a better way to do this.
        1. If MAILID environment variable is set, use that.
        2. If MAILID="", then use the mailid in $BIRCH/local/admin/BIRCH.properties
        """

        emailaddr = os.environ.get("MAILID")
        if emailaddr == "" :
            BIRCH = os.environ.get("BIRCH")
            propfile = os.path.join(BIRCH,'local','admin','BIRCH.properties')
            propfile_h = open(propfile,'r')
            lines = propfile_h.readlines()
            if any(line.startswith('BirchProps.adminEmail') for line in lines) :
                emailaddr = line.strip().split('=')[1]
        return str(emailaddr)

    def GetBIRCHProperties(self,BIRCHDIR,PropName) :
        """
        Retrieve a value from BIRCH.properties. eg. To retrieve the value
        of BirchProps.adminEmail:

        GetBIRCHProperties(BIRCHDIR,"adminEmail")
        """
        PFN = os.path.join(BIRCHDIR , 'local' , 'admin' , 'BIRCH.properties')
        pfile = open(PFN,'r')
        Value = ""
        Target = 'BirchProps.' + PropName
        lines = pfile.readlines()
        pfile.close()
        plen = len(lines)
        if plen > 0 :
            i = 0
            while (i < plen) and  (Value == "") :
                line = lines[i]
                # ignore blank lines and comment lines
                if not (line.startswith('#')) :
                    tokens = line.split("=")
                    if tokens[0] == Target :
                        Value = tokens[1].strip()
                i += 1
        return Value

    def wget(self, url, name):
        """Downloads from url specified to the filename/path specified and displays progress"""
        print("Fetching "+name+" from "+url+"\n\n")
        import urllib.request
        def reporthook(numblocks, blocksize, filesize, url=None):
                    #base = os.path.basename(url)
            try:
                percent = min((numblocks * blocksize * 100) / filesize, 100)
            except:
                percent = 100
            if numblocks != 0:
                MB_CONST = 1000000 # 1 MB is 1 million bytes
                out_str = self.PROGRAM + "Progress:" + str(percent) + '%' + " of " + str(filesize / MB_CONST) + "MB\r"
                sys.stdout.write(out_str)
        #urlStream = urllib.urlretrieve(url, name, reporthook)
        response = urllib.request.urlopen(url)
        content = response.read()
        f = open(name, 'wb')
        f.write(content)
        f.close()

    def untar(self, file, path="."):
        """Extracts the tarfile given by file to current working directory by default, or path"""
        tarball = tarfile.open(file)
        tarball.extractall(path)
        tarball.close()

    #def get_free_bytes(self, path, blocksize=1024):
    #       """Returns the number of free megabytes in a directory"""
    #       stat = os.statvfs(path)
    #       free_bytes = (stat.f_bavail * stat.f_frsize) / blocksize # 1024 is constant across all systems tested
    #       #free = free_bytes / 1000 # if we want free space in something managable, megabytes
    #       return free_bytes

    def rmrf(self, path):
        """Mimics the behavior or unix rm -rf for the given path"""

        if (os.path.exists(path)):
            shutil.rmtree(path)

    def printusage(self):
        """Called when a program performs an illegal operation. Causes program to print its appropriate usage, and exit nicely"""
        print(self.PROGRAM + self.USAGE)
        raise(SystemExit)



    def file_error(self, file_name):
        """Called when reading a file fails, used to provide more comprehensive output"""

        file_name = str(file_name)
        print(self.PROGRAM + "An error occurred trying to process file named : \'" + file_name + "\'")

        if (os.path.isfile(file_name)):
            print(self.PROGRAM + "The file \'" + file_name + "\' exists, but may not be accessed.")

        else:
            print(self.PROGRAM + "The file \'" + file_name + "\' does not exist in \'" + os.getcwd() + "\'")

        raise(SystemExit)


    def exit_success(self):
        """Used to indicate to the user that a script did execute, and completed successfully"""
        print(self.PROGRAM + "Successfully completed execution")
        raise(SystemExit)


    def arg_given(self, argId):
        """Returns true if "argId" was a string passed on the command line"""
        if(not isinstance(argId, str)):
            print(self.PROGRAM + "Argument to be check for not supplied as string")
        else:
            if (argId in sys.argv):
                return True
            else:
                return False


    def documentor(self):
        """called by using "pydoc {path to script} pydoc" * note must pass "pydoc" as an argument to toggle documentation mode*"""
        if (self.arg_given("-pydoc")):
            print(self.PROGRAM + "Generating documentation...")
            import doctest
            doctest.testmod()
            return True
        else:
            return False



class Argument:
    """
    The purpose of this class is to provide a simplified and common way
    for all scripts retrieve arguments from the command line parameters provided (sys.argv)
    This is done by declaring an Argument variable, and specifying any advanced attributes
    with the various augmentation methods provided.

    BUG: Argument will split up a quoted command line argument containing blanks into individual
    tokens. Eg. 'this should be a single argument'.

    DEPRECATED in favor of optparse. Even optparse is deprecated  in favor of agrparse,
    but argparse is new in Python 2.7. Therefore, it is safer to use optparse, which has
    a very similar syntax to argparse, unless you are sure you will be using Python 2.7 or later.
    It is also worth mentioning that optparse will correctly parse command line arguments
    enclosed in quotes. There have been reports that
    argparse will break up strings between blank spaces.

    Examples:

    # optional arguments with parameters
    self.AInf = Argument("-inf", str, BM)
    self.AInf.set_optional()

    self.AOutf = Argument("-outf", str, BM)
    self.AOutf.set_optional()

    # optional argument with no parameters ie. switch
    self.AInvert = Argument("-inv", str, BM)
    self.AInvert.set_is_switch()
    self.AInvert.set_optional()

    # required arguments at a specific position.
    Ainfile = Argument("", str, BM)
    Ainfile.set_position(-2)

    Aoutfile = Argument("", str, BM)
    Aoutfile.set_position(-1)


    try:
        if (BM.arg_given("-inf")):
           self.InFormat = self.AInf.fetch()
        if (BM.arg_given("-outf")):
           self.OutFormat = self.AOutf.fetch()
        self.Invert = BM.arg_given("-inv")
        self.Ifn = Ainfile.fetch()
        self.Ofn = Aoutfile.fetch()
    except ValueError:
        BM.printusage()


    """

    def __init__(self, arg_id, arg_type, BMOD):
        """
        Initializer:
        arg_id: The command line flag that specifies an argument parameter is to follow,
                   ex: "-o outfile"
        type: The type that the argument is. This must be a basic type, such as str, float, bool.
                  if you are unsure if something is a basic type, try running "type(yourtypename)" at interpretter
        BM: a pointer to the Birchmod module for the class using this argument (they are coupled)
        """
        self.arg_id = str(arg_id)
        self.is_required = True # by default arguments are required
        self.position = 0 #by default the argument can go anywhere, so we put a sentinal value here
        self.exclude = None#by default not mutually exclusive
        self.is_flag = False
        self.BM = None

        #ensure that the type passed is a valid KNOWN type
        if(isinstance(arg_type, type)):
            self.arg_type = arg_type

        elif(arg_type == None):
            self.is_flag = True
            self.arg_type = None

        else:
            raise(ValueError)

        #The Argument class uses comon methods from the Birchmod class, so it must be passed an instance
        if(isinstance(BMOD, Birchmod)):
            self.BM = BMOD
        else:
            raise(ValueError)



    def set_position(self, position):
        """
        Specify the position on sys.argv where the argument is always found.
        Argument 0 is the name of the program, so argument 1 is the first argument.
        Note that arguments near the end can be specified by len(sys.argv)-X,
        where X is the index from the end. "-1" refers to the last argument, "-2"
        refers to the next to last argument, etc.
        """
        self.position = int(position)#if this argument needs to be a specific position, this is where it must be


    def add_exclusive(self, exclude):
        """Add an argument (flag) to the list of mutually exclusive argument flags for this Argument's flag"""
        if (exclude == self.arg_id):
            print(self.BM.PROGRAM + "An argument may not exclude itself")

        if(self.exclude == None):
            self.exclude = list()

        self.exclude.append(exclude)


    def set_optional(self):
        """Use this if the paramter passed is NOT required"""
        self.is_required = False


    def set_is_switch(self):
        """if this argument is just a switch (with no parameters), set this to true (ex ls -l, -l is a switch)"""
        self.is_flag = True
        self.arg_type = None


    def fetch(self):
        """
        This method attempts to fetch the argument with the specified attributes from sys.argv
        """
        #if a specified location is already given, get it from there
        if (abs(self.position) > 0):
            if (self.position < len(sys.argv)):
                to_return = sys.argv[self.position]

            else:
                raise(ValueError)

        #if it is a simple flag
        elif(self.is_flag):
            if (self.exclude != None):
                for each in self.exclude:
                    if(each in sys.argv and self.arg_id in sys.argv):

                        print(self.BM.PROGRAM + "Cannot combine mutually exclusive options \"" + each + "\" and \"" + self.arg_id + "\"")
                        exit()
            to_return = (self.arg_id in sys.argv)

        #if it is a flag WITH a paramter
        else:
            if (self.is_required):#required arguments must provided
                if (not self.arg_id in sys.argv):
                    print(sys.argv)
                    raise(ValueError)

                else:

                    to_return = self.__get_arg(self.arg_id)
            else:#optional


                if (self.exclude != None):
                    for each in self.exclude:
                        if(each in sys.argv and self.arg_id in sys.argv):

                            raise(ValueError)

                to_return = self.__get_arg(self.arg_id)


        if(self.arg_type != None and self.arg_id in sys.argv):


            to_return = self.arg_type(to_return)

        if(self.arg_type == str and to_return == None):
            to_return = ''

        #print("got arg:"+str(to_return))
        return to_return



    ###########################PRIVATE########################################


    def __get_arg(self, argId):
        """
        Used as a helper method. Retrieves a flags parameter, assuming it is one index to the right.
        ex "-o outfile", if argId= -o, this will return outfile
        """
        argId = str(argId)

        try:
            position = sys.argv.index(argId) + 1
            if (position < len(sys.argv)):
                return sys.argv[position]
            else:
                raise(ValueError)
        except ValueError:
            print (self.BM.PROGRAM + "Argument " + argId + " not supplied at command line.")
            return None

class HTMLWriter:
    "Methods for writing html to a file"

    def __init__(self):
        """
          Initializes arguments:
                indentwidth=3
                col=0
                lpad=""
          """
        self.indentwidth = 3
        self.col = 0 # current indentation column
        self.lpad = ""


    def indent(self):
        """
          **indent is not currently used by htmlwriter**
          decrease indent using identwidth blank spaces
          """

        self.col = self.col + self.indentwidth
        self.lpad = ' '.rjust(self.col)

    def undent(self):
        """
          **undent is not currently used by htmlwriter**
          decrease indent using identwidth blank spaces
          """

        self.col = self.col - self.indentwidth
        if self.col < 0:
            self.col = 0
            self.lpad = ""
        else:
            self.lpad = ' '.rjust(self.col)

    def start(self, htmlfile, tagname, attributes):
        """
          Write begin tag, with attributes
          @param htmlfile: The name of the html file to write the tag for
          @type htmlfile: str
          @param tagname: The name of the tag
          @type tagname: str
          @param attributes: The tag information itself
          @type attributes: str
          """

        htmlfile.write('<' + tagname + attributes + '>\n')

    def end(self, htmlfile, tagname):
        """
          Write end tag
          @param htmlfile: The of the html file to write tag for
          @type htmlfile: str
          @param tagname: The name of the tag to write
          @type tagname: str
          """

        htmlfile.write('</' + tagname + '>\n')

    def page_title(self, htmlfile, title):
        """
          Write title
          @param htmlfile: The name of the html file to add the title to
          @type htmlfile: str
          @param title: The title to write
          @type title: str
          """

        htmlfile.write('<title>' + 'birch - ' + title + '</title>\n')

    def link(self, htmlfile, url, attributes, text):
        """
          FIXME
          @param htmlfile:
          @type htmlfile:
          @param url:
          @type url:
          @param attributes:
          @type attributes:
          @param text:
          @type text:
          """
        "Write hypertext link"
        htmlfile.write('<a href="' + url + '"' + attributes + '>' + text + '</a>')

    def start_page(self, htmlfile, title):
        """
          FIXME
          @param htmlfile:
          @type htmlfile:
          @param title:
          @type title:
          """
        "Information at the top of each page is the same."
        htmlfile.write('<!-- this page generated automatically by htmldoc.py -->\n')

        self.start(htmlfile, 'html', '')
        self.page_title(htmlfile, title)
        attributes = ' style ="background-color: rgb(255, 204, 0);"'
        self.start(htmlfile, 'body', attributes)
        # main heading, including birch logo which links to birch
        # home page.
        self.start(htmlfile, 'h1', '')
        url = '../../index.html'
        text = '<img alt="birch - " src="../../images/birch_white.png" height="68" width="100">'
        self.link(htmlfile, url, '', text)
        htmlfile.write(' ' + title)
        self.end(htmlfile, 'h1')
        htmlfile.write('<br>')

    def indent_text(self, htmlfile, text):
        """
          FIXME
          @param htmlfile:
          @type htmlfile:
          @param text:
          @type text:
          """
        "indent a line of text"
        attributes = ' style="margin-left: 40px;"'
        self.start(htmlfile, 'div', attributes)
        htmlfile.write(text)
        self.end(htmlfile, 'div')

    def end_page(self, htmlfile):
        """
          FIXME
          @param htmlfile:
          @type htmlfile:
          """
        "html tags for end of page"
        self.end(htmlfile, 'body')
        self.end(htmlfile, 'html')

class Htmlutils:

    def __init__(self, CATDICT, PROGDICT):
        """
                FIXME
                @param CATDICT:
                @type CATDICT:
                @param PROGDICT:
                @type PROGDICT:
                """
        self.CATDICT = CATDICT
        self.PROGDICT = PROGDICT

    # - - - - - - - - - - - - - - - - - - - - - - - -
    def tokenize(self, line):
        """
                FIXME
                @param line:
                @type line:
                """
        "split up input line into tokens, where one or more spaces are seperators"
        "tokenize implicitly gets rid of the first token in the list"

        # parse the line into tokens
        tokens = line.split()

        # strip quotes that begin and end data values in .ace files
        i = 1
        while i < len(tokens):
            tokens[i] = tokens[i].strip('"')
            # get rid of \ escape characters added by acedb
            tokens[i] = tokens[i].replace('\\', '')
            i = i + 1

        return tokens

    def cmp_to_key(self, mycmp):
        """
                this function simplifies the transition to python 3 by eleiminating the need for the "cmp" function
        this was a recommended workaround for using "cmp"'s in sorts (recommended by: http://wiki.python.org/moin/HowTo/Sorting/)
                """
        class K(object):
            def __init__(self, obj, * args):
                """
                                FIXME
                                @param obj:
                                @type obj:
                                @param *args:
                                @type *args:
                                """
                self.obj = obj
            def __lt__(self, other):
                """
                                FIXME
                                @param other:
                                @type other:
                                """
                return mycmp(self.obj, other.obj) < 0
            def __gt__(self, other):
                """
                                FIXME
                                @param other:
                                @type other:
                                """
                return mycmp(self.obj, other.obj) > 0
            def __hash__(self, other):
                """
                                FIXME
                                @param other:
                                @type other:
                                """
                return mycmp(self.obj, other.obj) == 0
            def __le__(self, other):
                """
                                FIXME
                                @param other:
                                @type other:
                                """
                return mycmp(self.obj, other.obj) <= 0
            def __ge__(self, other):
                """
                                FIXME
                                @param other:
                                @type other:
                                """
                return mycmp(self.obj, other.obj) >= 0
            def __ne__(self, other):
                """
                                FIXME
                                @param other:
                                @type other:
                                """
                return mycmp(self.obj, other.obj) != 0
        return K

    def name_to_url(self, name, doc_prefix):
        """
                FIXME
                @param name:
                @type name:
                @param doc_prefix:
                @type doc_prefix:
                """
        "Convert a Unix path to a URL"
        "If the path begins with an environment variable like $doc,"
        "assume that the name of the directory is the name of the variable"
        " ie. just delete the '$' and append the path to DOCPREFIX"
        "Otherwise, assume it is a URL of the form 'http:///'"
        if name[0] == '$':
            url = doc_prefix + '/' + name[1:]
            #url = name[1:]
        else:
            url = name
        return url

    def get_prefix(self, fn, p):
        """
                FIXME
                @param fn:
                @type fn:
                @param p:
                @type p:
                """
        "read $birch/install-scripts/newstr.param, to get the prefixes"
        "needed for urls"
        file = open(fn, 'r')

        i = 1
        for line in file:

            if i == 3:
                p.DOCPREFIX = line.strip("\\s")

            i = i + 1
        file.close()

        if (p.DOCPREFIX.find('http://') == 0) or (p.DOCPREFIX.find('file:///') == 0):
            okay = True
        else:
            okay = False

        return okay



class SimpleXML:
    "Simple methods for working wth XML files as an alternative to xml or defusedxml"

    def __init__(self) :
        """
        """

    def GetXMLField(self,File,FieldName):
        """
                return a string value from a field of the form <FieldName>Value</FieldName>
                This function ONLY returns the first field in the file that matches the
                pattern, regardless of how many subsequent fields may match.
        """
        Value = ""
        BeginTarget = '<' + FieldName + '>'
        EndTarget = '</' + FieldName + '>'
        h_XMLFile = open(File, 'r')
        line = h_XMLFile.readline()
        POSN = -1
        while (line != "") and  (Value == "") :
            LPOSN = line.find(BeginTarget)
            RPOSN = line.find(EndTarget)
            if LPOSN > -1 and RPOSN > -1 :
                Value = line[LPOSN+len(BeginTarget):RPOSN]
            line = h_XMLFile.readline()
        h_XMLFile.close()

        return Value


#used to generate documentation
if __name__ == "__main__":
    import doctest
    doctest.testmod()
