# copied from https://github.com/JuliaLang/julia/pull/30924
# modified to be in compliance with C99: http://port70.net/~nsz/c/c99/n1256.html#6.7.2.2
# Cloned from github on 19/09/2021
# Modified to support StrEnum from Python
module PyEnum

import Core.Intrinsics.bitcast
export @pyenum

function namemap end
function name_value_pairs end

# Support both Integer and Strings
allowedtypes = Union{Integer,String}

struct PyenumValues{T}
    typename::Any
    value::T
end

# basetype(::Type{<:PyenumValues{T}}) where {T<:allowedtypes} = T

# (::Type{T})(x::PyenumValues{T2}) where {T<:allowedtypes,T2<:allowedtypes} = T(bitcast(T2, x.value))::T

Base.cconvert(::Type{T}, x::PyenumValues{T2}) where {T<:allowedtypes,T2<:allowedtypes} = T(x.value)
Base.write(io::IO, x::PyenumValues{T}) where {T<:allowedtypes} = write(io, T(x.value))

# Check this, could lead to potential problems:
Base.read(io::IO, ::Type{T}) where {T<:PyenumValues} = T(read(io, T(x.value))) 

Base.isless(x::T, y::T) where {T<:PyenumValues} = isless(x.value, y.value)

Base.Symbol(x::PyenumValues{T}) where {T <: Integer} = get(namemap(x.typename), Integer(x.value), :UnknownMember)
Base.Symbol(x::PyenumValues{T}) where {T <: String} = get(namemap(x.typename), string(x.value), :UnknownMember)

Base.print(io::IO, x::PyenumValues) = print(io, Symbol(x.typename))

function Base.show(io::IO, x::PyenumValues)
    sym = x.value
    if !get(io, :compact, false)
        from = get(io, :module, Main)
        def = typeof(x.typename).name.module
        if from === nothing || !Base.isvisible(sym, def, from)
            show(io, def)
            print(io, ".")
        end
    end
    print(io, sym)
end

function Base.show(io::IO, ::MIME"text/plain", x::PyenumValues)
    print(io, x, "::")
    show(IOContext(io, :compact => true), typeof(x.value))
    print(io, " = ")
    show_pyenum_value(io, x.value)
end

function Base.show(io::IO, ::MIME"text/plain", t::Type{<:PyenumValues})
    print(io, "PyEnum ")
    print(io, typeof(x.value))
    print(io, ":")
    for (s, i) in name_value_pairs(t)
        print(io, "\n", Symbol(s), " = ")
        show_pyenum_value(io, i)
    end
end

function show_pyenum_value(io::IO, x::allowedtypes)
    if x isa Integer
        show(io, Integer(x))
    elseif x isa String
        show(io, String(x))
    else
        show(io, x)
    end
end

# give PyEnum types scalar behavior in broadcasting
Base.broadcastable(x::PyenumValues) = Ref(x)

@noinline enum_argument_error(typename, x) = throw(ArgumentError(string("input value out of range for PyEnum $(typename): $x")))

macro pyenum(T::Union{Symbol,Expr}, syms...)
    if isempty(syms)
        throw(ArgumentError("no arguments given for PyEnum $T"))
    end
    basetype = Int32
    typename = T
    if isa(T, Expr) && T.head == :(::) && length(T.args) == 2 && isa(T.args[1], Symbol)
        typename = T.args[1]
        basetype = Core.eval(__module__, T.args[2])
        if !isa(basetype, DataType)
            throw(ArgumentError("invalid base type for PyEnum $typename, $T=::$basetype"))
        end
    elseif !isa(T, Symbol)
        throw(ArgumentError("invalid type expression for PyEnum $T"))
    end
    seen = Set{Symbol}()
    name_values = Tuple{Symbol,basetype}[]
    namemap = Dict{basetype,Symbol}()
    # lo = hi = 0
    i = Nothing
    if basetype <: Integer
        i = zero(basetype)
    elseif basetype <: String
        i = typemin(basetype)
    else
        throw(ArgumentError("Invalid Datatype for PyEnum $basetype"))
    end

    if length(syms) == 1 && syms[1] isa Expr && syms[1].head == :block
        syms = syms[1].args
    end
    for s in syms
        s isa LineNumberNode && continue
        if isa(s, Symbol)
            # TODO: Check if it works with Strings
            if i == typemin(basetype) && !isempty(name_values)
                throw(ArgumentError("overflow in value \"$s\" of PyEnum $typename"))
            end
        elseif isa(s, Expr) &&
               (s.head == :(=) || s.head == :kw) &&
               length(s.args) == 2 && isa(s.args[1], Symbol)
            i = Core.eval(__module__, s.args[2]) # allow exprs, e.g. uint128, "1"
            i = convert(basetype, i)
            s = s.args[1]
        else
            throw(ArgumentError(string("invalid argument for PyEnum ", typename, ": ", s)))
        end
        if !Base.isidentifier(s)
            throw(ArgumentError("invalid name for PyEnum $typename; \"$s\" is not a valid identifier"))
        end
        haskey(namemap, i) || (namemap[i] = s;)
        if s in seen
            throw(ArgumentError("name \"$s\" in PyEnum $typename is not unique"))
        end
        push!(seen, s)
        push!(name_values, (s,i))
        # if length(name_values) == 1
        #     lo = hi = i
        # else
        #     lo = min(lo, i)
        #     hi = max(hi, i)
        # end
        # i += oneunit(i)

    end
    # print(typename)
    # print(basetype)
    # print(doc)
    # doc = basetype <: Integer ? :(primitive type $(typename) <: PyenumValues{$(basetype)} $(sizeof(basetype) * 8) end) :
    #         :(primitive type $(typename) <: Pyenum{$(basetype)} $(sizeof(Char) * 8) end)
    # println(doc)

    blk = quote
        # enum definition
        # Base.@__doc__($(doc))

        # if $(basetype) <: Integer
        #     function $(esc(typename))(x::Integer)
        #         x ≤ typemax(x) || enum_argument_error($(Expr(:quote, typename)), x)
        #         return bitcast($(esc(typename)), convert($(basetype), x))
        #     end
        # elseif $(basetype) <: String
        #     function $(esc(typename))(x::String)
        #         # Does not work, bitcast cant do anything for String type
        #         # return bitcast($(esc(typename)), convert($(basetype), x)) 
        # end
        if $(basetype) <: $(esc(allowedtypes))
            function $(esc(typename))(x::String)
                return PyenumValues($(esc(typename)), convert($(basetype), x))
            end
        else
            throw(ArgumentError("Invalid Datatype for PyEnum $basetype"))
        end
        PyEnum.namemap(::Type{$(esc(typename))}) = $(esc(namemap))
        PyEnum.name_value_pairs(::Type{$(esc(typename))}) = $(esc(name_values))
        # Base.typemin(x::Type{$(esc(typename))}) = $(esc(typename))($lo)
        # Base.typemax(x::Type{$(esc(typename))}) = $(esc(typename))($hi)
        let insts = (Any[$(esc(typename))(v[2]) for v in $name_values ]...,)
            Base.instances(::Type{$(esc(typename))}) = insts
        end
    end
    if isa(typename, Symbol)
        for (sym, i) in name_values
            push!(blk.args, :(const $(esc(sym)) = $(esc(typename))($i)))
        end
    end
    push!(blk.args, :nothing)
    blk.head = :toplevel
    return blk
end

include("operators_test.jl")

end # module
