text:
import AlphaZero.GI
using StaticArrays

@enum ActionType paika=0 approach=1 withdrawal=2 pass=3
@enum BoardSize small=9 medium=25 large=45

### CONSTANTS
const EMPTY = 0x00
const WHITE = 0x01
const BLACK = 0x02
const DEFAULT_SIZE = large

### TYPES
const Player = UInt8
const Cell = UInt8
const Coord = UInt8
const Direction = UInt8
const Board = Array{Cell}

### DIFFERENT INITIAL BOARDS
const INITIAL_BOARD_LARGE = MVector{45, Cell}(
    BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,
    BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,BLACK,
    BLACK,WHITE,BLACK,WHITE,EMPTY,BLACK,WHITE,BLACK,WHITE,
    WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,
    WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,WHITE,WHITE
)

const INITIAL_BOARD_MEDIUM = MVector{25, Cell}(
    BLACK,BLACK,BLACK,BLACK,BLACK,
    BLACK,BLACK,BLACK,BLACK,BLACK,
    BLACK,WHITE,EMPTY,BLACK,WHITE,
    WHITE,WHITE,WHITE,WHITE,WHITE,
    WHITE,WHITE,WHITE,WHITE,WHITE
)

const INITIAL_BOARD_SMALL = MVector{9, Cell}(
    BLACK,BLACK,BLACK,
    BLACK,EMPTY,WHITE,
    WHITE,WHITE,WHITE
)

#######################################
### IMPLEMENTATION OF GAMEINTERFACE ###
#######################################

"""
Structure to store information related to an action
"""
struct Action
    type :: ActionType
    x0 :: Coord
    y0 :: Coord
    x1 :: Coord
    y1 :: Coord
    direction :: Direction
end

"""
Implementation of abstract type for a game specification.

The specification holds all _static_ information about a game, which does not
depend on the current state.
"""
struct GameSpec <: GI.AbstractGameSpec
    possible_actions :: Array{Action}
    action_indices :: Dict{Action, Int}
    size :: BoardSize
    num_cols :: UInt8
    num_rows :: UInt8
    num_cells :: UInt8
    pieces_per_player :: UInt8
    num_actions :: Int
    initial_board :: Board

    function GameSpec()
        return GameSpec(DEFAULT_SIZE)
    end

    function GameSpec(size::BoardSize)
        num_cols = size == small ? 0x03 : size == medium ? 0x05 : 0x09
        num_rows = size == small ? 0x03 : 0x05
        num_cells = num_cols * num_rows
        pieces_per_player = UInt8(floor(num_cells/2))
        initial_board = size == small ? INITIAL_BOARD_SMALL : size == medium ? INITIAL_BOARD_MEDIUM : INITIAL_BOARD_LARGE
        possible_actions = calc_possible_actions(num_cols, num_rows)
        action_indices = calc_action_indices(possible_actions)
        num_actions = length(possible_actions)
        return new(possible_actions, action_indices, size, num_cols, num_rows, num_cells, pieces_per_player, num_actions, initial_board)
    end
end

"""
Implementation of abstract base type for a game environment.

Intuitively, a game environment holds a game specification and a current state.
"""
mutable struct GameEnv <: GI.AbstractGameEnv
    spec :: GameSpec
    board :: Board
    current_player :: Player
    white_pieces :: Int
    black_pieces :: Int
    action_mask :: Array{Bool}
    extended_capture :: Bool
    current_position :: Tuple
    last_positions :: Array{Tuple}
    last_direction :: Int
end

"""
Create a new game environment in an initial state depending on the size specified.
"""
function GI.init(spec::GameSpec)
    board = copy(spec.initial_board)
    current_player = WHITE
    env = GameEnv(spec, board, current_player, spec.pieces_per_player, spec.pieces_per_player, [], false, (0x0, 0x0), [], 0)
    update_actions_mask!(env)
    return env
end

"""
Return the game specification of an environment.
"""
GI.spec(g::GameEnv) = g.spec

#####
##### Queries on specs
#####

"""
Return whether or not a game is a two-players game.
"""
GI.two_players(::GameSpec) = true

"""
Return the vector of all game actions.
"""
GI.actions(spec::GameSpec) = spec.possible_actions

"""
Return a vectorized representation of a given state.
"""
function GI.vectorize_state(spec::GameSpec, state)
    board = state.current_player == WHITE ? state.board : flip_colors(state.board)
    return Float32[
        board[cord_to_pos(spec, col, row)] == c
        for col in 0x01:spec.num_cols,
            row in 0x01:spec.num_rows,
            c in [EMPTY, WHITE, BLACK]
    ]
end

#####
##### Operations on envs
#####

"""
Modify the state of a game environment in place.
"""
function GI.set_state!(g::GameEnv, state)
    g.board = copy(state.board)
    g.current_player = state.current_player
    g.last_direction = state.last_direction
    g.last_positions = copy(state.last_positions) #this copy is necessary
    g.extended_capture = state.extended_capture
    g.current_position = (state.current_position[1], state.current_position[2])
    g.white_pieces = count(==(WHITE), g.board)
    g.black_pieces = count(==(BLACK), g.board)
    if g.extended_capture
        update_actions_mask_extended_capture!(g)
    else
        update_actions_mask!(g)
    end
end

"""
Return the game state.
"""
function GI.current_state(g::GameEnv)
    state_copy = (board=copy(g.board),
        current_player=g.current_player,
        extended_capture=g.extended_capture,
        last_positions=copy(g.last_positions),
        last_direction=g.last_direction,
        current_position=(g.current_position[1], g.current_position[2])
    )
    return state_copy
end

"""
Return a boolean indicating whether or not the game is in a terminal state.
"""
function GI.game_terminated(g::GameEnv)
    return !(g.white_pieces > 0 && g.black_pieces > 0)
end

"""
Return `true` if white is to play and `false` otherwise.
"""
function GI.white_playing(g::GameEnv)
    return g.current_player == WHITE
end

"""
Return a boolean mask indicating what actions are available.

The following identities must hold:

  - `game_terminated(game) || any(actions_mask(game))`
  - `length(actions_mask(game)) == length(actions(spec(game)))`
"""
function GI.actions_mask(g::GameEnv)
    return g.action_mask
end

"""
Update the game environment by making the current player perform `action`.
Note that this function does not have to be deterministic.
"""
function GI.play!(g::GameEnv, action::Action)
    if action.type == pass
        swap_player!(g)
    elseif action.type == paika
        apply_action!(g, action)
        swap_player!(g)
    else
        apply_action!(g, action)

        g.extended_capture = true
        g.last_direction = action.direction
        push!(g.last_positions, (action.x0, action.y0))
        g.current_position = (action.x1, action.y1)

        found_capture = update_actions_mask_extended_capture!(g)

        if !found_capture
            swap_player!(g)
        end
    end
    # if g.white_pieces + g.black_pieces < 5
    #     print_state_information(g)
    # end
