# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************

function prodfactor(pv::PV, latitude::Real, longitude::Real; timeframe="hourly")

    if !(ismissing(pv.prod_factor_series_kw))
        return pv.prod_factor_series_kw
    end

    url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key,
        "&lat=", latitude , "&lon=", longitude, "&tilt=", pv.tilt,
        "&system_capacity=1", "&azimuth=", pv.azimuth, "&module_type=", pv.module_type,
        "&array_type=", pv.array_type, "&losses=", round(pv.losses*100, digits=3), "&dc_ac_ratio=", pv.dc_ac_ratio,
        "&gcr=", pv.gcr, "&inv_eff=", pv.inv_eff*100, "&timeframe=", timeframe, "&dataset=nsrdb",
        "&radius=", pv.radius
    )

    try
        @info "Querying PVWatts for prodfactor with " pv.name
        r = HTTP.get(url)
        response = JSON.parse(String(r.body))
        if r.status != 200
            error("Bad response from PVWatts: $(response["errors"])")
        end
        @info "PVWatts success."
        watts = collect(get(response["outputs"], "ac", []) / 1000)  # scale to 1 kW system (* 1 kW / 1000 W)
        if length(watts) != 8760
            @error "PVWatts did not return a valid production factor. Got $watts"
        end
        return watts
    catch e
        @error "Error occurred when calling PVWatts: $e"
    end
end

function prodfactor(g::AbstractGenerator; ts_per_hour::Int=1)
    return ones(8760 * ts_per_hour)
end

