text:
### A Pluto.jl notebook ###
# v0.12.18

using Markdown
using InteractiveUtils

# ╔═╡ ad8ff974-4b71-11eb-3eb6-790d687615c0
using DrWatson

# ╔═╡ b35cdada-4689-11eb-2629-c1cedd8052bd
begin
	@quickactivate

	using GameZero
	using Dates
	using Colors
	using Random
	using Serialization
	using PlaymatSimulator

    import PlaymatSimulator.Actors.Text
	import PlaymatSimulator.Actors.Image
	import PlaymatSimulator.kill_actor!

	const AN = PlaymatSimulator.Animations
	const SDL2 = SimpleDirectMediaLayer

	pd = projectdir()

	USER_SETTINGS = deserialize("$pd/tmp/user_selection.jls")
	GAME_NAME = USER_SETTINGS[:GAME_NAME]

	game_include("$pd/Base/notebooks/game_rules.jl")

	md"""
	### Common Mage.jl funcs
	"""
end

# ╔═╡ 409648d8-468e-11eb-2856-5bda6584cf79
function zone_check(a::Actor, gs::Dict)

	for zone in keys(gs[:zone])

		if SDL2.HasIntersection(
            Ref(SDL2.Rect(Int32[
				ceil(a.x + a.w * a.scale[1] / 2),
				ceil(a.y + a.h * a.scale[2] / 2), 1, 1]...)), # intersection determined from top-left most pixel
            Ref(gs[:stage][zone].position))

			return zone
        end
    end

    @warn "$(a.label) not found in any :stage area!"
end

# ╔═╡ 438d93b6-468e-11eb-2cdd-650d3873e81d
function kill_card!(c::Card)
    global gs

	kill_actor!.(c.faces)
    filter!.(x->x!==c, [ values(gs[:zone])..., values(gs[:group])... ])
end

# ╔═╡ 799c2578-4cb3-11eb-1af7-b1ab01270e6d
function reset_stage!(gs::Dict)
    GAME_NAME = gs[:GAME_NAME]
    DECK_NAME = gs[:DECK_NAME]
    SCREEN_WIDTH = gs[:SCREEN_WIDTH]
    SCREEN_HEIGHT = gs[:SCREEN_HEIGHT]
    SCREEN_BORDER = gs[:SCREEN_BORDER]

    for k in keys(gs[:zone])
        gs[:zone][k] = []
    end

    for k in keys(gs[:group])
        gs[:group][k] = []
    end

    deck = deserialize("$pd/$GAME_NAME/decks/$DECK_NAME/$DECK_NAME.jls")

	gs[:CARDS] = []

	for (name, imgs) in gs[:deck][:CARD_FACES]
		id = randstring(10)
		c = Card(
			id,
			name,
			"Player1",
			"Player1",
			[ Image("Backside", deck[:CARD_BACK_IMG]), [
				length(strides(img)) > 2 ? GIF(name, img) : Image(name, img) for img in imgs ]... ],
			false,
			false,
			[1,1],
			Dict(),
		)
		for a in c.faces
			a.data[:parent_id] = c.id
		end

		push!(gs[:CARDS], c)
	end

	gs[:zone]["Command"] = [ c for c in gs[:CARDS] if c.name in deck[:commander_names] ]
	gs[:zone]["Library"] = shuffle(gs[:CARDS])
	filter!(x->!(x in gs[:zone]["Command"]), gs[:zone]["Library"])

	gs[:zone]["Hand"] = reverse([ pop!(gs[:zone]["Library"]) for i in 1:7 ])

	for c in [ gs[:zone]["Hand"]..., gs[:zone]["Command"]... ]
		c.faces = circshift(c.faces, 1)
	end

	gs[:ALL_CARDS] = vcat(gs[:zone]["Library"], gs[:zone]["Hand"], gs[:zone]["Command"])

	pushfirst!(gs[:group][:clickables], [ c.faces[begin] for c in gs[:zone][:"Hand"] ]...)
	pushfirst!(gs[:group][:clickables], [ c.faces[begin] for c in gs[:zone][:"Command"] ]...)
	pushfirst!(gs[:group][:clickables], gs[:zone]["Library"][end].faces[begin])

	AN.splay_actors!([ c.faces[begin] for c in gs[:zone]["Library"] ],  # stack library cards into deck
		SCREEN_BORDER,
		ceil(Int32, SCREEN_HEIGHT - SCREEN_BORDER - gs[:zone]["Library"][end].faces[begin].h),
		SCREEN_HEIGHT,
		SCREEN_BORDER,
		pitch=[0.001, -0.005],
	)
	AN.splay_actors!([ c.faces[begin] for c in gs[:zone]["Hand"] ], 	# splay cards into hand zone
        SCREEN_BORDER,
        SCREEN_BORDER,
        SCREEN_HEIGHT,
        SCREEN_BORDER,
        pitch=[0.05, 0.1],
    )

    for (i,c) in enumerate([ c.faces[begin] for c in gs[:zone]["Command"] ])
		c.y = SCREEN_BORDER + (i-1) * 30
        c.x = gs[:stage]["Command"].x + (i-1) * 15
    end

	push!(gs[:overlay][:dice], gs[:stage][:six_sided_die])
	push!(gs[:overlay][:counters], gs[:stage][:glass_counter])
	push!(gs[:group][:clickables], values(gs[:resource_spinners])...)
	push!(gs[:group][:clickables], [ d.faces[begin] for d in gs[:overlay][:dice] ]...)
	push!(gs[:group][:clickables], [ c.faces[begin] for c in gs[:overlay][:counters] ]...)

	gs
end

# ╔═╡ 47f50362-468e-11eb-211d-f561273a906c
function on_mouse_move(g::Game, pos::Tuple)
    global gs

    gs[:ui][:cursor_icon].x = gs[:ui][:cursor].x = gs[:MOUSE_POS][1] = pos[1]
    gs[:ui][:cursor_icon].y = gs[:ui][:cursor].y = gs[:MOUSE_POS][2] = pos[2]

	if gs[:sfx][:sel_box] in gs[:overlay][:shades]
		c = gs[:sfx][:sel_box]
        c.w = gs[:ui][:cursor].x - c.x
        c.h = gs[:ui][:cursor].y - c.y
	end

	for c in gs[:group][:selected]
		c.x = gs[:MOUSE_POS][1] + c.data[:mouse_offset][1]
		c.y = gs[:MOUSE_POS][2] + c.data[:mouse_offset][2]
    end
end

# ╔═╡ 69917186-468e-11eb-1175-dd4bbfe2f109
function draw(g::Game)
    draw.([
        # bottom layer
        values(gs[:stage])...,
		values(gs[:resource_spinners])...,
		(values(gs[:zone])... )...,
        values(gs[:texts])...,
        values(gs[:ui])...,
		(values(gs[:overlay])... )...,
        gs[:group][:selected]...,
        gs[:ui][:cursor_icon],
        # top layer
    ])
end

# ╔═╡ cdd4c524-4cba-11eb-07a2-c7651ae7f211
function add_texts!(gs::Dict)
    SCREEN_WIDTH = gs[:SCREEN_WIDTH]
    SCREEN_HEIGHT = gs[:SCREEN_HEIGHT]
    SCREEN_BORDER = gs[:SCREEN_BORDER]

	gs[:texts] = Dict{Symbol,Actor}()
    gs[:texts][:deck_info] = Text("Library: $(length(gs[:zone]["Library"]))",
        "$pd/Base/fonts/OpenSans-Regular.ttf",
        x=2SCREEN_BORDER,
        y=SCREEN_HEIGHT - 4SCREEN_BORDER,
        pt_size=22,
        )
    gs[:texts][:hand_info] = Text("Hand: $(length(gs[:zone]["Hand"]))",
        "$pd/Base/fonts/OpenSans-Regular.ttf",
        x=2SCREEN_BORDER,
        y=gs[:stage]["Hand"].h - 2SCREEN_BORDER,
        pt_size=22,
        )
    gs[:texts][:battlefield_info] = Text("Battlefield: $(length(gs[:zone]["Battlefield"]))",
        "$pd/Base/fonts/OpenSans-Regular.ttf",
        x=gs[:stage]["Hand"].w + 10SCREEN_BORDER,
        y=SCREEN_HEIGHT - 4SCREEN_BORDER,
        pt_size=22,
        )
    gs[:texts][:command_info] = Text("Command / Exile: $(length(gs[:zone]["Command"]))",
        "$pd/Base/fonts/OpenSans-Regular.ttf",
        x=gs[:stage]["Command"].x + SCREEN_BORDER,
        y=gs[:stage]["Command"].h - 2SCREEN_BORDER,
        pt_size=22,
        )
    gs[:texts][:graveyard_info] = Text("Graveyard: $(length(gs[:zone]["Graveyard"]))",
        "$pd/Base/fonts/OpenSans-Regular.ttf",
        x=gs[:stage]["Graveyard"].x + SCREEN_BORDER,
        y=SCREEN_HEIGHT - 4SCREEN_BORDER,
        pt_size=22,
        )
    gs[:texts][:welcome_text] = Text("PlaymatSimulator",
        "$pd/Base/fonts/OpenSans-Regular.ttf",
        x=ceil(Int32, SCREEN_WIDTH * 0.3),
        y=ceil(Int32, SCREEN_HEIGHT * 0.5),
        pt_size=85,
        font_color=[220,220,220,40],
        wrap_length=1000,
        )

    push!(gs[:overlay][:texts],
        values(gs[:resource_spinners])...,
        values(gs[:texts])...,
        )

    AN.splay_actors!([ values(gs[:resource_spinners])... ],
        ceil(Int32, SCREEN_WIDTH * 0.955),
        Int32(2SCREEN_BORDER),
        SCREEN_HEIGHT,
        SCREEN_BORDER,
        pitch=Float64[0,1]
        )

    gs[:group][:clickables] = [
        gs[:zone]["Command"]...,
        gs[:zone]["Hand"]...,
        values(gs[:resource_spinners])...,
        gs[:overlay][:dice]...,
		gs[:overlay][:counters]...,
        ]

    for s in values(gs[:resource_spinners])
        s.data[:value] = 0
    end

    gs[:resource_spinners][:life].data[:value] = 40
    reverse(gs[:group][:clickables])
    gs
end

# ╔═╡ 7ef01b58-523d-11eb-0c16-2b1d02dd1836
function in_bounds(gs::Dict, as=Actor[])

	for a in gs[:group][:clickables]

		pos = if a.angle == 90 || a.angle == 270  # corrects for 90 & 270 rot abt center
            SDL2.Rect(
                ceil(Int32, a.x - (a.scale[2] * a.h - a.scale[1] * a.w) / 2),
                ceil(Int32, a.y + (a.scale[2] * a.h - a.scale[1] * a.w) / 2),
                a.h,
                a.w,
            )
        else
            a.position
        end

        if SDL2.HasIntersection(
            Ref(pos), Ref(gs[:ui][:cursor].position))
            push!(as, a)
        end
    end

	return as
end

