text:
# a convenient way of interacting with a simulation
# maintained by @zsunberg

"""
    sim(polfunc::Function, mdp::MDP)
    sim(polfunc::Function, pomdp::POMDP)

Alternative way of running a simulation with a function specifying how to calculate the action at each timestep.

The intended usage is

    sim(mdp) do s
        # code that calculates action `a` based on `s` - this is the policy
        # you can also do other things like display something
        return a
    end

for an MDP or

    sim(pomdp) do o
        # code that does belief updates with observation `o` and calculates `a`
        # you can also do other things like display something
        return a
    end

for a POMDP.

Use the `simulator` keyword argument to specify any simulator to run the simulation. If nothing is specified for the simulator, a HistoryRecorder will be used as the simulator, with all keyword arguments forwarded to it, e.g.

    sim(mdp, max_steps=100) do s
        # ...
    end

will limit the simulation to 100 steps
"""
function sim end

function sim(polfunc::Function, mdp::MDP,
             initial_state=nothing;
             simulator=nothing,
             kwargs...
            )

    kwargd = Dict(kwargs)
    if initial_state==nothing && state_type(mdp) != Void
        if haskey(kwargd, :initial_state)
            initial_state = pop!(kwargd, :initial_state)
        else
            initial_state = default_init_state(mdp)
        end
    end
    delete!(kwargd, :initial_state)
    if simulator==nothing
        simulator = HistoryRecorder(;kwargd...)
    end
    policy = FunctionPolicy(polfunc)
    simulate(simulator, mdp, policy, initial_state)
end

function sim(polfunc::Function, pomdp::POMDP,
             initial_state=nothing;
             simulator=nothing,
             initial_obs=nothing,
             updater=nothing,
             kwargs...
            )

    kwargd = Dict(kwargs)
    if initial_state==nothing && state_type(pomdp) != Void
        if haskey(kwargd, :initial_state)
            initial_state = pop!(kwargd, :initial_state)
        else
            initial_state = default_init_state(pomdp)
        end
    end
    delete!(kwargd, :initial_state)
    if simulator==nothing
        simulator = HistoryRecorder(;kwargd...)
    end
    if updater==nothing
        if initial_obs == nothing
            initial_obs = default_init_obs(pomdp, initial_state)
        end
        if typeof(initial_obs)==obs_type(pomdp)
            O = obs_type(pomdp)
        else
            O = Any
        end
        updater = FastPreviousObservationUpdater{O}()
    else # an updater was specified
        if initial_obs == nothing
            initial_obs = initial_state_distribution(pomdp)
        end
    end
    policy = FunctionPolicy(polfunc)
    simulate(simulator, pomdp, policy, updater, initial_obs, initial_state)
end

function default_init_obs(p::POMDP, s)
    if implemented(generate_o, Tuple{typeof(p), typeof(s), typeof(Base.GLOBAL_RNG)})
        return generate_o(p, s, Base.GLOBAL_RNG)
    else
        return nothing
    end
end

@generated function default_init_state(p::Union{MDP,POMDP})
    if implemented(initial_state, Tuple{p, typeof(Base.GLOBAL_RNG)})
        return :(initial_state(p, Base.GLOBAL_RNG))
    else
        return quote
            error("""
                  Error in sim(::$(typeof(p))): No initial state specified.

                  Please supply it as an argument after the mdp or define the method POMDPs.initial_state(::$(typeof(p)), ::$(typeof(Base.GLOBAL_RNG))) or define the method POMDPs.initial_state_distribution(::$(typeof(p))).

                  """)
        end
    end
end


text:
module AssociatedLegendrePolynomials

import Base: @boundscheck, @propagate_inbounds

include("scalar.jl")

# Public interfaces interface
export AbstractLegendreNorm
include("interface.jl")

# Specific normalizations
export LegendreNormCoeff,
       LegendreUnitNorm, LegendreUnitCoeff,
       LegendreFourPiNorm, LegendreFourPiCoeff,
       LegendreOrthoNorm, LegendreOrthoCoeff,
       LegendreSphereNorm, LegendreSphereCoeff
include("norm_unit.jl")
include("norm_sphere.jl")
include("norm_table.jl")

export legendre, legendre!
include("calculation.jl")

# Other functionality
export Plm, Plm!, Nlm, λlm, λlm!
include("aliases.jl")
include("broadcasting.jl")

include("precompile.jl")
_precompile_()
end # module AssociatedLegendrePolynomials

ext: jl


text:
@rule AR(:x, Marginalisation) (m_y::NormalDistributionsFamily, q_θ::NormalDistributionsFamily, q_γ::GammaShapeRate, meta::ARMeta) = begin
    mθ, Vθ = mean_cov(q_θ)
    my, Vy = mean_cov(m_y)

    mγ = mean(q_γ)

    mA = as_companion_matrix(mθ)
    mV = ar_transition(getvform(meta), getorder(meta), mγ)

    C = mA' * inv(add_transition(Vy, mV))

    W = C * mA + mγ * Vθ
    ξ = C * my

    return convert(promote_variate_type(getvform(meta), NormalWeightedMeanPrecision), ξ, W)
end


text:
# Rendering Abstraction #######################################################
mutable struct RenderData
  data::AbstractArray{GLfloat}
  dim::Integer
  usage::GLenum
end

struct RenderBuffer
  id::GLuint
  size::Int
  usage::GLenum
end

mutable struct RenderObject
  context::Context
  vertex_array::GLuint
  render_buffers::AbstractArray{RenderBuffer}
  render_data::AbstractArray{RenderData}
  elnb::Int
  idx_buffer::Union{RenderBuffer, Nothing}
  idx_data::Union{AbstractArray{GLuint}, Nothing}
  T::Array{GLfloat, 2}
  render_lock::Threads.SpinLock
end

function RenderObject(context::Context, render_data::AbstractArray{RenderData},
                      idxXORelnb::Union{AbstractArray{GLuint}, Int})
  # save the configuration in the vertex array
  vertex_array = glGenVertexArray()
  glBindVertexArray(vertex_array)

  # allocate the buffers, buffer data and set attributes
  render_buffers = Array{RenderBuffer, 1}(undef, length(render_data))
  for i in 1:length(render_data)
    render_buffers[i] = RenderBuffer(glGenBuffer(),
                                     sizeof(render_data[i].data),
                                     render_data[i].usage)
    glBindBuffer(GL_ARRAY_BUFFER, render_buffers[i].id)
    # usage is GL_STATIC_DRAW or GL_DYNAMIC_DRAW
    glBufferData(GL_ARRAY_BUFFER, render_buffers[i].size,
                 render_data[i].data, render_buffers[i].usage)
    glVertexAttribPointer(context.attributes[i], render_data[i].dim,
                          GL_FLOAT, false, 0, C_NULL)
    glEnableVertexAttribArray(context.attributes[i])
  end

  # allocate the index buffer for drawing normal objects
  idx_buffer = nothing
  idx_data = nothing
  elnb = 0
  if typeof(idxXORelnb) == Int
    idx_buffer = nothing
    elnb = idxXORelnb
  else
    idx = idxXORelnb
    idx_buffer = RenderBuffer(glGenBuffer(), sizeof(idx), GL_DYNAMIC_DRAW)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idx_buffer.id)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, idx_buffer.usage)
    elnb = length(idx)
    idx_data = copy(idxXORelnb)
  end

  # finish saving the configuration
  glBindVertexArray(0)

  return RenderObject(context, vertex_array, render_buffers, render_data, elnb,
                      idx_buffer, idx_data, identity_mat(), Threads.SpinLock())
end

function render(obj::RenderObject)
  lock(obj.render_lock)

  glUniformMatrix4fv(obj.context.Tmat_location[], 1, GL_FALSE, obj.T)

  glBindVertexArray(obj.vertex_array)
  if obj.idx_buffer != nothing
    glDrawElements(GL_TRIANGLES, obj.elnb, GL_UNSIGNED_INT, Ptr{Nothing}(0))
  else
    glDrawArrays(GL_LINES, 0, obj.elnb)
  end
  glBindVertexArray(0)

  unlock(obj.render_lock)
end

# Updating Data ###############################################################
function update_buffer!(obj::RenderObject, data::Array{GLfloat},
                        attribute::GLint)
  lock(obj.render_lock)

  attr_idx = findfirst(x -> x == attribute, obj.context.attributes)
  if sizeof(data) <= obj.render_buffers[attr_idx].size
    glBindBuffer(GL_ARRAY_BUFFER, obj.render_buffers[attr_idx].id)
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(data), data)
  else
    #println("RESIZING BUFFER")

    id = obj.render_buffers[attr_idx].id
    usage = obj.render_buffers[attr_idx].usage
    size = sizeof(data)
    new_buffer = RenderBuffer(id, size, usage)
    obj.render_buffers[attr_idx] = new_buffer

    glBindBuffer(GL_ARRAY_BUFFER, obj.render_buffers[attr_idx].id)
    glBufferData(GL_ARRAY_BUFFER, obj.render_buffers[attr_idx].size, data,
                 obj.render_buffers[attr_idx].usage)
  end
  obj.render_data[attr_idx] = RenderData(data,
                                         obj.render_data[attr_idx].dim,
                                         obj.render_data[attr_idx].usage)
  unlock(obj.render_lock)
end

function update_idx!(obj::RenderObject,
                     idxXORelnb::Union{Array{GLuint}, Int})
  @assert ((obj.idx_buffer == nothing && typeof(idxXORelnb) == Int) ||
           (obj.idx_buffer != nothing &&
            typeof(idxXORelnb) == Array{GLuint, 1}))

  lock(obj.render_lock)

  if obj.idx_buffer != nothing
    idx = idxXORelnb
    if sizeof(idx) <= obj.idx_buffer.size
      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, obj.idx_buffer.id)
      glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(idx), idx)
    else
      #println("RESIZING IDX")

      id = obj.idx_buffer.id
      usage = obj.idx_buffer.usage
      size = sizeof(idx)
      new_buffer = RenderBuffer(id, size, usage)
      obj.idx_buffer = new_buffer

      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, obj.idx_buffer.id)
      glBufferData(GL_ELEMENT_ARRAY_BUFFER, obj.idx_buffer.size, idx,
                   GL_DYNAMIC_DRAW)
    end
    obj.idx_data = copy(idxXORelnb)
    obj.elnb = length(idx)
  else
    obj.elnb = idxXORelnb
  end

  unlock(obj.render_lock)
end

# Matrices ####################################################################
function scale_mat(x, y)
  return GLfloat[x 0 0 0;
                 0 y 0 0;
                 0 0 1 0;
                 0 0 0 1]
end

function rotate_mat(th)
  return GLfloat[cos(th) -sin(th) 0 0;
                 sin(th) cos(th) 0 0;
                 0 0 1 0;
                 0 0 0 1]
end

function translate_mat(x, y)
  return GLfloat[1 0 0 x;
                 0 1 0 y;
                 0 0 1 0;
                 0 0 0 1]
end

function identity_mat()
  return GLfloat[1 0 0 0;
                 0 1 0 0;
                 0 0 1 0;
                 0 0 0 1]
end

# Line Drawing ################################################################
function make_line(context::Context, x1::Float64, y1::Float64,
                   x2::Float64, y2::Float64, color::AbstractArray{<: Real})
  @assert length(color) == 3

  position = RenderData(GLfloat[x1, y1, x2, y2], 2, GL_DYNAMIC_DRAW)
  color = RenderData(Array{GLfloat}(repeat(color, 2)), 3, GL_STATIC_DRAW)
  usetex = RenderData(fill(GLfloat(0), 2), 1, GL_STATIC_DRAW)
  texcoord = RenderData(fill(GLfloat(0), 4), 2, GL_DYNAMIC_DRAW)
  elnb = 2
  return RenderObject(context, [position, color, usetex, texcoord], elnb)
end

# Text Drawing ################################################################
function _make_letter(c::Char, x::Float64, y::Float64, dx::Float64,
                      dy::Float64)
  tx = rem(Int(c), 32) / 32
  ty = div(Int(c), 32) / 8

  tdx = 1 / 32
  tdy = 1 / 8

  position = GLfloat[x - dx / 2, y - dy / 2,
                     x - dx / 2, y + dy / 2,
                     x + dx / 2, y + dy / 2,
                     x + dx / 2, y - dy / 2]
  colors = fill(GLfloat(1), 3 * 4)
  usetex = fill(GLfloat(1), 4)
  texcoord = GLfloat[tx, ty + tdy,
                     tx, ty,
                     tx + tdx, ty,
                     tx + tdx, ty + tdy]
  indices = GLuint[0, 1, 2,
                   0, 2, 3]

  return (position, colors, usetex, texcoord, indices)
end

function _make_text_data(text::AbstractString, x::Float64, y::Float64,
                         scale::Float64)
  offset = 0

  dx = 0.05 * scale
  dy = 0.1 * scale
  width = length(text) * dx

  P = GLfloat[]
  C = GLfloat[]
  T = GLfloat[]
  U = GLfloat[]
  I = GLuint[]

  len = (length(text) - 1) / 2
  k = 0
  for c in text
    (p, c, u, t, i) = _make_letter(c, x - (len - k) * dx, y, dx, dy)
    append!(P, p)
    append!(C, c)
    append!(U, u)
    append!(T, t)
    append!(I, i .+ offset)

    offset += 4
    k += 1
  end

  return (P, C, U, T, I)
