# OPERATORS defines single characters that should be converted to an MML
# operator element: <mo></mo>.  There are other MML operators that can not be
# represented by a single ASCII character in TeX, e.g, \{, \}, \subset, \vee,
# \cup, \in, \forall, \approx, etc. They are processed in the command reader.
# chars in BRACKETS are converted to TK_PAREN type which will also be
# converted to <mo> but could carry size attributes.
const OPERATOR_IDS = "+-*/!:=,.\'"
const BRACKETS = "|<>()[]"

# types
# NOTE: Most are not types in TeX, e.g., objects in TK_OPID just represent
# regular symbols in TeX, but they will be converted to operator elements,
# i.e., <mo> elements in MathML
const TK_MATH = 0               # to the top elelment <math>
const TK_ID = 1                 # to <mi>, single-char identifiers
const TK_NUM = 2                # to <mn>
#const TK_SYM = 3               # to <mi> with UCS codepoint literal
const TK_FUN = 4                # to <mi>, multi-char identifiers
#const TK_CMD = 5
const TK_ENV = 6
const TK_OPID = 7               # convert to <mo>
# parenthesis' are also OPIDs. They are a little more special than single-char
# OPIDs in that they are paired, and may be rendered in a specified size when
# a \big TeX command is present (in this case, they are converted to <mo> with
# `maxsize` or `minsize` attributes)
const TK_PAREN = 8
const TK_UNKNOWN = 9
# types that need special layout treatment when sub-sup operators are present.
# TK_LIMs are parsed to TK_FUN, which are ultimately just special identifiers.
# TK_INTs and TK_BIGOPs are parsed to TK_OPID (they are math operator identifiers,
# not true TeX language operators)
const TK_LIM = 33               # \lim, \liminf, ... only subscripts
const TK_INT = 34               # \int, ..., sub-super-scripts
const TK_BIGOP = 35             # \sum, \prod, \bigcup, ..., sub-super-scripts

# TeX language commands, each with its own type
const TK_LBRACE = 11            # '{': initiate an expression group
const TK_RBRACE = 12            # '}': end an expression group
const TK_SUB = 13               # '-': subscript
const TK_SUP = 14               # '^': superscript
const TK_AMP = 15               # '&': ampersand
const TK_NEWLINE = 16           # '\\': new line
const TK_OPNAME = 70            # \operatorname
const TK_TEXT = 41
const TK_SQRT = 42
const TK_FRAC = 43
const TK_LEFT = 44
const TK_RIGHT = 45
const TK_MIDDLE = 46
const TK_BEGIN = 47
const TK_END = 48
const TK_BINOM = 49
const TK_OVERSET = 50
const TK_UNDERSET = 51
const TK_FRACOVER = 52
# TK_OEVRACCENT(TK_UNDERACCENT) are tokens for TeX command: \dot,
# \tilde, \hat, etc. The <mover> (<munder>) element carries an attrubute
# `accent` which is set to be "true", e.g.,
# "\hat{A}" is converted to
#   <mover accent="true">
#     <mi>A</mi>
#     <mo> ^<!--CIRCUMFLEX ACCENT--></mo>
#   </mover>
const TK_OVERACCENT = 54
const TK_UNDERACCENT = 55
const TK_SPACE = 60
const TK_BIG = 61
# \overbrace (\underbrace) are converted to <mover> (<munder>). They could
# carry a super(sub)-script.
const TK_OVERBRACE = 52
const TK_UNDERBRACE = 53

# other MML operators

# an expression group with specified style
# It seems MathJax does not support mathvariant, we may not implement
# style for now... need to do more experiments.
const TK_STYLE = 100            # \mathrm, \mathcal, ... convert to <mstyle>
const TK_IGSTYLE = 101          # in-group style: \bf, \it, \red, ...
const TK_ENV_NAME = 110         # env-names that follow \begin(\end) commands
const TK_ENV_ATTR = 111         # env-attributes that follow \begin(\end) commands

struct Token
    type::Int
    val::String
end
Token(t::Int, v::Char) = Token(t, string(v))

# symbols are copied from `latex_symbols.jl` in the `REPL` module.
const command_to_token = Dict(
    # styles
    "mathbf"     => Token(TK_STYLE, "mathbf"),
    "mathcal"    => Token(TK_STYLE, "mathcal"),
    "mathbb"     => Token(TK_STYLE, "mathbb"),
    "mathfrak"   => Token(TK_STYLE, "mathfrak"),
    "bigl"       => Token(TK_BIG, "1.2em"),
    "bigr"       => Token(TK_BIG, "1.2em"),
    "Bigl"       => Token(TK_BIG, "1.623em"),
    "Bigr"       => Token(TK_BIG, "1.623em"),
    "biggl"      => Token(TK_BIG, "2.047em"),
    "biggr"      => Token(TK_BIG, "2.047em"),
    "Biggl"      => Token(TK_BIG, "2.470em"),
    "Biggr"      => Token(TK_BIG, "2.470em"),
    "it"         => Token(TK_IGSTYLE, "it"),
    "bf"         => Token(TK_IGSTYLE, "bf"),
    "rm"         => Token(TK_IGSTYLE, "rf"),
    # commands
    "text"          => Token(TK_TEXT, ""),
    "sqrt"          => Token(TK_SQRT, ""),
    "frac"          => Token(TK_FRAC, ""),
    "over"          => Token(TK_FRACOVER, ""),
    "binom"         => Token(TK_BINOM, ""),
    "left"          => Token(TK_LEFT, ""),
    "right"         => Token(TK_RIGHT, ""),
#    "middle"        => Token(TK_MIDDLE, ""),
    "begin"         => Token(TK_BEGIN, ""),
    "end"           => Token(TK_END, ""),
    "overset"       => Token(TK_OVERSET, ""),
    "underset"      => Token(TK_UNDERSET, ""),
    # if \overbrace (\underspace) carries a super-script (sub-script)
    # operator, it has to be dealt with specially as the result is NOT
    # a usual super-scripted (sub-scripted) glyph.
    "overbrace"     => Token(TK_OVERBRACE, '\u23de'),
    "underbrace"    => Token(TK_UNDERBRACE, '\u23df'),
    # operator name
    "operatorname"  => Token(TK_OPNAME, ""),
    # over/under-script operator as accent
    "dot"               => Token(TK_OVERACCENT, '˙'),
    "ddot"              => Token(TK_OVERACCENT, '¨'),
    "bar"               => Token(TK_OVERACCENT, '¯'),
    "hat"               => Token(TK_OVERACCENT, '^'),
    "check"             => Token(TK_OVERACCENT, 'ˇ'),
    "breve"             => Token(TK_OVERACCENT, '˘'),
    "acute"             => Token(TK_OVERACCENT, '´'),
    "grave"             => Token(TK_OVERACCENT, '`'),
    "tilde"             => Token(TK_OVERACCENT, '~'),
    "vec"               => Token(TK_OVERACCENT, '→'),
    "overline"          => Token(TK_OVERACCENT, '_'),
    "underline"         => Token(TK_OVERACCENT, '_'),
    "widehat"           => Token(TK_OVERACCENT, '^'),
    "widetilde"         => Token(TK_OVERACCENT, '~'),
    "overrightarrow"    => Token(TK_OVERACCENT, '→'),
    "overleftarrow"     => Token(TK_OVERACCENT, '←'),
    # spaces and new-line
    # Space tokens are conveted to <mspace> elements
    # new-line is also converted to <mspace> with `linebreak="newline"`
    "\\"         => Token(TK_NEWLINE, ""),
    "!"          => Token(TK_SPACE, "-0.167em"),    # -3./18.
    ","          => Token(TK_SPACE, "0.167em"),     #  3./18.
    ":"          => Token(TK_SPACE, "0.222em"),     #  4./18.
    ";"          => Token(TK_SPACE, "0.278em"),     #  5./18"
    " "          => Token(TK_SPACE, "1.0em"),       #  1.
    "quad"       => Token(TK_SPACE, "1.0em"),       #  1.
    "qquad"      => Token(TK_SPACE, "2.0em"),       #  2.
    # parenthesis
    # parenthesis token are converted to either <mo> or <mo> with sizes if
    # \left (\right) command is present
    "langle"     => Token(TK_PAREN, "⟨"),
    "rangle"     => Token(TK_PAREN, "⟩"),
    "{"          => Token(TK_PAREN, "{"),
    "}"          => Token(TK_PAREN, "}"),
    "lceil"      => Token(TK_PAREN, "⌈"),
    "rceil"      => Token(TK_PAREN, "⌉"),
    "lfloor"     => Token(TK_PAREN, "⌊"),
    "rfloor"     => Token(TK_PAREN, "⌋"),
    "|"          => Token(TK_PAREN, '∥'),
    # \lim, \int, and \bigop, we might need some special treatment?
    "lim"        => Token(TK_LIM, "lim"),
    "liminf"     => Token(TK_LIM, "lim inf"),
    "limsup"     => Token(TK_LIM, "lim sup"),
    "min"        => Token(TK_LIM, "min"),
    "max"        => Token(TK_LIM, "max"),
    "inf"        => Token(TK_LIM, "inf"),
    "sup"        => Token(TK_LIM, "sup"),
    "int"        => Token(TK_INT, '∫'),
    "iint"       => Token(TK_INT, '∬'),
    "iiint"      => Token(TK_INT, '∭'),
    "oint"       => Token(TK_INT, '∮'),
    "sum"        => Token(TK_BIGOP, '∑'),
    "prod"       => Token(TK_BIGOP, '∏'),
    "coprod"     => Token(TK_BIGOP, '∐'),
    "bigcap"     => Token(TK_BIGOP, '⋂'),
    "bigcup"     => Token(TK_BIGOP, '⋃'),
    "bigsqcup"   => Token(TK_BIGOP, '⨆'),
    "bigvee"     => Token(TK_BIGOP, '⋁'),
    "bigwedge"   => Token(TK_BIGOP, '⋀'),
    "bigodot"    => Token(TK_BIGOP, '⨀'),
    "bitotimes"  => Token(TK_BIGOP, '⨂'),
    "bigoplus"   => Token(TK_BIGOP, '⨁'),
    "biguplus"   => Token(TK_BIGOP, '⨄'),

    # environments
    "matrix"     => Token(TK_ENV, "matrix"),
    "pmatrix"    => Token(TK_ENV, "pmatrix"),
    "bmatrix"    => Token(TK_ENV, "bmatrix"),
    "vmatrix"    => Token(TK_ENV, "vmatrix"),

    # symbols
    "Alpha"      => Token(TK_ID, 'Α'),
    "alpha"      => Token(TK_ID, 'α'),
    "Beta"       => Token(TK_ID, 'Β'),
    "beta"       => Token(TK_ID, 'β'),
    "Gamma"      => Token(TK_ID, 'Γ'),
    "gamma"      => Token(TK_ID, 'γ'),
    "digamma"    => Token(TK_ID, 'ϝ'),
    "Delta"      => Token(TK_ID, 'Δ'),
    "delta"      => Token(TK_ID, 'δ'),
    "Epsilon"    => Token(TK_ID, 'Ε'),
    "epsilon"    => Token(TK_ID, 'ϵ'),
    "varepsilon" => Token(TK_ID, 'ε'),
    "Zeta"       => Token(TK_ID, 'Ζ'),
    "zeta"       => Token(TK_ID, 'ζ'),
    "Eta"        => Token(TK_ID, 'Η'),
    "eta"        => Token(TK_ID, 'η'),
    "Theta"      => Token(TK_ID, 'Θ'),
    "theta"      => Token(TK_ID, 'θ'),
    "vartheta"   => Token(TK_ID, 'ϑ'),
    "Iota"       => Token(TK_ID, 'Ι'),
    "iota"       => Token(TK_ID, 'ι'),
    "Kappa"      => Token(TK_ID, 'Κ'),
    "kappa"      => Token(TK_ID, 'κ'),
    "Lambda"     => Token(TK_ID, 'Λ'),
    "lambda"     => Token(TK_ID, 'λ'),
    "Mu"         => Token(TK_ID, 'Μ'),
    "mu"         => Token(TK_ID, 'μ'),
    "Nu"         => Token(TK_ID, 'Ν'),
    "nu"         => Token(TK_ID, 'ν'),
    "Xi"         => Token(TK_ID, 'Ξ'),
    "xi"         => Token(TK_ID, 'ξ'),
    "Omicron"    => Token(TK_ID, 'Ο'),
    "omicron"    => Token(TK_ID, 'ο'),
    "Pi"         => Token(TK_ID, 'Π'),
    "pi"         => Token(TK_ID, 'π'),
    "varpi"      => Token(TK_ID, 'ϖ'),
    "Rho"        => Token(TK_ID, 'Ρ'),
    "rho"        => Token(TK_ID, 'ρ'),
    "varrho"     => Token(TK_ID, 'ϱ'),
    "Sigma"      => Token(TK_ID, 'Σ'),
    "sigma"      => Token(TK_ID, 'σ'),
    "varsigma"   => Token(TK_ID, 'ς'),
    "Tau"        => Token(TK_ID, 'Τ'),
    "tau"        => Token(TK_ID, 'τ'),
    "Upsilon"    => Token(TK_ID, 'Υ'),
    "upsilon"    => Token(TK_ID, 'υ'),
    "Phi"        => Token(TK_ID, 'Φ'),
    "phi"        => Token(TK_ID, 'ϕ'),
    "varphi"     => Token(TK_ID, 'φ'),
    "Chi"        => Token(TK_ID, 'Χ'),
    "chi"        => Token(TK_ID, 'χ'),
    "Psi"        => Token(TK_ID, 'Ψ'),
    "psi"        => Token(TK_ID, 'ψ'),
    "Omega"      => Token(TK_ID, 'Ω'),
    "omega"      => Token(TK_ID, 'ω'),
    "aleph"      => Token(TK_ID, 'ℵ'),
    "O"          => Token(TK_ID, 'Ø'),
    "o"          => Token(TK_ID, 'ø'),
    "ss"         => Token(TK_ID, 'ß'),
    "imath"      => Token(TK_ID, 'ı'),
    "jmath"      => Token(TK_ID, 'ȷ'),
    "ell"        => Token(TK_ID, 'ℓ'),
    "hbar"       => Token(TK_ID, 'ℏ'),
    "hslash"     => Token(TK_ID, 'ℏ'),
    "infty"      => Token(TK_ID, '∞'),
    "Re"         => Token(TK_ID, 'ℜ'),
    "Im"         => Token(TK_ID, 'ℑ'),
    "complement" => Token(TK_ID, '∁'),
    "emptyset"   => Token(TK_ID, '∅'),
    "therefore"  => Token(TK_ID, '∴'),
    "because"    => Token(TK_ID, '∵'),
    "Diamond"    => Token(TK_ID, '◊'),
    "Box"        => Token(TK_ID, '◻'),
    "triangle"   => Token(TK_ID, '△'),
    "angle"      => Token(TK_ID, '∠'),
    # function names
    "sin"        => Token(TK_FUN, "sin"),
    "cos"        => Token(TK_FUN, "cos"),
    "tan"        => Token(TK_FUN, "tan"),
    "csc"        => Token(TK_FUN, "csc"),
    "sec"        => Token(TK_FUN, "sec"),
    "cot"        => Token(TK_FUN, "cot"),
    "arcsin"     => Token(TK_FUN, "arcsin"),
    "arccos"     => Token(TK_FUN, "arccos"),
    "arctan"     => Token(TK_FUN, "arctan"),
    "sinh"       => Token(TK_FUN, "sinh"),
    "cosh"       => Token(TK_FUN, "cosh"),
    "tanh"       => Token(TK_FUN, "tanh"),
    "coth"       => Token(TK_FUN, "coth"),
    "exp"        => Token(TK_FUN, "exp"),
    "ln"         => Token(TK_FUN, "ln"),
    "log"        => Token(TK_FUN, "log"),
    "erf"        => Token(TK_FUN, "erf"),
    "erfc"       => Token(TK_FUN, "erfc"),
    "arg"        => Token(TK_FUN, "arg"),
    "ker"        => Token(TK_FUN, "ker"),
    "dim"        => Token(TK_FUN, "dim"),
    "det"        => Token(TK_FUN, "det"),
    # operators, mostly infix, some are prefix
    "times"              => Token(TK_OPID, '×'),
    "oplus"              => Token(TK_OPID, '⊕'),
    "ominus"             => Token(TK_OPID, '⊖'),
    "otimes"             => Token(TK_OPID, '⊗'),
    "oslash"             => Token(TK_OPID, '⊘'),
    "odot"               => Token(TK_OPID, '⊙'),
    "bigcirc"            => Token(TK_OPID, '◯'),
    "amalg"              => Token(TK_OPID, '⨿'),
    "pm"                 => Token(TK_OPID, '±'),
    "mp"                 => Token(TK_OPID, '∓'),
    "cdot"               => Token(TK_OPID, '·'),
    "cdots"              => Token(TK_OPID, '⋯'),
    "vdots"              => Token(TK_OPID, '⋮'),
    "ldots"              => Token(TK_OPID, '…'),
    "ddots"              => Token(TK_OPID, '⋱'),
    "circ"               => Token(TK_OPID, '∘'),
    "bullet"             => Token(TK_OPID, '∙'),
    "star"               => Token(TK_OPID, '⋆'),
    "div"                => Token(TK_OPID, '÷'),
    "lnot"               => Token(TK_OPID, '¬'),
    "land"               => Token(TK_OPID, '∧'),
    "lor"                => Token(TK_OPID, '∨'),
    "sim"                => Token(TK_OPID, '∼'),
    "simeq"              => Token(TK_OPID, '≃'),
    "nsim"               => Token(TK_OPID, '≁'),
    "cong"               => Token(TK_OPID, '≅'),
    "approx"             => Token(TK_OPID, '≈'),
    "ne"                 => Token(TK_OPID, '≠'),
    "neq"                => Token(TK_OPID, '≠'),
    "equiv"              => Token(TK_OPID, '≡'),
    "nequiv"             => Token(TK_OPID, '≢'),
    "prec"               => Token(TK_OPID, '≺'),
    "succ"               => Token(TK_OPID, '≻'),
    "preceq"             => Token(TK_OPID, '⪯'),
    "succeq"             => Token(TK_OPID, '⪰'),
    "dashv"              => Token(TK_OPID, '⊣'),
    "asymp"              => Token(TK_OPID, '≍'),
    "doteq"              => Token(TK_OPID, '≐'),
    "propto"             => Token(TK_OPID, '∝'),
    "barwedge"           => Token(TK_OPID, '⊼'),
    "ltimes"             => Token(TK_OPID, '⋉'),
    "rtimes"             => Token(TK_OPID, '⋊'),
    "Join"               => Token(TK_OPID, '⋈'),
    "lhd"                => Token(TK_OPID, '⊲'),
    "rhd"                => Token(TK_OPID, '⊳'),
    "unlhd"              => Token(TK_OPID, '⊴'),
    "unrhd"              => Token(TK_OPID, '⊵'),
    "vee"                => Token(TK_OPID, '∨'),
    "uplus"              => Token(TK_OPID, '⊎'),
    "wedge"              => Token(TK_OPID, '∧'),
    "veebar"             => Token(TK_OPID, '⊻'),
    "cap"                => Token(TK_OPID, '∩'),
    "cup"                => Token(TK_OPID, '∪'),
    "mid"                => Token(TK_OPID, '∣'),
    "nmid"               => Token(TK_OPID, '∤'),
    "parallel"           => Token(TK_OPID, '∥'),
    "perp"               => Token(TK_OPID, '⊥'),
    "forall"             => Token(TK_OPID, '∀'),
    "exists"             => Token(TK_OPID, '∃'),
    "nexists"            => Token(TK_OPID, '∄'),
    "lt"                 => Token(TK_OPID, '<'),
    "gt"                 => Token(TK_OPID, '>'),
    "leq"                => Token(TK_OPID, '≤'),
    "geq"                => Token(TK_OPID, '≥'),
    "ll"                 => Token(TK_OPID, '≪'),
    "gg"                 => Token(TK_OPID, '≫'),
    "lessapprox"         => Token(TK_OPID, '⪅'),
    "lesssim"            => Token(TK_OPID, '≲'),
    "eqslantless"        => Token(TK_OPID, '⪕'),
    "leqslant"           => Token(TK_OPID, '⩽'),
    "leqq"               => Token(TK_OPID, '≦'),
    "geqq"               => Token(TK_OPID, '≧'),
    "geqslant"           => Token(TK_OPID, '⩾'),
    "eqslantgtr"         => Token(TK_OPID, '⪖'),
    "gtrsim"             => Token(TK_OPID, '≳'),
    "gtrapprox"          => Token(TK_OPID, '⪆'),
    "approxeq"           => Token(TK_OPID, '≊'),
    "lessdot"            => Token(TK_OPID, '⋖'),
    "lll"                => Token(TK_OPID, '⋘'),
    "lessgtr"            => Token(TK_OPID, '≶'),
    "lesseqgtr"          => Token(TK_OPID, '⋚'),
    "lesseqqgtr"         => Token(TK_OPID, '⪋'),
    "doteqdot"           => Token(TK_OPID, '≑'),
    "leftarrow"          => Token(TK_OPID, '←'),
    "gets"               => Token(TK_OPID, '←'),
    "rightarrow"         => Token(TK_OPID, '→'),
    "to"                 => Token(TK_OPID, '→'),
    "nleftarrow"         => Token(TK_OPID, '↚'),
    "nrightarrow"        => Token(TK_OPID, '↛'),
    "leftrightarrow"     => Token(TK_OPID, '↔'),
    "nleftrightarrow"    => Token(TK_OPID, '↮'),
    "longleftarrow"      => Token(TK_OPID, '⟵'),
    "longrightarrow"     => Token(TK_OPID, '⟶'),
    "longleftrightarrow" => Token(TK_OPID, '⟷'),
    "Leftarrow"          => Token(TK_OPID, '⇐'),
    "Rightarrow"         => Token(TK_OPID, '⇒'),
    "nLeftarrow"         => Token(TK_OPID, '⇍'),
    "nRightarrow"        => Token(TK_OPID, '⇏'),
    "uparrow"            => Token(TK_OPID, '↑'),
    "downarrow"          => Token(TK_OPID, '↓'),
    "updownarrow"        => Token(TK_OPID, '↕'),
    "in"                 => Token(TK_OPID, '∈'),
    "ni"                 => Token(TK_OPID, '∉'),
    "notin"              => Token(TK_OPID, '∉'),
    "subset"             => Token(TK_OPID, '⊂'),
    "supset"             => Token(TK_OPID, '⊃'),
    "subseteq"           => Token(TK_OPID, '⊆'),
    "supseteq"           => Token(TK_OPID, '⊇'),
    "nsubseteq"          => Token(TK_OPID, '⊈'),
    "nsupseteq"          => Token(TK_OPID, '⊉'),
    "subsetneq"          => Token(TK_OPID, '⊊'),
    "supsetneq"          => Token(TK_OPID, '⊋'),
    "sqsubset"           => Token(TK_OPID, '⊏'),
    "sqsubseteq"         => Token(TK_OPID, '⊑'),
    "sqsupset"           => Token(TK_OPID, '⊐'),
    "sqsupseteq"         => Token(TK_OPID, '⊒'),
    "sqcap"              => Token(TK_OPID, '⊓'),
    "sqcup"              => Token(TK_OPID, '⊔'),
    "setminus"           => Token(TK_OPID, '∖'),
    "partial"            => Token(TK_OPID, '∂'),
    "nabla"              => Token(TK_OPID, '∇'),
    "__final__"  => Token(TK_ID, "__final__")
   )