"""
    prodfactor(wind::Wind, latitude::Real, longitude::Real)

If the user does not provide their own prod_factor_series_kw for the Wind turbine, then this method creates
a production factor time-series using resource data and the System Advisor Model Wind module.
If the user does not provide the resource data, the latitude and longitude are used to get the resource data from the
Wind Toolkit.
"""
function prodfactor(wind::Wind, latitude::Real, longitude::Real, time_steps_per_hour::Int)

    if !(ismissing(wind.prod_factor_series_kw))
        return wind.prod_factor_series_kw
    end

    resources = []
    heights_for_sam = [wind.hub_height]

    if all(length(a) > 0 for a in [wind.temperature_celsius, wind.pressure_atmospheres, wind.wind_direction_degrees,
                                   wind.wind_meters_per_sec])
        push!(resources, [wind.temperature_celsius, wind.pressure_atmospheres, wind.wind_direction_degrees,
                          wind.wind_meters_per_sec])
    else  # download resource data from WindToolKit

        # Allowed hub heights in meters for the Wind Toolkit
        windtoolkit_hub_heights = [10, 40, 60, 80, 100, 120, 140, 160, 200]
        """
        SAM can interpolate the wind power if the wind.hub_height is not one of the windtoolkit_hub_heights.
            If we do need to interpolate then we need to provide SAM with the resources for two hub heights.
        """
        if !(wind.hub_height in windtoolkit_hub_heights)
            if wind.hub_height < minimum(windtoolkit_hub_heights)
                heights_for_sam = [windtoolkit_hub_heights[1]]
            elseif wind.hub_height > maximum(windtoolkit_hub_heights)
                heights_for_sam = [windtoolkit_hub_heights[end]]
            else
                upper_index = findfirst(x -> x > wind.hub_height, windtoolkit_hub_heights)
                heights_for_sam = [windtoolkit_hub_heights[upper_index-1], windtoolkit_hub_heights[upper_index]]
            end
        end
        # TODO validate against API with different hub heights (not in windtoolkit_hub_heights)

        for height in heights_for_sam
            url = string("https://developer.nrel.gov/api/wind-toolkit/v2/wind/wtk-srw-download",
                "?api_key=", nrel_developer_key,
                "&lat=", latitude , "&lon=", longitude,
                "&hubheight=", Int(height), "&year=", 2012
            )
            resource = []
            try
                @info "Querying Wind Toolkit for resource data ..."
                r = HTTP.get(url; retries=5)
                if r.status != 200
                    error("Bad response from Wind Toolkit: $(response["errors"])")
                end
                @info "Wind Toolkit success."

                resource = readdlm(IOBuffer(String(r.body)), ',', Float64, '\n'; skipstart=5);
                # columns: Temperature, Pressure, Speed, Direction (C, atm, m/s, Degrees)
                if size(resource) != (8760, 4)
                    @error "Wind Toolkit did not return valid resource data. Got an array with size $(size(resource))"
                end
            catch e
                @error "Error occurred when calling Wind Toolkit: $e"
            end
            push!(resources, resource)
        end
        resources = hcat(resources...)
    end  # done filling in resources (can contain one Vector of Vectors or multiple Vectors of Vectors)

    # Initialize SAM inputs
    global hdl = nothing
    sam_prodfactor = []

    # Corresponding size in kW for generic reference turbines sizes
    system_capacity_lookup = Dict(
        "large"=> 2000,
        "medium" => 250,
        "commercial"=> 100,
        "residential"=> 2.5
    )
    system_capacity = system_capacity_lookup[wind.size_class]

    # Corresponding rotor diameter in meters for generic reference turbines sizes
    rotor_diameter_lookup = Dict(
        "large" => 55*2,
        "medium" => 21.9*2,
        "commercial" => 13.8*2,
        "residential" => 1.85*2
    )
    wind_turbine_powercurve_lookup = Dict(
        "large" => [0, 0, 0, 70.119, 166.208, 324.625, 560.952, 890.771, 1329.664,
                    1893.213, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000,
                    2000, 2000, 2000, 2000, 2000, 2000],
        "medium"=> [0, 0, 0, 8.764875, 20.776, 40.578125, 70.119, 111.346375, 166.208,
                    236.651625, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250,
                    250, 250, 250, 250, 250],
        "commercial"=> [0, 0, 0, 3.50595, 8.3104, 16.23125, 28.0476, 44.53855, 66.4832,
                        94.66065, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
                        100, 100, 100, 100, 100],
        "residential"=> [0, 0, 0, 0.070542773, 0.1672125, 0.326586914, 0.564342188,
                        0.896154492, 1.3377, 1.904654883, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5,
                        2.5, 2.5, 2.5, 0, 0, 0, 0, 0, 0, 0]
    )

    try
        if Sys.isapple()
            libfile = "libssc.dylib"
        elseif Sys.islinux()
            libfile = "libssc.so"
        elseif Sys.iswindows()
            libfile = "ssc.dll"
        else
            @error """Unsupported platform for using the SAM Wind module.
                      You can alternatively provide the Wind.prod_factor_series_kw"""
        end

        global hdl = joinpath(dirname(@__FILE__), "..", "sam", libfile)
        wind_module = @ccall hdl.ssc_module_create("windpower"::Cstring)::Ptr{Cvoid}
        wind_resource = @ccall hdl.ssc_data_create()::Ptr{Cvoid}  # data pointer
        @ccall hdl.ssc_module_exec_set_print(0::Cint)::Cvoid

        @ccall hdl.ssc_data_set_number(wind_resource::Ptr{Cvoid}, "latitude"::Cstring, latitude::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(wind_resource::Ptr{Cvoid}, "longitude"::Cstring, longitude::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(wind_resource::Ptr{Cvoid}, "elevation"::Cstring, 0::Cdouble)::Cvoid  # not used in SAM
        @ccall hdl.ssc_data_set_number(wind_resource::Ptr{Cvoid}, "year"::Cstring, 2012::Cdouble)::Cvoid

        heights_array = []  # have to repeat heights for each resource column
        for h in heights_for_sam
            append!(heights_array, repeat([h], 4))
        end
        heights_array = convert(Array{Float64}, heights_array)
        @ccall hdl.ssc_data_set_array(wind_resource::Ptr{Cvoid}, "heights"::Cstring,
           heights_array::Ptr{Cdouble}, length(heights_array)::Cint)::Cvoid

        # setup column data types: temperature=1, pressure=2, degree=3, speed=4
        fields = collect(repeat(range(1, stop=4), length(heights_for_sam)))
        fields = convert(Array{Float64}, fields)
        @ccall hdl.ssc_data_set_array(wind_resource::Ptr{Cvoid}, "fields"::Cstring,
            fields::Ptr{Cdouble}, length(fields)::Cint)::Cvoid

        nrows, ncols = size(resources)
        t = [row for row in eachrow(resources)];
        t2 = reduce(vcat, t);
        # the values in python api are sent to SAM as vector (35040) with rows concatenated
        c_resources = [convert(Float64, t2[i]) for i in eachindex(t2)]
        @ccall hdl.ssc_data_set_matrix(wind_resource::Ptr{Cvoid}, "data"::Cstring, c_resources::Ptr{Cdouble},
            Cint(nrows)::Cint, Cint(ncols)::Cint)::Cvoid

        data = @ccall hdl.ssc_data_create()::Ptr{Cvoid}  # data pointer
        @ccall hdl.ssc_data_set_table(data::Ptr{Cvoid}, "wind_resource_data"::Cstring, wind_resource::Ptr{Cvoid})::Cvoid
        @ccall hdl.ssc_data_free(wind_resource::Ptr{Cvoid})::Cvoid

        # # can get the same values back with:
        # a = @ccall hdl.ssc_data_get_matrix(wind_resource::Ptr{Cvoid}, "data"::Cstring, Ref(Cint(4))::Ptr{Cint}, Ref(Cint(4))::Ptr{Cint})::Ptr{Cdouble}
        # unsafe_load(a, 1)
        # unsafe_load(a, 35040)

        # Scaler inputs
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_resource_shear"::Cstring, 0.14000000059604645::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_resource_turbulence_coeff"::Cstring,
            0.10000000149011612::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "system_capacity"::Cstring,
            system_capacity::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_resource_model_choice"::Cstring, 0::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "weibull_reference_height"::Cstring, 50::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "weibull_k_factor"::Cstring, 2::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "weibull_wind_speed"::Cstring, 7.25::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_turbine_rotor_diameter"::Cstring,
            rotor_diameter_lookup[wind.size_class]::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_turbine_hub_ht"::Cstring, wind.hub_height::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_turbine_max_cp"::Cstring, 0.44999998807907104::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_farm_losses_percent"::Cstring, 0::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "wind_farm_wake_model"::Cstring, 0::Cdouble)::Cvoid
        @ccall hdl.ssc_data_set_number(data::Ptr{Cvoid}, "adjust:constant"::Cstring, 0::Cdouble)::Cvoid

        # Array inputs
        speeds = convert(Array{Float64},
            [0., 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25])
        @ccall hdl.ssc_data_set_array(data::Ptr{Cvoid}, "wind_turbine_powercurve_windspeeds"::Cstring,
            speeds::Ptr{Cdouble}, length(speeds)::Cint)::Cvoid

        powercurve = convert(Array{Float64}, wind_turbine_powercurve_lookup[wind.size_class])
        @ccall hdl.ssc_data_set_array(data::Ptr{Cvoid}, "wind_turbine_powercurve_powerout"::Cstring,
            powercurve::Ptr{Cdouble}, length(powercurve)::Cint)::Cvoid

        wind_farm_xCoordinates = [Float64(0)]
        @ccall hdl.ssc_data_set_array(data::Ptr{Cvoid}, "wind_farm_xCoordinates"::Cstring,
            wind_farm_xCoordinates::Ptr{Cdouble}, 1::Cint)::Cvoid

        wind_farm_yCoordinates = [Float64(0)]
        @ccall hdl.ssc_data_set_array(data::Ptr{Cvoid}, "wind_farm_yCoordinates"::Cstring,
            wind_farm_yCoordinates::Ptr{Cdouble}, 1::Cint)::Cvoid

        # example of getting a number:
        # val = convert(Cdouble, 0.0)
        # ref = Ref(val)
        # @ccall hdl.ssc_data_get_number(data::Ptr{Cvoid}, "wind_resource_shear"::Cstring, ref::Ptr{Cdouble})::Cvoid
        # Float64(ref[])

        if !Bool(@ccall hdl.ssc_module_exec(wind_module::Ptr{Cvoid}, data::Ptr{Cvoid})::Cint)
            log_type = 0
            log_type_ref = Ref(log_type)
            log_time = 0
            log_time_ref = Ref(log_time)
            msg_ptr = @ccall hdl.ssc_module_log(wind_module::Ptr{Cvoid}, 0::Cint, log_type_ref::Ptr{Cvoid},
                                            log_time_ref::Ptr{Cvoid})::Cstring
            msg = "no message from ssc_module_log."
            try
                msg = unsafe_string(msg_ptr)
            finally
                @error("SAM Wind simulation error: $msg")
            end
        end

        len = 0
        len_ref = Ref(len)
        a = @ccall hdl.ssc_data_get_array(data::Ptr{Cvoid}, "gen"::Cstring, len_ref::Ptr{Cvoid})::Ptr{Float64}
        for i in range(1, stop=8760)
            push!(sam_prodfactor, unsafe_load(a, i))
        end
        @ccall hdl.ssc_module_free(wind_module::Ptr{Cvoid})::Cvoid
        @ccall hdl.ssc_data_free(data::Ptr{Cvoid})::Cvoid

    catch e
        @error "Problem calling SAM C library!"
        showerror(stdout, e)
    end

    if !(length(sam_prodfactor) == 8760)
        @error "Wind production factor from SAM has length $(length(sam_prodfactor)) (should be 8760)."
    end

    @assert !(nothing in sam_prodfactor) "Did not get complete Wind production factor from SAM."

    # normalize by system_capacity
    normalized_prod_factor = sam_prodfactor ./ system_capacity
    # upsample if time steps per hour > 1
    if time_steps_per_hour > 1
        normalized_prod_factor = repeat(normalized_prod_factor, inner=time_steps_per_hour)
    end
    return normalized_prod_factor
end


"""
    isearlytermination(thread::Thread)

Check if we have met a termination condition for the thread.
"""
function isearlytermination(thread::Thread)
    # Ensure, minimum depth 1 searched.
    # Ensure, minumum 1023+ nodes searched.
    # And that time management is up.
    (thread.ss.depth > 1) && ((thread.ss.nodes & 1023) == 1023) && (elapsedtime(thread.timeman) > thread.timeman.max_time) &&
    (!thread.timeman.isinfinite) && (thread.timeman.depth == 0)
end

"""
    find_best_move()

Probe the tablebase if appropriate, or perform the absearch routine.
"""
function find_best_move(thread::Thread, ttable::TT_Table, depth::Int)::Int
    board = thread.board

    # Probe the syzygy tablebase.
    # At present, it is assumed 5 men tablebases are available.
    # Also assumes we want to always probe.
    if (count(occupied(board)) <= 5)
        res = tb_probe_root(board)
        if res !== TB_RESULT_FAILED
            # Extract the move to the PV and return an evaluation.
            return interpret_syzygy(thread, res)
        end
    end

    ABORT_SIGNAL[] = false
    initTimeManagement!(thread.timeman)

    # If no tablebase result found, we proceed with iterative deepening and absearch.
    eval = iterative_deepening(thread, ttable, depth)
    ABORT_SIGNAL[] = true
    return eval
end

"""
    iterative_deepening(thread::Thread, ttable::TT_Table, max_depth::Int)

Performs the iterative deepening approach to absearch, and forms the main core of the engine routine.
"""
function iterative_deepening(thread::Thread, ttable::TT_Table, max_depth::Int)::Int
    eval = -MATE
    # Below is an idea to reduce history heuristic "noise" between searches.
    # Testing shows some benefit.
    for i in 1:2
        for j in 1:64
            @inbounds @simd for k in 1:64
                thread.history[i][j][k] = fld(thread.history[i][j][k], 2)
            end
        end
    end
    for i in 1:6
        for j in 1:64
            for k in 1:6
                @inbounds @simd for l in 1:64
                    thread.counterhistory[i][j][k][l] = fld(thread.counterhistory[i][j][k][l], 2)
                    thread.followhistory[i][j][k][l] = fld(thread.followhistory[i][j][k][l], 2)
                end
            end
        end
    end
    # We iterate through depths until we either:
    # a) reach the maximum depth, or
    # b) hit a time management constraint.
    best_move = MOVE_NONE
    for depth in 1:max_depth
        eval = aspiration_window(thread, ttable, depth, eval)
        if thread.stop
            thread.pv[1].list[1] = best_move
            break
        else
            best_move = thread.pv[1][1]
        end
        if (!thread.timeman.isinfinite && elapsedtime(thread.timeman) > thread.timeman.max_time) ||
            (thread.timeman.depth !== 0 && depth >= thread.timeman.depth) || istermination(thread.timeman)
            break
        end
    end
    return eval
end

"""
    aspiration_window(thread::Thread, ttable::TT_Table, depth::Int, eval::Int)

When operating within the iterative deepening framework, aspiration windows are used via calls to this function.
"""
function aspiration_window(thread::Thread, ttable::TT_Table, depth::Int, eval::Int)::Int
    δ = 22 + fld(abs(eval), 256)
    if depth >= WINDOW_DEPTH
        α = max(-MATE, eval - δ)
        β = min(MATE, eval + δ)
    end
    return aspiration_window_internal(thread, ttable, depth, eval, -MATE, MATE, δ)
end

# The internals of aspiration_window
function aspiration_window_internal(thread::Thread, ttable::TT_Table, depth::Int, eval::Int, α::Int, β::Int, δ::Int)::Int
    eval = eval
    thread.ss.seldepth = 0
    thread.ss.depth = depth

    while !thread.stop
        eval = absearch(thread, ttable, α, β, depth, 0, false)

        # window cond met
        if ((α < eval < β) || (elapsedtime(thread.timeman) > 2.5)) && !thread.stop
            uci_report(thread, ttable, α, β, eval)
            return eval
        end

        # fail low
        if eval <= α
            β = fld(α + β, 2)
            α = max(-MATE, α - δ)

        # fail high
        elseif eval >= β
            β = min(MATE, β + δ)
        end

        # expand window
        δ += fld(δ, 2)
    end
    return eval
end

# https://www.chessprogramming.org/Quiescence_Search
"""
    qsearch()

Quiescence search function.
"""
function qsearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, ply::Int)::Int
    board = thread.board
    pv = thread.pv

    # ensure pv is clear
    clear!(thread.pv[ply + 1])

    # default val
    tt_eval = -MATE
    tt_move = MOVE_NONE

    thread.ss.seldepth = max(thread.ss.seldepth, ply)
    thread.ss.nodes += 1

    # Check for abort signal, or TimeManagement criteria.
    if isearlytermination(thread) || (ABORT_SIGNAL[] == true)
        thread.stop = true
        return 0
    end

    # Check if the current position is drawn. (Ignoring stalemate)
    if isdrawbymaterial(board) || is50moverule(board)
        return draw_noise(thread)
    end

    # If we have reached the maximum search ply depth, we stop here.
    if ply >= MAX_PLY
        return evaluate(board, thread.pktable)
    end

    # probe the transposition table
    tt_entry = getTTentry(ttable, board.hash)
    if tt_entry !== NO_ENTRY
        tt_eval = Int(tt_entry.eval)
        tt_move = tt_entry.move
        tt_value = Int(ttvalue(tt_entry, ply))
        if (tt_entry.bound == BOUND_EXACT) ||
            ((tt_entry.bound == BOUND_LOWER) && (tt_value >= β)) ||
            ((tt_entry.bound == BOUND_UPPER) && (tt_value <= α))
            return tt_value
        end
    end

    # We may treat checks in qsearch specially.
    if ischeck(board)
        best = -MATE
        margin = 0
    else
        if tt_eval !== -MATE
            eval = thread.evalstack[ply + 1] = tt_eval
        elseif (ply > 0) && (thread.movestack[ply] == NULL_MOVE)
            eval = thread.evalstack[ply + 1] = -thread.evalstack[ply] + 2*TEMPO_BONUS
        else
            eval = thread.evalstack[ply + 1] = evaluate(board, thread.pktable)
        end

        best = eval

        # Evaluation pruning step.
        if eval > α
            α = eval
        end
        if α >= β
            return eval
        end

        # Delta pruning step. / Futility pruning.
        # If a "best case" (but perhaps non-existent) move, plus a small margin, is not enough to raise alpha, we can stop here.
        # Rather not perform this when in check.
        margin = α - eval - Q_FUTILITY_MARGIN
        if !ischeck(board) && (optimistic_move_estimator(board) < margin)
            return eval
        end
    end

    moveorder = thread.moveorders[ply + 1]

    if ischeck(board)
        # We need evasions, so generate quiet moves.
        init_normal_moveorder!(thread, tt_move, ply)
        @inbounds thread.killer1s[ply + 2] = MOVE_NONE
        @inbounds thread.killer2s[ply + 2] = MOVE_NONE
    else
        # We just look at noisy moves.
        # We make sure that the moves satisfy a SEE margin.
        init_noisy_moveorder!(thread, ply, max(1, margin))
    end

    best = qsearch_internal(thread, ttable, α, β, ply, tt_move, best)
    clear!(moveorder)
    return best
end

# The internals of qsearch.
function qsearch_internal(thread::Thread, ttable::TT_Table, α::Int, β::Int, ply::Int, tt_move::Move, best::Int)::Int
    # We can select skipquiets = false during qsearch, as if it's a NOISY type moveorder, quiets are skipped anyway.
    # Otherwise, quiets are needed to generate king check evasions.
    played = 0
    skipquiets = false
    @inbounds pv_current = thread.pv[ply + 1]
    @inbounds pv_future = thread.pv[ply + 2]
    while ((move = selectmove!(thread, ply, skipquiets)) !== MOVE_NONE) && !thread.stop

        if ischeck(thread.board) && (played >= 4) && (best > -MATE + MAX_PLY) && !istactical(thread.board, move)
            skipquiets = true
            continue
        end

        u = apply_move!(thread, move)
        played += 1
        eval = -qsearch(thread, ttable, -β, -α, ply + 1)
        undo_move!(thread, move, u)

        # Check for improvements, and update PV.
        # Ensure we met no stop signals.
        if (eval > best) && (!thread.stop)
            best = eval
            if eval > α
                α = best
                clear!(pv_current)
                push!(pv_current, move)
                updatepv!(pv_current, pv_future)
            end
        end

        # fail high?
        if (α >= β) && !thread.stop
            return best
        end
    end
    return best
end

"""
    absearch()

The main alpha-beta search function, containing all the pruning heuristics.
"""
function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int, ply::Int, cutnode::Bool)::Int
    board = thread.board
    @inbounds pv_current = thread.pv[ply + 1]
    @inbounds pv_future = thread.pv[ply + 2]

    # Set initial history stats to zero.
    hist = cmhist = fmhist = 0

    # Initial α value for later computation.
    init_α = α
    clear!(pv_current)

    # Is this the root node?
    isroot = iszero(ply)

    # Is this a pvnode?
    pvnode = β !== α + 1

    # Best evaluation defaults to -MATE.
    best = -MATE

    # default tt_eval, tt_move
    tt_eval = -MATE
    tt_move = MOVE_NONE

    # ensure +ve depth
    if depth < 0
        depth = 0
    end

    # If depth reaches 0, we enter quiescence search.
    # If we are in check, we prefer to hold off one more ply.
    if iszero(depth) && !ischeck(board)
        q_eval = qsearch(thread, ttable, α, β, ply)
        return q_eval
    end

    # update thread details
    thread.ss.seldepth = max(thread.ss.seldepth, ply)
    thread.ss.nodes += 1

    # Check for abort signal, or TimeManagement criteria.
    if isearlytermination(thread) || (ABORT_SIGNAL[] == true)
        thread.stop = true
        return evaluate(board, thread.pktable)
    end

    # If this is not the root node, we can check for early exit conditions.
    if isroot == false

        # If we detect a draw, we can return an evaluation of zero.
        if isdrawbymaterial(board) || is50moverule(board) || isrepetition(board)
            return draw_noise(thread)
        end

        # If we reach the max ply, we evaluate the board here and return.
        if ply >= MAX_PLY
            eval = evaluate(board, thread.pktable)
            return eval
        end

        # Check for mating evaluations, and prune for the best mate.
        # This avoids the need to look for sub-optimal continuations.
        if α > -MATE + ply
            mate_α = α
        else
            mate_α = -MATE + ply
        end
        if β < MATE - ply - 1
            mate_β = β
        else
            mate_β = MATE - ply - 1
        end
        if mate_α >= mate_β
            return mate_α
        end
    end

    # Probe the transposition table.
    tt_entry = getTTentry(ttable, board.hash)
    if tt_entry !== NO_ENTRY
        tt_eval = Int(tt_entry.eval)
        tt_value = Int(ttvalue(tt_entry, ply))
        tt_move = tt_entry.move
        if (tt_entry.depth >= depth) && (depth == 0 || (pvnode == false))
            if (tt_entry.bound == BOUND_EXACT) ||
                ((tt_entry.bound == BOUND_LOWER) && (tt_value >= β)) ||
                ((tt_entry.bound == BOUND_UPPER) && (tt_value <= α))
                return tt_value
            end
        end
    end

    # If we have less than 5 pieces, we are in the realms of the Syzygy tablebase.
    # Attempt to probe.
    if (count(occupied(board)) <= 5) && !isroot
        _eval = tb_probe_wdl(board)
        if _eval !== TB_RESULT_FAILED
            thread.ss.tbhits += 1

            # Is the tablebase score losing?
            if iszero(_eval)
                eval = -MATE + MAX_PLY + ply + 1
                tt_bound = BOUND_UPPER

            # Is the tablebase a draw? Blessed / cursed loss and wins are draws
            elseif 1 <= _eval <= 3
                eval = 0
                tt_bound = BOUND_EXACT

            # Else, the tablebase is a win.
            else
                eval = MATE - MAX_PLY - ply - 1
                tt_bound = BOUND_LOWER
            end

            # add to transposition table
            if (tt_bound == BOUND_EXACT) ||
                ((tt_bound == BOUND_LOWER) && (eval >= β)) ||
                ((tt_bound == BOUND_UPPER) && (eval <= α))
                setTTentry!(ttable, board.hash, eval, MOVE_NONE, MAX_PLY - 1, tt_bound)
                return eval
            end
        end
    end

    # Set a static evaluation.
    # If the last move was a NULL move, the evaluation is simply a change in 2*TEMPO_BONUS (flip a sign).
    if tt_eval !== -MATE
        eval = thread.evalstack[ply + 1] = tt_eval
        if eval == 0
            eval = 2 * (thread.ss.nodes & 1) - 1
        end
    elseif (ply > 0) && (thread.movestack[ply] == NULL_MOVE)
        eval = thread.evalstack[ply + 1] = -thread.evalstack[ply] + 2*TEMPO_BONUS
    else
        eval = thread.evalstack[ply + 1] = evaluate(board, thread.pktable)
    end

    # Reset killer moves for the upcoming ply.
    @inbounds thread.killer1s[ply + 2] = MOVE_NONE
    @inbounds thread.killer2s[ply + 2] = MOVE_NONE

    # Razoring.
    # If the evaluation plus a small margin is still below alpha, we drop into the quiescence search.
    if (pvnode === false) && (ischeck(board) === false) && (depth <= RAZOR_DEPTH) && (eval + RAZOR_MARGIN <= α)
        q_eval = qsearch(thread, ttable, α, β, ply)
        return q_eval
    end

    # Check to see if the evaluation history has improved over the last few plies.
    # We can use this to modify some pruning heuristics later on.
    improving = ((ply >= 2) && (thread.evalstack[ply + 1] > thread.evalstack[ply - 1])) ? 2 : 1

    # Beta pruning.
    # If the evaluation minus a margin is still better than beta, we can prune here.
    if (pvnode === false) && (ischeck(board) === false) && (depth <= BETA_PRUNE_DEPTH) && (eval - BETA_PRUNE_MARGIN * (depth - improving + 1) >= β)
        return eval
    end

    # Null move pruning.
    # Check we are greater than depth 2, eval > β.
    # Not done in a pvnode.
    # Not done when in check.
    # Check we have non-pawn material left on the board.
    # Do not do more than 2 b2b null moves.
    if (depth >= 2) && (eval >= β) && !pvnode && !ischeck(board) &&
        (ply > 0 ? (thread.movestack[ply] !== NULL_MOVE) : true)  &&
        (ply > 1 ? (thread.movestack[ply - 1] !== NULL_MOVE) : true) &&
        ((tt_entry === NO_ENTRY) || (tt_eval >= β)) &&
        has_non_pawn_material(board)

        reduction = fld(depth, 5) + 3 + min(fld(eval - β, 150), 3)

        u = apply_null!(thread)

        cand_eval = -absearch(thread, ttable, -β, -β + 1, depth - reduction, ply + 1, !cutnode)
        undo_null!(thread, u)
        if (cand_eval >= β)
            return β
        end
    end

    best_move = MOVE_NONE

    # Set pruning variables used during the main loop.
    futility_margin = FUTILITY_MARGIN * depth
    see_quiet_margin = SEE_QUIET_MARGIN * depth
    see_noisy_margin = SEE_NOISY_MARGIN * depth^2
    skipquiets = false
    played = 0
    num_quiets = 0

    # Probcut pruning.
    # We are not in a PV-node.
    # A potential exists on the board such that it can beat β plus a margin.
    if !pvnode &&  (depth >= PROBCUT_DEPTH) && (abs(β) < (MATE - MAX_PLY)) &&
        (eval >= β || eval + optimistic_move_estimator(board) >= β + PROBCUT_MARGIN)
        probcut_count = 0
        raised_β = min(β + PROBCUT_MARGIN, MATE - MAX_PLY - 1)
        init_noisy_moveorder!(thread, ply, raised_β - eval)
        while ((move = selectmove!(thread, ply, true)) !== MOVE_NONE) && !thread.stop && (probcut_count < 2 + (cutnode ? 2 : 0))
            probcut_count += 1
            u = apply_move!(thread, move)
            # We check that the move holds in a qsearch.
            eval = -qsearch(thread, ttable, -β, -β + 1, ply + 1)
            # If it holds, and is above raised β, we do a fuller search.
            if eval >= raised_β
                eval = -absearch(thread, ttable, -β, -β + 1, depth - 4, ply + 1, !cutnode)
            end
            undo_move!(thread, move, u)

            if (eval >= raised_β)
                clear!(thread.moveorders[ply + 1])
                return eval
            end
        end
        clear!(thread.moveorders[ply + 1])
    end

    # Internal iterative deepening step.
    # Aim is to perform a quick search to fill the transposition table with potential moves.
    if (depth > 6) && (tt_move === MOVE_NONE)
        absearch(thread, ttable, α, β, depth - 7, ply + 1, cutnode)
        tt_entry = getTTentry(ttable, board.hash)
        if tt_entry !== NO_ENTRY
            tt_move = tt_entry.move
        end
    end

    # Init the move ordering
    init_normal_moveorder!(thread, tt_move, ply)
    @inbounds quiets_tried = thread.quietstack[ply + 1]
    @inbounds moveorder = thread.moveorders[ply + 1]
    while ((move = selectmove!(thread, ply, skipquiets)) !== MOVE_NONE) && !thread.stop

        isquiet = !istactical(board, move)

        # If we pick a quiet move, extract the history statistics.
        if isquiet
            hist, cmhist, fmhist = gethistory(thread, move, ply)
            num_quiets += 1
        end

        # Quiet move pruning steps.
        # We require that a non-mating line exists before pruning.
        # Opt not to prune if there is only king + pawn material remaining.
        if isquiet && (best > -MATE + MAX_PLY) && has_non_pawn_material(board)
            # Quiet move futility pruning step, using history.
            if (depth <= FUTILITY_PRUNE_DEPTH) && (eval + futility_margin <= α) &&
                (hist + cmhist + fmhist < FUTILITY_LIMIT[improving])
                skipquiets = true
            end

            # Quiet move futility pruning, using an additional margin.
            if (depth <= FUTILITY_PRUNE_DEPTH) && (eval + futility_margin + FUTILITY_MARGIN_NOHIST <= α)
                skipquiets = true
            end

            # Late move pruning.
            if (depth <= LATE_MOVE_PRUNE_DEPTH) && (num_quiets >= LATE_MOVE_COUNT[improving][depth + 1])
                skipquiets = true
            end

            # Counter move pruning
            if (moveorder.stage > STAGE_COUNTER) && (depth <= COUNTER_PRUNE_DEPTH[improving]) && (cmhist < COUNTER_PRUNE_LIMIT[improving])
                continue
            end

            # Follow up move pruning
            if (moveorder.stage > STAGE_COUNTER) && (depth <= FOLLOW_PRUNE_DEPTH[improving]) && (fmhist < FOLLOW_PRUNE_LIMIT[improving])
                continue
            end
        end

        # Prune moves which fail the static exchange evaluator.
        # Only ran if our best evaluation is not a mating line.
        if (moveorder.stage > STAGE_GOOD_NOISY) && (depth <= SEE_PRUNE_DEPTH) && (best > -MATE + MAX_PLY) &&
            (static_exchange_evaluator(board, move, isquiet ? see_quiet_margin : see_noisy_margin) == false)
            continue
        end

        u = apply_move!(thread, move)
        played += 1
        if isquiet && (quiets_tried.idx <= MAX_QUIET_TRACK)
            push!(quiets_tried, move)
        end

        # Late move reduction calculations.
        # This allows later searches to probe to a lower depth under given scenarios.
        if (depth > 2) && (played > 1) && (isquiet || cutnode || skipquiets)
            reduction = @inbounds LMRTABLE[min(depth, 64)][min(played, 64)]
            # Below are quiet-move specific reduction conditions.
            if isquiet
                if !pvnode
                    reduction += 1
                end
                # If we are not improving, increase the reduction depth.
                if isone(improving)
                    reduction += 1
                end
                # Add reductions for cutnodes
                if cutnode
                    reduction += 2
                end
                # Killer moves, and counter moves, are worth looking at more.
                if moveorder.stage < STAGE_INIT_QUIET
                    reduction -= 1
                end
                # Alter if we are moving our king when in check.
                if ischeck(board) && (type(board[from(move)]) === KING)
                    reduction += 1
                end
                # Adjust on the history
                reduction -= max(-2, min(2, fld(hist + cmhist + fmhist, 5000)))
            elseif (depth <= 7) && (played >= 3)
                reduction += 1
            end
            reduction = min(depth - 1, max(reduction, 1))
        else
            reduction = 1
        end

        # do we need an extension?
        hist_thresh = 14000 + 10*ply^2
        if (ischeck(board) || (isquiet && num_quiets <= 4 && cmhist >= hist_thresh && fmhist >= hist_thresh)) && !isroot
            newdepth = depth + 1
        else
            newdepth = depth
        end

        # perform search, taking into account LMR
        if reduction !== 1
            cand_eval = -absearch(thread, ttable, -α - 1, -α, newdepth - reduction, ply + 1, true)
        end
        if ((reduction !== 1) && (cand_eval > α)) || (reduction == 1 && !(pvnode && played == 1))
            cand_eval = -absearch(thread, ttable, -α - 1, -α, newdepth - 1, ply + 1, !cutnode)
        end
        if (pvnode && (played == 1 || ((cand_eval > α) && (isroot || cand_eval < β))))
            cand_eval = -absearch(thread, ttable, -β, -α, newdepth - 1, ply + 1, false)
        end

        # Revert move.
        undo_move!(thread, move, u)

        # Have we found a better move?
        # Ensure we encountered no stops, and update the PV.
        if (cand_eval > best) && (!thread.stop)
            best = cand_eval
            best_move = move
            if cand_eval > α
                α = cand_eval
                # Update the PV
                clear!(pv_current)
                push!(pv_current, best_move)
                updatepv!(pv_current, pv_future)

                # Did we fail high? Then we can break.
                if α >= β
                    break
                end
            end
        end
    end

    if iszero(played)
        clear!(moveorder)
        clear!(quiets_tried)
        if ischeck(board)
            # add ply to give an indication of the "fastest" mate
            return -MATE + ply
        else
            # Stalemate is a draw.
            return 0
        end
    end

    # Update the history heuristics.
    # This is done when a quiet move fails high.
    if (best >= β) && (best_move !== MOVE_NONE) && !istactical(board, best_move)
        updatehistory!(thread, quiets_tried, ply, depth^2)
    end

    clear!(moveorder)
    clear!(quiets_tried)

    # Update the transposition table.
    # We don't do this at the root node.
    if isroot == false
        tt_bound = best >= β ? BOUND_LOWER : (best > init_α ? BOUND_EXACT : BOUND_UPPER)
        setTTentry!(ttable, board.hash, eval, best_move, depth, tt_bound)
    end
    return best