end

function make_text(context::Context, text::AbstractString, x::Float64,
                   y::Float64, scale::Float64=1.0)
  (P, C, U, T, I) = _make_text_data(text, x, y, scale)
  return RenderObject(context, [RenderData(P, 2, GL_DYNAMIC_DRAW),
                                RenderData(C, 3, GL_STATIC_DRAW),
                                RenderData(U, 1, GL_STATIC_DRAW),
                                RenderData(T, 2, GL_DYNAMIC_DRAW)], I)
end

function update_text!(obj::RenderObject, text::AbstractString, x::Float64,
                      y::Float64, scale::Float64=1.0)
  (P, C, U, T, I) = _make_text_data(text, x, y, scale)
  if length(I) > div(obj.idx_buffer.size, 4)
    update_buffer!(obj, C, obj.context.attributes[2])
    update_buffer!(obj, U, obj.context.attributes[3])
  end
  update_buffer!(obj, P, obj.context.attributes[1])
  update_buffer!(obj, T, obj.context.attributes[4])
  update_idx!(obj, I)

  return
end

function bounding_box(obj::RenderObject)
  @assert obj.render_data[1].dim == 2
  position = obj.render_data[1].data

  max_idx = nothing
  if obj.idx_buffer != nothing
    max_idx = 2 * (maximum(obj.idx_data) + 1)
  else
    max_idx = length(position)
  end
  xmax = Float64(maximum(position[1:2:max_idx]))
  xmin = Float64(minimum(position[1:2:max_idx]))

  ymax = Float64(maximum(position[2:2:max_idx]))
  ymin = Float64(minimum(position[2:2:max_idx]))

  xmax = xmax == nothing ? NaN : xmax
  xmin = xmin == nothing ? NaN : xmin
  ymax = ymax == nothing ? NaN : ymax
  ymin = ymin == nothing ? NaN : ymin

  return (xmin, ymin, xmax, ymax)
end


text:
module Graphics
# List of textures available for a given path

using LinearAlgebra
using Images
using RayTracer

include("utils.jl")

tex_paths = Dict()

# Cache of textures
tex_cache = Dict()

function get(tex_name::String, rng=nothing)
    ##
    #Manage the caching of textures, and texture randomization
    ##
    paths = Base.get(tex_paths, tex_name, [])

    # Get an inventory of the existing texture files
    if length(paths) == 0
        for i in 1:9
            path = get_file_path("src/textures", tex_name * "_$i", "png")
            !ispath(path) && break
            push!(paths, path)
        end
    end

    @assert length(paths) > 0 "failed to load textures for name " * tex_name

    if !isnothing(rng)
        path_idx = rand(rng, 1:length(paths))
        path = paths[path_idx]
    else
        path = paths[1]
    end

    if path ∉ keys(tex_cache)
        #TODO
        tex_cache[path] = load_texture(path)
    end

    return tex_cache[path]
end

function load_texture(tex_path::String)
    Vec3([Float32.(permutedims(channelview(load(tex_path)),
        (3, 2, 1)))[:,end:-1:1,i]  for i in 1:3]...)
end

#=
function bind(tex::Texture)
        from pyglet import gl
        gl.glBindTexture(self.tex.target, self.tex.id)
end

def create_frame_buffers(width, height, num_samples):
    """Create the frame buffer objects"""
    from pyglet import gl

    # Create a frame buffer (rendering target)
    multi_fbo = gl.GLuint(0)
    gl.glGenFramebuffers(1, byref(multi_fbo))
    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, multi_fbo)

    # The try block here is because some OpenGL drivers
    # (Intel GPU drivers on macbooks in particular) do not
    # support multisampling on frame buffer objects
    try:
        # Create a multisampled texture to render into
        fbTex = gl.GLuint(0)
        gl.glGenTextures( 1, byref(fbTex))
        gl.glBindTexture(gl.GL_TEXTURE_2D_MULTISAMPLE, fbTex)
        gl.glTexImage2DMultisample(
            gl.GL_TEXTURE_2D_MULTISAMPLE,
            num_samples,
            gl.GL_RGBA32F,
            width,
            height,
            True
        )
        gl.glFramebufferTexture2D(
                gl.GL_FRAMEBUFFER,
                gl.GL_COLOR_ATTACHMENT0,
                gl.GL_TEXTURE_2D_MULTISAMPLE,
            fbTex,load_texture
            0
        )

        # Attach a multisampled depth buffer to the FBO
        depth_rb = gl.GLuint(0)
        gl.glGenRenderbuffers(1, byref(depth_rb))
        gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, depth_rb)
        gl.glRenderbufferStorageMultisample(gl.GL_RENDERBUFFER, num_samples, gl.GL_DEPTH_COMPONENT, width, height)
        gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, gl.GL_DEPTH_ATTACHMENT, gl.GL_RENDERBUFFER, depth_rb)

    except:
        logger.debug('Falling back to non-multisampled frame buffer')

        # Create a plain texture texture to render into
        fbTex = gl.GLuint(0)
        gl.glGenTextures( 1, byref(fbTex))
        gl.glBindTexture(gl.GL_TEXTURE_2D, fbTex)
        gl.glTexImage2D(
            gl.GL_TEXTURE_2D,
            0,
            gl.GL_RGBA,
            width,
            height,
            0,
            gl.GL_RGBA,
            gl.GL_FLOAT,
            None
        )
        gl.glFramebufferTexture2D(
            gl.GL_FRAMEBUFFER,
            gl.GL_COLOR_ATTACHMENT0,
            gl.GL_TEXTURE_2D,
            fbTex,
            0
        )

        # Attach depth buffer to FBO
        depth_rb = gl.GLuint(0)
        gl.glGenRenderbuffers(1, byref(depth_rb))
        gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, depth_rb)
        gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, gl.GL_DEPTH_COMPONENT, width, height)
        gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, gl.GL_DEPTH_ATTACHMENT, gl.GL_RENDERBUFFER, depth_rb)

    # Sanity check
    import pyglet
    if pyglet.options['debug_gl']:
      res = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER)
      assert res == gl.GL_FRAMEBUFFER_COMPLETE

    # Create the frame buffer used to resolve the final render
    final_fbo = gl.GLuint(0)
    gl.glGenFramebuffers(1, byref(final_fbo))
    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, final_fbo)

    # Create the texture used to resolve the final render
    fbTex = gl.GLuint(0)
    gl.glGenTextures(1, byref(fbTex))
    gl.glBindTexture(gl.GL_TEXTURE_2D, fbTex)
    gl.glTexImage2D(
        gl. GL_TEXTURE_2D,
        0,
        gl.GL_RGBA,
        width,
        height,
        0,
        gl. GL_RGBA,
        gl.GL_FLOAT,
        None
    )
    gl.glFramebufferTexture2D(
            gl.GL_FRAMEBUFFER,
            gl.GL_COLOR_ATTACHMENT0,
            gl.GL_TEXTURE_2D,
        fbTex,
        0
    )
    import pyglet
    if pyglet.options['debug_gl']:
      res = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER)
      assert res == gl.GL_FRAMEBUFFER_COMPLETE

    # Enable depth testing
    gl.glEnable(gl.GL_DEPTH_TEST)

    # Unbind the frame buffer
    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)

    return multi_fbo, final_fbo
=#
function rotate_point(px, py, cx, cy, θ)
    ##
    #Rotate a 2D point around a center
    ##

    dx = px - cx
    dy = py - cy

    new_dx = dx * cos(θ) + dy * sin(θ)
    new_dy = dy * cos(θ) - dx * sin(θ)

    return [cx + new_dx, cy + new_dy]
end

function gen_rot_matrix(axis, θ)
    ##
    #Rotation matrix for a counterclockwise rotation around the given axis
    ##

    axis = axis / √dot(axis, axis)
    a = cos(θ/2)
    b, c, d = -axis * sin(θ/2)

    return [
            a^2+b^2-c^2-d^2  2(b*c-a*d)      2(b*d+a*c);
            2(b*c+a*d)       a^2+c^2-b^2-d^2 2(c*d-a*b);
            2(b*d-a*c)       2(c*d+a*b)      a^2+d^2-b^2-c^2]
end

function bezier_point(cps, t)
    ##
    #Cubic Bezier curve interpolation
    #B(t) = (1-t)^3 * P0 + 3t(1-t)^2 * P1 + 3t^2(1-t) * P2 + t^3 * P3
    ##

    p = ((1-t)^3) * cps[1,:]
    p = p .+ 3t * ((1-t)^2) * cps[2,:]
    p = p .+ 3(t^2) * (1-t) * cps[3,:]
    p = p .+ (t^3) * cps[4,:]

    return p
end

function bezier_tangent(cps, t)
    ##
    #Tangent of a cubic Bezier curve (first order derivative)
    #B'(t) = 3(1-t)^2(P1-P0) + 6(1-t)t(P2-P1) + 3t^2(P3-P2)
    ##

    p  = 3((1-t)^2) * (cps[2,:] - cps[1,:])
    p += 6(1-t) * t * (cps[3,:] - cps[2,:])
    p += 3(t^2) * (cps[4,:] - cps[3,:])

    p = p ./ norm(p)

    return p
end

function bezier_closest(cps, p, t_bot=0, t_top=1, n=8)
    mid = (t_bot + t_top) * 0.5

    n == 0 && (return mid)

    p_bot = bezier_point(cps, t_bot)
    p_top = bezier_point(cps, t_top)

    d_bot = norm(p_bot - p)
    d_top = norm(p_top - p)

    d_bot < d_top && return (bezier_closest(cps, p, t_bot, mid, n-1))

    return bezier_closest(cps, p, mid, t_top, n-1)
end

function bezier_draw(cps, n=20, red::Bool=false)
    pts = [bezier_point(cps, i/n) for i in -1:(n-2)]
    #gl.glBegin(gl.GL_LINE_STRIP)

    if red
    #    gl.glColor3f(1, 0, 0)
    else
    #    gl.glColor3f(0, 0, 1)
    end

    for (i, p) in enumerate(pts)
    #    gl.glVertex3f(*p)
    end

    #gl.glEnd()
    #gl.glColor3f(1,1,1)
end

export gen_rot_matrix, rotate_point, load_texture,
       bezier_closest, bezier_point, bezier_tangent
end #module


text:
using BinDeps

# Configuration / Autodetections
const x11 = @unix? (OS_NAME != :Darwin) : false
const gtk = isdir(Pkg.dir("Gtk"))

@BinDeps.setup

cgraph = library_dependency("cgraph",aliases = ["libcgraph","libcgraph.so.5"], validate = function(p,h)
    dlsym_e(h,:agmemread) != C_NULL
end)
gvc = library_dependency("gvc",aliases = ["libgvc"])

graphviz = [cgraph,gvc]

@osx_only begin
    using Homebrew
    provides( Homebrew.HB, "graphviz", graphviz, os = :Darwin, preload = """
    module GraphVizInit
    function __init__()
        ENV["GVBINDIR"] = Pkg.dir("Homebrew","deps","usr","lib","graphviz")
    end
    __init__()
    end
    """)
end

options = String[]
x11 ? push!(options,"--with-x") : push!(options,"--without-x")
gtk && push!(options,"--with-gtk")
push!(options,"--without-qt")
push!(options,"--with-pangocairo")
push!(options,"--enable-debug")

provides(Sources,URI("http://www.graphviz.org/pub/graphviz/stable/SOURCES/graphviz-2.36.0.tar.gz"),graphviz)
provides(BuildProcess,Autotools(libtarget = "lib/cgraph/.libs/libcgraph."*BinDeps.shlib_ext,configure_options=options,
    pkg_config_dirs=[Pkg.dir("Cairo","deps","usr","lib","pkgconfig")]),graphviz)

# Ubuntu GraphViz is too old
# provides(AptGet,"graphviz",graphviz)

@BinDeps.install [ :cgraph => :cgraph, :gvc => :gvc ]


text:
using Stheno, Random
using Stheno: @model

######################## Example 1: Vanilla Toeplitz ########################

@model function vanilla()
    f = GP(EQ())
    y = f + GP(Noise(0.1))
    return f, y
end
f, y = vanilla();

# Create regularly spaced data. Dispatch handles literally everything else.
x = range(-5.0, stop=5.0, length=10000);

# Notice that we don't struggle to construct large covariances. All of these matrices
# are either Toeplitz or SymmetricToeplitz. This structure is exploited to accelerate
# all computations. These covariance matrices are constucted in O(N)-time.
Σff = cov(f(x));
Σfy = xcov(f(x), y(x));
Σyy = cov(y(x));

# These operations are also quite a lot faster. They occur in O(N^2)-time.
rng = MersenneTwister(123456);
ŷ = rand(rng, y(x));
@show logpdf(y(x), ŷ);

######################## Example 2: Block Vanilla Toeplitz ########################

# Suppose that we have two separate blocks of regularly spaced data eg. missing data.
xl, xr = range(-5.0, stop=0.0, length=5000), range(0.0, stop=5.0, length=5000);

