# file for functions related to interpolating from solution points to
# faces

"""
  Holds all the temporary arrays needed to interpolate the metrics
"""
mutable struct Interpolation{Tmsh, Tdim}
  dxdxi_el::Array{Tmsh, 3}
  dxdxi_el_bar::Array{Tmsh, 3}

  dxdxi_elface::Array{Tmsh, 3}
  dxdxi_elface_bar::Array{Tmsh, 3}

  dxidx_node::Array{Tmsh, 2}
  dxidx_node_bar::Array{Tmsh, 2}

  dxdxi_node::Array{Tmsh, 2}
  dxdxi_node_bar::Array{Tmsh, 2}

  bndry_arr::Array{Boundary, 1}
end

function Interpolation(mesh::PumiMesh2D{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 4, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 4, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 2, 2)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 2, 2)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 2}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

function Interpolation(mesh::PumiMesh2D, ::Type{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 4, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 4, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 2, 2)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 2, 2)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 2}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

function Interpolation(mesh::PumiMesh3D{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 9, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 9, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 3, 3)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 3, 3)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 3}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

function Interpolation(mesh::PumiMesh3D, ::Type{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 9, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 9, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 3, 3)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 3, 3)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 3}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

"""
  Interpolates dxidx and jac to the face nodes, faces shared in parallel and
  boundary faces, as well as gets the coordinates of the nodes for
  the shared faces and boundaryfaces.  All the functions use smart allocators,
  so the fields of the mesh object will be allocated if needed, but not if
  they were previously allocated.

  This is the main entry point for everything in this file.
"""
function interpolateCoordinatesAndMetrics(mesh::PumiMeshDG)

  # interpolate metric terms
  interpolateMapping(mesh)

  #  allocate coordinate arrays if needed
  allocateFaceCoordinates(mesh)

  # get the coordinates
  getBndryCoordinates(mesh, mesh.bndryfaces, mesh.coords_bndry)
  getInterfaceCoordinates(mesh, mesh.interfaces, mesh.coords_interface)
  for i=1:mesh.npeers
    getBndryCoordinates(mesh, mesh.bndries_local[i],
                        mesh.coords_sharedface[i])
  end

  return nothing
end

"""
  Allocates the dxidx and jac arrays for the face nodes/ shared face
  data.  Non-curvilinear meshes only.  Specifically:
    mesh.dxidx_face, mesh.jac_face
    mesh.dxidx_bndry, mesh.jac_bndry
    mesh.dxidx_sharedface, mesh.jac_shareface (and their internal arrays)

    Also the corresponding bar variables
      mesh.dxidx_face_bar, mesh.jac_face_bar
      mesh.dxidx_bndry_bar, mesh.jac_bndry_bar
      mesh.dxidx_sharedface_bar, mesh.jac_sharedface_bar
"""
function allocateInterpolatedMetrics(mesh::PumiMeshDG{Tmsh}) where Tmsh

  dim = mesh.dim
  sbpface = mesh.sbpface
   if !isFieldDefined(mesh, :dxidx_face, :jac_face, :dxidx_sharedface,
                           :jac_sharedface, :dxidx_bndry, :jac_bndry)
    # interior faces
    mesh.dxidx_face = zeros(Tmsh, dim, dim, sbpface.numnodes, mesh.numInterfaces)
    mesh.jac_face = zeros(Tmsh, sbpface.numnodes, mesh.numInterfaces)

    mesh.dxidx_face_bar = zeros(mesh.dxidx_face)
    mesh.jac_face_bar = zeros(mesh.jac_face)

    # boundary faces
    mesh.dxidx_bndry = zeros(Tmsh, dim, dim, sbpface.numnodes,
                                   mesh.numBoundaryFaces)
    mesh.jac_bndry = zeros(Tmsh, sbpface.numnodes, mesh.numBoundaryFaces)
    mesh.dxidx_bndry_bar = zeros(mesh.dxidx_bndry)
    mesh.jac_bndry_bar = zeros(mesh.jac_bndry)

    # parallel shared faces
    mesh.dxidx_sharedface = Array{Array{Tmsh, 4}}(mesh.npeers)
    mesh.jac_sharedface = Array{Array{Tmsh, 2}}(mesh.npeers)
    mesh.dxidx_sharedface_bar = Array{Array{Tmsh, 4}}(mesh.npeers)
    mesh.jac_sharedface_bar = Array{Array{Tmsh, 2}}(mesh.npeers)
    for i=1:mesh.npeers
      mesh.dxidx_sharedface[i] = zeros(Tmsh, dim, dim, sbpface.numnodes,
                                       mesh.peer_face_counts[i])

      mesh.jac_sharedface[i] = Array{Tmsh}(sbpface.numnodes, mesh.peer_face_counts[i])
      mesh.dxidx_sharedface_bar[i] = zeros(mesh.dxidx_sharedface[i])
      mesh.jac_sharedface_bar[i] = zeros(mesh.jac_sharedface[i])

    end
  else
    # zero out metrics?
    fill!(mesh.dxidx_face, 0.0)
    fill!(mesh.jac_face, 0.0)
    fill!(mesh.dxidx_bndry, 0.0)
    fill!(mesh.jac_bndry, 0.0)
    for i=1:mesh.npeers
      fill!(mesh.dxidx_sharedface[i], 0.0)
      fill!(mesh.jac_sharedface[i], 0.0)
    end
  end

  return nothing
end

"""
  Allocates coordinate arays for the boundary and shared face nodes.
  Specifically:
    mesh.coords_bndry
    mesh.coords_sharedface (and its internal arrays)
"""
function allocateFaceCoordinates(mesh::PumiMeshDG{Tmsh}) where Tmsh

  sbpface = mesh.sbpface

  if !isFieldDefined(mesh, :coords_bndry, :coords_sharedface)
    mesh.coords_bndry = zeros(Tmsh, mesh.dim, sbpface.numnodes,
                                  mesh.numBoundaryFaces)
    mesh.coords_bndry_bar = zeros(Tmsh, mesh.dim, sbpface.numnodes,
                                  mesh.numBoundaryFaces)
    mesh.coords_interface = zeros(Tmsh, mesh.dim, sbpface.numnodes, mesh.numInterfaces)
    mesh.coords_sharedface = Array{Array{Tmsh, 3}}(mesh.npeers)
    for i=1:mesh.npeers
      mesh.coords_sharedface[i] = zeros(Tmsh, mesh.dim, sbpface.numnodes,
                                             mesh.peer_face_counts[i])
    end
  else
    fill!(mesh.coords_bndry, 0.0)
    fill!(mesh.coords_interface, 0.0)
    for i=1:mesh.npeers
      fill!(mesh.coords_sharedface[i], 0.0)
    end

  end

  return nothing
end

"""
  Interpolates dxidx, jac to the face nodes of local interfaces, shared
  faces, and boundary faces.  This function uses a smart allocator to
  allocate the relevent fields of the mesh object if they have been been
  allocated previously and to not allocate them if they have been previously.

  By convention, the metrics of elementL of an interface are interpolated
  (recall elementL is always the local elements for shared faces).

  See allocateInterpolatedMetrics for the fields that are populated by this
  function.
"""
function interpolateMapping(mesh::PumiMeshDG{Tmsh}) where Tmsh

  allocateInterpolatedMetrics(mesh)

  sbpface = mesh.sbpface
  dim = mesh.dim

  # pull out the needed arrays
  dxidx_face = mesh.dxidx_face
  dxidx_sharedface = mesh.dxidx_sharedface
  jac_face = mesh.jac_face

  dxidx_bndry = mesh.dxidx_bndry
  jac_bndry = mesh.jac_bndry
  jac_sharedface = mesh.jac_sharedface

  # temporary storage
  interp_data = Interpolation(mesh)

  for i=1:mesh.numInterfaces
    dxidx_i = sview(dxidx_face, :, :, :, i)
    jac_i = sview(jac_face, :, i)

    interface_i = mesh.interfaces[i]
    el = interface_i.elementL
    face = interface_i.faceL
    interp_data.bndry_arr[1] =  Boundary(1, face)

    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)

    interpolateFace(interp_data, mesh.sbpface, dxidx_in, jac_in, dxidx_i, jac_i)

  end
  # now do shared edges
  for peer = 1:mesh.npeers
    dxidx_p = dxidx_sharedface[peer]
    jac_p = jac_sharedface[peer]
    interfaces_p = mesh.shared_interfaces[peer]
    for i=1:mesh.peer_face_counts[peer]
      dxidx_i = sview(dxidx_p, :, :, :, i)
      jac_i = sview(jac_p, :, i)

      interface_i = interfaces_p[i]
      el = interface_i.elementL
      face = interface_i.faceL

      interp_data.bndry_arr[1] = Boundary(1, face)

      dxidx_in = sview(mesh.dxidx, :, :, :, el)
      jac_in = sview(mesh.jac, :, el)

      interpolateFace(interp_data, mesh.sbpface, dxidx_in, jac_in, dxidx_i, jac_i)
    end
  end

  # now do boundary
  for i=1:mesh.numBoundaryFaces
    bndry = mesh.bndryfaces[i]

    dxidx_i = sview(dxidx_bndry, :, :, :, i)
    jac_i = sview(jac_bndry, :, i)

    el = bndry.element
    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)
    interp_data.bndry_arr[1] = bndry
    interpolateFace(interp_data, mesh.sbpface, dxidx_in, jac_in, dxidx_i, jac_i)
  end

  return nothing

end  # end function

"""
  Reverse mode of interpolateMapping.  This function propigates the adjoint
  parts of the metrics at the interfaces, boundary faces, and shared faces
  back to the volume nodes.

  This function back propigates both dxidx and jac, although for 2D it turns
  out jac has no effect on the computation.

"""
function interpolateMapping_rev(mesh::PumiMeshDG{Tmsh}) where Tmsh

  sbpface = mesh.sbpface
  dim = mesh.dim

  # pull out the needed arrays
  dxidx_face = mesh.dxidx_face
  dxidx_face_bar = mesh.dxidx_face_bar

  dxidx_sharedface = mesh.dxidx_sharedface
  dxidx_sharedface_bar = mesh.dxidx_sharedface_bar

  dxidx_bndry = mesh.dxidx_bndry
  dxidx_bndry_bar = mesh.dxidx_bndry_bar

  jac_face = mesh.jac_face
  jac_face_bar = mesh.jac_face_bar

  jac_bndry = mesh.jac_bndry
  jac_bndry_bar = mesh.jac_bndry_bar

  jac_sharedface = mesh.jac_sharedface
  jac_sharedface_bar = mesh.jac_sharedface_bar

  # temporary storage
  interp_data = Interpolation(mesh)

  for i=1:mesh.numInterfaces
    dxidx_i = sview(dxidx_face, :, :, :, i)
    dxidx_bar_i = sview(dxidx_face_bar, :, :, :, i)
    jac_i = sview(jac_face, :, i)
    jac_bar_i = sview(jac_face_bar, :, i)

    interface_i = mesh.interfaces[i]
    el = interface_i.elementL
    face = interface_i.faceL
    interp_data.bndry_arr[1] =  Boundary(1, face)

    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    dxidx_bar_in = sview(mesh.dxidx_bar, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)
    jac_bar_in = sview(mesh.jac_bar, :, el)

    interpolateFace_rev(interp_data, mesh.sbpface,
                         dxidx_in, dxidx_bar_in, jac_in, jac_bar_in,
                         dxidx_i, dxidx_bar_i, jac_i, jac_bar_i)

  end
  # now do shared edges
  for peer = 1:mesh.npeers
    dxidx_p = dxidx_sharedface[peer]
    dxidx_bar_p = dxidx_sharedface_bar[peer]
    jac_p = jac_sharedface[peer]
    jac_bar_p = jac_sharedface_bar[perr]

    interfaces_p = mesh.shared_interfaces[peer]
    for i=1:mesh.peer_face_counts[peer]
      dxidx_i = sview(dxidx_p, :, :, :, i)
      dxidx_bar_i = sview(dxidx_bar_p, :, :, :, i)
      jac_i = sview(jac_p, :, i)
      jac_bar_i = sview(jac_bar_p, :, i)

      interface_i = interfaces_p[i]
      el = interface_i.elementL
      face = interface_i.faceL

      interp_data.bndry_arr[1] = Boundary(1, face)

      dxidx_in = sview(mesh.dxidx, :, :, :, el)
      dxidx_bar_in = sview(mesh.dxidx_bar, :, :, :, el)
      jac_in = sview(mesh.jac, :, el)
      jac_bar_in = sview(mesh.jac_bar, :, el)

      interpolateFace_rev(interp_data, mesh.sbpface,
                      dxidx_in, dxidx_bar_in, jac_in, jac_bar_in,
                      dxidx_i, dxidx_bar_i, jac_i, jac_bar_i)
    end
  end

  # now do boundary
  for i=1:mesh.numBoundaryFaces
    bndry = mesh.bndryfaces[i]

    dxidx_i = sview(dxidx_bndry, :, :, :, i)
    dxidx_bar_i = sview(dxidx_bndry_bar, :, :, :, i)
    jac_i = sview(jac_bndry, :, i)
    jac_bar_i = sview(jac_bndry_bar, :, i)

    el = bndry.element

    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    dxidx_bar_in = sview(mesh.dxidx_bar, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)
    jac_bar_in = sview(mesh.jac_bar, :, el)

    interp_data.bndry_arr[1] = bndry
    interpolateFace_rev(interp_data, mesh.sbpface,
                    dxidx_in, dxidx_bar_in, jac_in, jac_bar_in,
                    dxidx_i, dxidx_bar_i, jac_i, jac_bar_i)
  end

  return nothing
