# 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 Image


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

    Use the methods `getPixel` and `setPixel` 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)."""

    # Fields
    _rows = 0     # Number of rows
    _cols = 0     # Number of columns
    _data = None  # List with image data

    # Immutable properties

    @property
    def rows(self):
        """The number of rows in this image

        *This attribute is set by the constructor and may not be altered*"""
        return self._rows

    @property
    def cols(self):
        """The number of columns in this image

        *This attribute is set by the constructor and may not be altered*"""
        return self._cols

    @property
    def len(self):
        """Length of pixel array

        *This attribute is set by the constructor and may not be altered*"""
        return len(self._data)

    @property
    def texture(self):
        """An OpenGL texture for this image (used for rendering)

        *This attribute is recomputed each time by the getter and may not be
        altered directly.*"""
        texture = Texture.create((self.cols,self.rows))
        buf = bytearray(0)
        for r in xrange(self.rows-1,-1,-1):
            for c in xrange(self.cols):
                pixel = self.getPixel(r,c)
                pixel = (pixel[0],pixel[1],pixel[2],255)
                buf.extend(pixel)
        buf = ''.join(map(chr, buf))
        texture.blit_buffer(buf, colorfmt='rgba', bufferfmt='ubyte')
        return texture

    @property
    def image(self):
        """An Image object for this ImageArray. Used to save results.

        *This attribute is recomputed each time by the getter and may not be
        altered directly.*"""
        im = Image.new('RGBA',(self.cols,self.rows))
        im.putdata(self._data)
        return im

    # Alternate Constructors

    @classmethod
    def LoadFile(cls,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)
        (c,r) = im.size
        self = cls(rows=r,cols=c,data=list(im.getdata()))
        return self

    @classmethod
    def Copy(cls,data):
        """**Constructor** (alternate): Create a copy of the given ImageArray

            :param data: image array to copy
            **Precondition**: an ImageArray object

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

    # Built-in Methods

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

            :param rows: The number of rows in this image.
            **Precondition**: an int >= 0

            :param cols: The number of columns in this image.
            **Precondition**: an int >= 0

            :param data: The pixel array for this image.
            **Precondition**: an array of pixels (i.e. 3-element tuples,
            each element an int in 0..255) of size rows*cols

        In general, you should leave the parameter `data` as `None`. If
        you wish to initialize the image with some data, use one of the
        alternate constructors like `Copy` or `LoadFile`."""
        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
        if data is None:
            self._data = [(0,0,0)]*(rows*cols)
        else:
            self._data = data

    # Normal Methods

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

            :param row: The pixel row
            **Precondition**: an int for a valid pixel row

            :param col: The pixel col
            **Precondition**: an int for a valid pixel column

        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 setPixel(self, row, col, pixel):
        """Sets the pixel value at (row, col) to pixel

            :param row: The pixel row
            **Precondition**: an int for a valid pixel row

            :param col: The pixel column
            **Precondition**: an int for a valid pixel column

            :param pixel: The pixel value
            **Precondition**: a 3-element tuple (r,g,b) where each
            value is 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 swapPixels(self, row1, col1, row2, col2):
        """Swaps the pixel at (row1, col1) with the pixel at (row2, col2)

            :param row1: The pixel row to swap from
            **Precondition**: an int for a valid pixel row

            :param row2: The pixel row to swap to
            **Precondition**: an int for a valid pixel row

            :param col1: The pixel column to swap from
            **Precondition**: an int for a valid pixel column

            :param col2: The pixel column to swap to
            **Precondition**: an int for a valid pixel column

        Preconditions are enforced only if enforced in `getPixel`, `setPixel`."""
        temp = self.getPixel(row1, col1)
        self.setPixel(row1, col1, self.getPixel(row2, col2))
        self.setPixel(row2, col2, temp)

    def getFlatPixel(self, n):
        """**Returns**: Pixel number n of the image (in row major order)

            :param n: The pixel number to access
            **Precondition**: an int in 0..(length of the image buffer - 1)

        This method is used when you want to treat an image as a flat,
        one-dimensional list rather than a 2-dimensional image.  It is useful
        for the steganography part of the assignment.

        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.len, `n`+' is an invalid pixel position'
        return self._data[n]

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

            :param n: The pixel number to access
            **Precondition**: an int in 0..(length of the image buffer - 1)

            :param pixel: The pixel value
            **Precondition**: a 3-element tuple (r,g,b) where each
            value is 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.  It is useful
        for the steganography part of the assignment.

        This method does not enforce the preconditions; that
        is the responsibility of the user."""
        #assert type(n) == int and n >=0 and n < self.len, `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