text:
mutable struct Resistor <: Component
    R
end

mutable struct VoltageSource <: Component
    V
end

mutable struct CurrentSource <: Component
    I
end

mutable struct TransConductance <: Component
    G
end

mutable struct Capacitor <: Component
    C
end

"""
mutable struct CircuitComponent{T<: Component}
Circuit Component to be added to a netlist
# Parameters
name::String - Component name on the circuit
component::T - Componenet to be added
connections::Dict{String,Union{Int,Nothing}} - pin(key) connected to node number(value)
symParametes::AbstractVector - Vector of parameters used for stamp creation
"""
mutable struct CircuitComponent{T<: Component}
    name::String
    component::T
    connections::Dict{String,Union{Int,Nothing}}
    symParametes::AbstractVector
end

pins(circomp::CircuitComponent) = collect(keys(circomp.connections))
CircuitComponent(name::String,comp::TransistorModel) = CircuitComponent(name,comp,Dict{String,Union{Int,Nothing}}("d"=>nothing,"g"=>nothing,"s"=>nothing),[])
CircuitComponent(name::String,comp::Resistor) = CircuitComponent(name,comp,Dict{String,Union{Int,Nothing}}("p"=>nothing,"n"=>nothing),[])
CircuitComponent(name::String,comp::VoltageSource) = CircuitComponent(name,comp,Dict{String,Union{Int,Nothing}}("p"=>nothing,"n"=>nothing,"j"=>nothing),[])
CircuitComponent(name::String,comp::CurrentSource) = CircuitComponent(name,comp,Dict{String,Union{Int,Nothing}}("p"=>nothing,"n"=>nothing),[])
CircuitComponent(name::String,comp::TransConductance) = CircuitComponent(name,comp,Dict{String,Union{Int,Nothing}}("op"=>nothing,"om"=>nothing,"ip"=>nothing,"im"=>nothing),[])
CircuitComponent(name::String,comp::Capacitor) = CircuitComponent(name,comp,Dict{String,Union{Int,Nothing}}("p"=>nothing,"n"=>nothing),[])

function MOSFuncCall(Vg,Vd,Vs,name::String)
    return @eval ( $( Symbol(name) ) )($Vg,$Vd,$Vs)
end
@register MOSFuncCall(Vg,Vd,Vs,name::String)

"""
function symSubs(T::CircuitComponent{W},v)
# Parameters
T - Circuit Component
V - Node Voltages and voltage source currents of the circuit
"""
function symSubs(T::CircuitComponent{W},v) where W <: TransistorModel
    Vg = T.connections["g"] == 0 ? 0.0 : v[T.connections["g"]]
    Vd = T.connections["d"] == 0 ? 0.0 : v[T.connections["d"]]
    Vs = T.connections["s"] == 0 ? 0.0 : v[T.connections["s"]]
    #Register Functions for Simulation
    eval(@eval ( $( Symbol("Id_$(T.name)") ) )(Vg,Vd,Vs) = Id(Vg,Vd,Vs,$(T.component)))
    eval(@eval ( $( Symbol("gm_$(T.name)") ) )(Vg,Vd,Vs) = gm(Vg,Vd,Vs,$(T.component)))
    eval(@eval ( $( Symbol("gds_$(T.name)") ) )(Vg,Vd,Vs) = gds(Vg,Vd,Vs,$(T.component)))

    d = Dict([T.symParametes[1]=> MOSFuncCall(Vg,Vd,Vs,"gm_$(T.name)"), T.symParametes[2] => MOSFuncCall(Vg,Vd,Vs,"gm_$(T.name)"),T.symParametes[3] => MOSFuncCall(Vg,Vd,Vs,"Id_$(T.name)")-MOSFuncCall(Vg,Vd,Vs,"gm_$(T.name)")*(Vg-Vs)-MOSFuncCall(Vg,Vd,Vs,"gds_$(T.name)")*(Vd-Vs)])
    # #println(d)
    return d
end

function symSubs(T::CircuitComponent{Capacitor},v)
    return Dict([T.symParametes[1]=> T.component.C])
end

function symSubs(T::CircuitComponent{Resistor},v)
    return Dict([T.symParametes[1]=> T.component.R])
end

function symSubs(T::CircuitComponent{VoltageSource},v)
    return Dict([T.symParametes[1]=> T.component.V])
end

function symSubs(T::CircuitComponent{CurrentSource},v)
    return Dict([T.symParametes[1]=> T.component.I])
end

function symSubs(T::CircuitComponent{TransConductance},v)
    return Dict([T.symParametes[1]=> T.component.G])
end

addSymParameter(comp::CircuitComponent,p::Num) = push!(comp.symParametes,p)

struct Node
    name::String
    position::Int
end

Node(i::Int) = Node("V_n$(i)",i)
pos(n::Node) = n.position

mutable struct Netlist
    components::AbstractVector{CircuitComponent}
    nodes::AbstractVector{Node}
end

nnodes(net::Netlist) = length(net.nodes)

Netlist() = Netlist([],[Node("gnd!",0)])

add_connection(cc::CircuitComponent,pin::String,node::Int) = cc.connections[pin] = node

add_node!(net::Netlist,node::Node) = push!(net.nodes,node)
add_node!(net::Netlist,i::Int) = push!(net.nodes,Node(i))

function addComponent(net::Netlist,comp::CircuitComponent,con::Dict{String,Int})
    for (i,c) in enumerate(con)
        if last(c) ∉ pos.(net.nodes)
            add_node!(net,last(c))
        end
        add_connection(comp,first(c),last(c))
    end
    push!(net.components,comp)
end

function addComponent(net::Netlist,comp::CircuitComponent{VoltageSource},con::Dict{String,Int})
    for (i,c) in enumerate(con)
        if last(c) ∉ pos.(net.nodes)
            add_node!(net,last(c))
        end
        add_connection(comp,first(c),last(c))
    end
    push!(net.components,comp)
    Jnode = nnodes(net)
    add_node!(net,Node("J_$(comp.name)",Jnode))
    add_connection(comp,"j",Jnode)
end


text:
using LinearAlgebra
using Test

using SetProg, SetProg.Sets
using Polyhedra
using MultivariatePolynomials

import DynamicPolynomials
import MultivariateBases
const MB = MultivariateBases
const MonoBasis = MB.MonomialBasis{DynamicPolynomials.Monomial{true}, DynamicPolynomials.MonomialVector{true}}

using JuMP
const MOIT = MOI.Test

function invariant_square_test(optimizer, config::MOIT.Config,
                               inner::Bool, variable::SetProg.AbstractVariable,
                               metric::Function, objective_value, set_test)
    model = _model(optimizer)

    □ = polyhedron(HalfSpace([1, 0], 1.0) ∩ HalfSpace([-1, 0], 1) ∩ HalfSpace([0, 1], 1) ∩ HalfSpace([0, -1], 1), lib)

    @variable(model, ◯, variable)
    if inner
        cref = @constraint(model, ◯ ⊆ □)
    else
        cref = @constraint(model, □ ⊆ ◯)
    end

    A = [0.0 -1.0
         1.0  0.0]

    if variable.symmetric
        @constraint(model, A * ◯ ⊆ ◯)
    else
        @constraint(model, A * ◯ ⊆ ◯, S_procedure_scaling = 1.0)
    end

    @objective(model, inner ? MOI.MAX_SENSE : MOI.MIN_SENSE,
               metric(volume(◯)))

    SetProg.optimize!(model)
    @test JuMP.termination_status(model) == MOI.OPTIMAL
    @test JuMP.objective_sense(model) == MOI.MAX_SENSE
    @test JuMP.objective_value(model) ≈ objective_value atol=config.atol rtol=config.rtol
    return set_test(JuMP.value(◯))
end

function maximal_invariant_ell_homogeneous_test(optimizer, config)
    invariant_square_test(
        optimizer, config, true, Ellipsoid(symmetric=true, dimension=2),
        nth_root, 1.0,
        ◯ -> begin
            @test ◯ isa Sets.Polar{Float64, Sets.Ellipsoid{Float64}}
            @test Sets.polar(◯).Q ≈ Symmetric([1.0 0.0; 0.0 1.0]) atol=config.atol rtol=config.rtol
        end)
end

function maximal_convex_invariant_quad_homogeneous_test(optimizer, config)
    invariant_square_test(
        optimizer, config, true,
        PolySet(degree=2, convex=true, symmetric=true),
        nth_root, 2.0,
        ◯ -> begin
            @test ◯ isa Sets.Polar{Float64, Sets.ConvexPolySet{Float64, MonoBasis, Float64}}
            x, y = Sets.space_variables(◯)
            ◯_polar = Sets.polar(◯)
            @test ◯_polar.p ≈ x^2 + y^2 atol=config.atol rtol=config.rtol
        end)
