#!/usr/bin/env python
# $Id: solid_pentominoes.py 646 2017-01-15 17:38:33Z goodger $

# Author: David Goodger <goodger@python.org>
# Copyright: (C) 1998-2016 by David J. Goodger
# License: GPL 2 (see __init__.py)

"""
Concrete solid pentomino puzzles.
"""

from puzzler.puzzles import Puzzle3D, Puzzle2D
from puzzler.puzzles.polycubes import SolidPentominoes
from puzzler.coordsys import Cartesian3D


class SolidPentominoes2x3x10(SolidPentominoes):

    """12 solutions"""

    height = 3
    width = 10
    depth = 2

    def customize_piece_data(self):
        self.piece_data['F'][-1]['flips'] = None

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for x_coords, x_aspect in self.pieces['X']:
            if not x_aspect.bounds[-1]: # get the one in the XY plane
                break
        for x in range(4):
            translated = x_aspect.translate((x, 0, 0))
            self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)

    transform_solution_matrix = Puzzle3D.swap_yz_transform


class SolidPentominoes2x5x6(SolidPentominoes):

    """264 solutions"""

    height = 5
    width = 6
    depth = 2

    @classmethod
    def components(cls):
        return (SolidPentominoes2x5x6A, SolidPentominoes2x5x6B)

    transform_solution_matrix = Puzzle3D.swap_yz_transform


class SolidPentominoes2x5x6A(SolidPentominoes2x5x6):

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for x_coords, x_aspect in self.pieces['X']:
            if not x_aspect.bounds[-1]: # get the one in the XY plane
                break
        for x in range(2):
            translated = x_aspect.translate((x, 0, 0))
            self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoes2x5x6B(SolidPentominoes2x5x6):

    """symmetry: X in center; remove flip of F"""

    def customize_piece_data(self):
        self.piece_data['F'][-1]['flips'] = None

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for x_coords, x_aspect in self.pieces['X']:
            if not x_aspect.bounds[-1]: # get the one in the XY plane
                break
        for x in range(2):
            translated = x_aspect.translate((x, 1, 0))
            self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoes3x4x5(SolidPentominoes):

    """
    3940 solutions
    """

    height = 4
    width = 5
    depth = 3

    check_for_duplicates = True

    duplicate_conditions = ({'x_reversed': True},
                            {'z_reversed': True},
                            {'x_reversed': True, 'z_reversed': True})

    def build_matrix_i(self, y_range, z_range):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['I']:
            if aspect.bounds[0]: # get the one on the X axis
                break
        for z in z_range:
            for y in y_range:
                translated = aspect.translate((0, y, z))
                self.build_matrix_row('I', translated)
        keys.remove('I')
        return keys

    def build_matrix(self):
        keys = self.build_matrix_i((0, 1), (0, 1))
        self.build_regular_matrix(keys)

    transform_solution_matrix = Puzzle3D.swap_yz_transform


class SolidPentominoesRing(SolidPentominoes):

    check_for_duplicates = True

    duplicate_conditions = ({'x_reversed': True},
                            {'y_reversed': True},
                            {'z_reversed': True},
                            {'x_reversed': True, 'y_reversed': True},
                            {'x_reversed': True, 'z_reversed': True},
                            {'y_reversed': True, 'z_reversed': True},
                            {'x_reversed': True,
                             'y_reversed': True,
                             'z_reversed': True})

    def coordinates(self):
        for z in range(self.depth):
            for y in range(self.height):
                for x in range(self.width):
                    if ( (x == 0) or (x == self.width - 1)
                         or (z == 0) or (z == self.depth - 1)):
                        yield (x, y, z)

    def build_matrix_header(self):
        headers = []
        for i, key in enumerate(sorted(self.pieces.keys())):
            self.matrix_columns[key] = i
            headers.append(key)
        for (x, y, z) in self.coordinates():
            header = '%0*i,%0*i,%0*i' % (
                self.x_width, x, self.y_width, y, self.z_width, z)
            self.matrix_columns[header] = len(headers)
            headers.append(header)
        self.matrix.append(tuple(headers))

    def build_regular_matrix(self, keys):
        for key in keys:
            for coords, aspect in self.pieces[key]:
                for z in range(self.depth - aspect.bounds[2]):
                    for y in range(self.height - aspect.bounds[1]):
                        for x in range(self.width - aspect.bounds[0]):
                            translated = aspect.translate((x, y, z))
                            if translated.issubset(self.solution_coords):
                                self.build_matrix_row(key, translated)

    def format_solution(self, solution, normalized=True,
                        x_reversed=False, y_reversed=False, z_reversed=False):
        order_functions = (lambda x: x, reversed)
        x_reversed_fn = order_functions[x_reversed]
        y_reversed_fn = order_functions[1 - y_reversed] # reversed by default
        z_reversed_fn = order_functions[z_reversed]
        z_unreversed_fn = order_functions[1 - z_reversed]
        s_matrix = self.build_solution_matrix(solution)
        lines = []
        left_index = [0, -1][x_reversed]
        right_index = -1 - left_index
        for y in y_reversed_fn(range(self.height)):
            back = ' '.join(x_reversed_fn(s_matrix[0][y]))
            front = ' '.join(x_reversed_fn(s_matrix[-1][y]))
            if z_reversed:
                back, front = front, back
            left = ' '.join(s_matrix[z][y][left_index]
                            for z in z_reversed_fn(range(self.depth)))
            right = ' '.join(
                s_matrix[z][y][right_index]
                for z in z_unreversed_fn(range(self.depth)))
            lines.append(('%s    %s    %s    %s'
                          % (left, front, right, back)).rstrip())
        return '\n'.join(lines)


