#!/usr/bin/env python3
#  coding=utf-8
from __future__ import print_function
#==============================================================================
#
# File: funcs.py - Various utility functions
#
# Functions: print              - Replacement for Python's print (with counter in place)
#            execute            - Execute a shell command without redirection
#            Error              - Print an error
#            Warning            - Print a warning
#            Info               - Print info (if verbose set)
#            Internal           - Print an internal error
#            Literal            - Print literal message
#            Debug              - Print debug message (if debug and verbose are set)
#            NotImp             - Print "Not implemented"
#            Notice             - Print a notice message
#            Prefix             - Print string with prefix
#            deepSearch         - Search directory deep for files
#
#            dtNow              - datetime.datetime object for datetime now
#            dtToReadable       - datetime.datetime object to human readable date @ time
#
#            getHostName        - "What's my machine name?"
#            myIps              - Return list of my IP addresses
#            promptInput        - Prompt and get input from <stdin>
#            fixUri             - Substitute proper URI characters (%xx) if needed
#            hostAlive          - "Is named host alive? (ping it once)"
#            getPid             - General "find PID of something somewhere"
#            getPids            - General "Find PID(s) of something somewhere"
#            flushout           - Flush stdout and stderr
#            isInt              - "Is this string a valid integer?"
#            isFloat            - "Is this string a valid floating point numbere?"
#            isHexString        - "Is this a hexidecimal string?"
#            username           - Return current OS user name
#
#            postEllipse        - Turn string into fixed size post-ellipsified string
#            preEllipse         - Turn string into fixed size pre-ellipsified string
#            midEllipse         - Turn string into fixed size mid-ellipsified string
#
#            pathFindUp         - Search for something from directory up
#            reprLen            - "How many characters in repr(x)?"
#            findChars          - Look for many characters in a string
#            flushOut           - Flush sys.stdout
#            isIpv4             - "Is this string a valid ipv4 address?"
#            parseIp            - Parse IPv4/IPv6 address/port
#            splitName          - Split a "...#...@..." object name into 3 parts
#
#            formatHex          - Format bytes into readable text
#            hexToBytes         - Convert hexidecimal string into bytes
#            bytesToHex         - Convert bytes to hexidecimal string
#
#            sha256             - Calculate sha256 checksum of file
#            sha1               - Calculate sha1   checksum of file
#            sha1_bytes         - Compute sha1 checksum of a byte string
#            sha256_bytes       - Compute sha2 checksum of a byte string
#
#            kallsyms           - Return dictionary of /proc/kallsyms 'T' addresses
#            linuxVersion       - Return Linux equivalent of "uname -r"
#
#            osName             - Return 'Ubuntu', 'Centos', or None based on OS version
#            isUbuntu           - "Are we running on Ubuntu?"
#            isCentOS           - "Are we running on CentOS?"
#
#            which              - Search $PATH for executable
#
#            exceptionLines     - Turn exception traceback into printable lines
#
#            sudo               - Restart running process as sudo if not already (!!!Danger!!!)
#
#            ipAddresses        - Return iterable of host's IP addresses
#            ipv4Addresses      - Return iterable of host's IPv4 addresses
#
#            packages           - Return list of installed packages
#
#            red   - Print red error
#            green - Print green message
#            blue  - Print blue message
#            error - Print red error and exit with status
#            color - Use colr.color if stdout is a tty
#
#==============================================================================

import subprocess
import os
import sys
import pwd
import datetime
import time
import json
import shlex
import hashlib
import re
try:
  import colr
except:
  class colr:
    def color(*args,**pargs):
      return args[0]

verbose = False
debug   = False
quiet   = False

#-------------------------------------------------------------------------------
#
# Function: execute - Execute shell command and, optionally, show output
#
# Usage:    rc,out,err = execute(cmd,prefix=None)
#
# Where:    cmd     - str:  Shell command to execute
#           prefix  - str:  If set, prefix to prepend to output lines (if showout=True)
#           showout - bool: False="Quiet", True="Show stdout"
#           showerr - bool: False="Quiet", True="Show stderr"
#           env     - dict: Environment to use
#           debug   - bool: False="Normal", True="Call gdb --args with command"
#
#           rc      - int: Return code
#           out     - str: Output from stdout
#           err     - str: Output from stderr
#
# Notes:    If prefix is None, stdout will not be displayed.
#

def execute(cmd,prefix='',showout=False,showerr=False,env=os.environ,debug=False,shell=False):

    if type(cmd) == list:
      args = cmd
    else:
      args = shlex.split(cmd)

    if args and args[0] == 'gdb':
      stdout = sys.stdout
      stderr = sys.stderr
    else:
      stdout = subprocess.PIPE
      stderr = subprocess.PIPE

    p = subprocess.run(args,stdin=sys.stdin,stdout=stdout,stderr=stderr,env=env,shell=shell)

    rc  = p.returncode
    out = str(p.stdout,'utf-8')
    err = str(p.stderr,'utf-8')

    if showout:
        lines = out.split('\n')
        if lines and not lines[-1]:
            del lines[-1]
        print("execute: stdout...")
        for line in lines:
            print(prefix+line)
        print("...stdout")

    if showerr and err:
        lines = err.split('\n')
        if lines and not lines[-1]:
            del lines[-1]
        print("execute: stderr...")
        for line in lines:
            print(prefix+line)
        print("...stderr")
        if not showerr and env:
            if 'LD_LIBRARY_PATH' not in env:
                print("Error: LD_LIBRARY_PATH missing")
            else:
                print("LD_LIBARY_PATH = %s"%repr(env['LD_LIBRARY_PATH']))

    return (rc,out,err)

#-------------------------------------------------------------------------------
#
# Functions: Error    - Print an error
#            Warning  - Print a warning
#            Internal - Print an internal error
#            Info     - Print info (if verbose set)
#            Literal  - Print literal message
#            Debug    - Print debug message (if debug and verbose are set)
#            Notice   - Print a notice message
#            NotImp   - Print "Not implemented"
#
#            Prefix   - Print string with prefix
#
#-------------------------------------------------------------------------------

def Error(msg,nl=True,fore='red',style='bright'):

    Prefix("Error: ",msg,nl=nl,fore=fore,style=style)

def Warning(msg,nl=True):

    Prefix("Warning: ",msg,nl=nl)

def Internal(msg,nl=True):

    Prefix("Internal Error: ",msg,nl=nl)

def Info(msg,nl=True):

    if verbose:
        Prefix("Info: ",msg,nl=nl)

def Literal(msg,nl=True):

    Prefix('',msg,nl=nl)

def Debug(msg,nl=True):

    if debug and verbose:
      Prefix("Debug: ",msg,nl=nl)

def Notice(msg,nl=True):

    if not quiet:
      Prefix("Notice: ",msg,nl=nl)

def NotImp(item=None,nl=True):

    if not item:
        Error("***Not Implemented Yet***",nl)
    else:
        Error("*** %s Not Implemented Yet ***"%repr(item)),nl

def Deprecated(item=None,nl=True):

    if not item:
        error("*** Deprecated ***",nl)
    else:
        Error("*** %s Deprecated ***"%repr(item),nl)

def Prefix(msg,prefix='',samePrefix=False,nl=True,fore=None,style=None):

    end = '\n' if nl else ''

    msgs = msg.split('\n')
    thisPrefix = prefix
    if len(msgs) > 1:
        print('')
        for i in range(0,len(msgs)):
            msg = thisPrefix+msgs[i]
            msg = color(msg,fore=fore,style=style)
            print(msg)
            if not samePrefix:
                thisPrefix = ' '*len(prefix)
    else:
        msg = thisPrefix+msg
        msg = color(msg,fore=fore,style=style)
        print(msg)

#-------------------------------------------------------------------------------
#
# Function: typeName - Return the type name as a String
#
#-------------------------------------------------------------------------------

def typeName(that):

    typeStr = repr(type(that))
    typeStr = typeStr[1:-1].split(' ')[1][1:-1]

    return typeStr

#-------------------------------------------------------------------------------
#
# Function: formatKeys - Given a dict, format the keys in a list of output lines
#
#-------------------------------------------------------------------------------

def formatKeys(dict):
    keys = list(dict.keys())
    keys.sort()
    maxLen = 0
    for key in keys: maxLen = max(maxLen,len(key))
    result = []
    for key in keys:
        line = key+' '*(maxLen-len(key))+' = '+repr(dict[key])
        result.append(line)
    return result

#-------------------------------------------------------------------------------
#
# Function: deepSearch - Recursively search for files
#
# Usage:    files = deepFind(dir,match)
#
# Where:    dir   - str:  Source directory
#           match - func: Function to test if file is wanted (passed full path)
#           files - list: List of selected files
#
# Example:  The following is a sample 'match' function looking for "*.war"...
#
#             def matchWar(that):
#               return that[-4:] == '.war'
#
#           Can do it even cooler than that...
#
#             files = deepFind('<some dir>',lambda x: x[-4:] == '.war')
#

def deepSearch(dir,match):

    stack = []
    stack.append(dir)
    files = []

    while stack:
        dir = stack[0]
        del stack[0]
        things = os.listdir(dir)
        for thing in things:
            thing = dir+os.sep+thing
            if os.path.isdir(thing):
                stack.append(thing)
            elif os.path.isfile(thing) and match(thing):
                files.append(thing)

    return files

#-------------------------------------------------------------------------------
#
# Function:  dtNow        - datetime.datetime object for time now
#            dtFromNonce  - datetime.datetime object from Nonce string
#            dtToNonce    - datetime.datetime object to a Nonce string
#            dtToReadable - datetime.datetime object to human readable date @ time
#            deltaSeconds - "How old is a datetime object in seconds?"
#            dtNowNonce   - Return string Timestamp of datetime now
#            isValidNonce - "Is this a valid timestamp nonce?"
#

def dtNow():

    result = datetime.datetime.fromtimestamp(time.time())

    return result

def dtFromNonce(nonce):

    try:
        parts = nonce.split('T')
        #print("dtFromNonce: a...")
        year,mon,day = parts[0].split('-')
        #print("dtFromNonce: b...")
        hour,min,sec = parts[1].split(':')
        #print("dtFromNonce: c...")
        sec,ms = sec.split('.')
        #print("dtFromNonce: d...")
        #print("dtFromNonce: year=%s, mon=%s, day=%s"%(repr(year),repr(mon),repr(day)))
        #print("dtFromNonce: hour=%s, min=%s, sec=%s, ms=%s"%(repr(hour),repr(min),repr(sec),repr(ms)))
        year = int(year)
        mon  = int(mon)
        day  = int(day)
        hour = int(hour)
        min  = int(min)
        sec  = int(sec)
        ms   = int(ms)%1000
        #print("dtFromNonce: y,m,d,h,m,s = %d,%d,%d,%d,%d,%d,%d"%(year,mon,day,hour,min,sec,ms))
    except:
        return None

    result = datetime.datetime(year,mon,day,hour=hour,minute=min,second=sec,microsecond=ms*1000)
    #print("dtFromNonce: result = %s"%repr(result))

    return result

def dtToNonce(dt):

    year  = dt.year
    mon   = dt.month
    day   = dt.day
    hour  = dt.hour
    min   = dt.minute
    sec   = dt.second
    ms    = (dt.microsecond+500)/1000
    nonce = '%04d-%02d-%02dT%02d:%02d:%02d.%03d'%(year,mon,day,hour,min,sec,ms%1000)

    return nonce