# ╔═╡ 4cbb84f2-468e-11eb-09af-718f893c0449
function on_mouse_down(g::Game, pos::Tuple, button::GameZero.MouseButtons.MouseButton)
    global gs
    ib = in_bounds(gs)
	@show length(ib)

    if button == GameZero.MouseButtons.LEFT

		if !isempty(gs[:group][:selected])
			# same code is on_mouse_up... TODO: wrap up into own func?
			for a in gs[:group][:selected]
				zone = zone_check(a, gs)

                if zone !== nothing

					for (i,c) in enumerate(gs[:ALL_CARDS])

						if haskey(a.data, :parent_id) && a.data[:parent_id] == c.id
							c = popat!(gs[:ALL_CARDS], i)
							filter!.(x->x!=c, [ values(gs[:zone])... ])
							g.keyboard.LCTRL || g.keyboard.RCTRL ? pushfirst!(gs[:zone][zone], c) : push!(gs[:zone][zone], c)
							push!(gs[:ALL_CARDS], c)
						end
					end

					AN.update_text_actor!(gs[:texts][:deck_info],
		                "Library: $(length(gs[:zone]["Library"]))")
		            AN.update_text_actor!(gs[:texts][:hand_info],
		                "Hand: $(length(gs[:zone]["Hand"]))")
		            AN.update_text_actor!(gs[:texts][:graveyard_info],
		                "Graveyard: $(length(gs[:zone]["Graveyard"]))")
		            AN.update_text_actor!(gs[:texts][:command_info],
		                "Command / Exile: $(length(gs[:zone]["Command"]))")
		            AN.update_text_actor!(gs[:texts][:battlefield_info],
		                "Battlefield: $(length(gs[:zone]["Battlefield"]))")

					filter!(x->x!==a, gs[:group][:clickables])
					a.x = round_to(30, a.x)  # snap in x
					a.y = round_to(30, a.y)  # snap in y
					a.scale = [1, 1]

				else
					a.scale = [1, 1]
				end
			end

			push!(gs[:group][:clickables], gs[:group][:selected]...)
			gs[:group][:selected] = []

		elseif isempty(ib)

			if isempty(gs[:overlay][:shades])
	            gs[:sfx][:sel_box].x = gs[:MOUSE_POS][1]
	            gs[:sfx][:sel_box].y = gs[:MOUSE_POS][2]
	            gs[:sfx][:sel_box].w = 1
	            gs[:sfx][:sel_box].h = 1
	            gs[:sfx][:sel_box].alpha = 50
	            push!(gs[:overlay][:shades], gs[:sfx][:sel_box])
			else
				gs[:group][:selected] = Actor[]
			end

        elseif !isempty(ib)

            if g.keyboard.LSHIFT || g.keyboard.RSHIFT
                # play_sound("Base/sounds/select.wav")
                ib[end].scale = [1.02, 1.02]
                push!(gs[:group][:selected], ib[end])

                for a in gs[:group][:selected]
                    a.data[:mouse_offset] = [
						a.x - gs[:MOUSE_POS][1], a.y - gs[:MOUSE_POS][2] ]
                end

			# pull card from top of deck into hand & selected if any cards left in library
			elseif ib[end] === gs[:zone]["Library"][end].faces[begin] &&
				length(gs[:zone]["Library"]) > 0

				c = pop!(gs[:zone]["Library"])
				c.faces = circshift(c.faces, 1)
				a = c.faces[begin]
				a.scale = [1.02, 1.02]
                a.x = ceil(Int32, length(gs[:zone]["Hand"]) > 0 ?
                    gs[:zone]["Hand"][end].faces[begin].x + a.w * 0.05 : gs[:stage]["Hand"].x)
                a.y = ceil(Int32, length(gs[:zone]["Hand"]) > 0 ?
                    gs[:zone]["Hand"][end].faces[begin].y + a.h * 0.1 : gs[:stage]["Hand"].y)

				push!(gs[:zone]["Hand"], c)
                push!(gs[:group][:selected], a)
				push!(gs[:group][:clickables], a)
				push!(gs[:group][:clickables], gs[:zone]["Library"][end].faces[begin])

            elseif ib[end] in values(gs[:overlay][:dice])

				if g.keyboard.LCTRL || g.keyboard.RCTRL
                    push!(gs[:group][:selected], ib[end])

                else
                    copy = GIF("die_$(randstring(5))", ib[end].data[:gif],
						x=gs[:MOUSE_POS][1], y=gs[:MOUSE_POS][2])
					push!(gs[:overlay][:dice], copy)
                    push!(gs[:group][:selected], copy)
                end

            elseif isempty(gs[:group][:selected]) && !(ib[end] in values(gs[:resource_spinners]))
                # play_sound("Base/sounds/select.wav")

				ib[end].scale=[1.02, 1.02]
                @show zs = zone_check(ib[end], gs)

				# "sticky" counters
                dice_and_counters = [ dc.faces[begin] for dc in [
					values(gs[:overlay][:dice])...,
					values(gs[:overlay][:dice])...
					] if SDL2.HasIntersection(
                        Ref(ib[end].position),
                        Ref(dc.faces[begin].position)) && !(ib[end] in [
						values(gs[:overlay][:dice])...,
						values(gs[:overlay][:dice])... ]
						)
                    ]

                for dc in dice_and_counters
                    dc.data[:mouse_offset] = [
						dc.x - gs[:MOUSE_POS][1], dc.y - gs[:MOUSE_POS][2] ]
                end

                push!(gs[:group][:selected], [ ib[end] ]...) #, dice_and_counters... )

                if zs !== nothing
                    filter!(x->x!==ib[end], gs[:zone][zs])
                end

                ib[end].data[:mouse_offset] = [ ib[end].x -
					gs[:MOUSE_POS][1], ib[end].y - gs[:MOUSE_POS][2] ]
			end
        end

    elseif button == GameZero.MouseButtons.RIGHT

		if !isempty(gs[:group][:selected])

			for a in gs[:group][:selected]
                a.angle = a.angle == 0 ? g.keyboard.LALT ? 90 : 270 : 0
            end

        elseif !isempty(ib)

            if ib[end] in [ c.faces[begin] for c in gs[:zone]["Library"] ]
				sort!(gs[:zone]["Library"], by=x->x.name)
                gs[:group][:clickables] = [ c.faces[begin] for c in gs[:zone]["Library"] ]

                if SDL2.HasIntersection(
                    Ref(gs[:zone]["Library"][end].faces[begin].position), Ref(gs[:stage]["Library"].position))

					for c in gs[:zone]["Library"]
						c.faces = circshift(c.faces, 1)
					end

					gs[:group][:clickables] = [ c.faces[begin] for c in gs[:zone]["Library"] ]

                    AN.splay_actors!(
                        sort([ c.faces[begin] for c in gs[:zone]["Library"] ], by=x->x.label),
                        ceil(Int32, gs[:stage]["Hand"].w + 2SCREEN_BORDER),
                        SCREEN_BORDER,
                        SCREEN_HEIGHT,
                        SCREEN_BORDER,
                        pitch=[0.032, 0.1]
                	)
	            else
					for (i,c) in enumerate(gs[:zone]["Library"])
						a = c.faces[begin]

						if SDL2.HasIntersection(Ref(a.position), Ref(gs[:stage]["Hand"].position))
	                        c = popat!(gs[:zone]["Library"], i)
							push!(gs[:zone]["Hand"], c)
	                    end
	                end

					for c in gs[:zone]["Library"]

						while c.faces[begin].label != "Backside"
							c.faces = circshift(c.faces, 1)
						end
					end

                	gs[:zone]["Library"] = shuffle(gs[:zone]["Library"])

	                AN.splay_actors!([ c.faces[begin] for c in gs[:zone]["Library"] ],
	                    SCREEN_BORDER,
	                    ceil(Int32, SCREEN_HEIGHT - SCREEN_BORDER - gs[:zone]["Library"][end].faces[begin].h),
	                    SCREEN_HEIGHT,
	                    SCREEN_BORDER,
	                    pitch=[0.001, -0.005]
	                )

	                gs[:group][:clickables] = [
	                    [ c.faces[begin] for c in [ (values(gs[:zone])... )... ] ]...,
	                    values(gs[:resource_spinners])...,
	                    [ d.faces[begin] for d in values(gs[:overlay][:dice]) ]...,
						[ c.faces[begin] for c in values(gs[:overlay][:counters]) ]...,
                	]

					push!(gs[:group][:clickables], gs[:zone]["Library"][end].faces[begin])
            	end

            elseif ib[end] in values(gs[:resource_spinners])
                delta = g.keyboard.LSHIFT || g.keyboard.RSHIFT ? 5 : 1
                f = gs[:MOUSE_POS][1] > ib[end].x + ib[end].w / 2 ? 1 : -1
                ib[end].data[:value] += f * delta

                AN.update_text_actor!(ib[end],
                    " $(ib[end].data[:value])" * ib[end].label[end-2:end]
                )

			else
            	ib[end].angle = ib[end].angle == 0 ? 270 : 0
            end

		elseif isempty(ib)
			gs[:group][:selected] = []
		end
    end
end

# ╔═╡ 54761f68-468e-11eb-3ab9-db06b7174615
function on_mouse_up(g::Game, pos::Tuple, button::GameZero.MouseButtons.MouseButton)
    global gs
    ib = in_bounds(gs)

    if button == GameZero.MouseButtons.LEFT

		if gs[:sfx][:sel_box] in gs[:overlay][:shades]
            sb = gs[:sfx][:sel_box]

            for a in gs[:group][:clickables]

                pos = if a.angle == 90 || a.angle == 270  # corrects for sideways rot abt center
                    SDL2.Rect(
                        ceil(Int32, a.x - (a.scale[2] * a.h - a.scale[1] * a.w) / 2),
                        ceil(Int32, a.y + (a.scale[2] * a.h - a.scale[1] * a.w) / 2),
                        a.h,
                        a.w,
                    )
                else
                    a.position
                end

                if SDL2.HasIntersection(
                    Ref(SDL2.Rect(
                        sb.w < 0 ? sb.x + sb.w : sb.x,
                        sb.h < 0 ? sb.y + sb.h : sb.y,
                        sb.w < 0 ? -sb.w : sb.w,
                        sb.h < 0 ? -sb.h : sb.h)
                        ),
                    Ref(pos)) &&
                        !(a in values(gs[:resource_spinners])) &&
                        !(a in [ c.faces[begin] for c in values(gs[:overlay][:counters]) ]) &&
						!(a in [ d.faces[begin] for d in values(gs[:overlay][:dice]) ])

					push!(gs[:group][:selected], a)
                    filter!(x->x!==a, [ (values(gs[:zone])...)... ] )
                end
            end

			filter!(x->x!==sb, gs[:overlay][:shades])
			filter!(x->x!==gs[:zone]["Library"][end].faces[begin], gs[:group][:selected])

            if length(gs[:group][:selected]) > 0

				for a in gs[:group][:selected]
					a.scale = [1.025, 1.025]
                    a.data[:mouse_offset] = [ a.x - gs[:MOUSE_POS][1], a.y - gs[:MOUSE_POS][2] ]
                end

				# play_sound("Base/sounds/select.wav")
            end

        elseif !isempty(gs[:group][:selected]) && !(g.keyboard.LSHIFT || g.keyboard.RSHIFT)

			for a in gs[:group][:selected]
				zone = zone_check(a, gs)

                if zone !== nothing

					for (i,c) in enumerate(gs[:ALL_CARDS])

						if haskey(a.data, :parent_id) && a.data[:parent_id] == c.id
							c = popat!(gs[:ALL_CARDS], i)
							filter!.(x->x!=c, [ values(gs[:zone])... ])
							g.keyboard.LCTRL || g.keyboard.RCTRL ? pushfirst!(gs[:zone][zone], c) : push!(gs[:zone][zone], c)
							push!(gs[:ALL_CARDS], c)
						end
					end

					AN.update_text_actor!(gs[:texts][:deck_info],
		                "Library: $(length(gs[:zone]["Library"]))")
		            AN.update_text_actor!(gs[:texts][:hand_info],
		                "Hand: $(length(gs[:zone]["Hand"]))")
		            AN.update_text_actor!(gs[:texts][:graveyard_info],
		                "Graveyard: $(length(gs[:zone]["Graveyard"]))")
		            AN.update_text_actor!(gs[:texts][:command_info],
		                "Command / Exile: $(length(gs[:zone]["Command"]))")
		            AN.update_text_actor!(gs[:texts][:battlefield_info],
		                "Battlefield: $(length(gs[:zone]["Battlefield"]))")

					filter!(x->x!==a, gs[:group][:clickables])
					a.x = round_to(30, a.x)  # snap in x
					a.y = round_to(30, a.y)  # snap in y
					a.scale = [1, 1]

				else
					a.scale = [1, 1]
				end
			end

			push!(gs[:group][:clickables], gs[:group][:selected]...)
			gs[:group][:selected] = []
		end
	end
end