class SolidPentominoes3x3x9Ring(SolidPentominoesRing):

    """3 solutions"""

    width = 9
    height = 3
    depth = 3

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['X']:
            if aspect.bounds[0] == 0:   # YZ plane
                self.build_matrix_row('X', aspect)
            if aspect.bounds[2] == 0:   # XY plane
                for x in range(4):
                    translated = aspect.translate((x, 0, 0))
                    self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoes3x4x8Ring(SolidPentominoesRing):

    """0 solutions"""

    width = 8
    height = 3
    depth = 4


class SolidPentominoes3x5x7Ring(SolidPentominoesRing):

    """1 solution"""

    width = 7
    height = 3
    depth = 5

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['X']:
            if aspect.bounds[0] == 0:   # YZ plane
                for z in range(2):
                    translated = aspect.translate((0, 0, z))
                    self.build_matrix_row('X', translated)
            if aspect.bounds[2] == 0:   # XY plane
                for x in range(3):
                    translated = aspect.translate((x, 0, 0))
                    self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoes3x6x6Ring(SolidPentominoesRing):

    """0 solutions"""

    width = 6
    height = 3
    depth = 6


class SolidPentominoes5x3x5Ring(SolidPentominoesRing):

    """186 solutions"""

    width = 5
    height = 5
    depth = 3

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['X']:
            if aspect.bounds[0] == 0:   # YZ plane
                for y in range(2):
                    translated = aspect.translate((0, y, 0))
                    self.build_matrix_row('X', translated)
            if aspect.bounds[2] == 0:   # XY plane
                for y in range(2):
                    for x in range(2):
                        translated = aspect.translate((x, y, 0))
                        self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoes5x4x4Ring(SolidPentominoesRing):

    """0 solutions"""

    width = 4
    height = 5
    depth = 4


class SolidPentominoes6x3x4Ring(SolidPentominoesRing):

    """46 solutions"""

    width = 4
    height = 6
    depth = 3

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['X']:
            if aspect.bounds[0] == 0 or aspect.bounds[2] == 0: # YZ or XY plane
                for y in range(2):
                    translated = aspect.translate((0, y, 0))
                    self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoes4x4x8Crystal(SolidPentominoes):

    """251 solutions"""

    width = 4
    height = 8
    depth = 4

    def customize_piece_data(self):
        self.piece_data['F'][-1]['flips'] = None

    def coordinates(self):
        for z in range(self.depth):
            for y in range(self.height):
                for x in range(self.width):
                    total = x + y + z
                    xz_total = x + z
                    if total < 8 and (y > 3 or xz_total < 4):
                        yield (x, y, z)


class SolidPentominoes5x5x4Steps(SolidPentominoes):

    """137 solutions"""

    width = 4
    height = 5
    depth = 5

    check_for_duplicates = True

    duplicate_conditions = ({'x_reversed': True},
                            {'yz_swapped': True},
                            {'x_reversed': True,
                             'yz_swapped': True},)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['flips'] = None

    def coordinates(self):
        for z in range(self.depth):
            for y in range(self.height):
                for x in range(self.width):
                    total = x + y + z
                    if y + z < self.height:
                        yield (x, y, z)


class SolidPentominoes4x4x6Steps(SolidPentominoes5x5x4Steps):

    """279 solutions"""

    width = 6
    height = 4
    depth = 4


class SolidPentominoes3x3x10Steps(SolidPentominoes5x5x4Steps):

    """9 solutions"""

    width = 10
    height = 3
    depth = 3


class SolidPentominoes3x3x12Tower(SolidPentominoes):

    """0 solutions"""

    width = 3
    height = 12
    depth = 3

    def coordinates(self):
        for y in range(self.height):
            for x, z in ((0,1), (1,0), (1,1), (1,2), (2,1)):
                yield (x, y, z)


class SolidPentominoes3x5x7Slope(SolidPentominoes):

    """ solutions"""

    width = 5
    height = 7
    depth = 3

    def coordinates(self):
        for z in range(self.depth):
            for y in range(self.height):
                for x in range(self.width):
                    if x + y + z < self.height:
                        yield (x, y, z)