end

# 2D version
function interpolateFace(interp_data::Interpolation{Tmsh, 2}, sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh}) where Tmsh

  # unpack argumetns
  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  dim = 2
  numNodesPerElement = size(dxidx_hat_in, 3)
  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    detJ = jac_in[j]

    adjugate2(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

  # now store dxidx, |J| at the boundary nodesa
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det2(dxdxi_node)
    adjugate2(dxdxi_node, dxidx_node)
    scale!(dxidx_node, 1./det_dxdxi)

    detJ = det2(dxidx_node)
    fac = 1./detJ
    for k=1:dim
      for p=1:dim
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
  end

  return nothing

end

"""
  Reverse mode of interpolateFace.

  Propigates the adjoint parts of the inputs to the adjoint parts of the
  outputs.  This function also verifies the primal parts have not been modified
  (ie. it computes values dxidx_hat_out and verifies they are the same as
  the ones already present in the array).

  Inputs (see interpolateFace):
    interp_data
    sbpface
    dxidx_hat_in
    jac_in
    dxidx_hat_out
    dxidx_hat_out_bar: the adjoint part of dxidx_hat_out
    jac_out
    jac_out_bar: the adjoint part of jac_out

  Outputs:
    dxidx_hat_in_bar: the adjoint part of dxidx_hat_in
    jac_in_bar: the adjoint part of jac_in

  Aliasing restrictions: please don't
"""
function interpolateFace_rev(interp_data::Interpolation{Tmsh, 2},
                         sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         dxidx_hat_in_bar::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         jac_in_bar::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         dxidx_hat_out_bar::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh},
                         jac_out_bar::AbstractVector{Tmsh}) where Tmsh

  #----------------------------------------------------------------------------
  # forward sweep
  # unpack arguments
  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  fill!(dxdxi_el, 0.0)
  fill!(dxdxi_elface, 0.0)

  dim = 2
  numNodesPerElement = size(dxidx_hat_in, 3)

  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