const unicode_to_optoken = Dict(
    # integrals and big operators
    '∫' => Token(TK_INT, '∫'),
    '∬' => Token(TK_INT, '∬'),
    '∭' => Token(TK_INT, '∭'),
    '∮' => Token(TK_INT, '∮'),
    '∑' => Token(TK_BIGOP, '∑'),
    '∏' => Token(TK_BIGOP, '∏'),
    '∐' => Token(TK_BIGOP, '∐'),
    '⋂' => Token(TK_BIGOP, '⋂'),
    '⋃' => Token(TK_BIGOP, '⋃'),
    '⨆' => Token(TK_BIGOP, '⨆'),
    '⋁' => Token(TK_BIGOP, '⋁'),
    '⋀' => Token(TK_BIGOP, '⋀'),
    '⨀' => Token(TK_BIGOP, '⨀'),
    '⨂' => Token(TK_BIGOP, '⨂'),
    '⨁' => Token(TK_BIGOP, '⨁'),
    '⨄' => Token(TK_BIGOP, '⨄'),
    # operators
    '×' => Token(TK_OPID, '×'),
    '⊕' => Token(TK_OPID, '⊕'),
    '⊖' => Token(TK_OPID, '⊖'),
    '⊗' => Token(TK_OPID, '⊗'),
    '⊘' => Token(TK_OPID, '⊘'),
    '⊙' => Token(TK_OPID, '⊙'),
    '◯' => Token(TK_OPID, '◯'),
    '⨿' => Token(TK_OPID, '⨿'),
    '±' => Token(TK_OPID, '±'),
    '∓' => Token(TK_OPID, '∓'),
    '·' => Token(TK_OPID, '·'),
    '⋯' => Token(TK_OPID, '⋯'),
    '⋮' => Token(TK_OPID, '⋮'),
    '…' => Token(TK_OPID, '…'),
    '⋱' => Token(TK_OPID, '⋱'),
    '∘' => Token(TK_OPID, '∘'),
    '∙' => Token(TK_OPID, '∙'),
    '⋆' => Token(TK_OPID, '⋆'),
    '÷' => Token(TK_OPID, '÷'),
    '¬' => Token(TK_OPID, '¬'),
    '∧' => Token(TK_OPID, '∧'),
    '∨' => Token(TK_OPID, '∨'),
    '∼' => Token(TK_OPID, '∼'),
    '≃' => Token(TK_OPID, '≃'),
    '≁' => Token(TK_OPID, '≁'),
    '≅' => Token(TK_OPID, '≅'),
    '≈' => Token(TK_OPID, '≈'),
    '≠' => Token(TK_OPID, '≠'),
    '≠' => Token(TK_OPID, '≠'),
    '≡' => Token(TK_OPID, '≡'),
    '≢' => Token(TK_OPID, '≢'),
    '≺' => Token(TK_OPID, '≺'),
    '≻' => Token(TK_OPID, '≻'),
    '⪯' => Token(TK_OPID, '⪯'),
    '⪰' => Token(TK_OPID, '⪰'),
    '⊣' => Token(TK_OPID, '⊣'),
    '≍' => Token(TK_OPID, '≍'),
    '≐' => Token(TK_OPID, '≐'),
    '∝' => Token(TK_OPID, '∝'),
    '⊼' => Token(TK_OPID, '⊼'),
    '⋉' => Token(TK_OPID, '⋉'),
    '⋊' => Token(TK_OPID, '⋊'),
    '⋈' => Token(TK_OPID, '⋈'),
    '⊲' => Token(TK_OPID, '⊲'),
    '⊳' => Token(TK_OPID, '⊳'),
    '⊴' => Token(TK_OPID, '⊴'),
    '⊵' => Token(TK_OPID, '⊵'),
    '∨' => Token(TK_OPID, '∨'),
    '⊎' => Token(TK_OPID, '⊎'),
    '∧' => Token(TK_OPID, '∧'),
    '⊻' => Token(TK_OPID, '⊻'),
    '∩' => Token(TK_OPID, '∩'),
    '∪' => Token(TK_OPID, '∪'),
    '∣' => Token(TK_OPID, '∣'),
    '∤' => Token(TK_OPID, '∤'),
    '∥' => Token(TK_OPID, '∥'),
    '⊥' => Token(TK_OPID, '⊥'),
    '∀' => Token(TK_OPID, '∀'),
    '∃' => Token(TK_OPID, '∃'),
    '∄' => Token(TK_OPID, '∄'),
    '<' => Token(TK_OPID, '<'),
    '>' => Token(TK_OPID, '>'),
    '≤' => Token(TK_OPID, '≤'),
    '≥' => Token(TK_OPID, '≥'),
    '≪' => Token(TK_OPID, '≪'),
    '≫' => Token(TK_OPID, '≫'),
    '⪅' => Token(TK_OPID, '⪅'),
    '≲' => Token(TK_OPID, '≲'),
    '⪕' => Token(TK_OPID, '⪕'),
    '⩽' => Token(TK_OPID, '⩽'),
    '≦' => Token(TK_OPID, '≦'),
    '≧' => Token(TK_OPID, '≧'),
    '⩾' => Token(TK_OPID, '⩾'),
    '⪖' => Token(TK_OPID, '⪖'),
    '≳' => Token(TK_OPID, '≳'),
    '⪆' => Token(TK_OPID, '⪆'),
    '≊' => Token(TK_OPID, '≊'),
    '⋖' => Token(TK_OPID, '⋖'),
    '⋘' => Token(TK_OPID, '⋘'),
    '≶' => Token(TK_OPID, '≶'),
    '⋚' => Token(TK_OPID, '⋚'),
    '⪋' => Token(TK_OPID, '⪋'),
    '≑' => Token(TK_OPID, '≑'),
    '←' => Token(TK_OPID, '←'),
    '←' => Token(TK_OPID, '←'),
    '→' => Token(TK_OPID, '→'),
    '→' => Token(TK_OPID, '→'),
    '↚' => Token(TK_OPID, '↚'),
    '↛' => Token(TK_OPID, '↛'),
    '↔' => Token(TK_OPID, '↔'),
    '↮' => Token(TK_OPID, '↮'),
    '⟵' => Token(TK_OPID, '⟵'),
    '⟶' => Token(TK_OPID, '⟶'),
    '⟷' => Token(TK_OPID, '⟷'),
    '⇐' => Token(TK_OPID, '⇐'),
    '⇒' => Token(TK_OPID, '⇒'),
    '⇍' => Token(TK_OPID, '⇍'),
    '⇏' => Token(TK_OPID, '⇏'),
    '↑' => Token(TK_OPID, '↑'),
    '↓' => Token(TK_OPID, '↓'),
    '↕' => Token(TK_OPID, '↕'),
    '∈' => Token(TK_OPID, '∈'),
    '∉' => Token(TK_OPID, '∉'),
    '∉' => Token(TK_OPID, '∉'),
    '⊂' => Token(TK_OPID, '⊂'),
    '⊃' => Token(TK_OPID, '⊃'),
    '⊆' => Token(TK_OPID, '⊆'),
    '⊇' => Token(TK_OPID, '⊇'),
    '⊈' => Token(TK_OPID, '⊈'),
    '⊉' => Token(TK_OPID, '⊉'),
    '⊊' => Token(TK_OPID, '⊊'),
    '⊋' => Token(TK_OPID, '⊋'),
    '⊏' => Token(TK_OPID, '⊏'),
    '⊑' => Token(TK_OPID, '⊑'),
    '⊐' => Token(TK_OPID, '⊐'),
    '⊒' => Token(TK_OPID, '⊒'),
    '⊓' => Token(TK_OPID, '⊓'),
    '⊔' => Token(TK_OPID, '⊔'),
    '∖' => Token(TK_OPID, '∖'),
    '∂' => Token(TK_OPID, '∂'),
    '∇' => Token(TK_OPID, '∇')
)