class SolidPentominoes6x6x6Crystal1(SolidPentominoes):

    """2 solutions"""

    width = 6
    height = 6
    depth = 6

    extras = ((1,1,4), (1,4,1), (4,1,1), (2,2,2))

    def customize_piece_data(self):
        self.piece_data['P'][-1]['flips'] = None
        self.piece_data['P'][-1]['axes'] = None

    def coordinates(self):
        for z in range(self.depth):
            for y in range(self.height):
                for x in range(self.width):
                    if x + y + z < 6:
                        yield (x, y, z)
        for coord in self.extras:
            yield coord


class SolidPentominoes6x6x6Crystal2(SolidPentominoes6x6x6Crystal1):

    """1 solution"""

    extras = ((1,2,3), (2,2,2), (3,2,1), (1,4,1))

    check_for_duplicates = True

    duplicate_conditions = ({'xz_swapped': True},)

    def customize_piece_data(self):
        return


class SolidPentominoes6x6x6Crystal3(SolidPentominoes6x6x6Crystal1):

    """9 solutions"""

    extras = ((1,2,3), (3,1,2), (2,3,1), (2,2,2))

    def customize_piece_data(self):
        self.piece_data['P'][-1]['axes'] = None


class SolidPentominoes6x6x6Crystal4(SolidPentominoes6x6x6Crystal1):

    """2 solutions"""

    extras = ((0,5,1), (1,4,1), (1,5,0), (1,5,1))

    check_for_duplicates = True

    duplicate_conditions = ({'xz_swapped': True},)

    def customize_piece_data(self):
        return


class SolidPentominoes6x6x6Crystal5(SolidPentominoes6x6x6Crystal4):

    """4 solutions"""

    extras = ((1,3,2), (2,2,2), (2,3,1), (2,3,2))


class SolidPentominoes6x6x6CrystalX1(SolidPentominoes6x6x6Crystal1):

    """0 solutions"""

    extras = ((1,1,4), (2,1,3), (3,1,2), (4,1,1))

    def customize_piece_data(self):
        return


class SolidPentominoes6x6x6CrystalX2(SolidPentominoes6x6x6Crystal1):

    """0 solutions"""

    extras = ((3,3,0), (3,0,3), (0,3,3), (2,2,2))

    def customize_piece_data(self):
        return


class SolidPentominoes6x6x6CrystalX3(SolidPentominoes6x6x6Crystal4):

    """0 solutions"""

    extras = ((2,1,3), (3,0,3), (3,1,2), (3,1,3))


class SolidPentominoes6x6x6CrystalX4(SolidPentominoes6x6x6Crystal4):

    """0 solutions"""

    extras = ((1,3,2), (2,2,2), (2,2,1), (1,4,1))


class SolidPentominoes7x7x7Crystal(SolidPentominoes):

    """0 solutions"""

    width = 7
    height = 7
    depth = 7

    def coordinates(self):
        for z in range(self.depth):
            for y in range(self.height):
                for x in range(self.width):
                    if x + y + z < 6:
                        yield (x, y, z)
        for x, y, z in ((0,0,6), (0,6,0), (6,0,0), (2,2,2)):
            yield (x, y, z)


class SolidPentominoesTower1(SolidPentominoes):

    """
    27 solutions

    Design by David Klarner
    """

    width = 3
    height = 8
    depth = 3

    extras = ((0,7,1), (1,7,0), (1,7,2), (2,7,1))

    def coordinates(self):
        coords = (
            set(self.coordinates_cuboid(3, 7, 3))
            - set(self.coordinates_cuboid(1, 7, 1, offset=(1,0,1))))
        coords.update(set(self.coordinate_offset(x, y, z, None)
                          for x, y, z in self.extras))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['flips'] = None
        self.piece_data['F'][-1]['axes'] = None

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['F']:
            for y in range(6):
                translated = aspect.translate((0, y, 0))
                if translated.issubset(self.solution_coords):
                    self.build_matrix_row('F', translated)
        keys.remove('F')
        self.build_regular_matrix(keys)


class SolidPentominoesTower2(SolidPentominoesTower1):

    """
    10 solutions

    Design by David Klarner
    """

    extras = ((0,7,0), (0,7,2), (2,7,0), (2,7,2))


class SolidPentominoesTower3(SolidPentominoesTower1):

    """
    many solutions

    Design by Leslie Young via Kadon's Quintillions booklet
    """

    extras = ((0,6,1), (1,6,0), (1,6,1), (1,6,2), (2,6,1), (1,7,1))

    def coordinates(self):
        coords = set(self.coordinates_cuboid(3, 6, 3))
        coords.update(set(self.coordinate_offset(x, y, z, None)
                          for x, y, z in self.extras))
        return sorted(coords)

    def customize_piece_data(self):
        pass

    def build_matrix(self):
        SolidPentominoes.build_matrix(self)