def dtToReadable(dt,ampm=True):

    nonce = dtToNonce(dt)

    parts = nonce.split('T')

    if ampm:
      dat = parts[1][:-4]
      hour = int(dat[-8:-6])
      dat  = dat[:-8] + '%02d' + dat[-6:] + '%s'
      if hour > 12:
        ampm = 'pm'
        hour -= 12
      else:
        ampm = 'am'
      parts[1] = dat%(hour,ampm)

    return ' @ '.join(parts)

def deltaSeconds(delta): # ◄── delta=datetime.timedelta, returns float seconds

    diff = float(delta.days*(60*60*24)+delta.seconds) + delta.microseconds/1000000.0

    return diff

def dtNowNonce():

    return dtToNonce(dtNow())

def isValidNonce(ts):

    if type(ts) is not str:
        return False
    if len(ts) != 23:
        return False
    if ts[10] != 'T':
        return False

    # Example: 2018-06-22T11:39:50.898

    date = ts[:10]
    time = ts[11:]

    if date[4] != '-' or date[7] != '-':
        return False
    if not isInt(date[:4]) or not isInt(date[5:7]) or not isInt(date[8:]):
        return False
    if time[2] != ':' or time[5] != ':' or time[8] != '.':
        return False
    if not isInt(time[:2]) or not isInt(time[3:5]) or not isInt(time[6:8]) or not isInt(time[9:]):
        return False

    return True

#------------------------------------------------------------------------------
#
# Function: myIps - Return list of my IP addresses (removes '127.0.0.1')
#
# Usage:    ips = myIps()
#
# Where:    ips - str/list: List of non 127.0.0.1 ips or string if only one
#

def myIps():

    rc,output,err = execute("sudo ifconfig",showout=False)
    lines = output.split('\n')

    ips = []
    for i in range(0,len(lines)):
        line = lines[i]
        if 'inet ' not in line:
            continue
        parts = line.split(' ')
        for j in range(0,len(parts)-1):
          ip = parts[j]
          if not isIpv4(ip):
            continue
          if ip != '127.0.0.1':
            ips.append(ip)
            break
    if not ips:
      return ips

    return ips if len(ips) > 1 else ips[0]

#-------------------------------------------------------------------------------
#
# Function: getHostName - "What's my host name?"
#
# Usage:    iam = getHostName()
#
# Where:    iam - str: Current host's name (if any)
#
def getHostName():

    import socket
    return socket.gethostname()

#-------------------------------------------------------------------------------
#
# Function: promptInput - Prompt and get input from <stdin>
#
# Usage:    line = promptInput(prompt,history=None)
#
# Where     line    - str:  Input line
#           prompt  - str:  Prompt string
#           history - list: History list (or None)
#

def promptInput(prompt,history=None):

    import getch

    line = ''

    print(prompt,end='')

    while True:
        c = getch()

#------------------------------------------------------------------------------
#
# Function: fixUri - Replace reserved characters with %xx equivalents
#
# Notes: From https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters
#
#        Reserved characters after percent-encoding ()
#        !   #   $   &   '   (   )   *   +   ,   /   :   ;   =   ?   @   [   ]
#        %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D
#

fixes = {}
fixes['!'] = '%21'
fixes['#'] = '%23'
fixes['$'] = '%24'
fixes['&'] = '%26'
fixes["'"] = '%27'
fixes['('] = '%28'
fixes[')'] = '%29'
fixes['*'] = '%2a'
fixes['+'] = '%2b'
fixes[','] = '%2c'
fixes['/'] = '%2f'
fixes[':'] = '%3a'
fixes[';'] = '%3b'
fixes['='] = '%3d'
fixes['?'] = '%3f'
fixes['@'] = '%40'
fixes['['] = '%5b'
fixes[']'] = '%5d'

def fixUri(uri):

    i = -1
    while True:
        i += 1
        if i >= len(uri): break
        c = uri[i]
        if c in fixes:
            uri = uri[:i] + fixes[c] + uri[i+1:]
            i += 2

    return uri

#-------------------------------------------------------------------------------
#
# Function: hostAlive -  "Is named host alive? (ping it once)"
#

def hostAlive(host):

    if type(host) != str:
        raise ValueError("Expected host parameter to be a string")

    cmd = "ping -c 1 "+host
    rc,out,err = execute(cmd,showout=False)
    if host in out and ", 1 received," in out:
        return True

    return False

#------------------------------------------------------------------------------
#
# Function: kill - Kill a given pid or pids on host
#

def kill(host,pids):

    if not pids:
        return

    if type(pids) in (list,tuple):
        pids = ' '.join(pids)
    cmd = "sudo ssh %s kill -9 %s"%(host,pids)
    rc,output,err = execute(cmd,showout=False)
    if output:
        lines = output.split('\n')
        for i in range(0,len(lines)):
            lines[i] = '... '+lines[i]
        output = '\n'.join(lines)
        print("funcs.kill: Failed to kill pids %s, output follows...\n%s"%(pids,output))

#-------------------------------------------------------------------------------
#
# Functions: getPid - General "find PID of something somewhere"
#
# Usage: pid = getPid(lookFor=<lookFor>,host=<host>)
#        pids = getPids(lookFor,host=<host>)
#
#        Where: <lookFor> - str:  String to look for
#               <host>    - str:  Host to look on ('' or None means "This machine")
#               pid       - str:  Pid found ('' means "Not Found")
#               pids      - list: List of PID(s)
#
# Note: We GUARANTEE return value with be either '' or a valid integer string
#
#       If neither lookFor or host are specified this returns our Pid.
#

def getPid(lookFor='',host=''):

    if not lookFor and not host:
        return str(os.getpid())

    pids = getPids(lookFor,host)
    if not pids:
        return ''

    return pids[0]

def getPids(lookFor='',host=''):

    if not lookFor and not host:
        return str(os.getpid())

    if not lookFor and host:
        raise ValueError("lookFor == '' and host %s specified, doesn't mean anything to me"%repr(host))

    if type(lookFor) != str:
        raise ValueError("Expected 'lookFor' to be type str")
    if type(host) != str:
        raise ValueError("Expected 'host' to be type str")

    if not host:
        cmd = "ps -aux"
    else:
        cmd = "ssh %s 'ps -aux'"%host

    rc,out,err = execute(cmd,showout=False)
    if not out:
        raise ValueError("Expected non-empty output")
        return []

    # Find a line with lookFor in it...
    lines = out.split('\n')
    results = []
    for line in lines:
        if lookFor in line:
            parts = line.split(' ')
            i = 0
            while i < len(parts):
                if not parts[i]:
                    del parts[i]
                else:
                    i += 1
            if len(parts) < 2:
                continue
            if isInt(parts[1]):
                results.append(parts[1])

    return results

#-------------------------------------------------------------------------------
#
# Function: flushOut - Flush stdout and stderr
#

def flushOut():

    sys.stdout.flush()
    sys.stderr.flush()

#-------------------------------------------------------------------------------
#
# Functions: isInt        - "Is this string a valid integer?"
#            isFloat      - "Is this string a valid floating point number?"
#            isHexString  - "Is this string a valid hexidecimal string?"
#

def isInt(string):

    try:
        i = int(string)
    except:
        return False

    return True

def isFloat(string):

    try:
        f = float(string)
    except:
        return False

    return True

def isHexString(string):

    if type(string) != str or not string or len(string)%2 != 0:
        return False

    string = string.strip().lower()
    chars = '0123456789abcdefg'
    for char in string:
        if char not in chars:
            return False

    return True

#-------------------------------------------------------------------------------
#
# Function: username - Return current OS user name
#

def username():

    return pwd.getpwuid(os.getuid()).pw_name

#------------------------------------------------------------------------------
#
# Function: postEllipse - Turn string into fixed size post-ellipsified string
#                         (e.g. "abcdef...")
#
# Usage:    result = postEllipse(inStr,tlen,fill=' '))
#
# Where:    inStr  - str: Input string
#           tlen   - int: Resulting string length (see notes below)
#           fill   - str: Fill character
#           result - str: Resulting string of length len
#
# Notes:    If len < 3, as many '.' as possible will form the string
#
#           If len <= 0, an empty string is returned
#
#           if len(inStr) < len, it will be padded with ' '
#

def postEllipse(inStr,tlen,fill=' '):

    if type(inStr) is not str:
        raise TypeError("Expected inStr to be type str")
    if type(tlen) is not int:
        raise TypeError("Expected tlen to be type int")

    if tlen <= 0:
        return ''
    if tlen <= 3:
        return '...'[0:tlen]

    if len(inStr) <= tlen-3:
        return inStr + fill[0]*(tlen-len(inStr))

    inStr = inStr[:tlen-3] + fill[0]*3

    return inStr

#------------------------------------------------------------------------------
#
# Function: preEllipse - Turn string into fixed size pre-ellipsified string
#                         (e.g. "...abcdef")
#
# Usage:    result = preEllipse(inStr,tlen,fill=' '))
#
# Where:    inStr  - str: Input string
#           tlen   - int: Resulting string length (see notes below)
#           fill   - str: Single character string to fill with
#           result - str: Resulting string of length len
#
# Notes:    If len < 3, as many '.' as possible will form the string
#
#           If len <= 0, an empty string is returned
#
#           if len(inStr) < len, it will be pre/post-padded with ' '
#

def preEllipse(inStr,tlen,fill=' '):

    if type(inStr) is not str:
        raise TypeError("Expected inStr to be type str")
    if type(tlen) is not int:
        raise TypeError("Expected tlen to be type int")

    if tlen < 3:
        return ''

    if len(inStr) >= tlen-3:
        return fill[0]*3 + inStr[-(tlen-3):]
    else:
        return fill[0]*(tlen-len(inStr)) + inStr[tlen-3:]

#------------------------------------------------------------------------------
#
# Function: midEllipse - Turn string into fixed size mid-ellipsified string
#                         (e.g. "abcdef...wxyz")
#
# Usage:    result = midEllipse(inStr,tlen)
#
# Where:    inStr  - str: Input string
#           tlen   - int: Length of resulting String
#           result - str: Resulting string of length len
#
# Notes:    If len < 3, as many '.' as possible will form the string
#
#           If len <= 0, an empty string it returned
#
#           if len(inStr) < len, it will be padded with ' '
#

def midEllipse(inStr,tlen):

    if type(inStr) is not str:
        raise TypeError("Expected inStr to be type str")
    if type(tlen) is not int:
        raise TypeError("Expected tlen to be type int")

    if tlen <= 0:
        return ''
    if tlen <= 3:
        return '...'[0:tlen]

    if len(inStr) <= tlen:
        return inStr + ' '*(tlen-len(inStr))

    len1 = int((tlen-3)/2)
    len2 = (tlen-3)-len1

    return inStr[0:(len1)] + '...' + inStr[-len2:]

#------------------------------------------------------------------------------
#
# Function: pathFindUp - Search for something from directory up
#
# Usage:    loc = pathFindUp(startDir,searchFor)
#
# Where:    startDir  - str: Directory to start from ('' means current dir)
#           searchFor - str: What to search for (musn't have os.sep in it)
#           loc       - str: Where found ('' = "Not Found")
#
# Notes:    If searchFor has os.sep ('/' in it), '' will be returned in loc.
#