# ╔═╡ 6159d724-468e-11eb-05fb-db68237e3fa0
function on_key_down(g::Game, key, keymod)
    global gs
	DECK_NAME = gs[:DECK_NAME]

    ib = in_bounds(gs)

    if key == Keys.C # && keymod !== 0 && keymod == Keymods.LCTRL || keymod == Keymods.RCTRL
        if !isempty(gs[:group][:selected])
            as = copy_actor.(gs[:group][:selected])
            push!(gs[:group][:selected], as...)
            push!(gs[:group][:clickables], as...)

        elseif !isempty(ib)
            copy = copy_actor(ib[end])
            push!(gs[:group][:selected], copy)
            push!(gs[:group][:clickables], copy)
        end

    elseif key == Keys.K
        if !isempty(gs[:group][:selected])
            for c in gs[:group][:selected]
                c.data[:shake] = c.data[:shake] ? false : true
            end
        elseif !isempty(ib)
            ib[end].data[:shake] = ib[end].data[:shake] ? false : true
        end

    elseif key == Keys.F

        if !isempty(gs[:group][:selected])

			for a in gs[:group][:selected]

				for c in gs[:ALL_CARDS]

					if a.data[:parent_id] == c.id
						AN.reset_actor!(c.faces[begin], a.h, a.w)
						filter!(x->x!==a, gs[:group][:clickables])
						push!(gs[:group][:clickables], c.faces[begin])
					end
				end
            end

		elseif !isempty(ib)
			@show typeof(ib[end])

			for c in gs[:ALL_CARDS]

				if ib[end].data[:parent_id] == c.id
					@show "Changing face of $(c.name)!"
					for f in c.faces; f.angle = c.faces[begin].angle end
					for f in c.faces; f.position = ib[end].position end
					c.faces = circshift(c.faces, 1)
					filter!(x->x!==ib[end], gs[:group][:clickables])
					push!(gs[:group][:clickables], c.faces[begin])
				end
			end

			for dc in [ gs[:stage][:six_sided_die], gs[:stage][:glass_counter] ]

				if ib[end].data[:parent_id] == dc.id

					for f in dc.faces
						f.position = ib[end].position
					end

					dc.faces = circshift(dc.faces, 1)
					filter!(x->x!==ib[end], gs[:group][:clickables])
					push!(gs[:group][:clickables], dc.faces[begin])
				end
			end
        end

    elseif key == Keys.V
        if !isempty(gs[:group][:selected])
            for c in gs[:group][:selected]
                c.data[:fade] = true
            end
        elseif !isempty(ib)
            ib[end].data[:fade] = true
        end

    elseif key == Keys.S
        spin_cw = g.keyboard.RALT || g.keyboard.LALT ? false : true
        if !isempty(gs[:group][:selected])
            for a in gs[:group][:selected]
                a.data[:spin_cw] = spin_cw
                a.data[:spin] = a.data[:spin] ? false : true
            end
        elseif !isempty(ib)
            ib[end].data[:spin_cw] = spin_cw
            ib[end].data[:spin] = ib[end].data[:spin] ? false : true
        end

    elseif key == Keys.L
        if !isempty(gs[:group][:selected])
            for c in gs[:group][:selected]
                if haskey(c.data, :next_frame)
					c.data[:next_frame] = c.data[:next_frame] ? false : true
				end
            end
        elseif !isempty(ib)
			if haskey(ib[end].data, :next_frame)
            	ib[end].data[:next_frame] = ib[end].data[:next_frame] ? false : true
			end
        end

    elseif key == Keys.DELETE
        if !isempty(gs[:group][:selected])
            kill_card!.(gs[:group][:selected])
            # play_sound("$pd/Base/sounds/wilhelm.mp3")
        elseif !isempty(ib)
            kill_card!(ib[end])
            # play_sound("$pd/Base/sounds/wilhelm.mp3")
        end

    elseif key == Keys.TAB

		if g.keyboard.RSHIFT || g.keyboard.LSHIFT

			if g.keyboard.RCTRL || g.keyboard.LCTRL
                reset_stage!(gs)

			else
				for c in gs[:ALL_CARDS]
                	AN.reset_actor!(c.faces[begin],
						c.faces[begin].data[:sz][2],
						c.faces[begin].data[:sz][1])
				end
            end

        elseif !isempty(gs[:group][:selected])
            AN.reset_actor!.(gs[:group][:selected], gs[:deck][:CARD_WIDTH],
				gs[:deck][:CARD_HEIGHT])

        elseif !isempty(ib)
            AN.reset_actor!(ib[end], gs[:deck][:CARD_WIDTH], gs[:deck][:CARD_HEIGHT])
        end

    elseif key == Keys.EQUALS

		if !isempty(gs[:group][:selected])
            AN.grow_actor!.(gs[:group][:selected], gs[:deck][:CARD_WIDTH], gs[:deck][:CARD_HEIGHT])

		elseif !isempty(ib)
            AN.grow_actor!(ib[end], gs[:deck][:CARD_WIDTH], gs[:deck][:CARD_HEIGHT])
        end

    elseif key == Keys.MINUS

		if !isempty(gs[:group][:selected])
            AN.shrink_actor!.(gs[:group][:selected], gs[:deck][:CARD_WIDTH], gs[:deck][:CARD_HEIGHT])

		elseif !isempty(ib)
            AN.shrink_actor!(ib[end], gs[:deck][:CARD_WIDTH], gs[:deck][:CARD_HEIGHT])
        end

    elseif key == Keys.SPACE && length(ib) > 0
        zone = zone_check(ib[end], gs)

        if zone isa String && length(gs[:zone][zone]) > 0
            if zone == "Graveyard"
                AN.splay_actors!(
                    [ c.faces[begin] for c in gs[:zone][zone] ],
                    gs[:stage][zone].x,
                    gs[:stage][zone].y,
                    SCREEN_HEIGHT,
                    SCREEN_BORDER,
                    pitch=[0.02, 0.04],
                    )
			elseif zone == "Library"
				AN.splay_actors!([ c.faces[begin] for c in gs[:zone][zone] ],  # stack library cards into deck
					SCREEN_BORDER,
					ceil(Int32, SCREEN_HEIGHT - SCREEN_BORDER - gs[:zone]["Library"][end].faces[begin].h),
					SCREEN_HEIGHT,
					SCREEN_BORDER,
					pitch=[0.001, -0.005],
				)

            else
                AN.splay_actors!(
                    [ c.faces[begin] for c in gs[:zone][zone] ],
                    gs[:stage][zone].x,
                    gs[:stage][zone].y,
                    SCREEN_HEIGHT,
                    SCREEN_BORDER,
                    pitch=[0.05, 0.1],
                )
            end
        end
        # play_sound("$pd/Base/sounds/splay_actors.mp3")

    elseif key == Keys.BACKQUOTE
        try
            if "terminal.jls" in readdir("tmp")
                @show eval(g.game_module, Meta.parse(deserialize("$pd/tmp/terminal.jls")))
                rm("$pd/tmp/terminal.jls")
            end
        catch e
            @warn e
        end

	elseif key == Keys.F11
        #SDL2.SetWindowFullscreen(g.screen.window, SDL2.WINDOW_FULLSCREEN)
        SDL2.SetWindowFullscreen(g.screen.window, SDL2.WINDOW_FULLSCREEN_DESKTOP)
    end
end

# ╔═╡ 64b89860-468e-11eb-2b22-fff8ae5ea566
function update(g::Game)
    global gs

    ib = in_bounds(gs)

    for a in gs[:group][:clickables]
		if a isa Card || a isa Dice || a isa Counter
			a = a.faces[begin]
		end
        if a.data[:spin]; AN.spin_actor!(a) end
        if a.data[:shake]; AN.shake_actor!(a) end
        if a.data[:fade]; AN.fade_actor!(a) end
        if haskey(a.data, :next_frame) && a.data[:next_frame]
            if now() - a.data[:then] > a.data[:frame_delays][begin]
                AN.next_frame!(a)
            end
        end
    end
end

# ╔═╡ 70d25e54-468e-11eb-160f-9bdafa1ee16c
begin
	round_to(n, x) = round(Int32, x / n) * n
	draw(x::Card) = draw(x.faces[begin])
	draw(x::Dice) = draw(x.faces[begin])
	draw(x::Counter) = draw(x.faces[begin])

	#play_music(gs[:music][end], 1)  # play_music(name, loops=-1)

	#SDL2.SetWindowFullscreen(game[].screen.window, SDL2.WINDOW_FULLSCREEN_DESKTOP)
	SDL2.ShowCursor(Int32(0))  # hides system mouse cursor

	finalizer(kill_actor!, Actor)
end

# ╔═╡ Cell order:
# ╟─ad8ff974-4b71-11eb-3eb6-790d687615c0
# ╠═b35cdada-4689-11eb-2629-c1cedd8052bd
# ╟─409648d8-468e-11eb-2856-5bda6584cf79
# ╠═438d93b6-468e-11eb-2cdd-650d3873e81d
# ╟─799c2578-4cb3-11eb-1af7-b1ab01270e6d
# ╟─47f50362-468e-11eb-211d-f561273a906c
# ╟─4cbb84f2-468e-11eb-09af-718f893c0449
# ╟─54761f68-468e-11eb-3ab9-db06b7174615
# ╟─6159d724-468e-11eb-05fb-db68237e3fa0
# ╟─64b89860-468e-11eb-2b22-fff8ae5ea566
# ╠═69917186-468e-11eb-1175-dd4bbfe2f109
# ╟─cdd4c524-4cba-11eb-07a2-c7651ae7f211
# ╟─7ef01b58-523d-11eb-0c16-2b1d02dd1836
# ╠═70d25e54-468e-11eb-160f-9bdafa1ee16c


text:
function linear_map_slope(xiL,xiR,xL,xR)
    return (xR-xL)./(xiR-xiL)
end

function reference_interval_1d()
    return (-1.0,+1.0)
end

function reference_interval(dim)
    a,b = reference_interval_1d()
    xiL = a*ones(dim)
    xiR = b*ones(dim)
    return xiL,xiR
end

struct LineMap{dim,T}
    xiL::T
    xiR::T
    xL::MVector{dim,T}
    xR::MVector{dim,T}
    slope::MVector{dim,T}
    function LineMap(xiL::T,xiR::T,
        xL::MVector{dim,T},xR::MVector{dim,T}) where {dim,T}

        @assert 1 <= dim <= 3
        @assert xiL < xiR

        slope = linear_map_slope(xiL,xiR,xL,xR)

        new{dim,T}(xiL,xiR,xL,xR,slope)
    end
end

function LineMap(xiL,xiR,xL,xR)
    dim = length(xL)
    @assert length(xR) == dim
    sxL = MVector{dim}(xL)
    sxR = MVector{dim}(xR)
    return LineMap(xiL,xiR,sxL,sxR)
end

function dimension(M::LineMap{dim}) where {dim}
    return dim
end

function reference_interval(M::LineMap)
    return (M.xiL,M.xiR)
end

# function update!(M::LineMap,xL,xR)
#     dim = dimension(M)
#     @assert length(xL) == dim
#     @assert length(xR) == dim
#
#     M.xL .= xL
#     M.xR .= xR
#     M.slope .= linear_map_slope(M.xiL,M.xiR,M.xL,M.xR)
# end

function LineMap(xL,xR)
    xiL,xiR = reference_interval_1d()
    return LineMap(xiL,xiR,xL,xR)
end

function (M::LineMap)(xi)
    return M.xL .+ M.slope*(xi-M.xiL)
end

function jacobian(M::LineMap)
    return M.slope
end

function inverse_jacobian(M::LineMap)
    return 1.0 ./ jacobian(M)
end

function determinant_jacobian(M::LineMap)
    return norm(M.slope)
end

struct CellMap{dim,T}
    xiL::SVector{dim,T}
    xiR::SVector{dim,T}
    xL::MVector{dim,T}
    xR::MVector{dim,T}
    slope::MVector{dim,T}
    function CellMap(xiL::SVector{dim,T},xiR::SVector{dim,T},
        xL::MVector{dim,T},xR::MVector{dim,T}) where {dim,T}

        @assert 1 <= dim <= 3
        @assert all(xiL .< xiR)
        @assert all(xL .< xR)

        slope = linear_map_slope(xiL,xiR,xL,xR)

        new{dim,T}(xiL,xiR,xL,xR,slope)

    end
end

function CellMap(xiL,xiR,xL,xR)
    dim = length(xiL)
    @assert length(xiR) == dim
    @assert length(xL) == dim
    @assert length(xR) == dim

    sxiL = SVector{dim}(xiL)
    sxiR = SVector{dim}(xiR)
    mxL = MVector{dim}(xL)
    mxR = MVector{dim}(xR)
    return CellMap(sxiL,sxiR,mxL,mxR)
end

function CellMap(xi::IntervalBox{dim},x::IntervalBox{dim}) where {dim}
    xiL = [int.lo for int in xi]
    xiR = [int.hi for int in xi]
    xL = [int.lo for int in x]
    xR = [int.hi for int in x]
    CellMap(xiL,xiR,xL,xR)
end

function CellMap(x::IntervalBox{dim}) where {dim}
    xiL,xiR = reference_interval(dim)
    xL = [int.lo for int in x]
    xR = [int.hi for int in x]
    CellMap(xiL,xiR,xL,xR)
end

function CellMap(xL,xR)
    dim = length(xL)
    @assert length(xR) == dim
    xiL,xiR = reference_interval(dim)
    return CellMap(xiL,xiR,xL,xR)
end

function CellMap(dim::Z) where {Z<:Integer}
    xiL,xiR = reference_interval(dim)
    return CellMap(xiL,xiR,xiL,xiR)
end

function dimension(C::CellMap{dim}) where {dim}
    return dim
end

function update_range!(C::CellMap,xL,xR)
    dim = dimension(C)
    @assert length(xL) == dim
    @assert length(xR) == dim

    C.xL .= xL
    C.xR .= xR
    C.slope .= linear_map_slope(C.xiL,C.xiR,C.xL,C.xR)
end

function update_range!(C::CellMap{2},box::IntervalBox{2})
    C.xL[1] = box[1].lo
    C.xL[2] = box[2].lo
    C.xR[1] = box[1].hi
    C.xR[2] = box[2].hi

    C.slope .= linear_map_slope(C.xiL,C.xiR,C.xL,C.xR)
end

function (C::CellMap)(xi)
    return C.xL .+ C.slope .* (xi .- C.xiL)
end

function jacobian(C::CellMap)
    return C.slope
end

function face_determinant_jacobian(C::CellMap{2})
    j = jacobian(C)
    return [j[1],j[2],j[1],j[2]]
end

function inverse_jacobian(C::CellMap)
    return 1.0 ./ jacobian(C)
end

