"""
# Problem Set 5
# Name: Gyalpo Dongo
# Collaborators: Pamela D Abede Cudjoe
# Time: 3:00
"""    
from PIL import Image
import numpy 

def make_matrix(color):
    """
    Generates a transformation matrix for the specified color.
    Inputs:
        color: string with exactly one of the following values:
               'red', 'blue', 'green', or 'none'
    Returns:
        matrix: a transformation matrix corresponding to
                deficiency in that color
    """
    # You do not need to understand exactly how this function works.
    if color == 'red':
        c = [[.567, .433, 0],[.558, .442, 0],[0, .242, .758]]
    elif color == 'green':
        c = [[0.625,0.375, 0],[ 0.7,0.3, 0],[0, 0.142,0.858]]
    elif color == 'blue':
        c = [[.95, 0.05, 0],[0, 0.433, 0.567],[0, 0.475, .525]]
    elif color == 'none':
        c = [[1, 0., 0],[0, 1, 0.],[0, 0., 1]]
    return c


def matrix_multiply(m1,m2):
    """
    Multiplies the input matrices.
    Inputs:
        m1,m2: the input matrices
    Returns:
        result: matrix product of m1 and m2
        in a list of floats
    """
    product = numpy.matmul(m1,m2)
    if type(product) == numpy.int64:
        return float(product)
    else:
        result = list(product)
        return result


def img_to_pix(filename):
    """
    Takes a filename (must be inputted as a string
    with proper file attachment ex: .jpg, .png)
    and converts to a list of representing pixels.

    For RGB images, each pixel is a tuple containing (R,G,B) values.
    For BW images, each pixel is an integer.

    # Note: Don't worry about determining if an image is RGB or BW.
            The PIL library functions you use will return the 
            correct pixel values for either image mode.

    Returns the list of pixels.

    Inputs:
        filename: string representing an image file, such as 'lenna.jpg'
        returns: list of pixel values 
                 in form (R,G,B) such as [(0,0,0),(255,255,255),(38,29,58)...] for RGB image
                 in form L such as [60,66,72...] for BW image
    """
    im = Image.open(filename)
    return list(im.getdata())
    #Use of list() required as function getdata() return a 
    #sequence object which is an internal PIL data type, therefore use of
    #list() makes it be usable for any sort of sequence operations in Python


def pix_to_img(pixels, size, mode):
    """
    Creates an Image object from a inputted set of RGB tuples.

    Inputs:
        pixels: a list of pixels such as the output of
                img_to_pixels.
        size: a tuple of (width,height) representing
              the dimensions of the desired image. Assume
              that size is a valid input such that
              size[0] * size[1] == len(pixels).
        mode: 'RGB' or 'L' to indicate an RGB image or a 
              BW image, respectively
    returns:
        img: Image object made from list of pixels
    """
    im = Image.new(mode,size)
    #The first line creates a black image by default of the dimensions given
    #by the size
    im.putdata(pixels)
    #the function putdata()adds the pixel data to this black image. 
    #This method copies data from a sequence object into the image, 
    #starting at the upper left corner (0, 0), and continuing until either 
    #the image or the sequence ends
    return im
    
    


def filter(pixels, color):
    """
    pixels: a list of pixels in RGB form, such as 
            [(0,0,0),(255,255,255),(38,29,58)...]
    color: 'red', 'blue', 'green', or 'none', must be a string representing 
           the color deficiency that is being simulated.
    returns: list of pixels in same format as earlier functions,
    transformed by matrix multiplication
    """
    new_pixels = []
    #Empty list which will contain the new filtered/transformed pixels
    transf = make_matrix(color)
    #This assings the matrix created by make_matrix() to transf
    for i in pixels:
        pixel_t = tuple(matrix_multiply(transf,i))
        #using tuple(), the product of the matrix multiplication is 
        #assigend to pixel_t
        new_pixels.append(tuple(int(x) for x in pixel_t))
        #a tuple is added to new_pixels after transforming each value in the
        #tuple into an integer as the matrix multiplication returns them as 
        #floats
    return new_pixels
        

