# The methods below are specialized on the mortar type
# and called from the basic `create_cache` method at the top.
function create_cache(mesh::P4estMesh{3}, equations, mortar_l2::LobattoLegendreMortarL2, uEltype)
  # TODO: Taal compare performance of different types
  fstar_threaded = [Array{uEltype, 4}(undef, nvariables(equations), nnodes(mortar_l2), nnodes(mortar_l2), 4)
                    for _ in 1:Threads.nthreads()]

  fstar_tmp_threaded = [Array{uEltype, 3}(undef, nvariables(equations), nnodes(mortar_l2), nnodes(mortar_l2))
                         for _ in 1:Threads.nthreads()]
  u_threaded          = [Array{uEltype, 3}(undef, nvariables(equations), nnodes(mortar_l2), nnodes(mortar_l2))
                         for _ in 1:Threads.nthreads()]

  (; fstar_threaded, fstar_tmp_threaded, u_threaded)
end

function prolong2interfaces!(cache, u,
                             mesh::P4estMesh{3},
                             equations, surface_integral, dg::DG)
  @unpack interfaces = cache

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  @threaded for interface in eachinterface(dg, cache)
    primary_element   = interfaces.element_ids[1, interface]
    secondary_element = interfaces.element_ids[2, interface]

    primary_indices   = interfaces.node_indices[1, interface]
    secondary_indices = interfaces.node_indices[2, interface]

    # Use Tuple `node_indices` and `evaluate_index` to copy values
    # from the correct face and in the correct orientation
    for j in eachnode(dg), i in eachnode(dg)
      for v in eachvariable(equations)
        interfaces.u[1, v, i, j, interface] = u[v, evaluate_index(primary_indices, size_, 1, i, j),
                                                   evaluate_index(primary_indices, size_, 2, i, j),
                                                   evaluate_index(primary_indices, size_, 3, i, j),
                                                   primary_element]

        interfaces.u[2, v, i, j, interface] = u[v, evaluate_index(secondary_indices, size_, 1, i, j),
                                                   evaluate_index(secondary_indices, size_, 2, i, j),
                                                   evaluate_index(secondary_indices, size_, 3, i, j),
                                                   secondary_element]
      end
    end
  end

  return nothing
end

function calc_interface_flux!(surface_flux_values,
                              mesh::P4estMesh{3},
                              nonconservative_terms::Val{false},
                              equations, surface_integral, dg::DG, cache)
  @unpack surface_flux = surface_integral
  @unpack u, element_ids, node_indices = cache.interfaces

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  @threaded for interface in eachinterface(dg, cache)
    # Get neighboring elements
    primary_element   = element_ids[1, interface]
    secondary_element = element_ids[2, interface]

    primary_indices   = node_indices[1, interface]
    secondary_indices = node_indices[2, interface]

    primary_direction   = indices2direction(primary_indices)
    secondary_direction = indices2direction(secondary_indices)

    # Use Tuple `node_indices` and `evaluate_index` to access node indices
    # at the correct face and in the correct orientation to get normal vectors
    for j in eachnode(dg), i in eachnode(dg)
      u_ll, u_rr = get_surface_node_vars(u, equations, dg, i, j, interface)

      normal_vector = get_normal_vector(primary_direction, cache,
                                        evaluate_index(primary_indices, size_, 1, i, j),
                                        evaluate_index(primary_indices, size_, 2, i, j),
                                        evaluate_index(primary_indices, size_, 3, i, j),
                                        primary_element)

      flux_ = surface_flux(u_ll, u_rr, normal_vector, equations)

      # Use Tuple `node_indices` and `evaluate_index_surface` to copy flux
      # to left and right element storage in the correct orientation
      for v in eachvariable(equations)
        surf_i = evaluate_index_surface(primary_indices, size_, 1, i, j)
        surf_j = evaluate_index_surface(primary_indices, size_, 2, i, j)
        surface_flux_values[v, surf_i, surf_j, primary_direction, primary_element] = flux_[v]

        surf_i = evaluate_index_surface(secondary_indices, size_, 1, i, j)
        surf_j = evaluate_index_surface(secondary_indices, size_, 2, i, j)
        surface_flux_values[v, surf_i, surf_j, secondary_direction, secondary_element] = -flux_[v]
      end
    end
  end

  return nothing
end

function prolong2boundaries!(cache, u,
                             mesh::P4estMesh{3},
                             equations, surface_integral, dg::DG)
  @unpack boundaries = cache

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  @threaded for boundary in eachboundary(dg, cache)
    element       = boundaries.element_ids[boundary]
    node_indices  = boundaries.node_indices[boundary]

    # Use Tuple `node_indices` and `evaluate_index` to copy values
    # from the correct face and in the correct orientation
    for j in eachnode(dg), i in eachnode(dg)
      for v in eachvariable(equations)
        boundaries.u[v, i, j, boundary] = u[v, evaluate_index(node_indices, size_, 1, i, j),
                                               evaluate_index(node_indices, size_, 2, i, j),
                                               evaluate_index(node_indices, size_, 3, i, j),
                                               element]
      end
    end
  end

  return nothing
end

function calc_boundary_flux!(cache, t, boundary_condition, boundary_indexing,
                             mesh::P4estMesh{3},
                             equations, surface_integral, dg::DG)
  @unpack boundaries = cache
  @unpack surface_flux_values, node_coordinates = cache.elements
  @unpack surface_flux = surface_integral

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  @threaded for local_index in eachindex(boundary_indexing)
    # Use the local index to get the global boundary index from the pre-sorted list
    boundary = boundary_indexing[local_index]

    element       = boundaries.element_ids[boundary]
    node_indices  = boundaries.node_indices[boundary]
    direction     = indices2direction(node_indices)

    # Use Tuple `node_indices` and `evaluate_index` to access node indices
    # at the correct face and in the correct orientation to get normal vectors
    for j in eachnode(dg), i in eachnode(dg)
      node_i = evaluate_index(node_indices, size_, 1, i, j)
      node_j = evaluate_index(node_indices, size_, 2, i, j)
      node_k = evaluate_index(node_indices, size_, 3, i, j)

      # Extract solution data from boundary container
      u_inner = get_node_vars(boundaries.u, equations, dg, i, j, boundary)

      # Outward-pointing normal vector
      normal_vector = get_normal_vector(direction, cache, node_i, node_j, node_k, element)

      # Coordinates at boundary node
      x = get_node_coords(node_coordinates, equations, dg, node_i, node_j, node_k, element)

      flux_ = boundary_condition(u_inner, normal_vector, x, t, surface_flux, equations)

      # Use Tuple `node_indices` and `evaluate_index_surface` to copy flux
      # to left and right element storage in the correct orientation
      for v in eachvariable(equations)
        surf_i = evaluate_index_surface(node_indices, size_, 1, i, j)
        surf_j = evaluate_index_surface(node_indices, size_, 2, i, j)
        surface_flux_values[v, surf_i, surf_j, direction, element] = flux_[v]
      end
    end
  end