class SolidPentominoesOhnosBlock(SolidPentominoes):

    """
    1 solution

    Design by Yoshio Ohno, from Kadon's Quintillions booklet
    (title: 'A Gift From Japan')
    """

    width = 6
    height = 6
    depth = 3

    extras = ((0,2), (2,5), (3,0), (5,3))

    def coordinates(self):
        coords = set(self.coordinates_cuboid(4, 4, 3, offset=(1,1,0)))
        coords.update(set(self.coordinate_offset(x, y, z, None)
                          for x, y in self.extras for z in range(3)))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['L'][-1]['rotations'] = None
        self.piece_data['L'][-1]['flips'] = None


class SolidPentominoesSpinnerBlock(SolidPentominoesOhnosBlock):

    """42 solutions"""

    extras = ((0,1), (1,5), (4,0), (5,4))

    def customize_piece_data(self):
        pass

    check_for_duplicates = True
    duplicate_conditions = ({'z_reversed': True},)

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['L']
        self.aspects['L'] = self.make_aspects(
            data, flips=None, axes=None, rotations=None)
        self.aspects['L'].update(self.make_aspects(
            data, flips=None, axes=(1,), rotations=None))
        names.remove('L')
        self.build_regular_aspects(names)


class SolidPentominoesCornerWalls(SolidPentominoes):

    """253 solutions"""

    width = 5
    height = 5
    depth = 5

    def coordinates(self):
        coords = (
            set(self.coordinates_cuboid(5, 5, 5))
            - set(self.coordinates_cuboid(4, 4, 4, offset=(1,1,1)))
            - set(((0,0,0),)))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['axes'] = None
        self.piece_data['F'][-1]['flips'] = None


class SolidPentominoesCornerPiece(SolidPentominoes):

    """
    70 solutions

    Design from Kadon's Quintillions booklet
    """

    width = 5
    height = 6
    depth = 5

    def coordinates(self):
        coords = set(
            list(self.coordinates_cuboid(5, 6, 1))
            + list(self.coordinates_cuboid(1, 6, 5))
            + list(self.coordinates_cuboid(1, 6, 1, offset=(1,0,1))))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['rotations'] = (0, 1)
        self.piece_data['F'][-1]['flips'] = None


class SolidPentominoesThreeWalls(SolidPentominoes):

    """
    90 solutions

    Design from Kadon's Quintillions booklet
    """

    width = 7
    height = 6
    depth = 4

    check_for_duplicates = True
    duplicate_conditions = ({'y_reversed': True},)

    def coordinates(self):
        coords = set(
            list(self.coordinates_cuboid(7, 6, 1))
            + list(self.coordinates_cuboid(1, 6, 3, offset=(3,0,1))))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['rotations'] = (0, 1)
        self.piece_data['F'][-1]['flips'] = None


class SolidPentominoesEmptyBottle(SolidPentominoes):

    """
    49 solutions

    Design from Kadon's Quintillions booklet
    """

    width = 3
    height = 9
    depth = 3

    def coordinates(self):
        coords = (
            set(list(self.coordinates_cuboid(3, 7, 3))
                + list(self.coordinates_cuboid(1, 2, 1, offset=(1,7,1))))
            - set(self.coordinates_cuboid(1, 5, 1, offset=(1,1,1))))
        return sorted(coords)

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['F']
        self.aspects['F'] = self.make_aspects(
            data, flips=None, axes=None)
        self.aspects['F'].update(self.make_aspects(
            data, flips=None, axes=(1,), rotations=None))
        names.remove('F')
        self.build_regular_aspects(names)

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['F']:
            if aspect.bounds[-1]:       # the one in the XZ plane
                translated = aspect.translate((0, 0, 0))
                self.build_matrix_row('F', translated)
            else:                       # the ones in the XY plane
                for y in range(5):
                    translated = aspect.translate((0, y, 0))
                    self.build_matrix_row('F', translated)
        keys.remove('F')
        self.build_regular_matrix(keys)


class SolidPentominoesCondominiumB(SolidPentominoes):

    """
    1 solution

    Design from Kadon's Quintillions booklet
    """

    width = 3
    height = 9
    depth = 4

    transform_solution_matrix = Puzzle3D.cycle_xyz_transform

    def coordinates(self):
        coords = set(
            list(self.coordinates_cuboid(1, 5, 4, offset=(0,4,0)))
            + list(self.coordinates_cuboid(1, 5, 4, offset=(1,2,0)))
            + list(self.coordinates_cuboid(1, 5, 4, offset=(2,0,0))))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['rotations'] = (0, 1)
        self.piece_data['F'][-1]['flips'] = None