#    detJ = jac_in[j]

    adjugate2(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

# everything in here is local to the loop, no need to compute it here
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det2(dxdxi_node)
    adjugate2(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det2(dxidx_node)
    fac = 1./detJ

    for k=1:dim
      for p=1:dim
        # verify this forward sweep is consistent with the original function
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
#        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
  end

  #----------------------------------------------------------------------------
  # reverse sweep

  # unpack arguments
  dxdxi_el_bar = interp_data.dxdxi_el_bar
  dxdxi_elface_bar = interp_data.dxdxi_elface_bar
  dxdxi_node_bar = interp_data.dxdxi_node_bar
  dxidx_node_bar = interp_data.dxidx_node_bar

  # zero them out
  fill!(dxdxi_el_bar, 0.0)
  fill!(dxdxi_elface_bar, 0.0)

  # each interation of this loop is independent - combine forward and reverse
  for j=sbpface.numnodes:-1:1

    #--------------------------------------------------------------------------
    # forward sweep
    fill!(dxdxi_node_bar, 0.0)
    fill!(dxidx_node_bar, 0.0)

    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det2(dxdxi_node)
    adjugate2(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det2(dxidx_node)
    fac = 1./detJ
#=
    for k=1:dim
      for p=1:dim
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
=#
    #--------------------------------------------------------------------------
    # reverse sweep

    det_dxdxi_bar = zero(det_dxdxi)
    fac_bar = zero(eltype(dxidx_hat_out))

    detJ_bar = jac_out_bar[j]*1

    for k=dim:-1:1
      for p=dim:-1:1
        dxidx_node_bar[p, k] += dxidx_hat_out_bar[p, k, j]*fac

        fac_bar += dxidx_hat_out_bar[p, k, j]*dxidx_node[p, k]
      end
    end

    detJ_bar -= fac_bar*1/(detJ*detJ)
    det2_rev(dxidx_node, dxidx_node_bar, detJ_bar)

    # uncomment this to introdue the bug
    for i=1:length(dxidx_node)
       dxidx_node[i] *= det_dxdxi  # need to reverse the primal variables too
       det_dxdxi_bar += dxidx_node_bar[i]*dxidx_node[i]*(-1/(det_dxdxi*det_dxdxi))
       dxidx_node_bar[i] = dxidx_node_bar[i]/det_dxdxi
    end

    adjugate2_rev(dxdxi_node, dxdxi_node_bar, dxidx_node, dxidx_node_bar)
    det2_rev(dxdxi_node, dxdxi_node_bar, det_dxdxi_bar)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxdxi_elface_bar[pos, j, 1] += dxdxi_node_bar[k, p]
      end
    end

  end  # end loop j

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate_rev!(sbpface, interp_data.bndry_arr, dxdxi_el_bar, dxdxi_elface_bar)

  # propagate back to the inputs
  for j=numNodesPerElement:-1:1
    fill!(dxidx_node_bar, 0.0)
    fill!(dxdxi_node_bar, 0.0)

    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    dxidx_hat_bar = sview(dxidx_hat_in_bar, :, :, j)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxidx_node_bar[k, p] += dxdxi_el_bar[pos, j, 1]
      end
    end

    adjugate2_rev(dxidx_hat, dxidx_hat_bar, dxidx_node, dxidx_node_bar)

    # it turns out jac_in isn't used for anything
#    jac_in_bar[j] = detJ_bar
  end  # end loop j

  return nothing

end

# 3d version
"""
  Interpolates the metric terms from the volume nodes of an element to
  the nodes of a particular face.

  Inputs:
    interp_data:  Interpolation object containing the temporary arrays.
                  Which face is interpolated to depends
                  interp_data.bndry_arr.face.  The element field does
                  not matter.  This array is overwritten.

    sbpface: an SBPFace
    dxidx_hat_in: 3D array holding dxidx/|J| at the volume nodes,
                   2 x 2 x numNodesPerElement in 2D or
                   3 x 3 x numNodesPerElemente in 3D.
    jac_hat_in: determanent of the dxidx at the volume nodes, vector of
                length numNodesPerElement

  Outputs:
    dxidx_i: array to be populated with dxidx/|J| at the face nodes,
             2 x 2 x numNodesPerFace in 2 or
             3 x 3 x numNodesPerFace in 3D

    jac_i: array to be populated with |J|, vector of length numNodesPerFAce

  Aliasing restrictions: do not alias
"""
function interpolateFace(interp_data::Interpolation{Tmsh, 3}, sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh}) where Tmsh
# herein, dxidx_hat is dxidx/|J|, and dxidx is the true dxidx

  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  dim = 3
  numNodesPerElement = size(dxidx_hat_in, 3)
  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    detJ = jac_in[j]

    adjugate3(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        dxidx_node[k, p] *= detJ
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
#  face = bndry.face
#  bndry_arr = [Boundary(1, face)]
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)

  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

  # now store dxidx, |J| at the boundary nodesa
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det3(dxdxi_node)
    adjugate3(dxdxi_node, dxidx_node)
    scale!(dxidx_node, 1./det_dxdxi)

    detJ = det3(dxidx_node)
    fac = 1./detJ
    for k=1:dim
      for p=1:dim
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
  end

  return nothing

end

function interpolateFace_rev(interp_data::Interpolation{Tmsh, 3},
                         sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         dxidx_hat_in_bar::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         jac_in_bar::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         dxidx_hat_out_bar::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh},
                         jac_out_bar::AbstractVector{Tmsh}) where Tmsh

  #----------------------------------------------------------------------------
  # forward sweep
  # unpack arguments
  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  fill!(dxdxi_el, 0.0)
  fill!(dxdxi_elface, 0.0)

  dim = 3
  numNodesPerElement = size(dxidx_hat_in, 3)

  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    detJ = jac_in[j]

    adjugate3(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        dxidx_node[k, p] *= detJ
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

  # everything in here is local to the loop, no need to compute it here
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det3(dxdxi_node)
    adjugate3(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det3(dxidx_node)
    fac = 1./detJ

    for k=1:dim
      for p=1:dim
        # verify this forward sweep is consistent with the original function
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
#        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
#    jac_out[j] = detJ
  end

  #----------------------------------------------------------------------------
  # reverse sweep

  # unpack arguments
  dxdxi_el_bar = interp_data.dxdxi_el_bar
  dxdxi_elface_bar = interp_data.dxdxi_elface_bar
  dxdxi_node_bar = interp_data.dxdxi_node_bar
  dxidx_node_bar = interp_data.dxidx_node_bar

  # zero them out
  fill!(dxdxi_el_bar, 0.0)
  fill!(dxdxi_elface_bar, 0.0)

  # each interation of this loop is independent - combine forward and reverse
  for j=sbpface.numnodes:-1:1

    #--------------------------------------------------------------------------
    # forward sweep
    fill!(dxdxi_node_bar, 0.0)
    fill!(dxidx_node_bar, 0.0)

    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det3(dxdxi_node)
    adjugate3(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det3(dxidx_node)
    fac = 1./detJ
#=
    for k=1:dim
      for p=1:dim
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
=#
    #--------------------------------------------------------------------------
    # reverse sweep
    det_dxdxi_bar = zero(det_dxdxi)
    fac_bar = zero(eltype(dxidx_hat_out))

    detJ_bar = jac_out_bar[j]*1

    for k=dim:-1:1
      for p=dim:-1:1
        dxidx_node_bar[p, k] += dxidx_hat_out_bar[p, k, j]*fac

        fac_bar += dxidx_hat_out_bar[p, k, j]*dxidx_node[p, k]
      end
    end

    detJ_bar -= fac_bar*1/(detJ*detJ)
    det3_rev(dxidx_node, dxidx_node_bar, detJ_bar)

    # uncomment this to introdue the bug
    for i=1:length(dxidx_node)
       dxidx_node[i] *= det_dxdxi  # need to reverse the primal variables too
       det_dxdxi_bar += dxidx_node_bar[i]*dxidx_node[i]*(-1/(det_dxdxi*det_dxdxi))
       dxidx_node_bar[i] = dxidx_node_bar[i]/det_dxdxi
    end

    adjugate3_rev(dxdxi_node, dxdxi_node_bar, dxidx_node, dxidx_node_bar)
    det3_rev(dxdxi_node, dxdxi_node_bar, det_dxdxi_bar)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxdxi_elface_bar[pos, j, 1] += dxdxi_node_bar[k, p]
      end
    end

  end  # end loop j

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate_rev!(sbpface, interp_data.bndry_arr, dxdxi_el_bar, dxdxi_elface_bar)

  # propagate back to the inputs
  for j=numNodesPerElement:-1:1
    fill!(dxidx_node_bar, 0.0)
    fill!(dxdxi_node_bar, 0.0)
    detJ = jac_in[j]
    detJ_bar = zero(detJ)

    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    dxidx_hat_bar = sview(dxidx_hat_in_bar, :, :, j)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxidx_node_bar[k, p] += dxdxi_el_bar[pos, j, 1]

        dxidx_node[k, p] /= detJ
        detJ_bar += dxidx_node_bar[k, p]*dxidx_node[k, p]
        dxidx_node_bar[k, p] = dxidx_node_bar[k, p]*detJ
      end
    end

    adjugate3_rev(dxidx_hat, dxidx_hat_bar, dxidx_node, dxidx_node_bar)

    jac_in_bar[j] = detJ_bar
  end  # end loop j

  return nothing

end

#TODO: eorder these operations for column major arrays
function det2(A::AbstractMatrix)
# determinet of 2x2 matrix

  return A[1,1]*A[2,2] - A[1,2]*A[2,1]
end

"""
  Computes the reverse mode of det2, returning only the derivative and not
  the value of the determinant.

  Inputs:
    A: The matrix
    y_bar: the adjoint part of the determinant (the primal part is not required
           for the reverse calculation)

  Inputs/Outputs:
    A_bar: the adjoint part of A, which is incremented, not overwritten

"""
function det2_rev(A::AbstractMatrix, A_bar::AbstractMatrix, y_bar::Number)

  A_bar[1,1] += y_bar*A[2,2]
  A_bar[1,2] += -y_bar*A[2,1]
  A_bar[2,1] += -y_bar*A[1,2]
  A_bar[2,2] += y_bar*A[1,1]

  return nothing
end

function det3(A::AbstractMatrix)
# determinent of 3x3 matrix
# cofactor expansion about first column
  val1 = A[1,1]*(A[2,2]*A[3,3] - A[2,3]*A[3,2])
  val2 = -A[2,1]*(A[1,2]*A[3,3] - A[1,3]*A[3,2])
  val3 = A[3,1]*(A[1,2]*A[2,3] - A[1,3]*A[2,2])
  return val1 + val2 + val3
end

function det3_rev(A::AbstractMatrix, A_bar::AbstractMatrix, y_bar::Number)

  # initialize intermediate adjoints to zero
  val1_bar = y_bar
  val2_bar = y_bar
  val3_bar = y_bar

  # val3 expression
  # for true alias compatability, need to apply reverse mode to each line
  # right to left, rather than left to right
  A_bar[1,1] += val3_bar*(A[2,2]*A[3,3] - A[2,3]*A[3,2])
  A_bar[2,2] += val3_bar*(A[1,1]*A[3,3])
  A_bar[3,3] += val3_bar*(A[1,1]*A[2,2])

  A_bar[2,3] += -val3_bar*A[1,1]*A[3,2]
  A_bar[3,2] += -val3_bar*A[1,1]*A[2,3]

  # val2 expression
  A_bar[2,1] += -val2_bar*(A[1,2]*A[3,3] - A[1,3]*A[3,2])
  A_bar[1,2] += -val2_bar*A[2,1]*A[3,3]
  A_bar[3,3] += -val2_bar*A[2,1]*A[1,2]

  A_bar[1,3] += val2_bar*A[2,1]*A[3,2]
  A_bar[3,2] += val2_bar*A[2,1]*A[1,3]

  # val1 expression
  A_bar[3,1] += val1_bar*(A[1,2]*A[2,3] - A[1,3]*A[2,2])
  A_bar[1,2] += val1_bar*A[3,1]*A[2,3]
  A_bar[2,3] += val1_bar*A[3,1]*A[1,2]

  A_bar[1,3] += -val1_bar*A[3,1]*A[2,2]
  A_bar[2,2] += -val1_bar*A[3,1]*A[1,3]

  return nothing
end

function adjugate2(A::AbstractMatrix, B::AbstractMatrix)
  B[1,1] = A[2,2]
  B[1,2] = -A[1,2]
  B[2,1] = -A[2,1]
  B[2,2] = A[1,1]
end

"""
  Computes the reverse mode of adjugate2, without modifying A or B

  Inputs:
    A: The original matrix
    B: The adjuage matrix (presumably computed by adjugate2)
    B_bar: the adjoint part of B

  Inputs/Outputs:
    A_bar: The adjoint part of A, which is incremented, not overwritten

  Aliasing restrictions: I'm not sure what happens if things alias, so don't
                         do it.
"""
function adjugate2_rev(A::AbstractMatrix, A_bar::AbstractMatrix,
                       B::AbstractMatrix, B_bar::AbstractMatrix)

  A_bar[2,2] += B_bar[1,1]
  A_bar[1,2] += -B_bar[1,2]
  A_bar[2,1] += -B_bar[2,1]
  A_bar[1,1] += B_bar[2,2]

end

function adjugate3(A::AbstractMatrix, B::AbstractMatrix)

  B[1,1] = A[2,2]*A[3,3] - A[2,3]*A[3,2]
  B[1,2] = -(A[1,2]*A[3,3] - A[1,3]*A[3,2])
  B[1,3] = A[1,2]*A[2,3] - A[1,3]*A[2,2]

  B[2,1] = -(A[2,1]*A[3,3] - A[2,3]*A[3,1])
  B[2,2] = A[1,1]*A[3,3] - A[1,3]*A[3,1]
  B[2,3] = -(A[1,1]*A[2,3] - A[1,3]*A[2,1])

  B[3,1] = A[2,1]*A[3,2] - A[2,2]*A[3,1]
  B[3,2] = -(A[1,1]*A[3,2] - A[1,2]*A[3,1])
  B[3,3] = A[1,1]*A[2,2] - A[1,2]*A[2,1]
end

"""
  Computes the reverse mode of adjugate2, without modifying A or B

  Inputs:
    A: the original matrix
    B: the adjugate matrix (presumably computed by adjugate3)
    B_bar: the adjoint part of B

  Inputs/Outputs
    A_bar: the adjoint part of A, which is incremented, not overwritten

  Aliasing restrictions: Please don't
"""
function adjugate3_rev(A::AbstractMatrix, A_bar::AbstractMatrix,
                       B::AbstractMatrix, B_bar::AbstractMatrix)

  # B11
  B11_bar = B_bar[1,1]
  A_bar[2,2] += B11_bar*A[3,3]
  A_bar[3,3] += B11_bar*A[2,2]
  A_bar[2,3] += -B11_bar*A[3,2]
  A_bar[3,2] += -B11_bar*A[2,3]

  bar = B_bar[1,2]
  A_bar[1,2] += -bar*A[3,3]
  A_bar[3,3] += -bar*A[1,2]
  A_bar[1,3] += bar*A[3,2]
  A_bar[3,2] += bar*A[1,3]

  bar = B_bar[1,3]
  A_bar[1,2] += bar*A[2,3]
  A_bar[2,3] += bar*A[1,2]
  A_bar[1,3] += -bar*A[2,2]
  A_bar[2,2] += -bar*A[1,3]

  bar = B_bar[2,1]
  A_bar[2,1] += -bar*A[3,3]
  A_bar[3,3] += -bar*A[2,1]
  A_bar[2,3] += bar*A[3,1]
  A_bar[3,1] += bar*A[2,3]

  bar = B_bar[2,2]
  A_bar[1,1] += bar*A[3,3]
  A_bar[3,3] += bar*A[1,1]
  A_bar[1,3] += -bar*A[3,1]
  A_bar[3,1] += -bar*A[1,3]

  bar = B_bar[2,3]
  A_bar[1,1] += -bar*A[2,3]
  A_bar[2,3] += -bar*A[1,1]
  A_bar[1,3] += bar*A[2,1]
  A_bar[2,1] += bar*A[1,3]

  bar = B_bar[3,1]
  A_bar[2,1] += bar*A[3,2]
  A_bar[3,2] += bar*A[2,1]
  A_bar[2,2] += -bar*A[3,1]
  A_bar[3,1] += -bar*A[2,2]

  bar = B_bar[3,2]
  A_bar[1,1] += -bar*A[3,2]
  A_bar[3,2] += -bar*A[1,1]
  A_bar[1,2] += bar*A[3,1]
  A_bar[3,1] += bar*A[1,2]

  bar = B_bar[3,3]
  A_bar[1,1] += bar*A[2,2]
  A_bar[2,2] += bar*A[1,1]
  A_bar[1,2] += -bar*A[2,1]
  A_bar[2,1] += -bar*A[1,2]

  return nothing
end


module GateMap
# =========================================================================== #
#                           Imported modules
# =========================================================================== #

using Base
using LinearAlgebra
using StaticArrays
import QXZoo.GateOps
import QXZoo.DefaultGates

# =========================================================================== #
#                           Exported functions
# =========================================================================== #

export create_gate_1q, create_gate_2q

# =========================================================================== #
#                       Gate caching and retrieval
# =========================================================================== #

"""Module gate cache

Since many different circuits may use the same gates, keeping a module-global
cache makes sense to avoid recreating them. Each subcircuit can have a subset of
the global cache's gates."""
gates = Dict{Union{GateOps.GateSymbol, GateOps.GateSymbolP, Function}, Function}()

# TODO
# const gates = let .... end

"""
    init_cache()

Initialise the cache for default gates used in circuit generation. Modules generating
gates not stored here should cache them for use by other circuits.

"""
function init_cache() #gates::Dict{Union{<:GateOps.AGateSymbol, Function}, Function})
    push!(gates, DefaultGates.GateSymbols.p00 => p00)
    push!(gates, DefaultGates.GateSymbols.p10 => p10)
    push!(gates, DefaultGates.GateSymbols.p01 => p01)
    push!(gates, DefaultGates.GateSymbols.p11 => p11)
    push!(gates, DefaultGates.GateSymbols.I => I)
    push!(gates, DefaultGates.GateSymbols.h => h)
    push!(gates, DefaultGates.GateSymbols.x => x)
    push!(gates, DefaultGates.GateSymbols.y => y)
    push!(gates, DefaultGates.GateSymbols.z => z)

    push!(gates, DefaultGates.GateSymbols.c_x => c_x)
    push!(gates, DefaultGates.GateSymbols.c_y => c_y)
    push!(gates, DefaultGates.GateSymbols.c_z => c_z)
    push!(gates, DefaultGates.GateSymbols.r_x => r_x)
    push!(gates, DefaultGates.GateSymbols.r_y => r_y)
    push!(gates, DefaultGates.GateSymbols.r_z => r_z)
    push!(gates, DefaultGates.GateSymbols.r_phase => r_phase)

    push!(gates, DefaultGates.GateSymbols.c_r_x => c_r_x)
    push!(gates, DefaultGates.GateSymbols.c_r_y => c_r_y)
    push!(gates, DefaultGates.GateSymbols.c_r_z => c_r_z)
    push!(gates, DefaultGates.GateSymbols.c_r_phase => c_r_phase)

    push!(gates, DefaultGates.GateSymbols.s => s )
    push!(gates, DefaultGates.GateSymbols.t => t )
    return ;
end

# =========================================================================== #
#                       Custom gate storage: 1 qubit
# =========================================================================== #

"""
    create_gate_1q(gate_label::String, gen_func::Function)

Creates a new user-defined gate, caches the generating function, and returns the
GateOps.GateSymbol key for use in a circuit.

# Examples
```julia-repl

```
"""
function create_gate_1q(gate_label::String, gen_func::Function)
    mat2x2 = nothing
    if applicable(gen_func, pi/2)
        mat2x2 = gen_func(pi/2)
    else
        mat2x2 = gen_func()
    end
    if size(mat2x2) != (2,2)
        error("Please ensure generated matrix is 2x2")
    end
    gs = GateOps.GateSymbol(Symbol(gate_label))
    cache_gate!(gs, gen_func)
    return gs
end

"""
    create_gate_1q(gate_label::String, mat::SArray{Tuple{2,2},Complex{Float64},2,4})

Creates a new user-defined gate, caches the generating matrix, and returns the
GateOps.GateSymbol key for use in a circuit.

# Examples
```julia-repl

```
"""
function create_gate_1q(gate_label::String, mat::SArray{Tuple{2,2},Complex{Float64},2,4})
    return create_gate_1q(gate_label, ()->mat)
end

"""
    create_gate_1q(gate_label::String, mat::Array{Complex{Float64},2})

Creates a new user-defined gate, caches the generating matrix, and returns the
GateOps.GateSymbol key for use in a circuit. Matrix is converted to 2x2 StaticArrays
SMatrix internally.

# Examples
```julia-repl

```
"""
function create_gate_1q(gate_label::String, mat::Array{<:Number,2})
    return create_gate_1q(gate_label, SArray{Tuple{2,2},Complex{Float64},2,4}(mat))
end

# =========================================================================== #
#                       Custom gate storage: 2 qubit
# =========================================================================== #

"""
    create_gate_2q(gate_label::String, gen_func::Function)

Creates a new user-defined gate, caches the generating function, and returns the
GateOps.GateSymbol key for use in a circuit.

# Examples
```julia-repl

```
"""
function create_gate_2q(gate_label::String, gen_func::Function)
    mat4x4 = nothing
    if applicable(gen_func, pi/2)
        mat4x4 = gen_func(pi/2)
    else
        mat4x4 = gen_func()
    end
    if size(mat4x4) != (4,4)
        error("Please ensure generated matrix is 4x4")
    end
    gs = GateOps.GateSymbol(Symbol(gate_label))
    cache_gate!(gs, gen_func)
    return gs
end

"""
    create_gate_2q(gate_label::String, mat::SArray{Tuple{2,2},Complex{Float64},2,4})

Creates a new user-defined gate, caches the generating matrix, and returns the
GateOps.GateSymbol key for use in a circuit.

# Examples
```julia-repl

```
"""
function create_gate_2q(gate_label::String, mat::SArray{Tuple{4,4},Complex{Float64},2,16})
    return create_gate_2q(gate_label, ()->mat)
end
"""
    create_gate_2q(gate_label::String, mat::Array{Complex{Float64},2})

Creates a new user-defined gate, caches the generating matrix, and returns the
GateOps.GateSymbol key for use in a circuit. Matrix is converted to 2x2 StaticArrays
SMatrix internally.

# Examples
```julia-repl

```
"""
function create_gate_2q(gate_label::String, mat::Array{<:Number,2})
    return create_gate_2q(gate_label, SArray{Tuple{4,4},Complex{Float64},2,16}(mat))
end

# =========================================================================== #

"""
    cache_gate!(label::GateOps.AGateSymbol, mat::Matrix{<:Number})

Adds a mapping between label=>mat for fast retrieval of gates in circuit generation.

# Examples
```julia-repl
julia> QXZoo.GateMap.cache_gate!(QXZoo.GateOps.GateSymbol(:mygate), ()->[ 1 0; 0 1])
```
"""
function cache_gate!(key::Union{GateOps.GateSymbol, GateOps.GateSymbolP, Function}, mat_func::Function)
    if ~haskey(gates, key)
        gates[key] = mat_func
    end
    return ;
end

"""
    replace_gate!(label::GateOps.AGateSymbol, mat::Matrix{<:Number})

Replaces existing gate mapping as defined with cache_gate! otherwise performs no-op.

# Examples
```julia-repl
julia> QXZoo.GateMap.replace_gate!(QXZoo.GateOps.GateSymbol(:mygate), ()->[ 1 0; 0 -1])
```
"""
function replace_gate!(key::Union{GateOps.GateSymbol, GateOps.GateSymbolP, Function}, mat_func::Function)
    if haskey(gates, key)
        gates[key] = mat_func
    end
    return ;
end

# =========================================================================== #
#                       Projection operators
# =========================================================================== #
"""
    p00()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the |0><0| projector gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.p00()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 1.0+0.0im  0.0+0.0im
 0.0+0.0im  0.0+0.0im
```
"""
function p00()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [1.0+0.0im 0.0; 0.0 0.0]
end

"""
    p01()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the |0><1| projector gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.p01()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.0+0.0im  1.0+0.0im
 0.0+0.0im  0.0+0.0im
```
"""
function p01()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [0.0 1.0+0.0im; 0.0 0.0]
end

"""
    p10()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the |1><0| projector gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.p10()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.0+0.0im  0.0+0.0im
 1.0+0.0im  0.0+0.0im
```
"""
function p10()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [0.0 0.0; 1.0+0.0im 0.0]
end

"""
    p11()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the |1><1| projector gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.p11()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.0+0.0im  0.0+0.0im
 0.0+0.0im  1.0+0.0im
```
"""
function p11()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [0.0 0.0; 0.0 1.0+0.0im]
end

# =========================================================================== #
#                       Pauli operators
# =========================================================================== #
"""
    x()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the Pauli-X gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.x()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.0+0.0im  1.0+0.0im
 1.0+0.0im  0.0+0.0im
```
"""
function x()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [0.0 1.0+0.0im; 1.0+0.0im 0.0]
end

"""
    y()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the Pauli-Y gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.y()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.0+0.0im  -0.0-1.0im
 0.0+1.0im   0.0+0.0im
```
"""
function y()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [0.0 -1.0im; 1.0im 0.0]
end

"""
    z()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the Pauli-Z gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.z()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 1.0+0.0im   0.0+0.0im
 0.0+0.0im  -1.0+0.0im
```
"""
function z()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return @SMatrix [1.0+0.0im 0.0; 0.0 -1.0+0.0im]
end

# =========================================================================== #
#                       Additional operators
# =========================================================================== #

"""
    h()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the Hadamard gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.h()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.707107+0.0im   0.707107+0.0im
 0.707107+0.0im  -0.707107+0.0im
```
"""
function h()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return SMatrix{2,2}([1.0+0.0im 1.0+0.0im; 1.0+0.0im -1.0+0.0im].*(1/sqrt(2)))
end

"""
    I()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the 2x2 identity gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.I()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 1.0+0.0im  0.0+0.0im
 0.0+0.0im  1.0+0.0im
```
"""
function I()::Diagonal{Complex{Float64},SArray{Tuple{2},Complex{Float64},1,2}}
    return StaticArrays.SDiagonal(1.0+0im,1.0+0im)
end

"""
    s()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the S gate (sqrt(Z)).

# Examples
```julia-repl
julia> QXZoo.GateMap.s()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 1.0+0.0im  0.0+0.0im
 0.0+0.0im  0.0+1.0im
```
"""
function s()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return sqrt(z())
end

"""
    t()::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the T gate (sqrt(S)).

# Examples
```julia-repl
julia> QXZoo.GateMap.t()
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 1.0+0.0im       0.0+0.0im
 0.0+0.0im  0.707107+0.707107im
```
"""
function t()::SArray{Tuple{2,2},Complex{Float64},2,4}
    return sqrt(s())
end

# =========================================================================== #
#                       Rotation operators
# =========================================================================== #
"""
    r_x(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the parametric r_x gate: exp(-0.5im*θ*x())

# Examples
```julia-repl
julia> QXZoo.GateMap.r_x(pi/3)
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.866025+0.0im       0.0-0.5im
      0.0-0.5im  0.866025+0.0im
```
"""
function r_x(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}
    return exp(-1im*x()*θ/2)
end
"""
    r_y(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the parametric r_y gate: exp(-0.5im*θ*y())

# Examples
```julia-repl
julia> QXZoo.GateMap.r_y(pi/6)
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.965926+0.0im  -0.258819+0.0im
 0.258819+0.0im   0.965926+0.0im
```
"""
function r_y(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}
    return exp(-1im*y()*θ/2)
end
"""
    r_z(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the parametric r_z gate: exp(-0.5im*θ*z())

# Examples
```julia-repl
julia> QXZoo.GateMap.r_z(pi/2)
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 0.707107-0.707107im       0.0+0.0im
      0.0+0.0im       0.707107+0.707107im
```
"""
function r_z(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}
    return exp(-1im*z()*θ/2)
end
"""
    r_phase(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}

Function which generates the parametric phase shifting gate [1 0; 0 exp(1im*θ)].
Note, this is not the Phase gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.r_phase(pi/4)
2×2 StaticArrays.SArray{Tuple{2,2},Complex{Float64},2,4} with indices SOneTo(2)×SOneTo(2):
 1.0+0.0im       0.0+0.0im
 0.0+0.0im  0.707107+0.707107im
```
"""
function r_phase(θ::Number)::SArray{Tuple{2,2},Complex{Float64},2,4}
    return [1 0; 0 exp(1im*θ)]
end

# =========================================================================== #
#                       Controlled Pauli operators
# =========================================================================== #
"""
    c_x()::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled Pauli-X gate as a block diagonal 4x4 matrix.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_x()
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im  0.0+0.0im  0.0+0.0im
 0.0+0.0im  1.0+0.0im  0.0+0.0im  0.0+0.0im
 0.0+0.0im  0.0+0.0im  0.0+0.0im  1.0+0.0im
 0.0+0.0im  0.0+0.0im  1.0+0.0im  0.0+0.0im
```
"""
function c_x()::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), x())
end

"""
    c_y()::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled Pauli-X gate as a block diagonal 4x4 matrix.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_y()
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im  0.0+0.0im  0.0+0.0im
 0.0+0.0im  1.0+0.0im  0.0+0.0im  0.0+0.0im
 0.0+0.0im  0.0+0.0im  0.0+0.0im  0.0-1.0im
 0.0+0.0im  0.0+0.0im  0.0+1.0im  0.0+0.0im
```
"""
function c_y()::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), y())
end