end

"""
    static_exchange_evaluator(board::Board, move::Move)

Returns true if a move passes a static exchange criteria, false otherwise.
"""
const SEE_VALUES = @SVector [200, 775, 775, 1250, 2400, 15000]

function static_exchange_evaluator(board::Board, move::Move, threshold::Int)
    from_sqr = Int(from(move))
    to_sqr = Int(to(move))

    move_flag = flag(move)

    from_piece = piece(board, from_sqr)
    to_piece = piece(board, to_sqr)
    victim = (move_flag < __KNIGHT_PROMO) ? from_piece : makepiece(PieceType((move_flag >> 12) - 3), board.turn)

    occ = occupied(board)
    occ ⊻= (Bitboard(from_sqr) | Bitboard(to_sqr))

    if move_flag === __ENPASS
        occ ⊻= Bitboard(board.enpass)
    end

    attackers = (pawns(board) & pawnAttacks(!board.turn, to_sqr) & friendly(board)) |
    (pawns(board) & pawnAttacks(board.turn, to_sqr) & enemy(board)) |
    (knightMoves(to_sqr) & knights(board)) |
    (kingMoves(to_sqr) & kings(board))
    if !isempty(bishoplike(board))
        attackers |= (bishopMoves(to_sqr, occ) & bishoplike(board))
    end
    if !isempty(rooklike(board))
        attackers |= (rookMoves(to_sqr, occ) & rooklike(board))
    end

    attackers &= occ

    if isempty(attackers & enemy(board))
        return true
    end

    color = !board.turn

    balance = -threshold

    if to_piece !== BLANK
        @inbounds balance += SEE_VALUES[type(to_piece).val]
    end

    if move_flag >= __KNIGHT_PROMO
        @inbounds balance += SEE_VALUES[type(victim).val] - SEE_VALUES[1]
    end

    if move_flag === __ENPASS
        @inbounds balance += SEE_VALUES[1]
    end

    if balance < 0
        return false
    end

    @inbounds balance -= SEE_VALUES[type(victim).val]

    if balance >= 0
        return true
    end

    while true
        our_attackers = attackers & board[color]

        # if we can't attack, we lose
        if isempty(our_attackers)
            break
        end

        # find weakest piece to recapture
        for i in 1:6
            piecetype = PieceType(i)
            if isempty(our_attackers & board[piecetype]) == false
                victim = piecetype
                break
            end
        end

        # remove our attacker
        occ ⊻= Bitboard(poplsb(our_attackers & board[victim])[1])

        # check for diag moves
        if (victim === PAWN || victim === BISHOP || victim === QUEEN)
            attackers |= bishopMoves(to_sqr, occ) & bishoplike(board)
        end

        # check for rank/file moves
        if (victim === ROOK || victim === QUEEN)
            attackers |= rookMoves(to_sqr, occ) & rooklike(board)
        end

        attackers &= occ

        balance = -balance - 1 - SEE_VALUES[victim.val]
        color = !color

        if balance >= 0
            break
        end

    end
    # if it's your turn, you lost the SEE loop
    if board.turn === color
        return false
    else
        return true
    end
