module CodeGen

using MacroTools
using JSON
using CxxWrap

include("stub.jl")

const JC_TYPEMAP = Dict(
    :Int8          => "int8_t",
    :UInt8         => "uint8_t",
    :Int16         => "int16_t",
    :UInt16        => "uint16_t",
    :Int32         => "int32_t",
    :UInt32        => "uint32_t",
    :Int64         => "int64_t",
    :UInt64        => "uint64_t",
    :CxxBool       => "bool",
    :CxxChar       => "char",
    :CxxWchar      => "wchar_t",
    :CxxSignedChar => "signed char",
    :CxxUChar      => "unsigned char",
    :CxxShort      => "short",
    :CxxUShort     => "unsigned short",
    :CxxInt        => "int",
    :CxxULongLong  => "unsigned long long",
    :CxxLong       => "long",
    :CxxULong      => "unsigned long",
    :CxxLongLong   => "long long",
    :CxxULongLong  => "unsigned long long",
    :Cstring       => "char*",
    :Cvoid         => "void",
    :Cdouble       => "double",
    :StdString => "std::string",
    :StdVector => "std::vector",
    :SharedPtr => "std::shared_ptr",
)

const DEREFERENCE_BLACKLIST = []

const REFERENCE_BLACKLIST = [
    :SharedPtr
    :UniquePtr
    :WeakPtr
]

const LTYPE_TO_RTYPE = Dict(:StdString => :Cstring)

function gen_call(f, ex...)
    @info typeof(f)
    callex = Expr(:call, f)
    for e in ex
        if @capture(e, a_ = b_)
            push!(callex.args, Expr(:kw, a, b))
        else
            push!(callex.args, e isa Symbol ? QuoteNode(e) : e)
        end
    end
    return callex
end

struct PType{N}
    name::Symbol
    parameters::NTuple{N,<:Union{Symbol,<:PType}}
end
PType(name::Symbol, parameters::Union{Symbol,<:PType}...) = PType(name, parameters)
const CType = Union{Symbol,<:PType}

struct Field
    name::Symbol
    type::CType
    dereference::Bool
    mutable::Bool
end
function Field(name, type; dereference=should_dereference(type), mutable=true)
    return Field(name, type, dereference, mutable)
end
macro field(ex...)
    return gen_call(Field, ex...)
end

struct Method
    name::Symbol
    cppmethod::String
end
Method(name, cppmethod) = Method(name, cppmethod)

struct Struct
    name::Symbol
    fields::Vector{Field}
    methods::Vector{Method}
end
Struct(name; fields=Field[], methods=Method[]) = Struct(name, fields, methods)
macro type(ex...)
    return gen_call(Struct, ex...)
end

struct GlobalMethod
    name::Symbol
    cppmethod::String
    override_module::Union{Nothing,Symbol}
    GlobalMethod(name, cppmethod, override_module=nothing) = new(name, cppmethod, override_module)
end

function operator(m::Symbol, A::Struct, B::Struct=A)
    @assert m in (:(==), :(>=), :(<=), :(>), :(<), :(-), :(+))
    return GlobalMethod(m, "[]($(A.name)& a, $(B.name)& b) { return a $m b; }", :Base)
end

cname(type::Symbol) = get(JC_TYPEMAP, type, type)
function cname(type::PType)
    return "$(cname(type.name))<$(join(map(cname, type.parameters), ", "))>"
end

write_jl_typestub(path::AbstractString, args...; kwargs...) =
    _write(write_jl_typestub, path, args, kwargs)
function write_jl_typestub(io::IO, type::Struct)
    function gen(i)
        field = type.fields[i]
        return (
            field,
            :(name === $(QuoteNode(field.name))),
            if field.dereference
                :(CxxWrap.CxxWrapCore.dereference_argument(
                    $(Symbol(type.name, :_get_, field.name))(x)
                ))
            else
                :($(Symbol(type.name, :_get_, field.name))(x))
            end,
            :($(Symbol(type.name, :_set_, field.name))(x, value)),
        )
    end

    get_ex = Expr(:block, :($Base.getfield(x, name)))
    set_ex = Expr(:block, :($Base.setfield!(x, name, value)))
    for i in 2:length(type.fields)
        field, case, getfield, setfield! = gen(i)
        get_ex = Expr(:elseif, case, getfield, get_ex)
        if field.mutable
            set_ex = Expr(:elseif, case, setfield!, set_ex)
        end
    end
    field, case, getfield, setfield! = gen(1)
    get_ex = Expr(:if, case, getfield, get_ex)
    if field.mutable
        set_ex = Expr(:if, case, setfield!, set_ex)
    end

    T = :(Union{$(type.name),SmartPointer{$(type.name)}})
    ex = quote
        function Base.getproperty(x::$T, name::Symbol)
            Base.@_inline_meta
            return $get_ex
        end
        function Base.setproperty!(x::$T, name::$Symbol, value)
            Base.@_inline_meta
            $set_ex
            return x
        end
        function Base.propertynames(x::$T)
            Base.@_inline_meta
            # return $(Expr(:tuple, map(QuoteNode, fields.args)...))
            return $(Tuple(f.name for f in type.fields))
        end
    end

    write(io, read(joinpath(@__DIR__, "stub.jl")))
    for line in prettify(ex).args
        write(io, string(line), '\n')
    end

    return nothing