"""
    c_z()::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled Pauli-X gate as a block diagonal 4x4 matrix.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_z()
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im  0.0+0.0im   0.0+0.0im
 0.0+0.0im  1.0+0.0im  0.0+0.0im   0.0+0.0im
 0.0+0.0im  0.0+0.0im  1.0+0.0im   0.0+0.0im
 0.0+0.0im  0.0+0.0im  0.0+0.0im  -1.0+0.0im
```
"""
function c_z()::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), z())
end

# =========================================================================== #
#                       Controlled rotation operators
# =========================================================================== #
"""
    c_r_x(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled parametric r_x gate as a block diagonal 4x4 matrix.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_r_x(pi/3)
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im       0.0+0.0im       0.0+0.0im
 0.0+0.0im  1.0+0.0im       0.0+0.0im       0.0+0.0im
 0.0+0.0im  0.0+0.0im  0.866025+0.0im       0.0-0.5im
 0.0+0.0im  0.0+0.0im       0.0-0.5im  0.866025+0.0im
```
"""
function c_r_x(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), r_x(θ))
end

"""
    c_r_y(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled parametric r_y gate as a block diagonal 4x4 matrix.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_r_y(pi/4)
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im       0.0+0.0im        0.0+0.0im
 0.0+0.0im  1.0+0.0im       0.0+0.0im        0.0+0.0im
 0.0+0.0im  0.0+0.0im   0.92388+0.0im  -0.382683+0.0im
 0.0+0.0im  0.0+0.0im  0.382683+0.0im    0.92388+0.0im
```
"""
function c_r_y(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), r_y(θ))
end
"""
    c_r_z(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled parametric r_y gate as a block diagonal 4x4 matrix.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_r_z(pi/6)
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im       0.0+0.0im            0.0+0.0im
 0.0+0.0im  1.0+0.0im       0.0+0.0im            0.0+0.0im
 0.0+0.0im  0.0+0.0im  0.965926-0.258819im       0.0+0.0im
 0.0+0.0im  0.0+0.0im       0.0+0.0im       0.965926+0.258819im
```
"""
function c_r_z(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), r_z(θ))
end