end

function minimal_invariant_ell_homogeneous_test(optimizer, config)
    invariant_square_test(
        optimizer, config, false, Ellipsoid(symmetric=true, dimension=2),
        nth_root, 0.5,
        ◯ -> begin
            @test ◯ isa Sets.Ellipsoid{Float64}
            @test ◯.Q ≈ Symmetric([0.5 0.0; 0.0 0.5]) atol=config.atol rtol=config.rtol
        end)
end

function minimal_invariant_quad_homogeneous_test(optimizer, config)
    invariant_square_test(
        optimizer, config, false,
        PolySet(degree=2, symmetric=true),
        set -> L1_heuristic(set, ones(2)), 4/3,
        ◯ -> begin
            @test ◯ isa Sets.PolySet{Float64, MonoBasis}
            x, y = Sets.space_variables(◯)
            @test ◯.p ≈ 0.5x^2 + 0.5y^2 atol=config.atol rtol=config.rtol
        end)
end

function minimal_convex_invariant_quad_homogeneous_test(optimizer, config)
    invariant_square_test(
        optimizer, config, false,
        PolySet(degree=2, convex=true, symmetric=true),
        nth_root, 1.0,
        ◯ -> begin
            @test ◯ isa Sets.ConvexPolySet{Float64, MonoBasis}
            x, y = Sets.space_variables(◯)
            @test ◯.p ≈ 0.5x^2 + 0.5y^2 atol=config.atol rtol=config.rtol
        end)
end

const invariant_tests = Dict("maximal_invariant_ell_homogeneous_test"  => maximal_invariant_ell_homogeneous_test,
                             "maximal_convex_invariant_quad_homogeneous_test" => maximal_convex_invariant_quad_homogeneous_test,
                             "minimal_invariant_ell_homogeneous_test"  => minimal_invariant_ell_homogeneous_test,
                             "minimal_invariant_quad_homogeneous_test" => minimal_invariant_quad_homogeneous_test,
                             "minimal_convex_invariant_quad_homogeneous_test" => minimal_convex_invariant_quad_homogeneous_test)

@test_suite invariant

ext: jl


text:
struct PressureDivergence <: AbstractTerm end
struct LinearPressureDivergence <: AbstractTerm end

@inline calc_component!(flux, ::Nothing, _...) = nothing
@inline calc_component!(flux, ::AbstractTerm, _...) = nothing

@inline function calc_component!(flux, ::PressureDivergence, state, aux, physics)
    eos = physics.eos
    parameters = physics.parameters

    p = calc_pressure(eos, state, aux, parameters)

    flux.ρu += p * I

    nothing
end

@inline function calc_component!(flux, ::LinearPressureDivergence, state, aux, physics)
    eos = physics.eos
    parameters = physics.parameters

    p = calc_linear_pressure(eos, state, aux, parameters)

    flux.ρu += p * I

    nothing
end

max_stars_repo_name: CliMA/CouplerMachine

max_issues_repo_name: CliMA/CouplerMachine


text:
export interactive_cobweb

"""
    interactive_cobweb(ds::DiscreteDynamicalSystem, prange, O::Int = 3; kwargs...)
Launch an interactive application for exploring cobweb diagrams of 1D discrete
dynamical systems. Two slides control the length of the plotted trajectory and the
current parameter value. The parameter values are obtained from the given `prange`.

In the cobweb plot, higher order iterates of the dynamic rule `f` are plotted as well,
starting from order 1 all the way to the given order `O`.
Both the trajectory in the cobweb, as well as any iterate `f` can be turned off by
using some of the buttons.

## Keywords
* `Ttr = 0`: transient amount of time to evolve before plotting
* `fkwargs = [(linewidth = 4.0, color = randomcolor()) for i in 1:O]`: plotting keywords
  for each of the plotted iterates of `f`
* `trajcolor = randomcolor()`: color of the trajectory
* `pname = "p"`: name of the parameter slider
* `pindex = 1`: parameter index
* `xmin = 0, xmax = 1`: limits the state of the dynamical system can take
* `Tmax = 1000`: maximum trajectory length
"""
function interactive_cobweb(
    ds, prange, O::Int = 3;
    Ttr = 0,
    fkwargs = [(linewidth = 4.0, color = randomcolor()) for i in 1:O],
    trajcolor = randomcolor(),
    pname = "p",
    xmin = 0.0,
    xmax = 1.0,
    Tmax = 1000,
    pindex = 1,
)

@assert O ≥ 1
xs = range(xmin, xmax; length = 5000)

figure = Figure(resolution = (1000, 800))
axts = figure[1, :] = Axis(figure)
axmap = figure[2, :] = Axis(figure)

slr = labelslider!(figure, "$pname =", prange)
figure[3, :] = slr.layout
r_observable = slr.slider.value

sln = labelslider!(figure, "n =", 1:Tmax; sliderkw = Dict(:startvalue => 20))
figure[4, :] = sln.layout
L = sln.slider.value

# Timeseries plot
function seriespoints(x)
    n = 0:length(x)+1
    c = [Point2f0(n[i], x[i]) for i in 1:length(x)]
end

x = Observable(DynamicalSystems.trajectory(ds, L[]; Ttr))
xn = lift(a -> seriespoints(a), x)
lines!(axts, xn; color = trajcolor, lw = 2.0)
scatter!(axts, xn; color = trajcolor, markersize = 5)
xlims!(axts, 0, 20) # this is better than autolimits
ylims!(axts, xmin, xmax)

# Cobweb diagram
DynamicalSystems.set_parameter!(ds, pindex, prange[1])

fobs = Any[Observable(ds.f.(xs, Ref(ds.p), 0))]
for order in 2:O
    push!(fobs, Observable(ds.f.(fobs[end][], Ref(ds.p), 0)))
end

# plot diagonal and fⁿ
lines!(axmap, [xmin,xmax], [xmin,xmax]; linewidth = 2, color = :black)
fcurves = Any[]
for i in 1:O
    _c = lines!(axmap, xs, fobs[i]; fkwargs[i]...)
    push!(fcurves, _c)
end

function cobweb(t) # transform timeseries x into cobweb (point2D)
    # TODO: can be optimized to become in-place instead of allocating
    c = Point2f0[]
    for i ∈ 1:length(t)-1
        push!(c, Point2f0(t[i], t[i]))
        push!(c, Point2f0(t[i], t[i+1]))
    end
    return c
end

cobs = lift(a -> cobweb(a), x)
ccurve = lines!(axmap, cobs; color = trajcolor)
# cscatter = scatter!(axmap, cobs; color = trajcolor, markersize = 2)

# xlims!(axmap, 0, 1)
# ylims!(axmap, 0, 1)

# On trigger r-slider update all plots:
on(r_observable) do r
    DynamicalSystems.set_parameter!(ds, pindex, r)
    x[] = DynamicalSystems.trajectory(ds, L[]; Ttr)
    fobs[1][] = ds.f.(xs, Ref(ds.p), 0)
    for order in 2:O
        fobs[order][] = ds.f.(fobs[order-1][], Ref(ds.p), 0)
    end
end

on(L) do l
    x[] = DynamicalSystems.trajectory(ds, l; Ttr)
    xlims!(axts, 0, l) # this is better than autolimits
end

# Finally add buttons to hide/show elements of the plot
cbutton = Button(figure; label = "cobweb")
fbuttons = Any[]
for i in 1:O
    _b = Button(figure; label = "f$(superscript(i))")
    push!(fbuttons, _b)
end
figure[5, :] = buttonlayout = GridLayout(tellwidth = false)
buttonlayout[:, 1:O+1] = [cbutton, fbuttons...]

# And add triggering for buttons
on(cbutton.clicks) do click
    ccurve.attributes.visible = !(ccurve.attributes.visible[])
    # cscatter.attributes.visible = !(cscatter.attributes.visible[])
end

for i in 1:O
    on(fbuttons[i].clicks) do click
        fcurves[i].attributes.visible = !(fcurves[i].attributes.visible[])
    end
end

display(figure)
return
end


text:
export FFTOp, \

mutable struct FFTOp{T,F1<:FuncOrNothing,F2<:FuncOrNothing,F3<:FuncOrNothing} <: AbstractLinearOperator{T,F1,F2,F3}
  nrow   :: Int
  ncol   :: Int
  symmetric :: Bool
  hermitian :: Bool
  prod   :: F1 # apply the operator to a vector
  tprod  :: F2 # apply the transpose operator to a vector
  ctprod :: F3 # apply the transpose conjugate operator to a vector
end