const _to_mathbf = Dict(
    "A" => "𝐀",  # mathematical bold capital a
    "B" => "𝐁",  # mathematical bold capital b
    "C" => "𝐂",  # mathematical bold capital c
    "D" => "𝐃",  # mathematical bold capital d
    "E" => "𝐄",  # mathematical bold capital e
    "F" => "𝐅",  # mathematical bold capital f
    "G" => "𝐆",  # mathematical bold capital g
    "H" => "𝐇",  # mathematical bold capital h
    "I" => "𝐈",  # mathematical bold capital i
    "J" => "𝐉",  # mathematical bold capital j
    "K" => "𝐊",  # mathematical bold capital k
    "L" => "𝐋",  # mathematical bold capital l
    "M" => "𝐌",  # mathematical bold capital m
    "N" => "𝐍",  # mathematical bold capital n
    "O" => "𝐎",  # mathematical bold capital o
    "P" => "𝐏",  # mathematical bold capital p
    "Q" => "𝐐",  # mathematical bold capital q
    "R" => "𝐑",  # mathematical bold capital r
    "S" => "𝐒",  # mathematical bold capital s
    "T" => "𝐓",  # mathematical bold capital t
    "U" => "𝐔",  # mathematical bold capital u
    "V" => "𝐕",  # mathematical bold capital v
    "W" => "𝐖",  # mathematical bold capital w
    "X" => "𝐗",  # mathematical bold capital x
    "Y" => "𝐘",  # mathematical bold capital y
    "Z" => "𝐙",  # mathematical bold capital z
    "a" => "𝐚",  # mathematical bold small a
    "b" => "𝐛",  # mathematical bold small b
    "c" => "𝐜",  # mathematical bold small c
    "d" => "𝐝",  # mathematical bold small d
    "e" => "𝐞",  # mathematical bold small e
    "f" => "𝐟",  # mathematical bold small f
    "g" => "𝐠",  # mathematical bold small g
    "h" => "𝐡",  # mathematical bold small h
    "i" => "𝐢",  # mathematical bold small i
    "j" => "𝐣",  # mathematical bold small j
    "k" => "𝐤",  # mathematical bold small k
    "l" => "𝐥",  # mathematical bold small l
    "m" => "𝐦",  # mathematical bold small m
    "n" => "𝐧",  # mathematical bold small n
    "o" => "𝐨",  # mathematical bold small o
    "p" => "𝐩",  # mathematical bold small p
    "q" => "𝐪",  # mathematical bold small q
    "r" => "𝐫",  # mathematical bold small r
    "s" => "𝐬",  # mathematical bold small s
    "t" => "𝐭",  # mathematical bold small t
    "u" => "𝐮",  # mathematical bold small u
    "v" => "𝐯",  # mathematical bold small v
    "w" => "𝐰",  # mathematical bold small w
    "x" => "𝐱",  # mathematical bold small x
    "y" => "𝐲",  # mathematical bold small y
    "z" => "𝐳"   # mathematical bold small z
    )
const _to_mathcal = Dict(
    "A" => "𝒜",  # mathematical script capital a
    "B" => "ℬ",  # mathematical script capital b
    "C" => "𝒞",  # mathematical script capital c
    "D" => "𝒟",  # mathematical script capital d
    "E" => "ℰ",  # mathematical script capital e
    "F" => "ℱ",  # mathematical script capital f
    "G" => "𝒢",  # mathematical script capital g
    "H" => "ℋ",  # mathematical script capital h
    "I" => "ℐ",  # mathematical script capital i
    "J" => "𝒥",  # mathematical script capital j
    "K" => "𝒦",  # mathematical script capital k
    "L" => "ℒ",  # mathematical script capital l
    "M" => "ℳ",  # mathematical script capital m
    "N" => "𝒩",  # mathematical script capital n
    "O" => "𝒪",  # mathematical script capital o
    "P" => "𝒫",  # mathematical script capital p
    "Q" => "𝒬",  # mathematical script capital q
    "R" => "ℛ",  # mathematical script capital r
    "S" => "𝒮",  # mathematical script capital s
    "T" => "𝒯",  # mathematical script capital t
    "U" => "𝒰",  # mathematical script capital u
    "V" => "𝒱",  # mathematical script capital v
    "W" => "𝒲",  # mathematical script capital w
    "X" => "𝒳",  # mathematical script capital x
    "Y" => "𝒴",  # mathematical script capital y
    "Z" => "𝒵",  # mathematical script capital z
    "a" => "𝒶",  # mathematical script small a
    "b" => "𝒷",  # mathematical script small b
    "c" => "𝒸",  # mathematical script small c
    "d" => "𝒹",  # mathematical script small d
    "e" => "ℯ",  # mathematical script small e
    "f" => "𝒻",  # mathematical script small f
    "g" => "ℊ",  # mathematical script small g
    "h" => "𝒽",  # mathematical script small h
    "i" => "𝒾",  # mathematical script small i
    "j" => "𝒿",  # mathematical script small j
    "k" => "𝓀",  # mathematical script small k
    "l" => "𝓁",  # mathematical script small l
    "m" => "𝓂",  # mathematical script small m
    "n" => "𝓃",  # mathematical script small n
    "o" => "ℴ",  # mathematical script small o
    "p" => "𝓅",  # mathematical script small p
    "q" => "𝓆",  # mathematical script small q
    "r" => "𝓇",  # mathematical script small r
    "s" => "𝓈",  # mathematical script small s
    "t" => "𝓉",  # mathematical script small t
    "u" => "𝓊",  # mathematical script small u
    "v" => "𝓋",  # mathematical script small v
    "w" => "𝓌",  # mathematical script small w
    "x" => "𝓍",  # mathematical script small x
    "y" => "𝓎",  # mathematical script small y
    "z" => "𝓏"   # mathematical script small z
    )

const _to_mathfrak = Dict(
    "A" => "𝔄",  # mathematical fraktur capital a
    "B" => "𝔅",  # mathematical fraktur capital b
    "C" => "ℭ",  # mathematical fraktur capital c
    "D" => "𝔇",  # mathematical fraktur capital d
    "E" => "𝔈",  # mathematical fraktur capital e
    "F" => "𝔉",  # mathematical fraktur capital f
    "G" => "𝔊",  # mathematical fraktur capital g
    "H" => "ℌ",  # mathematical fraktur capital h
    "I" => "𝕴",  # mathematical fraktur capital i (bold)
    "J" => "𝔍",  # mathematical fraktur capital j
    "K" => "𝔎",  # mathematical fraktur capital k
    "L" => "𝔏",  # mathematical fraktur capital l
    "M" => "𝔐",  # mathematical fraktur capital m
    "N" => "𝔑",  # mathematical fraktur capital n
    "O" => "𝔒",  # mathematical fraktur capital o
    "P" => "𝔓",  # mathematical fraktur capital p
    "Q" => "𝔔",  # mathematical fraktur capital q
    "R" => "𝕽",  # mathematical fraktur capital r (bold)
    "S" => "𝔖",  # mathematical fraktur capital s
    "T" => "𝔗",  # mathematical fraktur capital t
    "U" => "𝔘",  # mathematical fraktur capital u
    "V" => "𝔙",  # mathematical fraktur capital v
    "W" => "𝔚",  # mathematical fraktur capital w
    "X" => "𝔛",  # mathematical fraktur capital x
    "Y" => "𝔜",  # mathematical fraktur capital y
    "Z" => "ℨ",  # mathematical fraktur capital z
    "a" => "𝔞",  # mathematical fraktur small a
    "b" => "𝔟",  # mathematical fraktur small b
    "c" => "𝔠",  # mathematical fraktur small c
    "d" => "𝔡",  # mathematical fraktur small d
    "e" => "𝔢",  # mathematical fraktur small e
    "f" => "𝔣",  # mathematical fraktur small f
    "g" => "𝔤",  # mathematical fraktur small g
    "h" => "𝔥",  # mathematical fraktur small h
    "i" => "𝔦",  # mathematical fraktur small i
    "j" => "𝔧",  # mathematical fraktur small j
    "k" => "𝔨",  # mathematical fraktur small k
    "l" => "𝔩",  # mathematical fraktur small l
    "m" => "𝔪",  # mathematical fraktur small m
    "n" => "𝔫",  # mathematical fraktur small n
    "o" => "𝔬",  # mathematical fraktur small o
    "p" => "𝔭",  # mathematical fraktur small p
    "q" => "𝔮",  # mathematical fraktur small q
    "r" => "𝔯",  # mathematical fraktur small r
    "s" => "𝔰",  # mathematical fraktur small s
    "t" => "𝔱",  # mathematical fraktur small t
    "u" => "𝔲",  # mathematical fraktur small u
    "v" => "𝔳",  # mathematical fraktur small v
    "w" => "𝔴",  # mathematical fraktur small w
    "x" => "𝔵",  # mathematical fraktur small x
    "y" => "𝔶",  # mathematical fraktur small y
    "z" => "𝔷"   # mathematical fraktur small z
    )
const _to_mathbb = Dict(
    "A" => "𝔸",  # mathematical double-struck capital a
    "B" => "𝔹",  # mathematical double-struck capital b
    "C" => "ℂ",  # mathematical double-struck capital c
    "D" => "𝔻",  # mathematical double-struck capital d
    "E" => "𝔼",  # mathematical double-struck capital e
    "F" => "𝔽",  # mathematical double-struck capital f
    "G" => "𝔾",  # mathematical double-struck capital g
    "H" => "ℍ",  # mathematical double-struck capital h
    "I" => "𝕀",  # mathematical double-struck capital i
    "J" => "𝕁",  # mathematical double-struck capital j
    "K" => "𝕂",  # mathematical double-struck capital k
    "L" => "𝕃",  # mathematical double-struck capital l
    "M" => "𝕄",  # mathematical double-struck capital m
    "N" => "ℕ",  # mathematical double-struck capital o
    "O" => "𝕆",  # mathematical double-struck capital o
    "P" => "ℙ",  # mathematical double-struck capital p
    "Q" => "ℚ",  # mathematical double-struck capital q
    "R" => "ℝ",  # mathematical double-struck capital r
    "S" => "𝕊",  # mathematical double-struck capital s
    "T" => "𝕋",  # mathematical double-struck capital t
    "U" => "𝕌",  # mathematical double-struck capital u
    "V" => "𝕍",  # mathematical double-struck capital v
    "W" => "𝕎",  # mathematical double-struck capital w
    "X" => "𝕏",  # mathematical double-struck capital x
    "Y" => "𝕐",  # mathematical double-struck capital y
    "Z" => "ℤ",  # mathematical double-struck capital z
    "a" => "𝕒",  # mathematical double-struck small a
    "b" => "𝕓",  # mathematical double-struck small b
    "c" => "𝕔",  # mathematical double-struck small c
    "d" => "𝕕",  # mathematical double-struck small d
    "e" => "𝕖",  # mathematical double-struck small e
    "f" => "𝕗",  # mathematical double-struck small f
    "g" => "𝕘",  # mathematical double-struck small g
    "h" => "𝕙",  # mathematical double-struck small h
    "i" => "𝕚",  # mathematical double-struck small i
    "j" => "𝕛",  # mathematical double-struck small j
    "k" => "𝕜",  # mathematical double-struck small k
    "l" => "𝕝",  # mathematical double-struck small l
    "m" => "𝕞",  # mathematical double-struck small m
    "n" => "𝕟",  # mathematical double-struck small n
    "o" => "𝕠",  # mathematical double-struck small o
    "p" => "𝕡",  # mathematical double-struck small p
    "q" => "𝕢",  # mathematical double-struck small q
    "r" => "𝕣",  # mathematical double-struck small r
    "s" => "𝕤",  # mathematical double-struck small s
    "t" => "𝕥",  # mathematical double-struck small t
    "u" => "𝕦",  # mathematical double-struck small u
    "v" => "𝕧",  # mathematical double-struck small v
    "w" => "𝕨",  # mathematical double-struck small w
    "x" => "𝕩",  # mathematical double-struck small x
    "y" => "𝕪",  # mathematical double-struck small y
    "z" => "𝕫"   # mathematical double-struck small z
    )