# If we inspect the underlying blocks of the covariance, we see they are all still Toeplitz.
using Stheno: unbox
f_xl_xr = BlockGP([f(xl), f(xr)]);
y_xl_xr = BlockGP([y(xl), y(xr)]);
Σff = cov(f_xl_xr);
display(typeof.(unbox(unbox(Σff)).blocks));

# THIS CURRENTLY SEEMS A BIT SLOW! COMPARE TO DENSE PROPERLY!
rng = MersenneTwister(123456);
ŷ = rand(rng, y_xl_xr);
@show logpdf(y_xl_xr, ŷ);

######################### Example 3: Multi-Output Toeplitz ##########################

# Suppose now that we modify our model slightly. We construct a two-output process from
# three latent processes.
@model function simple_multi_output()
    f₁, f₂, f₃ = GP(EQ()), GP(EQ()), GP(EQ())
    y₁, y₂ = f₁ + f₂, f₂ + f₃
    return y₁, y₂
end
y₁, y₂ = simple_multi_output();

# Observe both at x. The joint covariance matrix is still Toeplitz!
y_1x_2x = BlockGP([y₁(x), y₂(x)]);
Σ12 = cov(y_1x_2x);
display(typeof.(unbox(unbox(Σ12)).blocks));

# Same again for the missing data.
y_1xl_2xr = BlockGP([y₁(xl), y₂(xr)]);
Σ12 = cov(y_1xl_2xr);
display(typeof.(unbox(unbox(Σ12)).blocks));

######################### Example 4: Temporally mis-aligned data ##########################

# What if each output has the same sampling rate, but the data are misaligned?
x₁ = range(-5.0, stop=5.0, length=5000);
x₂ = x₁ .+ 0.3;

# The joint covariance still comprises Toeplitz matrices.
y_x1_x2 = BlockGP([y₁(x₁), y₂(x₂)]);
Σ12 = cov(y_x1_x2);
display(typeof.(unbox(unbox(Σ12)).blocks));

######################### Example 5: Partial structure ###########################

# Suppose that we have some regularly sampled data, and some structure-less data.
x′ = randn(rng, 100);

# Notice that Σ_x_x′ comprised three dense blocks, and one Toeplitz. This is because
# cov(f(x′)) is dense, xcov(f(x), f(x′)) / xcov(f(x′), f(x)) is dense, whilst cov(f(x))
# is Toeplitz.
f_x_x′ = BlockGP([f(x), f(x′)]);
Σ_x_x′ = cov(f_x_x′);
display(typeof.(unbox(unbox(Σ_x_x′)).blocks))

# Note that any combination of these things will (should) also yield some combination of
# dense and Toeplitz matrices.


text:
#NOTE: These are helper functions to get data from JuMP models into the PIPS-NLP Interface.
#The implementation here was heavily framed around the code in Ipopt.jl

mutable struct ConstraintData
    linear_le_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericAffExpr{Float64,JuMP.VariableRef},MathOptInterface.LessThan{Float64}}}
    linear_ge_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericAffExpr{Float64,JuMP.VariableRef},MathOptInterface.GreaterThan{Float64}}}
	linear_interval_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericAffExpr{Float64,JuMP.VariableRef},MathOptInterface.Interval{Float64}}}
    linear_eq_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericAffExpr{Float64,JuMP.VariableRef},MathOptInterface.EqualTo{Float64}}}
    quadratic_le_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericQuadExpr{Float64,JuMP.VariableRef},MathOptInterface.LessThan{Float64}}}
    quadratic_ge_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericQuadExpr{Float64,JuMP.VariableRef},MathOptInterface.GreaterThan{Float64}}}
	quadratic_interval_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericQuadExpr{Float64,JuMP.VariableRef},MathOptInterface.Interval{Float64}}}
    quadratic_eq_constraints::Vector{JuMP.ScalarConstraint{JuMP.GenericQuadExpr{Float64,JuMP.VariableRef},MathOptInterface.EqualTo{Float64}}}
	nonlinear_constraints::Vector{JuMP._NonlinearConstraint}
end

ConstraintData() = ConstraintData([], [], [], [], [], [],[],[],[])

function get_constraint_data(node::OptiNode)
	con_data = ConstraintData()
	constraint_types = JuMP.list_of_constraint_types(node)

    for (func,set) in constraint_types
        if func == JuMP.VariableRef 	#This is a variable bound, not a PIPS-NLP constraint
			continue
		else
	        constraint_refs = JuMP.all_constraints(node, func, set)
	        for constraint_ref in constraint_refs
	            constraint = JuMP.constraint_object(constraint_ref)

				func_type = typeof(constraint.func)
				con_type = typeof(constraint.set)

				if func_type == JuMP.GenericAffExpr{Float64,JuMP.VariableRef}
					if con_type == MOI.LessThan{Float64}
						push!(con_data.linear_le_constraints,constraint)
					elseif con_type == MOI.GreaterThan{Float64}
						push!(con_data.linear_ge_constraints,constraint)
					elseif con_type == MOI.Interval{Float64}
						push!(con_data.linear_interval_constraints,constraint)
					elseif con_type == MOI.EqualTo{Float64}
						push!(con_data.linear_eq_constraints,constraint)
					end
				elseif func_type == JuMP.GenericQuadExpr{Float64,JuMP.VariableRef}
					if con_type == MOI.LessThan{Float64}
						push!(con_data.quadratic_le_constraints,constraint)
					elseif con_type == MOI.GreaterThan{Float64}
						push!(con_data.quadratic_ge_constraints,constraint)
					elseif con_type == MOI.Interval{Float64}
						push!(con_data.quadratic_interval_constraints,constraint)
					elseif con_type == MOI.EqualTo{Float64}
						push!(con_data.quadtratic_eq_constraints,constraint)
					end
	            else
	                error("Could not figure out constraint type for $(constraint_ref)")
	            end
	        end
        end
    end
	if node.nlp_data != nothing
		con_data.nonlinear_constraints = node.nlp_data.nlconstr
	end
	return con_data
end

linear_le_offset(constraints::ConstraintData) = 0
linear_ge_offset(constraints::ConstraintData) = length(constraints.linear_le_constraints)
linear_interval_offset(constraints::ConstraintData) = linear_ge_offset(constraints) + length(constraints.linear_ge_constraints)
linear_eq_offset(constraints::ConstraintData) = linear_interval_offset(constraints) + length(constraints.linear_interval_constraints)
quadratic_le_offset(constraints::ConstraintData) = linear_eq_offset(constraints) + length(constraints.linear_eq_constraints)
quadratic_ge_offset(constraints::ConstraintData) = quadratic_le_offset(constraints) + length(constraints.quadratic_le_constraints)
quadratic_interval_offset(constraints::ConstraintData) =  quadratic_ge_offset(constraints) + length(constraints.quadratic_ge_constraints)
quadratic_eq_offset(constraints::ConstraintData) = quadratic_interval_offset(constraints) + length(constraints.quadratic_interval_constraints)
nlp_constraint_offset(constraints::ConstraintData) = quadratic_eq_offset(constraints) + length(constraints.quadratic_eq_constraints)

function numconstraints(con_data::ConstraintData)
	fields = fieldnames(ConstraintData)
	return sum([length(getfield(con_data,field)) for field in fields])
end

function constraintbounds(con_data::ConstraintData)
	num_cons = numconstraints(con_data)
	# Setup indices.  Need to get number of constraints
	constraint_lower = ones(num_cons)*-Inf
	constraint_upper = ones(num_cons)*Inf

	#linear_le_constraints
	constraints = con_data.linear_le_constraints
	offset = linear_le_offset(con_data)
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].set.upper for i in 1:length(constraints)]

	#linear_ge_constraints
	constraints = con_data.linear_ge_constraints
	offset = linear_ge_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].set.lower for i in 1:length(constraints)]

	#linear_interval_constraints
	constraints = con_data.linear_interval_constraints
	offset = linear_interval_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].set.lower for i in 1:length(constraints)]
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].set.upper for i in 1:length(constraints)]

	#linear_eq_constraints
	constraints = con_data.linear_eq_constraints
	offset = linear_eq_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].set.value for i in 1:length(constraints)]
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].set.value for i in 1:length(constraints)]

	#quadratic_le_constraints
	constraints = con_data.quadratic_le_constraints
	offset = quadratic_le_offset(con_data)
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].set.upper for i in 1:length(constraints)]

	#quadratic_ge_constraints
	constraints = con_data.quadratic_ge_constraints
	offset = quadratic_ge_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].set.lower for i in 1:length(constraints)]

	#quadratic_interval_constraints
	constraints = con_data.quadratic_interval_constraints
	offset = quadratic_interval_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].set.lower for i in 1:length(constraints)]
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].set.upper for i in 1:length(constraints)]

	#quadratic_eq_constraints
	constraints = con_data.quadratic_eq_constraints
	offset = quadratic_eq_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].set.value for i in 1:length(constraints)]
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].set.value for i in 1:length(constraints)]

	#nonlinear constraints
	constraints = con_data.nonlinear_constraints
	offset = nlp_constraint_offset(con_data)
	constraint_lower[offset + 1:offset + length(constraints)] .= [constraints[i].lb for i in 1:length(constraints)]
	constraint_upper[offset + 1:offset + length(constraints)] .= [constraints[i].ub for i in 1:length(constraints)]

	return constraint_lower,constraint_upper
end

function numconstraints(node::OptiNode)
    num_cons = 0
    constraint_types = JuMP.list_of_constraint_types(node)
    for (func,set) in constraint_types
        if func != JuMP.VariableRef #This is a variable bound, not a PIPS-NLP constraint
            num_cons += JuMP.num_constraints(node,func,set)
        end
    end
    num_cons += JuMP.num_nl_constraints(node)
    return num_cons
end

function variableupperbounds(node::OptiNode)
    #Get upper bound variable constraints
    upper_bounds = ones(JuMP.num_variables(node))*Inf #Assume no upper bound by default

    var_bound_constraints = JuMP.all_constraints(node,JuMP.VariableRef,MOI.LessThan{Float64})
    for var_bound_ref in var_bound_constraints
        var_constraint = JuMP.constraint_object(var_bound_ref)
        var = var_constraint.func
        index = var.index
        upper_bound = var_constraint.set.upper
        upper_bounds[index.value] = upper_bound
    end
    #Get fixed variable constraints
    var_equal_constraints = JuMP.all_constraints(node,JuMP.VariableRef,MOI.EqualTo{Float64})
    for var_equal_ref in var_equal_constraints
        var_constraint = JuMP.constraint_object(var_equal_ref)
        var = var_constraint.func
        index = var.index
        upper_bound = var_constraint.set.value
        upper_bounds[index.value] = upper_bound
    end

    #TODO sort?
    return upper_bounds
end

function variablelowerbounds(node::OptiNode)
    lower_bounds = ones(JuMP.num_variables(node))*-Inf #Assume no upper bound by default
    var_bound_constraints = JuMP.all_constraints(node,JuMP.VariableRef,MOI.GreaterThan{Float64})
    for var_bound_ref in var_bound_constraints
        var_constraint = JuMP.constraint_object(var_bound_ref)
        var = var_constraint.func
        index = var.index
        lower_bound = var_constraint.set.lower
        lower_bounds[index.value] = lower_bound
    end

    var_equal_constraints = JuMP.all_constraints(node,JuMP.VariableRef,MOI.EqualTo{Float64})
    for var_equal_ref in var_equal_constraints
        var_constraint = JuMP.constraint_object(var_equal_ref)
        var = var_constraint.func
        index = var.index
        lower_bound = var_constraint.set.value
        lower_bounds[index.value] = lower_bound
    end

    #TODO sort?
    return lower_bounds
end

#####################################
# OBJECTIVE FUNCTION EVALUATION
#####################################
#x is an array of variable values returned from the solver
function eval_function(var::JuMP.VariableRef, x)
    return x[var.index.value]
end

function eval_function(aff::JuMP.GenericAffExpr{Float64,JuMP.VariableRef}, x)
    function_value = aff.constant
    for (var,coeff) in aff.terms
        # NOTE the implicit assumtion that VariableIndex values match up with
        # x indices. This is valid because in this wrapper ListOfVariableIndices
        # is always [1, ..., NumberOfVariables].
        function_value += coeff*x[var.index.value]
    end
    return function_value
end

function eval_function(quad::JuMP.GenericQuadExpr{Float64,JuMP.VariableRef}, x)
    function_value = quad.aff.constant
    for (var,coeff) in quad.aff.terms
        function_value += coeff*x[var.index.value]
    end
    for (terms,coeff) in quad.terms
        row_idx = terms.a.index
        col_idx = terms.b.index
		function_value += coeff*x[row_idx.value]*x[col_idx.value]
    end
    return function_value
end

function pips_eval_objective(d::JuMP.NLPEvaluator, x)
    # The order of the conditions is important. NLP objectives override regular objectives.
	m = d.m
    if has_nl_objective(m)
        return MOI.eval_objective(d, x)
    elseif JuMP.objective_function(m) !== nothing
        return eval_function(JuMP.objective_function(m), x)
    else
        return 0.0 # No objective function set. This could happen with FEASIBILITY_SENSE.
    end
end