end

# delta pruning
function optimistic_move_estimator(board::Board)
    # assume pawn at minimum
    value = SEE_VALUES[1]

    # find highest val targets
    for i in 5:-1:2
        piecetype = PieceType(i)
        if isempty(board[board.turn] & board[piecetype]) == false
            @inbounds value = SEE_VALUES[i]
            break
        end
    end

    # promo checks
    if isempty(pawns(board) & board[board.turn] & (board.turn == WHITE ? RANK_7 : RANK_2)) == false
        @inbounds value += SEE_VALUES[5] - SEE_VALUES[1]
    end

    return value
end

function updatepv!(pv_current::MoveStack, pv_future::MoveStack)
    for tmp_pv_move in pv_future
        push!(pv_current, tmp_pv_move)
    end
end

function draw_noise(thread::Thread)
    return 2 * (thread.ss.nodes & 1) - 1
end


using CUDArt
@everywhere CUDArt.device((myid()-1) % CUDArt.devcount())
using KUparser, KUnet, HDF5, JLD, ArgParse, Compat
VERSION < v"0.4-" && eval(Expr(:using,:Dates))

function parse_commandline()
    s = ArgParseSettings()
    @add_arg_table s begin
        "datafiles"
        help = "JLD files for input: first one will be used for training"
        nargs = '+'
        required = true
        "--out"
        help = "JLD file for saving last network"
        #default = ""
        "--in"
        help = "JLD file for loading init network"
        #default = ""
        "--best"
        help = "JLD file for best (dev) network"
        #default = ""
        "--nogpu"
        help = "Do not use gpu"
        action = :store_true
        "--ispunct"
        help = "Do not score punct as determined by this function"
        #default = "zn11punct"
        "--parser"
        help = "Parsing algorithm to use: g(reedy)parser, b(eam)parser, or o(racle)parser for static training"
        default = "bparser"
        "--arctype"
        help = "Move set to use: ArcEager{R1,13}, ArcHybrid{R1,13}"
        default = "ArcEager13"
        "--feats"
        help = "Features to use from KUparser.Flist"
        default = "zn11cpv"
        "--hidden"
        help = "One or more hidden layer sizes"
        nargs = '+'
        arg_type = Int
        #default = [16384]
        "--minepochs"
        help = "Minimum number of epochs to train"
        arg_type = Int
        #default = 100
        "--maxepochs"
        help = "Maximum number of epochs to train"
        arg_type = Int
        #default = 100
        "--epochs"
        help = "Exact number of epochs to train"
        arg_type = Int
        #default = 100
        # Just start Julia with desired workers instead
        # "--ncpu"
        # help = "Number of workers for multithreaded parsing"
        # arg_type = Int
        #default = 16
        "--usepmap"
        help = "Use multiple processes"
        action = :store_true
        "--strain"
        help = "Number of sentences to parse for a training batch"
        arg_type = Int
        default = 2500
        "--stest"
        help = "Number of sentences to parse for a testing batch"
        arg_type = Int
        default = 2500
        "--pbatch"
        help = "Minibatch argument for parse"
        arg_type = Int
        default = 25
        "--tbatch"
        help = "Minibatch argument for train"
        arg_type = Int
        default = 128
        "--nbeam"
        help = "Beam size for bparser"
        arg_type = Int
        default = 64
        "--adagrad"
        help = "Use adagrad"
        action = :store_true
        # arg_type = Float32
        # nargs = '+'
        # default = [1f-8]
        "--dropout"
        help = "Dropout probability"
        arg_type = Float32
        nargs = '+'
        # default = [0.1f0, 0.5f0]
        "--l1reg"
        help = "L1 regularization parameter"
        arg_type = Float32
        nargs = '+'
        "--l2reg"
        help = "L2 regularization parameter"
        arg_type = Float32
        nargs = '+'
        "--lr"
        help = "Learning rate"
        arg_type = Float32
        nargs = '+'
        default = [0.01f0]
        "--maxnorm"
        help = "If nonzero upper limit on weight matrix row norms"
        arg_type = Float32
        nargs = '+'
        "--momentum"
        help = "Momentum"
        arg_type = Float32
        nargs = '+'
        "--nesterov"
        help = "Apply nesterov's accelerated gradient"
        arg_type = Float32
        nargs = '+'
        "--seed"
        help = "Random number seed (-1 leaves the default seed)"
        arg_type = Int
        default = -1
        "--shuffle"
        help = "Whether to shuffle the data every epoch"
        action = :store_true
    end
    args = parse_args(s)
    for (arg,val) in args
        print("$arg:$val ")
    end
    println("")
    return args