end

function prolong2mortars!(cache, u,
                          mesh::P4estMesh{3}, equations,
                          mortar_l2::LobattoLegendreMortarL2,
                          surface_integral, dg::DGSEM)
  # temporary buffer for projections
  @unpack fstar_tmp_threaded = cache
  @unpack element_ids, node_indices = cache.mortars

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  @threaded for mortar in eachmortar(dg, cache)
    fstar_tmp = fstar_tmp_threaded[Threads.threadid()]

    small_indices = node_indices[1, mortar]
    large_indices = node_indices[2, mortar]

    # Copy solution small to small
    for pos in 1:4
      for j in eachnode(dg), i in eachnode(dg)
        for v in eachvariable(equations)
          # Use Tuple `node_indices` and `evaluate_index` to copy values
          # from the correct face and in the correct orientation
          cache.mortars.u[1, v, pos, i, j, mortar] = u[v, evaluate_index(small_indices, size_, 1, i, j),
                                                          evaluate_index(small_indices, size_, 2, i, j),
                                                          evaluate_index(small_indices, size_, 3, i, j),
                                                          element_ids[pos, mortar]]
        end
      end
    end

    # Buffer to copy solution values of the large element in the correct orientation
    # before interpolating
    u_buffer = cache.u_threaded[Threads.threadid()]

    # Copy solution of large element face to buffer in the correct orientation
    for j in eachnode(dg), i in eachnode(dg)
      for v in eachvariable(equations)
        # Use Tuple `node_indices` and `evaluate_index` to copy values
        # from the correct face and in the correct orientation
        u_buffer[v, i, j] = u[v, evaluate_index(large_indices, size_, 1, i, j),
                                 evaluate_index(large_indices, size_, 2, i, j),
                                 evaluate_index(large_indices, size_, 3, i, j),
                                 element_ids[5, mortar]]
      end
    end

    # Interpolate large element face data from buffer to small face locations
    multiply_dimensionwise!(view(cache.mortars.u, 2, :, 1, :, :, mortar),
                            mortar_l2.forward_lower,
                            mortar_l2.forward_lower,
                            u_buffer,
                            fstar_tmp)
    multiply_dimensionwise!(view(cache.mortars.u, 2, :, 2, :, :, mortar),
                            mortar_l2.forward_upper,
                            mortar_l2.forward_lower,
                            u_buffer,
                            fstar_tmp)
    multiply_dimensionwise!(view(cache.mortars.u, 2, :, 3, :, :, mortar),
                            mortar_l2.forward_lower,
                            mortar_l2.forward_upper,
                            u_buffer,
                            fstar_tmp)
    multiply_dimensionwise!(view(cache.mortars.u, 2, :, 4, :, :, mortar),
                            mortar_l2.forward_upper,
                            mortar_l2.forward_upper,
                            u_buffer,
                            fstar_tmp)
  end

  return nothing
end

function calc_mortar_flux!(surface_flux_values,
                           mesh::P4estMesh{3},
                           nonconservative_terms::Val{false}, equations,
                           mortar_l2::LobattoLegendreMortarL2,
                           surface_integral, dg::DG, cache)
  @unpack u, element_ids, node_indices = cache.mortars
  @unpack fstar_threaded, fstar_tmp_threaded = cache
  @unpack surface_flux = surface_integral

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  @threaded for mortar in eachmortar(dg, cache)
    # Choose thread-specific pre-allocated container
    fstar = fstar_threaded[Threads.threadid()]
    fstar_tmp = fstar_tmp_threaded[Threads.threadid()]

    small_indices = node_indices[1, mortar]
    small_direction = indices2direction(small_indices)

    # Use Tuple `node_indices` and `evaluate_index` to access node indices
    # at the correct face and in the correct orientation to get normal vectors
    for pos in 1:4
      for j in eachnode(dg), i in eachnode(dg)
        u_ll, u_rr = get_surface_node_vars(u, equations, dg, pos, i, j, mortar)

        normal_vector = get_normal_vector(small_direction, cache,
                                          evaluate_index(small_indices, size_, 1, i, j),
                                          evaluate_index(small_indices, size_, 2, i, j),
                                          evaluate_index(small_indices, size_, 3, i, j),
                                          element_ids[pos, mortar])

        flux_ = surface_flux(u_ll, u_rr, normal_vector, equations)

        # Copy flux to buffer
        set_node_vars!(fstar, flux_, equations, dg, i, j, pos)
      end
    end

    # Buffer to interpolate flux values of the large element to before copying
    # in the correct orientation
    u_buffer = cache.u_threaded[Threads.threadid()]

    mortar_fluxes_to_elements!(surface_flux_values,
                               mesh, equations, mortar_l2, dg, cache,
                               mortar, fstar, u_buffer, fstar_tmp)
  end

  return nothing
end

