import numpy
import collections
import random


class CubeModel:
    DIMENSION = 3  # Genericity of the dimensionality only sketched here. It is not sufficient to change the dimension
    # and expect the cube model to be true for the specified dimensionality.
    NUMBER_OF_CORNERS = 2 ** DIMENSION
    NUMBER_OF_EDGES = NUMBER_OF_CORNERS * DIMENSION // 2

    FACES = ['F', 'B', 'L', 'R', 'U', 'D']  # Limited to the 3D cube

    def __init__(self, size):
        self.size = size
        self.center = size // 2
        self.min_index = 0
        self.max_index = size - 1
        self.boundary = {self.min_index, self.max_index}
        if size % 2 == 0:
            self.center_index = -1
        else:
            self.center_index = self.center

        self.cubies = self.calculate_cubies()
        self.cube, self.locations, self.rotations = self.initialize_cube()

    def calculate_cubies(self):
        cubies = collections.OrderedDict()

        # triples of numbers: (pieces on the cube, possible positions of such pieces, possible rotations)
        cubies['corner'] = (self.NUMBER_OF_CORNERS, self.NUMBER_OF_CORNERS, self.DIMENSION)
        cubies['edge'] = (self.NUMBER_OF_EDGES * (self.size % 2), self.NUMBER_OF_EDGES, 2)
        cubies['wing'] = (self.NUMBER_OF_EDGES * (self.center - 1) * 2, self.NUMBER_OF_EDGES * 2, 1)
        cubies['facediagonal'] = (len(self.FACES) * (self.center - 1) * 4, len(self.FACES) * 4, 1)
        cubies['facecenter'] = (len(self.FACES) * (self.center - 1) * 4 * (self.size % 2), len(self.FACES) * 4, 1)
        cubies['facewing'] = (len(self.FACES) * (self.center - 2) * (self.center - 1) * 4, len(self.FACES) * 8, 1)

        return cubies

    def initialize_cube(self):
        variable = numpy.zeros((self.size, self.size, self.size), dtype=int) - 1
        location = numpy.zeros((self.size, self.size, self.size), dtype=int) - 1
        rotation = numpy.zeros((self.size, self.size, self.size), dtype=(int, self.DIMENSION))

        variable_counter = 0

        for key in self.cubies.keys():
            pieces, positions, rotations = self.cubies[key]
            if key == 'facewing':
                max_i = self.center + 1
                for i in range(1, max_i):
                    for j in range(i + 1, max_i):
                        location_counter = 0
                        for x in range(self.size):
                            for y in range(self.size):
                                for z in range(self.size):
                                    if variable[x, y, z] < 0 and self.is_facewing(x, y, z, i, j):
                                        variable[x, y, z] = variable_counter
                                        variable_counter += 1
                                        location[x, y, z] = location_counter
                                        location_counter += 1
                                        rotation = self.init_rotation(key, x, y, z)
            else:
                for i in range(1, pieces // positions + 1):
                    location_counter = 0
                    for x in range(self.size):
                        for y in range(self.size):
                            for z in range(self.size):
                                if ((key == 'corner' and self.is_corner(x, y, z)) or
                                        (key == 'edge' and self.is_edge(x, y, z)) or
                                        (key == 'wing' and self.is_wing(x, y, z, i)) or
                                        (key == 'facediagonal' and self.is_facediagonal(x, y, z, i)) or
                                        (key == 'facecenter' and self.is_facecenter(x, y, z, i))):
                                    variable[x, y, z] = variable_counter
                                    variable_counter += 1
                                    location[x, y, z] = location_counter
                                    location_counter += 1
                                    rotation[x, y, z] = self.init_rotation(key, x, y, z)
        return variable, location, rotation

    def init_rotation(self, key, x, y, z):
        if key == 'corner':
            if ((x == self.max_index) + (y == self.max_index) + (z == self.max_index)) % 2 == 0:
                return 0, 1, 2
            else:
                return 0, 2, 1
        elif key == 'edge':
            if x in self.boundary and y in self.boundary:
                return 0, 1, -1
            elif x in self.boundary and z in self.boundary:
                return 0, -1, 1
            elif y in self.boundary and z in self.boundary:
                return -1, 0, 1
        else:
            return (x in self.boundary) - 1, (y in self.boundary) - 1, (z in self.boundary) - 1

    def rotate_cubie(self, rotation, axis1, axis2):
        rotation[axis1], rotation[axis2] = rotation[axis2], rotation[axis1]
        return rotation

    def turn_clockwise(self, face, layer, turns):
        if layer > self.center:
            print('Unknown rotation operation.\n')
            return

        rotation_indexes = range(self.size)

        if face in {'B', 'R', 'D'}:
            layer = self.max_index - layer
        if face in {'B', 'L', 'D'}:
            turns = -turns

        _x = rotation_indexes
        _y = rotation_indexes
        _z = rotation_indexes
        axis1, axis2 = -1, -1

        if face in {'F', 'B'}:
            axis1, axis2 = 1, 2
            _x = [layer]
        elif face in {'L', 'R'}:
            axis1, axis2 = 0, 2
            _y = [layer]
        elif face in {'U', 'D'}:
            axis1, axis2 = 0, 1
            _z = [layer]

        if turns % 2 != 0 and layer in self.boundary:
            for x in _x:
                for y in _y:
                    for z in _z:
                        self.rotations[x, y, z] = self.rotate_cubie(self.rotations[x, y, z], axis1, axis2)

        if face in {'F', 'B'}:
            self.cube[layer, :, :] = numpy.rot90(self.cube[layer, :, :], turns)
            self.rotations[layer, :, :] = numpy.rot90(self.rotations[layer, :, :], turns)
        elif face in {'L', 'R'}:
            self.cube[:, layer, :] = numpy.rot90(self.cube[:, layer, :], turns)
            self.rotations[:, layer, :] = numpy.rot90(self.rotations[:, layer, :], turns)
        elif face in {'U', 'D'}:
            self.cube[:, :, layer] = numpy.rot90(self.cube[:, :, layer], turns)
            self.rotations[:, :, layer] = numpy.rot90(self.rotations[:, :, layer], turns)

    def shuffle(self, turns, turn_options):
        scramble = ''
        last_face = None

        for i in range(turns):
            face = random.choice(self.FACES)
            while last_face == face:
                face = random.choice(self.FACES)
            last_face = face

            layer = random.randint(0, self.center - 1)
            turns = random.choice(turn_options)
            self.turn_clockwise(face, layer, turns)

            scramble += str(face)
            if turns == 2:
                scramble += '2'
            elif turns == 3:
                scramble += '\''
            if self.size > 3:
                scramble += '_' + str(layer)
            scramble += ' '

        return scramble

    def controlled_shuffle(self, scramble):
        scramble = scramble.split(' ')
        for step in scramble:
            s = step.split('_')
            if self.size < 4:
                index = 0
            else:
                index = int(s[1])

            face = s[0][:1]
            if s[0][1:2] == '2':
                turns = 2
            elif s[0][1:2] == '\'':
                turns = 3
            else:
                turns = 1

            self.turn_clockwise(face, index, turns)

    def get_value_for_variable(self, variable, value_type):
        for x in range(self.size):
            for y in range(self.size):
                for z in range(self.size):
                    if self.cube[x, y, z] == variable:
                        if value_type == 'location':
                            return int(self.locations[x, y, z])
                        elif value_type == 'rotation':
                            for i in range(self.DIMENSION):
                                if self.rotations[x, y, z][i] >= 0:
                                    return self.rotations[x, y, z][i]
        return None

    def get_position(self, variable):
        return self.get_value_for_variable(variable, 'location')

    def get_rotation(self, variable):
        return self.get_value_for_variable(variable, 'rotation')

    def rotate_single_cubie(self, variable):
        for x in range(self.size):
            for y in range(self.size):
                for z in range(self.size):
                    if self.cube[x, y, z] == variable:
                        if x in self.boundary and y in self.boundary and z in self.boundary:
                            _x, _y, _z = self.rotations[x, y, z]
                            self.rotations[x, y, z] = (_y, _z, _x)
                        elif (x in self.boundary) + (y in self.boundary) + (z in self.boundary) == 2:
                            for i in range(self.DIMENSION):
                                if self.rotations[x, y, z][i] < 0:
                                    tmp = self.rotations[x, y, z][i - 1]
                                    self.rotations[x, y, z][i - 1] = self.rotations[x, y, z][(i + 1) % self.DIMENSION]
                                    self.rotations[x, y, z][(i + 1) % self.DIMENSION] = tmp
                        return

    def is_corner(self, x, y, z):
        return x in self.boundary and y in self.boundary and z in self.boundary

    def is_edge(self, x, y, z):
        return (x in self.boundary and y in self.boundary and z == self.center_index) or \
               (x in self.boundary and z in self.boundary and y == self.center_index) or \
               (y in self.boundary and z in self.boundary and x == self.center_index)

    def is_wing(self, x, y, z, index):
        return (x in self.boundary and y in self.boundary and index == min(z, self.max_index - z)) or \
               (x in self.boundary and z in self.boundary and index == min(y, self.max_index - y)) or \
               (y in self.boundary and z in self.boundary and index == min(x, self.max_index - x))

    def is_facediagonal(self, x, y, z, index):
        return (x in self.boundary and (y == z or y == self.max_index - z) and index == min(y, self.max_index - y)) or \
               (y in self.boundary and (x == z or x == self.max_index - z) and index == min(x, self.max_index - x)) or \
               (z in self.boundary and (x == y or x == self.max_index - y) and index == min(x, self.max_index - x))

    def is_facecenter(self, x, y, z, index):
        x_center = x == self.center_index
        y_center = y == self.center_index
        z_center = z == self.center_index

        x_index = index == min(x, self.max_index - x)
        y_index = index == min(y, self.max_index - y)
        z_index = index == min(z, self.max_index - z)

        return (x in self.boundary and (y != z) and ((y_center and z_index) or (z_center and y_index))) or \
               (y in self.boundary and (x != z) and ((x_center and z_index) or (z_center and x_index))) or \
               (z in self.boundary and (x != y) and ((x_center and y_index) or (y_center and x_index)))

    def is_facewing(self, x, y, z, index1, index2):
        i = {index1, self.max_index - index1}
        j = {index2, self.max_index - index2}

        return (x in self.boundary and ((y in i and z in j) or (y in j and z in i))) or \
               (y in self.boundary and ((x in i and z in j) or (x in j and z in i))) or \
               (z in self.boundary and ((x in i and y in j) or (x in j and z in i)))

    def location_on_face(self, var_location, face, index, key, key_index1, key_index2):
        if face in {'B', 'R', 'D'}:
            index = self.size - 1 - index

        for x in range(self.size):
            for y in range(self.size):
                for z in range(self.size):
                    if self.locations[x, y, z] == var_location:
                        if key == 'corner' and self.is_corner(x, y, z):
                            return (face in {'F', 'B'} and x == index) or \
                                   (face in {'L', 'R'} and y == index) or \
                                   (face in {'U', 'D'} and z == index)
                        elif key == 'edge' and self.is_edge(x, y, z):
                            return (face in {'F', 'B'} and x == index) or \
                                   (face in {'L', 'R'} and y == index) or \
                                   (face in {'U', 'D'} and z == index)
                        elif key == 'wing' and self.is_wing(x, y, z, key_index1):
                            return (face in {'F', 'B'} and x == index) or \
                                   (face in {'L', 'R'} and y == index) or \
                                   (face in {'U', 'D'} and z == index)
                        elif key == 'facediagonal' and self.is_facediagonal(x, y, z, key_index1):
                            return (face in {'F', 'B'} and x == index) or \
                                   (face in {'L', 'R'} and y == index) or \
                                   (face in {'U', 'D'} and z == index)
                        elif key == 'facecenter' and self.is_facecenter(x, y, z, key_index1):
                            return (face in {'F', 'B'} and x == index) or \
                                   (face in {'L', 'R'} and y == index) or \
                                   (face in {'U', 'D'} and z == index)
                        elif key == 'facewing' and self.is_facewing(x, y, z, key_index1, key_index2):
                            return (face in {'F', 'B'} and x == index) or \
                                   (face in {'L', 'R'} and y == index) or \
                                   (face in {'U', 'D'} and z == index)