end

function getscore(parses, corpus, ispunct)
    if ispunct == nothing
        @show e = evalparse(parses, corpus)
        return e[1]
    else
        @show e = evalparse(parses, corpus; ispunct=ispunct)
        return e[2]
    end
end

function loaddata(files)
    data = Corpus[]
    deprel = postag = nothing
    ndeps = 0
    for f in files
        @show f
        @date d=load(f)
        push!(data, d["corpus"])
        if deprel == nothing
            deprel = d["deprel"]
            postag = d["postag"]
            ndeps = length(deprel)
        else
            @assert deprel == d["deprel"]
            @assert postag == d["postag"]
        end
    end
    return (data, ndeps)
end

function initnet(args, pt, ndeps)
    args["in"] != nothing && !isfile(args["in"]) && (warn("Cannot find net file, creating blank net"); args["in"]=nothing)
    if args["in"] == nothing
        net = Layer[]
        length(args["dropout"]) > 1 && push!(net, Drop(args["dropout"][1]))
        for h in args["hidden"]
            append!(net, [Mmul(h), Bias(), Relu()])
            !isempty(args["dropout"]) && push!(net, Drop(args["dropout"][end]))
        end
        yrows = pt(1,ndeps).nmove
        append!(net, [Mmul(yrows), Bias()])
        push!(net, args["parser"]=="bparser" ? ScalLoss() : XentLoss())
    else
        @date net = loadnet(args["in"])
    end
    for k in fieldnames(KUparam)
        haskey(args, string(k)) || continue
        v = args[string(k)]
        if isempty(v)
            continue
        elseif length(v)==1
            @eval setparam!($net; $k=$v[1])
        else
            @assert length(v)==length(net) "$k should have 1 or $(length(net)) elements"
            for i=1:length(v)
                @eval setparam!($net[$i]; $k=$v[$i])
            end
        end
    end
    return net