const LETTER_TRANSLATION = Dict(
    "mathbf"    => _to_mathbf,
    "mathcal"   => _to_mathcal,
    "mathbb"    => _to_mathbb,
    "mathfrak"  => _to_mathfrak
    )

# this dictionary is for printing token names in debug
const token_names = Dict(
    TK_ID           =>   "TK_ID         ",
    TK_NUM          =>   "TK_NUM        ",
    #TK_SYM         =>   "TK_SYM        ",
    TK_FUN          =>   "TK_FUN        ",
    #TK_CMD         =>   "TK_CMD        ",
    TK_ENV          =>   "TK_ENV        ",
    TK_OPID         =>   "TK_OPID       ",
    #TK_OPID2       =>   "TK_OPID2      ",
    TK_LIM          =>   "TK_LIM        ",
    TK_INT          =>   "TK_INT        ",
    TK_BIGOP        =>   "TK_BIGOP      ",
    TK_LBRACE       =>   "TK_LBRACE     ",
    TK_RBRACE       =>   "TK_RBRACE     ",
    TK_SUB          =>   "TK_SUB        ",
    TK_SUP          =>   "TK_SUP        ",
    TK_AMP          =>   "TK_AMP        ",
    TK_NEWLINE      =>   "TK_NEWLINE    ",
    TK_TEXT         =>   "TK_TEXT       ",
    TK_SQRT         =>   "TK_SQRT       ",
    TK_FRAC         =>   "TK_FRAC       ",
    TK_LEFT         =>   "TK_LEFT       ",
    TK_RIGHT        =>   "TK_RIGHT      ",
    TK_MIDDLE       =>   "TK_MIDDLE     ",
    TK_BEGIN        =>   "TK_BEGIN      ",
    TK_END          =>   "TK_END        ",
    TK_BINOM        =>   "TK_BINOM      ",
    TK_OVERSET      =>   "TK_OVERSET    ",
    TK_UNDERSET     =>   "TK_UNDERSET   ",
    TK_OVERBRACE    =>   "TK_OVERBRACE  ",
    TK_UNDERBRACE   =>   "TK_UNDERBRACE ",
    TK_OVERACCENT   =>   "TK_OVERACCENT ",
    TK_UNDERACCENT  =>   "TK_UNDERACCENT",
    TK_SPACE        =>   "TK_SPACE      ",
    TK_BIG          =>   "TK_BIG        ",
    TK_PAREN        =>   "TK_PAREN      ",
    TK_ENV_NAME     =>   "TK_ENV_NAME   ",
    TK_ENV_ATTR     =>   "TK_ENV_ATTR   ",
    TK_OPNAME       =>   "TK_OPNAME     ",
    TK_STYLE        =>   "TK_STYLE      ",
    TK_IGSTYLE      =>   "TK_IGSTYLE    ",
    TK_UNKNOWN      =>   "TK_UNKNOWN    "
   )


using GPUifyLoops
include("../../src/DGmethods/DGBalanceLawDiscretizations_kernels.jl")
using Random
using StaticArrays

# From src/Mesh/Grids.jl
"""
    mappings(N, elemtoelem, elemtoface, elemtoordr)

This function takes in a polynomial order `N` and parts of a topology (as
returned from `connectmesh`) and returns index mappings for the element surface
flux computation.  The returned `Tuple` contains:

 - `vmapM` an array of linear indices into the volume degrees of freedom where
   `vmapM[:,f,e]` are the degrees of freedom indices for face `f` of element
    `e`.

 - `vmapP` an array of linear indices into the volume degrees of freedom where
   `vmapP[:,f,e]` are the degrees of freedom indices for the face neighboring
   face `f` of element `e`.
"""
function mappings(N, elemtoelem, elemtoface, elemtoordr)
  nface, nelem = size(elemtoelem)

  d = div(nface, 2)
  Np, Nfp = (N+1)^d, (N+1)^(d-1)

  p = reshape(1:Np, ntuple(j->N+1, d))
  fd(f) =   div(f-1,2)+1
  fe(f) = N*mod(f-1,2)+1
  fmask = hcat((p[ntuple(j->(j==fd(f)) ? (fe(f):fe(f)) : (:), d)...][:]
                for f=1:nface)...)
  inds = LinearIndices(ntuple(j->N+1, d-1))

  vmapM = similar(elemtoelem, Nfp, nface, nelem)
  vmapP = similar(elemtoelem, Nfp, nface, nelem)

  for e1 = 1:nelem, f1 = 1:nface
    e2 = elemtoelem[f1,e1]
    f2 = elemtoface[f1,e1]
    o2 = elemtoordr[f1,e1]

    vmapM[:,f1,e1] .= Np*(e1-1) .+ fmask[:,f1]

    if o2 == 1
      vmapP[:,f1,e1] .= Np*(e2-1) .+ fmask[:,f2]
    elseif d == 3 && o2 == 3
      n = 1
      @inbounds for j = 1:N+1, i = N+1:-1:1
        vmapP[n,f1,e1] = Np*(e2-1) + fmask[inds[i,j],f2]
        n+=1
      end
    else
      error("Orientation '$o2' with dim '$d' not supported yet")
    end
  end

  (vmapM, vmapP)
end

function DGProfiler(ArrayType, DFloat, dim, nelem, N, nstate, flux!,
                    numerical_flux!;
                    nauxstate = 0, source! = nothing,
                    numerical_boundary_flux! = nothing,
                    nviscstate = 0,
                    ngradstate = 0,
                    states_grad = (),
                    viscous_transform! = nothing,
                    gradient_transform! = nothing,
                    viscous_penalty! = nothing,
                    viscous_boundary_penalty! = nothing,
                    t = zero(DFloat),
                    rnd = MersenneTwister(0),
                    numerical_boundary_faces = 0,
                    stateoffset = ())

  # Generate a random mapping
  nface = 2dim

  elemtoelem = zeros(Int, nface, nelem)
  elemtobndy = zeros(Int, nface, nelem)
  elemtoface = zeros(Int, nface, nelem)
  elemtoordr = ones(Int, nface, nelem)

  Faces = Set(1:nelem*nface)

  for b = 1:numerical_boundary_faces
    gf = rand(Faces)
    pop!(Faces, gf)
    e = div(gf-1, nface) + 1
    f = ((gf-1) % nface) + 1

    @assert elemtoelem[f, e] == 0
    @assert elemtoface[f, e] == 0
    @assert elemtobndy[f, e] == 0

    elemtoelem[f, e] = e
    elemtoface[f, e] = f
    elemtobndy[f, e] = 1
  end

  for gf1 in Faces
    pop!(Faces, gf1)
    e1 = div(gf1-1, nface) + 1
    f1 = ((gf1-1) % nface) + 1

    @assert elemtoelem[f1, e1] == 0
    @assert elemtoface[f1, e1] == 0
    @assert elemtobndy[f1, e1] == 0

    gf2 = rand(rnd, Faces)
    pop!(Faces, gf2)
    e2 = div(gf2-1, nface) + 1
    f2 = ((gf2-1) % nface) + 1

    @assert elemtoelem[f2, e2] == 0
    @assert elemtoface[f2, e2] == 0
    @assert elemtobndy[f2, e2] == 0

    elemtoelem[f1, e1], elemtoelem[f2, e2] = e2, e1
    elemtoface[f1, e1], elemtoface[f2, e2] = f2, f1
  end
  vmapM, vmapP = mappings(N, elemtoelem, elemtoface, elemtoordr)

  # Generate random geometry terms and solutions
  Nq = N + 1
  Nqk = dim == 3 ? N + 1 : 1
  Np = Nq * Nq * Nqk
  Q = rand(rnd, DFloat, Np, nstate, nelem)
  for (s, offset) in stateoffset
    Q[:, s, :] .+= offset
  end
  Qvisc = rand(rnd, DFloat, Np, nviscstate, nelem)
  auxstate = rand(rnd, DFloat, Np, nauxstate, nelem)
  vgeo = rand(rnd, DFloat, Np, _nvgeo, nelem)
  sgeo = rand(rnd, DFloat, _nsgeo, Nq^(dim-1), nface, nelem)
  D = rand(rnd, DFloat, Nq, Nq)
  rhs = similar(Q)
  rhs .= 0

  # Make sure the entries of the mass matrix satisfy the inverse relation
  vgeo[:, _MJ, :] .+= 3
  vgeo[:, _MJI, :] .= 1 ./ vgeo[:, _MJ, :]

  (D                         = ArrayType(D),
   Q                         = ArrayType(Q),
   Qvisc                     = ArrayType(Qvisc),
   auxstate                  = ArrayType(auxstate),
   elemtobndy                = ArrayType(elemtobndy),
   rhs                       = ArrayType(rhs),
   sgeo                      = ArrayType(sgeo),
   vgeo                      = ArrayType(vgeo),
   vmapM                     = ArrayType(vmapM),
   vmapP                     = ArrayType(vmapP),
   N                         = N,
   dim                       = dim,
   elems                     = 1:nelem,
   flux!                     = flux!,
   gradient_transform!       = gradient_transform!,
   nauxstate                 = nauxstate,
   ngradstate                = ngradstate,
   nstate                    = nstate,
   numerical_boundary_flux!  = numerical_boundary_flux!,
   numerical_flux!           = numerical_flux!,
   nviscstate                = nviscstate,
   source!                   = source!,
   states_grad               = states_grad,
   t                         = t,
   viscous_boundary_penalty! = viscous_boundary_penalty!,
   viscous_penalty!          = viscous_penalty!,
   viscous_transform!        = viscous_transform!,
   device                    = ArrayType == Array ? CPU() : CUDA())
end

function volumerhs!(dg)
  Nq = dg.N+1
  Nqk = dg.dim == 2 ? 1 : Nq
  DEV = dg.device
  nelem = length(dg.elems)

  @launch(DEV, threads=(Nq, Nq, Nqk), blocks=nelem,
          volumerhs!(Val(dg.dim), Val(dg.N), Val(dg.nstate), Val(dg.nviscstate),
                     Val(dg.nauxstate), dg.flux!, dg.source!, dg.rhs, dg.Q,
                     dg.Qvisc, dg.auxstate, dg.vgeo, dg.t, dg.D, dg.elems))
end

function facerhs!(dg)
  DEV = dg.device
  Nq = dg.N+1
  Nqk = dg.dim == 2 ? 1 : Nq
  Nfp = Nq * Nqk
  nelem = length(dg.elems)

  @launch(DEV, threads=Nfp, blocks=nelem,
          facerhs!(Val(dg.dim), Val(dg.N), Val(dg.nstate), Val(dg.nviscstate),
                   Val(dg.nauxstate), dg.numerical_flux!,
                   dg.numerical_boundary_flux!, dg.rhs, dg.Q, dg.Qvisc,
                   dg.auxstate, dg.vgeo, dg.sgeo, dg.t, dg.vmapM, dg.vmapP,
                   dg.elemtobndy, dg.elems))
end

function volumeviscterms!(dg)
  Nq = dg.N+1
  Nqk = dg.dim == 2 ? 1 : Nq
  DEV = dg.device
  nelem = length(dg.elems)

  @launch(DEV, threads=(Nq, Nq, Nqk), blocks=nelem,
          volumeviscterms!(Val(dg.dim), Val(dg.N), Val(dg.nstate),
                           Val(dg.states_grad), Val(dg.ngradstate),
                           Val(dg.nviscstate), Val(dg.nauxstate),
                           dg.viscous_transform!, dg.gradient_transform!, dg.Q,
                           dg.Qvisc, dg.auxstate, dg.vgeo, dg.t, dg.D,
                           dg.elems))
end

function faceviscterms!(dg)
  DEV = dg.device
  Nq = dg.N+1
  Nqk = dg.dim == 2 ? 1 : Nq
  Nfp = Nq * Nqk
  nelem = length(dg.elems)

  @launch(DEV, threads=Nfp, blocks=nelem,
          faceviscterms!(Val(dg.dim), Val(dg.N), Val(dg.nstate),
                         Val(dg.states_grad), Val(dg.ngradstate),
                         Val(dg.nviscstate), Val(dg.nauxstate),
                         dg.viscous_penalty!, dg.viscous_boundary_penalty!,
                         dg.gradient_transform!, dg.Q, dg.Qvisc, dg.auxstate,
                         dg.vgeo, dg.sgeo, dg.t, dg.vmapM, dg.vmapP,
                         dg.elemtobndy, dg.elems))
end

function runall!(dg)
  volumerhs!(dg)
  facerhs!(dg)
  if dg.nviscstate > 0
    volumeviscterms!(dg)
    faceviscterms!(dg)
  end
end


using HetaSimulator, Plots

model = load_jlmodel("../dist/julia/model.jl")

res = sim(model; tspan = (1,340))

plotd = plot(res; vars = [:P_p, :A_p, :O_p])
savefig(plotd, "julia-plot-1.png")

plotd = plot(res; vars = [:PFM_p])
savefig(plotd, "julia-plot-2.png")