def extract_end_bits(num_bits, pixel):
    """
    Extracts the last num_bits bits of each value of a given pixel. 

    example for BW pixel:
        num_bits = 5
        pixel = 214

        214 in binary is 11010110. 
        The last 5 bits of 11010110 are 10110.
                              ^^^^^
        The integer representation of 10110 is 22, so we return 22.

    example for RBG pixel:
        num_bits = 2
        pixel = (214, 17, 8)

        last 3 bits of 214 = 110 --> 6
        last 3 bits of 17 = 001 --> 1
        last 3 bits of 8 = 000 --> 0

        so we return (6,1,0)

    Inputs:
        num_bits: the number of bits to extract
        pixel: an integer between 0 and 255, or a tuple of RGB values between 0 and 255

    Returns:
        The last num_bits bits of pixel, as an integer (BW) or tuple of integers (RGB).
    """
    #By looking at pattern, the value of each lsb is given by the pixel or one
    #of the numbers in the tuple of pixel and the remainder of this with 2
    #to the power of num_bits
    if type(pixel) == int:
        value = pixel%(2**num_bits)
        return value
    else:
        values = []
        #emty list which will have each of the values from each lsb in the 
        #tuple
        for i in pixel:
            #after iterating over tuple we do the same procedure as before
            value = i%(2**num_bits)
            values.append(value)
        #use of tuple() to return the desired tuple as this was the original
        #class of pixel
        return tuple(values)
        
        

def reveal_bw_image(filename):
    """
    Extracts the single LSB for each pixel in the BW input image. 
    Inputs:
        filename: string, input BW file to be processed
    Returns:
        result: an Image object containing the hidden image
    """
    im = Image.open(filename)
    #use of open() to open the image
    pixels = img_to_pix(filename)
    #use of img_to_pix to obtain a sequence of the image's pixels
    new_pixels = []
    #empty list where the new pixels from the secret image will be added
    for i in pixels:
        new_pixels.append(255*extract_end_bits(1,i))
        #as only values that can be obtained for the last single bit
        #in the list of pixels are 0 or 1, this means that the image will have
        #a very low contrast, therefore in order to increase it, as maximum
        #value of intensity is 255, either the 0 or 1 are multiplied by 255
    im_new = pix_to_img(new_pixels,im.size,im.mode)
    #using the new_pixels list and the mode and the size of the original image
    #we can reveal the hidden image
    return im_new


def reveal_color_image(filename):
    """
    Extracts the 2 LSBs for each pixel in the RGB input image. 
    Inputs:
        filename: string, input RGB file to be processed
    Returns:
        result: an Image object containing the hidden image
    """
    im = Image.open(filename)
    pixels = img_to_pix(filename)
    new_pixels = []
    for i in pixels:
        #the first part of this code is identical as previous reveal_bw_image()
        new_pix = extract_end_bits(2,i)
        #as specification says, only last 2 bits are required, so input of 2 
        #is used and as the input is an RGB file, a tuple will be returned
        #from extract_end_bits instead of an integer as i was also a tuple
        new_pixels.append(tuple(x*85 for x in new_pix))
        #Now the maximum possible value in any of the integers in each tuple 
        #is 3 and the maximum value of intensity would still be 255.
        #As image would still in very low contrast, each of the integers in
        #tuple shuld be multiplied by 85 so that the maximum value obtained
        #would be 255
    im_new = pix_to_img(new_pixels,im.size,im.mode)
    #Again use of pix_to_img() to reveal the hidden image
    return im_new

def reveal_image(filename):
    """
    Extracts the single LSB (for a BW image) or the 2 LSBs (for a 
    color image) for each pixel in the input image. Hint: you can
    use a function to determine the mode of the input image (BW or
    RGB) and then use this mode to determine how to process the image.
    Inputs:
        filename: string, input BW or RGB file to be processed
    Returns:
        result: an Image object containing the hidden image
    """    
    im = Image.open(filename)
    if im.mode == '1' or im.mode == 'L':
        return(reveal_bw_image(filename))
    elif im.mode == 'RGB':
        return(reveal_color_image(filename))
    else:
        raise Exception("Invalid mode %s" % im.mode)


def main():
    pass
    # Uncomment the following lines to test part 1

    im = Image.open('image_15.png')
    width, height = im.size
    pixels = img_to_pix('image_15.png')
    non_filtered_pixels = filter(pixels,'none')
    
    im = pix_to_img(non_filtered_pixels, (width, height), 'RGB')
    im.show()
    #this should show the same image as 'image_15.png'
    

    red_filtered_pixels = filter(pixels,'red')
    im2 = pix_to_img(red_filtered_pixels,(width,height), 'RGB')
    im2.show()
    #this should show the same image as 'image_15.png' but without the red 
    #colours

    # Uncomment the following lines to test part 2
    #Both of these should show their respective hidden images
    im = reveal_image('hidden1.bmp')
    im.show()
    
    im2 = reveal_image('hidden2.bmp')
    im2.show()


if __name__ == '__main__':
    pass
    main()