"""
  FFTOp(T::Type, shape::Tuple, shift=true)

returns an operator which performs an FFT on Arrays of type T

# Arguments:
* `T::Type`       - type of the array to transform
* `shape::Tuple`  - size of the array to transform
* (`shift=true`)  - if true, fftshifts are performed
"""
function FFTOp(T::Type, shape::Tuple, shift=true)
  plan = plan_fft(zeros(T, shape);flags=FFTW.MEASURE)
  iplan = plan_ifft(zeros(T, shape);flags=FFTW.MEASURE)

  if shift
    return FFTOp{T,Function, Nothing,Function}(prod(shape), prod(shape), false, false
              , x->vec(fftshift(plan*fftshift(reshape(x,shape))))/sqrt(prod(shape))
              , nothing
              , y->vec(ifftshift(iplan*ifftshift(reshape(y,shape)))) * sqrt(prod(shape)) )
  else
    return FFTOp{T,Function, Nothing,Function}(prod(shape), prod(shape), false, false
            , x->vec(plan*(reshape(x,shape)))/sqrt(prod(shape))
            , nothing
            , y->vec(iplan*(reshape(y,shape))) * sqrt(prod(shape)) )
  end
end

\(A::FFTOp, x::Vector) = adjoint(A) * x


text:
import GitLab
using Base.Test

myauth = GitLab.authenticate(ENV["GITLAB_AUTH"]) # don't hardcode your access tokens!
println("Authentication successful")
options = Dict("private_token" => myauth.token)

myrepo = GitLab.repo_by_name("TestProject1"; headers=options)

## Create new owner:
user_info = Dict{AbstractString, Any}()
user_info["name"] = "Test User"
user_info["username"] = "testuser"
own = GitLab.Owner(user_info)
## @show own

name = GitLab.namefield(own)
@test get(name) == user_info["username"]

setindex!(options, "mdpradeep", "username")

new_owner = GitLab.owner("mdpradeep", false; headers = options)
## @show new_owner

users, page_data = GitLab.users(; params = options)
## @show users, page_data
@test GitLab.name(first(users)) == "mdpradeep"

orgs, page_data = GitLab.orgs(own; params=options)
@test GitLab.name(last(orgs)) == "TestProject1"
## @show orgs, page_data

#=
followers, page_data  = GitLab.followers(own; params=options)
@show followers, page_data

following, page_data  = GitLab.following(own; params=options)
@show following, page_data
=#

repos, page_data  = GitLab.repos(own; params=options)
@test GitLab.name(last(repos)) == "TestProject1"
## @show repos, page_data

println("Owner Tests Done !!!")


text:
"""
Module Keyboard

Provides functions for controlling the keyboard by voice.

# Functions

###### Typing
- [`type`](@ref)

###### Special keys
- [`page`](@ref)

To see a description of a function type `?<functionname>`.

See also: [`Mouse`](@ref)
"""
module Keyboard

using PyCall
import ..JustSayIt: @voiceargs, pyimport_pip, controller, set_controller, PyKey, TYPE_MODEL_NAME, ALPHABET_ENGLISH, DIGITS_ENGLISH, tic, toc, is_next, are_next, all_consumed, was_partial_recognition, InsecureRecognitionException, reset_all, do_delayed_resets

## PYTHON MODULES

const Pynput = PyNULL()

function __init__()
    ENV["PYTHON"] = ""                                              # Force PyCall to use Conda.jl
    copy!(Pynput, pyimport_pip("pynput"))
    set_controller("keyboard", Pynput.keyboard.Controller())
end

## CONSTANTS

const TYPE_MEMORY_ALLOC_GRANULARITY = 32
const TYPE_KEYWORDS_ENGLISH = Dict("undo"=>"undo", "redo"=>"redo", "uppercase"=>"uppercase", "lowercase"=>"lowercase", "letters"=>"letters", "digits"=>"digits", "point"=>"point", "comma"=>"comma", "colon"=>"colon", "semicolon"=>"semicolon", "exclamation"=>"exclamation", "interrogation"=>"interrogation", "paragraph" => "paragraph")
const TYPE_END_KEYWORD_ENGLISH = "terminus"

## FUNCTIONS

