using AccurateArithmetic

"""
    compute(::ComputeTaskABC_P, data::ABCParticleValue)

Return the particle and value as is. 

0 FLOP.
"""
function compute(::ComputeTaskABC_P, data::ABCParticleValue{P})::ABCParticleValue{P} where {P <: ABCParticle}
    return data
end

"""
    compute(::ComputeTaskABC_U, data::ABCParticleValue)

Compute an outer edge. Return the particle value with the same particle and the value multiplied by an ABC_outer_edge factor.

1 FLOP.
"""
function compute(::ComputeTaskABC_U, data::ABCParticleValue{P})::ABCParticleValue{P} where {P <: ABCParticle}
    return ABCParticleValue{P}(data.p, data.v * ABC_outer_edge(data.p))
end

"""
    compute(::ComputeTaskABC_V, data1::ABCParticleValue, data2::ABCParticleValue)

Compute a vertex. Preserve momentum and particle types (AB->C etc.) to create resulting particle, multiply values together and times a vertex factor.

6 FLOP.
"""
function compute(
    ::ComputeTaskABC_V,
    data1::ABCParticleValue{P1},
    data2::ABCParticleValue{P2},
)::ABCParticleValue where {P1 <: ABCParticle, P2 <: ABCParticle}
    p3 = ABC_preserve_momentum(data1.p, data2.p)
    dataOut = ABCParticleValue{typeof(p3)}(p3, data1.v * ABC_vertex() * data2.v)
    return dataOut
end

"""
    compute(::ComputeTaskABC_S2, data1::ABCParticleValue, data2::ABCParticleValue)

Compute a final inner edge (2 input particles, no output particle).

For valid inputs, both input particles should have the same momenta at this point.

12 FLOP.
"""
function compute(
    ::ComputeTaskABC_S2,
    data1::ParticleValue{P},
    data2::ParticleValue{P},
)::Float64 where {P <: ABCParticle}
    #=
    @assert isapprox(abs(data1.p.momentum.E), abs(data2.p.momentum.E), rtol = 0.001, atol = sqrt(eps())) "E: $(data1.p.momentum.E) vs. $(data2.p.momentum.E)"
    @assert isapprox(data1.p.momentum.px, -data2.p.momentum.px, rtol = 0.001, atol = sqrt(eps())) "px: $(data1.p.momentum.px) vs. $(data2.p.momentum.px)"
    @assert isapprox(data1.p.momentum.py, -data2.p.momentum.py, rtol = 0.001, atol = sqrt(eps())) "py: $(data1.p.momentum.py) vs. $(data2.p.momentum.py)"
    @assert isapprox(data1.p.momentum.pz, -data2.p.momentum.pz, rtol = 0.001, atol = sqrt(eps())) "pz: $(data1.p.momentum.pz) vs. $(data2.p.momentum.pz)"
    =#
    inner = ABC_inner_edge(data1.p)
    return data1.v * inner * data2.v
end

"""
    compute(::ComputeTaskABC_S1, data::ABCParticleValue)

Compute inner edge (1 input particle, 1 output particle).

11 FLOP.
"""
function compute(::ComputeTaskABC_S1, data::ABCParticleValue{P})::ABCParticleValue{P} where {P <: ABCParticle}
    return ABCParticleValue{P}(data.p, data.v * ABC_inner_edge(data.p))
end

"""
    compute(::ComputeTaskABC_Sum, data::Vector{Float64})

Compute a sum over the vector. Use an algorithm that accounts for accumulated errors in long sums with potentially large differences in magnitude of the summands.

Linearly many FLOP with growing data.
"""
function compute(::ComputeTaskABC_Sum, data::Vector{Float64})::Float64
    return sum_kbn(data)
end

