export CardinalDirection, N, E, S, W, CARDINAL_DIRECTIONS
export opposite
export rotation, GridCell
export get_edge, set_edge!, direction_for_edge
export Grid, new_grid, get_neighboring_edge, fit_piece

Puzzle pieces are assembled into a grid when the puzzle is being put together. That grid has some number of rows and columns. For a given row and column the grid has a cell which can be associated with the puzzle piece that belongs there.

The grid has three kinds of cells: corner, edge, and middle. If a grid has R rows and C columns, it will have R × C cells. No matter the size of the grid, it will have 4 corner cells. The number of edge cells is 2 × (R - 2) + 2 * (C - 2). The remaining (R - 2) × (C - 2) cells are middle cells.

Each cell has four neighbors, one in each of the cardinal compass directions.

"""
    GridCell(::AbstractPuzzlePiece, rotation::Int)

A GridCell is the container for a puzzle piece in a puzzle grid.
"""
struct GridCell
    row::Int
    col::Int
    puzzle_piece
    rotation::Int

    GridCell(row, col, piece, rotation) =
        new(row, col, piece, mod(rotation, 4))
end

ImmutablePuzzlePiece(cell::GridCell) =
    ImmutablePuzzlePiece(cell.puzzle_piece)


const Grid = Array{Union{Missing, GridCell}, 2}

"""
    new_grid(number_of_rows, number_of_columns)::Grid

Creates an empty puzzle grid of the specified dimensions.
"""
new_grid(number_of_rows, number_of_columns)::Grid =
    Grid(missing, number_of_rows, number_of_columns)


abstract type CardinalDirection end
struct N <: CardinalDirection end
struct E <: CardinalDirection end
struct S <: CardinalDirection end
struct W <: CardinalDirection end

const CARDINAL_DIRECTIONS = (N(), E(), S(), W())


"""
    opposite(::CardinalDirection)::CardinalDirection

Returns the opposite direction.  `N()` and `S()` are opposites.
`E()` and `W()` are opposites.
"""
opposite(::N) = S()
opposite(::E) = W()
opposite(::S) = N()
opposite(::W) = E()

edge_index(::N) = 1
edge_index(::E) = 2
edge_index(::S) = 3
edge_index(::W) = 4

Starting from a given CardinalDirection, we can identify a sequence of all of the CardinalDirections in clockwise order.

Given the row and column indices of a grid cell, and a CardinalDirection, we can compute the coordinates of the neighboring cell in that direction.

Each instance of a CardinalDirection serves as an operator for going from one pair of row/coumn indices to the neighboring ones in that direction. As these operators don't know the size of the grid, bounds checking must be done by their callers.

(::N)(row::Int, col::Int) = [row - 1, col]
(::E)(row::Int, col::Int) = [row,     col + 1]
(::S)(row::Int, col::Int) = [row + 1, col]
(::W)(row::Int, col::Int) = [row,     col - 1]

Which edge of a puzzle piece is facing a given CardinalDirection depends on the rotation of that piece.

The rotation of a puzzle piece is represented by one of the integers 0, 1, 2, or 3, also in clockwise order.

"""
    rotation(rot::Int)

Normalizes the rotation of the placement of a puzzle piece to one of
0, 1, 2, or 3.
"""
rotation(r::Int) = mod(r, 4)

Within a grid, a GridCell has a neighbor in each direction.

(cd::CardinalDirection)(gc::GridCell) = cd(gc.row, gc.col)

function (cd::CardinalDirection)(grid::Grid, row::Int, col::Int)
    (r, c) = cd(row, col)
    (nrows, ncols) = size(grid)
    if !(r in 1:nrows) || !(c in 1:ncols)
        return missing
    end
    return grid[r, c]
end

(cd::CardinalDirection)(grid::Grid, cell::GridCell) =
    cd(grid, cell.row, cell.col)


function get_edge(gc::GridCell, direction::CardinalDirection)
    i = edge_index(edge_index(direction) + gc.rotation)
    gc.puzzle_piece.edges[i]
end

function set_edge!(gc::GridCell, direction::CardinalDirection,
                   edge::Edge)
    i = edge_index(edge_index(direction) + gc.rotation)
    gc.puzzle_piece.edges[i] = edge
end

isperimeter(cell::GridCell, direction::CardinalDirection) =
    isperimeter(get_edge(cell, direction))

function direction_for_edge(cell::GridCell, edge::Edge)
    for d in CARDINAL_DIRECTIONS
        e = get_edge(cell, d)
        if e isa Edge
            if e == edge
                return d
            end
        end
    end
    return missing
end


"""
    perimeter_edge_indices(grid::Grid, row::int, col::Int)

Returns a vector of the indices of edges of the specified cell of
`grid` that are perimeter edges.
"""
function perimeter_edge_indices(grid::Grid, row::Int, col::Int)
    indices = []
    (nrows, ncols) = size(grid)
    if row == 1;      push!(indices, 1); end
    if col == ncols;  push!(indices, 2); end
    if row == nrows;  push!(indices, 3); end
    if col == 1;      push!(indices, 4); end
    indices
end

"""
   get_neighboring_edge(grid::Grid, row::Int, col::Int, direction::CardinalDirection)

Returns the `Edge` of the "neighboring cell" to the specified grid
location.  If that location is out of bounds then `Edge(EdgeType(true,
0), Straight())` is returned.  Otherwise, if there is no GridCell at
the specified location yet, then `missing` is returned.
"""
function get_neighboring_edge(grid::Grid, row::Int, col::Int,
                              direction::CardinalDirection)
    (nrows, ncols) = size(grid)
    neighbor_indices = direction(row, col)
    if (neighbor_indices[1] < 1 ||
        neighbor_indices[1] > nrows ||
        neighbor_indices[2] < 1 ||
        neighbor_indices[2] > ncols
        )
        return Edge(EdgeType(true, 0), Straight())
    else
        cell = grid[neighbor_indices...]
        if cell isa GridCell
            return get_edge(cell, opposite(direction))
        end
    end
    return missing
end


"""
    fit_piece(continuation, grid::Grid, row::Int, col::Int,
                   piece::ImmutablePuzzlePiece)

Attempts to fit `piece` into the specified location of `grid`.
`continuation` is called for each rotation of `piece` that fits.
"""
function fit_piece(continuation, grid::Grid, row::Int, col::Int,
                   piece::ImmutablePuzzlePiece)
    # If the piece fits at the specified location in grid, call
    # continuation with the rotation of the piece.
    for rot in 0:3
        for d in CARDINAL_DIRECTIONS
            piece_edge = piece.edges[edge_index(rot + edge_index(d))]
            neighbor_edge = get_neighboring_edge(grid, row, col, d)
            if ismissing(neighbor_edge)
                continue
            end
            if !edges_mate(piece_edge, neighbor_edge)
                @goto no_fit
            end
        end
        continuation(rot)
        @label no_fit
    end
end

This page was generated using Literate.jl.