def pathFindUp(startDir,searchFor):

    sep = os.sep

    if sep in searchFor:
        return ''

    if not startDir:
        startDir = os.getcwd()

    startDir = os.path.abspath(startDir)
    parts = startDir.split(sep)
    if parts[0] == '':
        del parts[0]

    while parts:
        curDir = sep + sep.join(parts)
        if os.path.isdir(curDir):
            lookFor = sep + sep.join(parts) + sep + searchFor
            if os.path.exists(lookFor):
                return lookFor
        del parts[-1]

    return ''

#-------------------------------------------------------------------------------
#
# Function: reprLen - "How many characters in repr(x)?"
#
# Usage:    numchr = reprLen(that)
#
# Where:    that   - object: Thing to repr()
#           numchr - int:    Number of characters
#

def reprLen(that):

    return len(repr(that))

#-------------------------------------------------------------------------------
#
# Function: findChars  - Look for many characters in a string
#
# Usage:    pos = findChars(string,chars,startPos)
#
# Where:    string   - str: String to search
#           chars    - str: Characters to search for
#           startPos - int: Starting position
#
#           pos      - int: Where found (-1 = "Not found")
#

def findChars(string,chars,startPos):

    for pos in range(startPos,len(string)):
        c = string[pos]
        if c in chars:
            return pos
    return -1

#------------------------------------------------------------------------------
#
# Function: flushout - Flush sys.stdout
#

def flushout():

    sys.stdout.flush()

#-------------------------------------------------------------------------------
#
# Function: isIpv4 - "Is this string a valid ipv4 address?"
#

def isIpv4(that):

    if type(that) != str:
        return False

    parts = that.split('.')
    if len(parts) != 4:
        return False

    for part in parts:
        if not isInt(part):
            return False
        p = int(part)
        if p < 0 or p > 255:
            return False

    return True

#--------------------------------------------------------------------------
#
# Function: parseIp - Parse IPv4/IPv6 address/port
#
# Usage:    host, ip, port, error = parseIp(that,ipChar='@')
#
# Where:    that   - str: IPv4/IPv6 address/port string
#           ipChar - str: Character separating <host> and <ip> (if any)
#           host   - str: Host name ('' = None)
#           ip     - str: IP Address ('' = None)
#           port   - str: Port number ('' = None)
#           error  - str: Error message ('' = No error)
#
# Note:     We don't currently handle IPv6 address ([...]:port) because
#           can't be arsed at the moment.
#
#           We handle the following formats of IP...
#
#             <host>[@[<ip>[:<port>]]]
#

def parseIp(that,ipChar='@'):

    if type(that) != str:
        return '','','','*** cmd_help.parseIp: Expected String ***'

    if '[' in that and ']' in that:
        return '','','','IPv6 addresses not implemented yet'

    error = "Invalid host %s"%repr(that)
    host  = ''
    ip    = ''
    port  = ''

    parts = that.split(ipChar)
    if not parts or len(parts) > 2:
        return '','','',error

    host = parts[0]
    del parts[0]
    if parts:
        ipAndPort = parts[0]
    else:
        ipAndPort = ''

    if ipAndPort:
        parts = ipAndPort.split(':')
        if len(parts) != 1 and len(parts) != 2:
            return '','','',error + " (too many ':')"
        ip = parts[0]
        if len(parts) == 2:
            port = parts[1]

    if ip:
        parts = ip.split('.')
        for part in parts:
            if not isInt(part):
                return '','','',"Invalid ip octet %s"%repr(part)
            octet = int(part)
            if octet < 0 or octet > 255:
                return '','','',"Invalid ip octet %s"%repr(part)

    if port:
        if not isInt(port):
            return '','','',"Invalid port %s"%repr(part)
        p = int(port)
        if p <= 0 or p > 65535:
            return '','','',"Invalid port %s"%repr(port)

    return host,ip,port,''

#--------------------------------------------------------------------------
#
# Function: splitName - Split a "...#...@..." object name into parts
#
# Usage:    result = splitName(that)
#
# Where:    result - List or None
#           that   - The object name
#

def splitName(objName):

    parts = objName.split('#')
    if len(parts) != 2:
        return (parts[0],'','')
    parts2 = parts[1].split('@')
    if len(parts2) != 2:
        return (parts[0],parts2[0],'')
    parts[1] = parts2[0]
    parts.append(parts2[1])

    return parts

#-------------------------------------------------------------------------------
#
# Function: formatHex - Format bytes into readable hex/ascii text
#
# Usage:    out = formatHex(bin)
#
# Where:    bin - bytes: Bytes to format
#           out - str:   Formatted hex/ascii text (with embeddded newlines)
#

def formatHex(bin,startOffset=0):

    if type(bin) != bytes:
        raise ValueError("formatHex: Expected bytes argument")

    digits = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']

    hex   = ''
    chars = ''
    for val in bin:
        nibs = [val>>4,val&0xf];
        for nib in nibs:
            hex += digits[nib]
        if val < 32 or val > 127:
            chars += '.'
        else:
            chars += chr(val)

    # Now, break both into 16 byte chunks...
    out = ''
    pos = 0
    chrOut = ''
    while pos < len(chars):
        if pos:
            out += '\n'
        out += "%08x"%(pos+startOffset) + ' '
        hexPart = hex[pos*2:pos*2+32]
        for i in range(0,len(hexPart)>>1):
            out += ' '+hexPart[i*2:i*2+2]
        if pos + 16 >= len(chars):
            out += '   '*(16-(len(chars)-pos))
        out += '  '+chars[pos:pos+16]
        pos += 16

    return out

#-------------------------------------------------------------------------------
#
# Functions: hexToBytes - Convert hexidecimal string into bytes
#            bytesToHex - Convert bytes to hexidecimal string
#

def hexToBytes(that):

    if len(that) % 2 != 0:
        raise ValueError("Expected 'that' to be an even number of bytes")
    if type(that) != str:
        raise ValueError("Expected 'that' to be a str")

    b = []
    byteVal = 0
    for i in range(0,len(that)):
        nib = that[i]
        if nib >= '0' and nib <= '9':
            nib = ord(nib) - ord('0')
        elif nib >= 'a' and nib <= 'f':
            nib = ord(nib) - ord('a') + 10
        elif nib >= 'A' and nib <= 'F':
            nib = ord(nib) - ord('A') + 10
        else:
            raise ValueError("Invalid hex digit %s"%repr(nib))
        byteVal = (byteVal*16) + nib
        if i and i%2 == 1:
            b.append(byteVal)
            byteVal = 0

    b = bytes(b)

    return b

def bytesToHex(that):

    s = ''
    if type(that) != bytes:
        raise ValueError("Expected 'that' to be bytes")
    for i in range(0,len(that)):
        byteVal = int(that[i])
        nib1 = int(byteVal/16)
        nib2 = int(byteVal)-(nib1*16)
        if nib1 <= 9:
            s += chr(nib1+ord('0'))
        else:
            s += chr(nib1-10+ord('a'))
        if nib2 <= 9:
            s += chr(nib2+ord('0'))
        else:
            s += chr(nib2-10+ord('a'))

    return s

#-------------------------------------------------------------------------------
#
# Function: sha256 - Calculate sha256 checksum of file
#           sha1   - Calculate sha1   checksum of file
#

def sha256(filename, block_size=65536):
    sha = hashlib.sha256()
    try:
        with open(filename, 'rb') as f:
            for block in iter(lambda: f.read(block_size), b''):
                sha.update(block)
        return sha.hexdigest()
    except:
        return None

def sha1(filename, block_size=65536):
    sha = hashlib.sha1()
    with open(filename,'rb') as f:
        for block in iter(lambda: f.read(block_size),b''):
            sha.update(block)
    return sha.hexdigest()

def sha1_bytes(byteVal):

    sha = hashlib.sha1()
    if type(byteVal) == str:
        byteVal = byteVal.encode('utf-8')
    elif type(byteVal) != bytes:
        raise ValueError("Expected 'bytes' to type bytes")
    sha.update(byteVal)
    return sha.hexdigest()

def sha256_bytes(byteVal):

    sha = hashlib.sha256()
    if type(byteVal) == str:
        byteVal = byteVal.decode('utf-8')
    elif type(byteVal) != bytes:
        raise ValueError("Expected 'bytes' to type bytes")
    sha.update(byteVal)
    return sha.hexdigest()

#-------------------------------------------------------------------------------
#
# Function: kallsyms - Return dictionary of function:address from /proc/kallsyms
#

def kallsyms():

    cmd = 'sudo cat /proc/kallsyms | grep " T "'
    rc,output,err = execute(cmd,showout=False)

    lines = output.split('\n')

    syms = {}

    wanted = [' T ',' B ',' b ']

    numbad = 0
    for line in lines:
        tag = line[16:19]
        if tag not in wanted:
            continue
        parts = line.split(tag)
        if len(parts) != 2:
            numbad += 1
            continue
        syms[parts[1]] = parts[0]

    if numbad:
        print("kallsyms: Skipped %d malformed lines"%numbad)

    return syms

#------------------------------------------------------------------------------
#
# Function: linuxVersion - Return Linux equivalent of "uname -r"
#
# Usage:    version = linuxVersion()
#
# Where:    version - str: ***see "uname -r"
#

_version = None

def linuxVersion():

    global _version

    if not _version:
        _version = execute('uname -r',showout=False)[1]

    return _version

#-------------------------------------------------------------------------------
#
# Functions: sha256_checksum - Compute sha256 checksum of file
#            sha1_checksum   - Compute sha1 checksum of file
#

def sha256_checksum(filename, block_size=65536):
    sha256 = hashlib.sha256()
    with open(filename, 'rb') as f:
        for block in iter(lambda: f.read(block_size), b''):
            sha256.update(block)
    return sha256.hexdigest()

def sha1_checksum(filename, block_size=655536):
    sha1 = hashlib.sha1()
    with open(filename,'rb') as f:
        for block in iter(lambda: f.read(block_size),b''):
            sha1.update(block)
    return sha1.hexdigest()

#-------------------------------------------------------------------------------
#
# Function: osName   - Return 'Ubuntu', 'CentOS', or None based on OS version
#           isUbuntu - "Are we running on Ubuntu?"
#           isCentOS - "Are we running on CentOS?"
#

def osName():

    import platform
    osname,version,name = platform.linux_distribution()
    if 'Ubuntu' in osname:
        return 'Ubuntu'
    elif 'CentOS' in osname:
        return 'CentOS'
    return None

def isUbuntu():

    return osName() == 'Ubuntu'

def isCentOS():

    return osName() == 'CentOS'

#-------------------------------------------------------------------------------
#
# Function: which - Search $PATH for executable
#
# Usage:    where = which(name)
#
# Where:    name  - str: Executable name
#
#           where - str: Where name is founc ('' if not found)
#
# Notes:    If found, we don't check it's executable or not so this may need to
#           change later. All for performance/lazy reasons.  ;-)
#

def which(name):

    if 'PATH' not in os.environ:
        return ''

    paths = os.environ['PATH'].split(':')
    for path in paths:
        fname = path+os.sep+name
        if os.path.exists(fname) and os.path.isfile(fname):
            return fname

    return ''