end

"""
Return the intermediate reward obtained by the white player after the last
transition step. The result is undetermined when called at an initial state.
"""
function GI.white_reward(g::GameEnv)
    if GI.game_terminated(g)
        return g.white_pieces > 0.0 ? 1.0 : -1.0
    else
        return 0
    end
end

"""
Return a heuristic estimate of the state value for the current player.

The given state must be nonfinal and returned values must belong to the
``(-∞, ∞)`` interval.

This function is not needed by AlphaZero but it is useful for building
baselines such as minmax players.
"""
function GI.heuristic_value(g::GameEnv)
    if g.current_player == WHITE
        return white_heuristic_value(g)
    else
        return -white_heuristic_value(g)
    end
end

#####
##### Interface for interactive exploratory tools
#####

"""
Print the game state on the standard output.
"""
function GI.render(g::GameEnv)
    board = g.board
    buffer = IOBuffer()

    xLength = g.spec.num_cols * 02 - 01
    yLength = g.spec.num_rows * 02 - 01

    player = g.current_player == WHITE ? 'W' : 'B'

    if g.spec.size == small
        println(buffer, player * " a   b   c")
    elseif g.spec.size == medium
        println(buffer, player * " a   b   c   d   e")
    else
        println(buffer, player * " a   b   c   d   e   f   g   h   i")
    end

    for y in UnitRange(1, yLength)
        print(buffer, y % 2 != 0 ? string(ceil(Int, y / 2)) * " " : "  ")
        for x in UnitRange(1, xLength)
            if x % 2 != 0
                if y % 2 != 0
                    pos = cord_to_pos(g.spec, floor(UInt8, x/2 + 1), floor(UInt8, y/2 + 1))
                    val = board[pos]
                    print(buffer, val == 0 ? " " : val == 1 ? "W" : "B")
                else
                    print(buffer, "|")
                end
            else
                if y % 2 != 0
                    print(buffer, " - ")
                else
                    print(buffer, (x + y) % 4 == 2 ? " / " : " \\ ")
                end
            end
        end
        println(buffer, "")
    end
    return String(take!(buffer))
end

"""
Return a human-readable string representing the provided action.
"""
function GI.action_string(::GameSpec, action::Action)
    if action.type == pass
        return "pass"
    else
        return Char(action.x0 + 96) * string(action.y0) * Char(action.x1 + 96) * string(action.y1) * (action.type == paika ? 'P' : action.type == approach ? 'A' : action.type == withdrawal ? 'W' : error("Unknown action type $(action.type)"))
    end
end

"""
Return the action described by string `str` or `nothing` if `str` does not
denote a valid action.
"""
function GI.parse_action(::GameSpec, str::String)
    if str == "pass"
        return Action(pass, 0x00, 0x00, 0x00, 0x00, 0x00)
    else
        parts = collect(Char, str)
        x0 = Coord(parts[1] - 96)
        y0 = Coord(parse(Int, parts[2]))
        x1 = Coord(parts[3] - 96)
        y1 = Coord(parse(Int, parts[4]))
        d = determine_direction(x0, y0, x1, y1)
        return Action(x0, y0, x1, y1, d, paika : parts[5] == 'A' ? approach : withdrawal)
    end
end

"""
Read a state from the standard input.
Return the corresponding state (with type `state_type(game_spec)`)
or `nothing` in case of an invalid input.
"""
function GI.read_state(spec::GameSpec)
    board = Board(undef, NUM_CELLS)
    println(board)
    try
        i = 1
        input = lowercase(readline())
        player = input[1] == 'w' ? WHITE : input[1] == 'b' ? BLACK : error("First character must either be 'W' or 'B' to identify the current player")
        linecount = spec.size == small ? 6 : 10
        line = 1

        while line < linecount
            input = lowercase(readline())
            print("read line: $input")
            line += 1
            if line % 2 == 0
                for (col, val) in enumerate(input)
                    if (col % 4 == 3)
                        # println("val $val col $col index $i")
                        if val == 'w'
                            board[i] = WHITE
                        elseif val == 'b'
                            board[i] = BLACK
                        else
                            board[i] = EMPTY
                        end
                        i += 1
                    end
                end
            end
        end
        return (board, player)
    catch e
        println("Error while reading input: $e")
        return nothing
    end
end

######################
### HELPER METHODS ###
######################

"""
Calculates the opponent of given player and returns it
"""
other(p::Player) = 0x03 - p

"""
Applies an action inplace to given GameEnv. Is capable of applying any simple action.
"""
function apply_action!(g::GameEnv, action::Action)
    if action.type == approach
        dirX = action.x1 - action.x0
        dirY = action.y1 - action.y0
        capture!(g, action.x1 + dirX, action.y1 + dirY, dirX, dirY, other(g.current_player))
    elseif action.type == withdrawal
        dirX = action.x0 - action.x1
        dirY = action.y0 - action.y1
        capture!(g, action.x1 + 0x02 * dirX, action.y1 + 0x02 * dirY, dirX, dirY, other(g.current_player))
    end
    setindex!(g.board, EMPTY, cord_to_pos(g.spec, action.x0, action.y0))
    setindex!(g.board, g.current_player, cord_to_pos(g.spec, action.x1, action.y1))
end

"""
Changes the current player, updates and resets all relevant datastructures inplace.
"""
function swap_player!(g::GameEnv)
    g.current_player = other(g.current_player)
    g.extended_capture = false
    g.last_positions = []
    g.last_direction = 0
    g.current_position = (0x0, 0x0)
    update_actions_mask!(g)
end

"""
Applies a capturing move to GameEnv inplace
"""
function capture!(g::GameEnv, x, y, dirX, dirY, opponent::Player)
    setindex!(g.board, 0x00, cord_to_pos(g.spec, x, y))
    if opponent == WHITE
        g.white_pieces -= 1
    else
        g.black_pieces -= 1
    end

    x1 = x + dirX
    y1 = y + dirY

    if is_in_board_space(g.spec, x1, y1) && g.board[cord_to_pos(g.spec, x1, y1)] == opponent
        capture!(g, x1, y1, dirX, dirY, opponent)
    end
end

"""
Flips the value of a single board cell.
"""
flip_cell_color(c::Cell) = c == EMPTY ? EMPTY : other(c)

