"""
NEOSServer(;email)

Construct a `NEOSServer` object. The `email` argument should take the users valid
email address (required for solvers like CPLEX).
"""
mutable struct NEOSServer
    useragent::String
    host::String
    contenttype::String
    email::String
    NEOSServer(;email="") = new("NEOS.jl", "https://neos-server.org:3333", "text/xml", email)
end

function addemail!(s::NEOSServer, email::String)
    s.email = email
end

struct NEOSJob
    number::Int
    password::String
end

_add_text(value, arg::String) = add_text(value, arg)
_add_text(value, arg) = add_text(new_child(value, "int"), string(arg))

function buildxml(s::NEOSServer, name::String, args...)
    xml = XMLDocument()
    mthd = create_root(xml, "methodCall")
    mname = new_child(mthd, "methodName")
    add_text(mname, name)
    if length(args) > 0
	params = new_child(mthd, "params")
	for a in args
	    param = new_child(params, "param")
	    value = new_child(param, "value")
	    _add_text(value, a)
	end
    end
    return string(xml)
end

function apimethod(s::NEOSServer, name::String, args...)
    xml = buildxml(s, name, args...)
    res = send(s, xml)
    parseresponse(res)
end

function send(s::NEOSServer, xml::String)
    hdrs = Dict{String, String}("user-agent" => s.useragent, "host" => s.host,
	                        "content-type" => s.contenttype, "content-length" => string(length(xml)))
    res = HTTP.request("POST", s.host; headers=hdrs, body=xml)
end

function getvalues!(values, node)
    types = ["int", "i4", "string", "double", "base64", "dateTime.iso8601"]
    for child in child_nodes(node)
	if name(child) in types
	    push!(values, content(child))
	else
	    getvalues!(values, child)
	end
    end
end

function parseresponse(res)
    if res.status != 200
	error("XML-RPC failed with code: $(res.status)")
    end
    parameters = Any[]
    xml = parse_string(String(res.body))

    xroot = root(xml)
    getvalues!(parameters, xroot)
    return parameters
end

# ===========================================
#
# 	NEOS API Methods
#
function neosHelp(s::NEOSServer)
    return apimethod(s, "help")[1]
end

messages = [:emailHelp, :welcome, :version, :ping, :printQueue]
for m in messages
    m_str = string(m)
    @eval function ($m)(s::NEOSServer)
	return apimethod(s, $(m_str))[1]
    end
end

lists = [:listAllSolvers, :listCategories]
for m in lists
    m_str = string(m)
    @eval 	function ($m)(s::NEOSServer)
	return apimethod(s, $(m_str))
    end
end

function getSolverTemplate(s::NEOSServer, category::String, solvername::String, inputMethod::String)
    apimethod(s, "getSolverTemplate", category, solvername, inputMethod)[1]
end

function listSolversInCategory(s::NEOSServer, category::String)
    apimethod(s, "listSolversInCategory", category)
end

function submitJob(s::NEOSServer, xmlstring::String, print_level::Int = 1)
    res = apimethod(s, "submitJob", xmlstring)
    if print_level >= 1
	println("==================")
	println("NEOS Job submitted")
	println("number:\t$(res[1])")
	println("pwd:\t$(res[2])")
	println("==================")
    end
    return NEOSJob(parse(Int, res[1]), res[2])
end

jobapimethods = [:getJobStatus, :killJob]
for m in jobapimethods
    m_str = string(m)
    @eval 	function ($m)(s::NEOSServer, j::NEOSJob)
	return apimethod(s, $(m_str), j.number, j.password)[1]
    end
end

function getJobInfo(s::NEOSServer, j::NEOSJob)
    apimethod(s, "getJobInfo", j.number, j.password)
end


for s in ["", "NonBlocking"]
    _final_str = "getFinalResults$s"
    _final = Symbol(_final_str)
    _intermediate_str = "getIntermediateResults$s"
    _intermediate = Symbol(_intermediate_str)
    @eval function ($_final)(s::NEOSServer, j::NEOSJob)
	decode_to_string(apimethod(s, $(_final_str), j.number, j.password)[1])
    end

    @eval function ($(_intermediate))(s::NEOSServer, j::NEOSJob; offset=0)
	(results, offset) = apimethod(s, $(_intermediate_str), j.number, j.password, offset)
	decode_to_string(results), parse(Int, offset)
    end
end


function decode_to_string(s)
    String(base64decode(replace(s, "\n" => "")))
end


struct NEOSData
    input::String
    category::String
    model::String
    data::String
    commands::String
    comments::String
end

function createNEOSJob(input::String, category::String, model::String, data::String, commands::String, comments::String)
    return NEOSData(input, category, model, data, commands, comments)
end


function getNEOSJobXML(solver::String, m :: NEOSData)
    category = m.category
    input = m.input
    model = m.model
    data = m.data
    commands = m.commands
    comments = m.comments

    return "<document>\n<category>$category</category>\n<solver>$solver</solver>\n<inputMethod>$input</inputMethod>\n\n<model><![CDATA[\n$model\n]]></model>\n\n<data><![CDATA[\n$data\n]]></data>\n\n<commands><![CDATA[\n$commands\n]]></commands>\n\n<comments><![CDATA[\n$comments\n]]></comments>\n\n</document>\n"
end