@doc """
    type `text` | `words` | `letters` | `digits`

Type in one of the following modes:
- `text`
- `words`
- `letters`
- `digits`

Each of the modes supports a set of keywords which can trigger some immediate action or modify the handling of subsequent speech input. It is important to note that the speech is analysed in word groups which are naturally delimited by longer silences; and keywords are only considered as such if their word group does not contain anything else then keywords. This allows to determine whether a word that is recognised as a keyword should trigger some action or be typed instead.

# Keywords
- "terminus": end typing.
- "undo": undo typing of last word group.
- "redo": redo typing of last word group.
- "uppercase": type the first word of the next word group uppercase (automatic in `text` mode after '.', '!' and '?').
- "lowercase": type the first word of the next word group lowercase (default).
- "letters": interpret the next word group as letters (interprets in addition 'space' as ' ').
- "digits": interpret the next word group as digits (interprets in addition 'dot' as '.' and 'space' as ' ').
- "point": type '.'.
- "comma": type ','.
- "colon": type ':'.
- "semicolon": type ';'.
- "exclamation": type '!'.
- "interrogation": type '?'.
- "paragraph": start a new paragraph.

# Modes

#### `text`
Type any kind of text supported by the keywords, including spelled letters, digits and punctuation marks. All keywords are supported.

#### `words`
Type words only (upper and lower case). Supported keywords are:
- "terminus"
- "undo"
- "redo"
- "uppercase"
- "lowercase"

#### `letters`
Type letters only. Supported keywords are:
- "terminus"
- "undo"
- "redo"
- "space"

#### `digits`
Type digits only. Supported keywords are:
- "terminus"
- "undo"
- "redo"
- "space"
- "dot"
"""
type
@enum TokenGroupKind undefined_kind keyword_kind word_kind letter_kind digit_kind punctuation_kind space_kind
@enum TypeMode text words letters digits
@voiceargs (mode=>(valid_input_auto=true)) function type(mode::TypeMode; end_keyword::String=TYPE_END_KEYWORD_ENGLISH, do_keystrokes::Bool=true)
    @info "Typing $(string(mode))..."
    type_memory      = Vector{String}()
    tokengroup_str   = ""
    tokengroup_kind  = undefined_kind
    is_typing        = true
    was_keyword      = false
    is_new_group     = false
    is_uppercase     = (mode == text) ? true : false
    was_space        = true # We want the first token to be dealt with as if there had been a space character before (new paragraph).
    punctuation      = Vector{String}()
    spaces           = Vector{String}()
    undo_count       = 0
    ig               = 0  # group index
    it               = 0  # token index
    nb_keyword_chars = 0
    if     (mode == text)    type_keywords = [end_keyword, values(TYPE_KEYWORDS_ENGLISH)...]
    elseif (mode == words)   type_keywords = [end_keyword, TYPE_KEYWORDS_ENGLISH["undo"], TYPE_KEYWORDS_ENGLISH["redo"], TYPE_KEYWORDS_ENGLISH["uppercase"], TYPE_KEYWORDS_ENGLISH["lowercase"]]
    elseif (mode == letters) type_keywords = [end_keyword, TYPE_KEYWORDS_ENGLISH["undo"], TYPE_KEYWORDS_ENGLISH["redo"], TYPE_KEYWORDS_ENGLISH["letters"]]
    elseif (mode == digits)  type_keywords = [end_keyword, TYPE_KEYWORDS_ENGLISH["undo"], TYPE_KEYWORDS_ENGLISH["redo"], TYPE_KEYWORDS_ENGLISH["digits"]]
    end
    while is_typing
        is_new_group = (all_consumed() && !was_partial_recognition())
        if ig == 0
            ig = 1
        elseif is_new_group && !was_keyword
            if ig > length(type_memory)
                resize!(type_memory, length(type_memory) + TYPE_MEMORY_ALLOC_GRANULARITY)
            end
            type_memory[ig] = tokengroup_str
            tokengroup_str = ""
            ig += 1
            it = 0
        end
        if is_new_group && (tokengroup_kind == undefined_kind)
            reset_all(; hard=true, exclude_active=true)      # NOTE: a reset is required to avoid that the dynamic recognizers generated in is_next and are_next include the previous recognition as desired for normal command recognition.
            if is_next(type_keywords; use_max_speed=true, ignore_unknown=false)
                keywords = String[]
                try
                    are_keywords, keywords = are_next(type_keywords; consume_if_match=true, ignore_unknown=false)
                catch e
                    if isa(e, InsecureRecognitionException)
                        are_keywords = false
                    else
                        rethrow(e)
                    end
                end
            else
                are_keywords = false
            end
            if are_keywords
                tokengroup_kind = keyword_kind
                reset_all(; hard=true)         # Force hard resets for performance reasons.
            else
                do_delayed_resets(; hard=true) # Force delayed resets to be done hard for performance reasons (audio must imperatively not be reset if not consumed!)
            end
        end
        if tokengroup_kind == undefined_kind
            if     (mode == text)    tokengroup_kind = word_kind
            elseif (mode == words)   tokengroup_kind = word_kind
            elseif (mode == letters) tokengroup_kind = letter_kind
            elseif (mode == digits)  tokengroup_kind = digit_kind
            end
        end
        if tokengroup_kind == keyword_kind
            @debug "Keywords found:" keywords
            tokengroup_kind = undefined_kind
            keyword_sign = ""
            for i = 1:length(keywords)
                keyword = keywords[i]
                if tokengroup_kind in (letter_kind, digit_kind)
                    @info "ABORT of keyword interpretation: keyword '$(keywords[i-1])' was followed by another keyword ('$keyword'). However, keyword '$(keywords[i-1])' defines the kind of the next word group and, therefore, no other keyword can follow it."
                    tokengroup_kind = undefined_kind
                    break
                end
                if keyword == end_keyword
                    is_typing = false
                elseif keyword == TYPE_KEYWORDS_ENGLISH["undo"]
                    if ig > 1
                        ig -= 1
                        @info "Undo typing of last word group..."
                        type_backspace(;count=length(type_memory[ig]) + nb_keyword_chars, do_keystrokes=do_keystrokes)
                        nb_keyword_chars = 0
                        undo_count += 1
                        if (ig == 1) || (type_memory[ig-1] in (".", "!", "?")) is_uppercase = true
                        else                                                   is_uppercase = false
                        end
                        if (ig == 1) || (type_memory[ig-1] in ("\n",)) was_space = true
                        else                                           was_space = false
                        end
                    else
                        @info "Nothing to undo."
                    end
                elseif keyword == TYPE_KEYWORDS_ENGLISH["redo"]
                    if undo_count > 0 && ig <= length(type_memory)
                        @info "Redo typing of last word group..."
                        if nb_keyword_chars > 0
                            type_backspace(;count=nb_keyword_chars, do_keystrokes=do_keystrokes)
                            nb_keyword_chars = 0
                            is_uppercase = false
                            was_space = false
                        end
                        type_string(type_memory[ig]; do_keystrokes=do_keystrokes)
                        undo_count -= 1
                        ig += 1
                        if (type_memory[ig-1] in (".", "!", "?")) is_uppercase = true
                        else                                      is_uppercase = false
                        end
                        if (type_memory[ig-1] in ("\n",)) was_space = true
                        else                              was_space = false
                        end
                    else
                        @info "Nothing to redo."
                    end
                elseif keyword == TYPE_KEYWORDS_ENGLISH["uppercase"]
                    is_uppercase = true
                    keyword_sign = "[$(TYPE_KEYWORDS_ENGLISH["uppercase"])]"
                elseif keyword == TYPE_KEYWORDS_ENGLISH["lowercase"]
                    is_uppercase = false
                    keyword_sign = "[$(TYPE_KEYWORDS_ENGLISH["lowercase"])]"
                elseif keyword == TYPE_KEYWORDS_ENGLISH["letters"]
                    tokengroup_kind = letter_kind
                elseif keyword == TYPE_KEYWORDS_ENGLISH["digits"]
                    tokengroup_kind = digit_kind
                elseif keyword == TYPE_KEYWORDS_ENGLISH["point"]
                    push!(punctuation, ".")
                    tokengroup_kind = punctuation_kind
                    is_uppercase = true
                elseif keyword == TYPE_KEYWORDS_ENGLISH["comma"]
                    push!(punctuation, ",")
                    tokengroup_kind = punctuation_kind
                elseif keyword == TYPE_KEYWORDS_ENGLISH["colon"]
                    push!(punctuation, ":")
                    tokengroup_kind = punctuation_kind
                elseif keyword == TYPE_KEYWORDS_ENGLISH["semicolon"]
                    push!(punctuation, ";")
                    tokengroup_kind = punctuation_kind
                elseif keyword == TYPE_KEYWORDS_ENGLISH["exclamation"]
                    push!(punctuation, "!")
                    tokengroup_kind = punctuation_kind
                    is_uppercase = true
                elseif keyword == TYPE_KEYWORDS_ENGLISH["interrogation"]
                    push!(punctuation, "?")
                    tokengroup_kind = punctuation_kind
                    is_uppercase = true
                elseif keyword == TYPE_KEYWORDS_ENGLISH["paragraph"]
                    push!(spaces, "\n")
                    tokengroup_kind = space_kind
                    was_space = true
                else
                    @info "Unkown keyword." #NOTE: this should never occur as the are_next should only match known keywords.
                end
            end
            was_keyword = true
            if     (tokengroup_kind == letter_kind) keyword_sign = keyword_sign * "[$(TYPE_KEYWORDS_ENGLISH["letters"])]"
            elseif (tokengroup_kind == digit_kind)  keyword_sign = keyword_sign * "[$(TYPE_KEYWORDS_ENGLISH["digits"])]"
            end
            if keyword_sign != ""
                type_string(keyword_sign; do_keystrokes=do_keystrokes)
                nb_keyword_chars += length(keyword_sign)
            end
        else
            if     (tokengroup_kind == word_kind)   token = next_word()
            elseif (tokengroup_kind == letter_kind) token = next_letter()
            elseif (tokengroup_kind == digit_kind)  token = next_digit()
            end
            if nb_keyword_chars > 0
                type_backspace(;count=nb_keyword_chars, do_keystrokes=do_keystrokes) # NOTE: the removal of keyword signs must be done after the call to obtain the next token, in order to have it visible until it is spoken.
                nb_keyword_chars = 0
            end
            if (tokengroup_kind == word_kind)
                if (is_uppercase || startswith(token, "i'") || (token == "i")) token = uppercasefirst(token) end
                if (was_space) token_str = token
                else           token_str = " " * token
                end
            elseif (tokengroup_kind == letter_kind)
                if (is_uppercase) token = uppercase(token) end
                token_str = token
            elseif (tokengroup_kind == digit_kind)
                token_str = token
            elseif (tokengroup_kind == punctuation_kind)
                token_str = join(punctuation, "")
                punctuation = Vector{String}()
            elseif (tokengroup_kind == space_kind)
                token_str = join(spaces, "")
                spaces = Vector{String}()
            elseif (tokengroup_kind == word_kind)
                @info "Unkown token group." #NOTE: this should never occur.
            end
            type_string(token_str; do_keystrokes=do_keystrokes)
            tokengroup_str *= token_str
            it += 1
            undo_count = 0
            was_keyword = false
            if !(tokengroup_kind in (punctuation_kind, space_kind)) is_uppercase = false end
            if (tokengroup_kind != space_kind) was_space = false end
            if all_consumed() tokengroup_kind = undefined_kind end
            sleep(0.05)
        end
    end
    @info "...stopped typing $(string(mode))."
    return join(type_memory[1:ig-1])
end

interpret_letters(input::AbstractString) = (return ALPHABET_ENGLISH[input])
interpret_digits(input::AbstractString)  = (return DIGITS_ENGLISH[input])

@doc "Get next word from speech."
next_word
@voiceargs word=>(model=TYPE_MODEL_NAME, ignore_unknown=true) next_word(word::String) = (return word)

@doc "Get next letter from speech."
next_letter
@voiceargs letter=>(valid_input=[keys(ALPHABET_ENGLISH)...], interpret_function=interpret_letters, ignore_unknown=true) next_letter(letter::String) = (return letter)

@doc "Get next digit from speech."
next_digit
@voiceargs digit=>(valid_input=[keys(DIGITS_ENGLISH)...], interpret_function=interpret_digits, ignore_unknown=true) next_digit(digit::String) = (return digit)

function type_string(str::String; do_keystrokes::Bool=true)
    if do_keystrokes
        keyboard  = controller("keyboard")
        keyboard.type(str)
    end
end

function type_backspace(; count::Integer=1, do_keystrokes::Bool=true)
    if do_keystrokes
        keyboard  = controller("keyboard")
        backspace = Pynput.keyboard.Key.backspace
        for i = 1:count
            keyboard.press(backspace); keyboard.release(backspace)
        end
    end
end

function press_keys(keys::PyKey...)
    keyboard  = controller("keyboard")
    keys = map(convert_key, keys)
    @pywith keyboard.pressed(keys[1:end-1]...) begin
        keyboard.press(keys[end])
        keyboard.release(keys[end])
    end
end

convert_key(key::Char)     = string(key)
convert_key(key::PyObject) = key