"""
Flips the values of the whole board.
"""
function flip_colors(board::Board)
    return Cell[
        flip_cell_color(board[position])
        for position in 1:length(board)
    ]
end

"""
Calculates all possible actions for the current board.
"""
function calc_possible_actions(num_cols::UInt8, num_rows::UInt8)
    actions = Action[]

    for x0 in 0x01:num_cols, y0 in 0x01:num_rows
        for (x1, y1, direction) in get_surrounding_nodes(num_cols, num_rows, x0, y0)
            push!(actions, Action(paika, x0, y0, x1, y1, direction))
            push!(actions, Action(approach, x0, y0, x1, y1, direction))
            x_w, y_w = new_coords(x0, y0, opposite_direction(direction)) # calculate position where opponent would reside when withdrawing
            if is_in_board_space(num_cols, num_rows, x_w, y_w) # if the opponents position would be on the board then add this possible move
                push!(actions, Action(withdrawal, x0, y0, x1, y1, direction))
            end
        end
        # x1, y1 = new_coords(x0, y0, direction)
        # # if is_in_board_space(x1, y1)
        # index = action_index(position, direction)
        # actions[index + UInt8(paika)] = (paika, x0, y0, x1, y1, direction)
        # actions[index + UInt8(approach)] = (approach, x0, y0, x1, y1, direction)
        # actions[index + UInt8(withdrawal)] = (withdrawal, x0, y0, x1, y1, direction)
        # end
    end
    push!(actions, Action(pass, 0x00, 0x00, 0x00, 0x00, 0x00))
    return actions
end

function calc_action_indices(actions::Array{Action})
    indices = Dict{Action, Int}()
    index = 1

    for action in actions
        indices[action] = index
        index += 1
    end

    return indices
end

function action_index(spec::GameSpec, position::UInt8, direction::Direction, type::ActionType)
    x, y = pos_to_cord(spec, position)
    return action_index(spec, x, y, direction, type)
end

function action_index(spec::GameSpec, x::Coord, y::Coord, direction::Direction, type::ActionType)
    x1, y1 = new_coords(x, y, direction)
    action = Action(type, x, y, x1, y1, direction)
    return spec.action_indices[action]
end

"""
Calculates the destination coordinates of an action originating in (x0, y0) in direction
"""
function new_coords(x0::Coord, y0::Coord, direction::Direction)
    x1 = Int # destination coordinate x (columns)
    y1 = Int # destination coordinate y (rows)

    if direction == 0x01
        x1 = x0
        y1 = y0 - 0x01
    elseif direction == 0x02
        x1 = x0 + 0x01
        y1 = y0 - 0x01
    elseif direction == 0x03
        x1 = x0 + 0x01
        y1 = y0
    elseif direction == 0x04
        x1 = x0 + 0x01
        y1 = y0 + 0x01
    elseif direction == 0x05
        x1 = x0
        y1 = y0 + 0x01
    elseif direction == 0x06
        x1 = x0 - 0x01
        y1 = y0 + 0x01
    elseif direction == 0x07
        x1 = x0 - 0x01
        y1 = y0
    elseif direction == 0x08
        x1 = x0 - 0x01
        y1 = y0 - 0x01
    else
        error("Direction out of bounds (0x01:0x08)")
    end

    return x1, y1
end

"""
Determines the direction according to the origin (x0, y0) and destination (x1, y1)
"""
function determine_direction(x0::Coord, y0::Coord, x1::Coord, y1::Coord)
    if x0 < x1 #+x
        if y0 < y1 #+y
            return 0x04
        elseif y0 == y1
            return 0x03
        else
            return 0x02
        end
    elseif x1 < x0 #-x
        if y0 < y1 #+y
            return 0x06
        elseif y0 == y1
            return 0x07
        else
            return 0x08
        end
    else
        if y0 < y1 #+y
            return 0x05
        else
            return 0x01
        end
    end
end

"""
Converts position to x, y coordinates
"""
function pos_to_cord(spec::GameSpec, position::UInt8)
    return pos_to_cord(spec.num_cols, position)
end

"""
Converts position to x, y coordinates
"""
function pos_to_cord(num_cols::UInt8, position::UInt8)
    x = (position - 0x01) % num_cols + 0x01 # origin coordinate x (columns)
    y = UInt8(ceil(position / num_cols)) # origin coordinate y (rows)
    return x, y
end

"""
Converts x, y coordinates to position
"""
function cord_to_pos(spec::GameSpec, x::Coord, y::Coord)
    return UInt8((y - 0x01) * spec.num_cols + x)
end

"""
Updates the action_mask array inplace according to the current board state.
"""
function update_actions_mask!(g::GameEnv)
    g.action_mask = falses(g.spec.num_actions)

    found_capture = false
    paika_moves = Vector{Tuple}()

    player = g.current_player
    opponent = other(player)

    for x = UnitRange(0x01, g.spec.num_cols), y = UnitRange(0x01, g.spec.num_rows)
        if g.board[cord_to_pos(g.spec, x, y)] == player
            for (x1, y1, d) in get_empty_neighbours(g.spec, x, y, g.board)
                found_capture |= update_action_mask_for_neighbour!(g, x, y, x1, y1, d, opponent)
                if !found_capture
                    push!(paika_moves, (x, y, d))
                end
            end
        end
    end

    if !found_capture
        for (x, y, d) in paika_moves
            g.action_mask[action_index(g.spec, x, y, d, paika)] = true
        end
    end

    return found_capture
end

"""
Updates the action_mask array inplace according to the current board state with respect to an ongoing
extended capture move.
"""
function update_actions_mask_extended_capture!(g::GameEnv)
    g.action_mask = falses(g.spec.num_actions)
    opponent = other(g.current_player)
    found_capture = false

    for (x1, y1, d) in get_empty_neighbours(g.spec, g.current_position[1], g.current_position[2], g.board)
        if d != g.last_direction && !((x1, y1) ∈ g.last_positions)
            found_capture |= update_action_mask_for_neighbour!(g, g.current_position[1], g.current_position[2], x1, y1, d, opponent)
        end
    end

    g.action_mask[g.spec.num_actions] = true #pass

    return found_capture
end

"""
Updates the action_mask array inplace for a specific neighbour node.
"""
function update_action_mask_for_neighbour!(g::GameEnv, x0::Coord, y0::Coord, x1::Coord, y1::Coord, d::Direction, opponent::Player)
    found_capture = false;
    if opponent_in_direction(g, x1, y1, d, opponent)
        found_capture = true;
        g.action_mask[action_index(g.spec, x0, y0, d, approach)] = true
    end
    if opponent_in_direction(g, x0, y0, opposite_direction(d), opponent)
        found_capture = true
        g.action_mask[action_index(g.spec, x0, y0, d, withdrawal)] = true
    end
    return found_capture
