include(joinpath(@__DIR__, "testcommon.jl"))
cxx_available = false

# Wrap the functions defined in C++
module CppHalfFunctions
  using CxxWrap

  @wrapmodule(CxxWrap.CxxWrapCore.libfunctions, :init_half_module)

  function __init__()
    @initcxx
  end
end

module CppTestFunctions

using CxxWrap
@wrapmodule(CxxWrap.CxxWrapCore.libfunctions, :init_test_module)

function __init__()
    @initcxx
end

end

testf(x,y) = x+y

function byval_cb(n::CppTestFunctions.BoxedNumber, result::Ref{Int32})
  result[] = CppTestFunctions.getnumber(n)
end

function byref_cb(n::CxxRef{CppTestFunctions.BoxedNumber}, result::Ref{Int32})
  result[] = CppTestFunctions.getnumber(n)
end

function byptr_cb(n::CxxPtr{CppTestFunctions.BoxedNumber}, result::Ref{Int32})
  result[] = CppTestFunctions.getnumber(n[])
end

function testf_arf(v::Vector{Float64}, s::AbstractString)
  CxxWrap.gcprotect(s) # Not sure why this is needed, since s is protected in C++ using GC_PUSH
  GC.enable(true)
  r = sum(v)
  GC.gc()
  printstyled("callback in Julia: $s = $r\n", color=:green)
  return r
end

function testf2(p::ConstCxxPtr{Float64}, n_elems::Int)
  arr = ConstArray(p, n_elems)
  @test arr[1] == 1.0
  @test arr[2] == 2.0
  return
end

@testset "$(basename(@__FILE__)[1:end-3])" begin

@test isdir(CxxWrap.prefix_path())
@test isfile(joinpath(CxxWrap.prefix_path(), "lib", "cmake", "JlCxx", "FindJulia.cmake")) || isfile(joinpath(CxxWrap.prefix_path(), "FindJulia.cmake"))

# Test functions from the CppHalfFunctions module
@test CppHalfFunctions.half_d(3) == 1.5
@show methods(CppHalfFunctions.half_d)
@test CppHalfFunctions.half_i(-2) == -1
@test CppHalfFunctions.half_u(3) == 1
@test CppHalfFunctions.half_lambda(2.) == 1.
if Sys.iswindows() && get(ENV, "CI", "false") == "true" # Disabled on Windows CI due to https://github.com/JuliaLang/julia/issues/28325
  @warn @show CppHalfFunctions.strict_half(3.) == 1.5
else
  @test CppHalfFunctions.strict_half(3.) == 1.5
end
@test_throws MethodError CppHalfFunctions.strict_half(3)

# Test functions from the CppTestFunctions module
@test CppTestFunctions.concatenate_numbers(4, 2.) == "42"
if VERSION < v"1.7"
  @test startswith(methods(CppTestFunctions.concatenate_numbers_with_named_args).ms[1].slot_syms, "#self#\0i\0d\0")
else
  @test startswith(methods(CppTestFunctions.concatenate_numbers_with_named_args)[1].slot_syms, "#self#\0i\0d\0")
end
@test CppTestFunctions.concatenate_numbers_with_kwargs(d=2., i=4) == "42"
@test CppTestFunctions.concatenate_numbers_with_default_values(3) == "35.2"
@test CppTestFunctions.concatenate_numbers_with_default_values_of_different_type(3) == "35"
@test CppTestFunctions.concatenate_numbers_with_default_kwarg(3) == "35.2"
@test CppTestFunctions.concatenate_numbers_with_default_kwarg(3, d=7) == "37"
@test hasmethod(CppTestFunctions.concatenate_numbers, (Union{Cint,CxxWrap.CxxWrapCore.argument_overloads(Cint)...},Union{Cdouble,CxxWrap.CxxWrapCore.argument_overloads(Cdouble)...}))
@test CppTestFunctions.concatenate_strings(2, "ho", "la") == "holahola"
@test CppTestFunctions.test_int32_array(Int32[1,2])
@test CppTestFunctions.test_int64_array(Int64[1,2])
@test CppTestFunctions.test_float_array(Float32[1.,2.])
@test CppTestFunctions.test_double_array([1.,2.])
if !(Sys.iswindows() && Sys.WORD_SIZE == 32)
  @test_throws ErrorException CppTestFunctions.test_exception()
end
ta = [1.,2.]
@test CppTestFunctions.test_array_len(ta) == 2
@test CppTestFunctions.test_array_get(ta, Int64(0)) == 1.
@test CppTestFunctions.test_array_get(ta, Int64(1)) == 2.
CppTestFunctions.test_array_set(ta, Int64(0), 3.)
CppTestFunctions.test_array_set(ta, Int64(1), 4.)
@test ta[1] == 3.
@test ta[2] == 4.
@test CppTestFunctions.test_type_name("IO") == "IO"