class SolidPentominoesCrossBlock1(SolidPentominoes):

    """92 solutions"""

    width = 4
    height = 4
    depth = 5

    transform_solution_matrix = Puzzle3D.swap_yz_transform

    @classmethod
    def components(cls):
        return (SolidPentominoesCrossBlock1A, SolidPentominoesCrossBlock1B)

    def coordinates(self):
        coords = set(
            self.coordinate_offset(x, y, z, None)
            for x, y in Puzzle2D.coordinates_aztec_diamond(2)
            for z in range(5))
        return sorted(coords)


class SolidPentominoesCrossBlock1A(SolidPentominoesCrossBlock1):

    """Limit the F pentomino to asymmetrical positions."""

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['F']
        self.aspects['F'] = self.make_aspects(
            data, flips=None, rotations=None, axes=None)
        self.aspects['F'].update(self.make_aspects(
            data, flips=None, axes=(1,), rotations=(0, 1)))
        names.remove('F')
        self.build_regular_aspects(names)

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['F']:
            if aspect.bounds[-1]:       # the ones in the XZ plane
                for x in range(2):
                    for z in range(3):
                        translated = aspect.translate((x, 1, z))
                        self.build_matrix_row('F', translated)
            else:                       # the one in the XY plane
                for x, y in ((0,1), (1,0), (1,1)):
                    for z in range(2):
                        translated = aspect.translate((x, y, z))
                        self.build_matrix_row('F', translated)
        keys.remove('F')
        self.build_regular_matrix(keys)


class SolidPentominoesCrossBlock1B(SolidPentominoesCrossBlock1):

    """
    Limit the F pentomino to symmetrical positions, and limit the X pentomino
    to one half of the puzzle.
    """

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['F']
        self.aspects['F'] = self.make_aspects(
            data, flips=None, rotations=None, axes=None)
        names.remove('F')
        self.build_regular_aspects(names)

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        assert len(self.pieces['F']) == 1
        coords, aspect = self.pieces['F'][0]
        for x, y in ((0,1), (1,0), (1,1)):
            translated = aspect.translate((x, y, 2))
            self.build_matrix_row('F', translated)
        keys.remove('F')
        for coords, aspect in self.pieces['X']:
            if aspect.bounds[-1]:       # the ones in the XZ & YZ planes
                for x in range(3):
                    for y in range(3):
                        translated = aspect.translate((x, y, 0))
                        if translated.issubset(self.solution_coords):
                            self.build_matrix_row('X', translated)
            else:                       # the one in the XY plane
                for x in range(2):
                    for y in range(2):
                        for z in range(3):
                            translated = aspect.translate((x, y, z))
                            if translated.issubset(self.solution_coords):
                                self.build_matrix_row('X', translated)
        keys.remove('X')
        self.build_regular_matrix(keys)


class SolidPentominoesCrossBlock2(SolidPentominoes):

    """0 solutions"""

    width = 5
    height = 5
    depth = 3

    transform_solution_matrix = Puzzle3D.swap_yz_transform

    @classmethod
    def components(cls):
        return (
            SolidPentominoesCrossBlock2A, SolidPentominoesCrossBlock2B,
            SolidPentominoesCrossBlock2C)

    def coordinates(self):
        coords = (
            set(list(self.coordinates_cuboid(5, 3, 3, offset=(0,1,0)))
                + list(self.coordinates_cuboid(3, 5, 3, offset=(1,0,0))))
            - set(self.coordinates_cuboid(1, 1, 3, offset=(2,2,0))))
        return sorted(coords)

class SolidPentominoesCrossBlock2A(SolidPentominoesCrossBlock2):

    """Limit the F pentomino to asymmetrical positions."""

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['F']
        self.aspects['F'] = self.make_aspects(
            data, flips=None, rotations=None, axes=None)
        self.aspects['F'].update(self.make_aspects(
            data, flips=None, axes=(1,), rotations=(0, 1)))
        names.remove('F')
        self.build_regular_aspects(names)

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        for coords, aspect in self.pieces['F']:
            if aspect.bounds[-1]:       # the ones in the XZ plane
                for x in range(3):
                    for y in range(2):
                        translated = aspect.translate((x, y, 0))
                        if translated.issubset(self.solution_coords):
                            self.build_matrix_row('F', translated)
            else:                       # the one in the XY plane
                for x, y in ((0,2), (2,0), (2,1)):
                    translated = aspect.translate((x, y, 0))
                    self.build_matrix_row('F', translated)
        keys.remove('F')
        self.build_regular_matrix(keys)