function determinant_jacobian(C::CellMap)
    return prod(jacobian(C))
end

function reference_cell_facemaps(dim)
    @assert dim == 2
    xL,xR = reference_interval(dim)
    m1 = LineMap([xL[1],xL[2]],[xR[1],xL[2]])
    m2 = LineMap([xR[1],xL[2]],[xR[1],xR[2]])
    m3 = LineMap([xL[1],xR[2]],[xR[1],xR[2]])
    m4 = LineMap([xL[1],xL[2]],[xL[1],xR[2]])
    return [m1,m2,m3,m4]
end

ext: jl


text:
using Test
using StructLH

struct AF1
    x
    y
end

function make_test_af_object()
    return AF1(AF1(1.0, AF1(2.0, "x")),  AF1(2.0, nothing))
end

function StructLH.describe(x :: AF1)
    return "AF1 object with children $(x.x) and $(x.y)"
end

function StructLH.describe(x :: Float64)
    return "Float64 with value $x"
end

function apply_fct_test()
    # Note that `sum` has methods for any object. Cannot be used for testing.
    @testset "Apply function to object" begin
        x = 1 : 3;
        xResult = apply_fct_to_object(x, :x, sum);
        @test obj_name(xResult) == :x;
        @test obj_type(xResult) == typeof(x);
        @test fct_value(xResult) == sum(x);
        @test isnothing(children(xResult))

        x = make_test_af_object()
        xResult = apply_fct_to_object(x, :x, abs);
        @test obj_name(xResult) == :x;
        @test obj_type(xResult) == typeof(x);
        @test isnothing(fct_value(xResult));
        @test length(children(xResult)) == 2

        child1 = children(xResult)[1];
        @test length(children(child1)) == 2
    end
end

function describe_test()
    @testset "Describe object" begin
        x = make_test_af_object();
        outV = describe_object(x)
        @test isa(outV, Vector{String})
        @test length(outV) > 5
        for j = 1 : length(outV)
            println(outV[j])
        end
    end
end

@testset "Apply functions" begin
    apply_fct_test()
    describe_test()
end

# ----------------


text:
farmer_model = @stochastic_model begin
    @stage 1 begin
        @parameters begin
            Crops = [:wheat, :corn, :beets]
            Cost = Dict(:wheat=>150, :corn=>230, :beets=>260)
            Budget = 500
        end
        @variable(model, x[c = Crops] >= 0)
        @objective(model, Min, sum(Cost[c]*x[c] for c in Crops))
        @constraint(model, sum(x[c] for c in Crops) <= Budget)
    end
    @stage 2 begin
        @decision x
        @parameters begin
            Purchased  = [:wheat, :corn]
            Sold = [:wheat, :corn, :bquota, :bextra]
            Required = Dict(:wheat=>200, :corn=>240, :beets=>0)
            PurchasePrice = Dict(:wheat=>238, :corn=>210)
            SellPrice = Dict(:wheat=>170, :corn=>150, :bquota=>36, :bextra=>10)
        end
        @uncertain ξ::YieldScenario = begin
            wheat::Float64
            corn::Float64
            beets::Float64
        end
        @variable(model, y[p = Purchased] >= 0)
        @variable(model, w[s = Sold] >= 0)
        @objective(model, Min, sum( PurchasePrice[p] * y[p] for p = Purchased) - sum( SellPrice[s] * w[s] for s in Sold))

        @constraint(model, const_minreq[p=Purchased],
            ξ[p] * x[p] + y[p] - w[p] >= Required[p])
        @constraint(model, const_minreq_beets,
            ξ[:beets] * x[:beets] - w[:bquota] - w[:bextra] >= Required[:beets])
        @constraint(model, const_aux, w[:bquota] <= 6000)
    end
end

ξ₁ = YieldScenario(3.0, 3.6, 24.0, probability = 1/3)
ξ₂ = YieldScenario(2.5, 3.0, 20.0, probability = 1/3)
ξ₃ = YieldScenario(2.0, 2.4, 16.0, probability = 1/3)

farmer = instantiate(farmer_model, [ξ₁,ξ₂,ξ₃])

push!(problems, (farmer, "Farmer"))


text:
struct FEOperatorFromWeakForm <: FEOperator
  res::Function
  jac::Function
  assem::Assembler
end

function FEOperator(res::Function,jac::Function,assem::Assembler)
  FEOperatorFromWeakForm(res,jac,assem)
end

function FEOperator(res::Function,jac::Function,args...)
  assem = SparseMatrixAssembler(args...)
  FEOperator(res,jac,assem)
end

function FEOperator(res::Function,assem::Assembler)
  jac(u,du,dv) = jacobian(x->res(x,dv),u)
  FEOperatorFromWeakForm(res,jac,assem)
end

function FEOperator(res::Function,args...)
  assem = SparseMatrixAssembler(args...)
  FEOperator(res,assem)
end

get_test(op::FEOperatorFromWeakForm) = get_test(op.assem)
get_trial(op::FEOperatorFromWeakForm) = get_trial(op.assem)

function allocate_residual(op::FEOperatorFromWeakForm,uh::FEFunction)
  v = get_cell_shapefuns(get_test(op))
  vecdata = collect_cell_vector(op.res(uh,v))
  allocate_vector(op.assem, vecdata)
end

function residual!(b::AbstractVector,op::FEOperatorFromWeakForm,uh::FEFunction)
  v = get_cell_shapefuns(get_test(op))
  vecdata = collect_cell_vector(op.res(uh,v))
  assemble_vector!(b,op.assem, vecdata)
  b
end

function allocate_jacobian(op::FEOperatorFromWeakForm,uh::FEFunction)
  du = get_cell_shapefuns_trial(get_trial(op))
  v = get_cell_shapefuns(get_test(op))
  matdata = collect_cell_matrix(op.jac(uh,du,v))
  allocate_matrix(op.assem, matdata)
end

function jacobian!(A::AbstractMatrix,op::FEOperatorFromWeakForm,uh::FEFunction)
  du = get_cell_shapefuns_trial(get_trial(op))
  v = get_cell_shapefuns(get_test(op))
  matdata = collect_cell_matrix(op.jac(uh,du,v))
  assemble_matrix!(A,op.assem,matdata)
  A
end

function residual_and_jacobian!(
  b::AbstractVector,A::AbstractMatrix,op::FEOperatorFromWeakForm,uh::FEFunction)
  du = get_cell_shapefuns_trial(get_trial(op))
  v = get_cell_shapefuns(get_test(op))
  data = collect_cell_matrix_and_vector(op.jac(uh,du,v),op.res(uh,v))
  assemble_matrix_and_vector!(A, b, op.assem, data)
  (b,A)
end

function residual_and_jacobian(op::FEOperatorFromWeakForm,uh::FEFunction)
  du = get_cell_shapefuns_trial(get_trial(op))
  v = get_cell_shapefuns(get_test(op))
  data = collect_cell_matrix_and_vector(op.jac(uh,du,v),op.res(uh,v))
  A, b = assemble_matrix_and_vector(op.assem, data)
  (b, A)
end


text:
output_surface = Winston.config_value("default","output_surface")
output_surface = symbol(lowercase(get(ENV, "WINSTON_OUTPUT", output_surface)))

export errorbar,
       file,
       imagesc,
       loglog,
       oplot,
       oplothist,
       plot,
       plothist,
       semilogx,
       semilogy,
       spy,
       title,
       xlabel,
       xlim,
       ylabel,
       ylim

type WinstonDisplay <: Display end
pushdisplay(WinstonDisplay())

import Base.display
if output_surface == :gtk
    include("gtk.jl")
    display(::WinstonDisplay, p::PlotContainer) = gtk(p)
elseif output_surface == :tk
    include("tk.jl")
    display(::WinstonDisplay, p::PlotContainer) = tk(p)
else
    assert(false)
end

_pwinston = FramedPlot()

#system functions
file(fname::String, args...; kvs...) = file(_pwinston, fname, args...; kvs...)
display() = display(_pwinston)

for f in (:xlabel,:ylabel,:title)
    @eval $f(s::String) = (setattr(_pwinston, $f=s); _pwinston)
end
for (f,k) in ((:xlim,:xrange),(:ylim,:yrange))
    @eval $f(a, b) = (setattr(_pwinston, $k=(a,b)); _pwinston)
    @eval $f(a) = (setattr(_pwinston, $k=(a[1],a[2])); _pwinston)
end

#shortcuts for creating log-scale plots
semilogx(args...; kvs...) = plot(args...; xlog=true, kvs...)
semilogy(args...; kvs...) = plot(args...; ylog=true, kvs...)
loglog(args...; kvs...) = plot(args...; xlog=true, ylog=true, kvs...)

const chartokens = [
    '-' => {:linekind => "solid"},
    ':' => {:linekind => "dotted"},
    ';' => {:linekind => "dotdashed"},
    '+' => {:symbolkind => "plus"},
    'o' => {:symbolkind => "circle"},
    '*' => {:symbolkind => "asterisk"},
    '.' => {:symbolkind => "dot"},
    'x' => {:symbolkind => "cross"},
    's' => {:symbolkind => "square"},
    'd' => {:symbolkind => "diamond"},
    '^' => {:symbolkind => "triangle"},
    'v' => {:symbolkind => "down-triangle"},
    '>' => {:symbolkind => "right-triangle"},
    '<' => {:symbolkind => "left-triangle"},
    'y' => {:color => "yellow"},
    'm' => {:color => "magenta"},
    'c' => {:color => "cyan"},
    'r' => {:color => "red"},
    'g' => {:color => "green"},
    'b' => {:color => "blue"},
    'w' => {:color => "white"},
    'k' => {:color => "black"},
]

function _parse_spec(spec::String)
    style = Dict()

    for (k,v) in [ "--" => "dashed", "-." => "dotdashed" ]
        splitspec = split(spec, k)
        if length(splitspec) > 1
            style[:linekind] = v
            spec = join(splitspec)
        end
    end

    for char in spec
        if haskey(chartokens, char)
            for (k,v) in chartokens[char]
                style[k] = v
            end
        end
    end

    style
end

function default_color(i::Int)
    cs = [0x000000, 0xED2C30, 0x008C46, 0x1859A9,
          0xF37C21, 0x652B91, 0xA11C20, 0xB33794]
    cs[mod1(i,length(cs))]
end

function plot(p::FramedPlot, args...; kvs...)
    args = {args...}
    components = {}
    i = 0

    while length(args) > 0
        local x, y, sopts

        if length(args) == 1 || typeof(args[2]) <: String
            y = shift!(args)
            x = 1:length(y)
        else
            x = shift!(args)
            y = shift!(args)
        end

        if length(args) > 0 && typeof(args[1]) <: String
            sopts = _parse_spec(shift!(args))
        else
            sopts = {:linekind => "solid"}
        end
        if !haskey(sopts, :color)
            i += 1
            sopts[:color] = default_color(i)
        end

        if haskey(sopts, :linekind) || !haskey(sopts, :symbolkind)
            push!(components, Curve(x, y, sopts))
        end
        if haskey(sopts, :symbolkind)
            push!(components, Points(x, y, sopts))
        end
    end

    for (k,v) in kvs
        if k in [:linekind,:symbolkind,:color,:linecolor,:linewidth,:symbolsize]
            for c in components
                style(c, k, v)
            end
        else
            setattr(p, k, v)
        end
    end

    for c in components
        add(p, c)
    end

    global _pwinston = p
    p
end
plot(args...; kvs...) = plot(FramedPlot(), args...; kvs...)

# shortcut for overplotting
oplot(args...; kvs...) = plot(_pwinston, args...; kvs...)

typealias Interval (Real,Real)

function data2rgb{T<:Real}(data::AbstractArray{T,2}, limits::Interval, colormap)
    img = similar(data, Uint32)
    ncolors = length(colormap)
    limlower = limits[1]
    limscale = ncolors/(limits[2]-limits[1])
    for i = 1:length(data)
        datai = data[i]
        if isfinite(datai)
            idxr = limscale*(datai - limlower)
            idx = itrunc(idxr)
            idx += idxr > convert(T, idx)
            if idx < 1 idx = 1 end
            if idx > ncolors idx = ncolors end
            img[i] = colormap[idx]
        else
            img[i] = 0x00000000
        end
    end
    img
end

# from http://www.metastine.com/?p=7
function jetrgb(x)
    fourValue = 4x
    r = min(fourValue - 1.5, -fourValue + 4.5)
    g = min(fourValue - 0.5, -fourValue + 3.5)
    b = min(fourValue + 0.5, -fourValue + 2.5)
    RGB(clamp(r,0.,1.), clamp(g,0.,1.), clamp(b,0.,1.))
end

JetColormap() = Uint32[ convert(RGB24,jetrgb(i/256)) for i = 1:256 ]

_default_colormap = JetColormap()

GrayColormap() = Uint32[ convert(RGB24,RGB(i/255,i/255,i/255)) for i = 0:255 ]