"""
    c_r_phase(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}

Function which generates the controlled parametric phase shifting gate [1 0; 0 exp(1im*θ)] as a block diagonal 4x4 matrix.

Note, this is not the controlled Phase gate.

# Examples
```julia-repl
julia> QXZoo.GateMap.c_r_phase(pi/9)
4×4 StaticArrays.SArray{Tuple{4,4},Complex{Float64},2,16} with indices SOneTo(4)×SOneTo(4):
 1.0+0.0im  0.0+0.0im  0.0+0.0im       0.0+0.0im
 0.0+0.0im  1.0+0.0im  0.0+0.0im       0.0+0.0im
 0.0+0.0im  0.0+0.0im  1.0+0.0im       0.0+0.0im
 0.0+0.0im  0.0+0.0im  0.0+0.0im  0.939693+0.34202im
```
"""
function c_r_phase(θ::Number)::SArray{Tuple{4,4},Complex{Float64},2,16}
    return kron(p00(), I()) + kron(p11(), r_phase(θ))
end

# =========================================================================== #
"""
Allow access directly using a gate-call for ease-of-use
"""
function Base.getindex(gc::Dict{GateOps.AGateSymbol, Matrix{<:Number}}, gs::GateOps.AGateCall)
    return gc[gs.gate_label]
end

"""
    clear_cache()

Empty the stored gates from the cache.
"""
function clear_cache()
    empty!(gates)
end

end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/UMAT.jl/blob/master/LICENSE

module UMAT

using Libdl
using Materials
using Parameters
using LinearAlgebra
using Tensors

const abaqus = [1 4 6; 0 2 5; 0 0 3]

pkg_dir = dirname(Base.find_package("UMAT"))
if Sys.iswindows()
    lib_dir = joinpath(pkg_dir,"..","deps","usr","bin")
else
    lib_dir = joinpath(pkg_dir,"..","deps","usr","lib")
end

"""
Variables updated by UMAT routine.
"""
@with_kw struct UmatVariableState <: AbstractMaterialState
    NTENS :: Int
    NSTATV :: Int = zero(Int)
    jacobian :: SymmetricTensor = zero(SymmetricTensor{4,convert(Int,NTENS/2),Float64})
    stress :: SymmetricTensor = zero(SymmetricTensor{2,convert(Int,NTENS/2),Float64})
    STATEV :: Array{Float64,1} = zeros(Float64, NSTATV)
    SSE :: Array{Float64,1} = zeros(Float64, 1)
    SPD :: Array{Float64,1} = zeros(Float64, 1)
    SCD :: Array{Float64,1} = zeros(Float64, 1)
    RPL :: Array{Float64,1} = zeros(Float64, 1)
    DDSDDT :: Array{Float64,1} = zeros(Float64, NTENS)
    DRPLDE :: Array{Float64,1} = zeros(Float64, NTENS)
    DRPLDT :: Array{Float64,1} = zeros(Float64, 1)
    PNEWDT :: Array{Float64,1} = ones(Float64, 1)
end

"""
Material parameters in order that is specific to chosen UMAT.
"""
@with_kw struct UmatParameterState <: AbstractMaterialState
    NPROPS :: Int = zero(Int)
    PROPS :: Array{Float64,1} = zeros(Float64, NPROPS)
end

"""
Variables passed in for information.
These drive evolution of the material state.
"""
@with_kw mutable struct UmatDriverState <: AbstractMaterialState
    NTENS :: Int
    strain :: SymmetricTensor = zero(SymmetricTensor{2,convert(Int,NTENS/2),Float64})
    time :: Float64 = zero(Float64)
    dtime :: Float64 = zero(Float64)
    TIME_ :: Array{Float64,1} = zeros(Float64, 2)
    TEMP :: Float64 = zero(Float64)
    PREDEF :: Float64 = zero(Float64)
end

"""
Other Abaqus UMAT variables passed in for information.
"""
@with_kw mutable struct UmatOtherState <: AbstractMaterialState
    CMNAME :: String = "U" # Ptr{Cuchar} # = 'U'
    NDI :: Int = 3
    NSHR :: Int = 3
    NTENS :: Int  # NDI + NSHR
    NSTATV :: Int
    NPROPS :: Int
    COORDS :: Array{Float64,1} = zeros(Float64, 3)
    DROT :: Array{Float64,2} = I + zeros(Float64, 3, 3)
    CELENT :: Float64 = zero(Float64)
    DFGRD0 :: Array{Float64,2} = I + zeros(Float64, 3, 3)
    DFGRD1 :: Array{Float64,2} = I + zeros(Float64, 3, 3)
    NOEL :: Int = zero(Int)
    NPT :: Int = zero(Int)
    LAYER :: Int = zero(Int)
    KSPT :: Int = zero(Int)
    JSTEP :: Array{Float64,1} = zeros(Int, 3)
    KSTEP :: Int = zero(Int)
    KINC :: Int = zero(Int)
end

"""
UMAT material structure.

`lib_path` is the path to the compiled shared library.
`behaviour` is the function name to call from the shared library
    generally this is compiler dependent, by default `umat_`.
    MFront Abaqus interface produces specific name, e.g. `ELASTICITY_3D`.
"""
@with_kw mutable struct UmatMaterial <: AbstractMaterial
    NTENS :: Int
    NSTATV :: Int = zero(Int)
    NPROPS :: Int = zero(Int)

    drivers :: UmatDriverState = UmatDriverState(NTENS=NTENS)
    ddrivers :: UmatDriverState = UmatDriverState(NTENS=NTENS)
    variables :: UmatVariableState = UmatVariableState(NTENS=NTENS, NSTATV=NSTATV)
    variables_new :: UmatVariableState = UmatVariableState(NTENS=NTENS, NSTATV=NSTATV)
    parameters :: UmatParameterState = UmatParameterState(NPROPS=NPROPS)
    dparameters :: UmatParameterState = UmatParameterState(NPROPS=NPROPS)

    umat_other :: UmatOtherState = UmatOtherState(NTENS=NTENS, NSTATV=NSTATV, NPROPS=NPROPS)

    lib_path :: String
    behaviour :: Symbol = :umat_

end

"""
Wrapper function to ccall the UMAT subroutine from the compiled shared library.
"""
function call_umat!(func_umat::Symbol, lib_path::String, STRESS_,STATEV,DDSDDE,
        SSE,SPD,SCD,RPL,DDSDDT,DRPLDE,DRPLDT,
        STRAN,DSTRAN,TIME,DTIME,TEMP,DTEMP,PREDEF,DPRED,CMNAME,NDI,NSHR,
        NTENS,NSTATV,PROPS,NPROPS,COORDS,DROT,PNEWDT,CELENT,DFGRD0,DFGRD1,
        NOEL,NPT,LAYER,KSPT,KSTEP,KINC)

    lib_umat = Libdl.dlopen(lib_path) # Open the library explicitly.
    sym_umat = Libdl.dlsym(lib_umat, func_umat) # Get a symbol for the umat function to call.

    ccall(sym_umat, Nothing,
         (Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ptr{Cuchar},Ref{Int},Ref{Int},
          Ref{Int},Ref{Int},Ref{Float64},Ref{Int},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Int},Ref{Int},Ref{Int},Ref{Int},Ref{Int},Ref{Int}),
         STRESS_,STATEV,DDSDDE,SSE,SPD,SCD,RPL,DDSDDT,DRPLDE,DRPLDT,
         STRAN,DSTRAN,TIME,DTIME,TEMP,DTEMP,PREDEF,DPRED,CMNAME,NDI,NSHR,
         NTENS,NSTATV,PROPS,NPROPS,COORDS,DROT,PNEWDT,CELENT,DFGRD0,DFGRD1,
         NOEL,NPT,LAYER,KSPT,KSTEP,KINC)

    Libdl.dlclose(lib_umat) # Close the library explicitly.

    return Nothing
end

"""
Calls UMAT and updates MaterialVariableState writing the result to material.variables_new
"""
function Materials.integrate_material!(material::UmatMaterial)
    @unpack CMNAME,NDI,NSHR,NTENS,NSTATV,NPROPS,COORDS,DROT,CELENT,DFGRD0,DFGRD1,NOEL,NPT,LAYER,KSPT,KSTEP,KINC = material.umat_other
    @unpack PROPS = material.parameters
    strain, time, TEMP, PREDEF = material.drivers.strain, material.drivers.time, material.drivers.TEMP, material.drivers.PREDEF
    dstrain, dtime, DTEMP, DPRED = material.ddrivers.strain, material.drivers.dtime, material.ddrivers.TEMP, material.ddrivers.PREDEF
    @unpack stress,STATEV,jacobian,SSE,SPD,SCD,RPL,DDSDDT,DRPLDE,DRPLDT,PNEWDT = material.variables

    # TODO need to replace with toabaqus etc.
    STRAN = tovoigt(strain; order=abaqus, offdiagscale = 2.0)
    DSTRAN = tovoigt(dstrain; order=abaqus, offdiagscale = 2.0)
    STRESS_ = tovoigt(stress, order=abaqus)
    DDSDDE = tovoigt(jacobian, order=abaqus)
    TIME_ = [(time - floor(time)) time]
    DTIME_ = dtime
    call_umat!(material.behaviour, material.lib_path, STRESS_, STATEV, DDSDDE, SSE, SPD, SCD, RPL, DDSDDT, DRPLDE, DRPLDT,
        STRAN, DSTRAN, TIME_, DTIME_, TEMP, DTEMP, PREDEF, DPRED, CMNAME, NDI, NSHR,
        NTENS, NSTATV, PROPS, NPROPS, COORDS, DROT, PNEWDT, CELENT, DFGRD0, DFGRD1,
        NOEL, NPT, LAYER, KSPT, KSTEP, KINC)

    stress = fromvoigt(SymmetricTensor{2,convert(Int,NTENS/2)}, STRESS_, order=abaqus)
    jacobian = fromvoigt(SymmetricTensor{4,convert(Int,NTENS/2)}, DDSDDE, order=abaqus)

    variables_new = UmatVariableState(NSTATV=NSTATV,NTENS=NTENS,stress=stress,
                                      STATEV=STATEV,jacobian=jacobian,SSE=SSE,
                                      SPD=SPD,SCD=SCD,RPL=RPL,DDSDDT=DDSDDT,
                                      DRPLDE=DRPLDE,DRPLDT=DRPLDT,PNEWDT=PNEWDT)
    material.variables_new = variables_new
end

function Materials.reset_material!(material::UmatMaterial)
    material.ddrivers = UmatDriverState(NTENS=material.NTENS)
    material.dparameters = UmatParameterState(NPROPS=material.NPROPS)
    material.variables_new = UmatVariableState(NTENS=material.NTENS, NSTATV=material.NSTATV)
    return nothing
end

include("gurson_model.jl")
include("druckerprager_model.jl")

export UmatMaterial, UmatDriverState, UmatParameterState, UmatVariableState
export UmatOtherState, GursonMaterial, DruckerPragerMaterial, integrate_material!

# Re-export update_material! and uniaxial_increment! from Materials.jl for convenience
export update_material!, uniaxial_increment!
end # module