@inline function mortar_fluxes_to_elements!(surface_flux_values,
                                            mesh::P4estMesh{3}, equations,
                                            mortar_l2::LobattoLegendreMortarL2,
                                            dg::DGSEM, cache, mortar, fstar, u_buffer, fstar_tmp)
  @unpack element_ids, node_indices = cache.mortars

  small_indices  = node_indices[1, mortar]
  large_indices  = node_indices[2, mortar]

  small_direction = indices2direction(small_indices)
  large_direction = indices2direction(large_indices)

  size_ = (nnodes(dg), nnodes(dg), nnodes(dg))

  # Copy solution small to small
  for pos in 1:4
    for j in eachnode(dg), i in eachnode(dg)
      for v in eachvariable(equations)
        # Use Tuple `node_indices` and `evaluate_index_surface` to copy flux
        # to left and right element storage in the correct orientation
        surface_index1 = evaluate_index_surface(small_indices, size_, 1, i, j)
        surface_index2 = evaluate_index_surface(small_indices, size_, 2, i, j)
        surface_flux_values[v, surface_index1, surface_index2, small_direction,
                            element_ids[pos, mortar]] = fstar[v, i, j, pos]
      end
    end
  end

  large_element = element_ids[5, mortar]

  # Project small fluxes to large element.
  multiply_dimensionwise!(
    u_buffer,
    mortar_l2.reverse_lower, mortar_l2.reverse_lower,
    view(fstar, .., 1),
    fstar_tmp)
  add_multiply_dimensionwise!(
    u_buffer,
    mortar_l2.reverse_upper, mortar_l2.reverse_lower,
    view(fstar, .., 2),
    fstar_tmp)
  add_multiply_dimensionwise!(
    u_buffer,
    mortar_l2.reverse_lower, mortar_l2.reverse_upper,
    view(fstar, .., 3),
    fstar_tmp)
  add_multiply_dimensionwise!(
    u_buffer,
    mortar_l2.reverse_upper, mortar_l2.reverse_upper,
    view(fstar, .., 4),
    fstar_tmp)

  # The flux is calculated in the outward direction of the small elements,
  # so the sign must be switched to get the flux in outward direction
  # of the large element.
  # The contravariant vectors of the large element (and therefore the normal vectors
  # of the large element as well) are four times as large as the contravariant vectors
  # of the small elements. Therefore, the flux need to be scaled by a factor of 4
  # to obtain the flux of the large element.
  u_buffer .*= -4

  # Copy interpolated flux values from buffer to large element face in the correct orientation
  for j in eachnode(dg), i in eachnode(dg)
    for v in eachvariable(equations)
      # Use Tuple `node_indices` and `evaluate_index_surface` to copy flux
      # to surface flux storage in the correct orientation
      surface_index1 = evaluate_index_surface(large_indices, size_, 1, i, j)
      surface_index2 = evaluate_index_surface(large_indices, size_, 2, i, j)
      surface_flux_values[v, surface_index1, surface_index2,
                          large_direction, large_element] = u_buffer[v, i, j]
    end
  end

  return nothing
end

function calc_surface_integral!(du, u,
                                mesh::P4estMesh{3},
                                equations,
                                surface_integral::SurfaceIntegralWeakForm,
                                dg::DGSEM, cache)
  @unpack boundary_interpolation = dg.basis
  @unpack surface_flux_values = cache.elements

  # Note that all fluxes have been computed with outward-pointing normal vectors
  @threaded for element in eachelement(dg, cache)
    for m in eachnode(dg), l in eachnode(dg)
      for v in eachvariable(equations)
        # surface at -x
        du[v, 1,          l, m, element] += (surface_flux_values[v, l, m, 1, element]
                                             * boundary_interpolation[1, 1])
        # surface at +x
        du[v, nnodes(dg), l, m, element] += (surface_flux_values[v, l, m, 2, element]
                                             * boundary_interpolation[nnodes(dg), 2])
        # surface at -y
        du[v, l, 1,          m, element] += (surface_flux_values[v, l, m, 3, element]
                                             * boundary_interpolation[1, 1])
        # surface at +y
        du[v, l, nnodes(dg), m, element] += (surface_flux_values[v, l, m, 4, element]
                                             * boundary_interpolation[nnodes(dg), 2])
        # surface at -z
        du[v, l, m, 1,          element] += (surface_flux_values[v, l, m, 5, element]
                                             * boundary_interpolation[1,          1])
        # surface at +z
        du[v, l, m, nnodes(dg), element] += (surface_flux_values[v, l, m, 6, element]
                                             * boundary_interpolation[nnodes(dg), 2])
      end
    end
  end

  return nothing
end


######## CONSTRAINTS ############

# Generic fallback functions
function get_startup_shutdown(
    device,
    ::Type{<:VariableType},
    ::Type{<:AbstractDeviceFormulation},
) #  -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}}
    nothing
end

function get_min_max_limits(
    device,
    ::Type{<:VariableType},
    ::Type{<:AbstractDeviceFormulation},
) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}
    return (min=0.0, max=0.0)
end

@doc raw"""
Constructs min/max range constraint from device variable.

If min and max within an epsilon width:

``` variable[name, t] == limits.max ```

Otherwise:

``` limits.min <= variable[name, t] <= limits.max ```

where limits in constraint_infos.

# LaTeX

`` x = limits^{max}, \text{ for } |limits^{max} - limits^{min}| < \varepsilon ``

`` limits^{min} \leq x \leq limits^{max}, \text{ otherwise } ``
"""
function add_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: VariableType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), V)
    _add_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

function add_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: RangeConstraintLBExpressions,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), V)
    _add_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

function add_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: RangeConstraintUBExpressions,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), V)
    _add_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

function add_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ComponentActivePowerVariableLimitsConstraint,
    U <: VariableType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), V)
    _add_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_parameterized_upper_bound_range_constraints_impl!(
        container,
        T,
        array,
        ActivePowerTimeSeriesParameter,
        devices,
        model,
    )
    return
end

function _add_lower_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}
    time_steps = get_time_steps(container)
    device_names = [PSY.get_name(d) for d in devices]

    con_lb =
        add_constraints_container!(container, T(), V, device_names, time_steps, meta="lb")

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type
        con_lb[ci_name, t] =
            JuMP.@constraint(container.JuMPmodel, array[ci_name, t] >= limits.min)
    end
    return
end

function _add_upper_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}
    time_steps = get_time_steps(container)
    device_names = [PSY.get_name(d) for d in devices]

    con_ub =
        add_constraints_container!(container, T(), V, device_names, time_steps, meta="ub")

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type
        con_ub[ci_name, t] =
            JuMP.@constraint(container.JuMPmodel, array[ci_name, t] <= limits.max)
    end
    return
end