end

function main()
    args = parse_commandline()
    @show nworkers()
    KUnet.gpu(!args["nogpu"])
    args["nogpu"] && blas_set_num_threads(20)
    args["seed"] >= 0 && setseed(args["seed"])
    (data, ndeps) = loaddata(args["datafiles"])
    @show (map(size, data), ndeps)
    @show feats = eval(parse("Flist."*args["feats"]))
    @show pt = eval(parse(args["arctype"]))
    net = initnet(args, pt, ndeps)
    display(net); println()
    ispunct = (args["ispunct"]==nothing ? nothing : eval(parse("KUparser."*args["ispunct"])))
    @show ispunct
    accuracy = Array(Float32, length(data))
    (bestscore,bestepoch,epoch)=(0,0,0)

    while true
        println("\n$(now()) epoch => $(epoch += 1)")
        icorpus=1
        corpus=data[icorpus]          # use data[1] as the training corpus
        parses=pt[]
        for c1=1:args["strain"]:length(corpus)
            c2=min(length(corpus), c1+args["strain"]-1)
            sentences=sub(corpus, c1:c2)
            @show (epoch, icorpus, c1, c2)
            if (args["parser"] == "oparser") # || ((epoch==1) && (args["in"]==nothing)))
                @date (p,x,y) = oparse(pt, sentences, ndeps, feats; usepmap=args["usepmap"])
            elseif (args["parser"] == "gparser")
                @date (p,x,y) = gparse(pt, sentences, ndeps, feats, net; usepmap=args["usepmap"], nbatch=args["pbatch"], returnxy=true)
            elseif (args["parser"] == "bparser")
                @date (p,x,y) = bparse(pt, sentences, ndeps, feats, net, args["nbeam"]; usepmap=args["usepmap"], nbatch=args["pbatch"], returnxy=true)
            else
                error("Unknown parser")
            end
            @show map(size, (p,x,y))
            @assert isa(p, Vector{pt})
            @assert isa(x, Vector{Matrix{wtype(sentences)}})
            @assert isa(y, Vector{Matrix{wtype(sentences)}})
            @assert length(x)==length(y)
            @everywhere gc(); @everywhere gc()
            append!(parses, p)
            @date for i=1:length(x)
                train(net, x[i], y[i]; batch=args["tbatch"], shuffle=args["shuffle"])
            end
        end
        accuracy[icorpus] = getscore(parses, corpus, ispunct)
        (args["out"] != nothing) && (@date savenet(args["out"], net))

        for icorpus=2:length(data)
            corpus=data[icorpus]
            parses=pt[]
            for c1=1:args["stest"]:length(corpus)
                c2=min(length(corpus), c1+args["stest"]-1)
                @show (:corpus, icorpus, c1, c2)
                sentences=sub(corpus, c1:c2)
                if in(args["parser"], ("oparser", "gparser"))
                    @date p = gparse(pt, sentences, ndeps, feats, net; usepmap=args["usepmap"], nbatch=args["pbatch"], returnxy=false)
                elseif (args["parser"] == "bparser")
                    @date p = bparse(pt, sentences, ndeps, feats, net, args["nbeam"]; usepmap=args["usepmap"], nbatch=args["pbatch"], returnxy=false)
                else
                    error("Unknown parser")
                end
                @show size(p)
                @assert isa(p, Vector{pt})
                @everywhere gc(); @everywhere gc()
                append!(parses, p)
            end
            accuracy[icorpus] = getscore(parses, corpus, ispunct)
        end

        println("DATA:\t$epoch\t"*join(accuracy, '\t')); flush(STDOUT)

        # We look at accuracy[2] (dev score) by default,
        # accuracy[1] (trn score) if there is no dev.

        myscore = (length(accuracy) > 1 ? accuracy[2] : accuracy[1])

        # Update best score and save best net

        if (myscore > bestscore)
            @show (bestscore,bestepoch)=(myscore,epoch)
            (args["best"] != nothing) && (@date savenet(args["best"], net))
        end

        # If epochs specified, do exactly epoch.
        # Otherwise quit if we have not made progress in the last half.
        # If minepochs specified, do not quit before minepoch.
        # If maxepochs specified, quit before maxepoch.

        if (args["epochs"] != nothing)
            (epoch >= args["epochs"]) && break
        elseif (epoch >= 2*bestepoch)
            (args["minepochs"]==nothing || epoch >= args["minepochs"]) && break
        elseif (args["maxepochs"]!=nothing)
            (epoch >= args["maxepochs"]) && break
        end

    end # while true