"""
    couple_models(aero::LiftingLine, stru::GEBT, dyn::RigidBody, flap::LiftingLineFlaps)

Create a coupled model using a lifting line aerodynamic model, a
geometrically exact beam theory model, a rigid body dynamics model, and a lifting
line control surface model.  This model introduces additional parameters
corresponding to the freestream air density ``\\rho_\\infty``, followed by the
external loads ``F_{x,i}, F_{y,i}, F_{z,i}, M_{x,i}, M_{y,i}, M_{z,i}`` or
displacements ``u_{x,i}, u_{y,i}, u_{z,i}, \\theta_{x,i}, \\theta_{y,i},
\\theta_{z,i}`` for each node, followed by the constant distributed loads
``f_{x,i}, f_{y,i}, f_{z,i}, m_{x,i}, m_{y,i}, m_{z,i}`` applied on each beam
element (excluding aerodynamic loads), followed by the control surface
deflections ``\\delta_1, \\delta_2, \\dots, \\delta_N``.

**NOTE: When using this model, the local frame for each beam element should be
oriented with the x-axis along the beam's axis, the y-axis forward, and the
z-axis normal to the surface**
"""
function couple_models(aero::LiftingLine, stru::GEBT, dyn::RigidBody,
    flap::LiftingLineFlaps)

    return (aero, stru, dyn, flap)
end

# --- traits --- #

function number_of_additional_parameters(aero::LiftingLine, stru::GEBT, dyn::RigidBody,
    flap::LiftingLineFlaps)

    return 1 + 6*length(stru.icol_point) + 6*length(stru.icol_elem) + length(flap.gains)
end

function coupling_inplaceness(::Type{<:LiftingLine}, ::Type{<:GEBT}, ::Type{<:RigidBody},
    ::Type{<:LiftingLineFlaps})

    return InPlace()
end