function imagesc{T<:Real}(xrange::Interval, yrange::Interval, data::AbstractArray{T,2}, clims::Interval)
    p = FramedPlot()
    setattr(p, "xrange", xrange)
    setattr(p, "yrange", reverse(yrange))
    img = data2rgb(data, clims, _default_colormap)
    add(p, Image(xrange, reverse(yrange), img))
    p
end

imagesc(xrange, yrange, data) = imagesc(xrange, yrange, data, (min(data),max(data)+1))
imagesc(data) = ((h, w) = size(data); imagesc((0,w), (0,h), data))
imagesc{T}(data::AbstractArray{T,2}, clims::Interval) = ((h, w) = size(data); imagesc((0,w), (0,h), data, clims))

function spy(S::SparseMatrixCSC, nrS::Integer, ncS::Integer)
    m, n = size(S)
    colptr = S.colptr
    rowval = S.rowval
    nzval  = S.nzval

    if nrS > m; nrS = m; end
    if ncS > n; ncS = n; end

    target = zeros(nrS, ncS)
    x = nrS / m
    y = ncS / n

    for col = 1:n
        for k = colptr[col]:colptr[col+1]-1
            row = rowval[k]
            target[ceil(row * x), ceil(col * y)] += 1
        end
    end

    imagesc((1,m), (1,n), target)
end

spy(S::SparseMatrixCSC) = spy(S, 100, 100)
spy(A::AbstractMatrix, nrS, ncS) = spy(sparse(A), nrS, ncS)
spy(A::AbstractMatrix) = spy(sparse(A))

function plothist(p::FramedPlot, h::(Range,Vector); kvs...)
    c = Histogram(h...)
    add(p, c)

    for (k,v) in kvs
        if k in [:color,:linecolor,:linekind,:linetype,:linewidth]
            style(c, k, v)
        else
            setattr(p, k, v)
        end
    end

    global _pwinston = p
    p
end
plothist(p::FramedPlot, args...; kvs...) = plothist(p::FramedPlot, hist(args...); kvs...)
plothist(args...; kvs...) = plothist(FramedPlot(), args...; kvs...)

# shortcut for overplotting
oplothist(args...; kvs...) = plothist(_pwinston, args...; kvs...)

#errorbar
errorbar(args...; kvs...) = errorbar(_pwinston, args...; kvs...)
function errorbar(p::FramedPlot, x::AbstractVector, y::AbstractVector; xerr=nothing, yerr=nothing, kvs...)

    xn=length(x)
    yn=length(y)

    if xerr != nothing
        xen = length(xerr)
        if xen == xn
            cx = SymmetricErrorBarsX(x, y, xerr)
        elseif xen == 2xn
            cx = ErrorBarsX(y, x.-xerr[1:xn], x.+xerr[xn+1:xen])
        else
            warn("Dimensions of x and xerr do not match!")
        end
        style(cx; kvs...)
        add(p,cx)
    end

    if yerr != nothing
        yen=length(yerr)
        if yen == yn
            cy = SymmetricErrorBarsY(x, y, yerr)
        elseif yen == 2yn
            cy = ErrorBarsY(x, y.-yerr[1:yn], y.+yerr[yn+1:yen])
        else
            warn("Dimensions of y and yerr do not match!")
        end
        style(cy; kvs...)
        add(p,cy)
    end

    global _pwinston = p
    p
end


text:
using LinearAlgebraicRepresentation
using Plasm
using PyCall
@pyimport pyplasm as p

geom_0 = hcat([[x] for x=0.:10]...)
topol_0 = [[i,i+1] for i=1:9]
geom_1 = hcat([[0.],[1.],[2.]]...)
topol_1 = [[1,2],[2,3]]

model_0 = (geom_0,topol_0)
model_1 = (geom_1,topol_1)
model_2 = LinearAlgebraicRepresentation.larModelProduct(model_0, model_1)
model_3 = LinearAlgebraicRepresentation.larModelProduct(model_2, model_1)

p.VIEW(Plasm.lar2hpc(model_3...))
V,CV = model_3

hpc = Plasm.hpc_exploded( (V,[CV]) )(1.5,1.5,1.5)
Plasm.view(hpc)

hpc = Plasm.lar2exploded_hpc(model_3[1], model_3[2])(1.5,1.5,1.5)
Plasm.view(hpc)

shape = [10,10,2]
cubes = LinearAlgebraicRepresentation.larCuboids(shape,true)
V,FV = cubes[1],cubes[2][3]
Plasm.viewexploded(V,FV)()

V,EV = LinearAlgebraicRepresentation.larCuboids([30])
V1 = hcat( map(u->[u; 0.0], V )...)
W = hcat( map(u->[cos(u*(2π/30)); sin(u*(2π/30))], V )...)
Plasm.viewexploded(V1,EV)()
Plasm.viewexploded(W,EV)()

V,FV = LinearAlgebraicRepresentation.larCuboids([30,6])
V2 = [2π/30 0; 0 1/6] * V
W = [V2[:,k] for k=1:size(V,2)]
Z = hcat( map(p->let (u,v) = p; [v*cos(u); v*sin(u)] end, W) ...)
Plasm.viewexploded(V2,FV)()
Plasm.viewexploded(Z,FV)()

V,CV = LinearAlgebraicRepresentation.larCuboids([16,32,1])
V3 = [π/16 0 0; 0 2π/32 0; 0 0 1] * V
W = [V3[:,k]-[π/2,π,1/2] for k=1:size(V3,2)]
Z = hcat( map(p->let (u,v,w) = p; [w*cos(u)*cos(v); w*cos(u)*sin(v); w*sin(u)] end, W) ...)
Plasm.viewexploded(V3,CV)()
Plasm.viewexploded(Z,CV)()


text:
# from a suggestion at https://discourse.julialang.org/t/install-a-packages-from-a-list/30920/3

using Pkg

dependencies = [
    "JLD",
    "LoopVectorization",
    "Optim",
    "NLsolve",
    "StatsBase",
    "Distributions",
    "Plots",
    "Revise"
]

Pkg.add(dependencies)


text:
""" Data structure for C-sets (copresheaves) and attributed C-sets.
"""
module CSetDataStructures
export AbstractACSet, ACSet, AbstractCSet, CSet, Schema, FreeSchema,
  AbstractACSetType, ACSetType, ACSetTableType, AbstractCSetType, CSetType,
  tables, parts, nparts, has_part, subpart, has_subpart, incident,
  add_part!, add_parts!, set_subpart!, set_subparts!, rem_part!, rem_parts!,
  copy_parts!, copy_parts_only!, disjoint_union, pretty_tables, @acset

using Compat: isnothing, only

using MLStyle: @match
using PrettyTables: pretty_table
import Tables, TypedTables

using ...Meta, ...Present
using ...Syntax: GATExpr, args
using ...Theories: Schema, FreeSchema, dom, codom, codom_num, data_num, attrs_by_codom,
  CatDesc, CatDescType, AttrDesc, AttrDescType, SchemaType

# Data types
############

""" Abstract type for attributed C-sets, including C-sets as a special case.

The type parameters are:

- `CD`: indexing category C, encoded as a type
- `AD`: data types and data attributes, encoded as a type
- `Ts`: Julia types corresponding to data types in schema

Together, the first two type parameters encode a schema, see `Schema`.

See also: [`AttributedCSet`](@ref).
"""
abstract type AbstractAttributedCSet{CD <: CatDesc, AD <: AttrDesc{CD},
                                     Ts <: Tuple} end

""" Alias for the abstract type `AbstractAttributedCSet`.
"""
const AbstractACSet = AbstractAttributedCSet

""" Data type for attributed C-sets, including C-sets as a special case.

Instead of filling out the type parameters manually, we recommend using the
function [`CSetType`](@ref) or [`ACSetType`](@ref) to generate a data type from
a schema. Nevertheless, the first three type parameters are documented at
[`AbstractAttributedCSet`](@ref). The remaining type parameters are an
implementation detail and should be ignored.
"""
struct AttributedCSet{CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple,
                      Idxed, UniqueIdxed, Tables <: NamedTuple,
                      Indices <: NamedTuple} <: AbstractACSet{CD,AD,Ts}
  tables::Tables
  indices::Indices
end

function AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed}(
    tables::Tables, indices::Indices) where
    {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple, Idxed, UniqueIdxed,
     Tables <: NamedTuple, Indices <: NamedTuple}
  AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed,Tables,Indices}(tables, indices)
end

function AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed}(;
    table_type::Type=TypedTables.Table) where
    {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple, Idxed, UniqueIdxed}
  tables = make_tables(table_type,CD,AD,Ts)
  indices = make_indices(CD,AD,Ts,Idxed,UniqueIdxed)
  AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed}(tables, indices)
end

function AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed,Tables,Indices}() where
    {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple, Idxed, UniqueIdxed,
     Tables <: NamedTuple, Indices <: NamedTuple}
  # Find first table type for a table with at least one column.
  i = findfirst(!=(Vector{NamedTuple{(),Tuple{}}}), Tables.types)
  if isnothing(i)
    AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed}()
  else
    T = Tables.types[i].name.wrapper # `UnionAll` corresponding to type.
    AttributedCSet{CD,AD,Ts,Idxed,UniqueIdxed}(table_type=T)
  end
end

""" Alias for the data type `AttributedCSet`.
"""
const ACSet = AttributedCSet

""" Generate an abstract type for attributed C-sets from a schema.

To generate a concrete type, use [`ACSetType`](@ref).
"""
function AbstractACSetType(pres::Presentation{Schema})
  type_vars = [ TypeVar(nameof(data)) for data in generators(pres, :Data) ]
  if isempty(type_vars)
    # When the schema has no data attributes, allow subtyping from any schema
    # extending it with attributes.
    AbstractACSet{CatDescType(pres)}
  else
    T = AbstractACSet{SchemaType(pres)..., Tuple{type_vars...}}
    foldr(UnionAll, type_vars, init=T)
  end
end

""" Generate a type for attributed C-sets from a schema.

In addition to the schema, you can specify which morphisms and data attributes
are (uniquely) indexed using the keyword argument `index` (or `unique_index`).
By default, no morphisms or data attributes are indexed.

See also: [`AbstractACSetType`](@ref).
"""
function ACSetType(pres::Presentation{Schema}; index=[], unique_index=[])
  type_vars = [ TypeVar(nameof(data)) for data in generators(pres, :Data) ]
  T = ACSet{SchemaType(pres)..., Tuple{type_vars...},
            Tuple(sort!(index ∪ unique_index)), Tuple(sort!(unique_index))}
  foldr(UnionAll, type_vars, init=T)
end

""" Given an attributed C-set type, generate a new type based on one object.

The resulting attributed C-set type can be seen as the type of a data table or
data frame, hence the name.
"""
function ACSetTableType(X::Type, ob₀::Symbol; union_all::Bool=false)
  (union_all ? ACSetTableUnionAll : ACSetTableDataType)(X, Val{ob₀})
end

@generated function ACSetTableDataType(::Type{X}, ::Type{Val{ob₀}}) where
    {CD<:CatDesc, AD<:AttrDesc{CD}, Ts, X<:AbstractACSet{CD,AD,Ts}, ob₀}
  CD₀, AD₀ = ACSetTableDesc(CD, AD, ob₀)
  :(ACSet{$CD₀,$AD₀,$Ts,(),()})
end

@generated function ACSetTableUnionAll(::Type{X}, ::Type{Val{ob₀}}) where
    {CD<:CatDesc, AD<:AttrDesc{CD}, X<:AbstractACSet{CD,AD}, ob₀}
  CD₀, AD₀ = ACSetTableDesc(CD, AD, ob₀)
  :(ACSet{$CD₀,$AD₀,Tuple{$(AD.data...)},(),()} where {$(AD.data...)})
end

function ACSetTableDesc(::Type{CD}, ::Type{AD}, ob₀::Symbol) where
    {CD<:CatDesc, AD<:AttrDesc{CD}}
  @assert ob₀ ∈ CD.ob
  attrs₀ = [ i for (i,j) in enumerate(AD.adom) if CD.ob[j] == ob₀ ]
  adom = Tuple(ones(Int, length(attrs₀)))
  CD₀ = CatDesc{(ob₀,),(),(),()}
  AD₀ = AttrDesc{CD₀,AD.data,AD.attr[attrs₀],adom,AD.acodom[attrs₀]}
  (CD₀, AD₀)
end

""" Abstract type for C-sets.

The special case of `AbstractAttributedCSet` with no data attributes.
"""
const AbstractCSet{CD} = AbstractACSet{CD,AttrDesc{CD,(),(),(),()},Tuple{}}

""" Data type for C-sets.

The special case of `AttributedCSet` with no data attributes.
"""
const CSet{CD,Idxed,UniqueIdxed} =
  ACSet{CD,AttrDesc{CD,(),(),(),()},Tuple{},Idxed,UniqueIdxed}