using MuZero

#=
@boardgame Tac (3,3)
@actions 1:9
@pieces [
    (name=:X, icon=(color="black")),
    (name=:O, icon=(color="red"))
]
=#

struct TacState <: AbstractBoardState
    board_state::BoardState{9}
    to_play::Integer
end

TacState() = TacState(BoardState{9}(), PLAYER_1)
TacState(encoded::String) = decode(TacState, encoded)
TacState(::Vector{Nothing}, to_play::Integer) = TacState()
TacState(pieces::Vector{Piece}, to_play::Integer) = init_state(TacState, pieces, to_play)
TacState(pieces::Vector{Union{Nothing, Piece}}, to_play::Integer) = init_state(TacState, pieces, to_play)

#@todo change this to player "A" and "B"
const PLAYER_1_PIECES = [Piece(:X, "X", icon_symbol="X", icon_color="black", icon_size=60)]
const PLAYER_2_PIECES = [Piece(:O, "O", icon_symbol="O", icon_color="red", icon_size=60)]
const ALL_PIECES = [PLAYER_1_PIECES..., PLAYER_2_PIECES...]

MuZero.get_pieces(::Type{TacState}) = ALL_PIECES
MuZero.num_rows(::Type{TacState}) = 3
MuZero.num_cols(::Type{TacState}) = 3
MuZero.image_scale(::Type{TacState}) = 750
MuZero.image_title_offset(::Type{TacState}) = (75,45)
MuZero.actions(::Type{TacState}) = collect(1:9)
MuZero.num_players(::Type{TacState}) = 2

struct TacStateSVG
    s::TacState
end

struct Tac <: AbstractBoardGame
    history::Vector{<:Integer}
    states::Vector{TacState}
    rewards::Vector{Float32}
    function Tac(history, states, rewards)
        @assert length(history) == length(rewards)
        @assert length(states) == (length(history) + 1)
        new(history, states, rewards)
    end
end

function Tac()
    Tac(
        Vector{Int8}(),
        [TacState()],
        Vector{Float32}(),
    )
end

Tac(actions::Vector{<:Integer}) = init_game(Tac, actions)

MuZero.getstatetype(::Type{Tac}) = TacState

################################################

# @todo change to get_coord(::TacState, ::AbstractAction)
function MuZero.get_coord(s::TacState, action::Integer)
    action_to_coord = (
        a7=(1,1), a8=(1,2), a9=(1,3),
        a4=(2,1), a5=(2,2), a6=(2,3),
        a1=(3,1), a2=(3,2), a3=(3,3))
    action_to_coord[Symbol("a$action")]
end

# @todo simplify this to get_marker(::Tac, ::AbstractAction)
# @todo break out into get_piece(::Tac, ::AbstractAction)
function MuZero.get_marker(s::TacState, player::Integer, action::Integer)
    row_idx, col_idx = get_coord(s, action)
    name = player == PLAYER_1 ? :X : :O
    piece = get_piece(s, name)
    Marker(row_idx, col_idx, piece)
end

function MuZero.get_winner(s::TacState)
    for player in [PLAYER_1, PLAYER_2]
        target = player * ones(UInt8, 3)
        for row_idx in 1:num_rows(s)
            if s[row_idx, 1:3] == target
                return player
            end
        end
        for col_idx in 1:num_cols(s)
            if s[1:3, col_idx] == target
                return player
            end
        end
        if s[1, 1] == s[2, 2] == s[3, 3] == player
            return player
        end
        if s[3, 1] == s[2, 2] == s[1, 3] == player
            return player
        end
    end
    nothing
end

function MuZero.legal_actions(s::TacState)
    if game_is_won(s)
        return []
    end
    filter(a->begin
        coord = get_coord(s, a)
        iszero(s[coord...])
    end, actions(s))
end

function MuZero.getconfig(::Type{Tac})
    config = make_board_game_config(
        Tac,
        network_type=Incremental,
        max_moves=9,
        root_dirichlet_alpha=0.3,
        action_space_size=9, # Number of all possible actions in the game
        support_size = 10, # Value and reward are scaled (with almost sqrt) and encoded on a vector with a range of -support_size to support_size. Choose it so that support_size <= sqrt(max(abs(discounted reward)))
        observation_shape = (3, 3, 3),
        stacked_observations = 0, # Number of previous observations and previous actions to add to the current observation
        hidden_state_size = 32, # The 1-dimensional size of the encoded game state
        checkpoint_interval = 100, #  Number of training steps before using the model for self-playing
        num_actors = 3,
        weight_decay = 1e-3,
        lr_init=0.002f0,
        save_interval = 10000,
        num_unroll_steps=1,
    )
    config.batch_size = 10
    config.num_simulations = 10
    config.training_steps = 12000
    config
end


module IrisBase

include("Bravais.jl")
using .Bravais
export BravaisLattice

include("CoordinateSystems.jl")
using .CoordinateSystems
export Polar,
Cartesian

include("BoundaryConditions.jl")
using .BoundaryConditions
export PML,
cPML,
noBL,
DirichletBC,
NeumannBC,
RobinBC,
FloquetBC,
MatchedBC

include("DielectricFunctions.jl")
using .DielectricFunctions

include("Shapes.jl")
using .Shapes
export Circle,
Ellipse,
Square,
Rectangle,
Parallelogram,
DeformedDisk,
Universe

include("DifferentialOperators.jl")
using .DifferentialOperators
export laplacian

include("Dispersions.jl")
using .Dispersions

include("DefineSimulation.jl")
using .DefineSimulation
export Domain,
System,
Discretization,
Boundary,
Channels,
Scattering,
Simulation

include("ConstructionTools.jl")
using .ConstructionTools
export Subdomains,
add_subdomain,
build_domain,
build_pc_domain,
line_defect,
site_defect,
add_halfspace_waveguide,
add_halfspace_waveguides,
add_planar_waveguide,
add_planar_waveguides,
add_pc_waveguide,
add_pc_waveguides

include("Bound.jl")
using .Bound

end # module


using Multilane
using StatsBase
using MCTS
using JLD
using POMCP

dpws = DPWSolver(depth=20,
                 n_iterations=500,
                 exploration_constant=50.0,
                 k_state=4.0,
                 alpha_state=1/8,
                 rollout_solver=SimpleSolver())

pomcps = POMCPDPWSolver(
    eps=0.001,
    max_depth=20,
    c=50.0,
    tree_queries=2500,
    k_observation=4.0,
    alpha_observation=1/8,
    rollout_solver=SimpleSolver()
)

solvers = Dict{String, Any}(
    "dpw"=>dpws,
    "assume_normal"=>SingleBehaviorSolver(dpws, Multilane.NORMAL),
    "pomcp"=>MLPOMDPSolver(pomcps, BehaviorRootUpdaterStub(0.05))
)

curve = TestSet(lambda=[0.1, 1.0, 2.15, 4.64, 10., 21.5, 46.4], brake_threshold=2.5, N=500)
# curve = TestSet(lambda=[46.4], N=1)

tests = [
    TestSet(curve, solver_key="dpw", behaviors="9_agents", key="dpw"),
    TestSet(curve, solver_key="assume_normal", behaviors="9_agents", key="assume_normal"),
    TestSet(curve, solver_key="pomcp", behaviors="9_agents", key="pomcp")
]

objects = gen_initials(tests, generate_physical=true)

@show objects["param_table"]
objects["solvers"] = solvers

# files = sbatch_spawn(tests, objects,
#                      batch_size=50,
#                      time_per_batch="1:00:00",
#                      submit_command="sbatch",
#                      template_name="sherlock.sh")

results = evaluate(tests, objects, parallel=true)

filename = string("results_", Dates.format(Dates.now(),"u_d_HH_MM"), ".jld")
results["histories"] = nothing
save(filename, results)
println("results saved to $filename")


#=
Author: Miles Cochran-Branson
Date: 2/15/22
=#

module LUComptonScatter

#dependencies
import LinearAlgebra
import DataFrames
import Findpeaks
import Smoothers
import Interpolations

#functions for peak finding and analysis
include("analysisFuncs.jl")
export expected_energy
export read_Txt
export get_xy_data
export select_part
export weighted_mean
export find_peak_ends
export peak_parameters
export get_peak_value
export gauss
export fit_to_gauss

#functions for data fitting
include("chiSquredFitting.jl")
export χ²
export compton
export scan
export scan_box

end # module


include("vi_optimizers.jl")
include("analytic.jl")
include("analyticVI.jl")
include("numericalVI.jl")
include("sampling.jl")
include("optimisers.jl")

export RobbinsMonro, ALRSVI

post_process!(::AbstractGPModel) = nothing

# Utils to iterate over inference objects
Base.length(::AbstractInference) = 1
Base.size(::AbstractInference) = (1,)

Base.iterate(i::AbstractInference) = (i, nothing)
Base.iterate(::AbstractInference, ::Any) = nothing

is_stochastic(i::AbstractInference) = i.stoch

## Multiple accessors
conv_crit(i::AbstractInference) = i.ϵ

## Conversion from natural to standard distribution parameters ##
function global_update!(gp::AbstractLatent)
    gp.post.Σ .= -inv(nat2(gp)) / 2
    return gp.post.μ .= cov(gp) * nat1(gp)
end

## For the online case, the size may vary and inplace updates are note valid
function global_update!(gp::OnlineVarLatent)
    gp.post.Σ = -inv(nat2(gp)) / 2
    return gp.post.μ = cov(gp) * nat1(gp)
end

## Default function for getting a view on y
batchsize(inf::AbstractInference) = inf.batchsize
set_batchsize!(inf::AbstractInference, n::Int) = inf.batchsize = n

ρ(inf::AbstractInference) = inf.ρ
ρ(m::AbstractGPModel) = ρ(inference(m))
set_ρ!(inf::AbstractInference, ρ) = inf.ρ = ρ
set_ρ!(m::AbstractGPModel, ρ) = set_ρ!(inference(m), ρ)

setHPupdated!(inf::AbstractInference, status::Bool) = inf.HyperParametersUpdated = status
isHPupdated(inf::AbstractInference) = inf.HyperParametersUpdated

n_iter(inf::AbstractInference) = inf.n_iter

opt(::AbstractInference) = nothing
opt(i::VariationalInference) = i.vi_opt
opt(i::SamplingInference) = i.opt


"""
    selectNodes(layer::Union{Vector{RelaxedSequencingNode},Vector{RelaxedStateSequencingNode}}, assignment::T)where{T<:Real}

Find all the nodes in a layer where some value is in someDown, but not in allDown
Return the subset of nodes

# Arguments
- `layer::Union{Vector{RelaxedSequencingNode},Vector{RelaxedStateSequencingNode}}`: the layer to search
- `assignment::T`:the value to check
"""
function selectNodes(layer::Union{Vector{RelaxedSequencingNode},Vector{RelaxedStateSequencingNode}}, assignment::T)where{T<:Real}
    nodes = typeof(layer)()
    if nodes isa Vector{RelaxedSequencingNode}
        for node in layer
            if node.state!=assignment && node.someDown[assignment] && !node.allDown[assignment]
                for parent in getParents(node)
                    if parent.allDown[assignment]
                        push!(nodes, node)
                        break
                    end
                end
            end
        end
    else
        for node in layer
            if node.someDown[assignment] && !node.allDown[assignment]
                if !node.state[assignment]
                    for parent in getParents(node)
                        if parent.allDown[assignment]
                            push!(nodes, node)
                            break
                        end
                    end
                else
                    push!(nodes, node)
                end
            end
        end
    end
    return nodes
end

"""
    splitNode!(node::Union{RelaxedSequencingNode, RelaxedStateSequencingNode}, assignment::U, model::SequencingModel) where{U<:Real}
Split a node during relaxation; updates all the down related variables, but not the up ones

# Arguments
- `node::Union{RelaxedSequencingNode, RelaxedStateSequencingNode}`: the node to split
- `assignment::U`: the equivalency class being created
- `model::SequencingModel`: the sequencing problem being evaluated
"""
function splitNode!(node::Union{RelaxedSequencingNode, RelaxedStateSequencingNode}, assignment::U, model::SequencingModel) where{U<:Real}
    if node isa RelaxedSequencingNode
        newNode = RelaxedSequencingNode(
            node.length, #bestPath
            node.state, #current state
            copy(node.allUp), #allUp
            BitVector(), #allDown
            copy(node.someUp), #someUp
            BitVector(), #someDown
            node.lengthToRoot, #to root
            node.lengthToTerminal, #to terminal
            Vector{Int}(),
            node.exact, #exact
            Vector{ExactArc}(), #arcsin
            Vector{ExactArc}() #arcsout
        )
    else
        newNode = RelaxedStateSequencingNode(
            node.length, #bestPath
            copy(node.state), #current state
            copy(node.allUp), #allUp
            BitVector(), #allDown
            copy(node.someUp), #someUp
            BitVector(), #someDown
            node.lengthToRoot, #to root
            node.lengthToTerminal, #to terminal
            Vector{Int}(),
            node.exact, #exact
            Vector{SimpleArc}(), #arcsin
            Vector{SimpleArc}() #arcsout
        )
    end

    #add new arcs out
    for arc in node.arcsOut
        if node isa RelaxedSequencingNode
            newArc = ExactArc(newNode, arc.destination, arc.assignment, arc.value)
        else
            newArc = SimpleArc(newNode, arc.destination, arc.assignment, arc.value)
        end
        push!(newNode.arcsOut, newArc)
        push!(arc.destination.arcsIn, newArc)
    end

    #redirect each incoming arc where it should go
    if node isa RelaxedSequencingNode
        toRemove = Vector{ExactArc}()
    else
        toRemove = Vector{SimpleArc}()
    end
    for arc in node.arcsIn
        if node isa RelaxedSequencingNode
            if arc.origin.allDown[assignment]
                arcIn = ExactArc(arc.origin, newNode, arc.assignment, arc.value)
                push!(newNode.arcsIn, arcIn)
                push!(arc.origin.arcsOut, arcIn)
                push!(toRemove, arc)
            end
        else
            if arc.origin.allDown[assignment] || arc.assignment == assignment
                arcIn = SimpleArc(arc.origin, newNode, arc.assignment, arc.value)
                push!(newNode.arcsIn, arcIn)
                push!(arc.origin.arcsOut, arcIn)
                push!(toRemove, arc)
            end
        end
    end
    deleteArcs!(toRemove)

    updateNodeDownVariables!(node,model)
    updateNodeDownVariables!(newNode,model)

    return newNode