#####################################
# OBJECTIVE GRADIENT EVALUATION
#####################################
function fill_gradient!(grad, x, var::JuMP.VariableRef)
    fill!(grad, 0.0)
    grad[var.index.value] = 1.0
	return
end

function fill_gradient!(grad, x, aff::JuMP.GenericAffExpr{Float64,JuMP.VariableRef})
    fill!(grad, 0.0)
	for	(var,coeff) in aff.terms
        grad[var.index.value] += coeff
    end
	return
end

function fill_gradient!(grad, x, quad::JuMP.GenericQuadExpr{Float64,JuMP.VariableRef})
    fill!(grad, 0.0)
    for	(var,coeff) in quad.aff.terms
        grad[var.index.value] += coeff
    end
    #for term in quad.quadratic_terms
	for (terms,coeff) in quad.terms
        row_idx = terms.a.index
        col_idx = terms.b.index
        if row_idx == col_idx
            grad[row_idx.value] += 2*coeff*x[row_idx.value]
        else
            grad[row_idx.value] += coeff*x[col_idx.value]
            grad[col_idx.value] += coeff*x[row_idx.value]
        end
    end
	return
end

function pips_eval_objective_gradient(d::JuMP.NLPEvaluator, grad, x)
	m = d.m
    if has_nl_objective(m)
        MOI.eval_objective_gradient(d, grad, x)
    elseif JuMP.objective_function(m) !== nothing
        fill_gradient!(grad, x, JuMP.objective_function(m))
    else
        fill!(grad, 0.0)
    end
    return
end

#####################################
# CONSTRAINT EVALUATION
#####################################
# Refers to local variables in eval_constraint() below.
macro eval_function(array_name)
    escrow = esc(:row)
    quote
        for constraint in $(esc(array_name))
			func = constraint.func
            $(esc(:g))[$escrow] = eval_function(func, $(esc(:x)))
            $escrow += 1
        end
    end
end

function pips_eval_constraint(d::JuMP.NLPEvaluator, g, x)
    row = 1
	con_data = getnode(d.m).ext[:constraint_data]
    @eval_function con_data.linear_le_constraints
    @eval_function con_data.linear_ge_constraints
	@eval_function con_data.linear_interval_constraints
    @eval_function con_data.linear_eq_constraints
    @eval_function con_data.quadratic_le_constraints
    @eval_function con_data.quadratic_ge_constraints
	@eval_function con_data.quadratic_interval_constraints
    @eval_function con_data.quadratic_eq_constraints
    nlp_g = view(g, row:length(g))
    MOI.eval_constraint(d, nlp_g, x)
    return
end

#####################
# JACOBIAN STRUCTURE
#####################
function append_to_jacobian_sparsity!(jacobian_sparsity, func::JuMP.GenericAffExpr{Float64,JuMP.VariableRef}, row)
	aff = func
    for term in keys(aff.terms)
        push!(jacobian_sparsity, (row, term.index.value))
    end
end

function append_to_jacobian_sparsity!(jacobian_sparsity, func::JuMP.GenericQuadExpr{Float64,JuMP.VariableRef}, row)
	quad = func
    for term in keys(quad.aff.terms)
        push!(jacobian_sparsity, (row, term.index.value))
    end
    for term in keys(quad.terms)
        row_idx = term.a.index
        col_idx = term.b.index
        if row_idx == col_idx
            push!(jacobian_sparsity, (row, row_idx.value))
        else
            push!(jacobian_sparsity, (row, row_idx.value))
            push!(jacobian_sparsity, (row, col_idx.value))
        end
    end
end

macro append_to_jacobian_sparsity(array_name)
    escrow = esc(:row)
    quote
        for constraint in $(esc(array_name))
			func = constraint.func
            append_to_jacobian_sparsity!($(esc(:jacobian_sparsity)), func, $escrow)
            $escrow += 1
        end
    end
end

function pips_jacobian_structure(d::JuMP.NLPEvaluator)
    num_nlp_constraints = length(d.m.nlp_data.nlconstr)
    if num_nlp_constraints > 0
		MOI.initialize(d,[:Grad,:Jac,:Hess])
        nlp_jacobian_sparsity = MOI.jacobian_structure(d)
    else
        nlp_jacobian_sparsity = []
    end

    jacobian_sparsity = Tuple{Int64,Int64}[]
    row = 1
	con_data = get_constraint_data(getnode(d.m))

    @append_to_jacobian_sparsity con_data.linear_le_constraints
    @append_to_jacobian_sparsity con_data.linear_ge_constraints
	@append_to_jacobian_sparsity con_data.linear_interval_constraints
    @append_to_jacobian_sparsity con_data.linear_eq_constraints
    @append_to_jacobian_sparsity con_data.quadratic_le_constraints
    @append_to_jacobian_sparsity con_data.quadratic_ge_constraints
	@append_to_jacobian_sparsity con_data.quadratic_interval_constraints
    @append_to_jacobian_sparsity con_data.quadratic_eq_constraints
    for (nlp_row, column) in nlp_jacobian_sparsity
        push!(jacobian_sparsity, (nlp_row + row - 1, column))
    end
    return jacobian_sparsity
end

#####################
# HESSIAN STRUCTURE
#####################
function has_nl_objective(m::JuMP.Model)
	if m.nlp_data == nothing
		return false
	elseif m.nlp_data.nlobj != nothing
		return true
	else
		return false
	end
end

append_to_hessian_sparsity!(hessian_sparsity, ::Union{JuMP.VariableRef,JuMP.GenericAffExpr}) = nothing

function append_to_hessian_sparsity!(hessian_sparsity, quad::JuMP.GenericQuadExpr{Float64,JuMP.VariableRef})
    for term in keys(quad.terms)
        push!(hessian_sparsity, (term.a.index.value,term.b.index.value))
    end
end

#NOTE: there can be duplicate entries
function pips_hessian_lagrangian_structure(d::JuMP.NLPEvaluator)
    hessian_sparsity = Tuple{Int64,Int64}[]
	m = d.m
	con_data = get_constraint_data(getnode(m))
	if m.nlp_data == nothing
	    append_to_hessian_sparsity!(hessian_sparsity, JuMP.objective_function(m))
	elseif m.nlp_data.nlobj == nothing
		append_to_hessian_sparsity!(hessian_sparsity, JuMP.objective_function(m))
	end
    for constraint in con_data.quadratic_le_constraints
		quad = constraint.func
        append_to_hessian_sparsity!(hessian_sparsity, quad)
    end
    for constraint in con_data.quadratic_ge_constraints
		quad = constraint.func
        append_to_hessian_sparsity!(hessian_sparsity, quad)
    end
	for constraint in con_data.quadratic_interval_constraints
		quad = constraint.func
		append_to_hessian_sparsity!(hessian_sparsity, quad)
	end
    for constraint in con_data.quadratic_eq_constraints
		quad = constraint.func
        append_to_hessian_sparsity!(hessian_sparsity, quad)
    end
    nlp_hessian_sparsity = MOI.hessian_lagrangian_structure(d)
    append!(hessian_sparsity, nlp_hessian_sparsity)
    return hessian_sparsity
end

#####################################
# JACOBIAN EVALUATION
#####################################
function fill_constraint_jacobian!(jac_values, start_offset, x, aff::JuMP.GenericAffExpr{Float64,JuMP.VariableRef})
    num_coefficients = length(aff.terms)
    #for i in 1:num_coefficients
	i = 1
	for coeff in values(aff.terms) #ordered dictionary
        jac_values[start_offset+i] = coeff
		i += 1
    end
    return num_coefficients
end

function fill_constraint_jacobian!(jac_values, start_offset, x, quad::JuMP.GenericQuadExpr{Float64,JuMP.VariableRef})
	aff = quad.aff
    num_affine_coefficients = length(aff.terms)
	i = 1
	for coeff in values(aff.terms)
        jac_values[start_offset+i] = coeff #quad.affine_terms[i].coefficient
		i += 1
    end
    num_quadratic_coefficients = 0

	for (terms,coeff) in quad.terms
        row_idx = terms.a.index
        col_idx = terms.b.index
        if row_idx == col_idx
            jac_values[start_offset+num_affine_coefficients+num_quadratic_coefficients+1] = 2*coeff*x[row_idx.value]
            num_quadratic_coefficients += 1
        else
            # Note that the order matches the Jacobian sparsity pattern.
            jac_values[start_offset+num_affine_coefficients+num_quadratic_coefficients+1] = coeff*x[col_idx.value]
            jac_values[start_offset+num_affine_coefficients+num_quadratic_coefficients+2] = coeff*x[row_idx.value]
            num_quadratic_coefficients += 2
        end
    end
    return num_affine_coefficients + num_quadratic_coefficients
end

# Refers to local variables in eval_constraint_jacobian() below.
macro fill_constraint_jacobian(array_name)
    esc_offset = esc(:offset)
    quote
        for constraint in $(esc(array_name))
			func = constraint.func
            $esc_offset += fill_constraint_jacobian!($(esc(:jac_values)),
                                                     $esc_offset, $(esc(:x)),
                                                     func)
        end
    end
end

function pips_eval_constraint_jacobian(d::JuMP.NLPEvaluator, jac_values, x)
    offset = 0
	m = d.m
	con_data = getnode(m).ext[:constraint_data]
    @fill_constraint_jacobian con_data.linear_le_constraints
    @fill_constraint_jacobian con_data.linear_ge_constraints
	@fill_constraint_jacobian con_data.linear_interval_constraints
    @fill_constraint_jacobian con_data.linear_eq_constraints
    @fill_constraint_jacobian con_data.quadratic_le_constraints
    @fill_constraint_jacobian con_data.quadratic_ge_constraints
	@fill_constraint_jacobian con_data.quadratic_interval_constraints
    @fill_constraint_jacobian con_data.quadratic_eq_constraints

    nlp_values = view(jac_values, 1+offset:length(jac_values))
    MOI.eval_constraint_jacobian(d, nlp_values, x)
    return
end

##########################################
# HESSIAN OF THE LAGRANGIAN EVALUATION
#########################################
function fill_hessian_lagrangian!(hess_values, start_offset, scale_factor,::Union{JuMP.VariableRef,JuMP.GenericAffExpr{Float64,JuMP.VariableRef},Nothing})
    return 0
end

function fill_hessian_lagrangian!(hess_values, start_offset, scale_factor, quad::JuMP.GenericQuadExpr{Float64,JuMP.VariableRef})
	i = 1
	for (terms,coeff) in quad.terms
		row_idx = terms.a.index
		col_idx = terms.b.index
		if row_idx == col_idx
			hess_values[start_offset + i] = 2*scale_factor*coeff
		else
			hess_values[start_offset + i] = scale_factor*coeff
		end
		i += 1
	end
    return length(quad.terms)
end

function pips_eval_hessian_lagrangian(d::JuMP.NLPEvaluator, hess_values, x, obj_factor, lambda)
    offset = 0
	m = d.m
	con_data = getnode(m).ext[:constraint_data]
	if !(has_nl_objective(m))
        offset += fill_hessian_lagrangian!(hess_values, 0, obj_factor, JuMP.objective_function(m))
    end
    for (i, constraint) in enumerate(con_data.quadratic_le_constraints)
		quad = constraint.func
        offset += fill_hessian_lagrangian!(hess_values, offset, lambda[i+quadratic_le_offset(con_data)], quad)
    end
    for (i, constraint) in enumerate(con_data.quadratic_ge_constraints)
		quad = constraint.func
        offset += fill_hessian_lagrangian!(hess_values, offset, lambda[i+quadratic_ge_offset(con_data)], quad)
    end
	for (i, constraint) in enumerate(con_data.quadratic_interval_constraints)
		quad = constraint.func
        offset += fill_hessian_lagrangian!(hess_values, offset, lambda[i+quadratic_interval_offset(con_data)], quad)
    end
    for (i, constraint) in enumerate(con_data.quadratic_eq_constraints)
		quad = constraint.func
        offset += fill_hessian_lagrangian!(hess_values, offset, lambda[i+quadratic_eq_offset(con_data)], quad)
    end
    nlp_values = view(hess_values, 1 + offset : length(hess_values))
    nlp_lambda = view(lambda, 1 + nlp_constraint_offset(con_data) : length(lambda))
    MOI.eval_hessian_lagrangian(d, nlp_values, x, obj_factor, nlp_lambda)
end


text:
using OAuth
using Compat
using FactCheck

facts("oauth_timestamp") do
    context("returns a string") do
        @fact typeof(oauth_timestamp()) <: AbstractString --> true
    end

    context("returns a value representing a time after 2014-01-25 20:25:00") do
        @compat @fact parse(Int, oauth_timestamp()) > 1422235471 --> true
    end
end

facts("ouath_nonce") do
    context("returns a string") do
        @fact typeof(oauth_nonce(15)) <: AbstractString --> true
    end

    context("with a length equal to the parameter length") do
        @fact length(oauth_nonce(15)) --> 15
        @fact length(oauth_nonce(20)) --> 20
        @fact length(oauth_nonce(25)) --> 25
        @fact length(oauth_nonce(30)) --> 30
        @fact length(oauth_nonce(32)) --> 32
    end
end