#-------------------------------------------------------------------------------
#
# Function: exceptionLines - Turn exception traceback into printable lines
#
# Usage:    lines = exceptionLines(exc,lines=[])
#
# Where:    ext   - Exception: Exception to format
#           lines - list:      Optional prefix lines (we append to this)
#
#           lines - list:      Line by line formatted traceback
#

def exceptionLines(exc,lines=[]):

    import traceback

    if type(lines) == str:
      lines = lines.split('\n')
    elif lines == None:
      lines = []

    if not '__traceback__' in dir(exc):
        lines = ['***exceptionLines(): Not called with Exception***']
    else:
        tb = exc.__traceback__
        bogusLines = list(traceback.format_exception(Exception,exc,tb))
        for line in bogusLines:
            if line and line[-1] == '\n':
                line = line[:-1]
            these = line.split('\n')
            for that in these:
                lines.append(that)
    return lines

#-------------------------------------------------------------------------------
#
# Function: sudo - Restart running process as sudo if not already
#                  (!!!Danger Danger!!!)
#
# Usage:    didit = sudo()
#
# Where:    didit - bool: True = "We weren't root", False = "We were already root"
#
# Notes:    Typically this is called from some python __main__ that knows it
#           needs to be running as root.
#
#           DO NOT USE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!!!
#

def sudo():

  # Switch to root if we're not already...
  import os
  if os.getuid():
    import subprocess
    import sys # ◄── No need to do this if already imported ;-)
    sys.argv.insert(0,sys.executable)
    sys.argv.insert(0,"sudo")
    execute(sys.argv,showout=True,prefix='')

    return True

  return False

#-------------------------------------------------------------------------------
#
# Function: ipAddresses   - Return iterable of host's IP addresses
#           ipv4Addresses - Return iterable of host's IPv4 addresses
#
# Usage:    addrs = ip[v4]Addresses()
#
# Where:    addrs - iterable: Known IP[v4] addresses (elements of type str)
#
# Note:     ipv4Addresses filters out '127.0.0.1' because we KNOW it's there
#           don't we?
#

def ipAddresses():

    try:
        import netifaces
    except:
        return ()

    ips = []

    for interface in netifaces.interfaces():

        ifInfo = netifaces.ifaddresses(interface)
        for info in ifInfo:
            elms = ifInfo[info]
            for elm in elms:
                if 'addr' in elm: ips.append(elm['addr'])

    return tuple(ips)

#-------------------------------------------------------------------------------
#
# Function: ipv4Addresses - Return iterable of host's IPv4 addresses
#
# Usage:    addrs = ipv4Addresses()
#
# Where:    addrs - iterable: Known IPv4 addresses (elements of type str)
#

def ipv4Addresses():

    addrs = ipAddresses()
    ipv4 = []
    for addr in addrs:
        parts = addr.split('.')
        if len(parts) != 4 or addr == '127.0.0.1':
            continue
        ipv4.append(addr)

    return tuple(ipv4)

#--------------------------------------------------------------------------------
#
# Function: packages - Return list of installed packages
#
# Usage:    packs = packages()
#
# Where:    packs - list: List of installed package names
#

def packages():

  packs = []

  if isUbuntu():

    # Ask apt...
    rc,out,err = execute("apt list --installed",showout=False)
    lines = out.split('\n')

    # For each line...
    for line in lines:

      if "Listing..." in line:
        continue

      pos = line.find("/")
      if pos < 0 or "Listing..." in line:
        continue

      pack = line[:pos]
      if pack not in packs:
        packs.append(pack)

  elif isCentOS():

    # Ask yum...
    rc,out,err = execute("yum list installed",showout=False)
    lines = out.split('\n')

    # For each line...
    for line in lines:

     if "Installed Packages" in line:
       continue

     parts = line.split(" ")
     if not parts:
       continue
     parts = parts[0].split('.')
     if not parts:
       continue

     pack = parts[0]
     if pack not in packs:
       packs.append(pack)

  packs.sort()

  return packs

#------------------------------------------------------------------------------
#
# Functions: red   - Print red error
#            error - Print red error and exit with status
#            green - Print green message
#            color - Use colr.color if stdout is a tty
#

def red(msg):

  print(color("ERROR: "+msg,fore='red',style='bright'))

def blue(msg):

  print(color(msg,fore='blue',style='bright'))

def green(msg):

  print(color(msg,fore='green',style='bright'))

def error(msg,exit=True):

  red(msg)
  if exit:
    sys.exit(1)

def color(*args,**kwargs):

  if not sys.stdout.isatty():
    return args[0]
  return colr.color(*args,**kwargs)

#-------------------------------------------------------------------------------
#
# __main__
#

if __name__ == "__main__":

    if 0:
        args = list(sys.argv[1:])
        print("args = %s"%repr(args))
        hosts = parseHosts(args)
        print("hosts = %s"%repr(hosts))

    if 0:
        Error("Dude!!!\nDude1",nl=False)
        Error("Dude2!!!")

    if 0:
        b = os.urandom(20)
        s = bytesToHex(b)
        print("b = %s"%repr(b))
        print("s = %s"%repr(s))

    if 0:
        s = 'abcdef0102030405'
        b = hexToBytes(s)
        print("s = %s"%repr(s))
        print("b = %s"%repr(b))

    if 0:
        print("%s IP addresses..."%repr(getHostName()))
        addresses = ipv4Addresses()
        for addr in addresses:
            print("  "+addr)
        if not addresses:
            print("  ***None***")

    if 0:

      rc,out,err = execute("ls -al",showout=True,prefix='')
      print("DEBUG: len(out) = %d"%len(out))

    if 0:

      s = "30"
      b = hexToBytes(s)
      print("b = %s"%repr(b))

    if 0:

      packs = packages()
      print("%d packages..."%len(packs))

      for i in range(0,len(packs)):
        if i == 10:
          print('...')
        elif i < 10 or i >= len(packs)-10:
          print("  "+packs[i])

    if 0:

      s = '0123456789112345678921234567893123456789412345678951234567896'
      pre  = preEllipse(s,50,fill='.')
      post = postEllipse(s,50,fill='.')
      print('preEllipse:  %s'%repr(pre))
      print('postEllipse: %s'%repr(post))

    if 1:

      s = dtToReadable(dtNow())
      print('Now: '+s)


'''
Schema for configuring a CloudFormation stack and its deployment targets.
'''

import schema

from . import aws, util

StackTarget = schema.Or(
    str,
    {
        'name': str,
        # overrides regions defined in the target
        'region': util.OneOrMany(str),
    },
)

StackParameter = schema.Schema({
  str: schema.And(
    util.OneOrMany(
      schema.Or(
        str,
        schema.And(bool, schema.Use(lambda x: 'true' if x else 'false')),
        schema.And(int, schema.Use(lambda x: str(x))),
      ),
    ),
    schema.Use(lambda x: ','.join(x)),
  ),
})

StackSchema = schema.Schema({
    'template': util.OneOrMany(str),
    schema.Optional('name'): str,
    schema.Optional('target'): util.OneOrMany(StackTarget),

    # overrides regions defined in the target
    schema.Optional('region'): util.OneOrMany(aws.Region),

    schema.Optional('capability', default=[]): util.OneOrMany(str),
    schema.Optional('parameter', default={}): StackParameter,
    schema.Optional('tag', default={}): {str: str},
}, name='Stack Description')


import numpy as np
import matplotlib.pyplot as plt

def get_action_index(list_txt='data/classInd.txt'):
    action_label = []
    with open(list_txt) as f:
        content = f.readlines()
        content = [x.strip('\r\n') for x in content]
    f.close()
    for line in content:
        label, action = line.split(' ')
        action_label.append(action)
    return action_label

def plot_matrix_test(list_txt, cfu_mat="../experiments/evaluation/ucf101/_confusion.npy", s_path="../experiments/evaluation/hmdb51/03-24-1659/confuse.png"):
    classes = get_action_index(list_txt)
    confuse_matrix = np.load(cfu_mat)
    plot_confuse_matrix(confuse_matrix, classes, s_path)
    plt.show()

def plot_confuse_matrix(matrix, classes,
                        s_path = "../experiments/evaluation/hmdb51/03-24-1659/confuse.png",
                        normalize=True,
                        title=None,
                        cmap=plt.cm.Blues
                        ):
    """
    :param matrix:
    :param classes:
    :param s_path:
    :param normalize:
    :param title:
    :param cmap:
    :return:
    """
    if not title:
        if normalize:
            title = 'Normalized confusion matrix'
        else:
            title = 'Confusion matrix, without normalization'

    # Compute confusion matrix
    cm = matrix
    # Only use the labels that appear in the data
    if normalize:
        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
        print("Normalized confusion matrix")
    else:
        print('Confusion matrix, without normalization')

    print(cm)

    fig, ax = plt.subplots()

    # We change the fontsize of minor ticks label
    ax.tick_params(axis='both', which='major', labelsize=3)
    ax.tick_params(axis='both', which='minor', labelsize=1)

    im = ax.imshow(cm, interpolation='nearest', cmap=cmap)
    ax.figure.colorbar(im, ax=ax)
    # We want to show all ticks...
    ax.set(xticks=np.arange(cm.shape[1]),
           yticks=np.arange(cm.shape[0]),
           # ... and label them with the respective list entries
           xticklabels=classes, yticklabels=classes,
           title=title,
           ylabel='True label',
           xlabel='Predicted label')

    # Rotate the tick labels and set their alignment.
    plt.setp(ax.get_xticklabels(), rotation=60, ha="right",
             rotation_mode="anchor")

    # Loop over data dimensions and create text annotations.
    fmt = '.2f'
    # thresh = cm.max() / 2.
    thresh = 0.2
    for i in range(cm.shape[0]):
        for j in range(cm.shape[1]):
            if cm[i, j] > thresh and i != j:
                ax.text(j, i, "({},{})".format(classes[i], classes[j]) + format(cm[i, j], fmt),
                        ha="center", va="center",fontsize='smaller',
                        color="black")
    fig.tight_layout()
    plt.savefig(s_path, dpi=1024)
    return ax

if __name__ == '__main__':
    # cf_name = "../experiments/evaluation/ucf101/04-12-1132/confusion.npy"
    # classList = "../datasets/lists/ucf101/classInd.txt"
    # save_path = "../experiments/evaluation/ucf101/04-12-1132/78.8_confuse.png"
    # plot_matrix_test(classList, cf_name, save_path)
    cf_name = "../experiments/evaluation/ucf101/04-14-1141/confusion.npy"
    classList = "../datasets/lists/ucf101/classInd.txt"
    save_path = "../experiments/evaluation/ucf101/04-14-1141/63.3_confuse.png"
    plot_matrix_test(classList, cf_name, save_path)


from itertools import combinations

import numpy as np
from pyparsing import Combine, Literal, Optional, Regex, Word, alphas, nums

from pgmpy.factors.discrete import DiscreteFactor, TabularCPD
from pgmpy.models import BayesianNetwork, MarkovNetwork
from pgmpy.utils import compat_fns