end # module Keyboard


text:
Base.lock(h::Hist2D) = lock(h.hlock)
Base.unlock(h::Hist2D) = unlock(h.hlock)

"""
    bincounts(h::Hist2D)

Get the bin counts of a histogram.
"""
@inline bincounts(h::Hist2D) = h.hist.weights

"""
    binedges(h::Hist2D)

Get a 2-tuple of the bin edges of a histogram.
"""
@inline binedges(h::Hist2D) = h.hist.edges

"""
    bincenters(h::Hist2D)

Get a 2-tuple of the bin centers of a histogram.
"""
function bincenters(h::Hist2D)
    StatsBase.midpoints.(binedges(h))
end

"""
    binerrors(f::T, h::Hist2D) where T<:Function = f.(h.sumw2)
    binerrors(h::Hist2D) = binerrors(sqrt, h)

Calculate the bin errors from `sumw2` with a Gaussian default.
"""
binerrors(f::T, h::Hist2D) where T<:Function = f.(h.sumw2)
binerrors(h::Hist2D) = binerrors(sqrt, h)

"""
    nbins(h::Hist2D)

Get a 2-tuple of the number of x and y bins of a histogram.
"""
function nbins(h::Hist2D)
    size(bincounts(h))
end

"""
    integral(h::Hist2D)

Get the integral a histogram.
"""
function integral(h::Hist2D)
    sum(bincounts(h))
end

"""
    empty!(h::Hist2D)

Resets a histogram's bin counts and `sumw2`.
"""
function Base.empty!(h::Hist2D{T,E}) where {T,E}
    h.hist.weights .= zero(T)
    h.sumw2 .= 0.0
    return h
end

"""
    push!(h::Hist2D, valx::Real, valy::Real, wgt::Real=1)
    atomic_push!(h::Hist2D, valx::Real, valy::Real, wgt::Real=1)

Adding one value at a time into histogram.
`sumw2` (sum of weights^2) accumulates `wgt^2` with a default weight of 1.
`atomic_push!` is a slower version of `push!` that is thread-safe.

"""
@inline function atomic_push!(h::Hist2D{T,E}, valx::Real, valy::Real, wgt::Real=1) where {T,E}
    lock(h)
    push!(h, valx, valy, wgt)
    unlock(h)
    return nothing
end

@inline function Base.push!(h::Hist2D{T,E}, valx::Real, valy::Real, wgt::Real=1) where {T,E}
    rx, ry = binedges(h)
    Lx, Ly = nbins(h)
    binidxx = _edge_binindex(rx, valx)
    binidxy = _edge_binindex(ry, valy)
    if h.overflow
        binidxx = clamp(binidxx, 1, Lx)
        binidxy = clamp(binidxy, 1, Ly)
        @inbounds h.hist.weights[binidxx,binidxy] += wgt
        @inbounds h.sumw2[binidxx,binidxy] += wgt^2
    else
        if (unsigned(binidxx - 1) < Lx) && (unsigned(binidxy - 1) < Ly)
            @inbounds h.hist.weights[binidxx,binidxy] += wgt
            @inbounds h.sumw2[binidxx,binidxy] += wgt^2
        end
    end
    return nothing
end

Base.broadcastable(h::Hist2D) = Ref(h)

"""
    Hist2D(elT::Type{T}=Float64; binedges, overflow) where {T}

Initialize an empty histogram with bin content typed as `T` and bin edges.
To be used with [`push!`](@ref). Default overflow behavior (`false`)
will exclude values that are outside of `binedges`.
"""
function Hist2D(elT::Type{T}=Float64; bins, overflow=_default_overflow) where {T}
    counts = zeros(elT, length.(bins) .- 1)
    return Hist2D(Histogram(bins, counts); overflow=overflow)
end

"""
    Hist2D(tuple, edges::Tuple{AbstractRange,AbstractRange}; overflow)
    Hist2D(tuple, edges::Tuple{AbstractVector,AbstractVector}; overflow)

Create a `Hist2D` with given bin `edges` and values from
a 2-tuple of arrays of x, y values. Weight for each value is assumed to be 1.
"""
function Hist2D(A::Tuple{AbstractVector,AbstractVector}, r::Tuple{AbstractRange,AbstractRange}; overflow=_default_overflow)
    h = Hist2D(Int; bins=r, overflow=overflow)
    push!.(h, A[1], A[2])
    return h
end
function Hist2D(A::Tuple{AbstractVector,AbstractVector}, edges::Tuple{AbstractVector,AbstractVector}; overflow=_default_overflow)
    if all(_is_uniform_bins.(edges))
        r = (range(first(edges[1]), last(edges[1]), length=length(edges[1])),
             range(first(edges[2]), last(edges[2]), length=length(edges[2])))
        return Hist2D(A, r; overflow=overflow)
    else
        h = Hist2D(Int; bins=edges, overflow=overflow)
        push!.(h, A[1], A[2])
        return h
    end
end

"""
    Hist2D(tuple, wgts::AbstractWeights, edges::Tuple{AbstractRange,AbstractRange}; overflow)
    Hist2D(tuple, wgts::AbstractWeights, edges::Tuple{AbstractVector,AbstractVector}; overflow)

Create a `Hist2D` with given bin `edges` and values from
a 2-tuple of arrays of x, y values.
`wgts` should have the same `size` as elements of `tuple`.
"""
function Hist2D(A::Tuple{AbstractVector,AbstractVector}, wgts::AbstractWeights, r::Tuple{AbstractRange,AbstractRange}; overflow=_default_overflow)
    @boundscheck @assert size(A[1]) == size(A[2]) == size(wgts)
    h = Hist2D(eltype(wgts); bins=r, overflow=overflow)
    push!.(h, A[1], A[2], wgts)
    return h
end
function Hist2D(A::Tuple{AbstractVector,AbstractVector}, wgts::AbstractWeights, edges::Tuple{AbstractVector,AbstractVector}; overflow=_default_overflow)
    if all(_is_uniform_bins.(edges))
        r = (range(first(edges[1]), last(edges[1]), length=length(edges[1])),
             range(first(edges[2]), last(edges[2]), length=length(edges[2])))
        return Hist2D(A, wgts, r; overflow=overflow)
    else
        h = Hist2D(Int; bins=edges, overflow=overflow)
        push!.(h, A[1], A[2], wgts)
        return h
    end
end

"""
    Hist2D(A::AbstractVector{T}; nbins::Tuple{Integer,Integer}, overflow) where T
    Hist2D(A::AbstractVector{T}, wgts::AbstractWeights; nbins::Tuple{Integer,Integer}, overflow) where T

Automatically determine number of bins based on `Sturges` algo.
"""
function Hist2D(A::Tuple{AbstractVector{T},AbstractVector{T}};
        nbins::Tuple{Integer,Integer}=_sturges.(A),
        overflow=_default_overflow,
    ) where {T}
    F = float(T)
    nbinsx, nbinsy = nbins
    lox, hix = minimum(A[1]), maximum(A[1])
    loy, hiy = minimum(A[2]), maximum(A[2])
    rx = StatsBase.histrange(F(lox), F(hix), nbinsx)
    ry = StatsBase.histrange(F(loy), F(hiy), nbinsy)
    r = (rx, ry)
    return Hist2D(A, r; overflow=overflow)
end

function Hist2D(A::Tuple{AbstractVector{T},AbstractVector{T}}, wgts::AbstractWeights;
        nbins::Tuple{Integer,Integer}=_sturges.(A),
        overflow=_default_overflow,
    ) where {T}
    F = float(T)
    nbinsx, nbinsy = nbins
    lox, hix = minimum(A[1]), maximum(A[1])
    loy, hiy = minimum(A[2]), maximum(A[2])
    rx = StatsBase.histrange(F(lox), F(hix), nbinsx)
    ry = StatsBase.histrange(F(loy), F(hiy), nbinsy)
    r = (rx, ry)
    return Hist2D(A, wgts, r; overflow=overflow)
end

"""
    function lookup(h::Hist2D, x, y)

For given x-axis and y-axis value `x`, `y`, find the corresponding bin and return the bin content.
If a value is out of the histogram range, return `missing`.
"""
function lookup(h::Hist2D, x, y)
    rx, ry = binedges(h)
    !(first(rx) <= x <= last(rx)) && return missing
    !(first(ry) <= y <= last(ry)) && return missing
    return bincounts(h)[_edge_binindex(rx, x), _edge_binindex(ry, y)]
end

"""
    normalize(h::Hist2D)

Create a normalized histogram via division by `integral(h)`.
"""
function normalize(h::Hist2D)
    return h*(1/integral(h))
end