""" Generate an abstract type for C-sets from a presentation of a category C.

To generate a concrete type, use [`CSetType`](@ref).
"""
function AbstractCSetType(pres::Presentation{Schema})
  AbstractCSet{CatDescType(pres)}
end

""" Generate a type for C-sets from a presentation of a category C.

In addition to the category, you can specify which morphisms are (uniquely)
indexed using the keyword argument `index` (or `unique_index`). By default, no
morphisms are indexed.

See also: [`AbstractCSetType`](@ref).
"""
function CSetType(pres::Presentation{Schema}; index=[], unique_index=[])
  if !(isempty(generators(pres, :Data)) && isempty(generators(pres, :Attr)))
    error("Use `ACSetType` instead of `CSetType` for schemas with data attributes")
  end
  CSet{CatDescType(pres),
       Tuple(sort!(index ∪ unique_index)), Tuple(sort!(unique_index))}
end

function make_indices(::Type{CD}, AD::Type{<:AttrDesc{CD}},
                      Ts::Type{<:Tuple}, Idxed::Tuple, UniqueIdxed::Tuple) where {CD}
  # TODO: Could be `@generated` for faster initialization of C-sets.
  NamedTuple{Idxed}(Tuple(map(Idxed) do name
    IndexType = name ∈ UniqueIdxed ? Int : Vector{Int}
    if name ∈ CD.hom
      Vector{IndexType}()
    elseif name ∈ AD.attr
      Dict{Ts.parameters[codom_num(AD,name)],IndexType}()
    else
      error("Cannot index $name: not a morphism or an attribute")
    end
  end))
end

function make_tables(Table::Type, ::Type{CD}, AD::Type{<:AttrDesc{CD}},
                     Ts::Type{<:Tuple}) where {CD}
  # TODO: Could be `@generated` for faster initialization of C-sets.
  cols = NamedTuple{CD.ob}((names=Symbol[], types=Type[]) for ob in CD.ob)
  for hom in CD.hom
    col = cols[dom(CD,hom)]
    push!(col.names, hom); push!(col.types, Int)
  end
  for attr in AD.attr
    col = cols[dom(AD,attr)]
    push!(col.names, attr); push!(col.types, Ts.parameters[codom_num(AD,attr)])
  end
  map(cols) do col
    if isempty(col.names)
      # Tables based on structs of arrays, such as StructArrays and TypedTables,
      # generally do not support empty structs, so we use an array of empty
      # structs instead. See also:
      # https://github.com/JuliaArrays/StructArrays.jl/issues/148
      NamedTuple{(),Tuple{}}[]
    else
      make_table(Table, NamedTuple{Tuple(col.names)}((T[] for T in col.types)))
    end
  end
end
make_table(::Type{T}, cols) where T = T(cols)
make_table(::Type{NamedTuple}, cols) = cols # No copy constructor defined.

function Base.:(==)(x1::T, x2::T) where T <: ACSet
  # The indices hold redundant information, so need not be compared.
  x1.tables == x2.tables
end

function Base.copy(acs::T) where T <: ACSet
  T(map(copy_table, acs.tables), deepcopy(acs.indices))
end

copy_table(table) = copy(table)
copy_table(table::NamedTuple) = map(copy, table)

function Base.show(io::IO, acs::T) where {CD,AD,Ts,T<:AbstractACSet{CD,AD,Ts}}
  print(io, T <: AbstractCSet ? "CSet" : "ACSet")
  println(io, "(")
  join(io, vcat(
    [ "  $ob = 1:$(nparts(acs,ob))" for ob in CD.ob ],
    [ "  $data = $(Ts.parameters[i])" for (i,data) in enumerate(AD.data) ],
    [ "  $hom : $(dom(CD,i)) → $(codom(CD,i)) = $(subpart(acs,hom))"
      for (i,hom) in enumerate(CD.hom) ],
    [ "  $attr : $(dom(AD,i)) → $(codom(AD,i)) = $(subpart(acs,attr))"
      for (i,attr) in enumerate(AD.attr) ],
  ), ",\n")
  print(io, ")")
end

function Base.show(io::IO, ::MIME"text/plain", acs::T) where {T<:AbstractACSet}
  print(io, T <: AbstractCSet ? "CSet" : "ACSet")
  print(io, " with elements ")
  join(io, ["$ob = 1:$(nparts(acs,ob))" for ob in keys(tables(acs))], ", ")
  println(io)
  pretty_tables(io, acs)
end

