import sys
import re


class Format:
    def __init__(self, cube, goal_cube, problem, moves, file):
        self.cube = cube
        self.goal_cube = goal_cube
        self.problem = problem
        self.moves = moves
        self.file = file
        self.variable_counter = 0
        self.mutex_counter = 0
        self.operator_counter = 0
        self.scramble = None

    def write(self):
        self.write_version()
        self.write_metric()
        self.write_variables()
        self.write_mutexes()
        self.write_initial_state()
        self.write_goal_state()
        self.write_operators()
        self.write_axioms()

        print('Built file successfully: ' + self.file.name)

    def write_version(self):
        self.file.write('begin_version\n3\nend_version\n')

    def write_metric(self):
        self.file.write('begin_metric\n0\nend_metric\n')

    def write_variables(self):
        number_of_variables = self.get_number_of_variables()
        self.file.write(str(number_of_variables) + '\n')

        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            for i in range(pieces // positions):
                for j in range(positions):
                    name = key
                    if name in self.cube.cubies.keys()[2:-1]:
                        name += str(i + 1)

                    self.write_variable(name, j, (positions, rotations))

        if self.variable_counter != number_of_variables:
            print('Number of variables does not agree with written variables.')
            sys.exit(2)

    def get_number_of_variables(self):
        count = 0
        for key in self.cube.cubies.keys():
            count += self.get_number_of_variables_of_cubie_type(key)
        return count

    def get_number_of_variables_of_cubie_type(self, cubie_type):
        raise NotImplementedError('Subclass must implement abstract method')

    def write_variable(self, name, index, options):
        raise NotImplementedError('Subclass must implement abstract method')

    def write_mutexes(self):
        number_of_mutexes = self.get_number_of_mutexes()
        self.file.write('{number_of_mutexes}\n'.format(**locals()))

        start_index = 0
        for key in self.cube.cubies.keys():
            start_index = self.write_mutex_groups(key, start_index)

        if self.mutex_counter != number_of_mutexes:
            print('Number of mutexes does not agree with written mutexes.\n')
            sys.exit(2)

    def get_number_of_mutexes(self):
        count = 0
        for key in self.cube.cubies.keys():
            count += self.cube.cubies[key][0]
        return count

    def write_mutex_groups(self, cubie_type, start):
        raise NotImplementedError('Subclass must implement abstract method')

    def write_initial_state(self):
        if self.problem is str:
            self.scramble = self.problem
            self.cube.controlled_shuffle(self.scramble)
        else:
            self.scramble = self.cube.shuffle(self.problem, self.moves)

        filename = self.file.name
        filename = re.sub('.sas$', '.scramble', filename)
        file = open(filename, 'w')
        file.write('{self.scramble}\n'.format(**locals()))

        state = self.render_initial_state()
        self.file.write('begin_state\n{state}end_state\n'.format(**locals()))

    def render_initial_state(self):
        raise NotImplementedError('Subclass must implement abstract method')

    def write_goal_state(self):
        goal = self.render_goal_state()
        self.file.write('begin_goal\n{self.variable_counter}\n{goal}end_goal\n'.format(**locals()))

    def render_goal_state(self):
        raise NotImplementedError('Subclass must implement abstract method')

    def write_operators(self):
        operators = self.calculate_number_of_operators()
        self.file.write('{operators}\n'.format(**locals()))
        for face in self.cube.FACES:
            for layer in range(self.cube.center):
                for move in self.moves:
                    number_of_effects = self.calculate_number_of_effects(layer)
                    self.write_operator(number_of_effects, layer, face, move)

    def calculate_number_of_operators(self):
        return len(self.cube.FACES) * self.cube.center * len(self.moves)

    def write_operator(self, effects, index, face, move):
        raise NotImplementedError('Subclass must implement abstract method')

    def calculate_number_of_effects(self, index):
        count = 0
        for key in self.cube.cubies.keys():
            additional_effects = self.cube.cubies[key][0] ** 2
            if key == 'corner':
                additional_effects //= 2
            elif key in {'edge', 'wing'}:
                additional_effects //= 3
            else:
                additional_effects //= 6

            if index > 0:
                if key in {'corner', 'edge'}:
                    additional_effects = 0
                elif key == 'wing':
                    additional_effects //= 2
                elif key == 'facediagonal':
                    additional_effects *= 2
                elif key == 'facewing':
                    additional_effects //= 4
            else:
                additional_effects *= self.calculate_effects_factor(key)
            count += additional_effects
        return count

    def calculate_effects_factor(self, cubie_type):
        raise NotImplementedError('Subclass must implement abstract method')

    def write_axioms(self):
        self.file.write('0\n')  # no axioms


class Separated(Format):
    def get_number_of_variables_of_cubie_type(self, cubie_type):
        return self.cube.cubies[cubie_type][0] * (1 + (self.cube.cubies[cubie_type][2] > 1))

    def write_variable(self, name, index, options):
        self.variable_counter += 1
        self.file.write(self.render_variable(False, name, index, options[0]))
        if options[1] > 1:
            self.variable_counter += 1
            self.file.write(self.render_variable(True, name, index, options[1]))

    @staticmethod
    def render_variable(rotation, cubie_type, index, options):
        index = str(index)

        string = 'begin_variable\n'
        if rotation:
            string += 'rotation_'.format(**locals())
        string += '{cubie_type}_{index}\n-1\n{options}\n'.format(**locals())
        for i in range(options):
            string += 'Atom '
            if rotation:
                string += 'rotated-towards('
            else:
                string += 'at('
            string += 'cubie_{cubie_type}_{index}, '.format(**locals())
            if rotation:
                string += 'direction_'
            else:
                string += 'location_{cubie_type}_'.format(**locals())
            string += '{i})\n'.format(**locals())
        string += 'end_variable\n'

        return string

    def write_mutex_groups(self, cubie_type, start):
        pieces, positions, rotations = self.cube.cubies[cubie_type]
        increment = (rotations > 1) + 1
        for i in range(pieces // positions):
            for j in range(positions):
                self.mutex_counter += 1
                self.file.write(self.render_mutex_group(positions, j, start, increment))
            start += positions * increment
        return start

    @staticmethod
    def render_mutex_group(number_of_cubies, position, start, increment):
        string = 'begin_mutex_group\n{number_of_cubies}\n'.format(**locals())
        for i in range(number_of_cubies):
            string += str(increment * i + start) + ' {position}\n'.format(**locals())
        string += 'end_mutex_group\n'

        return string

    def render_initial_state(self):
        string = ''
        count = 0
        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            for i in range(pieces // positions):
                for j in range(positions):
                    string += str(self.cube.get_position(count)) + '\n'
                    if rotations > 1:
                        string += str(self.cube.get_rotation(count)) + '\n'
                    count += 1
        return string

    def render_goal_state(self):
        string = ''
        count = 0
        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            for i in range(pieces // positions):
                for j in range(positions):
                    var = count // (1 + (rotations > 1))
                    val = self.goal_cube.get_position(var)
                    string += '{count} {val}\n'.format(**locals())
                    count += 1
                    if rotations > 1:
                        val = self.goal_cube.get_rotation(var)
                        string += '{count} {val}\n'.format(**locals())
                        count += 1
        return string

    def write_operator(self, effects, index, face, move):
        self.operator_counter += 1
        self.file.write(self.render_operator(effects, index, face, move))

    def calculate_effects_factor(self, cubie_type):
        return 1 + self.cube.cubies[cubie_type][2] * (self.cube.cubies[cubie_type][2] > 1)

    def render_operator(self, effects, index, face, move):
        extensions = ['', '2', '\'']
        extension = extensions[move - 1]
        string = 'begin_operator\n{face}{extension}_{index}\n0\n{effects}\n'.format(**locals())

        effect_counter = 0
        variable_counter = 0
        cube_variable_counter = 0
        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            step = 1 + (rotations > 1)
            if key == 'facewing':
                max_index = self.cube.size // 2 + 1
                for i in range(1, max_index):
                    for j in range(i + 1, max_index):
                        for position in range(positions):
                            if self.goal_cube.location_on_face(position, face, index, key, i, j):
                                variable = variable_counter + step * position
                                cube_variable = cube_variable_counter + position
                                self.goal_cube.turn_clockwise(face, index, move)
                                new_position = self.goal_cube.get_position(cube_variable)
                                self.goal_cube.turn_clockwise(face, index, -move)

                                for piece in range(pieces):
                                    effect_counter += 1
                                    var = variable_counter + step * piece
                                    string += '0 {var} {position} {new_position}\n'
            else:
                for i in range(1, pieces // positions + 1):
                    for position in range(positions):
                        if self.goal_cube.location_on_face(position, face, index, key, i, None):
                            variable = variable_counter + step * position
                            cube_variable = cube_variable_counter + position
                            self.goal_cube.turn_clockwise(face, index, move)
                            new_position = self.goal_cube.get_position(cube_variable)
                            self.goal_cube.turn_clockwise(face, index, -move)

                            for piece in range(pieces):
                                var_sas = variable_counter + step * piece

                                effect_counter += 1
                                string += '1 {var_sas} {position} {var_sas} -1 {new_position}\n'.format(**locals())

                                for rotation in range(rotations * (rotations > 1)):
                                    effect_counter += 1
                                    var_sas_2 = var_sas + 1
                                    string += '2 {var_sas} {position} {var_sas_2} '.format(**locals())
                                    rotation_value = self.goal_cube.get_rotation(cube_variable)
                                    string += '{rotation_value} {var_sas_2} -1 '.format(**locals())
                                    self.goal_cube.turn_clockwise(face, index, move)
                                    new_rotation_value = self.goal_cube.get_rotation(cube_variable)
                                    string += '{new_rotation_value}\n'.format(**locals())
                                    self.goal_cube.turn_clockwise(face, index, -move)
                                    self.goal_cube.rotate_single_cubie(cube_variable)
            variable_counter += step * pieces
            cube_variable_counter += pieces

        string += '1\nend_operator\n'

        if effect_counter != effects:
            print('Number of effects does not agree with written effects.')
            sys.exit(2)

        return string


class Combined(Format):
    def get_number_of_variables_of_cubie_type(self, cubie_type):
        return self.cube.cubies[cubie_type][0]

    def write_variable(self, cubie_type, index, options):
        self.variable_counter += 1
        self.file.write(self.render_variable(cubie_type, index, options))

    @staticmethod
    def render_variable(cubie_type, index, options):
        index = str(index)

        string = 'begin_variable\n'
        string += '{cubie_type}_{index}\n'.format(**locals())
        string += str(-1) + '\n'
        opts = options[0] * options[1]
        string += str(opts) + '\n'
        for i in range(opts):
            string += ('Atom at_and_rotated-towards(cubie_{cubie_type}_{index}, location_' + str(i // options[1]) +
                       ', directions_' + str(i % options[1]) + ')\n').format(**locals())
        string += 'end_variable\n'

        return string

    def write_mutex_groups(self, cubie_type, start):
        pieces, positions, rotations = self.cube.cubies[cubie_type]
        for i in range(pieces // positions):
            for j in range(positions):
                self.mutex_counter += 1
                self.file.write(self.render_mutex_group(pieces, rotations, j, start))
        return start + pieces

    @staticmethod
    def render_mutex_group(pieces, rotations, position, start):
        string = 'begin_mutex_group\n' + str(pieces * rotations) + '\n'
        for i in range(pieces):
            for j in range(rotations):
                string += str(i + start) + ' ' + str(position * rotations + j) + '\n'
        string += 'end_mutex_group\n'

        return string

    def render_initial_state(self):
        count = 0
        string = ''
        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            for i in range(pieces):
                pos = self.cube.get_position(count)
                rot = self.cube.get_rotation(count)
                string += str(pos * rotations + rot) + '\n'
                count += 1
        return string

    def render_goal_state(self):
        string = ''
        count = 0
        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            for i in range(pieces):
                pos = self.goal_cube.get_position(count)
                rot = self.goal_cube.get_rotation(count)
                string += str(count) + ' ' + str(pos * rotations + rot) + '\n'
                count += 1
        return string

    def calculate_effects_factor(self, cubie_type):
        return self.cube.cubies[cubie_type][2]

    def write_operator(self, effects, index, face, move):
        self.operator_counter += 1
        self.file.write(self.render_operator(effects, index, face, move))

    def render_operator(self, effects, index, face, move):
        extensions = ['', '2', '\'']
        extension = extensions[move - 1]
        string = 'begin_operator\n{face}{extension}_{index}\n0\n{effects}\n'.format(**locals())

        effect_counter = 0
        variable_counter = 0
        for key in self.cube.cubies.keys():
            pieces, positions, rotations = self.cube.cubies[key]
            step = 1 + (rotations > 1)
            if key == 'facewing':
                # TODO implement
                max_index = self.cube.size // 2 + 1
                for i in range(1, max_index):
                    for j in range(i + 1, max_index):
                        for position in range(positions):
                            print('case not implemented')
            else:
                for i in range(1, pieces // positions + 1):
                    for position in range(positions):
                        if self.goal_cube.location_on_face(position, face, index, key, i, None):
                            variable = variable_counter + position
                            for piece in range(pieces):
                                for rotation in range(rotations):
                                    var_sas = variable_counter + piece

                                    effect_counter += 1
                                    var = position * rotations + self.goal_cube.get_rotation(variable)
                                    string += '1 {var_sas} {var} {var_sas} -1 '.format(**locals())
                                    self.goal_cube.turn_clockwise(face, index, move)
                                    new_position = self.goal_cube.get_position(variable)
                                    var = new_position * rotations + self.goal_cube.get_rotation(variable)
                                    string += '{var}\n'.format(**locals())
                                    self.goal_cube.turn_clockwise(face, index, -move)
                                    self.goal_cube.rotate_single_cubie(variable)
            variable_counter += pieces
        string += '1\nend_operator\n'

        if effect_counter != effects:
            print('Number of effects does not agree with written effects.')
            sys.exit(2)

        return string
