# image_array.py
# Dexter Kozen (dck10) and Walker White (wmw2)
# October 26, 2012
"""This module provides the Model for our application.

It contains a single class.  Instances of this class support an
image that can be modified."""
from kivy.graphics.texture import Texture

import numpy
import Image


class ImageArray(object):
    """An instance maintains a row-major order array of pixels for an image.

    Use the methods get_pixel and set_pixel to get and set the various
    pixels in this image.  Pixels are represented as 3-element tuples,
    with each element in the range 0..255.  For example, red is (255,0,0).

    These pixels are not RGB objects, like in Assignment 3.  They are tuples,
    which are lists that cannot be modified (so you can slice them to get new
    tuples, but not assign or append to them).

    Instance variables:
        rows (int): Number of rows in the image
        cols (int): Number of columns in the image
        size (int): Number of pixels in the image (== rows*cols)
        _data (list of 3-tuples): List with image data
    The variables rows, cols, and size are set only by __init__ and never change.
    The list _data (of length <size>) contains 3-tuples of integers in 0..255.
    """

    def __init__(self, rows,cols,data=None):
        """Initialize an ImageArray of the given size.

        Parameters:
            rows: The number of rows in the new image.
            cols: The number of columns in the new image.
            data: The pixel array for this image, or None for a black image.

        Precondition: rows, cols >= 0.  data is either None or it is an array
            of pixels (i.e. 3-element tuples, each element an int in 0..255) 
            of size rows*cols

        Generally, you should leave the parameter 'data' as None. To create
        an image that isn't all zeroes, you can use the copy method or the
        load_file function."""
        assert type(rows) == int and rows >= 0, `rows`+' is an invalid argument'
        assert type(cols) == int and cols >= 0, `cols`+' is an invalid argument'
        assert data is None or type(data) == list, `data`+' is an invalid buffer'
        if not data is None:
            assert len(data) == rows*cols, `data`+' is an invalid size'

        self.rows = rows
        self.cols = cols
        self.size = rows*cols
        if data is None:
            self._data = [(0,0,0)]*(rows*cols)
        else:
            self._data = data

    def copy(self):
        """Return: A copy of this ImageArray

        Once the copy is created, changes to the copy will not affect
        this instance."""
        return ImageArray(rows=self.rows,cols=self.cols,data=self._data[:])

    def get_pixel(self, row, col):
        """**Returns**: The pixel value at (row, col)

        Pre: 0 <= row < self.rows and 0 <= col < self.cols
        Value returned is an 3-element tuple (r,g,b).

        This method does not enforce the preconditions; that
        is the responsibility of the user."""
        #assert type(row) == int and row >= 0 and row < self._rows, `row`+' is an invalid row'
        #assert type(col) == int and col >= 0 and col < self._cols, `col`+' is an invalid column'
        return self._data[row*self.cols + col]

    def set_pixel(self, row, col, pixel):
        """Sets the pixel value at (row, col) to pixel

        Pre: 0 <= row < self.rows and 0 <= col < self.cols
             pixel is a tuple of 3 values in 0..255.

        This method does not enforce the preconditions; that
        is the responsibility of the user."""
        #assert type(row) == int and row >= 0 and row < self._rows, `row`+' is an invalid row'
        #assert type(col) == int and col >= 0 and col < self._cols, `col`+' is an invalid column'
        #assert type(pixel) == tuple and len(tuple) == 3, `pixel`+' is not a 3-element tuple'
        #assert type(pixel[0]) == int and 0 <= pixel[0] and pixel[0] < 256, `pixel[0]`+' is an invalid red value'
        #assert type(pixel[1]) == int and 0 <= pixel[1] and pixel[1] < 256, `pixel[1]`+' is an invalid green value'
        #assert type(pixel[2]) == int and 0 <= pixel[2] and pixel[2] < 256, `pixel[2]`+' is an invalid blue value'
        self._data[row*self.cols + col] = pixel

    def swap_pixels(self, row1, col1, row2, col2):
        """Swaps the pixel at (row1, col1) with the pixel at (row2, col2)

        Pre: 0 <= row1, row2 < self.rows and 0 <= col1, col2 < self.cols
        """
        temp = self.get_pixel(row1, col1)
        self.set_pixel(row1, col1, self.get_pixel(row2, col2))
        self.set_pixel(row2, col2, temp)

    def get_flat_pixel(self, n):
        """Return: Pixel number n of the image (in row major order)

        Pre: n in 0..(self.size- 1)

        This method is used when you want to treat an image as a flat,
        one-dimensional list rather than a 2-dimensional image.

        Value returned is a 3-element tuple (r,g,b).

        This method does not enforce the preconditions; that
        is the responsibility of the user."""
        assert type(n) == int and n >=0 and n < self.size, `n`+' is an invalid pixel position'
        return self._data[n]

    def set_flat_pixel(self, n, pixel):
        """Sets pixel number n of the image (in row major order) to pixel

        Pre: n in 0..(self.size- 1); pixel is a tuple of 3 values in 0..255.

        This method is used when you want to treat an image as a flat,
        one-dimensional list rather than a 2-dimensional image.

        This method does not enforce the preconditions; that
        is the responsibility of the user."""
        #assert type(n) == int and n >=0 and n < self.size, `n`+' is an invalid pixel position'
        #assert type(pixel) == tuple and len(tuple) == 3, `pixel`+' is not a 3-element tuple'
        #assert type(pixel[0]) == int and 0 <= pixel[0] and pixel[0] < 256, `pixel[0]`+' is an invalid red value'
        #assert type(pixel[1]) == int and 0 <= pixel[1] and pixel[1] < 256, `pixel[1]`+' is an invalid green value'
        #assert type(pixel[2]) == int and 0 <= pixel[2] and pixel[2] < 256, `pixel[2]`+' is an invalid blue value'
        self._data[n] = pixel

    def get_texture(self):
        """An OpenGL texture for this image (used for rendering)."""
        texture = Texture.create((self.cols,self.rows), colorfmt='rgb')
        buf = bytearray(0)
        for r in xrange(self.rows):
            for c in xrange(self.cols):
                pixel = self.get_pixel(r,c)
                pixel = (pixel[0],pixel[1],pixel[2])
                buf.extend(pixel)
        buf = ''.join(map(chr, buf))
        texture.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte')
        return texture

    def get_image(self):
        """An Image object for this ImageArray. Used to save results."""
        im = Image.new('RGBA',(self.cols,self.rows))
        im.putdata(self._data)
        return im



# Function for creating new ImageArrays; notice it is outside the class.

def load_file(filename):
    """**Constructor** (alternate): Create an ImageArray for the given image file

        :param filename: image file to initialize array
        **Precondition**: a string representing an image file.

    If filename is not the name of a valid image file, this constructor will raise
    an exception."""
    assert type(filename) == str, `filename`+' is not a string'
    im = Image.open(filename)
    (cols,rows) = im.size
    return ImageArray(rows,cols,data=map(lambda x: x[:3], im.getdata()))