class UAIReader(object):
    """
    Initialize an instance of UAI reader class

    Parameters
    ----------
    path : file or str
        Path of the file containing UAI information.

    string : str
        String containing UAI information.

    Examples
    --------
    >>> from pgmpy.readwrite import UAIReader
    >>> reader = UAIReader('TestUai.uai')
    >>> model = reader.get_model()

    Reference
    ---------
    [1] https://uaicompetition.github.io/uci-2022/file-formats/model-format/
    [2] https://forgemia.inra.fr/thomas.schiex/toulbar2/-/blob/master/doc/UAI08Format.txt
    """

    def __init__(self, path=None, string=None):
        if path:
            with open(path, "r") as f:
                self.network = f.read()
        elif string:
            self.network = string
        else:
            raise ValueError("Must specify either path or string.")

        if "#" in self.network:
            self.network = (
                Regex("#.*").suppress().transformString(self.network)
            )  # removing comments from the file

        self.grammar = self.get_grammar()
        self.network_type = self.get_network_type()
        self.variables = self.get_variables()
        self.domain = self.get_domain()
        self.edges = self.get_edges()
        self.tables = self.get_tables()

    def get_grammar(self):
        """
        Returns the grammar of the UAI file.
        """
        network_name = Word(alphas).setResultsName("network_name")
        no_variables = Word(nums).setResultsName("no_variables")
        grammar = network_name + no_variables
        self.no_variables = int(grammar.parseString(self.network)["no_variables"])
        domain_variables = (Word(nums) * self.no_variables).setResultsName(
            "domain_variables"
        )
        grammar += domain_variables
        no_functions = Word(nums).setResultsName("no_functions")
        grammar += no_functions
        self.no_functions = int(grammar.parseString(self.network)["no_functions"])
        integer = Word(nums).setParseAction(lambda t: int(t[0]))
        for function in range(0, self.no_functions):
            scope_grammar = Word(nums).setResultsName("fun_scope_" + str(function))
            grammar += scope_grammar
            function_scope = grammar.parseString(self.network)[
                "fun_scope_" + str(function)
            ]
            function_grammar = ((integer) * int(function_scope)).setResultsName(
                "fun_" + str(function)
            )
            grammar += function_grammar

        floatnumber = Combine(
            Word(nums) + Optional(Literal(".") + Optional(Word(nums)))
        )
        for function in range(0, self.no_functions):
            no_values_grammar = Word(nums).setResultsName(
                "fun_no_values_" + str(function)
            )
            grammar += no_values_grammar
            no_values = grammar.parseString(self.network)[
                "fun_no_values_" + str(function)
            ]
            values_grammar = ((floatnumber) * int(no_values)).setResultsName(
                "fun_values_" + str(function)
            )
            grammar += values_grammar
        return grammar

    def get_network_type(self):
        """
        Returns the type of network defined by the file.

        Returns
        -------
        string : str
            String containing network type.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIReader
        >>> reader = UAIReader('TestUAI.uai')
        >>> reader.get_network_type()
        'MARKOV'
        """
        network_type = self.grammar.parseString(self.network)
        return network_type["network_name"]

    def get_variables(self):
        """
        Returns a list of variables.
        Each variable is represented by an index of list.
        For example if the no of variables are 4 then the list will be
        [var_0, var_1, var_2, var_3]

        Returns
        -------
        list: list of variables

        Examples
        --------
        >>> from pgmpy.readwrite import UAIReader
        >>> reader = UAIReader('TestUAI.uai')
        >>> reader.get_variables()
        ['var_0', 'var_1', 'var_2']
        """
        variables = []
        for var in range(0, self.no_variables):
            var_name = "var_" + str(var)
            variables.append(var_name)
        return variables

    def get_domain(self):
        """
        Returns the dictionary of variables with keys as variable name
        and values as domain of the variables.

        Returns
        -------
        dict: dictionary containing variables and their domains

        Examples
        --------
        >>> from pgmpy.readwrite import UAIReader
        >>> reader = UAIReader('TestUAI.uai')
        >>> reader.get_domain()
        {'var_0': '2', 'var_1': '2', 'var_2': '3'}
        """
        domain = {}
        var_domain = self.grammar.parseString(self.network)["domain_variables"]
        for var in range(0, len(var_domain)):
            domain["var_" + str(var)] = var_domain[var]
        return domain

    def get_edges(self):
        """
        Returns the edges of the network.

        Returns
        -------
        set: set containing the edges of the network

        Examples
        --------
        >>> from pgmpy.readwrite import UAIReader
        >>> reader = UAIReader('TestUAI.uai')
        >>> reader.get_edges()
        {('var_0', 'var_1'), ('var_0', 'var_2'), ('var_1', 'var_2')}
        """
        edges = []
        for function in range(0, self.no_functions):
            function_variables = self.grammar.parseString(self.network)[
                "fun_" + str(function)
            ]
            if isinstance(function_variables, int):
                function_variables = [function_variables]
            if self.network_type == "BAYES":
                child_var = "var_" + str(function_variables[-1])
                function_variables = function_variables[:-1]
                for var in function_variables:
                    edges.append(("var_" + str(var), child_var))
            elif self.network_type == "MARKOV":
                function_variables = ["var_" + str(var) for var in function_variables]
                edges.extend(list(combinations(function_variables, 2)))
        return set(edges)

    def get_tables(self):
        """
        Returns list of tuple of child variable and CPD in case of Bayesian
        and list of tuple of scope of variables and values in case of Markov.

        Returns
        -------
        list : list of tuples of child variable and values in Bayesian
            list of tuples of scope of variables and values in case of Markov.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIReader
        >>> reader = UAIReader('TestUAI.uai')
        >>> reader.get_tables()
        [(['var_0', 'var_1'], ['4.000', '2.400', '1.000', '0.000']),
         (['var_0', 'var_1', 'var_2'],
          ['2.2500', '3.2500', '3.7500', '0.0000', '0.0000', '10.0000',
           '1.8750', '4.0000', '3.3330', '2.0000', '2.0000', '3.4000'])]
        """
        tables = []
        for function in range(0, self.no_functions):
            function_variables = self.grammar.parseString(self.network)[
                "fun_" + str(function)
            ]
            if isinstance(function_variables, int):
                function_variables = [function_variables]
            if self.network_type == "BAYES":
                child_var = "var_" + str(function_variables[-1])
                values = self.grammar.parseString(self.network)[
                    "fun_values_" + str(function)
                ]
                tables.append((child_var, list(values)))
            elif self.network_type == "MARKOV":
                function_variables = ["var_" + str(var) for var in function_variables]
                values = self.grammar.parseString(self.network)[
                    "fun_values_" + str(function)
                ]
                tables.append((function_variables, list(values)))
        return tables

    def get_model(self):
        """
        Returns an instance of Bayesian Model or Markov Model.
        Variables are in the pattern var_0, var_1, var_2 where var_0 is
        0th index variable, var_1 is 1st index variable.

        Return
        ------
        model: an instance of Bayesian or Markov Model.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIReader
        >>> reader = UAIReader('TestUAI.uai')
        >>> reader.get_model()
        """
        if self.network_type == "BAYES":
            model = BayesianNetwork()
            model.add_nodes_from(self.variables)
            model.add_edges_from(self.edges)

            tabular_cpds = []
            for child_var, values in self.tables:
                states = int(self.domain[child_var])
                values = np.fromiter(values, dtype=float)
                values = values.reshape(states, values.size // states)
                parents = list(model.predecessors(child_var))
                if len(parents) == 0:
                    tabular_cpds.append(TabularCPD(child_var, states, values))
                else:
                    tabular_cpds.append(
                        TabularCPD(
                            child_var,
                            states,
                            values,
                            evidence=parents,
                            evidence_card=[int(self.domain[var]) for var in parents],
                        )
                    )

            model.add_cpds(*tabular_cpds)
            return model

        elif self.network_type == "MARKOV":
            model = MarkovNetwork(self.edges)

            factors = []
            for table in self.tables:
                variables = table[0]
                cardinality = [int(self.domain[var]) for var in variables]
                value = list(map(float, table[1]))
                factor = DiscreteFactor(
                    variables=variables, cardinality=cardinality, values=value
                )
                factors.append(factor)

            model.add_factors(*factors)
            return model

class UAIWriter(object):
    """
    Initialize an instance of UAI writer class

    Parameters
    ----------
    model: A Bayesian or Markov model
        The model to write

    round_values: int (default: None)
        The number to decimals to which to round the probability values. If None, keeps all decimals points.

    Examples
    --------
    >>> from pgmpy.readwrite import UAIWriter
    >>> from pgmpy.utils import get_example_model
    >>> model = get_example_model('asia')
    >>> writer = UAIWriter(asia)
    >>> writer.write_uai('asia.uai')
    """

    def __init__(self, model, round_values=None):
        if isinstance(model, BayesianNetwork):
            self.network = "BAYES\n"
        elif isinstance(model, MarkovNetwork):
            self.network = "MARKOV\n"
        else:
            raise TypeError("Model must be an instance of Bayesian or Markov model.")

        self.model = model
        self.round_values = round_values
        self.no_nodes = self.get_nodes()
        self.domain = self.get_domain()
        self.functions = self.get_functions()
        self.tables = self.get_tables()

    def __str__(self):
        """
        Returns the UAI file as a string.
        """
        self.network += self.no_nodes + "\n"
        domain = sorted(self.domain.items(), key=lambda x: (x[1], x[0]))
        self.network += " ".join([var[1] for var in domain]) + "\n"
        self.network += str(len(self.functions)) + "\n"
        for fun in self.functions:
            self.network += str(len(fun)) + " "
            self.network += " ".join(fun) + "\n"
        self.network += "\n"
        for table in self.tables:
            self.network += str(len(table)) + "\n"
            self.network += " ".join(table) + "\n"
        return self.network[:-1]

    def get_nodes(self):
        """
        Adds variables to the network.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIWriter
        >>> writer = UAIWriter(model)
        >>> writer.get_nodes()
        """
        no_nodes = len(self.model.nodes())
        return str(no_nodes)

    def get_domain(self):
        """
        Adds domain of each variable to the network.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIWriter
        >>> writer = UAIWriter(model)
        >>> writer.get_domain()
        """
        if isinstance(self.model, BayesianNetwork):
            cpds = self.model.get_cpds()
            cpds.sort(key=lambda x: x.variable)
            domain = {}
            for cpd in cpds:
                domain[cpd.variable] = str(cpd.variable_card)
            return domain
        elif isinstance(self.model, MarkovNetwork):
            factors = self.model.get_factors()
            domain = {}
            for factor in factors:
                variables = factor.variables
                for var in variables:
                    if var not in domain:
                        domain[var] = str(factor.get_cardinality([var])[var])
            return domain
        else:
            raise TypeError("Model must be an instance of Markov or Bayesian model.")

    def get_functions(self):
        """
        Adds functions to the network.

        Examples
        -------_
        >>> from pgmpy.readwrite import UAIWriter
        >>> writer = UAIWriter(model)
        >>> writer.get_functions()
        """
        if isinstance(self.model, BayesianNetwork):
            cpds = self.model.get_cpds()
            cpds.sort(key=lambda x: x.variable)
            variables = sorted(self.domain.items(), key=lambda x: (x[1], x[0]))
            functions = []
            for cpd in cpds:
                child_var = cpd.variable
                evidence = cpd.variables[:0:-1]
                function = [
                    str(variables.index((var, self.domain[var]))) for var in evidence
                ]
                function.append(
                    str(variables.index((child_var, self.domain[child_var])))
                )
                functions.append(function)
            return functions
        elif isinstance(self.model, MarkovNetwork):
            factors = self.model.get_factors()
            functions = []
            variables = sorted(self.domain.items(), key=lambda x: (x[1], x[0]))
            for factor in factors:
                scope = factor.scope()
                function = [
                    str(variables.index((var, self.domain[var]))) for var in scope
                ]
                functions.append(function)
            return functions
        else:
            raise TypeError("Model must be an instance of Markov or Bayesian model.")

    def get_tables(self):
        """
        Adds tables to the network.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIWriter
        >>> writer = UAIWriter(model)
        >>> writer.get_tables()
        """
        if isinstance(self.model, BayesianNetwork):
            cpds = self.model.get_cpds()
            cpds.sort(key=lambda x: x.variable)
            tables = []
            for cpd in cpds:
                values = list(
                    map(
                        str,
                        compat_fns.to_numpy(
                            cpd.values.ravel(), decimals=self.round_values
                        ),
                    )
                )
                tables.append(values)
            return tables
        elif isinstance(self.model, MarkovNetwork):
            factors = self.model.get_factors()
            tables = []
            for factor in factors:
                values = list(
                    map(
                        str,
                        compat_fns.to_numpy(
                            factor.values.ravel(), decimals=self.round_values
                        ),
                    )
                )
                tables.append(values)
            return tables
        else:
            raise TypeError("Model must be an instance of Markov or Bayesian model.")

    def write_uai(self, filename):
        """
        Write the xml data into the file.

        Parameters
        ----------
        filename: Name of the file.

        Examples
        --------
        >>> from pgmpy.readwrite import UAIWriter
        >>> from pgmpy.utils import get_example_model
        >>> model = get_example_model('asia')
        >>> writer = UAIWriter(asia)
        >>> writer.write_uai('asia.uai')
        """
        writer = self.__str__()
        with open(filename, "w") as fout:
            fout.write(writer)


# import itertools
import pdb
import time
import urllib
from collections import Counter
from typing import Union

import networkx as nx
import numpy as np
import obonet
import pandas as pd
from Bio import SeqIO
from kedro.extras.datasets.biosequence import BioSequenceDataSet
from kedro.extras.datasets.json import JSONDataSet
from kedro.extras.datasets.networkx import JSONDataSet as nxJSONDataSet
from kedro.io import PartitionedDataSet
from SPARQLWrapper import JSON, SPARQLWrapper

pd.set_option("display.max_rows", 500)
pd.set_option("display.max_columns", 500)

class ReturnDict(dict):
    def __missing__(self, key):
        return key

def _parse_annotation_id(x):
    """Removing identifier for each identification.
    ex: UniProtKB:P25762; parsing
    """
    return x.str.split(":").str[1]

def _parse_protein_length(prot_sequences, unique_gids):
    prot_sequences_dict = SeqIO.to_dict(prot_sequences)
    pdb.set_trace()
    return {gid: len(prot_sequences_dict[gid]) for gid in unique_gids}

def _merge_rows(df):
    df = df.groupby("gid").agg(set)
    res = [[",".join(map(str, x)) for x in df[col]] for col in df.columns]
    fdf = pd.DataFrame(res).T.set_index(df.index)
    fdf.columns = df.columns
    print("End")
    return fdf

def _merge_duplicate_info(df):
    """This function merges duplicate information in a dataframe and then merges based on
    a comma. It had some hardcoded column for index and groupby
    """
    unique_df = df[~df.gid.duplicated(keep=False)]
    unique_df = unique_df.set_index("gid")
    duplicated_df = df[df.gid.duplicated(keep=False)]
    tmp = duplicated_df.groupby("gid").agg(set)
    res = [[",".join(map(str, x)) for x in tmp[col]] for col in tmp.columns]
    joined_duplicates = pd.DataFrame(res).T.set_index(tmp.index)
    joined_duplicates.columns = tmp.columns
    final = pd.concat([unique_df, joined_duplicates], axis=0)
    return final

def _creating_edges_list_multilayer(annot, elist, edge, prokka_gff):
    """creating edges for multilayer networks using the list method
    [node1 layer1 node2 layer2 weight]

    What if a node comes from multiple bins. I need to check
    if both nodes have / then I am not accounting the types for both of them!
    """
    tmp = []
    edge_length = []
    edge_scaf = set()
    for node in edge:
        length, level, scaf_level = prokka_gff.loc[node][["length", "bin", "scaffold"]]
        # tmp.append(f"{node}|{level}")
        if "," in length:
            length = np.mean(np.array(length.split(",")).astype(np.int32))
        else:
            length = int(length)
        tmp.append(node)
        tmp.append(level)
        edge_length.append(length)
        edge_scaf.add(scaf_level)
    # if "low" in tmp[0] or "low" in tmp[1]:
    #    tmp.append(1)
    # else:
    #    tmp.append(-1)
    tmp.append(np.mean(edge_length))
    if len(edge_scaf) == 1:
        tmp.append(edge_scaf.pop())
    else:
        tmp.append("")
    tmp.append(annot)
    elist.append(tmp[:])
    return elist

def _merge_duplicate_seqrecords(seqrecords):
    """Removing protein sequences that are have the description and annotation of hypothetical protein
    If I merge two sequences that are the same, I need to merge the ids as well
    But if I merge those ids what will I do with the Nosema annotation for the gene?
    """
    unique_records = {}
    for rec in seqrecords:
        if "hypothetical protein" in rec.description:
            continue
        if rec.seq in unique_records:
            merged_rec = unique_records[rec.seq].id + "/" + rec.id
            unique_records[rec.seq].id = merged_rec
        else:
            unique_records[rec.seq] = rec
    return unique_records.values()

def _find_merged_ids(records):
    list_merged_id = [x.id for x in records if "/" in x.id]
    dict_merged_ids = {}
    for merged_id in list_merged_id:
        for original_id in merged_id.split("/"):
            dict_merged_ids[original_id] = merged_id
    return dict_merged_ids

def preprocess_prokka_sequences(
    partition_prokka_faa: PartitionedDataSet,
) -> Union[BioSequenceDataSet, JSONDataSet]:
    """Merging duplicate protein sequence from various partitions of prokka.

    partition_prokka_faa: A list of biopython sequence records from various partitions

    Returns:
    1. Biosequence dataset which loads fasta file
    2. JSON file of merged ids
    """
    start = time.time()
    combine_all = []
    for partition_load_func in partition_prokka_faa.values():
        partition_data = partition_load_func()
        combine_all.extend(partition_data)
    print(f"For loop {time.time() - start}")
    unique_records = _merge_duplicate_seqrecords(combine_all)
    merged_ids = _find_merged_ids(unique_records)
    return list(unique_records), merged_ids

def prokka_bins_gff(
    partition_prokka_gff: PartitionedDataSet,
) -> Union[pd.DataFrame, pd.DataFrame, JSONDataSet]:
    """Parsing prokka annotation information from the multiple runs
    I don't need to keep most of the information like gene or annotation info
    """
    list_gff_df = []
    for partition_load_func in partition_prokka_gff.values():
        list_gff_df.append(partition_load_func())
    concat_gff = pd.concat(list_gff_df, ignore_index=True)
    # concat_gff["scaf_level"] = concat_gff["scaffold"] + ":" + concat_gff["level"]
    prokka_bins = dict(concat_gff[["prokka_unique", "bin"]].values)
    prokka_gff = concat_gff.drop(["prokka_unique"], axis=1)

    uni_prokka_gff = prokka_gff[prokka_gff.annot.str.contains("UniProtKB")]
    rest_prokka_gff = prokka_gff[~prokka_gff.annot.str.contains("UniProtKB")]
    return uni_prokka_gff, rest_prokka_gff, prokka_bins

def preprocess_prokka_annotations(
    prot_sequences: BioSequenceDataSet,
    merged_gene_ids: JSONDataSet,
    annotations: pd.DataFrame,
) -> pd.DataFrame:
    """Preprocesses data for prokka annotations pulled from the gff files

    prot_sequences: A BioSequence dataset containing protein sequences
    merged_gene_ids: A JSON file containing a mapping of duplicated gene ids to a merged id
    annotations: A pandas dataframe containing information pulled from prokka's gff files

    Returns:
    A pandas dataframe containing processed prokka annotations, including gene id,
    """
    merged_gene_ids = ReturnDict(merged_gene_ids)
    annotations["gid"] = annotations.gid.map(merged_gene_ids)  # Duplicates
    # Why do a lot of the merged ids not show up in annnotations
    # pdb.set_trace()
    # all_bins = set(annotations.bin)
    # fixed_annotations = _merge_duplicate_info(annotations)
    annotations["length"] = annotations.gid.map(
        _parse_protein_length(prot_sequences, set(annotations.gid))
    )
    annotations = annotations[annotations.length > 100].copy()
    annotations["annot"] = _parse_annotation_id(annotations["annot"])
    # fixed_annotations["test"] = range(len(fixed_annotations))
    # fixed_annotations["test"] = fixed_annotations.test.astype(str)
    return annotations

def _sparql_uniprot_query(sparql, prot_list: list[str]):
    # Filter out the uniprot keywords
    prot_list = '("' + '") ("'.join(prot_list) + '")'
    sparql.setQuery(
        f"""
    PREFIX up: <http://purl.uniprot.org/core/>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT DISTINCT
        ?ac
        ?goterm_id
    WHERE \u007b
        VALUES (?ac) \u007b{prot_list}\u007d
        BIND (IRI(CONCAT("http://purl.uniprot.org/uniprot/",?ac)) AS ?protein)
        ?protein a up:Protein .
        ?protein up:classifiedWith ?goTerm .
        BIND (REPLACE(STR(?goTerm), "^.*/([^/]*)$","$1") as ?goterm_id)
        OPTIONAL \u007b
        ?goTerm rdfs:subClassOf <http://purl.obolibrary.org/obo/GO_0003674> .
        ?goTerm rdfs:label ?moltype .
        \u007d
        FILTER(?moltype)

    \u007d ORDER BY DESC(?ac)
    """
    )
    res = []
    for i in range(4):
        try:
            res = sparql.query().convert()
            if res != []:
                break
        except urllib.error.HTTPError as exec:
            print(f"Sleeping for 30 seconds try {i+1}")
            print(f"HTTPError {exec}")
            time.sleep(30)
            continue
            # res = sparql.query().convert()
    return res

def _chunks(lst, n):
    for i in range(0, len(lst), n):
        yield lst[i : i + n]

def go_annotations(prokka_gff: pd.DataFrame) -> pd.DataFrame:
    """I need to add asyncio here so that I can send all the requests all at once instead of waiting for the
    computation to be done and then requesting again
    This will improve the speed of performance
    """
    sparql = SPARQLWrapper("https://sparql.uniprot.org/sparql/")
    sparql.setReturnFormat(JSON)
    uni_annots = set(prokka_gff.annot)
    xres = []
    for annot_bin in _chunks(list(uni_annots), 150):
        res = _sparql_uniprot_query(sparql, annot_bin)
        if res == []:
            print("Broken, skipping this bin")
            print(annot_bin)
            continue
        xx = res["head"]["vars"]
        rbinds = res["results"]["bindings"]
        xres.extend([[s["value"] for s in x.values()] for x in rbinds])
    df = pd.DataFrame(xres, columns=xx)
    tmp = df.groupby("ac").agg(set)
    res = [",".join(map(str, x)) for x in tmp["goterm_id"]]
    df2 = pd.DataFrame(res, index=tmp.index, columns=tmp.columns)
    dict_df = df2.goterm_id.to_dict()
    prokka_gff["go"] = prokka_gff.annot.map(dict_df)
    return prokka_gff

def prokka_edges(
    prokka_gff: pd.DataFrame, nosema_conc: pd.DataFrame, prot_sequences: BioSequenceDataSet
) -> Union[pd.DataFrame, pd.DataFrame]:
    """I need to add the nosema intensity here. I need to implement multiprocessing here. It is taking wayyyyy too long
    I need to also take into account what's being not included in prokka_gff
    If two proteins are in the same bin with the same annotation then they are the same!
    """
    prokka_gff = prokka_gff.fillna("")
    prokka_gff["length"] = prokka_gff.length.astype(str)
    # prokka_edges = []
    # prokka_gff["bin"] = ["b" + str(b) for b in prokka_gff.bin]
    # prokka_gff["gid"] = ["g" + str(g) for g in prokka_gff.gid]
    # annot_groups = prokka_gff.groupby("annot")["bin","gid"].agg(list)
    # Merging genes whose annotation and bin are the same, thus they are effectively the same

    prokka_gff2 = _merge_rows(prokka_gff)
    pdb.set_trace()
    pg = prokka_gff.reset_index()
    res = pg.groupby(["annot", "bin"]).gid.agg(list)
    g1 = res[res.map(len) > 1]
    print(g1.head())
    return prokka_gff, g1

def _flat_func(x):
    return [item for sublist in x for item in sublist]

def go_ontology(go_prokka: pd.DataFrame) -> nxJSONDataSet:
    # Read the taxrank ontology
    url = "http://purl.obolibrary.org/obo/go/go-basic.obo"
    graph = obonet.read_obo(url)
    mapping = dict(
        zip(graph.nodes.keys(), [x.replace(":", "_") for x in graph.nodes.keys()])
    )
    graph = nx.relabel_nodes(graph, mapping)
    id_to_name = {id_: data["name"] for id_, data in graph.nodes(data=True)}
    # nx.descendants(graph, "GO:0000036")

    # Analysis of go_prokka
    goh_prokka = go_prokka[~go_prokka.go.isnull()].copy()
    goh_prokka["go"] = goh_prokka.go.str.split(",")
    res = goh_prokka.groupby("annot").go.agg(list)
    # dictionary of uniprot mapped to go
    flat_res = {key: set(_flat_func(val)) for key, val in res.items()}
    df = (
        pd.DataFrame.from_dict(flat_res, orient="index")
        .rename_axis("uniprots")
        .reset_index()
    )
    go_df = df.melt(id_vars=["uniprots"], value_name="go_term").dropna()
    # go mapped to uniprot
    go_annots = go_df.groupby("go_term").uniprots.agg(set)
    total_annots = [x for x in go_annots.values]
    shared_go = Counter(_flat_func(total_annots))
    go_shared = {}
    for key, val in shared_go.items():
        go_shared[val] = go_shared.get(val, []) + [key]
    shared_go_counts = Counter(list(shared_go.values()))
    count_lengths = Counter([len(x) for x in flat_res.values()])
    go_annots = go_annots.reset_index()
    go_annots["name"] = go_annots.go_term.map(id_to_name)
    go_annots.uniprots = go_annots.uniprots.map(list)
    sources = [
        x for x in graph.nodes() if graph.out_degree(x) == 1 and graph.in_degree(x) == 0
    ]
    sour = set(go_annots.go_term) & set(sources)
    print(shared_go_counts)
    print(count_lengths)
    print(sour)
    xx = go_annots.go_term.to_list()
    sub = graph.subgraph(xx)
    return (
        dict(zip(go_df.uniprots, go_df.go_term)),
        dict(zip(go_annots.go_term, go_annots.uniprots)),
        sub,
    )


#import json to manipualte the json file format the course data was provided in
import json
from cs50 import SQL

db = SQL("sqlite:///interests.db")

# Open the courses2.json file using json.load to convert the data into a python dictionary
# Used https://stackabuse.com/reading-and-writing-json-to-a-file-in-python/ for reference #
with open('courses2.json', 'r', encoding='utf-8') as data_file:
    data = json.load(data_file)

    # Iterate through each row in the data and write the subject and course number in a SQL database
    for p in range(len(data)):
        db.execute("INSERT INTO realclasses (subject, number) VALUES(:subject,:number)", subject=data[p]["classes"][0]["catalogSubject"], number=data[p]["classes"][0]["courseNumber"])


from llvmlite import ir

'''
Este módulo contém um exemplo com o laço while, printf e operações aritméticas.
Será gerado um código em LLVM como este em C:

int main() {
    int a = 10;

    while(a > 0) {
        printf("%d\n", a);
        a = a - 1;
    }

    return 0;
}

'''

def declara_e_atribui(name: str, _type: ir.Type, value) -> ir.AllocaInstr:
    global bloco

    temp = bloco.alloca(_type, name=name)

    # Define o alinhamento de 4 bytes
    temp.align = 4

    # Cria uma constante pra armazenar o valor passado
    constant = ir.Constant(_type, value)

    # Armazena a constante na variável criada
    bloco.store(constant, temp)

    return temp

def decrementa_var(var, value: int):
    global bloco

    # Carrega valor da variável
    _var = bloco.load(var)

    # Cria constante de decremento
    constant = ir.Constant(ir.IntType(32), value)

    # Efetua subtração
    result = bloco.sub(_var, constant)

    # Salva o resultado da subtração na variável
    bloco.store(result, var)

if __name__ == "__main__":
    # Cria o módulo.
    modulo = ir.Module('meu_modulo.bc')

    # Declaração das funções que serão 'adicionadas' em tempo de vinculação.
    _escrevaI = ir.FunctionType(ir.VoidType(), [ir.IntType(32)])
    escrevaI = ir.Function(modulo, _escrevaI, "escrevaInteiro")

    # Declaração da função 'principal', que DEVE ser usada com o nome 'main'.
    main_type = ir.FunctionType(ir.IntType(32), [])
    main = ir.Function(modulo, main_type, "main")

    # Cria blocos de entrada e saída
    bloco_entrada = main.append_basic_block("bloco_entrada")
    bloco_saida = main.append_basic_block("bloco_saida")

    # Entra no bloco de entrada
    bloco = ir.IRBuilder(bloco_entrada)
    a = declara_e_atribui("a", ir.IntType(32), 10)

    # Cria blocos para validar a condição e de execução, caso seja válida a expressão
    bloco_valida_while = main.append_basic_block("valida_while")
    bloco_exec_while = main.append_basic_block("executa_while")

    # Salta do bloco principal para o bloco de validação da expressão
    bloco.branch(bloco_valida_while)

    # Entra no bloco de validação
    bloco = ir.IRBuilder(bloco_valida_while)

    # Faz a comparação
    comparacao = bloco.icmp_signed(">", bloco.load(a),
                                   ir.Constant(ir.IntType(32), 0))

    # Salto condicional, se verdade salta para o bloco de execução, salta para o bloco de saída
    bloco.cbranch(comparacao, bloco_exec_while, bloco_saida)

    # Entra no bloco de execução
    bloco = ir.IRBuilder(bloco_exec_while)

    # Escreve o valor de 'a'
    bloco.call(escrevaI, args=[bloco.load(a)])

    # Decrementa o valor de 'a'
    decrementa_var(a, 1)

    # Salta para o bloco de validação (loop)
    bloco.branch(bloco_valida_while)

    # Entra no bloco de saída
    bloco = ir.IRBuilder(bloco_saida)

    # Cria constante de retorno
    retorno = ir.Constant(ir.IntType(32), 0)

    # Efetua o retorno
    bloco.ret(retorno)

    with open('meu_modulo.ll', 'w') as arquivo:
        arquivo.write(str(modulo))


import statistics
from math import log2

class Attribute:
    def __init__(self, name, values, is_label, is_numeric, median):
        self.attribute_name = name
        self.in_attributes = True
        self.values = values
        self.is_label = is_label
        self.is_numeric = is_numeric
        self.median = median

    def __str__(self):
        return 'name:' + Attribute.printable(self.attribute_name)

    @staticmethod
    def printable(thing):
        return thing + '\n'

class Node:

    count = 0

    def __init__(self, value, is_leaf):
        self.value = value
        self.children = {}
        self.is_leaf = is_leaf

    @staticmethod
    def check_error(tree, examples, attributes):
        correct_count = 0
        for example in examples:
            label_from_tree = tree.travel_tree(example, attributes)
            if label_from_tree == example[-1]:
                correct_count += 1
        return correct_count/len(examples) * 100

    def travel_tree(self, example, attributes):
        if self.is_leaf:
            return self.value
        attr_split = self.value
        index, values = Node.find_attribute_values(attr_split, attributes)
        value = example[index]
        if attributes[index].is_numeric:
            if value <= attributes[index].median:
                return self.children['le'].travel_tree(example, attributes)
            else:
                return self.children['g'].travel_tree(example, attributes)
        else:
            return self.children[value].travel_tree(example, attributes)

    @staticmethod
    def same_label(examples):  # check if all labels are the same
        first_label = examples[0][-1]
        for example in examples[1:]:
            if example[-1] != first_label:
                return False, ''
        return True, first_label

    @staticmethod
    def get_majority_label(examples, attributes):
        label = attributes[-1]
        label_values = {}
        for value in label.values:
            label_values[value] = 0
        majority_label = label.values[0]
        majority_label_count = 0
        for example in examples:
            label = example[-1]
            label_values[label] += 1
            if label_values[label] > majority_label_count:
                majority_label_count = label_values[label]
                majority_label = label
        return majority_label

    @staticmethod
    def decision_stump(examples, attributes, info_gain):
        return Node.id3(examples, attributes, info_gain, 0, 1)

    @staticmethod
    def id3(examples, attributes, info_gain, current_depth, max_depth):
        if max_depth == current_depth or Node.attributes_empty(attributes):
            value = Node.get_majority_label(examples, attributes)
            return Node(value, True)
        is_same_label, value = Node.same_label(examples)
        if is_same_label:
            return Node(value, True)
        else:
            attribute_split = info_gain(examples, attributes)
            new_root = Node(attribute_split, False)
            attribute_index, attribute_values = Node.find_attribute_values(attribute_split, attributes)
            for value in attribute_values:
                subset = Node.find_subset_examples(value, attribute_index, examples, attributes)
                if len(subset) == 0:
                    new_value = Node.get_majority_label(examples, attributes)
                    new_root.children[value] = Node(new_value, True)
                else:
                    attributes[attribute_index].in_attributes = False
                    new_root.children[value] = Node.id3(subset, attributes, info_gain, current_depth+1, max_depth)
                    attributes[attribute_index].in_attributes = True
            return new_root

    @staticmethod
    def find_label(attributes):
        for attribute in attributes:
            if attribute.is_label:
                return attribute

    @staticmethod
    def find_attribute_values(attribute_name, attributes):
        Node.count += 1
        # print(Node.count)
        for i, attribute in enumerate(attributes):
            if attribute.attribute_name == attribute_name and attribute.in_attributes:
                return i, attribute.values

    @staticmethod
    def find_subset_examples(attribute_value, index, examples, attributes):
        new_subset = []
        for example in examples:
            if attributes[index].is_numeric:
                if attribute_value == 'le' and example[index] <= attributes[index].median:
                    new_subset.append(example)
                elif attribute_value == 'g' and example[index] > attributes[index].median:
                    new_subset.append(example)
            elif example[index] == attribute_value:
                new_subset.append(example)
        return new_subset

    @staticmethod
    def reclaim_attributes(attributes):
        for attribute in attributes:
            attribute.in_attributes = True

    @staticmethod
    def attributes_empty(attributes):
        for attribute in attributes:
            if attribute.in_attributes and not attribute.is_label:
                return False
        return True

    @staticmethod
    def open_data(filename):
        examples = []  # represents our set of examples
        with open(filename) as data:
            for example in data:
                example = example.replace('\n', '').split(
                    ',')  # replace each end line with an empty string and then split
                examples.append(example)  # add to set of examples
        return examples

    @staticmethod
    def create_attributes(names, vals_to_create, attr_training_examples, attr_test_examples):
        attributes_to_create = []
        for create_index, name in enumerate(names):
            if vals_to_create[create_index] == 'numeric':
                attribute_vals = []
                for example in attr_training_examples:
                    example[create_index] = int(example[create_index])
                    attribute_vals.append(example[create_index])
                median = statistics.median(attribute_vals)
                for example in attr_test_examples:
                    example[create_index] = int(example[create_index])
                new_attribute = Attribute(name, ['le', 'g'], False, True, median)
                attributes_to_create.append(new_attribute)
            else:
                new_attribute = Attribute(name, vals_to_create[create_index], False, False, -1)
                attributes_to_create.append(new_attribute)
        attributes_to_create[-1].is_label = True
        return attributes_to_create

    @staticmethod
    def replace_unknowns(unk_training_examples, unk_test_examples, unk_attributes):
        for unknown_index, attribute in enumerate(unk_attributes):
            if 'unknown' in attribute.values:
                values_count = dict.fromkeys(attribute.values, 0)
                del values_count['unknown']
                max_count = 0
                max_label = unk_training_examples[0][unknown_index]
                for example in unk_training_examples:
                    if example[unknown_index] != 'unknown':
                        values_count[example[unknown_index]] += 1
                        if values_count[example[unknown_index]] > max_count:
                            max_label = example[unknown_index]
                for example in unk_training_examples:
                    if example[unknown_index] == 'unknown':
                        example[unknown_index] = max_label
                for example in unk_test_examples:
                    if example[unknown_index] == 'unknown':
                        example[unknown_index] = max_label

    @staticmethod
    def info_gain(examples, ig_attributes):
        label = Node.find_label(ig_attributes)  # label attribute object
        numb_examples = len(examples)  # number of examples
        entropy_s = 0
        for value in label.values:
            label_count = 0
            for example in examples:
                if example[-1] == value:
                    label_count += 1
            p = label_count / numb_examples
            if p != 0:
                entropy_s += (-p * log2(p))
        greatest_gain = 0
        greatest_gain_name = ig_attributes[0].attribute_name
        for ig_i, attribute in enumerate(ig_attributes):
            if not attribute.is_label and attribute.in_attributes:
                expected_entropy = 0
                if attribute.is_numeric:
                    values_numeric = {'g': 0, 'le': 0}
                    label_values_numeric = dict.fromkeys(label.values, 0)
                    for example in examples:
                        if example[ig_i] <= attribute.median:
                            values_numeric['le'] += 1
                            label_values_numeric[example[-1]] += 1
                    entropy_value_numeric = 0
                    if values_numeric['le'] != 0:
                        for label_value in label_values_numeric:
                            p = label_values_numeric[label_value] / values_numeric['le']
                            if p != 0:
                                entropy_value_numeric += (-p * log2(p))
                        expected_entropy += ((values_numeric['le'] / len(examples)) * entropy_value_numeric)
                    label_values_numeric = dict.fromkeys(label.values, 0)
                    for example in examples:
                        if example[ig_i] > attribute.median:
                            values_numeric['g'] += 1
                            label_values_numeric[example[-1]] += 1
                    entropy_value_numeric = 0
                    if values_numeric['g'] != 0:
                        for label_value in label_values_numeric:
                            p = label_values_numeric[label_value] / values_numeric['g']
                            if p != 0:
                                entropy_value_numeric += (-p * log2(p))
                        expected_entropy += ((values_numeric['g'] / len(examples)) * entropy_value_numeric)
                else:
                    for value in attribute.values:
                        label_values = dict.fromkeys(label.values, 0)
                        value_count = 0
                        for example in examples:
                            if example[ig_i] == value:
                                value_count += 1
                                label_values[example[-1]] += 1
                        proportion_value = value_count / numb_examples
                        entropy_value = 0
                        if value_count != 0:
                            for label_value in label_values:
                                p = label_values[label_value] / value_count
                                if p != 0:
                                    entropy_value += (-p * log2(p))
                            expected_entropy += (proportion_value * entropy_value)
                gain = entropy_s - expected_entropy
                if gain >= greatest_gain:
                    greatest_gain = gain
                    greatest_gain_name = attribute.attribute_name
        return greatest_gain_name

    @staticmethod
    def majority_error(examples, me_attributes):
        label = Node.find_label(me_attributes)  # label attribute object
        label_counts = dict.fromkeys(label.values, 0)
        max_count = 0
        max_label = label.values[0]
        for example in examples:
            label_counts[example[-1]] += 1
            if max_count < label_counts[example[-1]]:
                max_count = label_counts[example[-1]]
                max_label = example[-1]
        wrong_count = 0
        for example in examples:
            if example[-1] != max_label:
                wrong_count += 1
        numb_examples = len(examples)  # number of examples
        maj_error_s = wrong_count / numb_examples
        greatest_gain = 0
        greatest_gain_name = me_attributes[0].attribute_name
        for me_i, attribute in enumerate(me_attributes):
            if not attribute.is_label and attribute.in_attributes:
                maj_error_val = 0
                if attribute.is_numeric:
                    values_numeric = {'g': 0, 'le': 0}
                    label_values_numeric = dict.fromkeys(label.values, 0)
                    maj_label = label.values[0]
                    maj_count = 0
                    for example in examples:
                        if example[me_i] <= attribute.median:
                            values_numeric['le'] += 1
                            label_values_numeric[example[-1]] += 1
                            if label_values_numeric[example[-1]] > maj_count:
                                maj_count = label_values_numeric[example[-1]]
                                maj_label = example[-1]
                    error_count = 0
                    for numeric_label in label_values_numeric:
                        if numeric_label != maj_label:
                            error_count += label_values_numeric[numeric_label]
                    if label_values_numeric[maj_label] != 0:
                        maj_error_val += ((label_values_numeric[maj_label] / numb_examples) *
                                          (error_count / label_values_numeric[maj_label]))
                    maj_label = label.values[0]
                    maj_count = 0
                    label_values_numeric = dict.fromkeys(label.values, 0)
                    for example in examples:
                        if example[me_i] > attribute.median:
                            values_numeric['g'] += 1
                            label_values_numeric[example[-1]] += 1
                            if label_values_numeric[example[-1]] > maj_count:
                                maj_count = label_values_numeric[example[-1]]
                                maj_label = example[-1]
                    error_count = 0
                    for numeric_label in label_values_numeric:
                        if numeric_label != maj_label:
                            error_count += label_values_numeric[numeric_label]
                    if label_values_numeric[maj_label] != 0:
                        maj_error_val += ((label_values_numeric[maj_label] / numb_examples) *
                                          (error_count / label_values_numeric[maj_label]))
                else:
                    attribute_vals = attribute.values
                    for value in attribute_vals:
                        label_values = dict.fromkeys(label.values, 0)
                        maj_label = label.values[0]
                        maj_label_count = 0
                        for example in examples:
                            if example[me_i] == value:
                                label_values[example[-1]] += 1
                                if label_values[example[-1]] >= maj_label_count:
                                    maj_label_count = label_values[example[-1]]
                                    maj_label = example[-1]
                        error_count = 0
                        for label_value in label_values:
                            if label_value != maj_label:
                                error_count += label_values[label_value]
                        if label_values[maj_label] != 0:
                            maj_error_val += ((label_values[maj_label] / numb_examples) *
                                              (error_count / label_values[maj_label]))
                gain = maj_error_s - maj_error_val
                if gain >= greatest_gain:
                    greatest_gain = gain
                    greatest_gain_name = attribute.attribute_name
        return greatest_gain_name

    @staticmethod
    def gini_index(examples, gini_index_attributes):
        label = Node.find_label(gini_index_attributes)  # label attribute object
        label_counts = dict.fromkeys(label.values, 0)
        numb_examples = len(examples)
        for example in examples:
            label_counts[example[-1]] += 1
        gi_s = 1
        for label_count in label_counts:
            gi_s -= ((label_counts[label_count] / numb_examples) ** 2)
        greatest_gain = 0
        greatest_gain_name = gini_index_attributes[0].attribute_name
        for gini_index_i, attribute in enumerate(gini_index_attributes):
            if not attribute.is_label and attribute.in_attributes:
                expected_gain = 0
                if attribute.is_numeric:
                    values_numeric = {'g': 0, 'le': 0}
                    label_values_numeric = dict.fromkeys(label.values, 0)
                    for example in examples:
                        if example[gini_index_i] <= attribute.median:
                            values_numeric['le'] += 1
                            label_values_numeric[example[-1]] += 1
                    gi_val = 1
                    for label_value_numeric in label_values_numeric:
                        if values_numeric['le'] != 0:
                            gi_val -= ((label_values_numeric[label_value_numeric] / values_numeric['le']) ** 2)
                    expected_gain += ((values_numeric['le'] / numb_examples) * gi_val)
                    label_values_numeric = dict.fromkeys(label.values, 0)
                    for example in examples:
                        if example[gini_index_i] > attribute.median:
                            values_numeric['g'] += 1
                            label_values_numeric[example[-1]] += 1
                    gi_val = 1
                    for label_value_numeric in label_values_numeric:
                        if values_numeric['g'] != 0:
                            gi_val -= ((label_values_numeric[label_value_numeric] / values_numeric['g']) ** 2)
                    expected_gain += ((values_numeric['g'] / numb_examples) * gi_val)
                else:
                    attribute_vals = attribute.values
                    for value in attribute_vals:
                        label_values = dict.fromkeys(label.values, 0)
                        value_count = 0
                        for example in examples:
                            if example[gini_index_i] == value:
                                label_values[example[-1]] += 1
                                value_count += 1
                        gi_val = 1
                        for label_value in label_values:
                            if value_count != 0:
                                gi_val -= ((label_values[label_value] / value_count) ** 2)
                        expected_gain += ((value_count / numb_examples) * gi_val)
                gain = gi_s - expected_gain
                if gain >= greatest_gain:
                    greatest_gain = gain
                    greatest_gain_name = attribute.attribute_name
        return greatest_gain_name