end

"""
Changes given direction to the opposite
"""
function opposite_direction(d::Direction)
    return (d + 0x03) % 0x08 + 0x01
end

"""
Enumerates all neighbours of given position (x, y)
"""
function get_empty_neighbours(spec::GameSpec, x::Coord, y::Coord, board::Board)
    empty_neighbours = Vector{Tuple}()
    for (x1, y1, d) in get_surrounding_nodes(spec, x, y)
        if board[cord_to_pos(spec, x1, y1)] == EMPTY
            push!(empty_neighbours, (x1, y1, d))
        end
    end
    return empty_neighbours
end

"""
Enumerates all surrounding nodes that are reachable from given position (x, y)
"""
function get_surrounding_nodes(spec::GameSpec, x::Coord, y::Coord)
    return get_surrounding_nodes(spec.num_cols, spec.num_rows, x, y)
end

"""
Enumerates all surrounding nodes that are reachable from given position (x, y)
"""
function get_surrounding_nodes(num_cols::UInt8, num_rows::UInt8, x::Coord, y::Coord)
    if (is_strong_node(num_cols, x, y))
        return get_eight_neighbours(num_cols, num_rows, x, y)
    else
        return get_four_neighbours(num_cols, num_rows, x, y)
    end
end

"""
Indicates whether given position (x, y) is a strong node (strong: eight reachable neighbours, weak: four reachable neighbours)
"""
function is_strong_node(spec::GameSpec, x::Coord, y::Coord)
    return is_strong_node(spec.num_cols, x, y)
end

"""
Indicates whether given position (x, y) is a strong node (strong: eight reachable neighbours, weak: four reachable neighbours)
"""
function is_strong_node(num_cols::UInt8, x::Coord, y::Coord)
    return (x + y * num_cols) % 2 == 0
end

"""
Returns up to eight neighbours depending on whether they reside in the current boards space
"""
function get_eight_neighbours(spec::GameSpec, x::Coord, y::Coord)
    return get_eight_neighbours(spec.num_cols, spec.num_rows, x, y)
end

"""
Returns up to eight neighbours depending on whether they reside in the current boards space
"""
function get_eight_neighbours(num_cols::UInt8, num_rows::UInt8, x::Coord, y::Coord)
    neighbours = Tuple[]
    if (is_in_board_space(num_cols, num_rows, x, y - 0x01))
        push!(neighbours, (x, y - 0x01, 0x01))
    end
    if (is_in_board_space(num_cols, num_rows, x + 0x01, y - 0x01))
        push!(neighbours, (x + 0x01, y - 0x01, 0x02))
    end
    if (is_in_board_space(num_cols, num_rows, x + 0x01, y))
        push!(neighbours, (x + 0x01, y, 0x03))
    end
    if (is_in_board_space(num_cols, num_rows, x + 0x01, y + 0x01))
        push!(neighbours, (x + 0x01, y + 0x01, 0x04))
    end
    if (is_in_board_space(num_cols, num_rows, x, y + 0x01))
        push!(neighbours, (x, y + 0x01, 0x05))
    end
    if (is_in_board_space(num_cols, num_rows, x - 0x01, y + 0x01))
        push!(neighbours, (x - 0x01, y + 0x01, 0x06))
    end
    if (is_in_board_space(num_cols, num_rows, x - 0x01, y))
        push!(neighbours, (x - 0x01, y, 0x07))
    end
    if (is_in_board_space(num_cols, num_rows, x - 0x01, y - 0x01))
        push!(neighbours, (x - 0x01, y - 0x01, 0x08))
    end
    return neighbours
end

"""
Returns up to four neighbours depending on whether they reside in the current boards space
"""
function get_four_neighbours(spec::GameSpec, x::Coord, y::Coord)
    return get_four_neighbours(spec.num_cols, spec.num_rows, x, y)
end

"""
Returns up to four neighbours depending on whether they reside in the current boards space
"""
function get_four_neighbours(num_cols::UInt8, num_rows::UInt8, x::Coord, y::Coord)
    neighbours = Tuple[]
    if (is_in_board_space(num_cols, num_rows, x, y - 0x01))
        push!(neighbours, (x, y - 0x01, 0x01))
    end
    if (is_in_board_space(num_cols, num_rows, x + 0x01, y))
        push!(neighbours, (x + 0x01, y, 0x03))
    end
    if (is_in_board_space(num_cols, num_rows, x, y + 0x01))
        push!(neighbours, (x, y + 0x01, 0x05))
    end
    if (is_in_board_space(num_cols, num_rows, x - 0x01, y))
        push!(neighbours, (x - 0x01, y, 0x07))
    end
    return neighbours
end

"""
Indicates whether given coordinates are within the boundries of the current board
"""
function is_in_board_space(num_cols::UInt8, num_rows::UInt8, x::Coord, y::Coord)
    return x > 0 && x <= num_cols && y > 0 && y <= num_rows
end

"""
Indicates whether given coordinates are within the boundries of the current board
"""
function is_in_board_space(spec::GameSpec, x::Coord, y::Coord)
    return is_in_board_space(spec.num_cols, spec.num_rows, x, y)
end

"""
Determines whether an opponent piece resides in given direction
"""
function opponent_in_direction(g::GameEnv, x::Coord, y::Coord, d::Direction, opponent::Player)
    x1, y1 = new_coords(x, y, d)

    if is_in_board_space(g.spec, x1, y1)
        return g.board[cord_to_pos(g.spec, x1, y1)] == opponent
    else
        return false
    end
end

"""
Calculates a scalar value that describes how favorable the current situation is for the white player.
This value is a simple material function (white pieces - black pieces)
"""
function white_heuristic_value(g::GameEnv)
    return Float64(g.white_pieces - g.black_pieces)
end

"""
prints the current state and all possible actions in it.
"""
function print_state_information(g::GameEnv)
    println("state: $(GI.current_state(g))")
    println(GI.render(g))
    print_possible_action_strings(g)
end

"""
Prints all possible actions of the current state.
"""
function print_possible_action_strings(g::GameEnv)
    print("possible actions: ")
    for action in GI.actions(g.spec)[g.action_mask]
        print(GI.action_string(g.spec, action) * " ($action), ")
    end
    println()
end

####################
### TEST METHODS ###
####################

function run_test()
    spec = GameSpec()
    return run_test(spec)
end

