# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
# Since these FMAs can increase the performance of many numerical algorithms,
# we need to opt-in explicitly.
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
@muladd begin
#! format: noindent

@doc raw"""
    HyperbolicDiffusionEquations1D

The linear hyperbolic diffusion equations in one space dimension.
A description of this system can be found in Sec. 2.5 of the book
- Masatsuka (2013)
  I Do Like CFD, Too: Vol 1.
  Freely available at [http://www.cfdbooks.com/](http://www.cfdbooks.com/)
Further analysis can be found in the paper
- Nishikawa (2007)
  A first-order system approach for diffusion equation. I: Second-order residual-distribution
  schemes
  [DOI: 10.1016/j.jcp.2007.07.029](https://doi.org/10.1016/j.jcp.2007.07.029)
"""
struct HyperbolicDiffusionEquations1D{RealT <: Real} <:
       AbstractHyperbolicDiffusionEquations{1, 2}
    Lr::RealT     # reference length scale
    inv_Tr::RealT # inverse of the reference time scale
    nu::RealT     # diffusion constant
end

function HyperbolicDiffusionEquations1D(; nu = 1.0, Lr = inv(2pi))
    Tr = Lr^2 / nu
    return HyperbolicDiffusionEquations1D(promote(Lr, inv(Tr), nu)...)
end

# Together with our specialization of `Adapt.adapt_structure`,
# this allows to move semidiscretizations and their components including
# the equations to GPUs and adapt the floating point type, e.g.,
# to `Float32` to improve performance on GPUs.
function Base.similar(equations::HyperbolicDiffusionEquations1D,
                      ::Type{NewRealT}) where {NewRealT}
    return HyperbolicDiffusionEquations1D(convert(NewRealT, equations.Lr),
                                          convert(NewRealT, equations.inv_Tr),
                                          convert(NewRealT, equations.nu))
end

varnames(::typeof(cons2cons), ::HyperbolicDiffusionEquations1D) = ("phi", "q1")
varnames(::typeof(cons2prim), ::HyperbolicDiffusionEquations1D) = ("phi", "q1")
function default_analysis_errors(::HyperbolicDiffusionEquations1D)
    return (:l2_error, :linf_error, :residual)
end

@inline function residual_steady_state(du, ::HyperbolicDiffusionEquations1D)
    return abs(du[1])
end

"""
    initial_condition_poisson_nonperiodic(x, t, equations::HyperbolicDiffusionEquations1D)

A non-periodic smooth initial condition. Can be used for convergence tests in combination with
[`source_terms_poisson_nonperiodic`](@ref) and [`boundary_condition_poisson_nonperiodic`](@ref).
!!! note
    The solution is periodic but the initial guess is not.
"""
function initial_condition_poisson_nonperiodic(x, t,
                                               equations::HyperbolicDiffusionEquations1D)
    # elliptic equation: -νΔϕ = f
    # Taken from Section 6.1 of Nishikawa https://doi.org/10.1016/j.jcp.2007.07.029
    RealT = eltype(x)
    if t == 0
        # initial "guess" of the solution and its derivative
        phi = x[1]^2 - x[1]
        q1 = 2 * x[1] - 1
    else
        phi = sinpi(x[1])      # ϕ
        q1 = convert(RealT, pi) * cospi(x[1]) # ϕ_x
    end
    return SVector(phi, q1)
end

"""
    source_terms_poisson_nonperiodic(u, x, t,
                                     equations::HyperbolicDiffusionEquations1D)

Source terms that include the forcing function `f(x)` and right hand side for the hyperbolic
diffusion system that is used with [`initial_condition_poisson_nonperiodic`](@ref) and
[`boundary_condition_poisson_nonperiodic`](@ref).
"""
@inline function source_terms_poisson_nonperiodic(u, x, t,
                                                  equations::HyperbolicDiffusionEquations1D)
    # elliptic equation: -νΔϕ = f
    # analytical solution: ϕ = sin(πx) and f = π^2sin(πx)
    RealT = eltype(u)
    @unpack inv_Tr = equations

    dphi = convert(RealT, pi)^2 * sinpi(x[1])
    dq1 = -inv_Tr * u[2]

    return SVector(dphi, dq1)
end

"""
    boundary_condition_poisson_nonperiodic(u_inner, orientation, direction, x, t,
                                           surface_flux_function,
                                           equations::HyperbolicDiffusionEquations1D)

Boundary conditions used for convergence tests in combination with
[`initial_condition_poisson_nonperiodic`](@ref) and [`source_terms_poisson_nonperiodic`](@ref).
"""
function boundary_condition_poisson_nonperiodic(u_inner, orientation, direction, x, t,
                                                surface_flux_function,
                                                equations::HyperbolicDiffusionEquations1D)
    # elliptic equation: -νΔϕ = f
    RealT = eltype(u_inner)
    phi = sinpi(x[1])      # ϕ
    q1 = convert(RealT, pi) * cospi(x[1]) # ϕ_x
    u_boundary = SVector(phi, q1)

    # Calculate boundary flux
    if direction == 2 # u_inner is "left" of boundary, u_boundary is "right" of boundary
        flux = surface_flux_function(u_inner, u_boundary, orientation, equations)
    else # u_boundary is "left" of boundary, u_inner is "right" of boundary
        flux = surface_flux_function(u_boundary, u_inner, orientation, equations)
    end

    return flux