end

"""
    filterArcs!(arcs::Union{Vector{ExactArc},Vector{SimpleArc}},model::SequencingModel;bestKnownValue::Union{Nothing, U}=nothing,rrbSetting::Union{RRBSetting,Nothing}=nothing, rrbPacket=nothing)where{U<:Real}
Remove arcs that cannot contain the optimal solution

# Arguments
- `arcs::Union{Vector{ExactArc},Vector{SimpleArc}}`: the list of arcs to check
- `model::SequencingModel`: the sequencing problem being evaluated
- `bestKnownValue::Union{Nothing, U}`: the best known solution value
- `rrbSetting::Union{RRBSetting,Nothing}`:The rrb setting to use
- `rrbPacket`:The rrb pre calculated packet
"""
function filterArcs!(arcs::Union{Vector{ExactArc},Vector{SimpleArc}},model::SequencingModel;bestKnownValue::Union{Nothing, U}=nothing,rrbSetting::Union{RRBSetting,Nothing}=nothing, rrbPacket=nothing)where{U<:Real}
    toFilter = typeof(arcs)()
    for i in 1:length(arcs)
        if filterArcRules(arcs[i],model,bestKnownValue=bestKnownValue, rrbSetting=rrbSetting, rrbPacket=rrbPacket)
            push!(toFilter, arcs[i])
        end
    end
    for arc in toFilter
        deleteArc!(arc)
    end
end

"""
    filterArcRules(arc::Union{ExactArc,SimpleArc},model::SequencingModel;bestKnownValue::Union{Nothing, U}=nothing,rrbSetting::Union{RRBSetting,Nothing}=nothing, rrbPacket=nothing)where{U<:Real}
Returns true if an arc cannot contain the optinal solution, returns false otherwise

# Arguments
- `arcs::Union{ExactArc,SimpleArc}`: the  arc to check
- `model::SequencingModel`: the sequencing problem being evaluated
- `bestKnownValue::Union{Nothing, U}`: the best known solution value
- `rrbSetting::Union{RRBSetting,Nothing}`:The rrb setting to use
- `rrbPacket`:The rrb pre calculated packet
"""
function filterArcRules(arc::Union{ExactArc,SimpleArc},model::SequencingModel;bestKnownValue::Union{Nothing, U}=nothing,rrbSetting::Union{RRBSetting,Nothing}=nothing, rrbPacket=nothing)where{U<:Real}
    #the label  is used in every path to the root
    if arc.origin.allDown[arc.assignment]
        return true
    end
    #the label is used in every path to the terminal
    if arc.destination.allUp[arc.assignment]
        return true
    end
    #the number of remaining decisions matches the number of remaining labels, and this one is in use
    if sum(arc.destination.someUp) == (length(model)-length(arc.destination)) && arc.destination.someUp[arc.assignment]
        return true
    end
    #total available paths passing through the arc is less than total number of decisions
    pathsThrough = arc.origin.someDown .| arc.destination.someUp
    pathsThrough[arc.assignment] = true
    if sum(pathsThrough) < length(model)
        return true
    end
    #if the best feasible solution is better than the best optimal path through the arc
    if !isnothing(bestKnownValue)
        if !isBetterSolutionValue(model, arc.origin.lengthToRoot + arc.destination.lengthToTerminal + arc.value, bestKnownValue)
            return true
        end
    end

    #handle precedence constraints in both directions
    if model.precedenceConstraints
        if haskey(model.precedenceF2P,arc.assignment)
            for val in model.precedenceF2P[arc.assignment]
                if !arc.origin.someDown[val]
                    return true
                end
            end
        end
        if haskey(model.precedenceP2F,arc.assignment)
            for val in model.precedenceP2F[arc.assignment]
                if !arc.destination.someUp[val]
                    return true
                end
            end
        end
    end

    #handle rrb
    if !isnothing(rrbSetting) && !isnothing(bestKnownValue) && length(arc.destination)!=length(model)
        if isBetterSolutionValue(model, bestKnownValue, sequencingRRAB(rrbSetting, arc, rrbPacket))
            return true
        end
    end
    return false
end

"""
    bottomUpUpdate(model::SequencingModel, dd::Union{Vector{Vector{RelaxedSequencingNode}},Vector{Vector{RelaxedStateSequencingNode}}}, bestKnownValue::T)where(T<:Real)
Update the upwards variables and delete useless arcs

# Arguments
- `model::SequencingModel`: the sequencing problem being evaluated
- `dd::Union{Vector{Vector{RelaxedSequencingNode}},Vector{Vector{RelaxedStateSequencingNode}}}`: the dd to update
- `bestKnownValue::T`: the best known value, used as a label for infeasible arcs
"""
function bottomUpUpdate(model::SequencingModel, dd::Union{Vector{Vector{RelaxedSequencingNode}},Vector{Vector{RelaxedStateSequencingNode}}}, bestKnownValue::T)where(T<:Real)
    #remove garbage nodes
    for i in Iterators.reverse(2:length(dd)-1)
        for node in dd[i]
            if isempty(node.arcsOut)
                deleteArcs!(node.arcsIn)
            end
        end
        filter!(x->length(x.arcsIn)!=0, dd[i])
    end
    filter!(x->length(x.arcsOut)!=0, dd[1])
    filter!(x->length(x.arcsIn)!=0, dd[length(dd)])

    #update values for remaining nodes
    for i in Iterators.reverse(1:length(dd)-1)
        for node in dd[i]
            updateNodeUpVariables!(node,model)
        end
    end
end

"""
    checkFeasibleTerminalSolutions!(model::SequencingModel, finalNode::RelaxedSequencingNode;bestKnownValue::Union{T,Nothing}=nothing,bestKnownSolution::Union{Vector{U},Nothing}=nothing)where{T<:Real,U<:Int}

Get the best feasible soluution into the terminal node, if there is one better than the input one

# Arguments
- `model::SequencingModel`: the sequencing problem being evaluated
- `finalNode::RelaxedSequencingNode`: the terminal node
- `bestKnownValue::Union{T,Nothing}`: the value of the best known solution
- `bestKnownSolution::Union{Vector{U},Nothing}`: the best known solution
"""
function checkFeasibleTerminalSolutions!(model::SequencingModel, finalNode::RelaxedSequencingNode;bestKnownValue::Union{T,Nothing}=nothing,bestKnownSolution::Union{Vector{U},Nothing}=nothing)where{T<:Real,U<:Int}
    for arc in finalNode.arcsIn
        #if its better and valid, store it
        if isBetterSolutionValue(model, arc.value+arc.origin.lengthToRoot, bestKnownValue) && checkValidity(model, push!(copy(arc.origin.bestSeq),arc.assignment))
            bestKnownValue = arc.value+arc.origin.lengthToRoot
            bestKnownSolution = push!(copy(arc.origin.bestSeq),arc.assignment)
        end
    end
    return bestKnownSolution, bestKnownValue
end


using TracedSample, StatsBase, Random, Test

@testset "Addressed calls with global RNG" begin

for fn in (sample, wsample)
   Random.seed!(0)
   traced = fn(:addr, [1, 2, 3], weights([1, 2, 3]))
   Random.seed!(0)
   untraced = fn([1, 2, 3], weights([1, 2, 3]))
   @test traced == untraced

   Random.seed!(0)
   traced = fn(:addr, [1, 2, 3], weights([1, 2, 3]), 3; replace=false)
   Random.seed!(0)
   untraced = fn([1, 2, 3], weights([1, 2, 3]), 3; replace=false)
   @test traced == untraced
end

for fn in (sample!, wsample!)
   Random.seed!(0)
   traced = fn(:addr, [1, 2, 3], weights([1, 2, 3]), zeros(5))
   Random.seed!(0)
   untraced = fn([1, 2, 3], weights([1, 2, 3]), zeros(5))
   @test traced == untraced
end

end

@testset "Addressed calls with custom RNG" begin

rng = MersenneTwister()

for fn in (sample, wsample)
   Random.seed!(rng, 42)
   traced = fn(rng, :addr, [1, 2, 3], weights([1, 2, 3]))
   Random.seed!(rng, 42)
   untraced = fn(rng, [1, 2, 3], weights([1, 2, 3]))
   @test traced == untraced

   Random.seed!(rng, 42)
   traced = fn(rng, :addr, [1, 2, 3], weights([1, 2, 3]), 3; replace=false)
   Random.seed!(rng, 42)
   untraced = fn(rng, [1, 2, 3], weights([1, 2, 3]), 3; replace=false)
   @test traced == untraced
end

for fn in (sample!, wsample!)
   Random.seed!(rng, 42)
   traced = fn(rng, :addr, [1, 2, 3], weights([1, 2, 3]), zeros(5))
   Random.seed!(rng, 42)
   untraced = fn(rng, [1, 2, 3], weights([1, 2, 3]), zeros(5))
   @test traced == untraced
end

end


using Printf
using LinearAlgebra
BLAS.set_num_threads(1)  # 1 thread only

import Tulip

function run_rmp_tulip(finst)
    tlp = Tulip.Model{Float64}()
    Tulip.load_problem!(tlp, finst)

    # Set parameters
    tlp.params.OutputLevel = 1
    tlp.params.Presolve = 0

    # Solve model
    t = @elapsed Tulip.optimize!(tlp)

    println("Solve time: $t")
    println("Barrier iterations: ", Tulip.get_attribute(tlp, Tulip.BarrierIterations()))
    return nothing
end

if abspath(PROGRAM_FILE) == @__FILE__
    run_rmp_tulip(ARGS[1])
end


@nospecialize
"""the view point of the type tag for each term
"""
function tag_extract(points_of_view::Dict{Function,Int})
    viewpoint = points_of_view[tag_extract]

    function and(many)
        @assert !isempty(many)
        ts = getindex.(many, viewpoint)
        t = reduce(typeintersect, ts)
        if t === Base.Bottom
            core_msg = "and patterns require an intersection of $(ts), which seems empty!"
            error(core_msg)
        end
        t
    end

    function or(many)
        ts = getindex.(many, viewpoint)
        Union{ts...}
    end

    function literal(val)
        typeof(val)
    end

    wildcard = Any

    function decons(comp::PComp, _, ns)
        targs = getindex.(ns, viewpoint)
        try
            comp.tcons(targs...)
        catch e
            join(map(repr, targs), ",")
            if e isa MethodError && e.f === comp.tcons
                argstr = join(repeat(String["_"], length(targs)), ", ")
                error("invalid deconstructor $(comp.repr)($(argstr))")
            end
            rethrow()
        end
    end

    guard(_) = Any
    effect(_) = Any
    (
        and = and,
        or = or,
        literal = literal,
        wildcard = wildcard,
        decons = decons,
        guard = guard,
        effect = effect
    )
end
@specialize


""" ReactionSystem constructor """
function Catalyst.ReactionSystem(model::SBML.Model; kwargs...)  # Todo: requires unique parameters (i.e. SBML must have been imported with localParameter promotion in libSBML)
    rxs = mtk_reactions(model)
    u0map = get_u0map(model)
    parammap = get_paramap(model)
    defs = ModelingToolkit._merge(Dict(u0map), Dict(parammap))

    algrules, obsrules, raterules = get_rules(model)
    for o in obsrules
        defs[o.lhs] = substitute(o.rhs, defs)
    end
    constraints_sys = ODESystem(vcat(algrules, raterules, obsrules), Catalyst.DEFAULT_IV; name = gensym(:CONSTRAINTS))

    ReactionSystem(rxs, Catalyst.DEFAULT_IV, first.(u0map), first.(parammap); defaults = defs, name = gensym(:SBML),
        constraints = constraints_sys, kwargs...)
end

""" ODESystem constructor """
function ModelingToolkit.ODESystem(model::SBML.Model; include_zero_odes = false, kwargs...)
    rs = ReactionSystem(model; kwargs...)
    convert(ODESystem, rs; include_zero_odes = include_zero_odes)
end

""" Check if conversion to ReactionSystem is possible """
function checksupport(filename::String)
    not_implemented = ["listOfConstraints", "listOfEvents"]
    sbml = open(filename) do file
        read(file, String)
    end
    for item in not_implemented
        occursin(item, sbml) && throw(ErrorException("SBML models with $item are not yet implemented."))
    end
    occursin("<sbml xmlns:fbc=", sbml) && throw(ErrorException("This model was designed for constrained-based optimisation. Please use COBREXA.jl instead of SBMLToolkit."))
end

""" Convert intensive to extensive mathematical expression """
function to_extensive_math!(model::SBML.Model)
    function conv(x::SBML.MathApply)
        SBML.MathApply(x.fn, SBML.Math[conv(x) for x in x.args])
    end
    function conv(x::SBML.MathIdent)
        x_new = x
        if x.id in keys(model.species)
            specie = model.species[x.id]
            if !specie.only_substance_units
                compartment = model.compartments[specie.compartment]
                if isnothing(compartment.size)
                    @warn "Specie $(x.id) hasOnlySubstanceUnits but its compartment $(compartment.name) has no size. Cannot auto-correct the rate laws $(x.id) is involved in. Please check manually."
                else
                    x_new = SBML.MathApply("/", SBML.Math[x,
                        SBML.MathVal(compartment.size)])
                    specie.only_substance_units = true
                end
            end
        end
        x_new
    end
    conv(x::SBML.MathVal) = x
    conv(x::SBML.MathLambda) =
        throw(DomainError(x, "can't translate lambdas to extensive units"))
    for reaction in values(model.reactions)
        reaction.kinetic_math = conv(reaction.kinetic_math)
    end
    model
end

""" Get dictonary to change types in kineticLaw """
function _get_substitutions(model)
    subsdict = Dict()
    for k in keys(model.species)
        push!(subsdict, Pair(create_var(k), create_var(k, Catalyst.DEFAULT_IV)))
    end
    for k in keys(model.parameters)
        push!(subsdict, Pair(create_var(k), create_param(k)))
    end
    for k in keys(model.compartments)
        push!(subsdict, Pair(create_var(k), create_param(k)))
    end
    subsdict