function run_test(spec::GameSpec)
    env = GI.init(spec)
    i = 0
    while !GI.game_terminated(env)
        i += 1
        # println(GI.render(env))
        # println("its player $(env.current_player == WHITE ? " white's " : " black's ") turn!")
        # print_possible_action_strings(env)
        possible_actions = GI.actions(spec)[env.action_mask]
        GI.play!(env, rand(possible_actions))
        # input = parse(Int, readline())
        # GI.play!(env, input)
    end
    return i, spec, env
end

function performance_test(n::Int)
    for i in 1:n
        run_test()
    end
end


text:
using Gasdynamics1D
using Test
using Literate

const GROUP = get(ENV, "GROUP", "All")

#ENV["GKSwstype"] = "nul" # removes GKS warnings during plotting
ENV["GKSwstype"] = "100"

notebookdir = "../notebook"
docdir = "../docs/src/manual"
litdir = "./literate"

if GROUP == "All" || GROUP == "Basics"
  include("properties.jl")
end

if GROUP == "All" || GROUP == "Notebooks"
  for (root, dirs, files) in walkdir(litdir)
    for file in files
      #endswith(file,".jl") && startswith(file,"5") && Literate.notebook(joinpath(root, file),notebookdir)
      endswith(file,".jl") && Literate.notebook(joinpath(root, file),notebookdir)
    end
  end
end

if GROUP == "Documentation"
  for (root, dirs, files) in walkdir(litdir)
    for file in files
      endswith(file,".jl") && Literate.markdown(joinpath(root, file),docdir)
    end
  end
end

ext: jl


text:
@info "Starting precompile exec."

using BAT, Distributions, LinearAlgebra

bat_sample(MvNormal(float(I(4))), (10^4, 4), MetropolisHastings())

@info "Precompile exec finished."

max_stars_repo_name: bat/batjl-environment

max_issues_repo_name: bat/batjl-environment

max_forks_repo_name: bat/batjl-environment


text:
for f in (:seed_zeros, :seed_position, :seed_strength)
  @eval $f(v::Vector{<:Point},cfg::ComplexGradientConfig) =
      Point.($f(flux,v,cfg)...)

  @eval $f(v::Vector{<:Blob},cfg::ComplexGradientConfig) =
      Blob.($f(flux,v,cfg)...,Elements.blobradius(v))

end

for fiip in (:seed_position!, :seed_strength!)
  @eval function $fiip(vseed::Vector{<:Point},v::Vector{<:Point},posduals,strduals,
                      index,rseeds::NTuple{N,Partials{M,V}},iseeds::NTuple{N,Partials{M,V}},
                      chunksize=N) where {N,V,M}
      $fiip(posduals,strduals,flux,v,index,rseeds,iseeds,chunksize)
      vseed .= Point.(posduals,real(strduals))
  end

  @eval function $fiip(vseed::Vector{<:Blob},v::Vector{<:Blob},posduals,strduals,
                      index,rseeds::NTuple{N,Partials{M,V}},iseeds::NTuple{N,Partials{M,V}},
                      chunksize=N) where {N,V,M}
      $fiip(posduals,strduals,flux,v,index,rseeds,iseeds,chunksize)
      vseed .= Blob.(posduals,real(strduals),Elements.blobradius(v))
  end
end

for fiip in (:seed!,)
  @eval function $fiip(vseed::Vector{<:Point},v::Vector{<:Point},posduals,strduals)
      $fiip(posduals,strduals,flux,v)
      vseed .= Point.(posduals,real(strduals))
  end

  @eval function $fiip(vseed::Vector{<:Blob},v::Vector{<:Blob},posduals,strduals)
      $fiip(posduals,strduals,flux,v)
      vseed .= Blob.(posduals,real(strduals),Elements.blobradius(v))
  end

  @eval function $fiip(vseed::Vector{<:Point},v::Vector{<:Point},posduals,strduals,index) where {N,V,M}
      $fiip(posduals,strduals,flux,v,index)
      vseed .= Point.(posduals,real(strduals))
  end

  @eval function $fiip(vseed::Vector{<:Blob},v::Vector{<:Blob},posduals,strduals,index) where {N,V,M}
      $fiip(posduals,strduals,flux,v,index)
      vseed .= Blob.(posduals,real(strduals),Elements.blobradius(v))
  end
end


text:
# Analyze Monsoon rainfail data from
# @Article{sinha2015trends,
#   Title                    = {Trends and oscillations in the Indian summer monsoon rainfall over the last two millennia},
#   Author                   = {Sinha, A. and Kathayat, G. and Cheng, H. and others},
#   Journal                  = {Nature communications},
#   Year                     = {2015},
#   Volume                   = {6},
#   Publisher                = {Nature Publishing Group},
# }
using EstimateSDE
using XLSX
using DataFrames
using Pipe:@pipe
using Plots
using Serialization
using Base.Threads
using Random
using GLM
using Parameters

## Settings
settings = (
  Tkld=1, # evalation time interval
  Tr=1.0, # Regular resampling time interval
  Tsub=5.0, # Irregular subsampling time interval
  nsub=5 # Number of irregularly subsampled signals
)
@unpack Tkld,Tr,Tsub,nsub = settings
ssize=(300,200)

## Rainfall data
data_dir = joinpath(pkgdir(EstimateSDE),"data")
fn = joinpath(pkgdir(EstimateSDE),"data","ncomms7309-s3.xlsx")
df = @pipe(
  DataFrame(XLSX.readtable(fn,1)...) |>
  rename(_,"Age Yr."=>"age") |>
  Float64.(_) |>
  sort(_,:age)
)

## Basic analysis
if nthreads()<6 @warn("Only $(nthreads()) threads. More will accelerate processing") end
t = df.age
Tav = mean(diff(t))
N = length(t)
println("N = $N observations; average sampling interval Tav = ",round(Tav;digits=2)," years")

## Fit trend
trendtype = :moving_average
df.O18trend = if trendtype==:moving_average
  tr = df.age[1]:Tr:df.age[end]
  yr = interpolate(df.age,df.O18,tr;method=:linear)
  win = 25 #10
  Nr = length(yr)
  yma = map(1:Nr) do i
    i1 = max(1,i-win)
    i2 = min(Nr,i+win)
    mean(yr[i1:i2])
  end
  interpolate(tr,yma,df.age;method=:linear,extra=true)
elseif trendtype==:singularspectrum
  L = 50 # Window length
  tr = df.age[1]:Tr:df.age[end]
  yr = interpolate(df.age,df.O18,tr;method=:linear)
  yt,ys = analyze(yr,L)
  interpolate(tr,yt,df.age;method=:linear,extra=true)