end # main

main()

# DEAD CODE
    # Initialize training set using oparse on first corpus
    # @date (p,x,y) = oparse(pt, data[1], ndeps, args["ncpu"], feats)
    # @date (p,x,y) = oparse(pt, data[1], ndeps, feats)
    # @show evalparse(p, data[1]); p=nothing
    # for i=2:length(data)
    #     #@date p = oparse(pt, data[i], ndeps, args["ncpu"])
    #     @date p = oparse(pt, data[i], ndeps)
    #     @show evalparse(p, data[i]); p=nothing
    # end

    # else # if DBG
    #     @date net = loadnet("foo11nnet3.jld")
    #     # @date d = load("zn11cpv.jld")
    #     # (x,y) = (d["xtrn"],d["ytrn"])
    #     @date x = Array(Float32, 3105,1820392)
    #     @date y = Array(Float32, 24,1820392)
    # end  # if DBG
        # @meminfo
        # if !DBG
        #     if isa(eltype(x), Array)
        #         @assert length(x)==length(y)
        #         for i=1:length(x)
        #             @show map(size, (x[i], y[i]))
        #             @date train(net, x[i], y[i]; batch=args["tbatch"], shuffle=true)
        #         end
        #     else
        #         @show map(size, (x, y))
        #         @date train(net, x, y; batch=args["tbatch"], shuffle=true)
        #     end
        # end # if !DBG

        # @meminfo
        # if (args["parser"] == "bparser")
        #     # The first corpus gives us the new training set
        #     p = x = y = nothing; gc()
        #     @date (@everywhere gc())
        #     @date (@everywhere require("KUparser"))
        #     @date (for w in workers(); @fetchfrom w require("errlog.jl"); end)
        #     @meminfo
        #     @date pxy = bparse(pt, data[1], ndeps, feats, net, args["nbeam"], args["pbatch"], args["ncpu"]; xy=true)
        #     @meminfo
        #     p = map(z->z[1], pxy); @show map(size, p)
        #     x = map(z->z[2], pxy); @show map(size, x)
        #     y = map(z->z[3], pxy); @show map(size, y)
        #     @meminfo
        #     #@show map(getbytes, (p,x,y))
        #     @show e = evalparse(mycat(p), data[1]); p=nothing
        #     accuracy[1] = e[2]
        #     for i=2:length(data)
        #         @date p = bparse(pt, data[i], ndeps, feats, net, args["nbeam"], args["pbatch"], args["ncpu"])
        #         @show e = evalparse(mycat(p), data[i]); p=nothing
        #         accuracy[i] = e[2]
        #     end
        #     #@date Main.restartmachines()
        #     #@date Main.restartcuda()
        # elseif (args["parser"] == "oparser")
        #     # We never change the training set with oparser, just report accuracy
        #     for i=1:length(data)
        #         @date p = gparse(pt, data[i], ndeps, feats, net, args["pbatch"], args["ncpu"])
        #         @show e = evalparse(p, data[i]); p=nothing
        #         accuracy[i] = e[2]  # e[2] is UAS excluding punct
        #     end
        # elseif (args["parser"] == "gparser")
        #     # The first corpus gives us the new training set
        #     p = x = y = nothing;
        #     @date (@everywhere gc())
        #     @date (@everywhere require("KUparser"))
        #     @date (@everywhere require("errlog.jl"))
        #     @date (p,x,y) = gparse(pt, data[1], ndeps, feats, net, args["pbatch"], args["ncpu"]; xy=true)
        #     @show e = evalparse(p, data[1]); p=nothing
        #     accuracy[1] = e[2]
        #     for i=2:length(data)
        #         @date p = gparse(pt, data[i], ndeps, feats, net, args["pbatch"], args["ncpu"])
        #         @show e = evalparse(p, data[i]); p=nothing
        #         accuracy[i] = e[2]
        #     end
        #     @date Main.restartmachines()
        #     @date Main.restartcuda()
        # else
        #     error("Unknown parser "*args["parser"])
        # end
#macro meminfo() :(gc(); run(`nvidia-smi`); run(`ps auxww`|>`grep julia`); run(`free`)) end
#macro meminfo() :(run(`ps auxww`|>`grep julia`); run(`free`)) end
#macro meminfo() :(nothing) end

# function mycat(a::Array)
#     # concat elements of a along their last dimension
#     mx = size(a[1])[1:end-1]
#     nx = 0
#     for x in a
#         @assert size(x)[1:end-1] == mx
#         nx += size(x)[end]
#     end
#     b = similar(a[1], tuple(mx..., nx))
#     nb = 0
#     for x in a
#         copy!(b, nb+1, x, 1, length(x))
#         nb += length(x)
#     end
#     return b
# end

        # # gc() does not work, so we need these:
        # @date sleep(5)
        # @date Main.restartmachines()
        # @date sleep(5)
        # @date Main.restartcuda()
        # @date (@everywhere require("KUparser"))
        # @date (@everywhere gc())


# initialisation functions for GeMM

"""
    createpop(settings)

Create a new, random individual and replicates it a certain number of times
(depending on metabolic variables) to create a new population of organisms.
Returns an array of individuals.
"""
function createpop(settings::Dict{String, Any})
    # start with an archetypical individual
    archetype = createind(settings, true)
    population = [archetype]
    # calculate the population size
    popsize = 0
    if occursin("metabolic", settings["popsize"]) || occursin("single", settings["popsize"])
        # population size determined by adult size
        popsize = round(settings["fertility"] * archetype.traits["repsize"] ^ (-1 / 4) *
                        exp(-act / (boltz * 298.0)))
    elseif occursin("bodysize", settings["popsize"])
        # population size up to 25% of the maximum possible in this cell
        quarterpopsize = Integer(ceil((settings["cellsize"] / archetype.traits["repsize"]) / 4))
        popsize = rand(1:quarterpopsize) + 1
    elseif occursin("minimal", settings["popsize"]) || popsize == 0
        popsize = 2 #Takes two to tangle ;-)
    else
        simlog("Invalid value for `popsize`: $(settings["popsize"])", 'e')
    end
    # clone & mutate the archetype N times
    locivar = rand()
    for i in 2:popsize
        #TODO avoid `deepcopy` here
        ind = deepcopy(archetype) # XXX this is potentially very slow!
        ind.id = rand(UInt32)
        varyalleles!(ind.genome, settings, locivar)
        ind.traits = gettraitdict(ind.genome, settings["traitnames"])
        if !(settings["indsize"] == "adult" || settings["indsize"] == "seed")
            #XXX sizes uniformally distributed?
            ind.size = ind.traits["seedsize"] + rand() * (ind.traits["repsize"] - ind.traits["seedsize"])
        end
        push!(population, ind)
    end
    population
