using .SnakeGames

function SnakeGameEnv(; action_style = MINIMAL_ACTION_SET, kw...)
    game = SnakeGame(; kw...)
    n_snakes = length(game.snakes)
    num_agent_style = n_snakes == 1 ? SINGLE_AGENT : MultiAgent{n_snakes}()
    SnakeGameEnv{action_style,num_agent_style,typeof(game)}(
        game,
        map(length, game.snakes),
        Vector{CartesianIndex{2}}(undef, length(game.snakes)),
        false,
    )
end

RLBase.ActionStyle(env::SnakeGameEnv{A}) where {A} = A
RLBase.NumAgentStyle(env::SnakeGameEnv{<:Any,N}) where {N} = N
RLBase.DynamicStyle(env::SnakeGameEnv{<:Any,SINGLE_AGENT}) = SEQUENTIAL
RLBase.DynamicStyle(env::SnakeGameEnv{<:Any,<:MultiAgent}) = SIMULTANEOUS

const SNAKE_GAME_ACTIONS = (
    CartesianIndex(-1, 0),
    CartesianIndex(1, 0),
    CartesianIndex(0, 1),
    CartesianIndex(0, -1),
)

function (env::SnakeGameEnv{A})(actions::Vector{CartesianIndex{2}}) where {A}
    if A === MINIMAL_ACTION_SET
        # avoid turn back
        actions = [
            a_new == -a_old ? a_old : a_new for
            (a_new, a_old) in zip(actions, env.latest_actions)
        ]
    end

    env.latest_actions .= actions
    map!(length, env.latest_snakes_length, env.game.snakes)
    env.is_terminated = !env.game(actions)
end

(env::SnakeGameEnv)(action::Int) = env([SNAKE_GAME_ACTIONS[action]])
(env::SnakeGameEnv)(actions::Vector{Int}) = env(map(a -> SNAKE_GAME_ACTIONS[a], actions))

RLBase.action_space(env::SnakeGameEnv) = 1:4
RLBase.state(env::SnakeGameEnv) = env.game.board
RLBase.state_space(env::SnakeGameEnv) = Space(fill(false..true, size(env.game.board)))
RLBase.reward(env::SnakeGameEnv{<:Any,SINGLE_AGENT}) =
    length(env.game.snakes[]) - env.latest_snakes_length[]
RLBase.reward(env::SnakeGameEnv) = length.(env.game.snakes) .- env.latest_snakes_length
RLBase.is_terminated(env::SnakeGameEnv) = env.is_terminated

RLBase.legal_action_space(env::SnakeGameEnv{FULL_ACTION_SET,SINGLE_AGENT}) =
    findall(!=(-env.latest_actions[]), SNAKE_GAME_ACTIONS)
RLBase.legal_action_space(env::SnakeGameEnv{FULL_ACTION_SET}) =
    [findall(!=(-a), SNAKE_GAME_ACTIONS) for a in env.latest_actions]

RLBase.legal_action_space_mask(env::SnakeGameEnv{FULL_ACTION_SET,SINGLE_AGENT}) =
    [a != -env.latest_actions[] for a in SNAKE_GAME_ACTIONS]
RLBase.legal_action_space_mask(env::SnakeGameEnv{FULL_ACTION_SET}) =
    [[x != -a for x in SNAKE_GAME_ACTIONS] for a in env.latest_actions]

function RLBase.reset!(env::SnakeGameEnv)
    SnakeGames.reset!(env.game)
    env.is_terminated = false
    fill!(env.latest_actions, CartesianIndex(0, 0))
    map!(length, env.latest_snakes_length, env.game.snakes)
end

Base.display(io::IO, m::MIME, env::SnakeGameEnv) = display(io, m, env.game)


"""
$(SIGNATURES)

Attach `successor` to `predecessor` using `joint`.

See [`Joint`](@ref) for definitions of the terms successor and predecessor.

The `Transform3D`s `joint_pose` and `successor_pose` define where
`joint` is attached to each body. `joint_pose` should define
`frame_before(joint)` with respect to any frame fixed to `predecessor`, and likewise
`successor_pose` should define any frame fixed to `successor` with respect to
`frame_after(joint)`.

`predecessor` is required to already be among the bodies of the `Mechanism`.

If `successor` is not yet a part of the `Mechanism`, it will be added to the
`Mechanism`. Otherwise, the `joint` will be treated as a non-tree edge in the
`Mechanism`, effectively creating a loop constraint that will be enforced
using Lagrange multipliers (as opposed to using recursive algorithms).
"""
function attach!(mechanism::Mechanism{T}, predecessor::RigidBody{T}, successor::RigidBody{T}, joint::Joint{T};
        joint_pose::Transform3D = one(Transform3D{T}, frame_before(joint), default_frame(predecessor)),
        successor_pose::Transform3D = one(Transform3D{T}, default_frame(successor), frame_after(joint))) where {T}
    @assert joint_pose.from == frame_before(joint)
    @assert successor_pose.to == frame_after(joint)
    @assert predecessor ∈ bodies(mechanism)
    @assert joint ∉ joints(mechanism)

    # define where joint is attached on predecessor
    joint.joint_to_predecessor[] = joint_pose
    add_frame!(predecessor, joint_pose)

    # define where child is attached to joint
    joint.joint_to_successor[] = inv(successor_pose)
    add_frame!(successor, joint.joint_to_successor[])

    # update graph
    if successor ∈ bodies(mechanism)
        add_edge!(mechanism.graph, predecessor, successor, joint)
    else
        add_edge!(mechanism.tree, predecessor, successor, joint)
        canonicalize_frame_definitions!(mechanism, successor)
    end
    register_modification!(mechanism)
    mechanism
end

function _copyjoint!(dest::Mechanism{T}, src::Mechanism{T}, srcjoint::Joint{T},
        bodymap::AbstractDict{RigidBody{T}, RigidBody{T}}, jointmap::AbstractDict{Joint{T}, Joint{T}}) where {T}
    srcpredecessor = source(srcjoint, src.graph)
    srcsuccessor = target(srcjoint, src.graph)

    joint_to_predecessor = fixed_transform(srcpredecessor, frame_before(srcjoint), default_frame(srcpredecessor))
    successor_to_joint = fixed_transform(srcsuccessor, default_frame(srcsuccessor), frame_after(srcjoint))

    destpredecessor = get!(() -> deepcopy(srcpredecessor), bodymap, srcpredecessor)
    destsuccessor = get!(() -> deepcopy(srcsuccessor), bodymap, srcsuccessor)
    destjoint = jointmap[srcjoint] = deepcopy(srcjoint)

    attach!(dest, destpredecessor, destsuccessor, destjoint; joint_pose = joint_to_predecessor, successor_pose = successor_to_joint)
end

const bodymap_jointmap_doc = """
* `bodymap::AbstractDict{RigidBody{T}, RigidBody{T}}`: will be populated with
  the mapping from input mechanism's bodies to output mechanism's bodies
* `jointmap::AbstractDict{<:Joint{T}, <:Joint{T}}`: will be populated with
  the mapping from input mechanism's joints to output mechanism's joints

where `T` is the element type of `mechanism`.
"""

"""
$(SIGNATURES)

Attach a copy of `childmechanism` to `mechanism`.

Essentially replaces the root body of a copy of `childmechanism` with `parentbody` (which
belongs to `mechanism`).

Note: gravitational acceleration for `childmechanism` is ignored.

Keyword arguments:

* `child_root_pose`: pose of the default frame of the root body of `childmechanism` relative to that of `parentbody`.
  Default: the identity transformation.
$bodymap_jointmap_doc
"""
function attach!(mechanism::Mechanism{T}, parentbody::RigidBody{T}, childmechanism::Mechanism{T};
        child_root_pose = one(Transform3D{T}, default_frame(root_body(childmechanism)), default_frame(parentbody)),
        bodymap::AbstractDict{RigidBody{T}, RigidBody{T}}=Dict{RigidBody{T}, RigidBody{T}}(),
        jointmap::AbstractDict{<:Joint{T}, <:Joint{T}}=Dict{Joint{T}, Joint{T}}()) where {T}
    # FIXME: test with cycles

    @assert mechanism != childmechanism # infinite loop otherwise
    empty!(bodymap)
    empty!(jointmap)

    # Define where child root body is located w.r.t parent body and add frames that were attached to childroot to parentbody.
    childroot = root_body(childmechanism)
    add_frame!(parentbody, child_root_pose)
    for transform in frame_definitions(childroot)
        add_frame!(parentbody, transform)
    end
    bodymap[childroot] = parentbody

    # Insert childmechanism's non-root vertices and joints, starting with the tree joints (to preserve order).
    for joint in flatten((tree_joints(childmechanism), non_tree_joints(childmechanism)))
        _copyjoint!(mechanism, childmechanism, joint, bodymap, jointmap)
    end
    canonicalize_frame_definitions!(mechanism, parentbody)

    mechanism
end

"""
$(SIGNATURES)

Create a new `Mechanism` from the subtree of `mechanism` rooted at `submechanismroot`.

Any non-tree joint in `mechanism` will appear in the returned `Mechanism` if and
only if both its successor and its predecessor are part of the subtree.

Keyword arguments:

$bodymap_jointmap_doc
"""
function submechanism(mechanism::Mechanism{T}, submechanismroot::RigidBody{T};
        bodymap::AbstractDict{RigidBody{T}, RigidBody{T}}=Dict{RigidBody{T}, RigidBody{T}}(),
        jointmap::AbstractDict{<:Joint{T}, <:Joint{T}}=Dict{Joint{T}, Joint{T}}()) where {T}
    # FIXME: test with cycles
    empty!(bodymap)
    empty!(jointmap)

    # Create Mechanism
    root = bodymap[submechanismroot] = deepcopy(submechanismroot)
    ret = Mechanism(root; gravity = mechanism.gravitational_acceleration.v)

    # Add tree joints, preserving order in input mechanism.
    for joint in tree_joints(mechanism) # assumes toposort
        if haskey(bodymap, predecessor(joint, mechanism))
            _copyjoint!(ret, mechanism, joint, bodymap, jointmap)
        end
    end

    # Add non-tree joints.
    for joint in non_tree_joints(mechanism)
        if haskey(bodymap, predecessor(joint, mechanism)) && haskey(bodymap, successor(joint, mechanism))
            _copyjoint!(ret, mechanism, joint, bodymap, jointmap)
        end
    end

    ret
end

"""
Remove all bodies in the subtree rooted at `subtree_root`, including `subtree_root` itself,
as well as all joints connected to these bodies.

The ordering of the joints that remain in the mechanism is retained.
"""
function remove_subtree!(mechanism::Mechanism{T}, subtree_root::RigidBody{T}) where {T}
    @assert subtree_root != root_body(mechanism)
    tree = mechanism.tree
    graph = mechanism.graph
    bodies_to_remove = subtree_vertices(subtree_root, tree)
    new_tree_joints = copy(edges(tree))
    for body in bodies_to_remove
        # Remove the tree joint from our new ordered list of joints.
        tree_joint = edge_to_parent(body, tree)
        deleteat!(new_tree_joints, findfirst(isequal(tree_joint), new_tree_joints))
    end
    for body in bodies_to_remove
        # Remove all edges to and from the vertex in the graph.
        for joint in copy(in_edges(body, graph))
            remove_edge!(graph, joint)
        end
        for joint in copy(out_edges(body, graph))
            remove_edge!(graph, joint)
        end

        # Remove the vertex itself.
        remove_vertex!(graph, body)
    end
    # Construct a new spanning tree with the new list of tree joints.
    mechanism.tree = SpanningTree(graph, root_body(mechanism), new_tree_joints)
    canonicalize_graph!(mechanism)
    mechanism
end