facts("oauth_sign_hmac_sha1") do
    context("provides a consistent string") do
        expected = "WqKCG5iyhFiES3fWYVdWJWwinaY="
        @fact oauth_sign_hmac_sha1("randy", "zwitch") --> expected
    end
end

facts("oauth_signing_key") do
    context("returns a concatenation of values, seperated by &") do
        result = oauth_signing_key("9djdj82h48djs9d2", "kkk9d7dh3k39sjv7")
        expected = "9djdj82h48djs9d2&kkk9d7dh3k39sjv7"
        @fact result --> expected
    end
end

facts("oauth_signature_base_string") do
    context("returns a concatinated and percent-encoded string") do
        result = oauth_signature_base_string(
            "POST", "http://example.com", "?julia=fast&lang=elegant"
        )
        expected = "POST&http%3A%2F%2Fexample.com&%3Fjulia%3Dfast%26lang%3Delegant"
        @fact result --> expected
    end
end

facts("oauth_percent_encode_keys!") do
    context("replaces un-encoded keys with their encoded versions") do
        params   = @compat Dict("badkey!"   => "value", "goodkey" => "value")
        expected = @compat Dict("badkey%21" => "value", "goodkey" => "value")

        @fact oauth_percent_encode_keys!(params) --> expected
    end
end

facts("oauth_serialize_url_parameters") do
    context("returns an & concatinated string of key=value") do
        params = @compat Dict("language" => "julia", "result" => "awesome")
        expected = "language=julia&result=awesome"
        @fact oauth_serialize_url_parameters(params) --> expected
    end
end

facts("encodeURI!") do
    context("escapes all values in the parameters that are strings") do
        params   = @compat Dict("iv" => 10, "s" => "value!")
        expected = @compat Dict("iv" => 10, "s" => "value%21")

        @fact encodeURI!(params) --> expected
    end
end

facts("oauth_body_hash_encode") do
    context("returns the base64 encoded SHA1 digest of the data") do
        result = oauth_body_hash_encode("Hello, World!")
        expected = "CgqfKmdylCVXq1NV12r0Qvj2XgE="
        @fact result --> expected
    end
end

facts("oauth_body_hash_data") do
    context("returns a string of the oauth_body_hash key-value pair ") do
        result = oauth_body_hash_data("Hello, World!")
        encoded_hash = "CgqfKmdylCVXq1NV12r0Qvj2XgE="
        expected = "oauth_body_hash=$encoded_hash"

        @fact result --> expected
    end
end

facts("oauth_body_hash_file") do
    context("returns a string of the oauth_body_hash key-value pair ") do
        test_file = joinpath(dirname(@__FILE__), "auth_body_hash_file.txt")
        result = oauth_body_hash_file(test_file)
        encoded_hash = "CgqfKmdylCVXq1NV12r0Qvj2XgE="
        expected = "oauth_body_hash=$encoded_hash"

        @fact result --> expected
    end
end

#TODO
#oauth_header()

#TODO
#oauth_request_resource()

FactCheck.exitstatus()


text:
"""
    prune_constraint!(com::CS.CoM, constraint::BasicConstraint, fct::SAF{T}, set::CPE.DifferentFrom{T}; logs = true) where T <: Real

Reduce the number of possibilities given the not equal constraint.
Return if still feasible and throw a warning if infeasible and `logs` is set to `true`
"""
function prune_constraint!(
    com::CS.CoM,
    constraint::LinearConstraint,
    fct::SAF{T},
    set::CPE.DifferentFrom{T};
    logs = true,
) where {T<:Real}
    indices = constraint.indices

    # check if only one variable is variable
    nfixed = count(v -> isfixed(v), com.search_space[constraint.indices])
    if nfixed >= length(constraint.indices) - 1
        search_space = com.search_space
        sum = -set.value + fct.constant
        unfixed_i = 0
        for (i, vidx) in enumerate(indices)
            if isfixed(search_space[vidx])
                sum += CS.value(search_space[vidx]) * fct.terms[i].coefficient
            else
                unfixed_i = i
            end
        end
        # all fixed
        if unfixed_i == 0
            return get_approx_discrete(sum) != zero(T)
        end
        not_val = -sum
        not_val /= fct.terms[unfixed_i].coefficient
        # if not integer
        if !isapprox_discrete(com, not_val)
            return true
        end
        not_val = get_approx_discrete(not_val)
        # if can be removed => is removed and is feasible otherwise not feasible
        if has(search_space[indices[unfixed_i]], not_val)
            return rm!(com, search_space[indices[unfixed_i]], not_val)
        else
            return true
        end
    end
    return true
end

"""
still_feasible(com::CoM, constraint::LinearConstraint, fct::MOI.ScalarAffineFunction{T}, set::CPE.DifferentFrom{T}, vidx::Int, value::Int) where T <: Real

Return whether the `not_equal` constraint can be still fulfilled.
"""
function still_feasible(
    com::CoM,
    constraint::LinearConstraint,
    fct::SAF{T},
    set::CPE.DifferentFrom{T},
    vidx::Int,
    value::Int,
) where {T<:Real}
    indices = constraint.indices
    # check if only one variable is variable
    nfixed = count(v -> isfixed(v), com.search_space[indices])
    if nfixed >= length(indices) - 1
        search_space = com.search_space
        sum = -set.value + fct.constant
        unfixed_i = 0
        for (i, cvidx) in enumerate(indices)
            if isfixed(search_space[cvidx])
                sum += CS.value(search_space[cvidx]) * fct.terms[i].coefficient
            elseif vidx == cvidx
                sum += value * fct.terms[i].coefficient
            else
                unfixed_i = i
            end
        end
        # all fixed => must be != 0
        if unfixed_i == 0
            # not discrete => not 0 => feasible
            if !isapprox_discrete(com, sum)
                return true
            end
            return get_approx_discrete(sum) != zero(T)
        end
        # if not fixed there is a value which fulfills the != constraint
        return true
    end
    return true
end

function is_constraint_solved(
    constraint::LinearConstraint,
    fct::SAF{T},
    set::CPE.DifferentFrom{T},
    values::Vector{Int},
) where {T<:Real}

    indices = [t.variable_index.value for t in fct.terms]
    coeffs = [t.coefficient for t in fct.terms]
    return get_approx_discrete(sum(values .* coeffs) + fct.constant) != set.value
end

"""
    is_constraint_violated(
        com::CoM,
        constraint::LinearConstraint,
        fct::SAF{T},
        set::CPE.DifferentFrom{T},
    ) where {T<:Real}

Checks if the constraint is violated as it is currently set. This can happen inside an
inactive reified or indicator constraint.
"""
function is_constraint_violated(
    com::CoM,
    constraint::LinearConstraint,
    fct::SAF{T},
    set::CPE.DifferentFrom{T},
) where {T<:Real}
    if all(isfixed(var) for var in com.search_space[constraint.indices])
        return !is_constraint_solved(
            constraint,
            fct,
            set,
            [CS.value(var) for var in com.search_space[constraint.indices]],
        )
    end
    return false
end


text:
# ------------ Common support errors, checked statically. ------------ #

abstract type SupportException <: Exception end

# ------------ Mismatch measures across flow of control paths ------------ #

struct MeasureMismatch <: SupportException
    types::Dict
    violations::Set{Symbol}
end
function Base.showerror(io::IO, e::MeasureMismatch)
    println("\u001b[31m(SupportException):\u001b[0m Base measure mismatch.")
    for (k, v) in e.types
        println(io, " $k => $(map(l -> pretty(l), v))")
    end
    println(io, " Violations: $(e.violations)")
    println(io, "\u001b[32mFix: ensure that base measures match for addresses shared across branches in your model.\u001b[0m")
end

# Checks that addresses across flow of control paths share the same base measure.
# Assumes that the input IR has been inferred (using e.g. the trace type inference).
function check_branch_support(tr)
    types = Dict()
    for (v, st) in tr
        st.expr isa Expr || continue
        st.expr.head == :call || continue
        st.expr.args[1] == trace || continue
        st.expr.args[2] isa QuoteNode || continue
        addr = st.expr.args[2].value
        if haskey(types, addr)
            st.type isa NamedTuple ? push!(types[addr], st.type) : push!(types[addr], supertype(st.type))
        else
            if st.type isa NamedTuple
                Any[_lift(st.type)]
            else
                Any[supertype(_lift(st.type))]
            end
        end
    end
    se = MeasureMismatch(types, Set{Symbol}([]))
    for (addr, supports) in types
        length(supports) == 1 && continue
        foldr(==, supports) || begin
            push!(se.violations, addr)
        end
    end
    se
end

# ------------ Duplicate addresses across flow of control paths ------------ #

struct DuplicateAddresses <: SupportException
    violations::Set{Symbol}
    paths::Set
end
function Base.showerror(io::IO, e::DuplicateAddresses)
    println("\u001b[31m(SupportException):\u001b[0m Duplicate addresses along same flow of control path in model program.")
    println(io, " Violations: $(e.violations)")
    println(io, " On flow of control paths:")
    for p in e.paths
        println(io, " $p")
    end
    println(io, "\u001b[32mFix: ensure that all addresses in any execution path through the program are unique.\u001b[0m")
end

# Checks for duplicate symbols along each flow of control path.
# Expects untyped IR.
function check_duplicate_symbols(ir, paths)
    keys = Set{Symbol}([])
    blks = map(blk -> IRTools.BasicBlock(blk), blocks(ir))
    addresses = Dict( path => Symbol[] for path in paths )
    de = DuplicateAddresses(Set{Symbol}([]), Set([]))
    for (v, st) in ir
        st.expr isa Expr || continue
        st.expr.head == :call || continue
        unwrap(st.expr.args[1]) == :trace || continue
        st.expr.args[2] isa QuoteNode || continue
        addr = st.expr.args[2].value

        # Fix: can be much more efficient.
        relevant = Iterators.filter(Iterators.enumerate(blks)) do (ind, blk)
            st in blk.stmts
        end

        # Filter paths, then check if already seen on path.
        for (ind, blk) in relevant
            for p in filter(p -> ind in p, paths)
                if addr in addresses[p]
                    push!(de.violations, addr)
                    push!(de.paths, p)
                else
                    push!(addresses[p], addr)
                end
            end
        end
    end
    de
end

# ------------ Pipeline ------------ #

function support_checker(absint_ctx::InterpretationContext, fn, arg_types...)
    println("\u001b[34m\e[1m   Method name:\u001b[0m \e[4m$(fn)\u001b[0m")
    println("\u001b[34m\e[1m   Method argument types:\u001b[0m $(arg_types)\u001b[0m\n")
    errs = Exception[]
    ir = lower_to_ir(fn, arg_types...)
    paths = get_control_flow_paths(ir)
    push!(errs, check_duplicate_symbols(ir, paths))
    tr = infer_support_types(absint_ctx, fn, arg_types...)
    tr isa Missing ? println("\u001b[33m ? (SupportChecker): model program could not be traced.\n    The following checks cannot be run:\n\t* Branch support checks\u001b[0m\n") : push!(errs, check_branch_support(tr))
    any(map(errs) do err
            if isempty(err.violations)
                false
            else
                Base.showerror(stdout, err)
                true
            end
        end) ? error("SupportError found.") : println("\u001b[32m ✓ (SupportChecker): no errors detected by static checks.\u001b[0m\n")
    !(tr isa Missing) && begin
        if !control_flow_check(tr)
            println("\u001b[33m ? (SupportChecker): Detected control flow in model IR.\n    Static trace typing requires that control flow be extracted into combinators.\n    Proceeding to compile with \e[1mMissing\u001b[0m \u001b[33mtrace type.\u001b[0m\n")
            return missing
        else
            try
                return trace_type(tr)
            catch e
                println("\u001b[33m ? (SupportChecker): Failed to compute trace type. Caught:\n$e.\n\nProceeding to compile with \e[1mMissing\u001b[0m \u001b[33mtrace type.\u001b[0m")
                return missing
            end
        end
    end
    return missing
end


text:
@testset "Subplots" begin

    @testset "rows=1, cols=2" begin
        have1 = Layout(Subplots(rows=1, cols=2))

        # shared_xaxes does nothing with 1 column
        have2 = Layout(Subplots(rows=1, cols=2, shared_xaxes=false))

        for have in (have1, have2)
            @test have[:xaxis] == attr(anchor="y", domain=(0.0, 0.45)).fields
            @test have[:yaxis] == attr(anchor="x", domain=(0.0, 1.0)).fields
            @test have[:xaxis2] == attr(anchor="y2", domain=(0.55, 1.0)).fields
            @test have[:yaxis2] == attr(anchor="x2", domain=(0.0, 1.0)).fields
        end
    end

    @testset "rows=1, cols=2, shared_yaxes=true" begin
        have = Layout(Subplots(rows=1, cols=2, shared_yaxes=true))
        @test have[:xaxis] == attr(anchor="y", domain=(0.0, 0.45)).fields
        @test have[:yaxis] == attr(anchor="x", domain=(0.0, 1.0)).fields
        @test have[:xaxis2] == attr(anchor="y2", domain=(0.55, 1.0)).fields
        @test have[:yaxis2] == attr(anchor="x2", domain=(0.0, 1.0), matches="y", showticklabels=false).fields
    end

    @testset "rows=2, cols=1" begin
        have1 = Layout(Subplots(rows=2, cols=1))

        # shared_yaxes does nothing with 1 column
        have2 = Layout(Subplots(rows=2, cols=1, shared_yaxes=true))

        for have in (have1, have2)
            @test have[:xaxis] == attr(anchor="y", domain=(0.0, 1.0)).fields
            @test have[:yaxis] == attr(anchor="x", domain=(0.575, 1.0)).fields
            @test have[:xaxis2] == attr(anchor="y2", domain=(0.0, 1.0)).fields
            @test have[:yaxis2] == attr(anchor="x2", domain=(0.0, 0.425)).fields
        end
    end

    @testset "rows=2, cols=1, shared_yaxes=true" begin
        have = Layout(Subplots(rows=2, cols=1, shared_xaxes=true))
        @test have[:xaxis] == attr(anchor="y", domain=(0.0, 1.0), matches="x2", showticklabels=false).fields
        @test have[:yaxis] == attr(anchor="x", domain=(0.575, 1.0)).fields
        @test have[:xaxis2] == attr(anchor="y2", domain=(0.0, 1.0)).fields
        @test have[:yaxis2] == attr(anchor="x2", domain=(0.0, 0.425)).fields
    end

    @testset "can get subplot kind for trace type" begin
        @test PlotlyBase.get_subplotkind_from_trace_type.(PlotlyBase._TRACE_TYPES) isa Vector{String}
    end