elseif trendtype==:mean
  fill(mean(df.O18),nrow(df))
end

df.O18detrend = df.O18-df.O18trend

## Plot data
plt_orig = plot(df.age,df.O18,xlabel="time (years)",ylabel="O18",label=nothing)
plot!(df.age,df.O18trend,label="trend",fg_legend=nothing,bg_legend=nothing)
plt_detrend = plot(df.age,df.O18detrend,xlabel="time (years)",ylabel="O18",label=nothing)

plt = plot(plt_orig,plt_detrend;layout=(2,1),size=2 .* ssize)
display(plt)

## Estimation setup
y = df.O18detrend
y .-= mean(y)
data = Obs.(t,y)
tor = MlEstimator()
Ts = [0.5,1,2,5,10]
init_tors = default_init_tors(MersenneTwister(12320),Ts)

models = []

## No subsampling
problem = ProblemLike(data;p=8)
proc_est,trace = @time estimate(tor,problem;init_tors=init_tors,ret_trace=true)
push!(models,(label="original",proc=proc_est,trace=trace,Tav=Tav,data=data))
println("Estimate original data: ",proc_est)

## Subsampled
rng = MersenneTwister(23493)
for isub in 1:nsub
  iirand = rand(rng,length(data)).<Tav/Tsub
  data_sub = data[iirand]
  Tav_sub = mean(diff(getfield.(data_sub,:t)))
  problem_sub = ProblemLike(data_sub;p=8)
  proc_est_sub,trace_sub = @time estimate(tor,problem_sub;init_tors=init_tors,ret_trace=true)
  push!(models,(label="sub $isub",proc=proc_est_sub,trace=trace_sub,Tav=Tav_sub,data=data_sub))
  println("isub = $isub estimate ",proc_est_sub)
end

## Save results
results_dir = EstimateSDE.resultsdir("rainfall-data")
f = joinpath(results_dir,"rainfall-sde-estimate.jld")
R = (settings=settings,models=models,df=df)
serialize(f,R); @info "Results saved to: $f"


text:
function update_iterate!(iter::Iterate{T}, Δ::Iterate{T}, α::T) where T <: Real
    iter.x .+= α .* Δ.x
    iter.v .+= α .* Δ.v
    iter.λ .+= α .* Δ.λ
    iter.s .+= α .* Δ.s
    iter.w .+= α .* Δ.w

    iter.τ += α * Δ.τ
    iter.κ += α * Δ.κ

    nothing
end

function solve_newton_system!(
    Δ::Iterate{T}, # overwritten during solve
    ls::AbstractLinearSolver{T},
    θvw::Vector{T},
    b::Vector{T},
    c::Vector{T},
    ubi::Vector{Int},
    ubv::Vector{T},
    δx::Vector{T},
    δy::Vector{T},
    δw::Vector{T},
    δ0::T,
    iter::Iterate{T},
    ξp::Vector{T},
    ξu::Vector{T},
    ξd::Vector{T},
    ξg::T,
    ξxs::Vector{T},
    ξvw::Vector{T},
    ξτκ::T
) where T <: Real
    _ξd   = copy(ξd)
    _ξu   = copy(ξu)
    _ξd .-= (ξxs ./ iter.x)
    _ξu .-= (ξvw ./ iter.w)

    solve_augsys!(Δ.x, Δ.λ, Δ.w, ls, θvw, ubi, ξp, _ξd, _ξu)

    Δ.τ = (ξg + (ξτκ / iter.τ) + dot(c, Δ.x) - dot(b, Δ.λ) + dot(ubv, Δ.w)) / δ0
    Δ.κ = (ξτκ - iter.κ  * Δ.τ) / iter.τ

    Δ.x .+= (Δ.τ .* δx)
    Δ.λ .+= (Δ.τ .* δy)
    Δ.w .+= (Δ.τ .* δw)

    # δs = inv(X) * (ξxs - Sδx)
    Δ.s  .= ξxs
    Δ.s .-= (iter.s .* Δ.x)
    Δ.s ./= iter.x

    # δw = inv(W) * (ξvw - v .* Δx)
    Δ.v  .= ξvw
    Δ.v .-= (iter.v .* Δ.w)
    Δ.v ./= iter.w

    nothing
end

function solve_augsys!(
    δx::Vector{T}, # overwritten during solve
    δy::Vector{T}, # overwritten during solve
    δz::Vector{T}, # overwritten during solve
    ls::AbstractLinearSolver{T},
    θvw::Vector{T},
    ubi::Vector{Int},
    ξp::Vector{T},
    ξd::Vector{T},
    ξu::Vector{T}
) where T <: Real
    δz .= zero(T)

    _ξd = copy(ξd)
    @views _ξd[ubi] .-= (ξu .* θvw)

    solve_augmented_system!(δx, δy, ls, ξp, _ξd)

    δz .-= ξu
    @views δz .+= δx[ubi]
    δz .*= θvw

    nothing
end

function update_residuals!(
    res::Residuals{T},
    iter::Iterate{T},
    A::AbstractMatrix{T},
    b::Vector{T},
    c::Vector{T},
    ubi::Vector{Int},
    ubv::Vector{T}
) where T <: Real
    # calculate `rp` and its norm
    mul!(res.rp, A, iter.x)    # rp = A * x
    rmul!(res.rp, -oneunit(T)) # rp = - rp = - A * x
    axpy!(iter.τ, b, res.rp)   # rp = b * τ - rp
    res.rpn = norm(res.rp)

    # calculate `ru` and its norm
    rmul!(res.ru, zero(T))                       # zero out ru
    axpy!(-oneunit(T), iter.v, res.ru)           # ru = -w + ru = -w
    @views axpy!(-oneunit(T), iter.x[ubi], res.ru) # ru = ru - x
    axpy!(iter.τ, ubv, res.ru)                   # ru = τ * u + ru
    res.run = norm(res.ru)

    # calculate `rd` and its norm
    mul!(res.rd, transpose(A), iter.λ)           # rd = A * λ
    rmul!(res.rd, -oneunit(T))                   # rd = - rd = - A * λ
    axpy!(iter.τ, c, res.rd)                     # rd = τ * c + rd
    axpy!(-oneunit(T), iter.s, res.rd)           # rd = rd - s
    @views axpy!(oneunit(T), iter.w, res.rd[ubi]) # rd = w + rd

    # calculate `rg` and its norm
    # rg = c'x - (b'λ + u'w) + κ
    res.rg = iter.κ + dot(c, iter.x) - dot(b, iter.λ) + dot(ubv, iter.w)
    res.rgn = norm(res.rg)

    nothing