@doc raw"""
Constructs min/max range constraint from device variable and on/off decision variable.

If device min = 0:

``` varcts[name, t] <= limits.max*varbin[name, t]) ```

``` varcts[name, t] >= 0.0 ```

Otherwise:

``` varcts[name, t] <= limits.max*varbin[name, t] ```

``` varcts[name, t] >= limits.min*varbin[name, t] ```

where limits in constraint_infos.

# LaTeX

`` 0 \leq x^{cts} \leq limits^{max} x^{bin}, \text{ for } limits^{min} = 0 ``

`` limits^{min} x^{bin} \leq x^{cts} \leq limits^{max} x^{bin}, \text{ otherwise } ``
"""
function add_semicontinuous_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: VariableType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), V)
    _add_semicontinuous_lower_bound_range_constraints_impl!(
        container,
        T,
        array,
        devices,
        model,
    )
    _add_semicontinuous_upper_bound_range_constraints_impl!(
        container,
        T,
        array,
        devices,
        model,
    )
    return
end

function add_semicontinuous_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: RangeConstraintLBExpressions,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), V)
    _add_semicontinuous_lower_bound_range_constraints_impl!(
        container,
        T,
        array,
        devices,
        model,
    )
    return
end

function add_semicontinuous_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: RangeConstraintUBExpressions,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), V)
    _add_semicontinuous_upper_bound_range_constraints_impl!(
        container,
        T,
        array,
        devices,
        model,
    )
    return
end

function _add_semicontinuous_lower_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{V},
    ::DeviceModel{V, W},
) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]
    binary_variables = [OnVariable()]

    con_lb = add_constraints_container!(container, T(), V, names, time_steps, meta="lb")

    @assert length(binary_variables) == 1 "Expected $(binary_variables) for $U $V $T $W to be length 1"
    varbin = get_variable(container, only(binary_variables), V)

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type
        con_lb[ci_name, t] = JuMP.@constraint(
            container.JuMPmodel,
            array[ci_name, t] >= limits.min * varbin[ci_name, t]
        )
    end
    return
end

function _add_semicontinuous_upper_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]
    binary_variables = [OnVariable()]

    con_ub = add_constraints_container!(container, T(), V, names, time_steps, meta="ub")

    @assert length(binary_variables) == 1 "Expected $(binary_variables) for $U $V $T $W to be length 1"
    varbin = get_variable(container, only(binary_variables), V)

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type
        con_ub[ci_name, t] = JuMP.@constraint(
            container.JuMPmodel,
            array[ci_name, t] <= limits.max * varbin[ci_name, t]
        )
    end
    return
end

@doc raw"""
Constructs min/max range constraint from device variable and reservation decision variable.

``` varcts[name, t] <= limits.max * (1 - varbin[name, t]) ```

``` varcts[name, t] >= limits.min * (1 - varbin[name, t]) ```

where limits in constraint_infos.

# LaTeX

`` 0 \leq x^{cts} \leq limits^{max} (1 - x^{bin}), \text{ for } limits^{min} = 0 ``

`` limits^{min} (1 - x^{bin}) \leq x^{cts} \leq limits^{max} (1 - x^{bin}), \text{ otherwise } ``
"""
function add_reserve_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: InputActivePowerVariableLimitsConstraint,
    U <: VariableType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), V)
    _add_reserve_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_reserve_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

function add_reserve_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: InputActivePowerVariableLimitsConstraint,
    U <: ExpressionType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), W)
    _add_reserve_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_reserve_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

function _add_reserve_lower_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{V},
    ::DeviceModel{V, W},
) where {
    T <: InputActivePowerVariableLimitsConstraint,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]
    binary_variables = [ReservationVariable()]

    IS.@assert_op length(binary_variables) == 1
    varbin = get_variable(container, only(binary_variables), V)

    names = [PSY.get_name(x) for x in devices]
    # MOI has a semicontinous set, but after some tests is not clear most MILP solvers support it.
    # In the future this can be updated
    con_lb = add_constraints_container!(container, T(), V, names, time_steps, meta="lb")

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W)
        con_lb[ci_name, t] = JuMP.@constraint(
            container.JuMPmodel,
            array[ci_name, t] >= limits.min * (1 - varbin[ci_name, t])
        )
    end
    return
end

function _add_reserve_upper_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
) where {
    T <: InputActivePowerVariableLimitsConstraint,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]
    binary_variables = [ReservationVariable()]

    IS.@assert_op length(binary_variables) == 1
    varbin = get_variable(container, only(binary_variables), V)

    names = [PSY.get_name(x) for x in devices]
    # MOI has a semicontinous set, but after some tests is not clear most MILP solvers support it.
    # In the future this can be updated
    con_ub = add_constraints_container!(container, T(), V, names, time_steps, meta="ub")

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W)
        con_ub[ci_name, t] = JuMP.@constraint(
            container.JuMPmodel,
            array[ci_name, t] <= limits.max * (1 - varbin[ci_name, t])
        )
    end
    return
end

@doc raw"""
Constructs min/max range constraint from device variable and reservation decision variable.

``` varcts[name, t] <= limits.max * varbin[name, t] ```

``` varcts[name, t] >= limits.min * varbin[name, t] ```

where limits in constraint_infos.

# LaTeX

`` limits^{min} x^{bin} \leq x^{cts} \leq limits^{max} x^{bin},``
"""
function add_reserve_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{W},
    model::DeviceModel{W, X},
    ::Type{Y},
) where {
    T <:
    Union{ReactivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint},
    U <: VariableType,
    W <: PSY.Component,
    X <: AbstractDeviceFormulation,
    Y <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), W)
    _add_reserve_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_reserve_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

@doc raw"""
Constructs min/max range constraint from device variable and reservation decision variable.

``` varcts[name, t] <= limits.max * varbin[name, t] ```

``` varcts[name, t] >= limits.min * varbin[name, t] ```

where limits in constraint_infos.

# LaTeX

`` limits^{min} x^{bin} \leq x^{cts} \leq limits^{max} x^{bin},``
"""
function add_reserve_range_constraints!(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{W},
    model::DeviceModel{W, X},
    ::Type{Y},
) where {
    T <:
    Union{ReactivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint},
    U <: ExpressionType,
    W <: PSY.Component,
    X <: AbstractDeviceFormulation,
    Y <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), W)
    _add_reserve_upper_bound_range_constraints_impl!(container, T, array, devices, model)
    _add_reserve_lower_bound_range_constraints_impl!(container, T, array, devices, model)
    return