"""
    rebin(h::Hist2D, nx::Int=1, ny::Int=nx)
    rebin(nx::Int, ny::Int) = h::Hist2D -> rebin(h, nx, ny)

Merges `nx` (`ny`) consecutive bins into one along the x (y) axis by summing.
"""
function rebin(h::Hist2D, nx::Int=1, ny::Int=nx)
    sx, sy = nbins(h)
    @assert sx % nx == sy % ny == 0
    p1d = (x,n)->Iterators.partition(x, n)
    p2d = x->(x[i:i+(nx-1),j:j+(ny-1)] for i=1:nx:sx, j=1:ny:sy)
    counts = sum.(p2d(bincounts(h)))
    sumw2 = sum.(p2d(h.sumw2))
    ex = first.(p1d(binedges(h)[1], nx))
    ey = first.(p1d(binedges(h)[2], ny))
    _is_uniform_bins(ex) && (ex = range(first(ex), last(ex), length=length(ex)))
    _is_uniform_bins(ey) && (ey = range(first(ey), last(ey), length=length(ey)))
    return Hist2D(Histogram((ex,ey), counts), sumw2; overflow=h.overflow)
end
rebin(nx::Int, ny::Int) = h::Hist2D -> rebin(h, nx, ny)

"""
    project(h::Hist2D, axis::Symbol=:x)
    project(axis::Symbol=:x) = h::Hist2D -> project(h, axis)

Computes the `:x` (`:y`) axis projection of the 2D histogram by
summing over the y (x) axis. Returns a `Hist1D`.
"""
function project(h::Hist2D, axis::Symbol=:x)
    @assert axis ∈ (:x, :y)
    dim = axis == :x ? 2 : 1
    ex, ey = binedges(h)
    counts = [sum(bincounts(h), dims=dim)...]
    sumw2 = [sum(h.sumw2, dims=dim)...]
    edges = axis == :x ? ex : ey
    return Hist1D(Histogram(edges, counts), sumw2; overflow=h.overflow)
end
project(axis::Symbol=:x) = h::Hist2D -> project(h, axis)