end

function check_status!(
    solv::Solver{T},
    iter::Iterate{T},
    res::Residuals{T},
    tols::Tolerances{T},
    A::AbstractMatrix{T},
    b::Vector{T},
    c::Vector{T},
    ubi::Vector{Int},
    ubv::Vector{T}
) where T <: Real
    # _p = max(|rp| / (τ * (1 + |b|)), |ru| / (τ * (1 + |u|)))
    _p = max(
        res.rpn / (iter.τ * (oneunit(T) + norm(b, Inf))),
        res.run / (iter.τ * (oneunit(T) + norm(ubv, Inf)))
    )

    # _d = |rd| / (τ * (1 + |c|))
    _d = res.rdn / (iter.τ * (oneunit(T) + norm(c, Inf)))

    primal_bound = dot(c, iter.x)                     # c'x
    dual_bound   = dot(b, iter.λ) - dot(ubv, iter.w)  # b'λ - u'w

    # _g = |c'x - b'λ| / (τ + |b'λ|)
    _g = abs(primal_bound - dual_bound) / (iter.τ + abs(dual_bound))

    solv.status_primal = _p <= tols.εp ? IterateFeasible : IterateUndefined
    solv.status_dual   = _d <= tols.εd ? IterateFeasible : IterateUndefined

    # check optimality
    if _p <= tols.εp && _d <= tols.εd && _g <= tols.εg
        solv.status        = SolverOptimal
        solv.status_primal = IterateOptimal
        solv.status_dual   = IterateOptimal

        return nothing
    end

    # check infeasibility
    _ax_n = max(
        norm(A * iter.x, Inf),
        norm(iter.x[ubi] + iter.v, Inf)
    )
    _cb_n = norm(c, Inf) / max(1, norm(b, Inf))
    if _ax_n * _cb_n < - tols.εi * dot(c, iter.x)
        solv.status_primal = IterateInfeasible
        solv.status        = SolverDualInfeasible

        return nothing
    end

    δ = transpose(A) * iter.λ + iter.s
    δ[ubi] .-= iter.w
    if norm(δ, Inf) * norm(b, Inf) / max(1, norm(c, Inf)) < tols.εi * dual_bound
        solv.status_dual = IterateInfeasible
        solv.status      = SolverPrimalInfeasible

        return nothing
    end

    nothing
end

function _max_alpha(v::Vector{T}, dv::Vector{T}) where T <: Real
    n = size(v, 1)
    size(dv, 1) == n || throw(DimensionMismatch(
        "expected vector of length $n"
    ))

    α = T(Inf)
    for i in Base.OneTo(n)
        if dv[i] <  zero(T)
            _pot_α = -v[i] / dv[i]
            if _pot_α < α
                α = _pot_α
            end
        end
    end

    α
end

function max_alpha(iter::Iterate{T}, Δ::Iterate{T}) where T <: Real
    α_τ = Δ.τ < zero(T) ? (-iter.τ / Δ.τ) : oneunit(T)
    α_κ = Δ.κ < zero(T) ? (-iter.κ / Δ.κ) : oneunit(T)

    α = min(
        oneunit(T),
        _max_alpha(iter.x, Δ.x),
        _max_alpha(iter.v, Δ.v),
        _max_alpha(iter.s, Δ.s),
        _max_alpha(iter.w, Δ.w),
        α_τ, α_κ
    )
end


text:
@static if Base.VERSION < v"1.2"
    function hasfield(::Type{T}, name::Symbol) where {T}
        return name in fieldnames(T)
    end
    fieldtypes(::Type{T}) where {T} = Tuple(fieldtype(T, i) for i = 1:fieldcount(T))
end

# top type - unifying a type with top yeilds the type
struct Top end

# get the type from a named tuple, given a name
get_type(NT, k) = hasfield(NT, k) ? fieldtype(NT, k) : Nothing

# unify two types to a single type
function promoteunion(T, S)
    new = promote_type(T, S)
    return isabstracttype(new) ? Union{T, S} : new
end

unify(a, b) = unify(b, a)
unify(a::Type{T}, b::Type{S}) where {T,S} = promoteunion(T, S)
unify(a::Type{T}, b::Type{S}) where {T,S<:T} = T
unify(a::Type{Top}, b::Type{T}) where {T} = T

function unify(
    a::Type{NamedTuple{A,T}},
    b::Type{NamedTuple{B,S}},
) where {A,T<:Tuple,B,S<:Tuple}
    ks = []
    ts = []
    for (k, v) in zip(A, fieldtypes(a))
        push!(ks, k)
        push!(ts, unify(v, get_type(b, k)))
    end

    for (k, v) in zip(B, fieldtypes(b))
        if !(k in ks)
            push!(ks, k)
            push!(ts, unify(v, Nothing))
        end
    end

    return NamedTuple{tuple(ks...),Tuple{ts...}}
end

unify(a::Type{Vector{T}}, b::Type{Vector{S}}) where {T,S} = Vector{unify(T, S)}

# parse json into a type, maintain field order
"""
    JSON3.generate_type(json)

Given a JSON3 Object or Array, return a "raw type" from it.  A raw type is typically a `NamedTuple`, which can contain further nested `NamedTuples`, concrete, `Array`, or `Union` types.
"""
function generate_type(o::JSON3.Object)
    ks = []
    ts = []
    for (k, v) in o
        push!(ks, k)
        push!(ts, generate_type(v))
    end

    return NamedTuple{tuple(ks...),Tuple{ts...}}
end

function generate_type(a::JSON3.Array)
    t = Set([])
    nt = Top
    for item in a
        it = generate_type(item)
        if it <: NamedTuple
            nt = unify(nt, it)
        else
            push!(t, it)
        end
    end

    return Vector{foldl(unify, t; init = Union{nt})}
end

generate_type(x::T) where {T} = T

# get the AST of a type
function to_ast(::Type{T}) where {T}
    io = IOBuffer()
    print(io, T)
    str = String(take!(io))
    ast = Meta.parse(str)
    return ast
end

# make a field identifer into pascal case for struct name (my_names => MyName)
function pascalcase(s::Symbol)
    str = String(s)
    new_str = ""
    next_upper = true
    for letter in str
        if next_upper
            new_str *= uppercase(letter)
            next_upper = false
        elseif letter == '_'
            next_upper = true
        else
            new_str *= letter
        end
    end

    if length(new_str) > 1 && new_str[end] == 's' && new_str[end-1:end] != "ss"
        new_str = new_str[1:end-1]
    end

    return Symbol(new_str)