end

function _add_reserve_lower_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{W},
    ::DeviceModel{W, X},
) where {
    T <:
    Union{ReactivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint},
    W <: PSY.Component,
    X <: AbstractDeviceFormulation,
}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]
    binary_variables = [ReservationVariable()]

    con_lb = add_constraints_container!(container, T(), W, names, time_steps, meta="lb")

    @assert length(binary_variables) == 1 "Expected $(binary_variables) for $U $V $T $W to be length 1"
    varbin = get_variable(container, only(binary_variables), W)

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, X) # depends on constraint type and formulation type
        con_lb[ci_name, t] = JuMP.@constraint(
            container.JuMPmodel,
            array[ci_name, t] >= limits.min * varbin[ci_name, t]
        )
    end
    return
end

function _add_reserve_upper_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    devices::IS.FlattenIteratorWrapper{W},
    ::DeviceModel{W, X},
) where {
    T <:
    Union{ReactivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint},
    W <: PSY.Component,
    X <: AbstractDeviceFormulation,
}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]
    binary_variables = [ReservationVariable()]

    con_ub = add_constraints_container!(container, T(), W, names, time_steps, meta="ub")

    @assert length(binary_variables) == 1 "Expected $(binary_variables) for $U $V $T $W to be length 1"
    varbin = get_variable(container, only(binary_variables), W)

    for device in devices, t in time_steps
        ci_name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, X) # depends on constraint type and formulation type
        con_ub[ci_name, t] = JuMP.@constraint(
            container.JuMPmodel,
            array[ci_name, t] <= limits.max * varbin[ci_name, t]
        )
    end
    return
end

function add_parameterized_lower_bound_range_constraints(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    ::Type{P},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: ExpressionType,
    P <: ParameterType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), V)
    _add_parameterized_lower_bound_range_constraints_impl!(
        container,
        T,
        array,
        P,
        devices,
        model,
    )
    return
end

function add_parameterized_lower_bound_range_constraints(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    ::Type{P},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: VariableType,
    P <: ParameterType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), V)
    _add_parameterized_lower_bound_range_constraints_impl!(
        container,
        T,
        array,
        P,
        devices,
        model,
    )
    return
end

# This function is re-used in SemiContinuousFeedforward
function lower_bound_range_with_parameter!(
    jump_model::JuMP.Model,
    constraint_container::JuMPConstraintArray,
    lhs_array,
    param_multiplier::JuMPFloatArray,
    param_array::Union{JuMPParamArray, JuMPFloatArray},
    devices::IS.FlattenIteratorWrapper{V},
) where {V <: PSY.Component}
    time_steps = axes(constraint_container)[2]
    for device in devices, t in time_steps
        name = PSY.get_name(device)
        constraint_container[name, t] = JuMP.@constraint(
            jump_model,
            lhs_array[name, t] >= param_multiplier[name, t] * param_array[name, t]
        )
    end
    return
end

function _add_parameterized_lower_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
) where {
    T <: ConstraintType,
    U <: ParameterType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]

    constraint = add_constraints_container!(container, T(), V, names, time_steps, meta="lb")

    parameter = get_parameter_array(container, U(), V)
    multiplier = get_parameter_multiplier_array(container, U(), V)
    jump_model = get_jump_model(container)
    lower_bound_range_with_parameter!(
        jump_model,
        constraint,
        array,
        multiplier,
        parameter,
        devices,
    )
    return
end