end

""" Convert SBML.Reaction to MTK.Reaction """
function mtk_reactions(model::SBML.Model)
    subsdict = _get_substitutions(model)
    rxs = []
    for reaction in values(model.reactions)
        extensive_math = SBML.extensive_kinetic_math(
            model, reaction.kinetic_math,
            handle_empty_compartment_size = _ -> 1.0)
        symbolic_math = Num(convert(Num, extensive_math,
            convert_time = (x::SBML.MathTime) -> Catalyst.DEFAULT_IV))

        rstoich = reaction.reactants
        pstoich = reaction.products
        if reaction.reversible
            symbolic_math = getunidirectionalcomponents(symbolic_math)
            kl_fw, kl_rv = [substitute(x, subsdict) for x in symbolic_math]
            reagents = getreagents(rstoich, pstoich, model)
            isnothing(reagents[1]) && isnothing(reagents[2]) && continue
            kl_fw, our = use_rate(kl_fw, reagents[1], reagents[3])
            kl_rv = from_noncombinatoric(kl_rv, reagents[3], our)
            push!(rxs, Catalyst.Reaction(kl_fw, reagents...; only_use_rate = our))

            reagents = getreagents(rstoich, pstoich, model; rev = true)
            kl_rv, our = use_rate(kl_rv, reagents[1], reagents[3])
            kl_rv = from_noncombinatoric(kl_rv, reagents[3], our)
            push!(rxs, Catalyst.Reaction(kl_rv, reagents...; only_use_rate = our))
        else
            kl = substitute(symbolic_math, subsdict)
            reagents = getreagents(rstoich, pstoich, model)
            isnothing(reagents[1]) && isnothing(reagents[2]) && continue
            kl, our = use_rate(kl, reagents[1], reagents[3])
            kl = from_noncombinatoric(kl, reagents[3], our)
            push!(rxs, Catalyst.Reaction(kl, reagents...; only_use_rate = our))
        end
    end
    rxs
end

function from_noncombinatoric(rl::Num, stoich::Union{Vector{<:Real},Nothing}, only_use_rate::Bool)
    if !isnothing(stoich) && !only_use_rate
        coef = 1
        for s in stoich
            isone(s) && continue
            coef *= factorial(s)
        end
        !isone(coef) && (rl *= coef)
    end
    rl
end

""" Get kineticLaw for use in MTK.Reaction """
function use_rate(kl::Num, react::Union{Vector{Num},Nothing}, stoich::Union{Vector{<:Real},Nothing})
    rate_const = getmassaction(kl, react, stoich)
    if !isnan(rate_const)
        kl = rate_const
        our = false
    else
        our = true
    end
    return (kl, our)
end

""" Get reagents """
function getreagents(rstoichdict::Dict{String,<:Real}, pstoichdict::Dict{String,<:Real}, model::SBML.Model; rev = false)
    reactants = Num[]
    products = Num[]
    rstoich = Float64[]
    pstoich = Float64[]

    if rev
        tmp = rstoichdict
        rstoichdict = pstoichdict
        pstoichdict = tmp
    end

    for (k, v) in rstoichdict
        iszero(v) && @error("Stoichiometry of $k must be non-zero")
        push!(reactants, create_var(k, Catalyst.DEFAULT_IV))
        push!(rstoich, v)
        if model.species[k].boundary_condition == true
            push!(products, create_var(k, Catalyst.DEFAULT_IV))
            push!(pstoich, v)
        end
    end

    for (k, v) in pstoichdict
        iszero(v) && @error("Stoichiometry of $k must be non-zero")
        if model.species[k].boundary_condition != true
            push!(products, create_var(k, Catalyst.DEFAULT_IV))
            push!(pstoich, v)
        end
    end

    if (length(reactants) == 0)
        reactants = nothing
        rstoich = nothing
    end
    if (length(products) == 0)
        products = nothing
        pstoich = nothing
    end
    (reactants, products, rstoich, pstoich)
end

""" Infer forward and reverse components of bidirectional kineticLaw """
function getunidirectionalcomponents(bidirectional_math)
    err = "Cannot separate bidirectional kineticLaw `$bidirectional_math` to forward and reverse part. Please make reaction irreversible or rearrange kineticLaw to the form `term1 - term2`."
    bidirectional_math = Symbolics.tosymbol(bidirectional_math)
    bidirectional_math = simplify(bidirectional_math; expand = true)
    if (bidirectional_math isa Union{Real,Symbol}) || (SymbolicUtils.operation(bidirectional_math) != +)
        throw(ErrorException(err))
    end
    terms = SymbolicUtils.arguments(bidirectional_math)
    fw_terms = []
    rv_terms = []
    for term in terms
        if (term isa SymbolicUtils.Mul) && (term.coeff < 0)
            push!(rv_terms, Num(-term))  # PL: @Anand: Perhaps we should to create_var(term) or so?
        else
            push!(fw_terms, Num(term))  # PL: @Anand: Perhaps we should to create_var(term) or so?
        end
    end
    if (length(fw_terms) != 1) || (length(rv_terms) != 1)
        throw(ErrorException(err))
    end
    return (fw_terms[1], rv_terms[1])
end

""" Extract paramap from Model """
function get_paramap(model)
    paramap = Pair{Num,Float64}[]
    for (k, v) in model.parameters
        push!(paramap, Pair(create_param(k), v[1])) # [1] index drops unit
    end
    for (k, v) in model.compartments
        if !isnothing(v.size)
            push!(paramap, Pair(create_param(k), v.size))
        end
    end
    paramap
end

get_u0map(model) = [create_var(k, Catalyst.DEFAULT_IV) => v for (k, v) in SBML.initial_amounts(model, convert_concentrations = true)]
ModelingToolkit.defaults(model::SBML.Model) = ModelingToolkit._merge(get_u0map(model), get_paramap(model))

""" Get rate constant of mass action kineticLaws """
function getmassaction(kl::Num, reactants::Union{Vector{Num},Nothing}, stoich::Union{Vector{<:Real},Nothing})
    function check_args(x::SymbolicUtils.Symbolic{Real})
        for arg in SymbolicUtils.arguments(x)
            if isnan(check_args(arg)) || isequal(arg, Catalyst.DEFAULT_IV)
                return NaN
            end
        end
        return 0
    end
    check_args(x::Term{Real,Nothing}) = NaN  # Variable leaf node
    check_args(x::Sym{Real,Base.ImmutableDict{DataType,Any}}) = 0  # Parameter leaf node
    check_args(x::Real) = 0  # Real leaf node
    check_args(x) = throw(ErrorException("Cannot handle $(typeof(x)) types."))  # Unknow leaf node
    if isnothing(reactants) && isnothing(stoich)
        rate_const = kl
    elseif isnothing(reactants) | isnothing(stoich)
        throw(ErrorException("`reactants` and `stoich` are incosistent: `reactants` are $(reactants) and `stoich` is $(stoich)."))
    else
        rate_const = SymbolicUtils.simplify_fractions(kl / *((.^(reactants, stoich))...))
    end
    isnan(check_args(rate_const.val)) ? NaN : rate_const
end

function create_var(x)
    sym = Symbol(x)
    Symbolics.unwrap(first(@variables $sym))
end
function create_var(x, iv)
    sym = Symbol(x)
    Symbolics.unwrap(first(@variables $sym(iv)))
end
function create_param(x)
    sym = Symbol(x)
    Symbolics.unwrap(first(@parameters $sym))
end

function get_rules(model)
    subsdict = _get_substitutions(model)
    # these three go into `constraints` field of ReactionSystem
    obseqs = Equation[]
    algeqs = Equation[]
    raterules = Equation[]

    rules = model.rules
    for r in rules
        if r isa SBML.AlgebraicRule
            push!(algeqs, 0 ~ convert(Num, r.math))
        elseif r isa SBML.AssignmentRule
            push!(obseqs, assignmentrule_to_obseq(model, r))
        elseif r isa SBML.RateRule
            push!(raterules, raterule_to_diffeq(model, r))
        else
            error()
        end
    end
    algeqs, obseqs, raterules = map(x -> substitute(x, subsdict), (algeqs, obseqs, raterules))
    algeqs, obseqs, raterules
end

function assignmentrule_to_obseq(model, rule)
    if haskey(model.species, rule.id)
        sym = Symbol(rule.id)
        var = Symbolics.unwrap(first(@variables $sym(Catalyst.DEFAULT_IV)))
        assignment = Num(convert(Num, rule.math, convert_time = (x::SBML.MathTime) -> Catalyst.DEFAULT_IV))
        return var ~ assignment
    elseif haskey(model.compartments, rule.id)
        error("not handling setting compartment volumes rn")
    elseif haskey(model.parameters, rule.id)
        error("not handling setting non-constant parameters rn")
    else
        error()
    end
end

function raterule_to_diffeq(model, rule)
    # the rule.id can be a species, speciesRef, compartment, or param. currently not doingthis
    # for now im just doing species
    D = Differential(Catalyst.DEFAULT_IV)
    if haskey(model.species, rule.id)
        sym = Symbol(rule.id)
        var = Symbolics.unwrap(first(@variables $sym(Catalyst.DEFAULT_IV)))
        assignment = Num(convert(Num, rule.math, convert_time = (x::SBML.MathTime) -> Catalyst.DEFAULT_IV))
        return D(var) ~ assignment
    elseif haskey(model.comnpartments, rule.id)
        error("not handling setting compartment volumes rn")
    elseif haskey(model.parameters, rule.id)
        error("not handling setting non-constant parameters rn")
    else
        error()
    end
end


using Luxor

Drawing(400, 400, "logo.png")
origin()
background("white")
sethue("black")
squircle(Point(0,0), 150, 150, rt=0.3)
strokepath()
hoffset = 6.25
f(t) = Point(t-hoffset, cos(t)+3)
g(t) = Point(t-hoffset, cos(t+π/2))
h(t) = Point(t-hoffset, cos(t+π)-3)
setline(15)
fontsize(35)
@layer begin
    setopacity(0.4)
    sethue(Luxor.julia_red)
    poly(20f.(range(0, 4π, length=160)), :stroke)
    sethue(Luxor.julia_green)
    poly(20g.(range(0, 4π, length=160)), :stroke)
    sethue(Luxor.julia_purple)
    poly(20h.(range(0, 4π, length=160)), :stroke)
end
fontsize(135)
fontface("Helvetica")
text("WW", halign=:center, valign=:middle)
finish()
preview()


"""
Callback-based algorithm for decomposition on topology for Ground Structure with
Discrete Diameters.

Solver object to store parameter values.
"""
struct CallbackTopo <: GroundStructureSolver
    mastersolver
    subsolver
    timelimit::Float64 # seconds
    writemodels::Bool

    function CallbackTopo(mastersolver, subsolver;
                          timelimit=Inf, writemodels=false)
        new(mastersolver, subsolver, timelimit, writemodels)
    end
end

"Callbacks for decomposition based on topology"
function PipeLayout.optimize(inst::Instance, topo::Topology,
                             solver::CallbackTopo)
    # initialization
    finaltime = time() + solver.timelimit

    # reuse master problem from IterTopo algorithm
    model, y, q = make_semimaster(inst, topo, solver.mastersolver)
    solver.writemodels && writeLP(model, "master.lp", genericnames=false)

    # get access to SCIP.Optimizer instance (not factory)
    scip::SCIP.Optimizer = JuMP.backend(model)

    # enforce tree topology with constraint handler (higher prio)
    treetopohdlr = TreeTopoHdlr(scip)
    SCIP.include_conshdlr(scip, treetopohdlr; needs_constraints=true,
                          enforce_priority=-15,
                          check_priority=-7_000_000)
    SCIP.add_constraint(scip, treetopohdlr, TreeTopoCons(topo, y))

    # enforce feasible subproblems with constraint handler (lower prio)
    semisubhdlr = SemiSubHdlr(scip, solver.subsolver, finaltime)
    SCIP.include_conshdlr(scip, semisubhdlr; needs_constraints=true,
                          enforce_priority=-16,
                          check_priority=-7_100_000)
    SCIP.add_constraint(scip, semisubhdlr,
                        SemiSubCons(inst, topo, y, q))

    # solve the master (including callback)
    JuMP.optimize!(model)
    status = JuMP.termination_status(model)
    if !(status in (MOI.OPTIMAL, MOI.INFEASIBLE, MOI.TIME_LIMIT, MOI.NODE_LIMIT))
        error("Unexpected status: $(status)")
    end

    # actually, we solved it, but the master does not know
    if status == MOI.INFEASIBLE && semisubhdlr.best_solution !== nothing
        status = MOI.OPTIMAL
    end
    @debug "  solved, status: $(status)."

    # TODO: extract dual bound, nnodes?
    Result(status, semisubhdlr.best_solution, semisubhdlr.primal_bound, Inf, -1)
end


using mjWrap

################################################## Experiment Centric Functions

## accepts model,
## current and next states,
## current and next observations,
## applied controls that took us from current to next state

function rewardfunction(x::mjWrap.mjSet, tid::Int,
                                    s0::Vector{Float64}, # pass in states
                                    s1::Vector{Float64},
                                    o0::Vector{Float64}, # observations
                                    o1::Vector{Float64},
                                    ctrl::Vector{Float64}) # and controls
   pos0 = s0[1]
   pos1 = s1[1]
   height = s1[2]
   angle = s1[3]

   reward = (pos1 - pos0) / (x.dt*x.skip) # forward x direction

   upright_bonus = 3.0
   t_height = 0.8
   if height > t_height
      reward += upright_bonus
   end
   #reward -= upright_bonus * (height - t_height)^2
   reward -= 1e-2 * sum(ctrl.^2)

   return reward
end

function obsfunction(x::mjWrap.mjSet, tid::Int,
                                 s::Vector{Float64}, o::Vector{Float64})
   o[2] = mjWrap.wraptopi(o[2]) # global angle

   #o[:] = max(o[:], -10.0)
   #o[:] = min(o[:],  10.0) # clipped
   #scales = [0.5, 0.1, 0.5, 0.1, 0.5, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0]

   #for i=1:length(o)
   #    o[i] = o[i] / scales[i]
   #end
   #o[:] = o./scales
   #return o