class SolidPentominoesCrossBlock2B(SolidPentominoesCrossBlock2):

    """
    Limit the F pentomino to symmetrical positions, and limit the I pentomino
    to one half of the puzzle.
    """

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['F']
        self.aspects['F'] = self.make_aspects(
            data, flips=None, rotations=None, axes=None)
        names.remove('F')
        self.build_regular_aspects(names)

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        assert len(self.pieces['F']) == 1
        coords, aspect = self.pieces['F'][0]
        for x, y in ((0,2), (2,0), (2,1)):
            translated = aspect.translate((x, y, 1))
            self.build_matrix_row('F', translated)
        keys.remove('F')
        for coords, aspect in self.pieces['I']:
            if not aspect.bounds[-1]:   # the ones in the XY plane
                for x in range(5):
                    for y in range(5):
                        translated = aspect.translate((x, y, 0))
                        if translated.issubset(self.solution_coords):
                            self.build_matrix_row('I', translated)
        keys.remove('I')
        self.build_regular_matrix(keys)


class SolidPentominoesCrossBlock2C(SolidPentominoesCrossBlock2):

    """
    Limit the F pentomino to symmetrical positions, and limit the I pentomino
    to the central layer of the puzzle.
    """

    def build_aspects(self):
        names = sorted(self.piece_data.keys())
        data, kwargs = self.piece_data['F']
        self.aspects['F'] = self.make_aspects(
            data, flips=None, rotations=None, axes=None)
        names.remove('F')
        self.build_regular_aspects(names)

    def build_matrix(self):
        keys = sorted(self.pieces.keys())
        assert len(self.pieces['F']) == 1
        coords, aspect = self.pieces['F'][0]
        for x, y in ((0,2), (2,0), (2,1)):
            translated = aspect.translate((x, y, 1))
            self.build_matrix_row('F', translated)
        keys.remove('F')
        for coords, aspect in self.pieces['I']:
            if not aspect.bounds[-1]:   # the ones in the XY plane
                for x in range(5):
                    for y in range(5):
                        translated = aspect.translate((x, y, 1))
                        if translated.issubset(self.solution_coords):
                            self.build_matrix_row('I', translated)
        keys.remove('I')
        self.build_regular_matrix(keys)


class SolidPentominoesCrossBlock3(SolidPentominoes):

    """10 solutions"""

    width = 6
    height = 6
    depth = 2

    #transform_solution_matrix = Puzzle3D.swap_yz_transform

    def coordinates(self):
        coords = (
            set(list(self.coordinates_cuboid(6, 4, 2, offset=(0,1,0)))
                + list(self.coordinates_cuboid(4, 6, 2, offset=(1,0,0))))
            - set(self.coordinates_cuboid(2, 2, 1, offset=(2,2,1))))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['F'][-1]['rotations'] = None
        self.piece_data['F'][-1]['flips'] = None


class SolidPentominoesCrossBlock_x1(SolidPentominoes):

    """0 solutions"""

    width = 6
    height = 6
    depth = 3

    def coordinates(self):
        coords = set(
            list(self.coordinates_cuboid(6, 2, 3, offset=(0,2,0)))
            + list(self.coordinates_cuboid(2, 6, 3, offset=(2,0,0))))
        return sorted(coords)


class SolidPentominoesOpenBox8x3x3(SolidPentominoes):

    """many solutions"""

    width = 8
    height = 3
    depth = 3

    transform_solution_matrix = Puzzle3D.swap_yz_transform

    def coordinates(self):
        return self.coordinates_open_box(self.width, self.height, self.depth)


class SolidPentominoesOpenBox6x3x4(SolidPentominoesOpenBox8x3x3):

    """many solutions"""

    width = 6
    height = 3
    depth = 4