end

write_cpp_typestub(path::AbstractString, args...; kwargs...) =
    _write(write_cpp_typestub, path, args, kwargs)
function write_cpp_typestub(io::IO, mod::Symbol, type::Struct; extra=[])
    name = type.name
    buf = IOBuffer()
    write(buf, "$(mod).add_type<$name>(\"$name\")")

    for field in type.fields
        fname = field.name
        T = cname(field.type)
        R = isreferencetype(field.type) ? "auto&" : "auto"
        write(
            buf,
            ".method(\"$(name)_get_$(fname)\", [](const $(name)& x) -> $R { return x.$(fname); })",
        )
        if field.mutable
            write(
                buf,
                ".method(\"$(name)_set_$(fname)\", []($(name)& x, const $T y) { x.$(fname) = y; })",
            )
        end
    end

    for method in type.methods
        write(buf, ".method(\"$(method.name)\", $(method.cppmethod))")
    end

    extra = extra isa AbstractString ? [extra] : extra
    for l in extra
        !startswith(l, '.') && write(buf, '.')
        write(buf, l)
    end

    format_cpp(buf)
    write(io, seekstart(buf), ';', '\n')
    return nothing
end

write_cpp_methodstub(path::AbstractString, args...; kwargs...) =
    _write(write_cpp_methodstub, path, args, kwargs)
function write_cpp_methodstub(io::IO, mod::Symbol, methods::Vector{GlobalMethod}; extra=[])
    buf = IOBuffer()
    for method in methods
        if method.override_module === mod
            write(buf, "$mod.set_override_module($mod.julia_module());")
        elseif method.override_module === :Base
            write(buf, "$mod.set_override_module(jl_base_module);")
        elseif method.override_module !== nothing
            error("Unknown override_module $(method.override_module)")
        end
        write(buf, "$mod.method(\"$(method.name)\", $(method.cppmethod));")
        if method.override_module !== nothing
            write(buf, "$mod.unset_override_module();")
        end
    end

    extra = extra isa AbstractString ? [extra] : extra
    for l in extra
        write(buf, l)
        !endswith(l, ';') && write(buf, ';')
    end

    format_cpp(buf)
    write(io, seekstart(buf), '\n')
    return nothing
end

function _write(f::Function, path::AbstractString, args, kwargs)
    @info "Generating $path"
    return open(io -> f(io, args...; kwargs...), path, "w")
end

writeln(io::IO, xs...) = write(io, xs..., '\n')

function format_cpp(stdin::IO)
    style = JSON.json(Dict("BasedOnStyle" => "google", "ColumnLimit" => 120))
    clang_cmd = `clang-format --style=$style`
    if success(`nix --help`)
        cmd = `nix shell nixpkgs'#'libclang -c $clang_cmd`
    elseif success(`clang-format --help`)
        cmd = `clang-format`
    else
        return nothing
    end

    stdout = IOBuffer()
    seekstart(stdin)
    x = run(pipeline(seekstart(stdin), `nix shell nixpkgs'#'libclang -c $clang_cmd`, stdout))
    write(seekstart(stdin), seekstart(stdout))

    return nothing
end

function isreferencetype(type::Symbol)
    return !isprimitivetype(getfield(@__MODULE__, type)) && !(type in REFERENCE_BLACKLIST)
end
isreferencetype(type::PType) = isreferencetype(type.name)

should_dereference(type::Symbol) = isreferencetype(type) && !(type in DEREFERENCE_BLACKLIST)
should_dereference(type::PType) = should_dereference(type.name)

end