end

## accepts model,
## default state,
## preallocated matrix of states (nqnv x numT)
function initfunction!(x::mjWrap.mjSet,
                                    s::mjWrap.TrajSamples)
   numT = size(s.state, 3)

   nq, nv, nu, ns = mjWrap.modelparams(x)
   s0 = s.s0

   sTemp = zeros(nq+nv)
   c = zeros(nu)
   o = zeros(ns)
   tid = 1

   diverse = true #false

   init_state = view( s.state, :, 1, : )
   for t=1:numT
      init_state[:,t] .= s0
      if isodd(t) && diverse
         # print("D ")
         angle = pi
         init_state[2,t]            = s0[2]           +rand()  * 0.1 # - 0.05
         init_state[3,t]            = s0[3]           +rand()  * angle - angle/2.0

         mjWrap.reset(x, tid, init_state[:,t], c, o)
         for i=1:50
            mjWrap.step!(x, tid, c, sTemp, o)
         end

         if (init_state[2,t] < 1)
            println("Hopper height: ", init_state[2,t])
         end
         # s.state[:,t] = sTemp
      else
         # print("N ")
         s.state[2,t]            = s0[2]           +rand()  * 0.1 - 0.05
         init_state[3,t]          = s0[3]           +rand()  * 1.0 - 0.5
         init_state[(1+nq):end,t] = s0[(1+nq):end]+rand(nv) * 0.01 - 0.005
         if (init_state[2,t] < 1)
            println("Hopper height: ", init_state[2,t])
         end
      end
   end
end

function modelfunction!(x::mjWrap.mjSet, iter::Int=0)
end

## accepts model and ndarray of control noise to be applied at sample time
## Array is: nu x T x numT , same size as ctrl
function ctrlfunction!(x::mjWrap.mjSet, ctrl::Array{Float64})
   randn!(ctrl)
end

## evaluation function: reward function shapes exploration, evaluation
## function defines success or failure
function evalfunction(x::mjWrap.mjSet, d::mjWrap.TrajSamples)
   # swimmer success: max distance along x axis covered at end of T
   xaxis_index = 1
   return mean(d.state[xaxis_index,end,:]) # x position at time T, for all numT
end

using ExpFunctions
myfuncs = FunctionSet(modelfunction!,
                      initfunction!,
                      ctrlfunction!,
                      obsfunction,
                      rewardfunction,
                      evalfunction)


#Written By Johnathan Bizzano
module Functions
    using Reexport
    using ..Utils
    using ..Data
    using ..HyperDimensional

    export AbstractTrainable, train, train!, AbstractTrainer

    export ConstantPool

    const ConstantPool{T} = Array{T, 1}

    include("AbstractMathFun.jl")
    include("CommonStaticFunctions.jl")

    abstract type AbstractTrainable{T, N} <: AbstractMathFun{T, N} end
    abstract type AbstractTrainer{T, N} <: AbstractMathFun{T, N} end

    include("Error.jl")
    include("Optimization/Optimizer.jl")

    trainer(f::AbstractMathFun; precision=10) = ()
    train!(f::AbstractTrainer, data::AbstractDataSet) = ()
    train!(f::AbstractTrainer, data::AbstractDataSet; epochs::Int = 1, IsVerbose=true, testset::AbstractDataSet=data) = ()

    include("Impl/BuiltInFunctions.jl")
    @reexport using .BuiltInFunctions

    include("Regression/Regression.jl")
    @reexport using .Regression

end


module BioParticleSimulation

using Random, StatsBase, Plots

include("genome.jl")

export FieldSet, fillfield, plotfield, addresource, round
    abstract type AbstractField end

    const UP    = ( 1,  0)
    const DOWN  = (-1,  0)
    const LEFT  = ( 0, -1)
    const RIGHT = ( 0,  1)

    mutable struct FieldSet
        field::Matrix{Union{AbstractField, Bool}}
        list::Vector{AbstractField}
        rfield::Matrix{Float64}
        makecell::Function
        function FieldSet(x::Int, y::Int)::FieldSet
            new(Matrix{Union{AbstractField, Bool}}(undef, y, x), Vector{AbstractField}(undef, 0), zeros(Int, y, x), makecell_simple)::FieldSet
        end
    end

    function Base.length(f::FieldSet)
        length(f.list)
    end

struct CellType
    id::Int
    movep::Float32
    killp::Float32
    replp::Float32
    kille::Int
    maxe::Int
    maxl::Int
    replcost::Int
    replinite::Int
    movecost::Int
    idlecost::Int
    function CellType(
        id,
        movep,
        killp,
        replp,
        kille,
        maxe,
        maxl,
        replcost,
        replinite,
        movecost,
        idlecost,
    )::CellType
        new(
            id,
            movep,
            killp,
            replp,
            kille,
            maxe,
            maxl,
            replcost,
            replinite,
            movecost,
            idlecost,
        )::CellType
    end
    function CellType(;
        id = 0,
        movep = 0.3,
        killp = 0.000001,
        replp = 0.0007,
        kille = 100,
        maxe = 2000,
        maxl = 4000,
        replcost = 100,
        replinite = 100,
        movecost = 10,
        idlecost = 1,
    )::CellType
        new(
            id,
            movep,
            killp,
            replp,
            kille,
            maxe,
            maxl,
            replcost,
            replinite,
            movecost,
            idlecost,
        )::CellType
    end
end

    const CELL_A = CellType(0, 0.3, 0.000001, 0.0007, 100, 2000, 4000, 100, 100, 10, 1)

    mutable struct Cell <: AbstractField
        enrg::Float64
        x::Int
        y::Int
        livetime::Int
        field::FieldSet
        type::CellType
        function Cell(enrg, x, y, l, field)::Cell
            new(enrg, x, y, l, field, CELL_A)::Cell
        end
        function Cell(enrg, x, y, l, field, type)::Cell
            new(enrg, x, y, l, field, type)::Cell
        end
    end

    function fillfield(f::FieldSet, p, type)
        x    = size(f.field, 2)
        y    = size(f.field, 1)
        mask = sample([true, false], Weights([p, 1-p]), (x,y))
        for r = 1:y
            for c = 1:x
                if mask[r, c]
                    newcell = Cell(1000, c, r, type.maxl, f, type)
                    push!(f.list, newcell)
                    f.field[r, c] = newcell
                else
                    f.field[r, c] = false
                end
            end
        end
    end
    function fillfield(f::FieldSet, p)
        fillfield(f::FieldSet, p, CELL_A)
    end
"""
    fillfield(f::FieldSet)

p - cell contamination probability;
type -
"""
    function fillfield(f::FieldSet; p, type)
        fillfield(f::FieldSet, p, type)
    end

    function addresource(f::FieldSet, n, val)
        x     = size(f.rfield, 2)
        y     = size(f.rfield, 1)
        for i = 1:n
            rx = rand(2:x-1)
            ry = rand(2:y-1)
            if f.rfield[ry, rx] < 2000 f.rfield[ry, rx] += val end
        end
    end
    function growresource(f::FieldSet, m)
        for r = 2:size(f.rfield, 1)-1
            for c = 2:size(f.rfield, 2)-1
                f.rfield[r, c] += f.rfield[r, c] * m
                if f.rfield[r, c] > 2000 f.rfield[r, c] = 2000 end
                if f.rfield[r, c] > 10
                    if f.rfield[r, c] > f.rfield[r, c + 1] f.rfield[r, c] -=1;  f.rfield[r, c + 1] +=1 end
                    if f.rfield[r, c] > f.rfield[r + 1, c] f.rfield[r, c] -=1;  f.rfield[r + 1, c] +=1 end
                    if f.rfield[r, c] > f.rfield[r, c - 1] f.rfield[r, c] -=1;  f.rfield[r, c - 1] +=1 end
                    if f.rfield[r, c] > f.rfield[r - 1, c] f.rfield[r, c] -=1;  f.rfield[r - 1, c] +=1 end
                end
            end
        end
    end

    function cellonfield(f, x, y)
        if isa(f.field[y, x], Cell)
            if f.field[y, x].enrg > 0 && f.field[y, x].livetime > 0 return true end
        end
        false
    end
    #=
    function resonfield(f, x, y)
        isa(f.field[y, x], Resource)
    end
    =#
    function emptycellonfield(f, x, y)
        if isa(f.field[y, x], Bool)
            return true
        elseif isa(f.field[y, x], Cell)
            if f.field[y, x].enrg <= 0 || f.field[y, x].livetime <= 0 return true end
        end
        false
    end

    function smovecell(c, x, y)
        c.enrg -= c.type.movecost
        setcellfield(c, false)
        c.x = x
        c.y = y
        setcellfield(c, c)
    end
    function movecell(c)
        vec = sample([UP, DOWN, LEFT, RIGHT])
        newx = c.x + vec[1]
        newy = c.y + vec[2]
        if c.enrg > c.type.movecost && c.livetime > 0 && newx > 0 && newy > 0 && newx <= size(c.field.field, 2) && newy <= size(c.field.field, 1)
            if cellonfield(c.field, newx, newy)
                killcell(c, newx, newy)
                return
            end
            #c.enrg -= c.type.movecost
            smovecell(c, newx, newy)
            if c.enrg + getrfield(c, newx, newy) >= c.type.maxe
                setrfield(c, newx, newy, getrfield(c, newx, newy) - c.type.maxe + c.enrg)
                c.enrg = c.type.maxe
            else
                c.enrg += getrfield(c, newx, newy)
                setrfield(c, newx, newy, 0)
            end

        else
            idlecell(c)
        end
    end
    function replcell(c)
        if c.enrg > c.type.replcost && c.livetime > 0
            c.field.makecell(c)
        else
            idlecell(c)
        end
    end
    function makecell_simple(cp::Cell)
        vec = sample([UP, DOWN, LEFT, RIGHT])
        newx = cp.x + vec[1]
        newy = cp.y + vec[2]
        if newx > 0 && newy > 0 && newx <= size(cp.field.field, 2) && newy <= size(cp.field.field, 1) && emptycellonfield(cp.field, newx, newy)
            cp.enrg -= cp.type.replcost
            newcell = Cell(cp.type.replinite, newx, newy, cp.type.maxl, cp.field, cp.type)
            push!(cp.field.list, newcell)
            cp.field.field[newy, newx] = newcell
        else
            idlecell(cp)
        end
    end

    function killcell(c, x, y)
        if sample([true, false], Weights([c.type.killp, 1 - c.type.killp]))
            kc = getfield(c, x, y)
            if c.enrg + kc.type.kille >= c.type.maxe
                c.enrg = c.type.maxe
            else
                c.enrg += kc.type.kille
            end
            #println("KILL!")
            setcellfield(kc, false)
            kc.enrg     = 0.0
            kc.livetime = 0.0

            smovecell(c, x, y)
        else
            idlecell(c)
        end
    end
    function idlecell(c)
        c.enrg -= c.type.idlecost
        if c.enrg <= 0 || c.livetime <= 0
            setcellfield(c, false)
        end
    end

    function setcellfield(c::Cell, val)
        c.field.field[c.y, c.x] = val
    end
    function setfield(c::Cell, x, y, val)
        c.field.field[y, x] = val
    end
    function getfield(c::Cell, x, y)
        c.field.field[y, x]
    end
    function getrfield(c::Cell, x, y)
        c.field.rfield[y, x]
    end
    function setrfield(c::Cell, x, y, val)
        c.field.rfield[y, x] = val
    end

    function cellaction(c::Cell)
        if c.enrg > 0 && c.livetime > 0
            action =  sample([movecell, replcell, idlecell], Weights([c.type.movep, c.type.replp, 1 - c.type.movep - c.type.replp]))
            action(c)
            c.livetime -= 1
            #if c.livetime <= 0
            #    setcellfield(c, false)
            #end
        end
        if c.enrg <= 0 || c.livetime <= 0
            setcellfield(c, false)
        end
    end

    #function cellaction(b::Bool)
    #end

    function round(f::FieldSet)
        for c in f.list
            cellaction(c)
        end
        deleteat!(f.list, findall(x -> x.enrg <= 0.0 || x.livetime <= 0.0, f.list))
        nothing
    end

    function round(f::FieldSet, n::Int)
        for i = 1:n
            round(f)
        end
    end

    function plotfield(f)
        cnt  = 0
        mx   = Matrix{Int}(undef, length(f.list), 2)
        ccnt  = 1

        for r = 1:size(f.field, 1)
            for c = 1:size(f.field, 2)
                if isa(f.field[r, c], Cell)
                    mx[ccnt, 1] = r
                    mx[ccnt, 2] = c
                    ccnt += 1
                end
            end
        end
        mygrad = cgrad([:white, :green])
        p = heatmap(f.rfield, c = mygrad, clim = (0, 500), legend = false)
        #p = heatmap(f.rfield, c = :greens, clim = (0, 2000), legend = false)
        p = scatter!(mx[:,2], mx[:,1], title = "Cells", color = :red, legend = false)
        #p = scatter!(p, mxr[:,1], mxr[:,2], title = "Cells", color = :green, legend = false)
        p
    end
    #=
    function Base.show(io::IO, f::FieldSet)
        x    = size(f.field, 2)
        y    = size(f.field, 1)
        for r = 1:y
            for c = 1:x
                if f.field[r, c] == false
                    print(io, " * ")
                elseif isa(f.field[r, c], Cell)
                    print(io, " C ")
                else
                    print(io, "   ")
                end
            end
            println(io, "")
        end
    end
    =#
    function printall(f)
        x    = size(f.field, 2)
        y    = size(f.field, 1)
        cnt  = 0
        for r = 1:y
            for c = 1:x
                if isa(f.field[r, c], Cell)
                    println("X: $c ($(f.field[r, c].x)) Y: $r ($(f.field[r, c].y)) Energy: $(f.field[r, c].enrg) Live: $(f.field[r, c].livetime)")
                    cnt += 1
                end
            end
        end
        println("Total: $cnt")
    end

end # module