end


text:
keep_entry = CompatHelper.KeepEntry()
drop_entry = CompatHelper.DropEntry()
new_entry = CompatHelper.NewEntry()

hostname = "hostname"
github = GitHubActions(; clone_hostname=hostname)
gitlab = GitLabCI(; clone_hostname=hostname)

@testset "body_info -- $(entry)" for (entry, expected) in [
    (keep_entry, "keeps"), (drop_entry, "drops"), (new_entry, " brand new")
]
    name = "foobar"
    result = CompatHelper.body_info(entry, name)
    @test contains(result, expected)

    if entry isa CompatHelper.NewEntry
        @test contains(result, name)
    end
end

@testset "title_parenthetical -- $(entry)" for (entry, expected) in [
    (keep_entry, "keep"), (drop_entry, "drop"), (new_entry, "")
]
    result = CompatHelper.title_parenthetical(entry)

    if !(entry isa CompatHelper.NewEntry)
        @test contains(result, expected)
    else
        @test isempty(result)
    end
end

@testset "new_compat_entry" begin
    cases = Dict(
        keep_entry => [
            (" old ", " new ", "old, new"),
            ("old", "new", "old, new"),
            ("Old", "New", "Old, New"),
            ("OLD", "NEW", "OLD, NEW"),
            (nothing, "NEW", "NEW"),
        ],
        drop_entry => [
            (" old ", " new ", "new"),
            ("old", "new", "new"),
            ("Old", "New", "New"),
            ("OLD", "NEW", "NEW"),
            (nothing, "NEW", "NEW"),
        ],
        new_entry => [
            (" old ", " new ", "new"),
            ("old", "new", "new"),
            ("Old", "New", "New"),
            ("OLD", "NEW", "NEW"),
            (nothing, "NEW", "NEW"),
        ],
    )

    entries = collect(keys(cases))

    @testset "$(entry)" for entry in entries
        for case in cases[entry]
            old_compat, new_compat, expected = case
            result = CompatHelper.new_compat_entry(entry, old_compat, new_compat)

            @test result == expected
        end
    end
end

@testset "compat_version_number -- $(vn)" for (vn, expected) in [
    (VersionNumber("1.0.0"), "1"),
    (VersionNumber("1.1.1"), "1"),
    (VersionNumber("1.1.0"), "1"),
    (VersionNumber("0.1.0"), "0.1"),
    (VersionNumber("0.1.1"), "0.1"),
    (VersionNumber("0.0.1"), "0.0.1"),
    (VersionNumber("0.0.0"), "0.0.0"),
]
    @test CompatHelper.compat_version_number(vn) == expected
end

@testset "subdir_string -- $(subdir)" for (subdir, expected) in [
    ("foobar", "foobar"), ("foo/bar", "bar"), ("1", "1"), ("", "")
]
    if !isempty(subdir)
        @test contains(CompatHelper.subdir_string(subdir), expected)
    else
        @test CompatHelper.subdir_string(subdir) === ""
    end
end

@testset "skip_equality_specifiers" begin
    cases = [
        (false, "=", true)
        (false, ">=", false)
        (false, "<=", false)
        (false, ">", false)
        (false, "<", false)
    ]

    for case in cases
        bump_specifier, verbatim, expected = case
        @test CompatHelper.skip_equality_specifiers(bump_specifier, verbatim) == expected

        # If bump_compat_containing_equality_specifier is set to true, always return back false
        @test CompatHelper.skip_equality_specifiers(!bump_specifier, verbatim) == false
    end
end

@testset "pr_info -- $(typeof(case[1]))" for case in [
    (nothing, "add new compat entry for", "pull request sets the compat")
    ("", "bump compat for", "pull request changes the compat")
]
    verbatim, expected_title, expected_body = case
    title, body = CompatHelper.pr_info(verbatim, "", "", "", "", "", "", "")

    @test contains(title, expected_title)
    @test contains(body, expected_body)
end

@testset "create_new_pull_request" begin
    @testset "GitHub" begin
        apply(gh_pr_patch) do
            result, n = CompatHelper.create_new_pull_request(
                GitHub.GitHubAPI(),
                GitHub.Repo(; owner=GitHub.User(; login="username"), name="repo"),
                "new_branch",
                "master_branch",
                "title",
                "body",
            )

            @test isnothing(n)
            @test result isa GitHub.PullRequest
        end
    end

    @testset "GitLab" begin
        apply(gl_pr_patch) do
            result, n = CompatHelper.create_new_pull_request(
                GitLab.GitLabAPI(),
                GitLab.Project(; id=1),
                "new_branch",
                "master_brach",
                "title",
                "body",
            )

            @test isnothing(n)
            @test result isa GitLab.MergeRequest
        end
    end
end

@testset "get_url_with_auth" begin
    @testset "GitHub" begin
        result = CompatHelper.get_url_with_auth(
            GitHub.GitHubAPI(; token=GitHub.Token("token")),
            github,
            GitHub.Repo(; full_name="full_name"),
        )

        @test result == "https://x-access-token:token@hostname/full_name.git"
    end

    @testset "GitLab" begin
        result = CompatHelper.get_url_with_auth(
            GitLab.GitLabAPI(; token=GitLab.OAuth2Token("token")),
            gitlab,
            GitLab.Project(; path_with_namespace="full_name"),
        )

        @test result == "https://oauth2:token@hostname/full_name.git"
    end
end

@testset "get_url_for_ssh" begin
    @testset "GitHub" begin
        result = CompatHelper.get_url_for_ssh(
            GitHub.GitHubAPI(), github, GitHub.Repo(; full_name="full_name")
        )

        @test result == "git@hostname:full_name.git"
    end

    @testset "GitLab" begin
        result = CompatHelper.get_url_for_ssh(
            GitLab.GitLabAPI(), gitlab, GitLab.Project(; path_with_namespace="full_name")
        )

        @test result == "git@hostname:full_name.git"
    end
end

@testset "continue_with_pr" begin
    @testset "default passing case" begin
        pass_dep = CompatHelper.DepInfo(
            CompatHelper.Package("PackageA", UUID(1)); latest_version=VersionNumber(1)
        )
        @test CompatHelper.continue_with_pr(pass_dep, false)
        @test CompatHelper.continue_with_pr(pass_dep, true)
    end

    @testset "latest version in version spec" begin
        dep = CompatHelper.DepInfo(
            CompatHelper.Package("PackageA", UUID(1));
            latest_version=VersionNumber(1),
            version_spec=CompatHelper.VersionSpec(["0.9", "1.0"]),
        )
        @test !CompatHelper.continue_with_pr(dep, false)
        @test !CompatHelper.continue_with_pr(dep, true)
    end

    @testset "equality specifier" begin
        dep = CompatHelper.DepInfo(
            CompatHelper.Package("PackageA", UUID(1));
            version_verbatim="= 1.2",
            latest_version=VersionNumber(2),
        )
        @test !CompatHelper.continue_with_pr(dep, false)
        @test CompatHelper.continue_with_pr(dep, true)
    end

    @testset "no latest version" begin
        dep = CompatHelper.DepInfo(CompatHelper.Package("PackageA", UUID(1)))
        @test !CompatHelper.continue_with_pr(dep, false)
        @test !CompatHelper.continue_with_pr(dep, true)
    end
end

@testset "create_ssh_private_key" begin
    mktempdir() do tmpdir
        withenv(CompatHelper.PRIVATE_SSH_ENVVAR => "foo") do
            pkey = apply(decode_pkey_patch) do
                CompatHelper.create_ssh_private_key(tmpdir)
            end

            @test isfile(pkey)
            open(pkey, "r") do io
                @test read(io, String) == "pkey_info\n"
            end
        end
    end
end

@testset "force_ci_trigger" begin
    @testset "GitLab" begin
        mktempdir() do f
            cd(f) do
                run(`git init`)

                run(`touch foobar.txt`)
                CompatHelper.git_add()
                @test CompatHelper.git_commit("Message")

                run(`touch bazbar.txt`)
                CompatHelper.git_add()
                @test CompatHelper.git_commit("Message2")

                hash = read(`git rev-parse HEAD`, String)

                CompatHelper.force_ci_trigger(GitLab.GitLabAPI(), "title", "master", "pkey")
                new_hash = read(`git rev-parse HEAD`, String)
                @test hash == new_hash
            end
        end
    end

    @testset "GitHub" begin
        @testset "with pkey" begin
            mktempdir() do f
                cd(f) do
                    run(`git init`)

                    run(`touch foobar.txt`)
                    CompatHelper.git_add()
                    @test CompatHelper.git_commit("Message")

                    run(`touch bazbar.txt`)
                    CompatHelper.git_add()
                    @test CompatHelper.git_commit("Message2")

                    hash = read(`git rev-parse HEAD`, String)

                    apply(git_push_patch) do
                        CompatHelper.force_ci_trigger(
                            GitHub.GitHubAPI(), "title", "master", "pkey"
                        )
                    end
                    new_hash = read(`git rev-parse HEAD`, String)
                    @test hash != new_hash
                end
            end
        end

        @testset "without pkey" begin
            mktempdir() do f
                cd(f) do
                    run(`git init`)

                    run(`touch foobar.txt`)
                    CompatHelper.git_add()
                    @test CompatHelper.git_commit("Message")

                    run(`touch bazbar.txt`)
                    CompatHelper.git_add()
                    @test CompatHelper.git_commit("Message2")

                    hash = read(`git rev-parse HEAD`, String)

                    apply(git_push_patch) do
                        CompatHelper.force_ci_trigger(
                            GitHub.GitHubAPI(), "title", "master", nothing
                        )
                    end
                    new_hash = read(`git rev-parse HEAD`, String)
                    @test hash == new_hash
                end
            end
        end
    end
end

@testset "modify_project_toml" begin
    mktempdir() do tmpdir
        # Lets copy our test Project.toml to the tmpdir for this test
        src = joinpath(@__DIR__, "..", "deps", "Project.toml")
        dst = joinpath(tmpdir, "Project.toml")
        cp(src, dst; force=true)

        project = TOML.parsefile(dst)
        @test !haskey(project["compat"], "PackageA")

        CompatHelper.modify_project_toml("PackageA", tmpdir, "= 1.2", false)

        project = TOML.parsefile(dst)
        @test project["compat"]["PackageA"] == "= 1.2"
    end
end

@testset "bump_package_version" begin
    @testset "minor bump" begin
        mock_toml = Dict("version" => "1.4.5")

        @test mock_toml["version"] == "1.4.5"
        CompatHelper.bump_package_version!(mock_toml)
        @test mock_toml["version"] == "1.5.0"
    end

    @testset "patch bump" begin
        mock_toml = Dict("version" => "0.5.2")

        @test mock_toml["version"] == "0.5.2"
        CompatHelper.bump_package_version!(mock_toml)
        @test mock_toml["version"] == "0.5.3"
    end

    @testset "dev prerelease" begin
        mock_toml = Dict("version" => "1.2.3-DEV")

        @test mock_toml["version"] == "1.2.3-DEV"
        CompatHelper.bump_package_version!(mock_toml)
        @test mock_toml["version"] == "1.2.3-DEV"
    end

    @testset "version undefined" begin
        mock_toml = Dict()
        CompatHelper.bump_package_version!(mock_toml)
        @test isempty(mock_toml)
    end
end

@testset "cc_mention_user" begin
    @testset "GitHub" begin
        apply(gh_comment_patch) do
            withenv("GITHUB_ACTOR" => "username") do
                result, n = CompatHelper.cc_mention_user(
                    GitHub.GitHubAPI(),
                    GitHub.Repo(; owner=GitHub.User(; login="username"), name="repo"),
                    GitHub.PullRequest(; id=1),
                )

                @test isnothing(n)
                @test result isa GitHub.Comment
            end
        end
    end

    @testset "GitLab" begin
        apply(gl_comment_patch) do
            withenv("GITLAB_USER_LOGIN" => "username") do
                result, n = CompatHelper.cc_mention_user(
                    GitLab.GitLabAPI(), GitLab.Project(; id=1), GitLab.MergeRequest(; iid=1)
                )

                @test isnothing(n)
                @test result isa GitLab.Note
            end
        end
    end