"""
$(SIGNATURES)

Reconstruct the mechanism's spanning tree.

Optionally, the `flipped_joint_map` keyword argument can be used to pass in an associative container
that will be populated with a mapping from original joints to flipped joints, if the rebuilding process
required the polarity of some joints to be flipped.

Also optionally, `next_edge` can be used to select which joints should become part of the
new spanning tree.
"""
function rebuild_spanning_tree!(mechanism::Mechanism{M},
        flipped_joint_map::Union{AbstractDict, Nothing} = nothing;
        next_edge = first #= breadth first =#) where {M}
    mechanism.tree = SpanningTree(mechanism.graph, root_body(mechanism), flipped_joint_map; next_edge = next_edge)
    register_modification!(mechanism)
    canonicalize_frame_definitions!(mechanism)
    mechanism
end

"""
$(SIGNATURES)

Remove a joint from the mechanism. Rebuilds the spanning tree if the joint is
part of the current spanning tree.

Optionally, the `flipped_joint_map` keyword argument can be used to pass in an associative container
that will be populated with a mapping from original joints to flipped joints, if removing `joint`
requires rebuilding the spanning tree of `mechanism` and the polarity of some joints needed to be changed in the process.

Also optionally, `spanning_tree_next_edge` can be used to select which joints should become part of the
new spanning tree, if rebuilding the spanning tree is required.
"""
function remove_joint!(mechanism::Mechanism{M}, joint::Joint{M};
        flipped_joint_map::Union{AbstractDict, Nothing} = nothing,
        spanning_tree_next_edge = first #= breadth first =#) where {M}
    istreejoint = joint ∈ tree_joints(mechanism)
    remove_edge!(mechanism.graph, joint)
    register_modification!(mechanism)
    if istreejoint
        rebuild_spanning_tree!(mechanism, flipped_joint_map,
            next_edge=spanning_tree_next_edge) # also recanonicalizes frame definitions
        canonicalize_graph!(mechanism)
    end
    nothing
end

function replace_joint!(mechanism::Mechanism, oldjoint::Joint, newjoint::Joint)
    # TODO: can hopefully remove this again once Joint is mutable again
    @assert frame_before(newjoint) == frame_before(oldjoint)
    @assert frame_after(newjoint) == frame_after(oldjoint)
    set_joint_to_predecessor!(newjoint, oldjoint.joint_to_predecessor[])
    set_joint_to_successor!(newjoint, oldjoint.joint_to_successor[])
    if oldjoint ∈ tree_joints(mechanism)
        replace_edge!(mechanism.tree, oldjoint, newjoint)
    else
        replace_edge!(mechanism.graph, oldjoint, newjoint)
    end
    register_modification!(mechanism)
    nothing
end

"""
$(SIGNATURES)

Remove any fixed joints present as tree edges in `mechanism` by merging the
rigid bodies that these fixed joints join together into bodies with equivalent
inertial properties.
"""
function remove_fixed_tree_joints!(mechanism::Mechanism{T}) where T
    # FIXME: test with cycles
    graph = mechanism.graph

    # Update graph.
    fixedjoints = filter(j -> joint_type(j) isa Fixed, tree_joints(mechanism))
    newtreejoints = setdiff(tree_joints(mechanism), fixedjoints)
    for fixedjoint in fixedjoints
        pred = source(fixedjoint, graph)
        succ = target(fixedjoint, graph)

        # Add identity joint transform as a body-fixed frame definition.
        jointtransform = one(Transform3D{T}, frame_after(fixedjoint), frame_before(fixedjoint))
        add_frame!(pred, jointtransform)

        # Migrate body fixed frames to parent body.
        for tf in frame_definitions(succ)
            add_frame!(pred, tf)
        end

        # Migrate contact points to parent body.
        for point in contact_points(succ)
            add_contact_point!(pred, point)
        end

        # Add inertia to parent body.
        if has_defined_inertia(pred)
            inertia = spatial_inertia(succ)
            parentinertia = spatial_inertia(pred)
            toparent = fixed_transform(pred, inertia.frame, parentinertia.frame)
            spatial_inertia!(pred, parentinertia + transform(inertia, toparent))
        end

        # Merge vertex into parent.
        for joint in copy(in_edges(succ, graph))
            if joint == fixedjoint
                remove_edge!(graph, joint)
            else
                rewire!(graph, joint, source(joint, graph), pred)
            end
        end
        for joint in copy(out_edges(succ, graph))
            rewire!(graph, joint, pred, target(joint, graph))
        end
        remove_vertex!(mechanism.graph, succ)
    end

    # Recompute spanning tree (preserves order for non-fixed joints)
    mechanism.tree = SpanningTree(graph, root_body(mechanism), newtreejoints)

    # Recanonicalize graph and frames
    canonicalize_frame_definitions!(mechanism)
    canonicalize_graph!(mechanism)

    register_modification!(mechanism)

    mechanism
end

# TODO: remove floating_non_tree_joints

"""
$(SIGNATURES)

Return a dynamically equivalent `Mechanism`, but with a flat tree structure:
all bodies are attached to the root body via a floating joint, and
the tree joints of the input `Mechanism` are transformed into non-tree
joints (with joint constraints enforced using Lagrange multipliers in `dynamics!`).

Keyword arguments:

* `floating_joint_type::Type{<:JointType{T}}`: which floating joint type to
  use for the joints between each body and the root. Default: `QuaternionFloating{T}`
$bodymap_jointmap_doc
"""
function maximal_coordinates(mechanism::Mechanism{T};
        floating_joint_type::Type{<:JointType{T}}=QuaternionFloating{T},
        bodymap::AbstractDict{RigidBody{T}, RigidBody{T}}=Dict{RigidBody{T}, RigidBody{T}}(),
        jointmap::AbstractDict{<:Joint{T}, <:Joint{T}}=Dict{Joint{T}, Joint{T}}()) where T
    @assert isfloating(floating_joint_type)
    empty!(bodymap)
    empty!(jointmap)

    # Copy root.
    root = bodymap[root_body(mechanism)] = deepcopy(root_body(mechanism))
    ret = Mechanism(root, gravity = mechanism.gravitational_acceleration.v)

    # Copy non-root bodies and attach them to the root with a floating joint.
    for srcbody in non_root_bodies(mechanism)
        framebefore = default_frame(root)
        frameafter = default_frame(srcbody)
        body = bodymap[srcbody] = deepcopy(srcbody)
        floatingjoint = Joint(string(body), framebefore, frameafter, floating_joint_type())
        attach!(ret, root, body, floatingjoint, joint_pose = one(Transform3D{T}, framebefore), successor_pose = one(Transform3D{T}, frameafter))
    end

    # Copy input Mechanism's joints.
    for joint in flatten((tree_joints(mechanism), non_tree_joints(mechanism)))
        _copyjoint!(ret, mechanism, joint, bodymap, jointmap)
    end

    ret
end

function canonicalize_graph!(mechanism::Mechanism)
    root = root_body(mechanism)
    treejoints = copy(tree_joints(mechanism))
    edges = vcat(treejoints, non_tree_joints(mechanism))
    vertices = append!([root], successor(joint, mechanism) for joint in treejoints)
    reindex!(mechanism.graph, vertices, edges)
    mechanism.tree = SpanningTree(mechanism.graph, root, treejoints) # otherwise tree.edge_tree_indices can go out of sync
    register_modification!(mechanism)
    mechanism
end

add_environment_primitive!(mechanism::Mechanism, halfspace::HalfSpace3D) = push!(mechanism.environment, halfspace)
"""
$(SIGNATURES)

Create a random tree `Mechanism` with the given joint types. Each new body is
attached to a parent selected using the `parentselector` function.
"""
function rand_tree_mechanism(::Type{T}, parentselector::Function, jointtypes::Vararg{Type{<:JointType{T}}}) where {T}
    parentbody = RigidBody{T}("world")
    mechanism = Mechanism(parentbody)
    for (i, jointtype) in enumerate(jointtypes)
        joint = Joint("joint$i", rand(jointtype))
        joint_to_parent_body = rand(Transform3D{T}, frame_before(joint), default_frame(parentbody))
        body = RigidBody(rand(SpatialInertia{T}, CartesianFrame3D("body$i")))
        body_to_joint = one(Transform3D{T}, default_frame(body), frame_after(joint))
        attach!(mechanism, parentbody, body, joint, joint_pose = joint_to_parent_body, successor_pose = body_to_joint)
        parentbody = parentselector(mechanism)
    end
    return mechanism
end
rand_tree_mechanism(parentselector::Function, jointtypes::Vararg{Type{<:JointType{T}}}) where {T} = rand_tree_mechanism(T, parentselector, jointtypes...)

"""
$(SIGNATURES)

Create a random chain `Mechanism` with the given joint types.
"""
rand_chain_mechanism(::Type{T}, jointtypes::Vararg{Type{<:JointType{T}}}) where {T} = rand_tree_mechanism(T, mechanism::Mechanism -> last(bodies(mechanism)), jointtypes...)
rand_chain_mechanism(jointtypes::Vararg{Type{<:JointType{T}}}) where {T} = rand_chain_mechanism(T, jointtypes...)

"""
$(SIGNATURES)

Create a random tree `Mechanism`.
"""
rand_tree_mechanism(::Type{T}, jointtypes::Vararg{Type{<:JointType{T}}}) where {T} = rand_tree_mechanism(T, mechanism::Mechanism -> rand(bodies(mechanism)), jointtypes...)
rand_tree_mechanism(jointtypes::Vararg{Type{<:JointType{T}}}) where {T} = rand_tree_mechanism(T, jointtypes...)

"""
$(SIGNATURES)

Create a random tree `Mechanism`, with a quaternion floating
joint as the first joint (between the root body and the first non-root body).
"""
function rand_floating_tree_mechanism(::Type{T}, nonfloatingjointtypes::Vararg{Type{<:JointType{T}}}) where {T}
    parentselector = (mechanism::Mechanism) -> begin
        only_root = length(bodies(mechanism)) == 1
        only_root ? root_body(mechanism) : rand(collect(non_root_bodies(mechanism)))
    end
    rand_tree_mechanism(parentselector, QuaternionFloating{T}, nonfloatingjointtypes...)
end
rand_floating_tree_mechanism(nonfloatingjointtypes::Vararg{Type{<:JointType{T}}}) where {T} = rand_floating_tree_mechanism(T, nonfloatingjointtypes...)


@lexer FortranLexer let
    keywords = [
        "ABSTRACT", "ACCEPT", "ALL", "ALLSTOP", "ALLOCATABLE", "ALLOCATE", "ARRAY",
        "ASSIGN", "ASSOCIATE", "ASYNCHRONOUS", "BACKSPACE", "BIND", "BLOCK", "BLOCKDATA",
        "BYTE", "CALL", "CASE", "CLASS", "CLOSE", "CODIMENSION", "COMMON", "CONCURRRENT",
        "CONTIGUOUS", "CONTAINS", "CONTINUE", "CRITICAL", "CYCLE", "DATA", "DEALLOCATE",
        "DECODE", "DEFERRED", "DIMENSION", "DO", "ELEMENTAL", "ELSE", "ENCODE", "END",
        "ENTRY", "ENUM", "ENUMERATOR", "EQUIVALENCE", "EXIT", "EXTENDS", "EXTERNAL",
        "EXTRINSIC", "FILE", "FINAL", "FORALL", "FORMAT", "FUNCTION", "GENERIC", "GOTO",
        "IF", "IMAGES", "IMPLICIT", "IMPORT", "IMPURE", "INCLUDE", "INQUIRE", "INTENT",
        "INTERFACE", "INTRINSIC", "IS", "LOCK", "MEMORY", "MODULE", "NAMELIST", "NULLIFY",
        "NONE", "NON_INTRINSIC", "NON_OVERRIDABLE", "NOPASS", "OPEN", "OPTIONAL", "OPTIONS",
        "PARAMETER", "PASS", "PAUSE", "POINTER", "PRINT", "PRIVATE", "PROGRAM", "PROCEDURE",
        "PROTECTED", "PUBLIC", "PURE", "READ", "RECURSIVE", "RESULT", "RETURN", "REWIND",
        "SAVE", "SELECT", "SEQUENCE", "STOP", "SUBMODULE", "SUBROUTINE", "SYNC", "SYNCALL",
        "SYNCIMAGES", "SYNCMEMORY", "TARGET", "THEN", "TYPE", "UNLOCK", "USE", "VALUE",
        "VOLATILE", "WHERE", "WRITE", "WHILE"
    ]
    datatypes =[
        "CHARACTER", "COMPLEX", "DOUBLE PRECISION", "DOUBLE COMPLEX", "INTEGER", "LOGICAL",
        "REAL", "C_INT", "C_SHORT", "C_LONG", "C_LONG_LONG", "C_SIGNED_CHAR", "C_SIZE_T",
        "C_INT8_T", "C_INT16_T", "C_INT32_T", "C_INT64_T", "C_INT_LEAST8_T",
        "C_INT_LEAST16_T", "C_INT_LEAST32_T", "C_INT_LEAST64_T", "C_INT_FAST8_T",
        "C_INT_FAST16_T", "C_INT_FAST32_T", "C_INT_FAST64_T", "C_INTMAX_T", "C_INTPTR_T",
        "C_FLOAT", "C_DOUBLE", "C_LONG_DOUBLE", "C_FLOAT_COMPLEX", "C_DOUBLE_COMPLEX",
        "C_LONG_DOUBLE_COMPLEX", "C_BOOL", "C_CHAR", "C_PTR", "C_FUNPTR"
    ]
    builtins = [
        "Abort", "Abs", "Access", "AChar", "ACos", "ACosH", "AdjustL", "AdjustR", "AImag",
        "AInt", "Alarm", "All", "Allocated", "ALog", "AMax", "AMin", "AMod", "And", "ANInt",
        "Any", "ASin", "ASinH", "Associated", "ATan", "ATanH", "Atomic_Define",
        "Atomic_Ref", "BesJ", "BesJN", "Bessel_J0", "Bessel_J1", "Bessel_JN", "Bessel_Y0",
        "Bessel_Y1", "Bessel_YN", "BesY", "BesYN", "BGE", "BGT", "BLE", "BLT", "Bit_Size",
        "BTest", "CAbs", "CCos", "Ceiling", "CExp", "Char", "ChDir", "ChMod", "CLog",
        "Cmplx", "Command_Argument_Count", "Complex", "Conjg", "Cos", "CosH", "Count",
        "CPU_Time", "CShift", "CSin", "CSqRt", "CTime", "C_Loc", "C_Associated",
        "C_Null_Ptr", "C_Null_Funptr", "C_F_Pointer", "C_F_ProcPointer", "C_Null_Char",
        "C_Alert", "C_Backspace", "C_Form_Feed", "C_FunLoc", "C_Sizeof", "C_New_Line",
        "C_Carriage_Return", "C_Horizontal_Tab", "C_Vertical_Tab", "DAbs", "DACos", "DASin",
        "DATan", "Date_and_Time", "DbesJ", "DbesJN", "DbesY", "DbesYN", "Dble", "DCos",
        "DCosH", "DDiM", "DErF", "DErFC", "DExp", "Digits", "DiM", "DInt", "DLog", "DMax",
        "DMin", "DMod", "DNInt", "Dot_Product", "DProd", "DSign", "DSinH", "DShiftL",
        "DShiftR", "DSin", "DSqRt", "DTanH", "DTan", "DTime", "EOShift", "Epsilon", "ErF",
        "ErFC", "ErFC_Scaled", "ETime", "Execute_Command_Line", "Exit", "Exp", "Exponent",
        "Extends_Type_Of", "FDate", "FGet", "FGetC", "FindLoc", "Float", "Floor", "Flush",
        "FNum", "FPutC", "FPut", "Fraction", "FSeek", "FStat", "FTell", "Gamma", "GError",
        "GetArg", "Get_Command", "Get_Command_Argument", "Get_Environment_Variable",
        "GetCWD", "GetEnv", "GetGId", "GetLog", "GetPId", "GetUId", "GMTime", "HostNm",
        "Huge", "Hypot", "IAbs", "IAChar", "IAll", "IAnd", "IAny", "IArgC", "IBClr",
        "IBits", "IBSet", "IChar", "IDate", "IDiM", "IDInt", "IDNInt", "IEOr", "IErrNo",
        "IFix", "Imag", "ImagPart", "Image_Index", "Index", "Int", "IOr", "IParity",
        "IRand", "IsaTty", "IShft", "IShftC", "ISign", "Iso_C_Binding", "Is_Contiguous",
        "Is_Iostat_End", "Is_Iostat_Eor", "ITime", "Kill", "Kind", "LBound", "LCoBound",
        "Len", "Len_Trim", "LGe", "LGt", "Link", "LLe", "LLt", "LnBlnk", "Loc", "Log",
        "Log_Gamma", "Logical", "Long", "LShift", "LStat", "LTime", "MaskL", "MaskR",
        "MatMul", "Max", "MaxExponent", "MaxLoc", "MaxVal", "MClock", "Merge", "Merge_Bits",
        "Move_Alloc", "Min", "MinExponent", "MinLoc", "MinVal", "Mod", "Modulo", "MvBits",
        "Nearest", "New_Line", "NInt", "Norm2", "Not", "Null", "Num_Images", "Or", "Pack",
        "Parity", "PError", "Precision", "Present", "Product", "Radix", "Rand",
        "Random_Number", "Random_Seed", "Range", "Real", "RealPart", "Rename", "Repeat",
        "Reshape", "RRSpacing", "RShift", "Same_Type_As", "Scale", "Scan", "Second",
        "Selected_Char_Kind", "Selected_Int_Kind", "Selected_Real_Kind", "Set_Exponent",
        "Shape", "ShiftA", "ShiftL", "ShiftR", "Short", "Sign", "Signal", "SinH", "Sin",
        "Sleep", "Sngl", "Spacing", "Spread", "SqRt", "SRand", "Stat", "Storage_Size",
        "Sum", "SymLnk", "System", "System_Clock", "Tan", "TanH", "Time", "This_Image",
        "Tiny", "TrailZ", "Transfer", "Transpose", "Trim", "TtyNam", "UBound", "UCoBound",
        "UMask", "Unlink", "Unpack", "Verify", "XOr", "ZAbs", "ZCos", "ZExp", "ZLog",
        "ZSin", "ZSqRt"
    ]
    logical_operators = [
        "EQ", "NE", "LT", "LE", "GT", "GE", "NOT", "AND", "OR", "EQV", "NEQV",
    ]
    caseless = ws -> vcat(ws, map(lowercase, ws))
    Dict(
        :name => "Fortran 90",
        :description => "A lexer for Fortran 90 source code.",
        :comments => "Based on the Pygments lexer definition.",
        :aliases => ["fortran", "f90"],
        :tokens => Dict(
            :root => [
                (r"[ ]+", TEXT),
                (r"[\n\r]+", TEXT),
                (r"^#.*$"m, COMMENT_PREPROC),
                (r"!.*$"m, COMMENT),
                :strings,
                :core,
                (r"[a-z][\w$]*", NAME),
                :nums,
            ],
            :core => [
                (words(caseless(keywords); prefix = "\\b", suffix = "\\b"), KEYWORD),
                (words(caseless(datatypes); prefix = "\\b", suffix = "\\b"), KEYWORD_TYPE),
                (r"(\*\*|\*|\+|-|\/|<|>|<=|>=|==|\/=|=)", OPERATOR),
                (r"(::)", KEYWORD_DECLARATION),
                (words(builtins; prefix = "\\b", suffix = "\\b"), NAME_BUILTIN),
                (r"\.(true|false|TRUE|FALSE)\.", NAME_BUILTIN),
                (words(caseless(logical_operators); prefix = "\\.", suffix = "\\."), OPERATOR_WORD),
                (r"[()\[\],:&%;.]", PUNCTUATION),
            ],
            :strings => [
                (r"(?s)\"(\\\\|\\[0-7]+|\\.|[^\"\\])*\"", STRING_DOUBLE),
                (r"(?s)'(\\\\|\\[0-7]+|\\.|[^'\\])*'", STRING_SINGLE),
            ],
            :nums => [
                (r"\d+(?![.e])(_[a-z]\w+)?", NUMBER_INTEGER),
                (r"[+-]?\d*\.\d+([ed][-+]?'d+)?(_[a-z]\w+)?", NUMBER_FLOAT),
                (r"[+-]?\d+\.\d*([ed][-+]?\d+)?(_[a-z]\w+)?", NUMBER_FLOAT),
            ],
        ),
    )
end


import Random: AbstractRNG, rand, rand!

abstract type VSLDistribution{T} <: AbstractRNG end

rand(d::VSLDistribution{T}, dims::Integer...) where T = rand(d, T, Dims(dims))

function rand(d::VSLDistribution{T}, ::Type{X}) where {T, X}
    error("Only $(T) type is supported for $d.")
end

function rand!(d::VSLDistribution{T}, A::AbstractArray{K}, ::Type{X}=T) where {T, K, X}
    # T !== K || T !== X
    error("Only $(T) type is supported for $d.")
end


const Pt{T<:Real} = Union{AbstractVector{T},NTuple{<:Any,T}}
const Poly = AbstractVector{<:Union{NTuple{<:Any,<:Real},AbstractVector{<:Real}}}

const DEFAULT_POINT_ORDER = (XDim, YDim)

# _fill_geometry!
# Fill a raster with `fill` where it interacts with a geometry.
# We reduce all geometries to vectors with `coordinates` so that a simple
# vector can also represent a polygon. In future we should do this lazily.
function _fill_geometry!(B::AbstractRaster, coll::GI.AbstractFeatureCollection; order, kw...)
    if bbox_might_overlap(B, coll, order)
        foreach(coll.features) do feature
            _fill_geometry!(B, feature; order, kw...)
        end
    end
    return B
end
function _fill_geometry!(B::AbstractRaster, feature::GI.AbstractFeature; order, kw...)
    if bbox_might_overlap(B, feature, order)
        _fill_geometry!(B, GI.geometry(feature); order, kw...)
    end
    return B
end
function _fill_geometry!(B::AbstractRaster, geoms::AbstractVector{<:Union{Missing,<:GI.AbstractGeometry,<:GI.AbstractFeature}}; order, kw...)
    any(map(g -> bbox_might_overlap(B, g, order), geoms)) || return B
    if all(g -> typeof(g) == typeof(first(geoms)), geoms)
        coords = GI.coordinates.(GI.geometry.(geoms))
        _fill_geometry!(B, coords; order, shape=_geom_shape(GI.geometry(first(geoms))), kw...)
    else
        _fill_mixed_geometries!(B, geoms; order, kw...)
    end
end
function _fill_geometry!(B::AbstractRaster, geom::GI.AbstractGeometry; order, kw...)
    # Dont do anything if the bbox doesn't overlap
    bbox_might_overlap(B, geom, order) || return B
    _fill_geometry!(B, GI.coordinates(geom); order, shape=_geom_shape(geom), kw...)
end
function _fill_geometry!(B::AbstractRaster, geom::AbstractVector; shape=:polygon, order, kw...)
    gbounds = _geom_bounds(geom, order)
    abounds = bounds(dims(B, order)) # Only mask if the gemoetry bounding box overlaps the array bounding box
    bounds_overlap(gbounds, abounds) || return B
    if shape === :polygon
        _fill_polygon!(B, geom; polybounds=gbounds, order, kw...)
    elseif shape === :line
        _fill_linestring!(B, geom; order, kw...)
    elseif shape === :point
        _fill_points!(B, geom; order, kw...)
    else
        _shape_error(shape)
    end
    return B
end

# Multiple geometry types. Rasterize by groups of types separately
@noinline function _fill_mixed_geometries!(B, geoms; order, kw...)
    geomgroups = Dict{Type,Vector}()
    for geom in geoms
        key = typeof(geom)
        if haskey(geomgroups, key)
            push!(geomgroups[key], geom)
        else
            geomgroups[key] = [geom]
        end
    end
    for group in values(geomgroups)
        _fill_geometry!(B, GI.coordinates.(group); order, shape=_geom_shape(first(group)), kw...)
    end
end

# _fill_polygon!
# Fill a raster with `fill` where pixels are inside a polygon
# `boundary` determines how edges are handled
function _fill_polygon!(B::AbstractRaster, poly; polybounds, order, fill=true, boundary=:center, kw...)
    # TODO take a view of B for the polygon
    # We need a tuple of all the dims in `order`
    # We also need the index locus to be the center so we are
    # only selecting cells more than half inside the polygon
    shifted_dims = map(dims(B, order)) do d
        d = DD.maybeshiftlocus(Center(), d)
        # DimPoints are faster if we materialise the dims
        modify(Array, d)
    end
    pts = DimPoints(shifted_dims)
    inpoly = if any(map(d -> DD.order(d) isa Unordered, shifted_dims))
        inpolygon(vec(pts), poly)
    else
        pointbounds = map(shifted_dims) do d
            f = first(d)
            l = last(d)
            f < l ? (f, l) : (l, f)
        end
        # Precalculate vmin, vmax and iyperm permutation vector
        # This is much faster than calling `sortperm` in PolygonInbounds.jl
        vmin = [first.(pointbounds)...]'
        vmax = [last.(pointbounds)...]'
        pmin = [first.(polybounds)...]'
        pmax = [last.(polybounds)...]'
        pts = DimPoints(shifted_dims)
        iyperm = _iyperm(shifted_dims)
        inpolygon(vec(pts), poly; vmin, vmax, pmin, pmax, iyperm)
    end
    inpolydims = dims(B, order)
    reshaped = Raster(reshape(inpoly, size(inpolydims)), inpolydims)
    return _inner_fill_polygon!(B, poly, inpoly, reshaped; order, fill, boundary)
end

# split to make a type stability function barrier
function _inner_fill_polygon!(B::AbstractRaster, poly, inpoly, reshaped; order, fill=true, boundary=:center, kw...)
    # Get the array as points
    # Use the first column of the output - the points in the polygon,
    # and reshape to match `A`
    for D in DimIndices(B)
        @inbounds if reshaped[D...]
            @inbounds B[D...] = fill
        end
    end
    if boundary === :touches
        # Add the line pixels
        _fill_linestring!(B, poly; order, fill)
    elseif boundary === :inside
        # Remove the line pixels
        _fill_linestring!(B, poly; order, fill=!fill)
    elseif boundary !== :center
        _boundary_error(boundary)
    end
    return B
end

function _iyperm(dims::Tuple{<:Dimension,<:Dimension})
    a1, a2 = map(dims) do d
        l = parent(d)
        LA.ordered_firstindex(l):_order_step(l):LA.ordered_lastindex(l)
    end
    iyperm = Array{Int}(undef, length(a1) * length(a2))
    lis = (LinearIndices(size(dims))[i, j] for j in a2 for i in a1)
    for (i, li) in enumerate(lis)
        iyperm[i] = li
    end
    return iyperm
end
function _iyperm(dims::Tuple{<:Dimension,<:Dimension,<:Dimension})
    a1, a2, a3 = map(dims) do d
        l = parent(d)
        LA.ordered_firstindex(l):_order_step(l):LA.ordered_lastindex(l)
    end
    iyperm = Array{Int}(undef, length(a1) * length(a2) * length(a3))
    lis = (LinearIndices(size(dims))[i, j, k] for k in a3 for j in a2 for i in a1)
    for (i, li) in enumerate(lis)
        Iyperm[i] = li
    end
    return iyperm
end

_order_step(x) = _order_step(order(x))
_order_step(::ReverseOrdered) = -1
_order_step(::ForwardOrdered) = 1

# _fill_point!
# Fill a raster with `fill` where points are inside raster pixels
function _fill_points!(B::AbstractRaster, points; kw...)
    # Just find which pixels contian the points, and set them to true
    _without_mapped_crs(B) do B1
        _fill_points1!(B, points; kw...)
    end
    return B
end

function _fill_points1!(B::AbstractRaster, points::AbstractVector; kw...)
    for point in points
        _fill_points1!(B, point; kw...)
    end
end
function _fill_points1!(B::AbstractRaster, point::Union{Tuple,<:AbstractVector{<:Real}};
    order, fill=true, atol=nothing, kw...
)
    selectors = map(dims(B, order), ntuple(i -> i, length(order))) do d, i
        _at_or_contains(d, point[i], atol)
    end
    if hasselection(B, selectors)
        B[selectors...] = fill
    end
end

# _fill_linestring!
# Fill a raster with `fill` where pixels touch lines in a linestring
# Separated for a type stability function barrier
function _fill_linestring!(B::AbstractRaster, linestring::AbstractVector{<:AbstractVector{<:AbstractVector}}, args...; kw...)
    for ls in linestring
        _fill_linestring!(B, ls, args...; kw...)
    end
end
function _fill_linestring!(B::AbstractRaster, linestring::Poly; order, fill=true, kw...)
    linestring = collect(linestring)
    # Flip the order with a view to keep our alg simple
    forward_ordered_B = reduce(dims(B); init=B) do A, d
        if DD.order(d) isa ReverseOrdered
            A = view(A, rebuild(d, lastindex(d):-1:firstindex(d)))
            set(A, d => reverse(d))
        else
            A
        end
    end
    # For each line segment, burn the line into B with `fill` values
    _fill_line_segments!(forward_ordered_B, linestring, fill, order)
    return B
end

function _fill_line_segments!(B::AbstractArray, linestring::Poly, fill, order::Tuple{<:Dimension,<:Dimension})
    isfirst = true
    local firstpoint, lastpoint
    for point in linestring
        if isfirst
            isfirst = false
            firstpoint = point
            lastpoint = point
            continue
        end
        if point == firstpoint
            isfirst = true
        end
        line = (
            start=(x=lastpoint[1], y=lastpoint[2]),
            stop=(x=point[1], y=point[2])
        )
        _fill_line!(B, line, fill, order)
        lastpoint = point
    end
end

# fill_line!
# Fill a raster with `fill` where pixels touch a line
# TODO: generalise to 3d and Irregular spacing?
function _fill_line!(A::AbstractRaster, line, fill, order::Tuple{<:Dimension,<:Dimension})
    regular = map(dims(A, order)) do d
        lookup(d) isa AbstractSampled && span(d) isa Regular
    end
    msg = """
        Can only fill lines where dimensions are regular.
        Consider using `boundary=center` reprojecting the crs,
        or make an issue in Rasters.jl on github if you need this to work.
        """
    all(regular) || throw(ArgumentError(msg))

    xd, yd = order
    x_scale = abs(step(span(A, X)))
    y_scale = abs(step(span(A, Y)))
    raw_x_offset = bounds(A, xd)[1]
    raw_y_offset = bounds(A, yd)[1]
    raw_start, raw_stop = line.start, line.stop # Float
    start = (; x=(raw_start.x - raw_x_offset)/x_scale, y=(raw_start.y - raw_y_offset)/y_scale)
    stop = (; x=(raw_stop.x - raw_x_offset)/x_scale, y=(raw_stop.y - raw_y_offset)/y_scale)
    x, y = floor(Int, start.x) + 1, floor(Int, start.y) + 1 # Int

    diff_x = stop.x - start.x
    diff_y = stop.y - start.y
    step_x = signbit(diff_x) * -2 + 1
    step_y = signbit(diff_y) * -2 + 1
    # Ray/Slope calculations
    # Straight distance to the first vertical/horizontal grid boundaries
    xoffset = stop.x > start.x ? (ceil(start.x) - start.x) : (start.x - floor(start.x))
    yoffset = stop.y > start.y ? (ceil(start.y) - start.y) : (start.y - floor(start.y))
    # Angle of ray/slope.
    angle = atan(-diff_y, diff_x)
    # max: How far to move along the ray to cross the first cell boundary.
    # delta: How far to move along the ray to move 1 grid cell.
    cs = cos(angle)
    si = sin(angle)
    max_x, delta_x = if isapprox(cs, zero(cs); atol=1e-10)
        -Inf, Inf
    else
        1.0 / cs, xoffset / cs
    end
    max_y, delta_y = if isapprox(si, zero(si); atol=1e-10)
        -Inf, Inf
    else
        1.0 / si, yoffset / si
    end
    # Travel one grid cell at a time.
    manhattan_distance = floor(Int, abs(floor(start.x) - floor(stop.x)) + abs(floor(start.y) - floor(stop.y)))
    # For arbitrary dimension indexing
    dimconstructors = map(DD.basetypeof, dims(A, order))
    for t in 0:manhattan_distance
        D = map((d, o) -> d(o), dimconstructors, (x, y))
        if checkbounds(Bool, A, D...)
            if fill isa Function
                @inbounds A[D...] = fill(A[D...])
            else
                @inbounds A[D...] = fill
            end
        end
        # Only move in either X or Y coordinates, not both.
        if abs(max_x) <= abs(max_y)
            max_x += delta_x
            x += step_x
        else
            max_y += delta_y
            y += step_y
        end
    end
    return A
end
function _fill_line!(A::AbstractRaster, line, fill, order::Tuple{Vararg{<:Dimension}})
    msg = """"
        Converting a `:line` geometry to raster is currently only implemented for 2d lines.
        Make a Rasters.jl github issue if you need this for more dimensions.
        """
    throw(ArgumentError(msg))
end

# PolygonInbounds.jl setup

# _flat_nodes
# Convert a geometry/nested vectors to a flat iterator of point nodes for PolygonInbounds
_flat_nodes(A::GI.AbstractGeometry) = _flat_nodes(GI.coordinates(A))
_flat_nodes(A::GI.AbstractFeature) = _flat_nodes(GI.geometry(A))
_flat_nodes(A::AbstractVector{<:GI.AbstractGeometry}) = Iterators.flatten(map(_flat_nodes, A))
_flat_nodes(A::AbstractVector{<:GI.AbstractFeature}) = Iterators.flatten(map(_flat_nodes, A))
function _flat_nodes(A::AbstractVector{<:Union{Missing,<:GI.AbstractGeometry}})
    Iterators.flatten(map(_flat_nodes, A))
end
function _flat_nodes(A::AbstractVector{<:AbstractVector{<:AbstractVector}})
    Iterators.flatten(map(_flat_nodes, A))
end
_flat_nodes(A::AbstractVector{<:AbstractVector{<:Real}}) = A
_flat_nodes(A::AbstractVector{<:Real}) = A
_flat_nodes(A::AbstractVector{<:Tuple}) = A
_flat_nodes(iter::Base.Iterators.Flatten) = iter

_node_vecs(A::GI.AbstractGeometry) = GI.coordinates(A)
_node_vecs(A::GI.AbstractFeature) = _node_vecs(GI.geometry(A))
function _node_vecs(A::AbstractVector{<:Union{Missing,<:GI.AbstractGeometry,<:GI.AbstractFeature}})
    map(_node_vecs, A)
end
function _node_vecs(A::AbstractVector{<:AbstractVector{<:AbstractVector}})
    A
end
_node_vecs(A::AbstractVector{<:AbstractVector{<:AbstractFloat}}) = A
_node_vecs(A::AbstractVector{<:Tuple}) = A
_node_vecs(iter::Base.Iterators.Flatten) = collect(iter)

# _to_edges
# Convert a polygon to the `edges` needed by PolygonInbounds
function to_edges_and_nodes(poly)
    p1 = first(_flat_nodes(poly))
    edges = Vector{Tuple{Int,Int}}(undef, 0)
    nodes = Vector{typeof(p1)}(undef, 0)
    nodenum = 0
    _to_edges!(edges, nodes, nodenum, poly)
    edges = PermutedDimsArray(reinterpret(reshape, Int, edges), (2, 1))
    lastx = 0
    for (i, x) in enumerate(view(edges, :, 1))
        x == lastx + 1 || error("edge dup at $x $lastx $i")
        lastx = x
    end
    return edges, nodes
end

# _to_edges!(edges, edgenum, poly)
# fill edges vector with edges from polygon, numbered starting at `edgenum`
function _to_edges!(edges, nodes, pointnum, poly::AbstractVector{<:Union{Missing,<:GI.AbstractGeometry}})
    foldl(poly; init=pointnum) do n, p
        _to_edges!(edges, nodes, n, p)
    end
end
function _to_edges!(edges, nodes, pointnum, poly::GI.AbstractGeometry)
    _to_edges!(edges, nodes, pointnum, GI.coordinates(poly))
end
function _to_edges!(edges, nodes, pointnum, poly::AbstractVector{<:AbstractVector})
    foldl(poly; init=pointnum) do n, p
        _to_edges!(edges, nodes, n, p)
    end
end
# Analyse a single polygon
function _to_edges!(
    edges, nodes, pointnum,
    poly::AbstractVector{<:Union{<:NTuple{<:Any,T},<:AbstractVector{T}}}
) where T<:Real
    # Any polygon may actually be made up of multiple sub-polygons,
    # indicated by returning to the first point in the sub-polygon.
    # So we keep track of the first point, and insert closing edges
    # wherever we find it repeated.
    fresh_start = true
    startpoint = first(poly)
    start_pointnum = pointnum
    added_nodes = 0
    for (n, point) in enumerate(poly)
        if fresh_start
            # The first edge in the sub-polygon
            start_pointnum = pointnum + n
            startedge = (start_pointnum, start_pointnum + 1)
            push!(edges, startedge)
            startpoint = point
            fresh_start = false
        elseif point == startpoint || n == length(poly)
            # The closing edge of a sub-polygon
            closingedge = (pointnum + n, start_pointnum)
            push!(edges, closingedge)
            fresh_start = true
        else
            # A regular edge somewhere in a sub-polygon
            edge = (pointnum + n, pointnum + n + 1)
            push!(edges, edge)
        end
        # Track the total number of nodes we have added
        added_nodes = n
    end
    append!(nodes, poly)
    nextpoint = pointnum + added_nodes
    return nextpoint
end

# _geom_bounds
# Get the bounds of a geometry
function _geom_bounds(geom, order)
    nodes = _flat_nodes(geom)
    bounds = map(ntuple(identity, length(order))) do i
        extrema(p[i] for p in nodes)
    end
    return bounds
end

function _wrapped_geom_bounds(x, geom, order)
    wrapped_bounds = map(rebuild, dims(x, order), _geom_bounds(geom, order))
    return dims(wrapped_bounds, dims(x))
end

# bounds_overlap
# Check if two bounds tuples overlap
function bounds_overlap(a, b)
    axes_cross = map(a, b) do (a_min, a_max), (b_min, b_max)
        if a_max >= b_max
            a_min <= b_max
        else
            a_max >= b_min
        end
    end
    return all(axes_cross)
end

# bbox_might_overlap
# Check if there is a bbox for the geometry
function bbox_might_overlap(x, geom, order)
    x_bnds = bounds(x, order)
    p_bbox = GI.bbox(geom)
    # If there is no bbox available just act as if it
    # overlaps and let the checks later on sort it out
    p_bbox isa Nothing && return true
    bbox_dims = length(p_bbox) ÷ 2
    p_bnds = [(p_bbox[i], p_bbox[i+bbox_dims]) for i in 1:bbox_dims]

    return bounds_overlap(p_bnds, x_bnds)
end

# _geom_shape
# Get the shape category for a geometry
function _geom_shape(geom)
    typ = GI.geotype(geom)
    if typ in (:Point, :MultiPoint)
        return :point
    elseif typ in (:LineString, :MultiLineString)
        return :line
    elseif typ in (:Polygon, :MultiPolygon)
        return :polygon
    else
        throw(ArgumentError("Geometry type $typ not known"))
    end
    # TODO: What to do with :GeometryCollection
end

# Copied from PolygonInbounds, to add extra keyword arguments
# PR to include these when this has solidied
function inpoly2(vert, node, edge=zeros(Int);
    atol::T=0.0, rtol::T=NaN, iyperm=nothing,
    vmin=nothing, vmax=nothing, pmin=nothing, pmax=nothing
) where T<:AbstractFloat
    rtol = !isnan(rtol) ? rtol : iszero(atol) ? eps(T)^0.85 : zero(T)
    poly = PolygonInbounds.PolygonMesh(node, edge)
    points = PolygonInbounds.PointsInbound(vert)
    npoints = length(points)

    vmin = isnothing(vmin) ? minimum(points) : vmin
    vmax = isnothing(vmax) ? maximum(points) : vmax
    pmin = isnothing(pmin) ? minimum(poly) : pmin
    pmax = isnothing(pmax) ? maximum(poly) : pmax

    lbar = sum(pmax - pmin)
    tol = max(abs(rtol * lbar), abs(atol))

    ac = PolygonInbounds.areacount(poly)
    stat = ac > 1 ? falses(npoints, 2, ac) : falses(npoints, 2)
    # flip coordinates so expected effort is minimal

    dvert = vmax .- vmin
    if isnothing(iyperm)
        ix = dvert[1] < dvert[2] ? 1 : 2
        iyperm = sortperm(points, 3 - ix)
    else
        ix = 1
    end

    PolygonInbounds.inpoly2!(points, iyperm, poly, ix, tol, stat)
    return stat
end

_shape_error(shape) = throw(ArgumentError("`shape` must be :point, :line or :polygon, got $shape"))
_boundary_error(boundary) = throw(ArgumentError("`boundary` can be :touches, :inside, or :center, got $boundary"))


letterA = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(0.4, -2.0, 0.0), Thebes.Point3D(1.25, -2.0, 0.0), Thebes.Point3D(0.5, -2.5, 0.0), Thebes.Point3D(1.3125, -2.5, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(1.5, -4.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(2.6, -2.0, 0.0), Thebes.Point3D(1.75, -2.0, 0.0), Thebes.Point3D(2.5, -2.5, 0.0), Thebes.Point3D(1.6875, -2.5, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(0.4, -2.0, 1.0), Thebes.Point3D(1.25, -2.0, 1.0), Thebes.Point3D(0.5, -2.5, 1.0), Thebes.Point3D(1.3125, -2.5, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(1.5, -4.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(2.6, -2.0, 1.0), Thebes.Point3D(1.75, -2.0, 1.0), Thebes.Point3D(2.5, -2.5, 1.0), Thebes.Point3D(1.6875, -2.5, 1.0), Thebes.Point3D(2.0, -5.0, 1.0)], Array{Int64,1}[[3, 4, 2, 1], [5, 6, 4, 3], [7, 8, 6, 5], [12, 11, 9, 10], [14, 13, 11, 12], [8, 15, 13, 14], [6, 14, 12, 4], [7, 15, 8], [16, 17, 19, 18], [18, 19, 21, 20], [20, 21, 23, 22], [25, 24, 26, 27], [27, 26, 28, 29], [29, 28, 30, 23], [19, 27, 29, 21], [23, 30, 22], [1, 16, 18, 3], [3, 18, 20, 5], [5, 20, 22, 7], [7, 22, 30, 15], [15, 30, 28, 13], [13, 28, 26, 11], [11, 26, 24, 9], [9, 24, 25, 10], [10, 25, 27, 12], [12, 27, 19, 4], [4, 19, 17, 2], [2, 17, 16, 1], [8, 23, 21, 6], [6, 21, 29, 14], [14, 29, 23, 8]])
letterB = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, -0.5, 0.0), Thebes.Point3D(3.0, -1.5, 0.0), Thebes.Point3D(2.0, -2.5, 0.0), Thebes.Point3D(3.0, -3.5, 0.0), Thebes.Point3D(3.0, -4.5, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(2.0, -1.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(2.0, -3.0, 0.0), Thebes.Point3D(2.0, -4.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, -0.5, 1.0), Thebes.Point3D(3.0, -1.5, 1.0), Thebes.Point3D(2.0, -2.5, 1.0), Thebes.Point3D(3.0, -3.5, 1.0), Thebes.Point3D(3.0, -4.5, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(2.0, -1.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(2.0, -3.0, 1.0), Thebes.Point3D(2.0, -4.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0)], Array{Int64,1}[[10, 11, 2, 1], [11, 12, 5, 4, 3, 2], [5, 12, 13, 14, 15], [5, 15, 16, 8, 7, 6], [8, 16, 17, 9], [9, 17, 14, 13, 10, 1], [18, 19, 28, 27], [19, 20, 21, 22, 29, 28], [32, 31, 30, 29, 22], [23, 24, 25, 33, 32, 22], [26, 34, 33, 25], [18, 27, 30, 31, 34, 26], [1, 2, 19, 18], [2, 3, 20, 19], [3, 4, 21, 20], [4, 5, 22, 21], [5, 6, 23, 22], [6, 7, 24, 23], [7, 8, 25, 24], [8, 9, 26, 25], [9, 1, 18, 26], [11, 10, 27, 28], [10, 13, 30, 27], [12, 11, 28, 29], [13, 12, 29, 30], [17, 16, 33, 34], [16, 15, 32, 33], [15, 14, 31, 32], [14, 17, 34, 31]])
letterC = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(3.0, -1.5, 0.0), Thebes.Point3D(2.0, -1.5, 0.0), Thebes.Point3D(2.0, -1.0, 0.0), Thebes.Point3D(1.5, -0.5, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(1.5, -4.5, 0.0), Thebes.Point3D(2.0, -4.0, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(3.0, -3.5, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(3.0, -1.5, 1.0), Thebes.Point3D(2.0, -1.5, 1.0), Thebes.Point3D(2.0, -1.0, 1.0), Thebes.Point3D(1.5, -0.5, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0), Thebes.Point3D(1.5, -4.5, 1.0), Thebes.Point3D(2.0, -4.0, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(3.0, -3.5, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[6, 5, 4, 3], [7, 6, 3, 2], [7, 2, 1], [7, 1, 18, 8], [8, 18, 17, 9], [9, 17, 16, 10], [10, 16, 15], [15, 14, 11, 10], [14, 13, 12, 11], [21, 22, 23, 24], [20, 21, 24, 25], [19, 20, 25], [26, 36, 19, 25], [27, 35, 36, 26], [28, 34, 35, 27], [33, 34, 28], [28, 29, 32, 33], [29, 30, 31, 32], [1, 2, 20, 19], [2, 3, 21, 20], [3, 4, 22, 21], [4, 5, 23, 22], [5, 6, 24, 23], [6, 7, 25, 24], [7, 8, 26, 25], [8, 9, 27, 26], [9, 10, 28, 27], [10, 11, 29, 28], [11, 12, 30, 29], [12, 13, 31, 30], [13, 14, 32, 31], [14, 15, 33, 32], [15, 16, 34, 33], [16, 17, 35, 34], [17, 18, 36, 35], [18, 1, 19, 36]])
letterD = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(1.5, -1.0, 0.0), Thebes.Point3D(2.0, -1.5, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(1.5, -4.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(1.5, -1.0, 1.0), Thebes.Point3D(2.0, -1.5, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(1.5, -4.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0)], Array{Int64,1}[[7, 8, 2, 1], [8, 9, 3, 2], [9, 10, 4, 3], [10, 11, 5, 4], [11, 12, 6, 5], [12, 7, 1, 6], [13, 14, 20, 19], [14, 15, 21, 20], [15, 16, 22, 21], [16, 17, 23, 22], [17, 18, 24, 23], [18, 13, 19, 24], [1, 2, 14, 13], [2, 3, 15, 14], [3, 4, 16, 15], [4, 5, 17, 16], [5, 6, 18, 17], [6, 1, 13, 18], [8, 7, 19, 20], [9, 8, 20, 21], [10, 9, 21, 22], [11, 10, 22, 23], [12, 11, 23, 24], [7, 12, 24, 19]])
letterE = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(2.0, -3.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(2.0, -3.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[4, 3, 2, 1], [12, 9, 8, 5, 4, 1], [8, 7, 6, 5], [12, 11, 10, 9], [13, 14, 15, 16], [13, 16, 17, 20, 21, 24], [17, 18, 19, 20], [21, 22, 23, 24], [1, 2, 14, 13], [2, 3, 15, 14], [3, 4, 16, 15], [4, 5, 17, 16], [5, 6, 18, 17], [6, 7, 19, 18], [7, 8, 20, 19], [8, 9, 21, 20], [9, 10, 22, 21], [10, 11, 23, 22], [11, 12, 24, 23], [12, 1, 13, 24]])
letterF = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(2.0, -3.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(2.0, -3.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[1, 10, 7, 6, 3, 2], [6, 5, 4, 3], [10, 9, 8, 7], [12, 13, 16, 17, 20, 11], [13, 14, 15, 16], [17, 18, 19, 20], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])
letterG = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(3.0, -2.0, 0.0), Thebes.Point3D(1.5, -2.0, 0.0), Thebes.Point3D(1.5, -1.5, 0.0), Thebes.Point3D(2.0, -1.5, 0.0), Thebes.Point3D(2.0, -1.0, 0.0), Thebes.Point3D(1.5, -0.5, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(1.5, -4.5, 0.0), Thebes.Point3D(2.0, -4.0, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(3.0, -3.5, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(3.0, -2.0, 1.0), Thebes.Point3D(1.5, -2.0, 1.0), Thebes.Point3D(1.5, -1.5, 1.0), Thebes.Point3D(2.0, -1.5, 1.0), Thebes.Point3D(2.0, -1.0, 1.0), Thebes.Point3D(1.5, -0.5, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0), Thebes.Point3D(1.5, -4.5, 1.0), Thebes.Point3D(2.0, -4.0, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(3.0, -3.5, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[7, 6, 5, 4], [8, 7, 4, 3], [9, 8, 3, 2], [1, 9, 2], [10, 9, 1, 20], [11, 10, 20, 19], [12, 11, 19, 18], [12, 18, 17], [13, 12, 17, 16], [16, 15, 14, 13], [24, 25, 26, 27], [23, 24, 27, 28], [22, 23, 28, 29], [22, 29, 21], [40, 21, 29, 30], [39, 40, 30, 31], [38, 39, 31, 32], [37, 38, 32], [36, 37, 32, 33], [33, 34, 35, 36], [1, 2, 22, 21], [2, 3, 23, 22], [3, 4, 24, 23], [4, 5, 25, 24], [5, 6, 26, 25], [6, 7, 27, 26], [7, 8, 28, 27], [8, 9, 29, 28], [9, 10, 30, 29], [10, 11, 31, 30], [11, 12, 32, 31], [12, 13, 33, 32], [13, 14, 34, 33], [14, 15, 35, 34], [15, 16, 36, 35], [16, 17, 37, 36], [17, 18, 38, 37], [18, 19, 39, 38], [19, 20, 40, 39], [20, 1, 21, 40]])
letterH = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(2.0, -3.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(2.0, -3.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[12, 11, 10, 3, 2, 1], [10, 9, 4, 3], [9, 8, 7, 6, 5, 4], [13, 14, 15, 22, 23, 24], [15, 16, 21, 22], [16, 17, 18, 19, 20, 21], [1, 2, 14, 13], [2, 3, 15, 14], [3, 4, 16, 15], [4, 5, 17, 16], [5, 6, 18, 17], [6, 7, 19, 18], [7, 8, 20, 19], [8, 9, 21, 20], [9, 10, 22, 21], [10, 11, 23, 22], [11, 12, 24, 23], [12, 1, 13, 24]])
letterI = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(2.0, -1.0, 0.0), Thebes.Point3D(2.0, -4.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(2.0, -1.0, 1.0), Thebes.Point3D(2.0, -4.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[12, 11, 4, 3, 2, 1], [11, 10, 5, 4], [10, 9, 8, 7, 6, 5], [13, 14, 15, 16, 23, 24], [16, 17, 22, 23], [17, 18, 19, 20, 21, 22], [1, 2, 14, 13], [2, 3, 15, 14], [3, 4, 16, 15], [4, 5, 17, 16], [5, 6, 18, 17], [6, 7, 19, 18], [7, 8, 20, 19], [8, 9, 21, 20], [9, 10, 22, 21], [10, 11, 23, 22], [11, 12, 24, 23], [12, 1, 13, 24]])
letterJ = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.5, 0.0, 0.0), Thebes.Point3D(2.5, -1.0, 0.0), Thebes.Point3D(2.5, -4.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(1.75, -4.0, 0.0), Thebes.Point3D(1.75, -1.0, 0.0), Thebes.Point3D(1.25, -0.5, 0.0), Thebes.Point3D(0.75, -1.0, 0.0), Thebes.Point3D(0.75, -2.0, 0.0), Thebes.Point3D(0.0, -2.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.5, 0.0, 1.0), Thebes.Point3D(2.5, -1.0, 1.0), Thebes.Point3D(2.5, -4.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(1.75, -4.0, 1.0), Thebes.Point3D(1.75, -1.0, 1.0), Thebes.Point3D(1.25, -0.5, 1.0), Thebes.Point3D(0.75, -1.0, 1.0), Thebes.Point3D(0.75, -2.0, 1.0), Thebes.Point3D(0.0, -2.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[14, 13, 12, 15], [15, 12, 11, 2, 1], [2, 11, 10, 3], [3, 10, 9, 4], [9, 8, 7, 6, 5, 4], [30, 27, 28, 29], [16, 17, 26, 27, 30], [18, 25, 26, 17], [19, 24, 25, 18], [19, 20, 21, 22, 23, 24], [1, 2, 17, 16], [2, 3, 18, 17], [3, 4, 19, 18], [4, 5, 20, 19], [5, 6, 21, 20], [6, 7, 22, 21], [7, 8, 23, 22], [8, 9, 24, 23], [9, 10, 25, 24], [10, 11, 26, 25], [11, 12, 27, 26], [12, 13, 28, 27], [13, 14, 29, 28], [14, 15, 30, 29], [15, 1, 16, 30]])
letterK = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(1.5, -2.5, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(1.5, -2.5, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[11, 10, 9, 3, 2, 1], [9, 6, 3], [6, 5, 4, 3], [9, 8, 7, 6], [12, 13, 14, 20, 21, 22], [14, 17, 20], [14, 15, 16, 17], [17, 18, 19, 20], [1, 2, 13, 12], [2, 3, 14, 13], [3, 4, 15, 14], [4, 5, 16, 15], [5, 6, 17, 16], [6, 7, 18, 17], [7, 8, 19, 18], [8, 9, 20, 19], [9, 10, 21, 20], [10, 11, 22, 21], [11, 1, 12, 22]])
letterL = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[4, 3, 2, 1], [6, 5, 4, 1], [7, 8, 9, 10], [7, 10, 11, 12], [1, 2, 8, 7], [2, 3, 9, 8], [3, 4, 10, 9], [4, 5, 11, 10], [5, 6, 12, 11], [6, 1, 7, 12]])
letterM = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(0.5, 0.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.5, -2.5, 0.0), Thebes.Point3D(2.0, -3.0, 0.0), Thebes.Point3D(2.5, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(2.5, -5.0, 0.0), Thebes.Point3D(1.5, -3.5, 0.0), Thebes.Point3D(0.5, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(0.5, 0.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.5, -2.5, 1.0), Thebes.Point3D(2.0, -3.0, 1.0), Thebes.Point3D(2.5, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(2.5, -5.0, 1.0), Thebes.Point3D(1.5, -3.5, 1.0), Thebes.Point3D(0.5, -5.0, 1.0)], Array{Int64,1}[[10, 3, 2, 1], [10, 9, 4, 3], [9, 8, 5, 4], [8, 7, 6, 5], [11, 12, 13, 20], [13, 14, 19, 20], [14, 15, 18, 19], [15, 16, 17, 18], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])
letterN = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(2.0, -3.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(2.0, -3.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[10, 9, 3, 2, 1], [9, 8, 4, 3], [8, 7, 6, 5, 4], [11, 12, 13, 19, 20], [13, 14, 18, 19], [14, 15, 16, 17, 18], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])
letterO = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(1.0, -1.5, 0.0), Thebes.Point3D(1.5, -1.0, 0.0), Thebes.Point3D(2.0, -1.5, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(1.5, -4.0, 0.0), Thebes.Point3D(1.0, -3.5, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0), Thebes.Point3D(1.0, -1.5, 1.0), Thebes.Point3D(1.5, -1.0, 1.0), Thebes.Point3D(2.0, -1.5, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(1.5, -4.0, 1.0), Thebes.Point3D(1.0, -3.5, 1.0)], Array{Int64,1}[[10, 2, 1], [10, 11, 3, 2], [11, 12, 4, 3], [12, 13, 5, 4], [13, 6, 5], [13, 14, 7, 6], [14, 9, 8, 7], [9, 10, 1, 8], [15, 16, 24], [16, 17, 25, 24], [17, 18, 26, 25], [18, 19, 27, 26], [19, 20, 27], [20, 21, 28, 27], [21, 22, 23, 28], [22, 15, 24, 23], [1, 2, 16, 15], [2, 3, 17, 16], [3, 4, 18, 17], [4, 5, 19, 18], [5, 6, 20, 19], [6, 7, 21, 20], [7, 8, 22, 21], [8, 1, 15, 22], [10, 9, 23, 24], [11, 10, 24, 25], [12, 11, 25, 26], [13, 12, 26, 27], [14, 13, 27, 28], [9, 14, 28, 23]])
letterP = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(3.0, -3.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.5, -3.0, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(1.5, -4.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(3.0, -3.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.5, -3.0, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(1.5, -4.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0)], Array{Int64,1}[[8, 13, 9, 3, 2, 1], [9, 10, 4, 3], [10, 11, 5, 4], [11, 6, 5], [11, 12, 7, 6], [8, 7, 12, 13], [14, 15, 16, 22, 26, 21], [16, 17, 23, 22], [17, 18, 24, 23], [18, 19, 24], [19, 20, 25, 24], [26, 25, 20, 21], [1, 2, 15, 14], [2, 3, 16, 15], [3, 4, 17, 16], [4, 5, 18, 17], [5, 6, 19, 18], [6, 7, 20, 19], [7, 8, 21, 20], [8, 1, 14, 21], [9, 13, 26, 22], [13, 12, 25, 26], [12, 11, 24, 25], [11, 10, 23, 24], [10, 9, 22, 23]])
letterQ = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.5, 0.0, 0.0), Thebes.Point3D(2.0, -0.5, 0.0), Thebes.Point3D(2.5, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -0.5, 0.0), Thebes.Point3D(2.5, -1.0, 0.0), Thebes.Point3D(3.0, -1.5, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(1.5, -1.0, 0.0), Thebes.Point3D(2.0, -1.5, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(1.5, -4.0, 0.0), Thebes.Point3D(1.0, -3.5, 0.0), Thebes.Point3D(1.0, -1.5, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.5, 0.0, 1.0), Thebes.Point3D(2.0, -0.5, 1.0), Thebes.Point3D(2.5, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -0.5, 1.0), Thebes.Point3D(2.5, -1.0, 1.0), Thebes.Point3D(3.0, -1.5, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0), Thebes.Point3D(1.5, -1.0, 1.0), Thebes.Point3D(2.0, -1.5, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(1.5, -4.0, 1.0), Thebes.Point3D(1.0, -3.5, 1.0), Thebes.Point3D(1.0, -1.5, 1.0)], Array{Int64,1}[[14, 3, 2, 1], [14, 7, 6, 5, 4, 3], [14, 15, 8, 7], [15, 16, 9, 8], [16, 17, 10, 9], [17, 11, 10], [17, 18, 12, 11], [18, 19, 13, 12], [19, 14, 1, 13], [20, 21, 22, 33], [22, 23, 24, 25, 26, 33], [26, 27, 34, 33], [27, 28, 35, 34], [28, 29, 36, 35], [29, 30, 36], [30, 31, 37, 36], [31, 32, 38, 37], [32, 20, 33, 38], [1, 2, 21, 20], [2, 3, 22, 21], [3, 4, 23, 22], [4, 5, 24, 23], [5, 6, 25, 24], [6, 7, 26, 25], [7, 8, 27, 26], [8, 9, 28, 27], [9, 10, 29, 28], [10, 11, 30, 29], [11, 12, 31, 30], [12, 13, 32, 31], [13, 1, 20, 32], [19, 18, 37, 38], [18, 17, 36, 37], [17, 16, 35, 36], [16, 15, 34, 35], [15, 14, 33, 34], [14, 19, 38, 33]])
letterR = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(1.0, -1.5, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(1.5, -2.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(3.0, -3.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.5, -3.0, 0.0), Thebes.Point3D(2.0, -3.5, 0.0), Thebes.Point3D(1.5, -4.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(1.0, -1.5, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(1.5, -2.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(3.0, -3.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.5, -3.0, 1.0), Thebes.Point3D(2.0, -3.5, 1.0), Thebes.Point3D(1.5, -4.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0)], Array{Int64,1}[[11, 16, 12, 3, 2, 1], [6, 5, 4, 3], [6, 3, 12], [12, 13, 7, 6], [13, 14, 8, 7], [9, 8, 14], [10, 9, 14, 15], [11, 10, 15, 16], [17, 18, 19, 28, 32, 27], [19, 20, 21, 22], [28, 19, 22], [22, 23, 29, 28], [23, 24, 30, 29], [30, 24, 25], [31, 30, 25, 26], [32, 31, 26, 27], [1, 2, 18, 17], [2, 3, 19, 18], [3, 4, 20, 19], [4, 5, 21, 20], [5, 6, 22, 21], [6, 7, 23, 22], [7, 8, 24, 23], [8, 9, 25, 24], [9, 10, 26, 25], [10, 11, 27, 26], [11, 1, 17, 27], [16, 15, 31, 32], [15, 14, 30, 31], [14, 13, 29, 30], [13, 12, 28, 29], [12, 16, 32, 28]])
letterS = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -3.0, 0.0), Thebes.Point3D(1.0, -3.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -2.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(2.0, -1.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -3.0, 1.0), Thebes.Point3D(1.0, -3.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -2.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(2.0, -1.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[12, 11, 2, 1], [11, 10, 3, 2], [10, 9, 4, 3], [9, 8, 5, 4], [8, 7, 6, 5], [13, 14, 23, 24], [14, 15, 22, 23], [15, 16, 21, 22], [16, 17, 20, 21], [17, 18, 19, 20], [1, 2, 14, 13], [2, 3, 15, 14], [3, 4, 16, 15], [4, 5, 17, 16], [5, 6, 18, 17], [6, 7, 19, 18], [7, 8, 20, 19], [8, 9, 21, 20], [9, 10, 22, 21], [10, 11, 23, 22], [11, 12, 24, 23], [12, 1, 13, 24]])
letterT = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(2.0, -4.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(1.0, -4.0, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(2.0, -4.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(1.0, -4.0, 1.0)], Array{Int64,1}[[8, 3, 2, 1], [8, 7, 6, 5, 4, 3], [9, 10, 11, 16], [11, 12, 13, 14, 15, 16], [1, 2, 10, 9], [2, 3, 11, 10], [3, 4, 12, 11], [4, 5, 13, 12], [5, 6, 14, 13], [6, 7, 15, 14], [7, 8, 16, 15], [8, 1, 9, 16]])
letterU = (Thebes.Point3D[Thebes.Point3D(1.0, 0.0, 0.0), Thebes.Point3D(2.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(2.0, -1.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(1.0, 0.0, 1.0), Thebes.Point3D(2.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(2.0, -1.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[10, 7, 6, 3, 2, 1], [5, 4, 3, 6], [9, 8, 7, 10], [11, 12, 13, 16, 17, 20], [13, 14, 15, 16], [17, 18, 19, 20], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])
letterV = (Thebes.Point3D[Thebes.Point3D(1.5, 0.0, 0.0), Thebes.Point3D(3.0, -2.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.0, -5.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(1.5, -1.5, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(1.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -2.0, 0.0), Thebes.Point3D(1.5, 0.0, 1.0), Thebes.Point3D(3.0, -2.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.0, -5.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(1.5, -1.5, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(1.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -2.0, 1.0)], Array{Int64,1}[[6, 5, 2, 1], [5, 4, 3, 2], [7, 6, 1, 10], [10, 9, 8, 7], [11, 12, 15, 16], [12, 13, 14, 15], [20, 11, 16, 17], [17, 18, 19, 20], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])
letterW = (Thebes.Point3D[Thebes.Point3D(0.5, 0.0, 0.0), Thebes.Point3D(1.5, -1.5, 0.0), Thebes.Point3D(2.5, 0.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.5, -5.0, 0.0), Thebes.Point3D(2.0, -2.0, 0.0), Thebes.Point3D(1.5, -2.5, 0.0), Thebes.Point3D(1.0, -2.0, 0.0), Thebes.Point3D(0.5, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.5, 0.0, 1.0), Thebes.Point3D(1.5, -1.5, 1.0), Thebes.Point3D(2.5, 0.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.5, -5.0, 1.0), Thebes.Point3D(2.0, -2.0, 1.0), Thebes.Point3D(1.5, -2.5, 1.0), Thebes.Point3D(1.0, -2.0, 1.0), Thebes.Point3D(0.5, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0)], Array{Int64,1}[[1, 8, 7, 2], [7, 6, 3, 2], [6, 5, 4, 3], [10, 9, 8, 1], [12, 17, 18, 11], [12, 13, 16, 17], [13, 14, 15, 16], [11, 18, 19, 20], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])
letterX = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(0.5, 0.0, 0.0), Thebes.Point3D(1.5, -2.0, 0.0), Thebes.Point3D(2.5, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(2.0, -2.5, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.5, -5.0, 0.0), Thebes.Point3D(1.5, -3.0, 0.0), Thebes.Point3D(0.5, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(1.0, -2.5, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(0.5, 0.0, 1.0), Thebes.Point3D(1.5, -2.0, 1.0), Thebes.Point3D(2.5, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(2.0, -2.5, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.5, -5.0, 1.0), Thebes.Point3D(1.5, -3.0, 1.0), Thebes.Point3D(0.5, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(1.0, -2.5, 1.0)], Array{Int64,1}[[3, 2, 1, 12], [6, 5, 4, 3], [6, 3, 12, 9], [9, 8, 7, 6], [12, 11, 10, 9], [24, 13, 14, 15], [15, 16, 17, 18], [21, 24, 15, 18], [18, 19, 20, 21], [21, 22, 23, 24], [1, 2, 14, 13], [2, 3, 15, 14], [3, 4, 16, 15], [4, 5, 17, 16], [5, 6, 18, 17], [6, 7, 19, 18], [7, 8, 20, 19], [8, 9, 21, 20], [9, 10, 22, 21], [10, 11, 23, 22], [11, 12, 24, 23], [12, 1, 13, 24]])
letterY = (Thebes.Point3D[Thebes.Point3D(1.25, 0.0, 0.0), Thebes.Point3D(1.75, 0.0, 0.0), Thebes.Point3D(1.75, -2.75, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(2.5, -5.0, 0.0), Thebes.Point3D(1.5, -3.5, 0.0), Thebes.Point3D(0.5, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(1.25, -2.75, 0.0), Thebes.Point3D(1.25, 0.0, 1.0), Thebes.Point3D(1.75, 0.0, 1.0), Thebes.Point3D(1.75, -2.75, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(2.5, -5.0, 1.0), Thebes.Point3D(1.5, -3.5, 1.0), Thebes.Point3D(0.5, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(1.25, -2.75, 1.0)], Array{Int64,1}[[9, 6, 3, 2, 1], [6, 5, 4, 3], [9, 8, 7, 6], [10, 11, 12, 15, 18], [12, 13, 14, 15], [15, 16, 17, 18], [1, 2, 11, 10], [2, 3, 12, 11], [3, 4, 13, 12], [4, 5, 14, 13], [5, 6, 15, 14], [6, 7, 16, 15], [7, 8, 17, 16], [8, 9, 18, 17], [9, 1, 10, 18]])
letterZ = (Thebes.Point3D[Thebes.Point3D(0.0, 0.0, 0.0), Thebes.Point3D(3.0, 0.0, 0.0), Thebes.Point3D(3.0, -1.0, 0.0), Thebes.Point3D(1.0, -1.0, 0.0), Thebes.Point3D(3.0, -4.0, 0.0), Thebes.Point3D(3.0, -5.0, 0.0), Thebes.Point3D(0.0, -5.0, 0.0), Thebes.Point3D(0.0, -4.0, 0.0), Thebes.Point3D(2.0, -4.0, 0.0), Thebes.Point3D(0.0, -1.0, 0.0), Thebes.Point3D(0.0, 0.0, 1.0), Thebes.Point3D(3.0, 0.0, 1.0), Thebes.Point3D(3.0, -1.0, 1.0), Thebes.Point3D(1.0, -1.0, 1.0), Thebes.Point3D(3.0, -4.0, 1.0), Thebes.Point3D(3.0, -5.0, 1.0), Thebes.Point3D(0.0, -5.0, 1.0), Thebes.Point3D(0.0, -4.0, 1.0), Thebes.Point3D(2.0, -4.0, 1.0), Thebes.Point3D(0.0, -1.0, 1.0)], Array{Int64,1}[[1, 4, 3, 2], [1, 10, 9, 6, 5, 4], [9, 8, 7, 6], [12, 13, 14, 11], [14, 15, 16, 19, 20, 11], [16, 17, 18, 19], [1, 2, 12, 11], [2, 3, 13, 12], [3, 4, 14, 13], [4, 5, 15, 14], [5, 6, 16, 15], [6, 7, 17, 16], [7, 8, 18, 17], [8, 9, 19, 18], [9, 10, 20, 19], [10, 1, 11, 20]])


module ImageClassification

using ...NaiveGAflux
using ..AutoFlux: fit
using NaiveGAflux: GlobalPool
using NaiveGAflux: shapetrace, squashshapes, fshape, ndimsout, check_apply
using NaiveGAflux: StatefulGenerationIter
using NaiveNASlib.Advanced, NaiveNASlib.Extend
import Flux
using Flux: Dense, Conv, ConvTranspose, DepthwiseConv, CrossCor, LayerNorm, BatchNorm, InstanceNorm, GroupNorm,
            MaxPool, MeanPool, Dropout, AlphaDropout, GlobalMaxPool, GlobalMeanPool, cpu, gpu
using Flux: Descent, Momentum, Nesterov, ADAM, NADAM, ADAGrad, WeightDecay
import Functors
using Functors: fmap
using Random
import Logging
using Statistics
import IterTools: ncycle

# To store program state for pause/resume
using Serialization

export ImageClassifier, fit, TrainSplitAccuracy, TrainAccuracyVsSize, AccuracyVsSize, TrainIterConfig, BatchedIterConfig, ShuffleIterConfig, GlobalOptimizerMutation, EliteAndSusSelection, EliteAndTournamentSelection

modelname(c::AbstractCandidate) = NaiveGAflux.model(modelname, c)
modelname(g::CompGraph) = split(name(g.inputs[]),'.')[1]

include("strategy.jl")
include("archspace.jl")

"""
    ImageClassifier
    ImageClassifier(popinit, popsize, seed)
    ImageClassifier(;popsize=50, seed=1, newpop=false, mdir=defaultdir("ImageClassifier"); insize, outsize)

Type to make `AutoFlux.fit` train an image classifier using initial population size `popsize` using random seed `seed`.

Load models from `mdir` if directory contains models. If persistence is used (e.g. by providing `cb=persist` to `fit`) candidates will be stored in this directory.

If `newpop` is `true` the process will start with a new population and existing state in the specified directory will be overwritten.
"""
struct ImageClassifier{F}
    popinit::F
    popsize::Int
    seed::Int
end
function ImageClassifier(;popsize=50, seed=1, newpop=false, mdir=defaultdir("ImageClassifier"), insize, outsize)
    popinit = () -> generate_persistent(popsize, newpop, mdir, insize, outsize)
    return ImageClassifier(popinit, popsize, seed)
end

"""
    fit(c::ImageClassifier, x, y; cb, fitnesstrategy, evolutionstrategy, stopcriterion)

Return a population of image classifiers fitted to the given data.

# Arguments
- `c::ImageClassifier`: Type of models to train. See [`ImageClassifier`](@ref).

- `x`: Input data. Must be a 4D array.

- `y`: Output data. Can either be an 1D array in which case it is assumed that `y` is the raw labes (e.g. `["cat", "dog", "cat", ...]`) or a 2D array in which case it is assumed that `y` is one-hot encoded.

- `cb=identity`: Callback function. After training and evaluating each generation but before evolution `cb(population)` will be called where `population` is the array of candidates. Useful for persistence and plotting.

- `fitnesstrategy::AbstractFitnessStrategy=`[`TrainSplitAccuracy()`](@ref): Strategy for fitness from data.

- `evolutionstrategy::AbstractEvolutionStrategy=`[`EliteAndTournamentSelection(popsize=c.popsize)`](@ref): Strategy for evolution.

- `stopcriterion`: Takes the current population and returns true if fitting shall stop. Candidate fitness is available by calling `fitness(c)` where `c` is a member of the population.

"""
function AutoFlux.fit(c::ImageClassifier, x::AbstractArray, y::AbstractArray;
                            cb=identity,
                            fitnesstrategy::AbstractFitnessStrategy=TrainSplitAccuracy(),
                            evolutionstrategy::AbstractEvolutionStrategy=EliteAndTournamentSelection(popsize=c.popsize),
                            stopcriterion = pop -> generation(pop) > 100)
    ndims(x) == 4 || error("Must use 4D data, got $(ndims(x))D data")

    inshape = size(x)[1:2]
    return fit(c, fitnessfun(fitnesstrategy, x, y), evostrategy(evolutionstrategy, inshape), stopcriterion; cb)
end

"""
    fit(c::ImageClassifier, fitnesstrategy::AbstractFitness, evostrategy::AbstractEvolution, stopcriterion; cb)

Return a population of image classifiers fitted to the given data.

Lower level version of `fit` to use when `fit(c::ImageClassifier, x, y)` doesn't cut it.

# Arguments
- `c::ImageClassifier`: Type of models to train. See [`ImageClassifier`](@ref).

- `fitnessstrategy`: An `AbstractFitness` used to compute the fitness metric for a candidate.

- `evostrategy::AbstractEvolution`: Evolution strategy to use. Population `p` will be evolved through `p = evolve(evostrategy, p)`.

- `stopcriterion`: Takes the current population and returns true if fitting shall stop. Candidate fitness is available by calling `fitness(c)` where `c` is a member of the population.

- `cb=identity`: Callback function. After training and evaluating each generation but before evolution `cb(population)` will be called where `population` is the array of candidates. Useful for persistence and plotting.
"""
function AutoFlux.fit(c::ImageClassifier, fitnesstrategy::AbstractFitness, evostrategy::AbstractEvolution, stopcriterion; cb = identity)
    Random.seed!(NaiveGAflux.rng_default, c.seed)
    @info "Start training with baseseed: $(c.seed)"

    population = c.popinit()

    # If experiment was resumed we should start by evolving as population is persisted right before evolution
    population = generation(population) > 1 ? evolve(evostrategy, population) : population

    logfitness = LogFitness(;fitnesstrategy)

    return evolutionloop(population, evostrategy, logfitness, stopcriterion, cb)
end

function evolutionloop(population, evostrategy, fitnesstrategy, stop, cb)
    while true
        @info "Begin generation $(generation(population))"

        fittedpopulation = fitness(population, fitnesstrategy)
        cb(fittedpopulation)
        stop(fittedpopulation) && return fittedpopulation

        population = evolve(evostrategy, fittedpopulation)
    end
end

function generate_persistent(nr, newpop, mdir, insize, outsize, cwrap=identity, archspace = initial_archspace(insize[1:2], outsize))
    if newpop
        rm(mdir, force=true, recursive=true)
    end

    iv(i) = conv2dinputvertex(join(["model", i, ".input"]), insize[3])
    return Population(PersistentArray(mdir, nr, i -> create_model(join(["model", i]), archspace, iv(i), cwrap)))
end
function create_model(name, as, in, cwrap)
    optselect = optmutation(1.0)
    opt = optselect(Descent(rand() * 0.099 + 0.01))
    # Always cache even if not strictly needed for all fitnessfunctions because consequence is so bad if one forgets
    # it when needed. Users who know what they are doing can unwrap if caching is not wanted.
    cwrap(CandidateOptModel(opt, CompGraph(in, as(name, in))))
end

end


module WebModelLibs
#
# This contains the code to make the interactive parts of https://stb.virtual-worlds.scot, which is an early and very much simplified
# version of this model.
#
# Not currently used and will be updated moved to own repository. Kept around for reference.
#
#
using JSON3
using DataFrames
using CSV
using StatsBase
using Random
using Logging
using PovertyAndInequalityMeasures
using BudgetConstraints

import ScottishTaxBenefitModel: FRSHouseholdGetter, ExampleHouseholdGetter,
    ModelHousehold, Utils, MiniTB, GeneralTaxComponents, Definitions
using .FRSHouseholdGetter
using .ExampleHouseholdGetter
using .ModelHousehold
using .Utils
using .MiniTB
using .GeneralTaxComponents
using .RunSettings
using .Definitions

export do_one_run, local_makebc, load_data
export print_output_to_csv, create_base_results, summarise_results!

function load_data(; load_examples::Bool, load_main :: Bool, start_year = 2015 )
   example_names = Vector{AbstractString}()
   num_households = 0
   if load_examples
      example_names = ExampleHouseholdGetter.initialise(Settings())
   end
   if load_main
      rc = @timed begin
         num_households,num_people,nhh2 =
            FRSHouseholdGetter.initialise(
            household_name = "model_households_scotland",
            people_name    = "model_people_scotland",
            start_year = start_year )
      end
      mb = trunc(Integer, rc[3] / 1024^2)
      @info "loaded data; load time $(rc[2]); memory used $(mb)mb; loaded $num_households households\nready..."
   end
   (example_names, num_households, num_people )
end

const mr_edges = [-99999.99, 0.0, 0.1, 0.25, 0.5, 0.75, 1.0, 9999.0]
const growth = 0.02

function poverty_targetting_adder( dfr :: DataFrameRow, data :: Dict ) :: Real
   which = data[:which_element]
   if dfr.net_income_1 <= data[:poverty_line]
      return dfr[which]*dfr.weight_1
   end
   return 0.0
end

function create_base_results( num_households :: Integer, num_people :: Integer )
   base_results = do_one_run( MiniTB.DEFAULT_PARAMS, num_households, num_people, 1 )
   basenames = names( base_results )
   basenames = addsysnotoname( basenames, 1 )
   rename!( base_results, basenames )
   base_results
end

function characteristic_targetting_adder( dfr :: DataFrameRow, data :: Dict ) :: Real
   which = data[:which_element]
   characteristic = data[:characteristic]
   if dfr[characteristic] in data[:targets]
      return dfr[which]*dfr.weight_1
   end
   return 0.0
end

function operate_on_frame( results :: DataFrame, adder, data::Dict )
   n = size( results )[1]
   total = 0
   for i in 1:n
      total += adder( results[i,:], data )
   end
   total
end

function add_targetting( results :: DataFrame, total_spend:: AbstractArray, item_name :: AbstractString, poverty_line :: Real ) :: AbstractArray
    targetting = zeros(3)
    for sys in 1:3
        key = Symbol( "$(item_name)_$sys" )
        on_target = operate_on_frame( results, poverty_targetting_adder,
            Dict(
             :which_element=>key,
             :poverty_line=>poverty_line
            )
        )
        targetting[sys] = on_target
    end
    # targetting[3] = targetting[2]-targetting[1]
    for sys in 1:3
        if !(total_spend[sys] == 0.0)
            targetting[sys] /= total_spend[sys]
            targetting[sys] *= 100.0
        end
    end # loop to props
    targetting
end

function summarise_results!(; results::DataFrame, base_results :: DataFrame )::NamedTuple

    n_names = names( results )
    n_names_2 = addsysnotoname( n_names, 2 )
    rename!( results, n_names_2 )
    results = hcat( base_results, results )

    @assert results.pid_1 == results.pid_2
    for name in n_names
        diff_name = Symbol(String(name) * "_3")
        name_1 = Symbol(String(name) * "_1")
        name_2 = Symbol(String(name) * "_2")
        try
            results[!,diff_name] = results[!,name_2] - results[!,name_1]
        catch
            ; # idiot check for non numeric cols
        end
    end

    # CSV.write( "/home/graham_s/tmp/stb_test_results.tab", results, delim='\t')

    deciles = []
    push!( deciles, PovertyAndInequalityMeasures.binify( results, 10, :weight_1, :net_income_1 ))
    push!( deciles, PovertyAndInequalityMeasures.binify( results, 10, :weight_1, :net_income_2 ))
    push!( deciles, deciles[2] - deciles[1] )

    poverty_line = deciles[1][5,3]*(2.0/3.0)

    inequality = []
    push!( inequality, PovertyAndInequalityMeasures.make_inequality( results, :weight_1, :net_income_1 ))
    push!( inequality, PovertyAndInequalityMeasures.make_inequality( results, :weight_1, :net_income_2 ))
    push!( inequality, diff_between( inequality[2], inequality[1] ))

    poverty = []
    push!(poverty, PovertyAndInequalityMeasures.make_poverty( results, poverty_line, growth, :weight_1, :net_income_1  ))
    push!( poverty, PovertyAndInequalityMeasures.make_poverty( results, poverty_line, growth, :weight_1, :net_income_2  ))
    push!( poverty, diff_between( poverty[2], poverty[1] ))

    totals = []
    totals_1 = Dict()
    totals_1["total_taxes"]=sum(results[!,:total_taxes_1].*results[!,:weight_1])
    totals_1["total_benefits"]=sum(results[!,:total_benefits_1].*results[!,:weight_1])
    totals_1["benefit1"]=sum(results[!,:benefit1_1].*results[!,:weight_1])
    totals_1["benefit2"]=sum(results[!,:benefit2_1].*results[!,:weight_1])
    totals_1["basic_income"]=sum(results[!,:basic_income_1].*results[!,:weight_1])
    totals_1["vat"]=sum(results[!,:vat_1].*results[!,:weight_1])
    totals_1["other_indirect"]=sum(results[!,:other_indirect_1].*results[!,:weight_1])
    totals_1["total_indirect"]=sum(results[!,:total_indirect_1].*results[!,:weight_1])
    totals_1["net_incomes"]=sum(results[!,:net_income_1].*results[!,:weight_1]) # FIXME not true if we have min wage or (maybe) indirect taxes

    totals_2 = Dict()
    totals_2["total_taxes"]=sum(results[!,:total_taxes_2].*results[!,:weight_1])
    totals_2["total_benefits"]=sum(results[!,:total_benefits_2].*results[!,:weight_1])
    totals_2["benefit1"]=sum(results[!,:benefit1_2].*results[!,:weight_1])
    totals_2["benefit2"]=sum(results[!,:benefit2_2].*results[!,:weight_1])
    totals_2["basic_income"]=sum(results[!,:basic_income_2].*results[!,:weight_1])
    totals_2["vat"]=sum(results[!,:vat_1].*results[!,:weight_2])
    totals_2["other_indirect"]=sum(results[!,:other_indirect_2].*results[!,:weight_1])
    totals_2["total_indirect"]=sum(results[!,:total_indirect_2].*results[!,:weight_1])
    totals_2["net_incomes"]=sum(results[!,:net_income_2].*results[!,:weight_1]) # FIXME not true if we have min wage or (maybe) indirect taxes

    totals_3 = diff_between(totals_2, totals_1)

    push!( totals, totals_1 )
    push!( totals, totals_2 )
    push!( totals, totals_3 )

    disallowmissing!( results )

    # these are for the gain lose weights below
    results.gainers = (((results.net_income_2 - results.net_income_1)./results.net_income_1).>=0.01).*results.weight_1
    results.losers = (((results.net_income_2 - results.net_income_1)./results.net_income_1).<= -0.01).*results.weight_1
    results.nc = ((abs.(results.net_income_2 - results.net_income_1)./results.net_income_1).< 0.01).*results.weight_1

    unit_count = sum( results.weight_1 )

    gainlose_totals = (
        losers = sum( results.losers ),
        nc = sum( results.nc ),
        gainers = sum( results.gainers ))

    gainlose_by_thing = (
        thing=levels( results.thing_1 ),
        losers = counts(results.thing_1,fweights( results.losers )),
        nc= counts(results.thing_1,fweights( results.nc )),
        gainers = counts(results.thing_1,fweights( results.gainers )))
    gainlose_by_sex = (
        sex=pretty.(levels( results.sex_1 )),
        losers = counts(Int.(results.sex_1),fweights( results.losers )),
        nc= counts(Int.(results.sex_1),fweights( results.nc )),
        gainers = counts(Int.(results.sex_1),fweights( results.gainers )))

    metr_histogram = []
    avg_metr = zeros(3)
    wsum = sum( results.weight_1 )
    avg_metr[1] = sum( results.metr_1.*results.weight_1)./wsum
    avg_metr[2] = sum( results.metr_2.*results.weight_1)./wsum
    avg_metr[3] = sum( results.metr_3.*results.weight_1)./wsum

    push!( metr_histogram, fit(Histogram,results.metr_1,Weights(results.weight_1),mr_edges,closed=:right).weights )
    push!( metr_histogram, fit(Histogram,results.metr_2,Weights(results.weight_1),mr_edges,closed=:right).weights )
    push!( metr_histogram, metr_histogram[2]-metr_histogram[1] )

    @debug  "totals[1] $totals[1]"

    targetting_total_benefits =
        add_targetting( results,
                [totals[1]["total_benefits"],
                 totals[2]["total_benefits"],
                 totals[3]["total_benefits"]],
            "total_benefits", poverty_line )
    targetting_benefit1 = add_targetting( results, [totals[1]["benefit1"],totals[2]["benefit1"],totals[3]["benefit1"]], "benefit1", poverty_line )
    targetting_benefit2 = add_targetting( results, [totals[1]["benefit2"],totals[2]["benefit2"],totals[3]["benefit2"]], "benefit2", poverty_line )
    targetting_basic_income = add_targetting( results, [totals[1]["basic_income"],totals[2]["basic_income"],totals[3]["basic_income"]], "basic_income", poverty_line )

    for i in 1:3
        mult_dict!(totals[i], WEEKS_PER_YEAR ) # annualise
    end

    summary_output = (
        gainlose_totals=gainlose_totals,
        gainlose_by_sex=gainlose_by_sex,
        gainlose_by_thing=gainlose_by_thing,

        poverty=poverty,

        inequality=inequality,
        avg_metr=avg_metr,
        metr_histogram=metr_histogram,
        metr_axis=mr_edges,

        deciles=deciles,

        targetting_total_benefits = targetting_total_benefits,
        targetting_benefit1=targetting_benefit1,
        targetting_benefit2=targetting_benefit2,
        targetting_basic_income=targetting_basic_income,

        totals=totals,
        poverty_line=poverty_line,
        growth_assumption=growth,
        unit_count=unit_count
    )
    summary_output
end

function print_output_to_csv( output :: NamedTuple, dir :: AbstractString = "/var/tmp/" ) :: AbstractString
    datestr = basiccensor( "$(now())")
    randstr = randstring( 32 )
    filename = "$dir/stb_$(datestr)_$(randstr).tab"
    f = open( filename, write=true )
    #CSV.write( f, ["A","B"])
    CSV,write( f, [1 2 3; 4 5 6])
    filename
end

function map_to_example( modelpers :: ModelHousehold.Person ) :: MiniTB.Person
   inc = 0.0
   for (k,v) in modelpers.income
      inc += v
   end
   sex = MiniTB.Female
   if modelpers.sex == Definitions.Male ## easier way?
      sex = MiniTB.Male
   end
   MiniTB.Person( modelpers.pid, inc, modelpers.usual_hours_worked, modelpers.age, sex )
end

function local_getnet(data :: Dict, gross::Real)::Real
   person = data[:person]
   person.wage = gross
   person.hours = gross/MiniTB.DEFAULT_WAGE
   rc = MiniTB.calculate_internal( person, data[:params ] )
   return rc[:netincome]
end

function local_makebc(
    person :: MiniTB.Person,
    tbparams :: MiniTB.TBParameters,
    settings :: BCSettings = BudgetConstraints.Settings() ) :: NamedTuple
   data = Dict( :person=>person, :params=>tbparams )
   bc = BudgetConstraints.makebc( data, local_getnet, settings )
   annotations = annotate_bc( bc )
   ( points = pointstoarray( bc ), annotations = annotations )
end

function make_results_frame( n :: Integer ) :: DataFrame
   DataFrame(
     pid = Vector{Union{BigInt,Missing}}(missing, n),
     weight = Vector{Union{Real,Missing}}(missing, n),
     sex = Vector{Union{Gender,Missing}}(missing, n),
     thing = Vector{Union{Integer,Missing}}(missing, n),
     gross_income = Vector{Union{Real,Missing}}(missing, n),

     total_taxes = Vector{Union{Real,Missing}}(missing, n),
     total_benefits = Vector{Union{Real,Missing}}(missing, n),
     income_tax = Vector{Union{Real,Missing}}(missing, n),
     benefit1 = Vector{Union{Real,Missing}}(missing, n),
     benefit2 = Vector{Union{Real,Missing}}(missing, n),
     basic_income = Vector{Union{Real,Missing}}(missing, n),
     net_income = Vector{Union{Real,Missing}}(missing, n),
     metr = Vector{Union{Real,Missing}}(missing, n),
     tax_credit = Vector{Union{Real,Missing}}(missing, n),
     vat = Vector{Union{Real,Missing}}(missing, n),
     other_indirect = Vector{Union{Real,Missing}}(missing, n),
     total_indirect = Vector{Union{Real,Missing}}(missing, n))
end

function do_one_run( tbparams::TaxBenefitSystem, num_households :: Integer, num_people :: Integer ) :: DataFrame
   results = make_results_frame( num_people )
   pnum = 0
   for hhno in 1:num_households
      frshh = FRSHouseholdGetter.get_household( hhno )
      for (pid,frsperson) in frshh.people
         pnum += 1
         if pnum > num_people
            # break
            @goto end_of_calcs
         end
         experson = map_to_example( frsperson )
         rc = nothing
         for i in 1:num_repeats
            rc = MiniTB.calculate( experson, tbparams )
         end
         res = results[pnum,:]
         res.pid = experson.pid
         res.sex = experson.sex
         res.gross_income = experson.wage
         res.weight = frshh.weight
         res.thing = rand(1:10)
         res.tax = rc[:tax]
         res.benefit1 = rc[:benefit1]
         res.benefit2 = rc[:benefit2]
         res.basic_income = rc[:basic_income]

         res.total_taxes= rc[:tax]
         res.total_benefits = rc[:benefit2]+rc[:benefit1]+rc[:basic_income]
         res.net_income = rc[:netincome]
         res.metr = rc[:metr]
         res.tax_credit = rc[:tax_credit]
         res.vat = 0.0
         res.other_indirect = 0.0
         res.total_indirect = 0.0
      end # people
   end # hhlds
   @label end_of_calcs
   ran = rand()
   print("Done; people $pnum rand=$ran\n")
   results[1:pnum-1,:];
end

end