"""
    transpose(h::Hist2D)

Reverses the x and y axes.
"""
function transpose(h::Hist2D)
    edges = reverse(binedges(h))
    counts = collect(bincounts(h)')
    sumw2 = collect(h.sumw2')
    return Hist2D(Histogram(edges, counts), sumw2; overflow=h.overflow)
end

"""
    profile(h::Hist2D, axis::Symbol=:x)
    profile(axis::Symbol=:x) = h::Hist2D -> profile(h, axis)

Returns the `axis`-profile of the 2D histogram by
calculating the weighted mean over the other axis.
`profile(h, :x)` will return a `Hist1D` with the y-axis edges of `h`.
"""
function profile(h::Hist2D, axis::Symbol=:x)
    @assert axis ∈ (:x, :y)
    if axis == :y
        h = transpose(h)
    end

    edges = binedges(h)[1]
    centers = bincenters(h)[2]
    counts = bincounts(h)
    sumw2 = h.sumw2

    num = counts*centers
    den = sum(counts, dims=2)
    numerr2 = sumw2 * centers.^2
    denerr2 = sum(sumw2, dims=2)
    val = vec(num ./ den)
    sw2 = vec(@. numerr2/den^2 - denerr2*(num/den^2)^2)

    # ROOT sets the NaN entries and their error to 0
    val[isnan.(val)] .= zero(eltype(val))
    sw2[isnan.(sw2)] .= zero(eltype(sw2))

    return Hist1D(Histogram(edges, val), sw2; overflow=h.overflow)
end
profile(axis::Symbol=:x) = h::Hist2D -> profile(h, axis)

function _svg(h::Hist2D)
    paddingx1, paddingy1 = 0.15, 0.15 # left, bottom
    paddingx2, paddingy2 = 0.05, 0.05 # right, top
    framewidth, frameheight = 250, 200

    (xlow, xhigh), (ylow, yhigh) = extrema.(binedges(h))
    function transform(x::Real, y::Real)
        xfrac = (x - xlow)/(xhigh - xlow)
        xdraw = xfrac * framewidth*(1-paddingx1-paddingx2) + framewidth*paddingx1
        yfrac = (y - ylow)/(yhigh - ylow)
        ydraw = frameheight - (yfrac * frameheight*(1-paddingy1-paddingy2) + frameheight*paddingy1)
        return xdraw, ydraw
    end

    function colorscale(x::Real)
        x = isnan(x) ? 0. : x
        # x is fraction between [0,1]
        # 6th deg polynomial fit to viridis color palette
        # See https://gist.github.com/aminnj/dba84777718613d3a37291a43659feff
        # to generate other color palettes. Black and white is trivial:
        #   return 255 .* (1-x,1-x,1-x)
        r = @evalpoly(x, 0.2731, 0.1270, -0.3617, -4.7456, 6.7092, 4.2491, -5.2678)
        g = @evalpoly(x, 0.0039, 1.3810, 0.3969, -6.4246, 15.3241, -14.7345, 4.9587)
        b = @evalpoly(x, 0.3305, 1.3726, 0.3948, -20.7431, 59.7287, -68.3565, 27.4051)
        return round.(Int, 255 .* (r,g,b))
    end

    counts = bincounts(h)
    mincount, maxcount = extrema(counts)
    counts = clamp.(counts, 0, maxcount)
    ex, ey = binedges(h)
    sx, sy = size(counts)
    rectlines = String[]
    textstyle = """ dominant-baseline="middle" text-anchor="middle" font-size="85%" font-family="sans-serif" """
    for i in 1:sx, j in 1:sy
        tx1, ty1 = ceil.(Int, transform(ex[i], ey[j+1]))
        tx2, ty2 = ceil.(Int, transform(ex[i+1], ey[j]))
        tw, th = tx2-tx1, ty2-ty1
        c = counts[i,j]
        (c == 0) && continue
        r,g,b = colorscale((c-mincount)/(maxcount-mincount))
        rcolor = "rgb($(r),$(g),$(b))"
        line = """<rect x="$(tx1)" y="$(ty1)" width="$(tw)" height="$(th)" fill="$(rcolor)" stroke="none" />"""
        if (sx <= 15) && (sy <= 15)
            tcolor = ((0.299*r + 0.587*g + 0.114*b) < 0.60*255) ? "#fff" : "#000"
            line = """<g>$(line)<text class="svgplotlabels" x="$(tx1+tw/2)" y="$(ty1+th/2)" fill="$(tcolor)" $(textstyle)>$(c)</text></g>"""
        end
        push!(rectlines, line * "\n")
    end

    return """
    <svg width="$(framewidth)" height="$(frameheight)" version="1.1" xmlns="http://www.w3.org/2000/svg" class="svgplot">

        $(join(rectlines))
        <rect x="$(round(Int,framewidth*paddingx1))" y="$(round(Int,frameheight*paddingy2))" width="$(round(Int,framewidth*(1-paddingx1-paddingx2)))" height="$(frameheight*(1-paddingy1-paddingy2))" fill="none" stroke="#000" stroke-width="1" />
        <text x="$(framewidth*paddingx1)" y="$(frameheight*(1-0.5*paddingy1))" fill="black" $(textstyle)>$(minimum(ex))</text>
        <text x="$(framewidth*(1-paddingx2))" y="$(frameheight*(1-0.5*paddingy1))" fill="black" $(textstyle)>$(maximum(ex))</text>
        <text x="$(framewidth*0.5*paddingx1)" y="$(frameheight*(1-paddingy1))" fill="black" $(textstyle)>$(minimum(ey))</text>
        <text x="$(framewidth*0.5*paddingx1)" y="$(frameheight*paddingy2)" fill="black" $(textstyle)>$(maximum(ey))</text>
    </svg>
    """
end

function Base.show(io::IO, h::Hist2D)
    compact = get(io, :compact, false)
    if compact
        print(io, "Hist2D{$(eltype(bincounts(h)))}, ")
        print(io, "edges=$(repr(binedges(h), context=:limit => true)), ")
        print(io, "integral=$(integral(h))")
    else
        ex, ey = binedges(h)
        nx, ny = nbins(h)
        xfact = nx > 1 ? (maximum(ex)-minimum(ex))/(nx-1) : 0.0
        yfact = ny > 1 ? (maximum(ey)-minimum(ey))/(ny-1) : 0.0
        show(io, UnicodePlots.heatmap(bincounts(h)'; xfact=xfact, xoffset=minimum(ex), yfact=yfact, yoffset=minimum(ey)))
        println(io)
        println(io, "edges: ", binedges(h))
        println(io, "bin counts: ", bincounts(h))
        print(io, "total count: ", integral(h))
    end
end

function Base.show(io::IO, m::MIME"text/html", h::Hist2D)
    println(io, """

$(_svg(h))

edges: $(repr(binedges(h), context=:limit => true))

bin counts: $(repr(bincounts(h), context=:limit => true))

maximum count: $(maximum(bincounts(h)))

total count: $(integral(h))

    """)
end


text:
using Plots

module BattleshipSimulator

using Plots

export layoutBoard, layoutBoard!, distributionBoard, distributionBoardHit
export drawBoard, drawBoardComp, drawHit, animateGame, animateGameDist
export playManyUniqueGames, playManySingleGames, playManyGames
export randomHunter, targetHunter, distHunter

###############################################
#######  Board Layout and Placement  ##########
###############################################

# Create board positions
function layoutBoard(x::Int64,y::Int64; ships=[5,4,3,3,2])
    # Reset the matrix to all zeros
    M = zeros(Int64,x,y)
    Msize = (x,y)
    ship_counter = 0
    # Loop through ships and place on board
    for ship in ships
        placed = 0
        fit = [0, 0, 0, 0]
        ship_counter += 1
        while placed == 0
            trial = rand(find(.~M))
            checkShipFit!(fit,M,Msize,ship,trial)
            pickFit = rand([1 2 3 4]) # Pick a random orientation
            if fit[pickFit] > 0
                placeShip!(M,ship,ship_counter,trial,pickFit)
                placed = 1; #show(STDOUT, "text/plain", M); #println(" ")
            end
        end
    end
    return M
end

# Create board positions (reusing the same matrix)
function layoutBoard!(M::Array{Int64,2} ; ships=[5,4,3,3,2])
    # Reset the matrix to all zeros
    M[:] = 0
    Msize = size(M)
    ship_counter = 0
    # Loop through ships and place on board
    for ship in ships
        placed = 0
        fit = [0, 0, 0, 0]
        ship_counter += 1
        while placed == 0
            trial = rand(find(.~M))
            checkShipFit!(fit,M,Msize,ship,trial)
            pickFit = rand([1 2 3 4]) # Pick a random orientation
            if fit[pickFit] > 0
                placeShip!(M,ship,ship_counter,trial,pickFit)
                placed = 1; #show(STDOUT, "text/plain", M); #println(" ")
            end
        end
    end
end

# Check to see if the ship fits
function checkShipFit!(fit::Array{Int64,1},M::Array{Int64,2},Msize,ship::Int64,target::Int64)
    targetSub = ind2sub(M,target)
    fit[:] = 0
    (targetSub[2] + ship - 1) > Msize[2] ? fit[1] = 0 : (sum(M[targetSub[1],targetSub[2]:targetSub[2]+ship-1])    != 0 ? fit[1] = 0 : fit[1] = 1)
    (targetSub[1] - ship + 1) < 1        ? fit[2] = 0 : (sum(M[targetSub[1]:-1:(targetSub[1]-ship+1),targetSub[2]]) != 0 ? fit[2] = 0 : fit[2] = 1)
    (targetSub[2] - ship + 1) < 1        ? fit[3] = 0 : (sum(M[targetSub[1],targetSub[2]:-1:(targetSub[2]-ship+1)]) != 0 ? fit[3] = 0 : fit[3] = 1)
    (targetSub[1] + ship - 1) > Msize[1] ? fit[4] = 0 : (sum(M[targetSub[1]:targetSub[1]+ship-1,targetSub[2]])    != 0 ? fit[4] = 0 : fit[4] = 1)
end

# Check to see if the ship fits, but only in the vertical and horizontal directions
function checkShipFitVH!(fit::Array{Int64,1}, M::Array{Int64,2}, Msize, ship::Int64, target::Int64)
    targetSub = ind2sub(M,target)
    fit[:] = 0
    (targetSub[2] + ship - 1) > Msize[2] ? fit[1] = 0 : (sum(M[targetSub[1],targetSub[2]:targetSub[2]+ship-1])    != 0 ? fit[1] = 0 : fit[1] = 1)
    (targetSub[1] + ship - 1) > Msize[1] ? fit[2] = 0 : (sum(M[targetSub[1]:targetSub[1]+ship-1,targetSub[2]])    != 0 ? fit[2] = 0 : fit[2] = 1)
end
# Check to see if the ship fits given a hit has occured, but only in the vertical and horizontal directions
function checkShipFitHitVH!(fit::Array{Int64,1}, M::Array{Int64,2}, Msize, ship::Int64, target::Int64)
    targetSub = ind2sub(M,target)
    fit[:] = 0
    hits = find(M==-1)
    M[hits] = 0
    if !((targetSub[2] + ship - 1) > Msize[2])
        M[hits] = -1
        sum(M[targetSub[1],targetSub[2]:targetSub[2]+ship-1]) < 0 ? fit[1] = 1 : fit[1] = 0
        M[hits] = 0
    end
    if !((targetSub[1] + ship - 1) > Msize[1])
        M[hits] = -1
        sum(M[targetSub[1]:targetSub[1]+ship-1,targetSub[2]]) < 0 ? fit[2] = 1 : fit[2] = 0
    end
    M[hits] = -1
end

# Update the grid to place the ship
function placeShip!(M::Array{Int64,2},ship::Int64,shipVal::Int64,target::Int64,orientation::Int64)
    targetSub = ind2sub(M,target)
    if orientation == 1
       M[targetSub[1],targetSub[2]:targetSub[2]+ship-1] += shipVal
    elseif orientation == 2
        M[targetSub[1]:-1:targetSub[1]-ship+1,targetSub[2]] += shipVal
    elseif orientation == 3
        M[targetSub[1],targetSub[2]:-1:targetSub[2]-ship+1] += shipVal
    elseif orientation == 4
        M[targetSub[1]:targetSub[1]+ship-1,targetSub[2]] += shipVal
    end
end

###############################################
##########  Board Distributions  ##############
###############################################

# Create board distribution
function distributionBoard(S::Array{Int64,2} ; ships=[5,4,3,3,2])
    Ssize = size(S)
    D = zeros(Int64,Ssize)
    # Loop through ships and place on board
    for ship in ships
        fit = [0, 0]
        for i = 1:length(S)
            checkShipFitVH!(fit,S,Ssize,ship,i)
            if fit[1] == 1; placeShip!(D,ship,1,i,1); end
            if fit[2] == 1; placeShip!(D,ship,1,i,4); end
        end
    end
    return D
end

# Create board distribution (reuse the same distribution matrix)
function distributionBoard!(D::Array{Int64,2},S::Array{Int64,2} ; ships=[5,4,3,3,2])
    # Reset the matrix to all zeros
    D[:] = 0
    Ssize = size(S)
    # Loop through ships and place on board
    for ship in ships
        fit = [0, 0]
        for i = 1:length(S)
            checkShipFitVH!(fit,S,Ssize,ship,i)
            if fit[1] == 1; placeShip!(D,ship,1,i,1); end
            if fit[2] == 1; placeShip!(D,ship,1,i,4); end
        end
    end
end

# Create board distribution for when a hit has occurred
function distributionBoardHit(S::Array{Int64,2}; ships=[5,4,3,3,2], shipsAvail=[5,4,3,3,2])
    Ssize = size(S)
    D = zeros(Int64,Ssize)
    # Loop through ships and place on board
    for ship in shipsAvail
        fit = [0, 0]
        for i = 1:length(S)
            checkShipFitHitVH!(fit,S,Ssize,ship,i)
            if fit[1] == 1; placeShip!(D,ship,1,i,1); end
            if fit[2] == 1; placeShip!(D,ship,1,i,4); end
        end
    end
    return D
end

# Create board distribution for when a hit has occurred (reuse matrix)
function distributionBoardHit!(D::Array{Int64,2}, S::Array{Int64,2}; ships=[5,4,3,3,2], shipsAvail=[5,4,3,3,2])
    # Reset the matrix to all zeros
    D[:] = 0
    Ssize = size(S)
    # Loop through ships and place on board
    for ship in shipsAvail
        fit = [0, 0]
        for i = 1:length(S)
            checkShipFitHitVH!(fit,S,Ssize,ship,i)
            if fit[1] == 1; placeShip!(D,ship,1,i,1); end
            if fit[2] == 1; placeShip!(D,ship,1,i,4); end
        end
    end
end

###############################################
#########  Draw Boards and Hits  ##############
###############################################

function drawBoard(M; flatten=false)
    if flatten == true
        plt = heatmap(M.>0,colorbar=false,aspectratio=:equal,axis=false,dpi=20,grid=true,color=:blues)
    else
        plt = heatmap(M,colorbar=false,aspectratio=:equal,dpi=20,axis=false,grid=true,color=:blues)
    end
    return plt
end

function drawBoard!(plt,M; flatten=false)
    if flatten == true
        heatmap!(plt,M.>0,colorbar=false,aspectratio=:equal,dpi=20,axis=false,grid=true)
    else
        heatmap!(plt,M,colorbar=false,aspectratio=:equal,dpi=20,axis=false,grid=true)
    end
end

function drawBoardComp(M, D; name1="First", name2="Second", flatten=false)
    if flatten == true
        p1 = heatmap(M.>0,colorbar=false,aspectratio=:equal,axis=false,title=name1,dpi=20,color=:blues)
        p2 = heatmap(D.>0,colorbar=false,aspectratio=:equal,axis=false,title=name2,dpi=20)
    else
        p1 = heatmap(M,colorbar=false,aspectratio=:equal,axis=false,title=name1,dpi=20,color=:blues)
        p2 = heatmap(D,colorbar=false,aspectratio=:equal,axis=false,title=name2,dpi=20)
    end
    plt = plot(p1,p2, layout=(1,2), legend=false)
    return plt
end

function drawBoardComp!(plt,M, D; name1="First", name2="Second", flatten=false)

    if flatten == true
        heatmap(plt[1],M.>0,colorbar=false,aspectratio=:equal,axis=false,dpi=20,title=name1,color=:blues)
        heatmap(plt[2],D.>0,colorbar=false,aspectratio=:equal,axis=false,dpi=20,title=name2)
    else
        heatmap(plt[1],M,colorbar=false,aspectratio=:equal,axis=false,title=name1,dpi=20,color=:blues)
        heatmap(plt[2],D,colorbar=false,aspectratio=:equal,axis=false,title=name2,dpi=20)
    end
end

function drawHit!(plt,target,M)
    targetSub = ind2sub(M,target)
    if M[target] > 0
        plot!(plt,[targetSub[2]],[targetSub[1]],seriestype=:scatter, markersize = 15, markershape = :circle,dpi=20, c=RGB(1,0,0),legend=false,alpha=1)
    else
        plot!(plt,[targetSub[2]],[targetSub[1]],seriestype=:scatter, markersize = 10, markershape = :xcross,dpi=20, c=RGB(1,0,0),legend=false,alpha=1)
    end
end

function animateGame(M,hits)
    plt = drawBoard(M)
    drawHit!(plt,hits[1],M)
    anim = @animate for i = 1:length(hits)
            drawHit!(plt,hits[i],M)
        end
    return anim
end

function animateGameDist(M,hits; ships=[5,4,3,3,2])
    S = zeros(Int64,size(M))
    D = distributionBoard(S; ships=[5,4,3,3,2])
    plt = drawBoardComp(M,D; name1 = "True Hidden Map", name2="Distribution of Possible Ships")
    anim = @animate for i = 1:length(hits)
            drawHit!(plt[1],hits[i],M)
            S[hits[i]] = 1
            distributionBoard!(D,S; ships=[5,4,3,3,2])
            drawBoard!(plt[2],D)
        end
    return anim
end

###############################################
############  Play Many Games  ################
###############################################

# Create a new board every time you simulate, but only play the board once
function playManyUniqueGames(M,N,; hunt=randomHunter, ships=[5,4,3,3,2])
    winTime = zeros(Int64,N)
    for i = 1:N
        layoutBoard!(M; ships = ships)
        output = hunt(M; ships = ships)
        winTime[i] = length(output)
    end
    return winTime
end

# Simulate many games with a single board layout
function playManySingleGames(M,N,; hunt=randomHunter, ships=[5,4,3,3,2])
    winTime = zeros(Int64,N)
    for i = 1:N
        output = hunt(M; ships = ships)
        winTime[i] = length(output)
    end
    return winTime
end

# Create a new board and simulate many games with it
function playManyGames(M,N,P; hunt=randomHunter, ships=[5,4,3,3,2])
    winTime = zeros(Int64,P,N)
    for i = 1:N
        layoutBoard!(M; ships = ships)
        winTime[:,i] = playManySingleGames(M,N; hunt = hunt, ships = ships)
    end
    return winTime
end

###############################################
###########  Hunting Strategies  ##############
###############################################

# A simple but dumb random hunter
function randomHunter(M; ships = [5,4,3,3,2])
    numTargets = sum(M[:].>0)  # This is always known information
    availSpots = collect(1:length(M))
    hitSpots = Array{Int64,1}()
    numHitTargets = 0
    while numHitTargets != numTargets
        targ = rand(find(availSpots))
        availSpots[targ] = 0
        if M[targ] > 0; numHitTargets += 1; end
        append!(hitSpots,targ)
    end
    return hitSpots
end

# A slightly more sophisticated hunter, which eliminates a ship once found
# This strategy does not use ship information explicitly
function targetHunter(M;  ships = [5,4,3,3,2])
    numTargets = sum(M[:].>0)  # This is always known information
    availSpots = collect(1:length(M))
    hitSpots = Array{Int64,1}()
    toHit = Array{Int64,1}()
    numHitTargets = 0
    Msize = size(M)
    while numHitTargets != numTargets
        if length(toHit) == 0
            targ = rand(find(availSpots))
        else
            targ = pop!(toHit)
        end
        targSub = ind2sub(M,targ)
        availSpots[targ] = 0
        append!(hitSpots,targ)
        if M[targ] > 0
            numHitTargets += 1
            targSub = ind2sub(M,targ)
            if ((targSub[2] + 1) <= Msize[2]) && ~(in(sub2ind(Msize,targSub[1],targSub[2]+1),hitSpots))
                append!(toHit,sub2ind(Msize,targSub[1],targSub[2]+1))
            end
            if ((targSub[1] - 1) > 0) && ~(in(sub2ind(Msize,targSub[1]-1,targSub[2]),hitSpots))
                append!(toHit,sub2ind(Msize,targSub[1]-1,targSub[2]))
            end
            if ((targSub[2] - 1) > 0) && ~(in(sub2ind(Msize,targSub[1],targSub[2]-1),hitSpots))
                append!(toHit,sub2ind(Msize,targSub[1],targSub[2]-1))
            end
            if ((targSub[1] + 1) <= Msize[1]) && ~(in(sub2ind(Msize,targSub[1]+1,targSub[2]),hitSpots))
                append!(toHit,sub2ind(Msize,targSub[1]+1,targSub[2]))
            end
            toHit = unique(toHit)
        end
    end
    return hitSpots
end

# Use distribution of possible remaining ships to identify the most likely spots
# in grid to target.  This function DOES use ship information explicitly
# This function is not currently working correctly.
function distHunter(M;  ships = [5,4,3,3,2])
    numTargets = sum(M[:].>0)  # This is always known information
    hitSpots = Array{Int64,1}()
    stuck_counter = 0
    numHitTargets = 0
    shipsAvail = deepcopy(ships)
    Msize = size(M)
    S = zeros(Int64,Msize)
    D = zeros(Int64,Msize)
    while numHitTargets != numTargets
        distributionBoard!(D,S; ships=[5,4,3,3,2])
        temp = findmax(D)
        targ = temp[2]
        append!(hitSpots,targ)
        if M[targ] == 0
            S[targ] = 1
        else
            numHitTargets += 1
            shipTypes_orig = setdiff(unique(M[S.==0]),[0])
            numShipsLeft_orig = length(shipTypes_orig)
            S[targ] = -1;
            (length(setdiff(unique(M[S.==0]),[0]))) < numShipsLeft_orig ? sunk = 1 : sunk = 0
            while (sunk == 0) && (numHitTargets != numTargets)
                distributionBoardHit!(D, S; ships=ships, shipsAvail=shipsAvail)
                D[S.==-1] = 0; D[S.==1] = 0;
                temp = findmax(D)
                targ = temp[2]
                #println(targ)
                append!(hitSpots,targ)
                M[targ] > 0 ? (S[targ] = -1; numHitTargets += 1) : S[targ] = 1
                if ((length(unique(M[S.==0])) - 1) < numShipsLeft_orig)
                    sunk = 1
                end
                #show(STDOUT, "text/plain", S); println(" ")
                #println("STUCK! - ",stuck_counter); stuck_counter += 1
            end
            S[find(S.==-1)] = 1
            shipTypes = setdiff(unique(M[S.==0]),[0])
            shipsAvail = ships[shipTypes]
        end
        #show(STDOUT, "text/plain", S); println(" ")
        #println("STUCK! - ",stuck_counter); stuck_counter += 1
    end
    return hitSpots
end

end # module

max_stars_repo_name: mstobb/BattleshipSimulator

max_issues_repo_name: mstobb/BattleshipSimulator

max_forks_repo_name: mstobb/BattleshipSimulator