# binary_ops.py
# This file provides a class interface for commonly-used bitwise operations on VERY LARGE binary numbers


# Internal Imports

# External Imports



'''
----------Bitwise_Operator----------
- This class provides an interface for commonly-used bitwise operations on VERY LARGE binary numbers
  (Uses chunking to more appropriately calculate these operations)
-----Member Variables-----
- dimension - The dimension (number of bits) that the binary numbers will have
- chunk_size - The size of chunks used to calculate bitwise operations (MUST be cleanly divisible into the dimension)
-----Member Functions-----
- __init__ - The constructor
- and - Calculates a bitwise and between two given binary strings
- or - Calculates a bitwise or between two given binary strings
- not - Calculates a bitwise not for a given binary string
- xor - Calculates a bitwise xor between two given binary strings
- similarity - Calculates the bitwise similarity between two given binary strings
- distance - Calculates the bitwise distance between two given binary strings
'''
class Bitwise_Operator:
    '''
    ----------__init__----------
    - This function handles constructing the Bitwise_Operator object for a given dimension
    -----Inputs-----
    - dimension - The dimension (number of bits) that the binary numbers will have (defaults to 32)
    - chunk_size - The size of chunks used to calculate bitwise operations (defaults to the largest cleanly-divisible number less than 32)
    -----Output-----
    - N/A - The result is returned as the newly-created object
    '''
    def __init__(self, dimension = 32, chunk_size = 0):
        self.dimension = dimension
        if chunk_size and not (dimension % chunk_size):
            self.chunk_size = chunk_size
        else:
            # Get a cleanly-divisible chunk size for the given dimension
            self.chunk_size = [x for x in range(31, 1, -1) if not dimension % x][0]
    
    
    '''
    ----------and----------
    - This function handles calculating a bitwise and
    -----Inputs-----
    - embed1 - The first embedding (as a string)
    - embed2 - The second embedding (as a string)
    -----Output-----
    - result - The resultant embedding, representing the bitwise and between embedding 1 & embedding 2
    '''
    def and(self, embed1, embed2):
        result = 0
        for i in range(int(self.dimension / self.chunk_size)):
            index = [i * self.chunk_size, (i+1) * self.chunk_size]
            binary = bin(int(embed1[index[0]:index[1]], 2) & int(embed2[index[0]:index[1]], 2))
            result += binary[2:].rjust(self.chunk_size, "0")
        return result

    
    '''
    ----------or----------
    - This function handles calculating a bitwise or
    -----Inputs-----
    - embed1 - The first embedding (as a string)
    - embed2 - The second embedding (as a string)
    -----Output-----
    - result - The resultant embedding, representing the bitwise or between embedding 1 & embedding 2
    '''
    def or(self, embed1, embed2):
        result = 0
        for i in range(int(self.dimension / self.chunk_size)):
            index = [i * self.chunk_size, (i+1) * self.chunk_size]
            binary = bin(int(embed1[index[0]:index[1]], 2) | int(embed2[index[0]:index[1]], 2))
            result += binary[2:].rjust(self.chunk_size, "0")
        return result
    
    
    '''
    ----------not----------
    - This function handles calculating a bitwise not
    -----Inputs-----
    - embed - The embedding (as a string)
    -----Output-----
    - result - The resultant embedding, representing the bitwise not of embedding 1
    '''
    def not(self, embed):
        result = 0
        for i in range(int(self.dimension / self.chunk_size)):
            index = [i * self.chunk_size, (i+1) * self.chunk_size]
            #binary = bin(int(embed1[index[0]:index[1]], 2) | int(embed2[index[0]:index[1]], 2))
            #result += binary[2:].rjust(self.chunk_size, "0")
        return result

    
    '''
    ----------xor----------
    - This function handles calculating a bitwise xor
    -----Inputs-----
    - embed1 - The first embedding (as a string)
    - embed2 - The second embedding (as a string)
    -----Output-----
    - result - The resultant embedding, representing the bitwise xor between embedding 1 & embedding 2
    '''
    def xor(self, embed1, embed2):
        result = 0
        for i in range(int(self.dimension / self.chunk_size)):
            index = [i * self.chunk_size, (i+1) * self.chunk_size]
            binary = bin(int(embed1[index[0]:index[1]], 2) ^ int(embed2[index[0]:index[1]], 2))
            result += binary[2:].rjust(self.chunk_size, "0")
        return result


    '''
    ----------similarity----------
    - This function handles calculating binary similarity (bitwise xor -> zero count)
    -----Inputs-----
    - embed1 - The first embedding (as a string)
    - embed2 - The second embedding (as a string)
    -----Output-----
    - similarity - The similarity number between embedding 1 & embedding 2
    '''
    def similarity(self, embed1, embed2):
        similarity = 0
        for i in range(int(self.dimension / self.chunk_size)):
            index = [i * self.chunk_size, (i+1) * self.chunk_size]
            binary = bin(int(embed1[index[0]:index[1]], 2) ^ int(embed2[index[0]:index[1]], 2))
            similarity += binary[2:].rjust(self.chunk_size, "0").count("0")
        return similarity


    '''
    ----------distance----------
    - This function handles calculating binary distance (bitwise xor -> one count)
    -----Inputs-----
    - embed1 - The first embedding (as a string)
    - embed2 - The second embedding (as a string)
    -----Output-----
    - distance - The similarity number between embedding 1 & embedding 2
    '''
    def distance(self, embed1, embed2):
        distance = 0
        for i in range(int(self.dimension / self.chunk_size)):
            index = [i * self.chunk_size, (i+1) * self.chunk_size]
            binary = bin(int(embed1[index[0]:index[1]], 2) ^ int(embed2[index[0]:index[1]], 2))
            distance += binary[2:].rjust(self.chunk_size, "0").count("1")
        return distance