end

@testset "unsub_from_pr" begin
    @testset "GitHub" begin
        @test_throws ErrorException(
            "GitForge.GitHub.GitHubAPI has not implemented this function"
        ) CompatHelper.unsub_from_pr(
            GitHub.GitHubAPI(), GitHub.PullRequest(; repo=GitHub.Repo(; id=1), id=1)
        )
    end

    @testset "GitLab" begin
        apply(gl_unsub_patch) do
            result, n = CompatHelper.unsub_from_pr(
                GitLab.GitLabAPI(), GitLab.MergeRequest(; project_id=1, iid=1)
            )

            @test isnothing(n)
            @test result isa GitLab.MergeRequest
        end
    end
end

@testset "make_pr_for_new_version" begin
    @testset "latest_version === nothing" begin
        options = CompatHelper.Options()
        subdir = only(options.subdirs)
        @test isnothing(
            CompatHelper.make_pr_for_new_version(
                GitHub.GitHubAPI(),
                GitHub.Repo(),
                CompatHelper.DepInfo(CompatHelper.Package("PackageA", UUID(1))),
                CompatHelper.KeepEntry(),
                CompatHelper.GitHubActions();
                options,
                subdir,
            ),
        )
    end

    @testset "pr_title exists" begin
        apply(pr_titles_mock) do
            options = CompatHelper.Options()
            subdir = only(options.subdirs)
            @test isnothing(
                CompatHelper.make_pr_for_new_version(
                    GitHub.GitHubAPI(; token=GitHub.Token("token")),
                    GitHub.Repo(),
                    CompatHelper.DepInfo(
                        CompatHelper.Package("PackageA", UUID(1));
                        latest_version=VersionNumber(1),
                        version_verbatim="0.9",
                    ),
                    CompatHelper.KeepEntry(),
                    CompatHelper.GitHubActions();
                    options,
                    subdir,
                ),
            )
        end
    end

    @testset "Successful Run" begin
        mktempdir() do tmpdir
            # Create a temp git repo
            cd(tmpdir) do
                run(`git init`)

                # Lets copy our test Project.toml to the tmpdir for this test
                src = joinpath(@__DIR__, "..", "deps", "Project.toml")
                dst = joinpath(tmpdir, "Project.toml")
                cp(src, dst; force=true)

                CompatHelper.git_add()
                CompatHelper.git_commit("msg")
            end

            cd(@__DIR__) do
                patches = [
                    pr_titles_mock,
                    git_push_patch,
                    gh_pr_patch,
                    make_clone_https_patch(tmpdir),
                    make_clone_ssh_patch(tmpdir),
                    decode_pkey_patch,
                ]

                apply(patches) do
                    # HTTPS
                    # Make sure PRIVATE_SSH_ENVVAR is unset
                    if haskey(ENV, CompatHelper.PRIVATE_SSH_ENVVAR)
                        delete!(ENV, CompatHelper.PRIVATE_SSH_ENVVAR)
                    end

                    options = CompatHelper.Options()
                    subdir = only(options.subdirs)
                    pr = CompatHelper.make_pr_for_new_version(
                        GitHub.GitHubAPI(; token=GitHub.Token("token")),
                        GitHub.Repo(;
                            owner=GitHub.User(; login="username"), name="PackageB"
                        ),
                        CompatHelper.DepInfo(
                            CompatHelper.Package("PackageB", UUID(1));
                            latest_version=VersionNumber(2),
                            version_verbatim="1.2",
                        ),
                        CompatHelper.KeepEntry(),
                        CompatHelper.GitHubActions();
                        options,
                        subdir,
                    )
                    @test pr isa GitHub.PullRequest

                    # SSH
                    withenv(CompatHelper.PRIVATE_SSH_ENVVAR => "foo") do
                        options = CompatHelper.Options()
                        subdir = only(options.subdirs)
                        pr = CompatHelper.make_pr_for_new_version(
                            GitHub.GitHubAPI(; token=GitHub.Token("token")),
                            GitHub.Repo(;
                                owner=GitHub.User(; login="username"), name="PackageC"
                            ),
                            CompatHelper.DepInfo(
                                CompatHelper.Package("PackageC", UUID(1));
                                latest_version=VersionNumber(3),
                                version_verbatim="2.1",
                            ),
                            CompatHelper.KeepEntry(),
                            CompatHelper.GitHubActions();
                            options,
                            subdir,
                        )
                        @test pr isa GitHub.PullRequest
                    end
                end
            end
        end
    end
end


text:
using Test
using RobotDynamics
using StaticArrays
using ForwardDiff
using LinearAlgebra
using FiniteDiff
using Random
using Rotations
const RD = RobotDynamics

include("cartpole_model.jl")

## Test the custom LU method
function testcustomlu()
    n,m = 4,2
    J1 = randn(n, n+m)
    A = @view J1[:,1:n]
    A2 = copy(A)

    ipiv = similar(A, BLAS.BlasInt, min(n, n+m))
    # @allocated LAPACK.getrf!(A, ipiv)
    allocs = @allocated F = lu!(A, ipiv)
    F2 = lu!(A)
    @test F2.L ≈ F2.L
    @test F2.U ≈ F2.U
    @test allocs == 0
end
testcustomlu()

##
@testset "Implicit to Explicit" begin
model = Cartpole()
integrator = RD.ImplicitMidpoint(model)
dmodel = RD.DiscretizedDynamics(model, integrator)
dmodel_rk4 = RD.DiscretizedDynamics(model, RD.RK4(model))
n,m = RD.dims(model)
x1 = zeros(n)
u1 = fill(1.0, m)
dt = 0.01
z1 = KnotPoint{n,m}(x1, u1, 0.0, dt)
x2_rk4 = RD.discrete_dynamics(dmodel_rk4, z1)

function midpoint_residual(x2, z1)
    n,m = RD.dims(z1)
    v = zeros(eltype(x2), n+m)
    z2 = RD.StaticKnotPoint(z1, v)
    RD.setstate!(z2, x2)
    RD.dynamics_error(dmodel, z2, z1)
end

function newton_solve(v)
    z = RD.StaticKnotPoint(z1, v)
    n,m = RD.dims(z)
    r(xn) = midpoint_residual(xn, z)

    xn = copy(RD.state(z))
    for i = 1:10
        res = r(xn)
        if norm(res) < 1e-10
            break
        end

        ∇r = ForwardDiff.jacobian(r, xn)
        xn .-= ∇r\res
    end
    return xn
end
x2 = newton_solve(RD.getdata(z1))
@test norm(midpoint_residual(x2, z1)) < √eps()

# Test in-place evaluation
x2_inplace = copy(x1)
RD.discrete_dynamics!(dmodel, x2_inplace, z1)
@test norm(midpoint_residual(x2_inplace, z1)) < √eps()
x2_inplace .= 0
RD.evaluate!(dmodel, x2_inplace, z1)
@test norm(midpoint_residual(x2_inplace, z1)) < √eps()

# Test static evaluation
x1 = [1,pi,1.3,1.1]
z1 = KnotPoint{n,m}(x1, u1, 0.0, dt)
z1s = KnotPoint(SVector{n}(x1), SVector{m}(u1), z1.t, z1.dt)
x2_static = RD.discrete_dynamics(dmodel, z1s)
@test norm(midpoint_residual(x2_static, z1s)) < √eps()
x2_static = RD.evaluate(dmodel, z1s)
@test norm(midpoint_residual(x2_static, z1s)) < √eps()

# Test Jacobian
J = zeros(n, n+m)
y = zeros(n)
diff = RD.default_diffmethod(dmodel)
@test diff isa RD.ImplicitFunctionTheorem
@test diff === RD.ImplicitFunctionTheorem(RD.UserDefined())

using Random
Random.seed!(1)
for i = 1:4
    x = randn(n)
    u = randn(m)
    z1 = KnotPoint{n,m}(x, u, 0.0, dt)
    Jfd = FiniteDiff.finite_difference_jacobian(newton_solve, RD.getdata(z1))

    if iseven(i)
        RD.discrete_dynamics!(dmodel, y, z1)
    end
    RD.jacobian!(RD.InPlace(), diff, dmodel, J, y, z1)
    @test norm(Jfd - J) < 1e-4
end

for i = 1:4
    x = randn(n)
    u = randn(m)
    z1 = KnotPoint{n,m}(x, u, 0.0, dt)
    z1s = KnotPoint(SVector{n}(x), SVector{m}(u), z1.t, z1.dt)
    Jfd = FiniteDiff.finite_difference_jacobian(newton_solve, RD.getdata(z1))

    if iseven(i)
        RD.discrete_dynamics(dmodel, z1s)
    end
    RD.jacobian!(RD.StaticReturn(), diff, dmodel, J, y, z1s)
    @test norm(Jfd - J) < 1e-4
end

@test_throws ArgumentError RD.jacobian!(RD.InPlace(), RD.ForwardAD(), dmodel, J, y, z1)
@test_throws ArgumentError RD.jacobian!(RD.StaticReturn(), RD.ForwardAD(), dmodel, J, y, z1)
@test_throws ArgumentError RD.jacobian!(RD.StaticReturn(), RD.UserDefined(), dmodel, J, y, z1)
@test_throws ArgumentError RD.jacobian!(RD.InPlace(), RD.FiniteDifference(), dmodel, J, y, z1)

ENV["JULIA_DEBUG"] = "RobotDynamics"
z1.z .+= 1
@test_logs (:debug, r"Solving for next") RD.jacobian!(RD.InPlace(), diff, dmodel, J, y, z1)
@test_logs (:debug, r"Using cached") RD.jacobian!(RD.InPlace(), diff, dmodel, J, y, z1)
RD.setdata!(z1s, z1.z .+ 1)
@test_logs (:debug, r"Solving for next") RD.jacobian!(RD.StaticReturn(), diff, dmodel, J, y, z1s)
@test_logs (:debug, r"Using cached") RD.jacobian!(RD.StaticReturn(), diff, dmodel, J, y, z1s)

ENV["JULIA_DEBUG"] = ""
function test_implicit_allocs(dmodel, z::RD.AbstractKnotPoint{Nx,Nu}) where {Nx,Nu}
    zs = KnotPoint{Nx,Nu}(SVector{Nx+Nu}(RD.getdata(z)), z.t, z.dt)
    x2 = zeros(RD.state_dim(dmodel))
    J = zeros(Nx, Nx + Nu)
    y = zeros(Nx)
    diffmethod = RD.default_diffmethod(dmodel)
    allocs = @allocated RD.discrete_dynamics!(dmodel, x2, z)
    allocs += @allocated RD.jacobian!(RD.InPlace(), diffmethod, dmodel, J, y, z)

    allocs += @allocated RD.discrete_dynamics(dmodel, zs)
    allocs += @allocated RD.jacobian!(RD.StaticReturn(), diffmethod, dmodel, J, y, zs)

    z[1] += 1
    allocs += @allocated RD.jacobian!(RD.InPlace(), diffmethod, dmodel, J, y, z)

    zs.z = zs.z .+ 0.2
    allocs += @allocated RD.jacobian!(RD.StaticReturn(), diffmethod, dmodel, J, y, zs)
    return allocs
end
test_implicit_allocs(dmodel, z1)  # run once to compile non-logging versions
run_alloc_tests && @test test_implicit_allocs(dmodel, z1) == 0
end