function add_parameterized_upper_bound_range_constraints(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    ::Type{P},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: ExpressionType,
    P <: ParameterType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_expression(container, U(), V)
    _add_parameterized_upper_bound_range_constraints_impl!(
        container,
        T,
        array,
        P,
        devices,
        model,
    )
    return
end

function add_parameterized_upper_bound_range_constraints(
    container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    ::Type{P},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
) where {
    T <: ConstraintType,
    U <: VariableType,
    P <: ParameterType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    array = get_variable(container, U(), V)
    _add_parameterized_upper_bound_range_constraints_impl!(
        container,
        T,
        array,
        P,
        devices,
        model,
    )
    return
end

# This function is re-used in SemiContinuousFeedforward
function upper_bound_range_with_parameter!(
    jump_model::JuMP.Model,
    constraint_container::JuMPConstraintArray,
    lhs_array,
    param_multiplier::JuMPFloatArray,
    param_array::Union{JuMPParamArray, JuMPFloatArray},
    devices::IS.FlattenIteratorWrapper{V},
) where {V <: PSY.Component}
    time_steps = axes(constraint_container)[2]
    for device in devices, t in time_steps
        name = PSY.get_name(device)
        constraint_container[name, t] = JuMP.@constraint(
            jump_model,
            lhs_array[name, t] <= param_multiplier[name, t] * param_array[name, t]
        )
    end
    return
end

function _add_parameterized_upper_bound_range_constraints_impl!(
    container::OptimizationContainer,
    ::Type{T},
    array,
    ::Type{P},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
) where {
    T <: ConstraintType,
    P <: ParameterType,
    V <: PSY.Component,
    W <: AbstractDeviceFormulation,
}
    time_steps = get_time_steps(container)
    names = [PSY.get_name(d) for d in devices]

    constraint = add_constraints_container!(container, T(), V, names, time_steps, meta="ub")

    parameter = get_parameter_array(container, P(), V)
    multiplier = get_parameter_multiplier_array(container, P(), V)
    jump_model = get_jump_model(container)
    upper_bound_range_with_parameter!(
        jump_model,
        constraint,
        array,
        multiplier,
        parameter,
        devices,
    )
    return
end


### Latent models ####
include("prior.jl")
include("posterior.jl")

## Exact Gaussian Process
struct LatentGP{T,Tpr<:GPPrior,Tpo<:Posterior{T},O} <: AbstractLatent{T,Tpr,Tpo}
    prior::Tpr
    post::Tpo
    opt::O
end

function LatentGP(T::DataType, dim::Int, kernel::Kernel, mean::PriorMean, opt)
    return LatentGP(
        GPPrior(deepcopy(kernel), deepcopy(mean)),
        Posterior(dim, zeros(T, dim), cholesky(Matrix{T}(I(dim)))),
        deepcopy(opt),
    )
end

@traitimpl IsFull{LatentGP}

## AbstractVarLatent

abstract type AbstractVarLatent{T,Tpr,Tpo} <: AbstractLatent{T,Tpr,Tpo} end

## Variational Gaussian Process
mutable struct VarLatent{T,Tpr<:GPPrior,Tpo<:VarPosterior{T},O} <:
               AbstractVarLatent{T,Tpr,Tpo}
    prior::Tpr
    post::Tpo
    opt::O
end

function VarLatent(T::DataType, dim::Int, kernel::Kernel, mean::PriorMean, opt)
    return VarLatent(
        GPPrior(deepcopy(kernel), deepcopy(mean)), VarPosterior{T}(dim), deepcopy(opt)
    )
end

@traitimpl IsFull{VarLatent}

## Sparse Variational Gaussian Process

mutable struct SparseVarLatent{
    T,Tpr<:GPPrior,Tpo<:VarPosterior{T},Topt,TZ<:AbstractVector,TZopt
} <: AbstractVarLatent{T,Tpr,Tpo}
    prior::Tpr
    post::Tpo
    Z::TZ
    opt::Topt
    Zopt::TZopt
end

function SparseVarLatent(
    T::DataType,
    Z::AbstractVector,
    kernel::Kernel,
    mean::PriorMean,
    opt=nothing,
    Zopt=nothing,
)
    dim = length(Z)
    return SparseVarLatent(
        GPPrior(deepcopy(kernel), deepcopy(mean)),
        VarPosterior{T}(dim),
        deepcopy(Z),
        deepcopy(opt),
        deepcopy(Zopt),
    )
end

@traitimpl IsSparse{SparseVarLatent}

## Monte-Carlo Gaussian Process

struct SampledLatent{T,Tpr<:GPPrior,Tpo<:SampledPosterior{T}} <: AbstractLatent{T,Tpr,Tpo}
    prior::Tpr
    post::Tpo
end

function SampledLatent(T::DataType, dim::Int, kernel::Kernel, mean::PriorMean)
    return SampledLatent(
        GPPrior(deepcopy(kernel), deepcopy(mean)),
        SampledPosterior(dim, zeros(T, dim), Symmetric(Matrix{T}(I(dim)))),
    )
end

@traitimpl IsFull{SampledLatent}

## Online Sparse Variational Process

mutable struct OnlineVarLatent{
    T,
    Tpr<:GPPrior,
    Tpo<:AbstractVarPosterior{T},
    Topt,
    TZ<:AbstractVector,
    TZalg<:InducingPoints.OnIPSA,
    TZopt,
} <: AbstractVarLatent{T,Tpo,Tpr}
    prior::Tpr
    post::Tpo
    Z::TZ
    Zₐ::TZ
    Zalg::TZalg
    Zupdated::Bool
    opt::Topt
    Zopt::TZopt
end

function OnlineVarLatent(
    T::DataType,
    dim::Int,
    Z::AbstractVector,
    Zalg::InducingPoints.OnIPSA,
    kernel::Kernel,
    mean::PriorMean,
    opt=nothing,
    Zopt=nothing,
)
    return OnlineVarLatent(
        GPPrior(deepcopy(kernel), deepcopy(mean)),
        OnlineVarPosterior{T}(dim),
        Z,
        deepcopy(Z),
        Zalg,
        false,
        deepcopy(opt),
        deepcopy(Zopt),
    )
end

@traitimpl IsSparse{OnlineVarLatent}

## Variational Student-T Process

mutable struct TVarLatent{T<:Real,Tpr<:TPrior,Tpo<:VarPosterior{T},O} <:
               AbstractVarLatent{T,Tpr,Tpo}
    prior::Tpr
    post::Tpo
    opt::O
end

function TVarLatent(T::DataType, ν::Real, dim::Int, kernel::Kernel, mean::PriorMean, opt)
    return TVarLatent(
        TPrior(deepcopy(kernel), deepcopy(mean), ν, rand(T), rand(T)),
        VarPosterior{T}(dim),
        deepcopy(opt),
    )
end

@traitimpl IsFull{TVarLatent}

### Functions

prior(gp::AbstractLatent) = gp.prior
kernel(gp::AbstractLatent) = kernel(prior(gp))
setkernel!(gp::AbstractLatent, kernel::Kernel) = setkernel!(prior(gp), kernel)
pr_mean(gp::AbstractLatent) = mean(prior(gp))
pr_mean(gp::AbstractLatent, X::AbstractVector) = mean(prior(gp), X)
setpr_mean!(gp::AbstractLatent, μ₀::PriorMean) = setmean!(prior(gp), μ₀)

posterior(gp::AbstractLatent) = gp.post
Distributions.dim(gp::AbstractLatent) = dim(posterior(gp))
Distributions.mean(gp::AbstractLatent) = mean(posterior(gp))
Distributions.cov(gp::AbstractLatent) = cov(posterior(gp))
Distributions.var(gp::AbstractLatent) = var(posterior(gp))
nat1(gp::AbstractVarLatent) = nat1(posterior(gp))
nat2(gp::AbstractVarLatent) = nat2(posterior(gp))

@traitfn function mean_f(gp::T, kernel_matrices) where {T <: AbstractLatent; IsFull{T}}
    return mean_f(mean(gp))
end
@traitfn function mean_f(gp::T, kernel_matrices) where {T <: AbstractLatent; !IsFull{T}}
    return mean_f(mean(gp), kernel_matrices.κ)
end

mean_f(μ::AbstractVector) = μ
mean_f(μ::AbstractVector, κ::AbstractMatrix) = κ * μ

@traitfn function var_f(gp::T, kernel_matrices) where {T <: AbstractLatent; IsFull{T}}
    return var_f(cov(gp))
end
@traitfn function var_f(gp::T, kernel_matrices) where {T <: AbstractLatent; !IsFull{T}}
    return var_f(cov(gp), kernel_matrices.κ, kernel_matrices.K̃)
end

var_f(Σ::AbstractMatrix) = diag(Σ)
var_f(Σ::AbstractMatrix, κ::AbstractMatrix, K̃::AbstractVector) = diag_ABt(κ * Σ, κ) + K̃

Zview(gp::SparseVarLatent) = gp.Z
Zview(gp::OnlineVarLatent) = gp.Z

setZ!(gp::AbstractLatent, Z::AbstractVector) = gp.Z = Z

opt(gp::AbstractLatent) = gp.opt
Zopt(::AbstractLatent) = nothing
Zopt(gp::SparseVarLatent) = gp.Zopt
Zopt(gp::OnlineVarLatent) = gp.Zopt

function compute_K(gp::AbstractLatent, X::AbstractVector, jitt::Real)
    return cholesky(kernelmatrix(kernel(gp), X) + jitt * I)
end

function compute_K(gp::AbstractLatent, jitt::Real)
    return cholesky(kernelmatrix(kernel(gp), gp.Z) + jitt * I)
end

function compute_κ(gp::SparseVarLatent, X::AbstractVector, K, jitt::Real)
    Knm = kernelmatrix(kernel(gp), X, gp.Z)
    κ = copy(Knm / K)
    K̃ = kernelmatrix_diag(kernel(gp), X) .+ jitt - diag_ABt(κ, Knm)
    all(K̃ .> 0) || error("K̃ has negative values")
    return (; Knm, κ, K̃)
end

function compute_κ(gp::OnlineVarLatent{T}, X::AbstractVector, K, jitt::Real) where {T}
    k = dim(gp)
    # Covariance with the model at t-1
    if isempty(gp.Zₐ)
        Kab = zeros(T, k, k)
        κₐ = I
        K̃ₐ = zero(Kab)
    else
        Kab = kernelmatrix(kernel(gp), gp.Zₐ, gp.Z)
        κₐ = Kab / K
        Kₐ = Symmetric(kernelmatrix(kernel(gp), gp.Zₐ) + jitt * I)
        K̃ₐ = Kₐ - κₐ * transpose(Kab)
    end

    # Covariance with a new batch
    Knm = kernelmatrix(kernel(gp), X, gp.Z)
    κ = Knm / K
    K̃ = kernelmatrix_diag(kernel(gp), X) .+ jitt - diag_ABt(κ, Knm)
    all(K̃ .> 0) || error("K̃ has negative values")
    return (; Kab, κₐ, K̃ₐ, Knm, κ, K̃)
end


# move
"""
    move(data::Data, move_vec::Array)

Do this will move the model uniformly respect to `move_vec` in unit of Angstrom

# Example
```julia-repl
data_cell = genr_cell([1 1 1])
str = Si3N4()
data_atom = genr_atom(data_cell, str)
data_move = move(data_atom, [10 10 10])
```
"""
function move(data::Data_Unit, move_vec)
    # Motion of box
    data_new = Data_Unit(data)
    if typeof(move_vec) == Array{Int64,2}
        move_vec = conv(move_vec, 1)
    end
    data_new.data_basic.box_size = broadcast(+, data_new.data_basic.box_size, move_vec)
    # Motion of atoms
    for atom = 1 : data_new.data_basic.num_atoms
        data_new.vec_atom[atom].coord += move_vec
    end
    data_new
end

function move(data::Data_Sum, move_vec)
    # Motion of box
    data_new = Data_Sum(data)
    if typeof(move_vec) == Array{Int64,2}
        move_vec = conv(move_vec, 1)
    end
    data_new.data_basic.box_size = broadcast(+, data_new.data_basic.box_size, move_vec)
    # Motion of atoms
    for atom = 1 : data_new.data_basic.num_atoms
        data_new.vec_atom[atom].coord += move_vec
    end
    data_new
end

# select and remove

function find(vec_unit::Union{Vector{T}, Int64}, list_atom) where T <: Unit
    len = length(vec_unit)
    if typeof(vec_unit) == Int64
        return 0
    else
        atom = [vec_unit[n].atom for n = 1:len]
        list = 0
        id = 0
        for i in atom
            id += 1
            for j in i
                if sum(list_atom .== j) == 1
                    list = vcat(list, id)
                    break
                end
            end
        end
        list[2:end]
    end
end

"""
    select(data_cell::Data_Cell; mode::String, para)

Do this will return a list of id of cells in specific region controled by variables `mode` and `para`

# Arguments
- `data_cell`:
- `mode`: "cylinder" and "sphere" are supported currently.
- `para`: parameters needed to describe a region correspond to each modes

# Parameters List

- `"cylinder"`: `[radius dim]`, `dim` represents which axies cylinder will be prallel to
- `"sphere"`: `[radius]`
- `"plane"`: `[vec_plane, radius, origin]`, `vec_plane` is a vector prependicular to plane that you want to cut, `radius` is the cutoff radius cells whose projection to `vec_plane` over it will be selected, `origin` is the origin point of `vec_plane`, `[0, 0, 0]` is set defaultly

# Example
```julia-repl
data_cell = genr_cell([10 10 2])
data_select = select(data_cell, mode="cylinder", para=[3, 3])
```
"""
function select(data_cell::Data_Cell; mode::String, para)
    list_mode = split("cylinder sphere plane")
    if !in(mode, list_mode)
        error(join(["Error, mode: ",mode," is not supported."]))
    end

    center = data_cell.cell_vec ./ 2

    cell_list = 0

    if mode == "cylinder"
        for cell in 1:data_cell.num_cells
            if dist(data_cell.cell_mat[cell, :], center, para[2]) <= para[1]
                cell_list = vcat(cell_list, cell)
            end
        end
    elseif mode == "sphere"
        for cell in 1:data_cell.num_cells
            if dist(data_cell.cell_mat[cell, :], center) <= para[1]
                cell_list = vcat(cell_list, cell)
            end
        end
    elseif mode == "plane"
        vec = norm_vec(para[1:3])
        dist_proj = para[4]
        if length(para) == 4
            org = [0, 0, 0]
        else
            org = para[5:7]
        end
        for cell = 1:data_cell.num_cells
            dist_now = (data_cell.cell_mat[cell, :] .- org)' * vec
            if dist_now >= dist_proj
                cell_list = vcat(cell_list, cell)
            end
        end
    end
    if length(cell_list) == 1
        error("No cell has been selected")
    end
    return cell_list[2:end]
end

"""
    select(data_atom::Data; mode::String, para)

Do this will return a list of id of atoms in specific region controled by variables `mode` and `para`

# Arguments
- `data_atom`:
- `mode`: "cylinder" and "sphere" are supported currently.
- `para`: parameters needed to describe a region correspond to each modes

# Parameters List

- `"cylinder"`: `[radius, dim]`, `dim` represents which axies cylinder will be prallel to
- `"sphere"`: `[radius]`
- `"plane"`: `[vec_plane, radius, origin]`, `vec_plane` is a vector prependicular to plane that you want to cut, `radius` is the cutoff radius atoms whose projection to `vec_plane` over it will be selected, `origin` is the origin point of `vec_plane`, `[0, 0, 0]` is set defaultly

# Example
```julia-repl
data_cell = genr_cell([10 10 2])
str = Si3N4()
data_atom = genr_atom(data_cell, str)
data_select = select(data_atom, mode="cylinder", para=[10, 3])
```
"""
function select(data_unit::Data_Unit; mode::String, para)
    list_mode = split("cylinder sphere plane")
    if !in(mode, list_mode)
        error(join(["Error, mode: ",mode," is not supported."]))
    end

    box_size = data_unit.data_basic.box_size
    box_tilt = data_unit.data_basic.box_tilt
    center = (max(data_unit.vec_atom, "coord")-min(data_unit.vec_atom, "coord")) ./ 2
    #center[1] += box_tilt[1] + box_tilt[2]
    #center[2] += box_tilt[3]

    atom_list = 0

    if mode == "cylinder"
        for atom = 1:data_unit.data_basic.num_atoms
            if dist(data_unit.vec_atom[atom], center, para[2]) <= para[1]
                atom_list = vcat(atom_list, atom)
            end
        end
    elseif mode == "sphere"
        for atom = 1:data_unit.data_basic.num_atoms
            if dist(data_unit.vec_atom[atom], center) <= para[1]
                atom_list = vcat(atom_list, atom)
            end
        end
    elseif mode == "plane"
        vec = norm_vec(para[1:3])
        dist_proj = para[4]
        if length(para) == 4
            org = [0, 0, 0]
        else
            org = para[5:7]
        end
        for atom = 1:data_unit.data_basic.num_atoms
            dist_now = (data_unit.vec_atom[atom].coord .- org)' * vec
            #dist_now = sqrt(dist_now' * dist_now)
            if dist_now >= dist_proj
                atom_list = vcat(atom_list, atom)
            end
        end
    end

    if length(atom_list) == 1
        error("No atom has been selected")
    end
    return atom_list[2:end]
end

"""
    remove!(data_cell::Data_Cell, list_cell::Array)

Do this will remove all cells in `list_cell` from `data_cell`

# Example
```julia-repl
data_cell = genr_cell([10 10 2])
data_select = select(data_cell, mode="cylinder", para=[3, 3])
data_new = remove!(data_cell, data_select)
```
"""
function remove!(data_cell::Data_Cell, list_cell)
    # Reading Input
    cell_mat = data_cell.cell_mat
    len = size(cell_mat)[1]
    # Deleting Cells
    judge = trues(len)
    for id in list_cell
        judge .&= 1:len .!= id
    end
    data_cell.cell_mat = cell_mat[judge, :]
    data_cell.num_cells = size(data_cell.cell_mat)[1]

    # Output
    data_cell
end

"""
    remove!(data::Data, list_atom::Array)

Do this will remove all atoms in `list_atom` themselves and other infomation related to them  from `data`

# Example
```julia-repl
data_cell = genr_cell([10 10 2])
str = Si3N4()
data_atom = genr_atom(data_cell, str)
data_select = select(data_atom, mode="cylinder", para=[3, 3])
data_new = remove!(data_atom, data_select)
```
"""
function remove!(data::Data, list_atom::Array)
    fields = fieldnames(typeof(data))
    name_fields = [string(fields[n]) for n = 1:length(fields)]
    num_fields = length(fields)
    list_fields = findall(x->occursin("vec", x), name_fields)

    for field in  list_fields
        # Find all elements that need to be removed
        list_id = find(getfield(data, fields[field]), list_atom)

        if list_id == 0
            continue
        else
            # Changing # of specifc field
            num_ids = length(list_id)
            para_now = name_fields[field][findall(x->in('_', x), name_fields[field])[1]+1 : end]
            para_now = Meta.parse(join(["num_", para_now, "s"]))
            para_result = getfield(data.data_basic, para_now) - num_ids
            setfield!(data.data_basic, para_now, para_result)

            # Changing Vector of each field
            setfield!(data, fields[field], remove!(getfield(data, fields[field]), list_id)) # remove!(vec::Vector{T} , id::Array) where T<:Data_Unit
        end
    end
    sort_data!(data, list_atom)
end

function remove!(vec::Union{Vector{T}, Int64}, id::Array) where T <: Unit
    len = length(vec)
    judge = trues(len)
    for i in id
        judge .&= 1 : len .!= i
    end
    vec[judge]
end

function remove!(mat; id=1, dim=1)
    if dim == 1
        result = mat[1:end .!= id, :]
    elseif dim == 2
        result = mat[:, 1:end .!= id]
    else
        error("Error, dim should be 1 or 2, representing row or column respectively!")
    end
    result
end

"""
    remove_else!(data::Data, list_atom::Array)

Do this will remove all atoms but atoms in `list_atom` themselves and other infomation related to them  from `data`

# Example
```julia-repl
data_cell = genr_cell([10 10 2])
str = Si3N4()
data_atom = genr_atom(data_cell, str)
data_select = select(data_atom, mode="cylinder", para=[3, 3])
data_new = remove_else!(data_atom, data_select)
```
"""
function remove_else!(data::Data, list_atom::Array)
    atom_all = get_data(data.vec_atom, "atom")
    atom_else = setdiff(atom_all, list_atom) # Notice: order of `atom_all` and `atom_else` is vital
    remove!(data, atom_else)
end