#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Simple module that implements the playfair cipher"""

import string, itertools, re
from operator import attrgetter
# create an alphabet of ASCII letters without 'J'
alphabet = ''.join(letter for letter in string.ascii_uppercase if letter != 'J')

def chunkize(iterable, size):
    """Splits an iterable into size-defined chunks"""
    result = list()

    current_chunk = list()
    for index, item in enumerate(iterable):
        current_chunk.append(item)
        if index % size == size - 1:
            result.append(current_chunk)
            current_chunk = list()

    if current_chunk:
        result.append(current_chunk)

    return result

class RowMatrix(object):
    def __init__(self, matrix):
        # first get the length of a row by getting the lengt of the longest
        # row - because the matrix can contain empty lines which get ignored
        len_row = len(max(matrix.splitlines()))
        self.matrix = chunkize(matrix.replace('\n', ''), len_row)

    def row(self, number):
        return ''.join(self.matrix[number])

    def column(self, number):
        return ''.join(chunk[number] for chunk in self.matrix)

    def get_coords(self, letter):
        for row, content in enumerate(self.matrix):
            try:
                return row, content.index(letter)
            except ValueError:
                pass
        raise ValueError('Letter not in matrix')

    def get_letter(self, letter):
        # TODO: check for existance in matrix
        return MatrixLetter(letter, self)

    def __getitem__(self, position):
        row, column = position
        # get the number of rows and columns
        num_row = len(self.matrix[0])
        num_column = len(self.matrix)

        # calculate the base rows and columns
        normalized_row = row % num_row
        normalized_column = column % num_column
        # access the matrix via these and return a MatrixLetter
        return MatrixLetter(self.row(normalized_row)[normalized_column], self)

class MatrixLetter(str):
    def __new__(cls, text, matrix):
        return str.__new__(cls, text)

    def __init__(self, text, matrix, *args, **kwargs):
        str.__init__(self, text, *args, **kwargs)
        self.matrix = matrix

    @property
    def coords(self):
        return self.matrix.get_coords(str(self))

    def in_row(self, other_letter):
        own_coords = self.matrix.get_coords(str(self))
        other_coords = self.matrix.get_coords(other_letter)
        return True if own_coords[0] == other_coords[0] else False

    def in_column(self, other_letter):
        own_coords = self.matrix.get_coords(str(self))
        other_coords = self.matrix.get_coords(other_letter)
        return True if own_coords[1] == other_coords[1] else False

    @property
    def right_letter(self):
        row, column = self.coords
        return self.matrix[(row, column + 1)]

    @property
    def left_letter(self):
        row, column = self.coords
        return self.matrix[(row, column - 1)]

    @property
    def lower_letter(self):
        row, column = self.coords
        return self.matrix[(row + 1, column)]

    @property
    def upper_letter(self):
        row, column = self.coords
        return self.matrix[(row - 1, column)]

    def flip(self, other):
        # TODO: make sure letters belong to identical matrices
        own_row, own_column = self.coords
        other_row, other_column = other.coords
        return self.matrix[(own_row, other_column)]

def create_pairs(text):
    clean_text = text.upper()
    clean_text = clean_text.replace('J', 'I')

    # filter out all wrong characters
    clean_chars = [letter for letter in clean_text if letter in alphabet]

    # replace OO by OXO etc.
    previous_letter = None
    clean_letters = list()
    for letter in clean_chars:
        if previous_letter != letter:
            clean_letters.append(letter)
        else:
            clean_letters.extend(('X', letter))
        previous_letter = letter

    if len(clean_letters) % 2 == 1:
        # if odd, add X
        clean_letters.append('X')

    return chunkize(clean_letters, 2)

def create_matrix(key, width=5):
    """Creates a matrix in letter form by adding letters of the key to it and
    filling it with the rest of the alphabet"""
    matrix = list()
    # first populate by the key
    for letter in key:
        # uppercase it
        letter = letter.upper()
        # add it to the matrix it it is not already there and if it exists
        # in the alphabet
        if not letter in matrix and letter in alphabet:
            matrix.append(letter)

    # not fill it with the rest of the alphabet
    for letter in alphabet:
        if letter not in matrix:
            matrix.append(letter)

    # the matrix in list form
    list_matrix = chunkize(''.join(matrix), width)
    # the matrix as string
    joined_matrix = '\n'.join(''.join(letters) for letters in list_matrix)
    return joined_matrix

def uni_crypt(pairs, matrix, crypt=True):
    # the attribute accessors, chosen depending on the setting of crypt
    row_letter = attrgetter('right_letter' if crypt else 'left_letter')
    column_letter = attrgetter('lower_letter' if crypt else 'upper_letter')

    for pair in pairs:
        # convert the letters to matrix letters
        matrix_letters = [matrix.get_letter(letter) for letter in pair]
        # some checks
        if matrix_letters[0].in_row(matrix_letters[1]):
            yield [row_letter(letter) for letter in matrix_letters]
        elif matrix_letters[0].in_column(matrix_letters[1]):
            yield [column_letter(letter) for letter in matrix_letters]
        else:
            yield [matrix_letters[0].flip(matrix_letters[1]),
                    matrix_letters[1].flip(matrix_letters[0])]

def unpair(pairs):
    return ''.join(''.join(pair) for pair in pairs)

def prettify(decoded):
    # replace charXchar by charchar
    for group in re.findall(r'(\w)X(\w)', decoded):
        if group[0] == group[1]:
            decoded = decoded.replace('%sX%s' % tuple(group), ''.join(group))

    # get rid of trailing X if the sting has odd lenght and a trailing X
    if len(decoded) % 2 and decoded.endswith('X'):
        decoded = decoded[:-1]
    return decoded

def main():
    """Demonstration by using a simple matrix (key) and a simple plain text"""
    letter_matrix = create_matrix(u'Der Knabe im Moor')
    # TODO: fix umlaut-escaping
    pairs = create_pairs(u"Oh schaurig ist's, über's Moor zu gehn")
    matrix = RowMatrix(letter_matrix)
    enc = uni_crypt(pairs, matrix)
    #print unpair(enc)
    dec = uni_crypt(enc, matrix, crypt=False)
    decoded = unpair(dec)
    print decoded
    print prettify(decoded)


if __name__ == '__main__':
    main()