text:
#=
  Minesweeper solver in Julia + ConstraintSolver.jl

  From gecode/examples/minesweeper.cc:
  """
  A specification is a square matrix of characters. Alphanumeric
  characters represent the number of mines adjacent to that field.
  Dots represent fields with an unknown number of mines adjacent to
  it (or an actual mine).
  """

  E.g.
       "..2.3."
       "2....."
       "..24.3"
       "1.34.."
       ".....3"
       ".3.3.."
  """

  Also see:
  * http://www.janko.at/Raetsel/Minesweeper/index.htm

  * http://en.wikipedia.org/wiki/Minesweeper_(computer_game)

  * Ian Stewart on Minesweeper:
    http://www.claymath.org/Popular_Lectures/Minesweeper/

  * Richard Kaye's Minesweeper Pages
    http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.htm

  * Some Minesweeper Configurations
    http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.pdf

  Model created by Hakan Kjellerstrand, hakank@gmail.com
  See also my Julia page: http://www.hakank.org/julia/
=#

using ConstraintSolver, JuMP
const CS = ConstraintSolver
include("constraints_utils.jl")
include("minesweeper_problems.jl")

function minesweeper(problem,all_solutions=true,print_solutions=true, timeout=Inf)

    m = Model(optimizer_with_attributes(CS.Optimizer, "all_solutions"=>all_solutions,
                                                      "logging"=>[],
                                                      "traverse_strategy"=>:BFS,
                                                      # "traverse_strategy"=>:DFS,
                                                      # "traverse_strategy"=>:DBFS,

                                                      "branch_split"=>:Smallest,
                                                      # "branch_split"=>:Biggest,
                                                      # "branch_split"=>:InHalf,

                                                      # "simplify"=>false,
                                                      "simplify"=>true,
                                                      "time_limit"=>timeout
                                        ))

    problem = resize_matrix(problem) # Resize to "proper" matrix form
    # display(@show problem)
    # print_grid(problem)

    rows,cols = size(problem)
    println("rows:$rows cols:$cols")

    # @variable(m, 0 <= x[1:rows,1:cols] <= 1, Int)
    @variable(m, x[1:rows,1:cols], Bin)

    # Missing/unknown value
    M = -1
    for i in 1:rows, j in 1:cols
      # Check all that has a hint (i.e. number of surrounding mines)
      if problem[i,j] != M
           # 1) This cannot be a mine
          @constraint(m, x[i,j] == 0)

          # 2) problem[i,j] = the number of bombs in the neighborhood.
          s = sum([x[i+a,j+b] for a in -1:1 for b in -1:1
                            # if i+a > 0 && j+b > 0 && i+a <= rows && j+b <= cols
                            # if 1 <= i+a <= rows && 1 <= j+b <= cols
                            if i+a in 1:rows && j+b in 1:cols
                  ])
          @constraint(m, problem[i,j] == s)
      end
    end

    optimize!(m)
    status = JuMP.termination_status(m)
    println("status:$status")
    if status == MOI.OPTIMAL
        num_sols = MOI.get(m, MOI.ResultCount())
        println("num_sols:$num_sols")
        for sol in 1:num_sols
            s = convert.(Integer,JuMP.value.(x,result=sol))
            print_grid(s)
            # join(replace(replace(s,0=>"X"),1=>"_"),"")|>println
        end
        println("num_sols:$num_sols")
        println("\n")

    end

end

all_problems = all_minesweeper_problems

all_solutions = true
print_solutions = true
timeout = 3
println("all_solutions:$all_solutions print_solutions:$print_solutions timeout:$timeout")

# for p in 12
for p in sort(collect(keys(all_problems)))
    println("\n\nproblem $p")
    @time minesweeper(all_problems[p],all_solutions,print_solutions,timeout)

end

max_stars_repo_name: Wikunia/hakank

max_issues_repo_name: Wikunia/hakank

max_forks_repo_name: Wikunia/hakank


text:
module ExperimentalSetup

using DataStructures, JSON

export  Factor, Rep,
        delete, replace!,
        saveJSON, loadJSON

import Base: ∈, push!, delete!, empty!

struct Factor
    name::String
    levels::Vector{String}
end

struct Setup
    levels::Vector{Int}
    n::Base.RefValue{Int}
end

struct Metadata
    factors::Vector{Factor}
    setups::Vector{Setup}
    nfactors::Int

    Metadata(factors::Vector{Factor}, setups::Vector{Setup}) = new(factors, setups, length(factors))
end

Metadata(factors::Vector{Factor}) = Metadata(factors, Setup[])

Metadata() = Metadata(Factor[])

mutable struct Rep
    setup::Int
    replicate::Int
    comment::String
end

mutable struct Log
    md::Metadata
    last::UInt64

    reps::SortedDict{UInt64, Rep}
end

Log(md::Metadata) = Log(md, 0, SortedDict{UInt64, Rep}())

Log() = Log(Metadata())

# findfirst

# findfirst(a::Log, x::Rep) = findfirst(a.reps, x)

# in

# repID(md::Metadata, x::Rep) = hash((md.setups[x.setup],x.replicate, x.comment))

∈(k::UInt64, a::Log) = haskey(a.reps, k)
∈(k::T, a::Log) where T <: Integer = UInt64(k) ∈ a

# pushes

# Push a set of levels
function push!(md::Metadata, str_levels::Vector{String})
    @assert length(str_levels) == md.nfactors "Number of levels doesn't match metadata"
    levels = Int[]

    # Ensure that each of the levels supplied exist in the Metatdata
    for (factor, l) in zip(md.factors, str_levels)
        i = findfirst(factor.levels, l)
        @assert i ≠ 0 "Factor $(factor.name) does not have level $l"
        push!(levels, i)
    end

    # ??? Try to find an existing setup with the same levels perhaps?
    i = findfirst(x -> x.levels == levels, md.setups)
    if i == 0
        push!(md.setups, Setup(levels, Ref(0)))
        i = length(md.setups)
    end

    # Increment the level count
    md.setups[i].n.x += 1

    # Return the index and the count
    return (i, md.setups[i].n.x)
end

function push!(a::Log, str_levels::Vector{String}, comment::String)
    i, replicate = push!(a.md, str_levels)
    a.last += 1
    a.reps[a.last] = Rep(i, replicate, comment)
    return a
end
# empty

function empty!(a::Log)
    empty!(a.reps)
    empty!(a.md.setups)
    return a
end

# pop

pop(md::Metadata, x::Rep) = ([f.levels[i] for (i, f) in zip(md.setups[x.setup].levels, md.factors)], x.comment)

pop(a::Log, k::UInt64) = pop(a.md, a.reps[k])

pop(a::Log, k::T) where T <: Integer = pop(a, UInt64(k))

# deletes

function _delete!(md::Metadata, setup::Int)
    step_setup = md.setups[setup].n.x == 1
    if step_setup
        deleteat!(md.setups, setup)
    else
        md.setups[setup].n.x -= 1
    end
    return step_setup
end

function delete!(a::Log, k::UInt64)
    x = a.reps[k]
    step_setup = _delete!(a.md, x.setup)
    for (i,r) in a.reps
        if r.setup == x.setup
            if r.replicate > x.replicate
                r.replicate -= 1
            end
        elseif step_setup && r.setup > x.setup
            r.setup -= 1
        end
    end
    delete!(a.reps, k)
    return a
end

delete!(a::Log, k::T) where T <: Integer = delete!(a, UInt64(k))

function delete(a::Log, x::Factor)
    i = findfirst(y -> y == x, a.md.factors)
    @assert i ≠ 0 "factor not found in log"
    factors = deepcopy(a.md.factors)
    deleteat!(factors, i)
    b = Log(Metadata(factors))
    for r in values(a.reps)
        levels, comment = pop(a.md, r)
        deleteat!(levels, i)
        push!(b, levels, comment)
    end
    return b
end

function delete(a::Log, x::Factor, level::String)
    i = findfirst(y -> y == x, a.md.factors)
    @assert i ≠ 0 "factor not found in log"
    j = findfirst(a.md.factors[i].levels, level)
    @assert j ≠ 0 "level not found in log"
    nlevels = length(a.md.factors[i].levels)
    if nlevels == 1
        return delete(a, x)
    end
    factors = deepcopy(a.md.factors)
    deleteat!(factors[i].levels, j)
    b = Log(Metadata(factors))
    # if the level to be removed is first in the list of levels, then replace it with the second one. If it's the last, replace it with the next to last one. If it's neither, replace it with one level bellow.
    alternative = j == 1 ? a.md.factors[i].levels[2] : j == nlevels ? a.md.factors[i].levels[nlevels - 1] : a.md.factors[i].levels[j - 1]
    for r in values(a.reps)
        levels, comment = pop(a.md, r)
        if levels[i] == level
            levels[i] = alternative
        end
        push!(b, levels, comment)
    end
    return b
end

#=function delete!(a::Log, x::Rep)
    x ∉ a && return a
    step_setup = false
    if a.md.setups[x.setup].n.x == 1
        deleteat!(a.md.setups, x.setup)
        step_setup = true
    else
        a.md.setups[x.setup].n.x -= 1
    end
    ind = 0
    for (i,r) in enumerate(a.reps)
        if r.setup == x.setup
            if r == x
                ind = copy(i)
            else
                if r.replicate > x.replicate
                    r.replicate -= 1
                end
            end
        elseif r.setup > x.setup
            if step_setup
                r.setup -= 1
            end
        end
    end
    deleteat!(a.reps, ind)
    filter!(x -> x.rep ≠ ind, a.asss)
    for s in a.asss
        if s.rep > ind
            s.rep -= 1
        end
    end
    return a
end=#

# replace

function replace!(a::Log, o::UInt64, str_levels::Vector{String}, comment::String)
    @assert haskey(a.reps, o) ≠ 0 "old run not found"
    delete!(a, o)
    push!(a, str_levels, comment)
    a.reps[o] = pop!(a.reps, a.last)
    return a
end

replace!(a::Log, o::T, str_levels::Vector{String}, comment::String) where T <: Integer = replace!(a, UInt64(o), str_levels, comment)

# combine

function combine_factors(logs::Log...)
    factors = Factor[]
    for log in logs, new_factor in log.md.factors
        for old_factor in factors
            if new_factor.name == old_factor.name
                for new_level in new_factor.levels
                    new_level ∉ old_factor.levels && push!(old_factor.levels, new_level)
                end
                break
            end
        end
        push!(factors, new_factor)
    end
    return Metadata(factors)
end

function combine(logs::Log...)
    md = combine_factors(logs...)
    a = Log(md)
    x = OrderedDict{String, String}(f.name => f.levels[1] for f in md.factors)
    for log in logs, r in values(log.reps)
        for f in md.factors
            x[f.name] = f.levels[1] # non existant factors recieve a default first level
        end
        for (i, f) in zip(log.md.setups[r.setup].levels, log.md.factors)
            x[f.name] = f.levels[i]
        end
        push!(a, collect(values(x)), r.comment)
    end
    return a
end

# Converts a JSON representation of a Log to a Log object
function parseJSON(jsonString::AbstractString)
    # create the log objects
    json = JSON.parse(jsonString)
    log = Log(
        Metadata(
            map(x -> Factor(x["name"], x["levels"]), json["md"]["factors"]),
            map(y -> Setup(y["levels"], Base.RefValue{Int64}(y["n"]["x"])), json["md"]["setups"])
        ),
        json["last"],
        SortedDict{UInt64, Rep}()
    )

    # add the reps
    for (k,v) in json["reps"]
       log.reps[parse(UInt64, k)] = Rep(v["setup"], v["replicate"], v["comment"])
    end

    return log
end

# Reads the supplied file and tries to convert the containing JSON to a Log object
function loadJSON(filename::String)
    @assert isfile(filename) "File does not exist."

    json = ""
    # read and parse
    open(filename, "r") do f
        json = readstring(f)
    end

    @assert length(json) > 0 "Unable to read from file, or file is empty."
    return parseJSON(json)
end

# Converts the suppplied Log object to a JSON string and saves it to a file
function saveJSON(filename::String, log::Log, overwrite::Bool)
    @assert overwrite || !isfile(filename) "File alread exists."

    # write the Log out as a JSON string
    open(filename, "w") do f
        write(f, JSON.json(log))
    end
end

# Overloaded saveJSON method--this instance automatically overwrites the file
saveJSON(filename::String, log::Log) = saveJSON(filename, log, true)

end # module


text:
@testset "Interpolation" begin
using DiskArrayTools: InterpolatedDiskArray
using DiskArrayTools.Interpolations
using DiskArrays: GridChunks

a = reshape(1.0:40,8,5)
ad = _DiskArray(a, chunksize=(4,3))
adi = InterpolatedDiskArray(ad,GridChunks((17,5),(8,3)),0.5:0.5:8.5,nothing)
@test size(adi) == (17,5)
@test adi[15:17,4:5] == [31.5 39.5; 32.0 40.0; 32.0 40.0]
@test adi[1,:] == a[1,:]
@test adi[1:3,1] == [1.0,1.0,1.5]
@test getindex_count(ad) == 3
adi2 = InterpolatedDiskArray(ad,GridChunks((8,2),(4,4)),[1.1,1.2,1.3,1.4,1.6,1.7,1.8,1.9],[1.25,1.75], order = (Constant(),Linear()))
@test adi2[:,:] == [fill(3.0,4) fill(7.0,4); fill(4.0,4) fill(8.0,4)]
ad = _DiskArray(a, chunksize=(4,3))
adi3 = InterpolatedDiskArray(ad,GridChunks((3,3),(2,2)),[1.5,2.5,3.5],[1.25,1.5,1.75], order = (Constant(),Constant()))
@test all(adi3[1:2,1:2] .== a[2,1])
end


text:
mvn = is_windows() ? "mvn.cmd" : "mvn"
which = is_windows() ? "where" : "which"

try
    run(`$which $mvn`)
catch
    error("Cannot find maven. Is it installed?")
end

cd("../jvm/corenlp-wrapper")
run(`$mvn clean package`)


text:
using Clang.Generators
using HDF5_jll

include_dir = normpath(HDF5_jll.artifact_dir, "include")
#clang_dir = joinpath(include_dir, "clang-c")

# wrapper generator options
options = load_options(joinpath(@__DIR__, "generator.toml"))

# add compiler flags, e.g. "-DXXXXXXXXX"
args = get_default_args()
push!(args, "-I$include_dir -DH5_BUILT_AS_DYNAMIC_LIB")

headers = [normpath(include_dir, "hdf5.h")]
#headers = [joinpath(clang_dir, header) for header in readdir(clang_dir) if endswith(header, ".h")]
# there is also an experimental `detect_headers` function for auto-detecting top-level headers in the directory
# headers = detect_headers(clang_dir, args)

# create context
ctx = create_context(headers, args, options)

# run generator
build!(ctx)