end

"""
    createind(settings, marked=false)

Create an individual organism of a new species with a random genome.
"""
function createind(settings::Dict{String, Any}, marked::Bool = false)
    id = rand(Int32)
    lineage = randstring(6)
    ngenes = rand(1:settings["maxloci"]) * length(settings["traitnames"])
    settings["degpleiotropy"] == 0 && (ngenes = length(settings["traitnames"]))
    ngenes < 1 && (ngenes = 1)
    traits = createtraits(settings)
    genes = creategenes(ngenes, traits, settings)
    if settings["linkage"] == "none"
        nchrms = length(genes)
    elseif settings["linkage"] == "full"
        nchrms = 1
    else
        linkage = rand(1:length(genes))
        nchrms = Integer(round(ngenes/linkage))
    end
    chromosomes = createchrms(nchrms, genes)
    locivar = rand()
    varyalleles!(chromosomes, settings, locivar)
    traitdict = gettraitdict(chromosomes, settings["traitnames"])
    if settings["indsize"] == "adult"
        indsize = traitdict["repsize"]
    elseif settings["indsize"] == "seed"
        indsize = traitdict["seedsize"]
    else
        #XXX sizes uniformally distributed?
        indsize = traitdict["seedsize"] + rand() * (traitdict["repsize"] - traitdict["seedsize"])
    end
    Individual(lineage, chromosomes, traitdict, marked, 1.0, 1.0, indsize, hermaphrodite, 0, id)
end

"""
    genesis(settings)

Create a new community, composed of random new species populations, for a patch.
Returns an array of individuals.
"""
function genesis(settings::Dict{String, Any})
    community = Individual[]
    totalmass = 0.0
    while true
        population = createpop(settings)
        popsize = length(population)
        # Check the cell capacity
        popmass = sum(map(x -> x.size, population))
        if totalmass + popmass > settings["cellsize"] * settings["overfill"]
            # stop loop if cell is full
            if totalmass >= settings["cellsize"] * 0.9 || occursin("single", settings["popsize"])
                #make sure the cell is full enough
                simlog("Cell is now $(round((totalmass/settings["cellsize"])*100))% full.", 'd') #DEBUG
                break
            else
                continue
            end
        end
        totalmass += popmass
        append!(community, population)
        occursin("single", settings["popsize"]) && break
    end
    simlog("Patch initialized with $(length(community)) individuals.", 'd') #DEBUG
    community
end

"""
    createpatch(patchentry, settings)

Create a new patch and set its parameters by parsing one line of the map file.
"""
function createpatch(patchentry::Array{String,1}, settings::Dict{String, Any})
    size(patchentry,1) < 3 && simlog("please check your map file for incomplete or faulty entries. \n
Each line must contain patch information with at least \n
\t - a unique integer ID, \n
\t - an integer x coordinate, \n
\t - an integer y coordinate, \n
separated by a whitespace character (<ID> <x> <y>).", settings, 'e')
    # create the basic patch
    id = parse(Int, patchentry[1])
    xcord = parse(Int, patchentry[2])
    ycord = parse(Int, patchentry[3])
    capacity = settings["cellsize"]
    # XXX the 'global' here is a hack so that I can use eval() later on
    # (eval() always works on the global scope)
    global newpatch = Patch(id, (xcord, ycord), capacity)
    simlog("Creating patch $id at $xcord/$ycord, size $capacity", 'd') #DEBUG
    # parse other parameter options
    for p in patchentry[4:end]
        varval = split(p, '=')
        var = varval[1]
        if !(var in map(string, fieldnames(Patch)))
            simlog("Unrecognized patch parameter $var.", 'w')
            continue
        elseif length(varval) < 2
            # if no value is specified, assume 'true'
            # (unless the variable is prepended with an exclamation mark, then take 'false')
            if var[1] == '!'
                var = var[2:end]
                val = false
            else
                val = true
            end
        else
            val = Meta.parse(varval[2])
        end
        # check for correct type and modify the new patch
        vartype = typeof(eval(Meta.parse("newpatch."*var)))
        if !isa(val, vartype)
            try
                val = convert(vartype, val)
            catch
                simlog("Invalid patch parameter type $var: $val", 'w')
                continue
            end
        end
        eval(Meta.parse("newpatch."*string(var)*" = $val"))
    end
    newpatch
end

"""
    createworld(maptable, settings)

Use a parsed map file (as returned by `readmapfile`) to create the world.
Initialises each patch with its parameters and a new community, then returns
an array of patches.
"""
function createworld(maptable::Array{Array{String,1},1}, settings::Dict{String, Any})
    simlog("Creating world...")
    world = Patch[]
    for entry in maptable
        newpatch = createpatch(entry, settings)
        if newpatch.initpop
            if settings["mode"] == "zosterops"
                append!(newpatch.community, zgenesis(newpatch, settings))
            elseif settings["indsize"] != "seed"
                append!(newpatch.community, genesis(settings))
            else
                append!(newpatch.seedbank, genesis(settings))
            end
        end
        push!(world, newpatch)
        global newpatch = nothing #clear memory used in `createpatch()` #XXX does this make sense?
    end
    world
end

"""
    updateworld!(world, maptable, cellsize)

Reinitialise the world from another parsed map file. Works analogously to
`createworld`. Intended for use in scenarios where the model world changes
during a run (e.g. through global warming or island ontogeny).
"""
#FIXME this function needs to be folded into createworld(), it duplicates way too much code
function updateworld!(world::Array{Patch,1},maptable::Array{Array{String,1},1}, settings::Dict{String, Any})
    cellsize = settings["cellsize"]
    simlog("Updating world...")
    allids = Int[]
    for entry in maptable
        size(entry,1) < 3 && error("please check your map file for incomplete or faulty entries. \n
                            Each line must contain patch information with at least \n
                            \t - a unique integer ID, \n
                            \t - an integer x coordinate, \n
                            \t - an integer y coordinate, \n
                            separated by a whitespace character (<ID> <x> <y>).")
        id = parse(Int, entry[1])
        push!(allids, id)
        xcord = parse(Int, entry[2])
        ycord = parse(Int, entry[3])
        # XXX the 'global' here is a hack so that I can use eval() later on
        # (this always works on the global scope)
        idx = findall(x -> x.id == id, world)
        if length(idx) == 0
            marked = true
            global newpatch = Patch(id, (xcord, ycord), cellsize)
        else
            marked = false
            world[idx[1]].location = (xcord, ycord)
            global newpatch = world[idx[1]]
        end
        # parse other parameter options
        for p in entry[4:end]
            varval = split(p, '=')
            var = varval[1]
            if !(var in map(string, fieldnames(Patch)))
                simlog("Unrecognized patch parameter $var.", 'w')
                continue
            elseif length(varval) < 2
                val = true # if no value is specified, assume 'true'
            else
                val = Meta.parse(varval[2])
            end
            # check for correct type and modify the new patch
            vartype = typeof(eval(Meta.parse("newpatch."*var)))
            if !isa(val, vartype)
                try
                    val = convert(vartype, val)
                catch
                    simlog("Invalid patch parameter type $var: $val", 'w')
                    continue
                end
            end
            eval(Meta.parse("newpatch."*string(var)*" = $val"))
        end
        if marked
            push!(world, newpatch)
            global newpatch = nothing #clear memory
        end
    end
    filter!(x -> x.id in allids, world)
    world
end