@test CppTestFunctions.test_long_long() == 42
@test CppTestFunctions.test_short() == 43

@test CppTestFunctions.test_julia_call(1.,2.) == 2
@test CppTestFunctions.test_julia_call_any(1) == 1
@test CppTestFunctions.test_julia_call_any("Foo") == "Foo"
@test CppTestFunctions.test_julia_call_any([1,2.0,"3"]) == [1,2.0,"3"]
str_arr = StdString["first", "second"]
@test CppTestFunctions.test_string_array(CxxRef.(str_arr))
darr = [1.,2.]
CppTestFunctions.test_append_array!(darr)
@test darr == [1.,2.,3.]


c_func = @safe_cfunction(testf, Float64, (Float64,Float64))
CppTestFunctions.test_safe_cfunction(c_func)
CppTestFunctions.test_safe_cfunction2(c_func)

function local_testf()
  testf_private(x,y) = x + y
  return @safe_cfunction($testf_private, Float64, (Float64,Float64))
end
CppTestFunctions.test_safe_cfunction(local_testf())

Base.show(io::IO, x::CppTestFunctions.BoxedNumber) = show(io, CppTestFunctions.getnumber(x))

let boxed_num_val = Ref{Int32}(0)
  CppTestFunctions.callback_byval(byval_cb, boxed_num_val)
  GC.gc()
  @test boxed_num_val[] == 1
  @test CppTestFunctions.boxednumber_nb_created() == 3
  @test CppTestFunctions.boxednumber_nb_deleted() == 3

  CppTestFunctions.callback_byref(byref_cb, boxed_num_val)
  @test boxed_num_val[] == 2
  @test CppTestFunctions.boxednumber_nb_created() == 4
  @test CppTestFunctions.boxednumber_nb_deleted() == 4

  CppTestFunctions.callback_byptr(byptr_cb, boxed_num_val)
  @test boxed_num_val[] == 3
  @test CppTestFunctions.boxednumber_nb_created() == 5
  @test CppTestFunctions.boxednumber_nb_deleted() == 5

  CppTestFunctions.BoxedNumber(41)
  GC.gc()
  @test CppTestFunctions.boxednumber_nb_created() == 6
  @test CppTestFunctions.boxednumber_nb_deleted() == 6

  CxxWrap.gcprotect(CppTestFunctions.BoxedNumber(42))
  GC.gc()
  @test CppTestFunctions.boxednumber_nb_created() == 7
  @test CppTestFunctions.boxednumber_nb_deleted() == 6
  @test CppTestFunctions.getnumber(CppTestFunctions.marked_boxed_value()) == 43
  @test CppTestFunctions.boxednumber_nb_created() == 8
  @test CppTestFunctions.boxednumber_nb_deleted() == 6
  CppTestFunctions.unmark_boxed()
  GC.gc()
  @test CppTestFunctions.boxednumber_nb_created() == 8
  @test CppTestFunctions.boxednumber_nb_deleted() == 7
end

c_func_arf = @safe_cfunction(testf_arf, Float64, (Any,Any))

GC.enable(false) # enabled again in testf_arf
CppTestFunctions.fn_clb(c_func_arf)
CppTestFunctions.fn_clb2(testf_arf)

c_func2 = @safe_cfunction(testf2, Nothing, (ConstCxxPtr{Float64},Int))
CppTestFunctions.test_safe_cfunction3(c_func2)

dref = Ref(0.0)
CppTestFunctions.test_double_ref(dref)
@test dref[] == 1.0

@test CppTestFunctions.get_test_double() == 0.0
cppdref = CppTestFunctions.get_test_double_ref()
@test cppdref[] == 0.0
cppdref[] = 1.0
@test CppTestFunctions.get_test_double() == 1.0

@test CppTestFunctions.test_const_string_return() == "test"
@test CppTestFunctions.test_datatype_conversion(Float64) == Float64

@test CppTestFunctions.test_val(Val(Cint(1))) == 1
@test CppTestFunctions.test_val(Val(Cint(2))) == 2
@test CppTestFunctions.test_val(Val(Cshort(3))) == 3
@test CppTestFunctions.test_val(Val(Cint(4))) == Val(Cint(4))
@test CppTestFunctions.test_val(Val(:A)) === :A
@test CppTestFunctions.test_val(Val(:B)) === :B
@test CppTestFunctions.test_val(Val(:C)) == Val(:C)