"""
    get_expression(::ComputeTaskABC_P, device::AbstractDevice, inExprs::Vector{Expr}, outExpr::Expr)

Generate and return code evaluating [`ComputeTaskABC_P`](@ref) on `inSyms`, providing the output on `outSym`.
"""
function get_expression(::ComputeTaskABC_P, device::AbstractDevice, inExprs::Vector, outExpr)
    in = [eval(inExprs[1])]
    out = eval(outExpr)

    return Meta.parse("$out = compute(ComputeTaskABC_P(), $(in[1]))")
end

"""
    get_expression(::ComputeTaskABC_U, device::AbstractDevice, inExprs::Vector{Expr}, outExpr::Expr)

Generate code evaluating [`ComputeTaskABC_U`](@ref) on `inSyms`, providing the output on `outSym`.
`inSyms` should be of type [`ABCParticleValue`](@ref), `outSym` will be of type [`ABCParticleValue`](@ref).
"""
function get_expression(::ComputeTaskABC_U, device::AbstractDevice, inExprs::Vector, outExpr)
    in = [eval(inExprs[1])]
    out = eval(outExpr)

    return Meta.parse("$out = compute(ComputeTaskABC_U(), $(in[1]))")
end

"""
    get_expression(::ComputeTaskABC_V, device::AbstractDevice, inExprs::Vector{Expr}, outExpr::Expr)

Generate code evaluating [`ComputeTaskABC_V`](@ref) on `inSyms`, providing the output on `outSym`.
`inSym[1]` and `inSym[2]` should be of type [`ABCParticleValue`](@ref), `outSym` will be of type [`ABCParticleValue`](@ref).
"""
function get_expression(::ComputeTaskABC_V, device::AbstractDevice, inExprs::Vector, outExpr)
    in = [eval(inExprs[1]), eval(inExprs[2])]
    out = eval(outExpr)

    return Meta.parse("$out = compute(ComputeTaskABC_V(), $(in[1]), $(in[2]))")
end

"""
    get_expression(::ComputeTaskABC_S2, device::AbstractDevice, inExprs::Vector{Expr}, outExpr::Expr)

Generate code evaluating [`ComputeTaskABC_S2`](@ref) on `inSyms`, providing the output on `outSym`.
`inSyms[1]` and `inSyms[2]` should be of type [`ABCParticleValue`](@ref), `outSym` will be of type `Float64`.
"""
function get_expression(::ComputeTaskABC_S2, device::AbstractDevice, inExprs::Vector, outExpr)
    in = [eval(inExprs[1]), eval(inExprs[2])]
    out = eval(outExpr)

    return Meta.parse("$out = compute(ComputeTaskABC_S2(), $(in[1]), $(in[2]))")
end

"""
    get_expression(::ComputeTaskABC_S1, device::AbstractDevice, inExprs::Vector{Expr}, outExpr::Expr)

Generate code evaluating [`ComputeTaskABC_S1`](@ref) on `inSyms`, providing the output on `outSym`.
`inSyms` should be of type [`ABCParticleValue`](@ref), `outSym` will be of type [`ABCParticleValue`](@ref).
"""
function get_expression(::ComputeTaskABC_S1, device::AbstractDevice, inExprs::Vector, outExpr)
    in = [eval(inExprs[1])]
    out = eval(outExpr)

    return Meta.parse("$out = compute(ComputeTaskABC_S1(), $(in[1]))")
end

"""
    get_expression(::ComputeTaskABC_Sum, device::AbstractDevice, inExprs::Vector{Expr}, outExpr::Expr)

Generate code evaluating [`ComputeTaskABC_Sum`](@ref) on `inSyms`, providing the output on `outSym`.
`inSyms` should be of type [`Float64`], `outSym` will be of type [`Float64`].
"""
function get_expression(::ComputeTaskABC_Sum, device::AbstractDevice, inExprs::Vector, outExpr)
    in = eval.(inExprs)
    out = eval(outExpr)

    return Meta.parse("$out = compute(ComputeTaskABC_Sum(), [$(unroll_symbol_vector(in))])")
end