function Base.show(io::IO, ::MIME"text/html", acs::T) where {T<:AbstractACSet}
  println(io, "
")
  print(io, "")
  print(io, T <: AbstractCSet ? "CSet" : "ACSet")
  print(io, " with elements ")
  join(io, ["$ob = 1:$(nparts(acs,ob))" for ob in keys(tables(acs))], ", ")
  println(io, "")
  pretty_tables(io, acs, backend=:html, standalone=false)
  println(io, "
")
end

""" Show all tables in a C-set using PrettyTables.jl.

Any keyword arguments are passed to `pretty_table`.
"""
function pretty_tables(io::IO, acs::AbstractACSet; kw...)
  options = merge((nosubheader=true, show_row_number=true), (; kw...))
  for (ob, table) in pairs(tables(acs))
    # Note: PrettyTables will not print tables with zero rows.
    if !(isempty(Tables.columnnames(table)) || Tables.rowcount(table) == 0)
      pretty_table(io, table, row_number_column_title=string(ob); options...)
    end
  end
end
pretty_tables(acs::AbstractACSet; kw...) = pretty_tables(stdout, acs; kw...)

# Imperative interface
######################

""" Tables defining a C-set.

A named tuple with a table for each part type. To ensure consistency, do not
directly mutate these tables, especially when indexing is enabled!
"""
tables(acs::ACSet) = acs.tables

""" Parts of given type in a C-set.
"""
parts(acs::ACSet, type) = 1:nparts(acs, type)

""" Number of parts of given type in a C-set.
"""
nparts(acs::ACSet, type) = Tables.rowcount(acs.tables[type])

# XXX: https://github.com/JuliaData/Tables.jl/issues/170
Tables.rowcount(x::AbstractVector{NamedTuple{(),Tuple{}}}) = length(x)

""" Whether a C-set has a part with the given name.
"""
has_part(acs::ACSet, type::Symbol) = _has_part(acs, Val{type})

@generated function _has_part(::ACSet{CD,AD}, ::Type{Val{type}}) where
    {CD,AD,type}
  type ∈ CD.ob || type ∈ AD.data
end

has_part(acs::ACSet, type::Symbol, part::Int) = 1 <= part <= nparts(acs, type)
has_part(acs::ACSet, type::Symbol, part::AbstractVector{Int}) =
  let n=nparts(acs, type); [ 1 <= x <= n for x in part ] end

""" Whether a C-set has a subpart with the given name.
"""
has_subpart(acs::ACSet, name::Symbol) = _has_subpart(acs, Val{name})

@generated function _has_subpart(::ACSet{CD,AD}, ::Type{Val{name}}) where
    {CD,AD,name}
  name ∈ CD.hom || name ∈ AD.attr
end

""" Get subpart of part in C-set.

Both single and vectorized access are supported, with a view of the underlying
data being returned in the latter case. Chaining, or composition, of subparts is
also supported. For example, given a vertex-attributed graph `g`,

```
subpart(g, e, [:src, :vattr])
```

returns the vertex attribute of the source vertex of the edge `e`. As a
shorthand, subparts can also be accessed by indexing:

```
g[e, :src] == subpart(g, e, :src)
```

Be warned that indexing with lists of subparts works just like `subpart`:
`g[e,[:src,:vattr]]` is equivalent to `subpart(g, e, [:src,:vattr])`. This
convention differs from DataFrames but note that the alternative interpretation
of `[:src,:vattr]` as two independent columns does not even make sense, since
they have different domains (belong to different tables).
"""
@inline subpart(acs::ACSet, part, name) = view_slice(subpart(acs, name), part)
@inline subpart(acs::ACSet, name::Symbol) = _subpart(acs, Val{name})
# These accessors must be inlined to ensure that constant names are propagated
# at compile time, e.g., `subpart(g, :src)` becomes `_subpart(g, Val{:src})`.

subpart(acs::ACSet, expr::GATExpr{:generator}) = subpart(acs, first(expr))
subpart(acs::ACSet, expr::GATExpr{:id}) = parts(acs, first(dom(expr)))

function subpart(acs::ACSet, part, names::AbstractVector{Symbol})
  foldl(names, init=part) do part, name
    subpart(acs, part, name)
  end
end
subpart(acs::ACSet, part, expr::GATExpr{:compose}) =
  subpart(acs, part, subpart_names(expr))

subpart(acs::ACSet, names::AbstractVector{Symbol}) =
  subpart(acs, subpart(acs, names[1]), names[2:end])
subpart(acs::ACSet, expr::GATExpr{:compose}) = subpart(acs, subpart_names(expr))

subpart_names(expr::GATExpr{:generator}) = Symbol[first(expr)]
subpart_names(expr::GATExpr{:id}) = Symbol[]
subpart_names(expr::GATExpr{:compose}) =
  mapreduce(subpart_names, vcat, args(expr))

@generated function _subpart(acs::ACSet{CD,AD,Ts}, ::Type{Val{name}}) where
    {CD,AD,Ts,name}
  if name ∈ CD.hom
    :(acs.tables.$(dom(CD,name)).$name)
  elseif name ∈ AD.attr
    :(acs.tables.$(dom(AD,name)).$name)
  else
    throw(ArgumentError("$(repr(name)) not in $(CD.hom) or $(AD.attr)"))
  end
end

@inline Base.getindex(acs::ACSet, args...) = subpart(acs, args...)

@inline view_slice(x::AbstractVector, i) = view(x, i)
@inline view_slice(x::AbstractVector, i::Int) = x[i]

""" Get superparts incident to part in C-set.

If the subpart is indexed, this takes constant time; otherwise, it takes linear
time. As with [`subpart`](@ref), both single and vectorized access, as well as
chained access, are supported. Note that sequences of morphisms are supplied in
the usual left-to-right order, so that

```
incident(g, x, [:src, :vattr])
```

returns the list of all edges whose source vertex has vertex attribute `x`.

Note that when the subpart is indexed, this function returns a view of the
underlying index, which should not be mutated. To ensure that a fresh copy is
returned, regardless of whether indexing is enabled, set the keyword argument
`copy=true`.
"""
@inline incident(acs::ACSet, part, name::Symbol; copy::Bool=false) =
  _incident(acs, part, Val{name}; copy=copy)
# Inlined for same reason as `subpart`.

function incident(acs::ACSet, part, names::AbstractVector{Symbol};
                  copy::Bool=false)
  # Don't need to pass `copy` because copy will be made regardless.
  foldr(names, init=part) do name, part
    reduce(vcat, incident(acs, part, name), init=Int[])
  end
end
incident(acs::ACSet, part, expr::GATExpr; kw...) =
  incident(acs, part, subpart_names(expr); kw...)

@generated function _incident(acs::ACSet{CD,AD,Ts,Idxed}, part,
    ::Type{Val{name}}; copy::Bool=false) where {CD,AD,Ts,Idxed,name}
  if name ∈ CD.hom
    if name ∈ Idxed
      quote
        indices = view_slice(acs.indices.$name, part)
        copy ? Base.copy.(indices) : indices
      end
    else
      :(broadcast_findall(part, acs.tables.$(dom(CD,name)).$name))
    end
  elseif name ∈ AD.attr
    if name ∈ Idxed
      quote
        indices = get_data_index.(Ref(acs.indices.$name), part)
        copy ? Base.copy.(indices) : indices
      end
    else
      :(broadcast_findall(part, acs.tables.$(dom(AD,name)).$name))
    end
  else
    throw(ArgumentError("$(repr(name)) not in $(CD.hom) or $(AD.attr)"))
  end
end

broadcast_findall(xs, array::AbstractArray) =
  broadcast(x -> findall(y -> x == y, array), xs)

""" Add part of given type to C-set, optionally setting its subparts.

Returns the ID of the added part.

See also: [`add_parts!`](@ref).
"""
function add_part!(acs::ACSet, type::Symbol, args...; kw...)
  part = only(_add_parts!(acs, Val{type}, 1))
  try
    set_subparts!(acs, part, args...; kw...)
  catch e
    rem_part!(acs, type, part)
    rethrow(e)
  end
  part
end

""" Add parts of given type to C-set, optionally setting their subparts.

Returns the range of IDs for the added parts.

See also: [`add_part!`](@ref).
"""
function add_parts!(acs::ACSet, type::Symbol, n::Int, args...; kw...)
  parts = _add_parts!(acs, Val{type}, n)
  try
    set_subparts!(acs, parts, args...; kw...)
  catch e
    rem_parts!(acs, type, parts)
    rethrow(e)
  end
  parts
end

@generated function _add_parts!(acs::ACSet{CD,AD,Ts,Idxed}, ::Type{Val{ob}},
                                n::Int) where {CD,AD,Ts,Idxed,ob}
  out_homs = filter(hom -> dom(CD, hom) == ob, CD.hom)
  indexed_in_homs = filter(hom -> codom(CD, hom) == ob && hom ∈ Idxed, CD.hom)
  quote
    if n == 0; return 1:0 end
    nparts = Tables.rowcount(acs.tables.$ob) + n
    resize_table!(acs.tables.$ob, nparts)
    start = nparts - n + 1
    $(Expr(:block, map(out_homs) do hom
        :(@inbounds acs.tables.$ob.$hom[start:nparts] .= 0)
     end...))
    $(Expr(:block, map(indexed_in_homs) do hom
        quote
          resize!(acs.indices.$hom, nparts)
          @inbounds for i in start:nparts; acs.indices.$hom[i] = Int[] end
        end
      end...))
    start:nparts
  end
end

resize_table!(table, n) = map(col -> resize!(col, n), Tables.columns(table))
resize_table!(table::Vector, n) = resize!(table, n)

""" Mutate subpart of a part in a C-set.

Both single and vectorized assignment are supported.

See also: [`set_subparts!`](@ref).
"""
@inline set_subpart!(acs::ACSet, part, name::Symbol, subpart) =
  _set_subpart!(acs, part, Val{name}, subpart)
@inline set_subpart!(acs::ACSet, name::Symbol, subpart) =
  _set_subpart!(acs, Val{name}, subpart)
# Inlined for the same reason as `subpart`.

function _set_subpart!(acs::ACSet, part::AbstractVector{Int},
                       ::Type{Name}, subpart) where Name
  broadcast(part, subpart) do part, subpart
    _set_subpart!(acs, part, Name, subpart)
  end
end

_set_subpart!(acs::ACSet, ::Colon, ::Type{Name}, subpart) where Name =
  _set_subpart!(acs, Name, subpart)
_set_subpart!(acs::ACSet, ::Type{Name}, subpart) where Name =
  _set_subpart!(acs, 1:length(_subpart(acs, Name)), Name, subpart)

@generated function _set_subpart!(acs::ACSet{CD,AD,Ts,Idxed}, part::Int,
    ::Type{Val{name}}, subpart) where {CD,AD,Ts,Idxed,name}
  if name ∈ CD.hom
    ob, codom_ob = dom(CD, name), codom(CD, name)
    if name ∈ Idxed
      quote
        @assert 0 <= subpart <= Tables.rowcount(acs.tables.$codom_ob)
        old = acs.tables.$ob.$name[part]
        acs.tables.$ob.$name[part] = subpart
        if old > 0
          @assert deletesorted!(acs.indices.$name[old], part)
        end
        if subpart > 0
          insertsorted!(acs.indices.$name[subpart], part)
        end
      end
    else
      quote
        @assert 0 <= subpart <= Tables.rowcount(acs.tables.$codom_ob)
        acs.tables.$ob.$name[part] = subpart
      end
    end
  elseif name ∈ AD.attr
    ob = dom(AD, name)
    if name ∈ Idxed
      quote
        if isassigned(acs.tables.$ob.$name, part)
          old = acs.tables.$ob.$name[part]
          unset_data_index!(acs.indices.$name, old, part)
        end
        acs.tables.$ob.$name[part] = subpart
        set_data_index!(acs.indices.$name, subpart, part)
      end
    else
      :(acs.tables.$ob.$name[part] = subpart)
    end
  else
    throw(ArgumentError("$(repr(name)) not in $(CD.hom) or $(AD.attr)"))
  end
end

@inline Base.setindex!(acs::ACSet, value, args...) =
  set_subpart!(acs, args..., value)

""" Mutate subparts of a part in a C-set.

Both single and vectorized assignment are supported.

See also: [`set_subpart!`](@ref).
"""
set_subparts!(acs::ACSet, part; kw...) = set_subparts!(acs, part, (; kw...))

@generated function set_subparts!(acs::ACSet, part,
                                  subparts::NamedTuple{names}) where names
  Expr(:block,
    map(names) do name
      :(_set_subpart!(acs, part, Val{$(QuoteNode(name))}, subparts.$name))
    end...,
    nothing)
end

""" Remove part from a C-set.

The part is removed using the "pop and swap" strategy familiar from
[LightGraphs.jl](https://github.com/JuliaGraphs/LightGraphs.jl), where the
"removed" part is actually replaced by the last part, which is then deleted.
This strategy has important performance benefits since only the last part must
be assigned a new ID, as opposed to assigning new IDs to *every* part following
the removed part.

The removal operation is *not* recursive. When a part is deleted, any superparts
incident to it are retained, but their subparts become undefined (equal to the
integer zero). For example, in a graph, if you call `rem_part!` on a vertex, the
edges incident the `src` and/or `tgt` vertices of the edge become undefined but
the edge itself is not deleted.

Indexing has both positive and negative impacts on performance. On the one hand,
indexing reduces the cost of finding affected superparts from linear time to
constant time. On the other hand, the indices of subparts must be updated when
the part is removed. For example, in a graph, indexing `src` and `tgt` makes
removing vertices faster but removing edges (slightly) slower.

See also: [`rem_parts!`](@ref).
"""
rem_part!(acs::ACSet, type::Symbol, part::Int) =
  _rem_part!(acs, Val{type}, part)

@generated function _rem_part!(acs::ACSet{CD,AD,Ts,Idxed}, ::Type{Val{ob}},
                               part::Int) where {CD,AD,Ts,Idxed,ob}
  in_homs = filter(hom -> codom(CD, hom) == ob, CD.hom)
  indexed_out_homs = filter(hom -> dom(CD, hom) == ob && hom ∈ Idxed, CD.hom)
  indexed_attrs = filter(attr -> dom(AD, attr) == ob && attr ∈ Idxed, AD.attr)
  quote
    last_part = Tables.rowcount(acs.tables.$ob)
    @assert 1 <= part <= last_part
    # Unassign superparts of the part to be removed and also reassign superparts
    # of the last part to this part.
    for hom in $(Tuple(in_homs))
      set_subpart!(acs, incident(acs, part, hom, copy=true), hom, 0)
      set_subpart!(acs, incident(acs, last_part, hom, copy=true), hom, part)
    end
    last_row = getassigned(acs.tables.$ob, last_part)

    # Clear any morphism and data attribute indices for last part.
    for hom in $(Tuple(indexed_out_homs))
      set_subpart!(acs, last_part, hom, 0)
    end
    for attr in $(Tuple(indexed_attrs))
      if haskey(last_row, attr)
        unset_data_index!(acs.indices[attr], last_row[attr], last_part)
      end
    end

    # Finally, delete the last part and update subparts of the removed part.
    resize_table!(acs.tables.$ob, last_part - 1)
    if part < last_part
      set_subparts!(acs, part, last_row)
    end
  end
end

""" Get assigned fields of table row as a named tuple.
"""
function getassigned(table, i)
  cols, names = Tables.columns(table), Tables.columnnames(table)
  assigned = filter(name -> isassigned(cols[name], i), names)
  NamedTuple{assigned}(map(name -> cols[name][i], assigned))
end
getassigned(::AbstractVector{NamedTuple{(),Tuple{}}}, i) = NamedTuple()

""" Remove parts from a C-set.

The parts must be supplied in sorted order, without duplicates.

See also: [`rem_part!`](@ref).
"""
function rem_parts!(acs::ACSet, type::Symbol, parts::AbstractVector{Int})
  issorted(parts) || error("Parts to removed must be in sorted order")
  for part in Iterators.reverse(parts)
    rem_part!(acs, type, part)
  end
end

""" Copy parts from a C-set to a C′-set.

The selected parts must belong to both schemas. All subparts common to the
selected parts, including data attributes, are preserved. Thus, if the selected
parts form a sub-C-set, then the whole sub-C-set is preserved. On the other
hand, if the selected parts do *not* form a sub-C-set, then some copied parts
will have undefined subparts.
"""
@generated function copy_parts!(to::ACSet{CD},
                                from::ACSet{CD′}; kw...) where {CD, CD′}
  obs = intersect(CD.ob, CD′.ob)
  :(copy_parts!(to, from, isempty(kw) ? $(Tuple(obs)) : (; kw...)))
end

copy_parts!(to::ACSet, from::ACSet, obs::Tuple) =
  copy_parts!(to, from, NamedTuple{obs}((:) for ob in obs))
copy_parts!(to::ACSet, from::ACSet, parts::NamedTuple) =
  _copy_parts!(to, from, replace_colons(from, parts))

@generated function _copy_parts!(to::ACSet{CD}, from::ACSet{CD′},
                                 parts::NamedTuple{obs}) where {CD, CD′, obs}
  @assert obs ⊆ intersect(CD.ob, CD′.ob)
  homs = intersect(CD.hom, CD′.hom)
  homs = filter(homs) do hom
    c, c′, d, d′ = dom(CD,hom), dom(CD′,hom), codom(CD,hom), codom(CD′,hom)
    c == c′ && d == d′ && c ∈ obs && d ∈ obs
  end
  hom_triples = [ (hom, dom(CD,hom), codom(CD,hom)) for hom in homs ]
  in_obs = unique!(map(last, hom_triples))
  quote
    newparts = _copy_parts_only!(to, from, parts)
    partmaps = NamedTuple{$(Tuple(in_obs))}(tuple($(map(in_obs) do type
      :(Dict{Int,Int}(zip(parts.$type, newparts.$type)))
    end...)))
    for (name, dom, codom) in $(Tuple(hom_triples))
      for (p, newp) in zip(parts[dom], newparts[dom])
        q = subpart(from, p, name)
        newq = get(partmaps[codom], q, nothing)
        if !isnothing(newq)
          set_subpart!(to, newp, name, newq)
        end
      end
    end
    newparts
  end
end

""" Copy parts from a C-set to a C′-set, ignoring all non-data subparts.

The selected parts must belong to both schemas. Data attributes common to both
schemas are also copied, but no other subparts are copied.

See also: [`copy_parts!`](@ref).
"""
@generated function copy_parts_only!(to::ACSet{CD},
                                     from::ACSet{CD′}; kw...) where {CD, CD′}
  obs = intersect(CD.ob, CD′.ob)
  :(copy_parts_only!(to, from, isempty(kw) ? $(Tuple(obs)) : (; kw...)))
end

copy_parts_only!(to::ACSet, from::ACSet, obs::Tuple) =
  copy_parts_only!(to, from, NamedTuple{obs}((:) for ob in obs))
copy_parts_only!(to::ACSet, from::ACSet, parts::NamedTuple) =
  _copy_parts_only!(to, from, replace_colons(from, parts))

@generated function _copy_parts_only!(to::ACSet{CD,AD}, from::ACSet{CD′,AD′},
    parts::NamedTuple{obs}) where {CD, AD, CD′, AD′, obs}
  @assert obs ⊆ intersect(CD.ob, CD′.ob)
  attrs = intersect(AD.attr, AD′.attr)
  attrs = filter(attrs) do attr
    ob, ob′ = dom(AD, attr), dom(AD′, attr)
    ob == ob′ && ob ∈ obs
  end
  Expr(:block,
    :(newparts = (; $(map(obs) do ob
        Expr(:kw, ob, :(add_parts!(to, $(QuoteNode(ob)), length(parts.$ob))))
        end...))),
    map(attrs) do attr
      ob = dom(AD, attr)
      :(set_subpart!(to, newparts.$ob, $(QuoteNode(attr)),
                     subpart(from, parts.$ob, $(QuoteNode(attr)))))
    end...,
    :newparts)
end

function replace_colons(acs::ACSet, parts::NamedTuple{types}) where {types}
  NamedTuple{types}(map(types, parts) do type, part
    part == (:) ? (1:nparts(acs, type)) : part
  end)
end

function disjoint_union(acs1::T, acs2::T) where {T<:ACSet}
  acs = copy(acs1)
  copy_parts!(acs, acs2)
  acs
end

""" Look up key in C-set data index.
"""
get_data_index(d::AbstractDict{K,Int}, k::K) where K = get(d, k, 0)
get_data_index(d::AbstractDict{K,<:AbstractVector{Int}}, k::K) where K =
  get(d, k, 1:0)

""" Set key and value for C-set data index.
"""
function set_data_index!(d::AbstractDict{K,Int}, k::K, v::Int) where K
  if haskey(d, k)
    error("Key $k already defined in unique index")
  end
  d[k] = v
end
function set_data_index!(d::AbstractDict{K,<:AbstractVector{Int}},
                         k::K, v::Int) where K
  insertsorted!(get!(d, k) do; Int[] end, v)
end

""" Unset key and value from C-set data index, if it is set.
"""
function unset_data_index!(d::AbstractDict{K,Int}, k::K, v::Int) where K
  if haskey(d, k) && d[k] == v
    delete!(d, k)
  end
end
function unset_data_index!(d::AbstractDict{K,<:AbstractVector{Int}},
                           k::K, v::Int) where K
  if haskey(d, k)
    vs = d[k]
    if deletesorted!(vs, v) && isempty(vs)
      delete!(d, k)
    end
  end
end

""" Insert into sorted vector, preserving the sorting.
"""
function insertsorted!(a::AbstractVector, x)
  insert!(a, searchsortedfirst(a, x), x)
end

""" Delete one occurrence of value from sorted vector, if present.

Returns whether an occurence was found and deleted.
"""
function deletesorted!(a::AbstractVector, x)
  i = searchsortedfirst(a, x)
  found = i <= length(a) && a[i] == x
  if found; deleteat!(a, i) end
  found
end

""" More convenient syntax for declaring an ACSet

Example:

```
@acset Graph begin
  V = 2
  E = 2
  src = [1,2]
  tgt = [2,1]
end
```
"""
macro acset(head, body)
  expr = :(init_acset($(esc(head)), $(Expr(:quote, body))))
  Expr(:call, esc(:eval), expr)
end

"""
TODO: Well-formedness check
TODO: Could also rely on a @generated function that took in a "flat" named tuple
TODO: Alternative syntax for @acset input based on CSV
TODO: Actual CSV input
"""
function init_acset(T::Type{<:ACSet{CD,AD,Ts}},body) where {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple}
  body = strip_lines(body)
  @assert body.head == :block
  code = quote
    acs = $(T)()
  end
  for elem in body.args
    lhs, rhs = @match elem begin
      Expr(:(=), lhs, rhs) => (lhs,rhs)
      _ => error("Every line of `@acset` must be an assignment")
    end
    if lhs in CD.ob
      push!(code.args, :(add_parts!(acs, $(Expr(:quote, lhs)), $(rhs))))
    elseif lhs in CD.hom || lhs in AD.attr
      push!(code.args, :(set_subpart!(acs, :, $(Expr(:quote, lhs)), $(rhs))))
    end
  end
  push!(code.args, :(return acs))
  code
end

""" Map over a data type, in the style of Haskell functors
"""

function Base.map(acs::ACSet; kwargs...)
  fns = (;kwargs...)
  _map(acs, fns)
end

function sortunique!(x)
  sort!(x)
  unique!(x)
  x
end

@generated function _map(acs::AT, fns::NamedTuple{map_over}) where
    {map_over,AT<:ACSet}
  CD,AD,Ts,Idxed,UniqIdxed = AT.parameters[1:5]
  map_over = [map_over...]
  attrs = filter(x -> x ∈ AD.attr, map_over)
  data = filter(x -> x ∈ AD.data, map_over)
  abc = attrs_by_codom(AD)
  data_attrs = vcat(map(d -> abc[d], data)...)
  all_attrs = sortunique!(Symbol[attrs; data_attrs])
  affected_data = sortunique!(map(a -> codom(AD,a), all_attrs))
  needed_attrs = sortunique!(vcat(map(d -> abc[d], affected_data)...))
  all_attrs == needed_attrs || error("not enough functions provided to fully transform ACSet")

  fn_applications = map(all_attrs) do a
    qa = Expr(:quote,a)
    if a ∈ attrs
      :($a = (fns[$qa]).(subpart(acs, $qa)))
    else
      d = codom(AD,a)
      :($a = (fns[$(Expr(:quote,d))]).(subpart(acs, $qa)))
    end
  end
  data_types = map(enumerate(AD.data)) do (i,d)
    if d ∈ affected_data
      quote
        T = eltype(fn_vals[$(Expr(:quote, abc[d][1]))])
        $(Expr(:block, (map(abc[d][2:end]) do a
               :(@assert T == eltype(fn_vals[$(Expr(:quote,a))]))
               end)...))
        T
      end
    else
      :($(Ts[i]))
    end
  end
  quote
    fn_vals = $(Expr(:tuple, fn_applications...))
    new_Ts = Tuple{$(data_types...)}
    new_acs = ACSet{$CD,$AD,new_Ts,$Idxed,$UniqIdxed}()
    $(Expr(:block, map(CD.ob) do ob
           :(add_parts!(new_acs,$(Expr(:quote,ob)),nparts(acs,$(Expr(:quote,ob)))))
      end...))
    $(Expr(:block, map(CD.hom) do hom
           :(set_subpart!(new_acs,$(Expr(:quote,hom)),subpart(acs,$(Expr(:quote,hom)))))
      end...))
    $(Expr(:block, map(AD.attr) do attr
        qa = Expr(:quote, attr)
        if attr ∈ all_attrs
           :(set_subpart!(new_acs,$qa,fn_vals[$qa]))
        else
           :(set_subpart!(new_acs,$qa,subpart(acs,$qa)))
        end
      end...))
    return new_acs
  end
end

end


text:
send(url::AbstractString, to::AbstractVector{<:AbstractString},
              from::AbstractString, body::IO, opts::SendOptions = SendOptions()) =
  do_send(String(url), map(String, collect(to)), String(from), opts, ReadData(body))

function do_send(url::String, to::Vector{String}, from::String, options::SendOptions,
                 rd::ReadData)
  ctxt = nothing
  try
    ctxt = ConnContext(url = url, rd = rd, options = options)

    setmail_from!(ctxt, from)
    setmail_rcpt!(ctxt, to)

    connect(ctxt)
    getresponse!(ctxt)

    ctxt.resp
  finally
    cleanup!(ctxt)
  end
end


text:
@testset "AutoFlux" begin

    @testset "ImageClassifier smoketest" begin
        using NaiveGAflux.AutoFlux
        import NaiveGAflux.AutoFlux.ImageClassification: TrainSplitAccuracy, TrainIterConfig, BatchedIterConfig, ShuffleIterConfig, AccuracyVsSize, TrainAccuracyVsSize, EliteAndTournamentSelection, EliteAndSusSelection, GlobalOptimizerMutation, modelname
        using Random

        rng = MersenneTwister(123)
        x = randn(rng, Float32, 32,32,2,4)
        y = Float32.(Flux.onehotbatch(rand(rng, 0:5,4), 0:5))

        dummydir = joinpath(NaiveGAflux.modeldir, "ImageClassifier_smoketest")
        c = ImageClassifier(popsize = 5, seed=1, mdir=dummydir, insize=size(x), outsize=size(y,1))

        f = TrainSplitAccuracy(;split=0.25,
            accuracyconfig=BatchedIterConfig(batchsize=1),
            accuracyfitness=data -> AccuracyVsSize(data, 3;accwrap=EwmaFitness),
            trainconfig=TrainIterConfig(nbatches_per_gen=1, baseconfig=ShuffleIterConfig(batchsize=1)))

        # Logs are mainly to prevent CI timeouts
        @info "\tSmoke test with TrainSplitAccuracy and EliteAndSusSelection"
        pop = @test_logs (:info, "Begin generation 1") (:info, "Begin generation 2") (:info, "Begin generation 3") (:info, r"Mutate model") match_mode=:any fit(c, x, y, fitnesstrategy=f, evolutionstrategy = GlobalOptimizerMutation(EliteAndSusSelection(popsize=c.popsize, nelites=1)), stopcriterion = pop -> generation(pop) == 3)

        @test length(pop) == c.popsize
        @test modelname.(pop) == ["model$i" for i in 1:length(pop)]

        globallearningrate(c::AbstractCandidate) = globallearningrate(c.c)
        globallearningrate(c::CandidateOptModel) = globallearningrate(c.opt)
        globallearningrate(o::Flux.Optimiser) = prod(globallearningrate.(o.os))
        globallearningrate(o) = 1
        globallearningrate(o::ShieldedOpt{Descent}) = o.opt.eta

        @test unique(globallearningrate.(pop)) != [1]
        @test length(unique(globallearningrate.(pop))) == 1

        # Now try TrainAccuracyVsSize and EliteAndTournamentSelection
        f = TrainAccuracyVsSize(;trainconfig= TrainIterConfig(nbatches_per_gen=1, baseconfig=ShuffleIterConfig(batchsize=1)))

        @info "\tSmoke test with TrainAccuracyVsSize and EliteAndTournamentSelection"
        pop = @test_logs (:info, "Begin generation 1") (:info, "Begin generation 2") (:info, "Begin generation 3") (:info, r"Mutate model") match_mode=:any fit(c, x, y, fitnesstrategy=f, evolutionstrategy = GlobalOptimizerMutation(EliteAndTournamentSelection(popsize=c.popsize, nelites=1, k=2)), stopcriterion = pop -> generation(pop) == 3)

        @test length(pop) == c.popsize
        @test modelname.(pop) == ["model$i" for i in 1:length(pop)]

        @test unique(globallearningrate.(pop)) != [1]
        @test length(unique(globallearningrate.(pop))) == 1
    end

    @testset "sizevs" begin
        import NaiveGAflux.AutoFlux.ImageClassification: sizevs
        struct SizeVsTestFitness <: AbstractFitness
            fitness
        end
        NaiveGAflux._fitness(s::SizeVsTestFitness, c) = s.fitness

        struct SizeVsTestFakeModel <: AbstractCandidate
            nparams::Int
        end
        NaiveGAflux.nparams(s::SizeVsTestFakeModel) = s.nparams

        basefitness = SizeVsTestFitness(0.12345678)

        testfitness = sizevs(basefitness, 2)
        @test fitness(testfitness, SizeVsTestFakeModel(1)) == 0.121
        @test fitness(testfitness, SizeVsTestFakeModel(100_000)) ≈ 0.12001

        testfitness = sizevs(basefitness, 3)
        @test fitness(testfitness, SizeVsTestFakeModel(1)) == 0.1231
        @test fitness(testfitness, SizeVsTestFakeModel(100_000)) ≈ 0.12301

        testfitness = sizevs(basefitness, 4)
        @test fitness(testfitness, SizeVsTestFakeModel(1)) == 0.12351
        @test fitness(testfitness, SizeVsTestFakeModel(100_000)) ≈ 0.12351
    end

    @testset "TrainIterConfig" begin
        import NaiveGAflux.AutoFlux.ImageClassification: TrainIterConfig, ShuffleIterConfig, dataiter
        nexamples = 10
        x = randn(Float32, 4,4,3, nexamples)
        y = rand(0:7, nexamples)

        @testset "Test $ne epochs and $nbpg batches per generation" for ne in (1, 2, 10), nbpg in (2, 10)
            bs = 3
            s = TrainIterConfig(nbatches_per_gen=nbpg, baseconfig=ShuffleIterConfig(batchsize=bs, iterwrap=identity))
            itr =  Iterators.take(dataiter(s, x, y), cld(ne * nexamples, nbpg))

            totnrofexamples = ne * nexamples
            @test mapreduce(length ∘ collect, +, itr) == totnrofexamples
            @test length(itr |> first) == min(nbpg, totnrofexamples)
            @test size(itr |> first |> first |> first, 4) == bs

            # All models shall see the same examples
            for iitr in itr
                @test collect(iitr) == collect(iitr)
            end
        end
    end
end


text:
struct AdaptiveParameters{δτ⁻_T, δτ⁺_T, δΔτ⁻_T, δΔτ⁺_T, Nᵣ_T, fineupdate_T} <: AbstractAdaptiveParameters
    δτ⁻::δτ⁻_T
    δτ⁺::δτ⁺_T
    δΔτ⁻::δΔτ⁻_T
    δΔτ⁺::δΔτ⁺_T
    Nᵣ::Nᵣ_T
    fineupdate::fineupdate_T
end
AdaptiveParameters(; δτ⁻=0.5, δτ⁺=1.25, δΔτ⁻=0.25, δΔτ⁺=2.0, Nᵣ=10, fineupdate=true) = AdaptiveParameters(δτ⁻, δτ⁺, δΔτ⁻, δΔτ⁺, Nᵣ, fineupdate)


text:
const program = """
#!/usr/bin/env julia

import CohortIdentification
CohortIdentification.main(ARGS)
"""
const default_dir = joinpath(homedir(), ".local/bin")

function get_user_path()::String
    # User selected path is currently to used

    prompt = "Installation directory (should be in \$PATH): [$default]"

    println(prompt)

    response = readline()

    install_path = if response == ""
        default_dir
    else
        response
    end

    joinpath(install_path, "cohortid")

end

function write_program(install_path::String)

    write(install_path, program)

    chmod(install_path, 0o775)

    println("Installed cohortid to $install_path")
end

function force_xdg_path()

    println("Ensuring $default_dir exists...")
    mkpath(default_dir)

    cohortid_path = joinpath(default_dir, "cohortid")
    write_program(cohortid_path)

end

function force_userprofile()

    cohortid_path = joinpath(ENV["localappdata"], "cohortid")
    write_program(cohortid_path)
end

function install()

    if Sys.iswindows()

        force_userprofile()

    else

        force_xdg_path()
    end
end

install()

max_stars_repo_name: dorbarker/cohort-identification

max_issues_repo_name: dorbarker/cohort-identification

max_forks_repo_name: dorbarker/cohortid