end

"""
    source_terms_harmonic(u, x, t, equations::HyperbolicDiffusionEquations1D)

Source term that only includes the forcing from the hyperbolic diffusion system.
"""
@inline function source_terms_harmonic(u, x, t,
                                       equations::HyperbolicDiffusionEquations1D)
    # harmonic solution of the form ϕ = A + B * x, so f = 0
    @unpack inv_Tr = equations

    dq1 = -inv_Tr * u[2]

    return SVector(0, dq1)
end

"""
    initial_condition_eoc_test_coupled_euler_gravity(x, t, equations::HyperbolicDiffusionEquations1D)

Setup used for convergence tests of the Euler equations with self-gravity used in
- Michael Schlottke-Lakemper, Andrew R. Winters, Hendrik Ranocha, Gregor J. Gassner (2020)
  A purely hyperbolic discontinuous Galerkin approach for self-gravitating gas dynamics
  [arXiv: 2008.10593](https://arxiv.org/abs/2008.10593)
in combination with [`source_terms_harmonic`](@ref).
"""
function initial_condition_eoc_test_coupled_euler_gravity(x, t,
                                                          equations::HyperbolicDiffusionEquations1D)

    # Determine phi_x
    RealT = eltype(x)
    G = 1             # gravitational constant
    C = -4 * G / convert(RealT, pi) # -4 * G / ndims * pi
    A = convert(RealT, 0.1)           # perturbation coefficient must match Euler setup
    rho1 = A * sinpi(x[1] - t)
    # initialize with ansatz of gravity potential
    phi = C * rho1
    q1 = C * A * convert(RealT, pi) * cospi(x[1] - t) # = gravity acceleration in x-direction

    return SVector(phi, q1)
end

# Calculate 1D flux for a single point
@inline function flux(u, orientation::Integer,
                      equations::HyperbolicDiffusionEquations1D)
    phi, q1 = u
    @unpack inv_Tr = equations

    # Ignore orientation since it is always "1" in 1D
    f1 = -equations.nu * q1
    f2 = -phi * inv_Tr

    return SVector(f1, f2)
end

# Calculate maximum wave speed for local Lax-Friedrichs-type dissipation
@inline function max_abs_speed_naive(u_ll, u_rr, orientation::Integer,
                                     equations::HyperbolicDiffusionEquations1D)
    return sqrt(equations.nu * equations.inv_Tr)
end

"""
    have_constant_speed(::HyperbolicDiffusionEquations1D)

Indicates whether the characteristic speeds are constant, i.e., independent of the solution.
Queried in the timestep computation [`StepsizeCallback`](@ref) and [`linear_structure`](@ref).

# Returns
- `True()`
"""
@inline have_constant_speed(::HyperbolicDiffusionEquations1D) = True()

@inline function max_abs_speeds(eq::HyperbolicDiffusionEquations1D)
    return sqrt(eq.nu * eq.inv_Tr)
end

# Convert conservative variables to primitive
@inline cons2prim(u, equations::HyperbolicDiffusionEquations1D) = u

# Convert conservative variables to entropy found in I Do Like CFD, Too, Vol. 1
@inline function cons2entropy(u, equations::HyperbolicDiffusionEquations1D)
    phi, q1 = u

    w1 = phi
    w2 = equations.Lr^2 * q1

    return SVector(w1, w2)
end

"""
    entropy(u, equations::AbstractHyperbolicDiffusionEquations)

Calculate entropy for a conservative state `u`,
here same as [`energy_total(u, equations::AbstractHyperbolicDiffusionEquations)`](@ref).
"""
@inline function entropy(u, equations::HyperbolicDiffusionEquations1D)
    return energy_total(u, equations)
end

@doc raw"""
    energy_total(u, equations::AbstractHyperbolicDiffusionEquations)

Calculate total energy for a conservative state `u` as
```math
E = \frac{1}{2} \left( \phi^2 + L_r^2 \Vert \boldsymbol q \Vert_2^2 \right)
"""
@inline function energy_total(u, equations::HyperbolicDiffusionEquations1D)
    # energy function as found in equations (2.5.12) in the book "I Do Like CFD, Vol. 1"
    phi, q1 = u
    return 0.5f0 * (phi^2 + equations.Lr^2 * q1^2)
end
end # @muladd