class SolidPentominoes5x5x5QuarterPyramid(SolidPentominoes):

    """
    55-cube shape, so 11 pieces are used and one piece must be omitted.

    320 solutions:

    * 11 omitting F
    * 7 omitting I
    * none omitting L
    * 4 omitting N
    * none omitting P
    * 22 omitting T
    * 4 omitting U
    * 12 omitting V
    * 10 omitting W
    * 223 omitting X
    * 2 omitting Y
    * 25 omitting Z
    """

    width = 9
    height = 5
    depth = 5

    # These 9 coordinates form a minimal cover for all 12 pentominoes
    # (with Z=0 for solid pentominoes):
    omitted_piece_coordinates = (
        (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (1,3), (1,4), (2,2))

    # Since there are only 9 coordinates for the omitted piece, only 1 piece
    # can fit.  By setting these 9 coordinates as secondary columns, the extra
    # 4 coordinates are ignored.
    secondary_columns = 9

    # These are the fixed positions for omitted pieces, to prevent duplicates.
    omitted_piece_positions = {
        'F': ((0,1), (1,0), (1,1), (1,2), (2,2)),
        'I': ((1,0), (1,1), (1,2), (1,3), (1,4)),
        'L': ((0,0), (1,0), (1,1), (1,2), (1,3)),
        'N': ((0,0), (0,1), (0,2), (1,2), (1,3)),
        'P': ((0,0), (0,1), (0,2), (1,0), (1,1)),
        'T': ((0,2), (1,0), (1,1), (1,2), (2,2)),
        'U': ((0,0), (0,1), (0,2), (1,0), (1,2)),
        'V': ((0,0), (0,1), (0,2), (1,2), (2,2)),
        'W': ((0,0), (0,1), (1,1), (1,2), (2,2)),
        'X': ((0,2), (1,1), (1,2), (1,3), (2,2)),
        'Y': ((0,2), (1,0), (1,1), (1,2), (1,3)),
        'Z': ((0,0), (1,0), (1,1), (1,2), (2,2)),}

    omitted_cover_offset = (6,0,0)

    transform_solution_matrix = Puzzle3D.cycle_xyz_transform

    def coordinates(self):
        coords = set()
        for i in range(5):
            coords.update(
                set(self.coordinates_cuboid(5 - i, 5 - i, 1, offset=(0,0,i))))
        self.regular_solution_coords = coords.copy()
        dx, dy, dz = self.omitted_cover_offset
        for (x, y) in self.omitted_piece_coordinates:
            coords.add((x + dx, y + dy, dz))
        return sorted(coords)

    def build_matrix(self):
        self.build_rows_for_omitted_pieces()
        self.build_regular_matrix(
            sorted(self.piece_data.keys()),
            solution_coords=self.regular_solution_coords)

    def build_rows_for_omitted_pieces(self):
        #import pdb ; pdb.set_trace()
        dx, dy, dz = self.omitted_cover_offset
        for key, coords in self.omitted_piece_positions.items():
            coords3d = [(x + dx, y + dy, dz) for (x, y) in coords]
            self.build_matrix_row(key, coords3d)

    def build_aspects(self):
        """
        To eliminate duplicates from symmetry, limit the P pentomino to:

        * the XY plane, where flips are disallowed; and
        * the XZ plane (full freedom).
        """
        all_pieces_but_P = sorted(self.piece_data.keys())
        all_pieces_but_P.remove('P')
        self.build_regular_aspects(all_pieces_but_P)
        data, kwargs = self.piece_data['P']
        self.aspects['P'] = self.make_aspects(data, flips=None, axes=(2,))
        self.aspects['P'].update(self.make_aspects(data, axes=(1,)))
        self.pieces['P'] = tuple(
            sorted((tuple(sorted(aspect)), aspect)
                   for aspect in self.aspects['P']))


class SolidPentominoesCrossTower(SolidPentominoes):

    """
    7 solutions

    Design from `Thimo Rosenkranz's pentoma.de <http://www.pentoma.de>`_.
    """

    width = 5
    height = 5
    depth = 8

    transform_solution_matrix = Puzzle3D.cycle_xyz_transform

    check_for_duplicates = True

    duplicate_conditions = ({'x_reversed': True},)

    def coordinates(self):
        coords = set(
            list(self.coordinates_cuboid(5, 1, 6, offset=(0,2,0)))
            + list(self.coordinates_cuboid(1, 5, 6, offset=(2,0,0)))
            + [Cartesian3D(coord)
               for coord in ((1,2,6), (2,1,6), (2,2,6), (2,3,6), (3,2,6),
                             (2,2,7))])
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['P'][-1]['flips'] = None
        self.piece_data['P'][-1]['axes'] = (0,)


class SolidPentominoesInfinityTower(SolidPentominoes):

    """
    1 solution

    Design from `Thimo Rosenkranz's pentoma.de <http://www.pentoma.de>`_.
    """

    width = 5
    height = 5
    depth = 4

    transform_solution_matrix = Puzzle3D.cycle_xyz_transform

    def coordinates(self):
        coords = set(
            list(self.coordinates_ring_wall(3, 3, 4, offset=(0,2,0)))
            + list(self.coordinates_ring_wall(3, 3, 4, offset=(2,0,0))))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['P'][-1]['flips'] = None
        self.piece_data['P'][-1]['axes'] = (0,)
        self.piece_data['P'][-1]['rotations'] = (0, 1)


class SolidPentominoesSquareTower1(SolidPentominoes):

    """
    1 solution

    Design from `Thimo Rosenkranz's pentoma.de <http://www.pentoma.de>`_.
    """

    width = 5
    height = 5
    depth = 5

    transform_solution_matrix = Puzzle3D.cycle_xyz_transform

    check_for_duplicates = True

    duplicate_conditions = ({'x_reversed': True},)

    _square_offset = (1,1,0)
    _offsets = ((0,2), (2,0), (2,4), (4,2))

    def coordinates(self):
        coords = set(
            self.coordinates_ring_wall(3, 3, 5, offset=self._square_offset))
        for (x, y) in self._offsets:
            coords.update(set(self.coordinates_cuboid(1, 1, 5, offset=(x,y,0))))
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['P'][-1]['flips'] = None
        self.piece_data['P'][-1]['axes'] = (0,)
        self.piece_data['P'][-1]['rotations'] = (0, 1)


class SolidPentominoesSquareTower2(SolidPentominoesSquareTower1):

    """
    54 solutions

    Design from `Thimo Rosenkranz's pentoma.de <http://www.pentoma.de>`_.
    """

    _offsets = ((0,3), (1,4), (3,0), (4,1))

    check_for_duplicates = False


class SolidPentominoesSquareTower3(SolidPentominoesSquareTower2):

    """27 solutions"""

    _offsets = ((0,3), (1,0), (3,4), (4,1))


class SolidPentominoesSquareTower4(SolidPentominoesSquareTower2):

    """8 solutions"""

    _offsets = ((0,3), (0,4), (4,0), (4,1))

    def customize_piece_data(self):
        self.piece_data['P'][-1]['flips'] = None
        self.piece_data['P'][-1]['rotations'] = (0, 1)


class SolidPentominoesSquareTower5(SolidPentominoesSquareTower2):

    """106 solutions"""

    width = 3
    height = 7

    _square_offset = (0,2,0)
    _offsets = ((0,5), (0,6), (2,0), (2,1))

    def customize_piece_data(self):
        self.piece_data['P'][-1]['flips'] = None
        self.piece_data['P'][-1]['rotations'] = (0, 1)


class SolidPentominoesSquareTower6(SolidPentominoesSquareTower2):

    """154 solutions"""

    _square_offset = (0,0,0)
    _offsets = ((0,3), (0,4), (3,0), (4,0))

    def customize_piece_data(self):
        self.piece_data['P'][-1]['axes'] = (0,)
        self.piece_data['P'][-1]['flips'] = None


class SolidPentominoesStairstepWalls1(SolidPentominoes):

    """34 solutions"""

    width = 8
    height = 8
    depth = 6

    transform_solution_matrix = Puzzle3D.cycle_xyz_transform

    holes = set()

    def coordinates(self):
        coords = set()
        for i in range(self.depth):
            coords.update(
                set(self.coordinates_cuboid(1, 8-i, 1, offset=(0,0,i))))
            coords.update(
                set(self.coordinates_cuboid(8-i, 1, 1, offset=(0,0,i))))
        coords -= self.holes
        return sorted(coords)

    def customize_piece_data(self):
        self.piece_data['P'][-1]['axes'] = (0,)


class SolidPentominoesStairstepWalls2(SolidPentominoesStairstepWalls1):

    """1 solution"""

    depth = 8

    holes = set(
        list(SolidPentominoes.coordinates_cuboid(1, 2, 1, offset=(0,1,0)))
        + list(SolidPentominoes.coordinates_cuboid(2, 1, 1, offset=(1,0,0))))


class SolidPentominoesStairstepWalls_x3(SolidPentominoesStairstepWalls1):

    """0 solutions"""

    depth = 7

    holes = set(
        list(SolidPentominoes.coordinates_cuboid(1, 2, 1, offset=(0,2,0)))
        + list(SolidPentominoes.coordinates_cuboid(2, 1, 1, offset=(2,0,0))))

    holes = set(
        list(SolidPentominoes.coordinates_cuboid(1, 2, 1, offset=(0,3,0)))
        + list(SolidPentominoes.coordinates_cuboid(2, 1, 1, offset=(3,0,0))))

    holes = set(
        list(SolidPentominoes.coordinates_cuboid(1, 2, 1, offset=(0,4,0)))
        + list(SolidPentominoes.coordinates_cuboid(2, 1, 1, offset=(4,0,0))))

    holes = set(SolidPentominoes.coordinates_cuboid(2, 2, 1))


class SolidPentominoes5x5x4SteppedPyramid(SolidPentominoes):

    """
    55 solutions

    Suggested by Colin Lacy.
    """

    height = 5
    width = 5
    depth = 4

    transform_solution_matrix = Puzzle3D.swap_yz_transform

    check_for_duplicates = True

    duplicate_conditions = ({'x_reversed': True},)

    def coordinates(self):
        coords = set(
            list(self.coordinates_cuboid(5, 5, 2))
            + list(self.coordinates_cuboid(3, 3, 1, offset=(1,1,2)))
            + list(self.coordinates_cuboid(1, 1, 1, offset=(2,2,3))))
        return sorted(coords)

    def build_matrix(self):
        """
        In all solutions the 'I' piece is positioned at an edge of the square
        base. Restrict the 'I' piece to only one edge to reduce the duplicate
        solutions 4-fold. The x_reversed duplicate condition check eliminates
        the remaining duplicates.
        """
        keys = sorted(self.pieces.keys())
        # Choose the I aspect along the X axis:
        coords, aspect = self.pieces['I'][-1]
        for z in range(2):
            translated = aspect.translate((0, 0, z))
            self.build_matrix_row('I', translated)
        keys.remove('I')
        self.build_regular_matrix(keys)