end

"""
    JSON3.write_exprs(expr, f)

Write an `Expr` or `Vector{Expr}` to file.  Formatted so that it can be used with `include`.
"""
function write_exprs(expr::Expr, io::IOStream)
    str = repr(expr)[3:end-1] # removes :( and )
    str = replace(str, "\n  " => "\n") # un-tab each line
    Base.write(io, str)
    Base.write(io, "\n\n")
end

function write_exprs(exprs::Vector, fname::AbstractString)
    open(fname, "w") do io
        for expr in exprs
            write_exprs(expr, io)
        end
    end
end

function write_exprs(expr::Expr, fname::AbstractString)
    open(fname, "w") do io
        write_exprs(expr, io)
    end
end

# entry function for turning a "raw" type from `generate_type` to Exprs
"""
    JSON3.generate_exprs(raw_type; root_name=:Root, mutable=true)

Generate a vector of `Expr` from a "raw_type".  This will un-nest any sub-types within the root type.

The name of the root type is from the `name` variable (default :Root), then nested types are named from the key that they live under in the JSON.  The key is transformed to be pascal case and singular.

If `mutable` is `true`, an empty constructor is included in the struct definition. This allows the mutable structs to be used with `StructTypes.Mutable()` out of the box.
"""
function generate_exprs(t; root_name::Symbol = :Root, mutable = true)
    exprs = []
    generate_expr!(exprs, t, root_name; mutable = mutable)
    return exprs
end

# turn a "raw" type into an AST for a struct
function generate_expr!(
    exprs,
    nt::Type{NamedTuple{N,T}},
    root_name::Symbol;
    mutable::Bool = true,
) where {N,T<:Tuple}
    sub_exprs = []
    for (n, t) in zip(N, fieldtypes(nt))
        push!(sub_exprs, generate_field_expr!(exprs, t, n; mutable = mutable))
    end

    struct_name = pascalcase(root_name)
    if mutable
        push!(sub_exprs, Meta.parse("$struct_name() = new()"))
    end

    push!(exprs, Expr(:struct, mutable, struct_name, Expr(:block, sub_exprs...)))
    return struct_name
end

# should only hit this in the case of the array being the root of the type
function generate_expr!(
    exprs,
    ::Type{Base.Array{T,N}},
    root_name::Symbol;
    kwargs...,
) where {T<:NamedTuple,N}
    return generate_expr!(exprs, T, root_name; kwargs...)
end

function generate_expr!(exprs, t::Type{T}, root_name::Symbol; kwargs...) where {T}
    if T isa Union
        return Expr(
            :curly,
            :Union,
            generate_expr!(exprs, t.a, root_name; kwargs...),
            generate_expr!(exprs, t.b, root_name; kwargs...),
        )
    else
        return to_ast(T)
    end
end

# given the type of a field of a struct, return a node for that field's name/type
function generate_field_expr!(
    exprs,
    t::Type{NamedTuple{N,T}},
    root_name::Symbol;
    kwargs...,
) where {N,T}
    generate_expr!(exprs, t, root_name; kwargs...)
    return Expr(:(::), root_name, pascalcase(root_name))
end

function generate_field_expr!(
    exprs,
    ::Type{Base.Array{T,N}},
    root_name::Symbol;
    kwargs...,
) where {T,N}
    return Expr(
        :(::),
        root_name,
        Expr(:curly, :Array, generate_expr!(exprs, T, root_name; kwargs...), 1),
    )
end

function generate_field_expr!(exprs, ::Type{T}, root_name::Symbol; kwargs...) where {T}
    return Expr(:(::), root_name, generate_expr!(exprs, T, root_name; kwargs...))
end

# create a module with the struct declarations as well as the StructType declarations
"""
    JSON3.generate_struct_type_module(exprs, module_name)

Given a vector of `exprs` (output of [`generate_exprs`](@ref)), return an `Expr` containing the AST for a module with name `module_name`.  The module will map all types to the appropriate `StructType`, so the result can immediately used with `JSON3.read(json, T)`.
"""
function generate_struct_type_module(exprs, module_name)
    mutable = exprs[1].args[1]
    struct_type_import = Meta.parse("import StructTypes")
    struct_type = mutable ? "Mutable" : "Struct"
    struct_type_decls = []
    for expr in exprs
        push!(
            struct_type_decls,
            Meta.parse("StructTypes.StructType(::Type{$(expr.args[2])}) = StructTypes.$struct_type()"),
        )
    end
    type_block = Expr(:block, struct_type_import, exprs..., struct_type_decls...)
    return Expr(:module, true, module_name, type_block)
end

"""
    JSON3.generatetypes(json, module_name; mutable=true, root_name=:Root)

Convenience function to go from a json string or file name to an AST with a module of structs.

Performs the following:
1. If the JSON is a file, read to string
2. Call `JSON3.read` on the JSON string
3. Get the "raw type" from [`generate_type`](@ref)
4. Parse the "raw type" into a vector of `Expr` ([`generate_exprs`](@ref))
5. Generate a module containg the structs ([`generate_struct_type_module`](@ref))
"""
function generatetypes(
    json_str::AbstractString,
    module_name::Symbol;
    mutable::Bool = true,
    root_name::Symbol = :Root,
)
    # either a JSON.Array or JSON.Object
    json = read(
        length(json_str) < 255 && isfile(json_str) ? Base.read(json_str, String) :
            json_str,
    )

    # build a type for the JSON
    raw_json_type = generate_type(json)
    json_exprs = generate_exprs(raw_json_type; root_name=root_name, mutable=mutable)
    return generate_struct_type_module(
        json_exprs,
        module_name
    )
end

# macro to create a module with types generated from a json string
"""
    JSON3.@generatetypes json [module_name]

Evaluate the result of the [`generatetypes`](@ref) function in the current scope.
"""
macro generatetypes(json_str, module_name)
    :(Core.eval($__module__, generatetypes($(esc(json_str)), $(esc(module_name)))))
end

macro generatetypes(json)
    :(@generatetypes $(esc(json)) :JSONTypes)
end

# convenience function to go from json_string, to file with module
"""
    JSON3.writetypes(json, file_name; module_name=:JSONTypes, root_name=:Root, mutable=true)

Write the result of the [`generatetypes`](@ref) function to file.
"""
function writetypes(
    json,
    file_name;
    module_name::Symbol = :JSONTypes,
    root_name::Symbol = :Root,
    mutable::Bool = true,
)
    mod = generatetypes(json, module_name; mutable=mutable, root_name=root_name)
    write_exprs(mod, file_name)
end