'''
----------similarity----------
- This function handles calculating binary similarity (bitwise xor -> zero count)
-----Inputs-----
- embed1 - The first embedding (as a string)
- embed2 - The second embedding (as a string)
-----Output-----
- similarity - The similarity number between embedding 1 & embedding 2
'''
def similarity(embed1, embed2):
    similarity = 0
    for i in range(int(len(embed1) / chunk_size)):
        binary = bin(int(embed1[i * chunk_size:(i+1) * chunk_size], 2) ^ int(embed2[i * chunk_size:(i+1) * chunk_size], 2))
        similarity += binary[2:].rjust(chunk_size, "0").count("0")
    return similarity
    #binary = bin(int(embed1, 2) ^ int(embed2, 2))[2:]
    #return binary.rjust(dimension, "0").count("0")


'''
----------distance----------
- This function handles calculating binary distance (bitwise xor -> one count)
-----Inputs-----
- embed1 - The first embedding (as a string)
- embed2 - The second embedding (as a string)
-----Output-----
- distance - The similarity number between embedding 1 & embedding 2
'''
def distance(embed1, embed2):
    similarity = 0
    for i in range(int(len(embed1) / chunk_size)):
        binary = bin(int(embed1[i * chunk_size:(i+1) * chunk_size], 2) ^ int(embed2[i * chunk_size:(i+1) * chunk_size], 2))
        similarity += binary[2:].rjust(chunk_size, "0").count("1")
    return similarity
    #binary = bin(int(embed1, 2) ^ int(embed2, 2))[2:]
    #return binary.rjust(dimension, "0").count("1")


'''
----------xor----------
- This function handles calculating binary distance (bitwise xor -> one count)
-----Inputs-----
- embed1 - The first embedding (as a string)
- embed2 - The second embedding (as a string)
-----Output-----
- distance - The similarity number between embedding 1 & embedding 2
'''
def xor(embed1, embed2):
    result = ""
    for i in range(int(len(embed1) / chunk_size)):
        binary = bin(int(embed1[i * chunk_size:(i+1) * chunk_size], 2) ^ int(embed2[i * chunk_size:(i+1) * chunk_size], 2))
        result += binary[2:].rjust(chunk_size, "0")
    return result


def or(embed1, embed2):
    result = ""
    for i in range(int(len(embed1) / chunk_size)):
        binary = bin(int(embed1[i * chunk_size:(i+1) * chunk_size], 2) | int(embed2[i * chunk_size:(i+1) * chunk_size], 2))
        result += binary[2:].rjust(chunk_size, "0")
    return result