function coupling_rate_jacobian_type(::Type{LiftingLine{NA,TA}}, ::Type{<:GEBT},
    ::Type{<:RigidBody}, ::Type{LiftingLineFlaps{NF,NG,TF,TG}}) where {NA,NF,NG,TA,TF,TG}

    aero_model_types = TA.parameters
    flap_model_types = TF.parameters
    if all(islinear.(coupling_rate_jacobian_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Linear()
    else
        return Nonlinear()
    end
end

function coupling_state_jacobian_type(::Type{LiftingLine{NA,TA}}, ::Type{<:GEBT},
    ::Type{<:RigidBody}, ::Type{LiftingLineFlaps{NF,NG,TF,TG}}) where {NA,NF,NG,TA,TF,TG}

    aero_model_types = TA.parameters
    flap_model_types = TF.parameters
    if all(islinear.(coupling_state_jacobian_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Linear()
    else
        return Nonlinear()
    end
end

coupling_parameter_jacobian_type(::Type{<:LiftingLine}, ::Type{<:GEBT},
    ::Type{RigidBody}, ::Type{LiftingLineFlaps}) = Nonlinear()

function coupling_time_gradient_type(::Type{LiftingLine{NA,TA}}, ::Type{<:GEBT},
    ::Type{<:RigidBody}, ::Type{LiftingLineFlaps{NF,NG,TF,TG}}) where {NA,NF,NG,TA,TF,TG}

    aero_model_types = TA.parameters
    flap_model_types = TF.parameters
    if all(isempty.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Empty()
    elseif all(iszero.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Zeros()
    elseif all(isconstant.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Constant()
    elseif all(islinear.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Linear()
    else
        return Nonlinear()
    end
end

# --- methods --- #

function get_coupling_inputs!(y, aero::LiftingLine{NA,TA}, stru::GEBT, dyn::RigidBody,
    flap::LiftingLineFlaps{NF,NG,TF,TG}, dx, x, p, t) where {NA,NF,NG,TA,TF,TG}

    # extract number of state variables, inputs, and parameters
    nxa = number_of_states(aero) # number of aerodynamic states
    nxs = number_of_states(stru) # number of structural states
    nxd = number_of_states(dyn) # number of rigid body states
    nxf = number_of_states(flap) # number of control surface states
    nya = number_of_inputs(aero) # number of aerodynamic inputs
    nys = number_of_inputs(stru) # number of structural inputs
    nyd = number_of_inputs(dyn) # number of rigid body inputs
    nyf = number_of_inputs(flap) # number of control surface inputs
    npa = number_of_parameters(aero) # number of aerodynamic parameters
    nps = number_of_parameters(stru) # number of structural parameters
    npd = number_of_parameters(dyn) # number of rigid body parameters
    npf = number_of_parameters(flap) # number of control surface parameters
    npadd = number_of_additional_parameters(aero, stru, dyn, flap) # number of additional parameters

    # get indices for state variables, inputs, and parameters
    ixa = 1:nxa # indices of aerodynamic states
    iya = 1:nya # indices of aerodynamic inputs
    ipa = 1:npa # indices of aerodynamic parameters
    ixs = nxa + 1 : nxa + nxs # indices of structural states
    iys = nya + 1 : nya + nys # indices of structural inputs
    ips = npa + 1 : npa + nps # indices of structural parameters
    ixd = nxa + nxs + 1 : nxa + nxs + nxd # indices of rigid body states
    iyd = nya + nys + 1 : nya + nys + nyd # indices of rigid body inputs
    ipd = npa + nps + 1 : npa + nps + npd # indices of rigid body parameters
    ixf = nxa + nxs + nxd + 1 : nxa + nxs + nxd + nxf # indices of control surface states
    iyf = nya + nys + nyd + 1 : nya + nys + nyd + nyf # indices of control surface inputs
    ipf = npa + nps + npd + 1 : npa + nps + npd + npf # indices of control surface parameters
    ipadd = npa + nps + npd + npf + 1 : npa + nps + npd + npf + npadd # indices of additional parameters
    ixas = state_indices(aero.models) # indices of aerodynamic states for each section
    iyas = input_indices(aero.models) # indices of aerodynamic inputs for each section
    ipas = parameter_indices(aero.models) # indices of aerodynamic inputs for each section
    ixfs = state_indices(flap.models) # indices of control surface states for each section
    iyfs = input_indices(flap.models) # indices of control surface inputs for each section
    ipfs = parameter_indices(flap.models) # indices of control surface parameters for each section

    # separate state variables, inputs, and parameters
    dxa = view(dx, ixa) # aerodynamic rates
    xa = view(x, ixa) # aerodynamic states
    ya = view(y, iya) # aerodynamic inputs
    pa = view(p, ipa) # aerodynamic parameters
    dxs = view(dx, ixs) # structural rates
    xs = view(x, ixs) # structural states
    ys = view(y, iys) # structural inputs
    ps = view(p, ips) # structural parameters
    dxd = view(dx, ixd) # rigid body rates
    xd = view(x, ixd) # rigid body states
    yd = view(y, iyd) # rigid body inputs
    pd = view(p, ipd) # rigid body parameters
    dxf = view(dx, ixf) # structural rates
    xf = view(x, ixf) # structural states
    yf = view(y, iyf) # structural inputs
    pf = view(p, ipf) # structural parameters
    padd = view(p, ipadd) # additional parameters for coupled model
    dxas = view.(Ref(dxa), ixas) # aerodynamic rates for each section
    xas = view.(Ref(xa), ixas) # aerodynamic states for each section
    yas = view.(Ref(ya), iyas) # aerodynamic inputs for each section
    pas = view.(Ref(pa), ipas) # aerodynamic parameters for each section
    dxfs = view.(Ref(dxf), ixfs) # control surface rates for each section
    xfs = view.(Ref(xf), ixfs) # control surface states for each section
    yfs = view.(Ref(yf), iyfs) # control surface inputs for each section
    pfs = view.(Ref(pf), ipfs) # control surface parameters for each section

    # number of points and elements
    npoint = length(stru.icol_point)
    nelem = length(stru.icol_elem)

    # rigid body states
    xr, yr, zr, ϕr, θr, ψr, ur, vr, wr, pr, qr, rr = xd

    # rigid body state rates
    dxr, dyr, dzr, dϕr, dθr, dψr, dur, dvr, dwr, dpr, dqr, drr = dxd

    # extract global parameters
    ρ = padd[1] # freestream air density
    δ = padd[SVector{NG}(1 + 6*npoint + 6*nelem + 1 : 1 + 6*npoint + 6*nelem + NG)]
    dδ = zero(δ)

    # rigid body linear and angular velocity
    V = SVector(ur, vr, wr)
    Ω = SVector(pr, qr, rr)

    # rigid body linear and angular acceleration
    dV = SVector(dur, dvr, dwr)
    dΩ = SVector(dpr, dqr, drr)

    # construct assembly from parameters
    assembly = gxbeam_assembly(ps, npoint, nelem, stru.start, stru.stop)

    # initialize total forces and moments
    Ftot = @SVector zeros(3)
    Mtot = @SVector zeros(3)

    # initialize rigid body properties
    mass = 0.0
    Ir = @SMatrix zeros(3,3)

    # save prescribed point loads/displacements
    for ip = 1:npoint
        yoff = 6*(ip-1)
        poff = 1 + 6*(ip-1)
        ys[yoff+1] = padd[poff+1]
        ys[yoff+2] = padd[poff+2]
        ys[yoff+3] = padd[poff+3]
        ys[yoff+4] = padd[poff+4]
        ys[yoff+5] = padd[poff+5]
        ys[yoff+6] = padd[poff+6]
    end

    # add prescribed loads to total forces and moments
    for ip = 1:npoint
        # point location
        point = assembly.points[ip]
        # prescribed condition identities
        prescribed_forces = SVector{6}(view(stru.displacement, :, ip)) .== false
        # point loads
        poff = 1 + 6*(ip-1)
        F1 = ifelse(prescribed_forces[1], padd[poff+1], zero(padd[poff+1]))
        F2 = ifelse(prescribed_forces[2], padd[poff+2], zero(padd[poff+2]))
        F3 = ifelse(prescribed_forces[3], padd[poff+3], zero(padd[poff+3]))
        M1 = ifelse(prescribed_forces[4], padd[poff+4], zero(padd[poff+4]))
        M2 = ifelse(prescribed_forces[5], padd[poff+5], zero(padd[poff+5]))
        M3 = ifelse(prescribed_forces[6], padd[poff+6], zero(padd[poff+6]))
        Fp = SVector(F1, F2, F3)
        Mp = SVector(M1, M2, M3)
        Ftot += Fp
        Mtot += cross(point, Fp) + Mp
    end

    # calculate aerodynamic inputs, distributed loads, and element properties
    for i = 1:NA
        # models for this section
        section_aero = aero.models[i]
        section_stru = LiftingLineSection()
        section_flap = flap.models[i]
        section_ctrl = LiftingLineSectionControl()
        section_models = (section_aero, section_stru, section_flap, section_ctrl)

        # model dimensions for this section
        Nxai = number_of_states(section_aero)
        Nyai = number_of_inputs(section_aero)
        Npai = number_of_parameters(section_aero)
        Nxsi = number_of_states(section_stru)
        Nysi = number_of_inputs(section_stru)
        Npsi = number_of_parameters(section_stru)
        Nxfi = number_of_states(section_flap)
        Nyfi = number_of_inputs(section_flap)
        Npfi = number_of_parameters(section_flap)
        Nxci = number_of_states(section_ctrl)
        Nyci = number_of_inputs(section_ctrl)
        Npci = number_of_parameters(section_ctrl)

        # section properties
        icol = stru.icol_elem[i]
        element = assembly.elements[i]
        u_elem = SVector(xs[icol], xs[icol+1], xs[icol+2]) # linear displacement
        θ_elem = SVector(xs[icol+3], xs[icol+4], xs[icol+5]) # angular displacement
        F_elem = SVector(xs[icol+6], xs[icol+7], xs[icol+8]) .* stru.force_scaling # internal force
        M_elem = SVector(xs[icol+9], xs[icol+10], xs[icol+11]) .* stru.force_scaling # internal moment
        P_elem = SVector(xs[icol+12], xs[icol+13], xs[icol+14]) .* stru.mass_scaling # linear momentum
        H_elem = SVector(xs[icol+15], xs[icol+16], xs[icol+17]) .* stru.mass_scaling # angular momentum

        dF_elem = SVector(dxs[icol+6], dxs[icol+7], dxs[icol+8]) .* stru.force_scaling
        dM_elem = SVector(dxs[icol+9], dxs[icol+10], dxs[icol+11]) .* stru.force_scaling
        dP_elem = SVector(dxs[icol+12], dxs[icol+13], dxs[icol+14]) .* stru.mass_scaling
        dH_elem = SVector(dxs[icol+15], dxs[icol+16], dxs[icol+17]) .* stru.mass_scaling

        # convert rotation parameter to Wiener-Milenkovic parameters
        scaling = GXBeam.rotation_parameter_scaling(θ_elem)
        θ_elem *= scaling # angular displacement (Wiener-Milenkovic parameters)

        # length and location of the element
        ΔL = element.L # element length
        pe = element.x + u_elem # location of element (in body frame)

        # local to body transformation
        CtCab = GXBeam.get_C(θ_elem)'*element.Cab

        # local structural to aerodynamic transformation
        R = @SMatrix [0 -1 0; 1 0 0; 0 0 1]

        # local freestream linear and angular velocities
        vi = -R*(CtCab'*V + GXBeam.element_linear_velocity(element, P_elem, H_elem)) # local linear freestream velocity
        ωi = R*GXBeam.element_angular_velocity(element, P_elem, H_elem) # local angular freestream velocity

        # local freestream linear and angular accelerations
        dvi = -R*(CtCab'*dV + CtCab'*cross(dΩ, pe) + GXBeam.element_linear_velocity(element, dP_elem, dH_elem))
        dωi = R*CtCab'*dΩ + R*GXBeam.element_angular_velocity(element, dP_elem, dH_elem)

        # element inertial properties
        poff = 3*npoint + 36*(i-1)
        μ = ps[poff + 31] # mass per unit length
        xm2 = ps[poff + 32] # center of mass location
        xm3 = ps[poff + 33] # center of mass location
        i22 = ps[poff + 34] # rotational inertia
        i33 = ps[poff + 35] # rotational inertia
        i23 = ps[poff + 36] # product of inertia
        me = ΔL*μ # element mass
        Ie = ΔL*(@SMatrix [i22+i33 0 0; 0 i22 -i23; 0 -i23 i33]) # local frame inertia
        Ie = (CtCab*R')*Ie*(R*CtCab') # body frame inertia
        Ie = me*(pe'*pe*I - pe*pe') + Ie # body frame inertia about origin

        # NOTE: We assume that the local beam frame y-axis is oriented towards
        # the leading edge and the z-axis is oriented up
        # NOTE: We assume the local aerodynamic frame is oriented with the x-axis
        # in the chordwise direction and the z-axis in the (airfoil) normal
        # direction.

        # section state variable rates
        duai = SVector{Nxai}(dxas[i]) # aerodynamic state variables
        dusi = vcat(dvi, dωi) # structural state variables
        dufi = SVector{Nxfi}(dxfs[i]) # control surface state variables
        duci = SMatrix{1,NG}(getindex.(flap.gains, i)) * dδ # controller state variables
        dui = vcat(duai, dusi, dufi, duci)

        # section state variables
        uai = SVector{Nxai}(xas[i]) # aerodynamic state variables
        usi = vcat(vi, ωi) # structural state variables
        ufi = SVector{Nxfi}(xfs[i]) # control surface state variables
        uci = SMatrix{1,NG}(getindex.(flap.gains, i)) * δ # controller state variables
        ui = vcat(uai, usi, ufi, uci)

        # section parameters
        pai = SVector{Npai}(pas[i]) # aerodynamic parameters
        psi = SVector(ρ) # structural parameters
        pfi = SVector{Npfi}(pfs[i]) # control surface parameters
        pci = SVector{0,Float64}() # controller parameters
        pi = vcat(pai, psi, pfi, pci)

        # section inputs
        yi = get_coupling_inputs(section_models, dui, ui, pi, t)

        # separate inputs
        yai = view(yi, 1:Nyai)
        ysi = view(yi, Nyai + 1 : Nyai + Nysi)
        yfi = view(yi, Nyai + Nysi + 1 : Nyai + Nysi + Nyfi)
        yci = view(yi, Nyai + Nysi + Nyfi + 1 : Nyai + Nysi + Nyfi + Nyci)

        # save local inputs
        y[iya[iyas[i]]] .= yai # aerodynamic inputs
        y[iyf[iyfs[i]]] .= yfi # control surface inputs

        # section aerodynamic loads (in body frame)
        fi = CtCab*R'*SVector(ysi[1], ysi[2], ysi[3])
        mi = CtCab*R'*SVector(ysi[4], ysi[5], ysi[6])

        # add apparent forces due to body frame linear and angular acceleration
        fi += me*dV - cross(me*dΩ, pe)
        mi += me*dΩ

        # add constant distributed loads (in body frame)
        poff = 1 + 6*npoint + 6*(i-1)
        fi += SVector(padd[poff+1], padd[poff+2], padd[poff+3])
        mi += SVector(padd[poff+4], padd[poff+5], padd[poff+6])

        # save distributed loads for this element (in body frame)
        yoff = 6*npoint + 6*(i-1)
        y[iys[yoff+1 : yoff+3]] = fi
        y[iys[yoff+4 : yoff+6]] = mi

        # add distributed loads to total forces and moments
        Ftot += ΔL*fi
        Mtot += cross(pe, ΔL*fi) + ΔL*mi

        # add element mass and inertia to total mass and inertia
        mass += me
        Ir += Ie
    end

    # save body frame linear and angular velocities
    yoff = 6*npoint + 6*nelem
    y[iys[yoff+1]] = ur
    y[iys[yoff+2]] = vr
    y[iys[yoff+3]] = wr
    y[iys[yoff+4]] = pr
    y[iys[yoff+5]] = qr
    y[iys[yoff+6]] = rr

    # save rigid body model inputs
    y[iyd[1]] = mass
    y[iyd[2]] = Ixx = Ir[1,1]
    y[iyd[3]] = Iyy = Ir[2,2]
    y[iyd[4]] = Izz = Ir[3,3]
    y[iyd[5]] = Ixz = Ir[1,3]
    y[iyd[6]] = Ixy = Ir[1,2]
    y[iyd[7]] = Iyz = Ir[2,3]
    y[iyd[8:10]] = Ftot
    y[iyd[11:13]] = Mtot

    return y
end

# --- performance overloads --- #

# TODO

# --- convenience methods --- #

function set_additional_parameters!(padd, model::LiftingLine, stru::GEBT,
    dyn::RigidBody, flap::LiftingLineFlaps{NF,NG,TF,TG}; rho, point_conditions,
    element_loads, delta) where {NF,NG,TF,TG}

    np = length(stru.icol_point)
    ne = length(stru.icol_elem)

    padd[1] = rho

    for ip = 1:np
        padd[1+6*(ip-1)+1] = point_conditions[6*(ip-1)+1]
        padd[1+6*(ip-1)+2] = point_conditions[6*(ip-1)+2]
        padd[1+6*(ip-1)+3] = point_conditions[6*(ip-1)+3]
        padd[1+6*(ip-1)+4] = point_conditions[6*(ip-1)+4]
        padd[1+6*(ip-1)+5] = point_conditions[6*(ip-1)+5]
        padd[1+6*(ip-1)+6] = point_conditions[6*(ip-1)+6]
    end

    for ie = 1:ne
        padd[1+6*np+6*(ie-1)+1] = element_loads[6*(ie-1)+1]
        padd[1+6*np+6*(ie-1)+2] = element_loads[6*(ie-1)+2]
        padd[1+6*np+6*(ie-1)+3] = element_loads[6*(ie-1)+3]
        padd[1+6*np+6*(ie-1)+4] = element_loads[6*(ie-1)+4]
        padd[1+6*np+6*(ie-1)+5] = element_loads[6*(ie-1)+5]
        padd[1+6*np+6*(ie-1)+6] = element_loads[6*(ie-1)+6]
    end

    padd[1 + 6*np + 6*ne + 1 : 1 + 6*np + 6*ne + NG] = delta

    return padd
end

function separate_additional_parameters(model::LiftingLine, stru::GEBT,
    dyn::RigidBody, flap::LiftingLineFlaps{NF,NG,TF,TG}, padd) where {NF,NG,TF,TG}

    np = length(stru.icol_point)
    ne = length(stru.icol_elem)

    rho = padd[1]

    point_conditions = reshape(view(padd, 1 + 1 : 1 + 6*np), 6, np)

    element_loads = reshape(view(padd, 1 + 6*np + 1 : 1 + 6*np + 6*ne), 6, ne)

    delta = view(padd, 1 + 6*np + 6*ne + 1 : 1 + 6*np + 6*ne + NG)

    return (rho = rho, point_conditions = point_conditions, element_loads = element_loads,
        delta = delta)
end


include("Include.jl")

function update_model_dictionary(parameter_guess_array,model_data_dictionary)

    # what is the host_type?
    host_type = :cell_free

     # Phase 1: parameter update =========================================================================== #
    # update the paramaters in the model data dictionary -
    # for now - lets only search over dG's -
    R = model_data_dictionary["R"]
    T_K = model_data_dictionary["T_K"]

    # compute W -
    tmp_W_array = Float64[]
    for index = 1:8
        parameter_guess = parameter_guess_array[index]
        value = exp(-1*parameter_guess/(R*T_K))
        push!(tmp_W_array,value)
    end

    # update the control W's -
    control_parameter_dictionary = model_data_dictionary["control_parameter_dictionary"]
    control_parameter_dictionary["W_clssrA_RNAP"] = tmp_W_array[1]          # 1
	control_parameter_dictionary["W_clssrA_sigma_28"] = tmp_W_array[2]      # 2
	control_parameter_dictionary["W_deGFPssrA_RNAP"] = tmp_W_array[3]       # 3
	control_parameter_dictionary["W_deGFPssrA_sigma_70"] = tmp_W_array[4]   # 4
	control_parameter_dictionary["W_deGFPssrA_clssrA"] = tmp_W_array[5]     # 5
	control_parameter_dictionary["W_sigma_28_RNAP"] = tmp_W_array[6]        # 6
	control_parameter_dictionary["W_sigma_28_sigma_70"] = tmp_W_array[7]    # 7
	control_parameter_dictionary["W_sigma_28_clssrA"] = tmp_W_array[8]      # 8
    model_data_dictionary["control_parameter_dictionary"] = control_parameter_dictionary

    binding_parameter_dictionary = model_data_dictionary["binding_parameter_dictionary"]
    binding_parameter_dictionary["n_clssrA_sigma_28"] = parameter_guess_array[9]
	binding_parameter_dictionary["K_clssrA_sigma_28"] = parameter_guess_array[10]
	binding_parameter_dictionary["n_deGFPssrA_sigma_70"] = parameter_guess_array[11]
	binding_parameter_dictionary["K_deGFPssrA_sigma_70"] = parameter_guess_array[12]
	binding_parameter_dictionary["n_deGFPssrA_clssrA"] = parameter_guess_array[13]
	binding_parameter_dictionary["K_deGFPssrA_clssrA"] = parameter_guess_array[14]
	binding_parameter_dictionary["n_sigma_28_sigma_70"] = parameter_guess_array[15]
	binding_parameter_dictionary["K_sigma_28_sigma_70"] = parameter_guess_array[16]
	binding_parameter_dictionary["n_sigma_28_clssrA"] = parameter_guess_array[17]
    binding_parameter_dictionary["K_sigma_28_clssrA"] = parameter_guess_array[18]
    model_data_dictionary["binding_parameter_dictionary"] = binding_parameter_dictionary

    # time constant modifier -
    time_constant_modifier_array = [
		0.0	                          ;	  # 1	clssrA
		0.0	                          ;	  # 2	deGFPssrA
		0.0	                          ;	  # 3	sigma_28
		0.0                           ;	  # 4	sigma_70
		parameter_guess_array[19]     ;	  # 5	mRNA_clssrA
		parameter_guess_array[20]     ;	  # 6	mRNA_deGFPssrA
		parameter_guess_array[21]     ;	  # 7	mRNA_sigma_28
		1.0 	                      ;	  # 8	mRNA_sigma_70
		parameter_guess_array[22]	  ;	  # 9	protein_clssrA
		parameter_guess_array[23]	  ;	  # 10	protein_deGFPssrA
		parameter_guess_array[24]	  ;	  # 11	protein_sigma_28
		1.0	                          ;	  # 12	protein_sigma_70
	]
    model_data_dictionary["time_constant_modifier_array"] = time_constant_modifier_array

    # setup degradation_modifier_array -
    degradation_modifier_array = [
		0.0	                            ;	# 1	clssrA
		0.0	                            ;	# 2	deGFPssrA
		0.0	                            ;	# 3	sigma_28
		0.0	                            ;	# 4	sigma_70
		parameter_guess_array[25]	    ;	# 5	mRNA_clssrA 0.0085
		parameter_guess_array[26]		;	# 6	mRNA_deGFPssrA 0.05
		parameter_guess_array[27]		;	# 7	mRNA_sigma_28
		0.0                             ;   # 8 mRNA_sigma_70
		parameter_guess_array[28]		;	# 9	protein_clssrA
		parameter_guess_array[29]		;	# 10	protein_deGFPssrA
		parameter_guess_array[30]		;	# 11	protein_sigma_28
	    parameter_guess_array[31]		;	# 12	protein_sigma_70
    ]

    # update the translation time -
    model_data_dictionary["half_life_translation_capacity"] = parameter_guess_array[32]

    # lastly, update KL -
    biophysical_constants_dictionary = model_data_dictionary["biophysical_constants_dictionary"]
    biophysical_constants_dictionary["translation_saturation_constant"] = parameter_guess_array[33]
    model_data_dictionary["biophysical_constants_dictionary"] = biophysical_constants_dictionary

    # grab defaults -
    species_symbol_type_array = model_data_dictionary["species_symbol_type_array"]
    protein_coding_length_array = model_data_dictionary["protein_coding_length_array"]
    gene_coding_length_array = model_data_dictionary["gene_coding_length_array"]
    time_constant_modifier_array = model_data_dictionary["time_constant_modifier_array"]
    initial_condition_array = model_data_dictionary["initial_condition_array"]

    # # get gene IC -
    idx_gene = findall(x->x==:gene,species_symbol_type_array)
    gene_abundance_array = initial_condition_array[idx_gene]

    # Precompute the translation parameters -
	translation_parameter_array = precompute_translation_parameter_array(biophysical_constants_dictionary, protein_coding_length_array, time_constant_modifier_array,host_type)
    model_data_dictionary["translation_parameter_array"] = translation_parameter_array

	# Precompute the kinetic limit of transcription -
	transcription_kinetic_limit_array = precompute_transcription_kinetic_limit_array(biophysical_constants_dictionary, gene_coding_length_array, gene_abundance_array, time_constant_modifier_array, host_type)
    model_data_dictionary["transcription_kinetic_limit_array"] = transcription_kinetic_limit_array

    # Dilution degrdation matrix -
    dilution_degradation_matrix = build_dilution_degradation_matrix(biophysical_constants_dictionary,species_symbol_type_array,degradation_modifier_array)
    model_data_dictionary["dilution_degradation_matrix"] = dilution_degradation_matrix
    # ===================================================================================================== #

    # return -
    return model_data_dictionary
end

# Function to set some values for the weights, gene lengths etc -
function customize_data_dictionary(default_data_dictionary::Dict{String,Any}, host_type::Symbol)::Dict{String,Any}

    # make a deepcopy -
    customized_data_dictionary = deepcopy(default_data_dictionary)

    # set the gene lengths -
    gene_length_array = customized_data_dictionary["gene_coding_length_array"]
    gene_length_array[1] = 744.0    # clssrA
    gene_length_array[2] = 711.0    # deGFPssrA
    gene_length_array[3] = 720.0    # S28
    gene_length_array[4] = 720.0    # S70 => doesn't matter, gene expression = 0

    # set the protein lengths -
    protein_coding_length_array = customized_data_dictionary["protein_coding_length_array"]
    protein_coding_length_array[1] = 248.0  # clssrA
    protein_coding_length_array[2] = 237.0  # deGFPssrA
    protein_coding_length_array[3] = 240.0  # S28
    protein_coding_length_array[4] = 240.0  # S70 => doesn't matter, translation = 0

    # set ICs -
    initial_condition_array = customized_data_dictionary["initial_condition_array"]
    initial_condition_array[1] = 0.001  # clssrA gene
    initial_condition_array[2] = 0.008  # deGFP gene
    initial_condition_array[3] = 0.0015 # S28 gene
    initial_condition_array[4] = 0.0    # no S70 gene, just S70 protein in the extract
    #initial_condition_array[11] = 0.020 # sigma28 protein in muM
    initial_condition_array[12] = 0.035 # S70 protein in muM

    # setup the W's -
    control_parameter_dictionary = customized_data_dictionary["control_parameter_dictionary"]
    control_parameter_dictionary["W_clssrA_RNAP"] = 0.000014
	control_parameter_dictionary["W_clssrA_sigma_28"] = 1.0
	control_parameter_dictionary["W_deGFPssrA_RNAP"] = 0.000014
	control_parameter_dictionary["W_deGFPssrA_sigma_70"] = 10.0
	control_parameter_dictionary["W_deGFPssrA_clssrA"] = 100.0
	control_parameter_dictionary["W_sigma_28_RNAP"] = 0.00014
	control_parameter_dictionary["W_sigma_28_sigma_70"] = 100.0
	control_parameter_dictionary["W_sigma_28_clssrA"] = 0.1
	control_parameter_dictionary["W_sigma_70_RNAP"] = 0.0

    # setup the binding parameters -
    bP = readdlm("bP-Opt.dat")
    binding_parameter_dictionary = customized_data_dictionary["binding_parameter_dictionary"]
    bp_symbol_name_array = String[]
    push!(bp_symbol_name_array, "n_clssrA_sigma_28")
    push!(bp_symbol_name_array, "K_clssrA_sigma_28")
    push!(bp_symbol_name_array, "n_deGFPssrA_sigma_70")
    push!(bp_symbol_name_array, "K_deGFPssrA_sigma_70")
    push!(bp_symbol_name_array, "n_deGFPssrA_clssrA")
    push!(bp_symbol_name_array, "K_deGFPssrA_clssrA")
    push!(bp_symbol_name_array, "n_sigma_28_sigma_70")
    push!(bp_symbol_name_array, "K_sigma_28_sigma_70")
    push!(bp_symbol_name_array, "n_sigma_28_clssrA")
    push!(bp_symbol_name_array, "K_sigma_28_clssrA")
    for (index,key_text) in enumerate(bp_symbol_name_array)

        # get value -
        value = bP[index]

        # cache -
        binding_parameter_dictionary[key_text] = value
    end

    # setup degradation_modifier_array -
    degradation_modifier_array = [
		0.0	    ;	# 1	clssrA
		0.0	    ;	# 2	deGFPssrA
		0.0	    ;	# 3	sigma_28
		0.0	    ;	# 4	sigma_70
		0.05    ;	# 5	mRNA_clssrA 0.0085
		0.05	;	# 6	mRNA_deGFPssrA 0.05
		0.05	;	# 7	mRNA_sigma_28
		1.0	    ;	# 8	mRNA_sigma_70
		30.0	;	# 9	protein_clssrA
		28.0	;	# 10	protein_deGFPssrA
		1.5	    ;	# 11	protein_sigma_28
		1.0	    ;	# 12	protein_sigma_70
	]
    customized_data_dictionary["degradation_modifier_array"] = degradation_modifier_array
    biophysical_constants_dictionary = customized_data_dictionary["biophysical_constants_dictionary"]
    species_symbol_type_array = customized_data_dictionary["species_symbol_type_array"]
    protein_coding_length_array = customized_data_dictionary["protein_coding_length_array"]
    gene_coding_length_array = customized_data_dictionary["gene_coding_length_array"]

    # get gene IC -
    idx_gene = findall(x->x==:gene,species_symbol_type_array)
    gene_abundance_array = initial_condition_array[idx_gene]

    # Dilution degrdation matrix -
	dilution_degradation_matrix = build_dilution_degradation_matrix(biophysical_constants_dictionary,species_symbol_type_array,degradation_modifier_array)
    customized_data_dictionary["dilution_degradation_matrix"] = dilution_degradation_matrix

    # time constant modifier -
    time_constant_modifier_array = [
		0.0	                    ;	  # 1	clssrA
		0.0	                    ;	  # 2	deGFPssrA
		0.0	                    ;	  # 3	sigma_28
		0.0                     ;	  # 4	sigma_70
        0.5549555127597618      ;	  # 5	mRNA_clssrA
        0.9961201331095983      ;	  # 6	mRNA_deGFPssrA
		0.9573079006791378      ;	  # 7	mRNA_sigma_28
        2.47595196225463	    ;	  # 8	mRNA_sigma_70
		1.2340314042937286	    ;	  # 9	protein_clssrA
		2.0833904410438735 	    ;	  # 10	protein_deGFPssrA
		4.99999996459193	    ;	  # 11	protein_sigma_28
        2.47595196225463	    ;	  # 12	protein_sigma_70
	]
    customized_data_dictionary["time_constant_modifier_array"] = time_constant_modifier_array

    # Precompute the translation parameters -
	translation_parameter_array = precompute_translation_parameter_array(biophysical_constants_dictionary, protein_coding_length_array, time_constant_modifier_array,host_type)
    customized_data_dictionary["translation_parameter_array"] = translation_parameter_array

	# Precompute the kinetic limit of transcription -
	transcription_kinetic_limit_array = precompute_transcription_kinetic_limit_array(biophysical_constants_dictionary, gene_coding_length_array, gene_abundance_array, time_constant_modifier_array, host_type)
    customized_data_dictionary["transcription_kinetic_limit_array"] = transcription_kinetic_limit_array

    # return -
    return customized_data_dictionary
end

function main(path_to_ensemble_file::String, path_to_sim_dir::String; sample_fraction::Float64=1.0)

    # load the default data_dictionary -
    time_start = 0.0
    time_stop = 16.0
    time_step_size = 0.01

    # what is the host_type?
    host_type = :cell_free

    # path to parameters -
    path_to_biophysical_constants_file = "./CellFree.json"

    # Load the data dictionary (uses the default biophysical_constants file)
    default_data_dictionary = build_data_dictionary((time_start,time_stop,time_step_size), path_to_biophysical_constants_file, host_type)

    # Set some values for the weights, gene lengths etc -
    custom_data_dictionary = customize_data_dictionary(default_data_dictionary,host_type)

    # load the ensemble file -
    parameter_ensemble_array = readdlm(path_to_ensemble_file)

    # sort the array -
    parameter_ensemble_array = sort_parameter_ensemble_array(parameter_ensemble_array)

    # what is the size of the ensemble -
    (number_of_parameters, number_of_samples) = size(parameter_ensemble_array)

    # take the top sample fraction -
    number_of_samples = round(Int,sample_fraction*number_of_samples)
    for sample_index = 1:number_of_samples

        # grab the parameter array -
        local_parameter_array = parameter_ensemble_array[:,sample_index]

        # update the default data_dictionary to reflect the new parameters -
        model_data_dictionary = update_model_dictionary(local_parameter_array, custom_data_dictionary)

        # solve the model equations -
        (T,X) = SolveBalances(time_start,time_stop,time_step_size,model_data_dictionary)

        # dump -
        data_array = [T X]
        filename = "$(path_to_sim_dir)/simulation-P$(sample_index).dat"
        writedlm(filename, data_array)

        # give user some notification -
        @show sample_index
    end
end

# setup paths -
path_to_sim_file = "$(pwd())/simulations"
path_to_ensemble_file = "$(pwd())/Ensemble-c1-restriction-T20.dat"

# call -
main(path_to_ensemble_file, path_to_sim_file; sample_fraction = 1.0)