@test typeof(CppTestFunctions.test_double_pointer()) == CxxPtr{Float64}
@test CppTestFunctions.test_double_pointer() == C_NULL
@test typeof(CppTestFunctions.test_double2_pointer()) == CxxPtr{CxxPtr{Float64}}
@test CppTestFunctions.test_double2_pointer() == C_NULL
@test typeof(CppTestFunctions.test_double3_pointer()) == CxxPtr{CxxPtr{CxxPtr{Float64}}}
@test CppTestFunctions.test_double3_pointer() == C_NULL

@test CppTestFunctions.real_part(2.0 + 1.0*im) == 2.0
@test CppTestFunctions.imag_part(2.0 + 1.0*im) == 1.0
@test CppTestFunctions.make_complex(Float32(3.0), Float32(4.0)) == 3.0 + 4.0*im
@test typeof(CppTestFunctions.make_complex(Float32(3.0), Float32(4.0))) == Complex{Float32}

@test CppTestFunctions.process_irrational(π, 2) == 2*π

@test CppTestFunctions.open("foo") == "foo"

let bref = Ref{Cuchar}(0)
  @test bref[] == false
  CppTestFunctions.boolref(bref)
  @test bref[] == true
  CppTestFunctions.boolref(bref)
  @test bref[] == false
end


end # testset end

# Performance tests
const test_size = Sys.ARCH == :armv7l ? 1000000 : 50000000
const numbers = rand(test_size)
output = zeros(test_size)

# Build a function to loop over the test array
function make_loop_function(name)
    fname = Symbol(:half_loop_,name,:!)
    inner_name = Symbol(:half_,name)
    @eval begin
        function $(fname)(n::Array{Float64,1}, out_arr::Array{Float64,1})
            test_length = length(n)
          for i in 1:test_length
                out_arr[i] = $(inner_name)(n[i])
          end
        end
    end
end

# Julia version
half_julia(d::Float64) = d*0.5

libfunctions = CxxWrap.CxxWrapCore.libfunctions()

# C version
half_c(d::Float64) = ccall((:half_c, libfunctions), Cdouble, (Cdouble,), d)

# Bring C++ versions into scope
using .CppHalfFunctions: half_d, half_lambda, half_loop_cpp!, half_loop_jlcall!, half_loop_cfunc!

@static if cxx_available
  # Cxx.jl version
  cxx"""
  double half_cxx(const double d)
  {
    return 0.5*d;
  }
  """
  half_cxxjl(d::Float64) = @cxx half_cxx(d)
end

# Make the looping functions
make_loop_function(:julia)
make_loop_function(:c)
make_loop_function(:d) # C++ with regular C++ function pointer
make_loop_function(:lambda) # C++ lambda, so using std::function
if cxx_available
  make_loop_function(:cxxjl) # Cxx.jl version
end

# test that a "half" function does what it should
function test_half_function(f)
  input = [2.]
  output = [0.]
  f(input, output)
  @test output[1] == 1.
end
test_half_function(half_loop_julia!)
test_half_function(half_loop_c!)
test_half_function(half_loop_d!)
test_half_function(half_loop_lambda!)
test_half_function(half_loop_cpp!)
if cxx_available
  test_half_function(half_loop_cxxjl!)
end

# Run timing tests
println("---- Half test timings ----")
println("Julia test:")
@time half_loop_julia!(numbers, output)
@time half_loop_julia!(numbers, output)
@time half_loop_julia!(numbers, output)

println("C test:")
@time half_loop_c!(numbers, output)
@time half_loop_c!(numbers, output)
@time half_loop_c!(numbers, output)

println("C++ test:")
@time half_loop_d!(numbers, output)
@time half_loop_d!(numbers, output)
@time half_loop_d!(numbers, output)

if cxx_available
  println("Cxx.jl test:")
  @time half_loop_cxxjl!(numbers, output)
  @time half_loop_cxxjl!(numbers, output)
  @time half_loop_cxxjl!(numbers, output)
end

println("C++ lambda test:")
@time half_loop_lambda!(numbers, output)
@time half_loop_lambda!(numbers, output)
@time half_loop_lambda!(numbers, output)

println("C++ test, loop in the C++ code:")
@time half_loop_cpp!(numbers, output)
@time half_loop_cpp!(numbers, output)
@time half_loop_cpp!(numbers, output)

println("cfunction in C++ loop")
half_cfunc = @safe_cfunction(half_julia, Float64, (Float64,))
@time half_loop_cfunc!(numbers, output, half_cfunc)
@time half_loop_cfunc!(numbers, output, half_cfunc)
@time half_loop_cfunc!(numbers, output, half_cfunc)

const small_in = rand(test_size÷100)
small_out = zeros(test_size÷100)

println("jl_call inside C++ loop (array is 100 times smaller than other tests):")
@time half_loop_jlcall!(small_in, small_out)
@time half_loop_jlcall!(small_in, small_out)
@time half_loop_jlcall!(small_in, small_out)
