# SPDX-FileCopyrightText: 2026 Yating Zhang
# SPDX-License-Identifier: MIT

"""
numerical 6.3 cyclic policy for M = 1,2 
name: Cyclic 
""" 
function enable_stay_awake()
    if Sys.iswindows()
        # Windows: Call Kernel32 API
        # ES_CONTINUOUS (0x80000000) | ES_SYSTEM_REQUIRED (0x00000001)
        # Tells the system that this thread requires the system to be in the working state.
        try
            ccall((:SetThreadExecutionState, "kernel32"), UInt32, (UInt32,), 0x80000001)
            println(">> Windows: System sleep prevention enabled.")
        catch
            println(">> Windows: Failed to set sleep prevention via API.")
        end
    elseif Sys.isapple()
        # macOS: Use caffeinate
        # -i: prevent idle sleep
        # -w: wait for the process with the given PID to exit
        run(`caffeinate -i -w $(getpid())`, wait=false)
        println(">> macOS: System sleep prevention enabled (caffeinate).")
    elseif Sys.islinux()
        # Linux: Use systemd-inhibit
        # It creates a lock that prevents sleep/idle.
        try
            run(`systemd-inhibit --why="Numerical Experiments" --who="Julia" --what=idle:sleep sleep infinity`, wait=false)
            println(">> Linux: System sleep prevention enabled (systemd-inhibit).")
        catch
            println(">> Linux: systemd-inhibit not found. System may still sleep.")
        end
    end
end

enable_stay_awake()

# Check if "--mode" exists and is followed by "full"
mode_idx = findfirst(x -> x == "--mode", ARGS)
is_full = mode_idx !== nothing && mode_idx < length(ARGS) && ARGS[mode_idx+1] == "full"


if is_full
    @info "RUNNING IN FULL MODE: This will reproduce the full tables."
else
    @info "RUNNING IN FULL MODE: This will reproduce the full tables."
end

# ==================== import packages ====================
using JuMP,CSV,DataFrames,Gurobi,BenchmarkTools,MosekTools, XLSX
import LinearAlgebra 
import Random
Random.seed!(314)
import MathOptInterface as MOI 
using Dates, JSON 
 
# --- PATH SETUP ---
const SCRIPT_DIR = @__DIR__
const PROJECT_ROOT = dirname(SCRIPT_DIR) 
# ------------------

include(joinpath(PROJECT_ROOT, "src", "Gene_data.jl")) 
include(joinpath(PROJECT_ROOT, "src", "Project_CuttingPlane.jl"))   
# ==================== Set parameters ====================
println("=" ^ 50)
println("Set optimization parameters ...") 
 
testname = "Cyclic" 

Mlist = [1,2] 
Nlist = [50,100] 
posi = 0 # consider satiation case 
mix = 0 
strong = 0  # weak satiation 
ratio = 0 
testn = 5 # each parameter combination has 5 instances  
 
v0 = 1 
time_bound = 60*60 
stop_gap = 0.005
init = 0
init_X = Any[]
output = 1
IP = 1   

println("Numerical study: ", testname)
println("Number of products N = ", Nlist)
println("Memory length M = ", Mlist)
println("Planning horizon T = M+1", ) 
println("Time limit = ", time_bound, " seconds ")
println("Stop optimality gap = ", stop_gap*100, "%")



println("Start solving HAP problems...")
println("=" ^ 50)

Result = [ ]
BF1RES = [ ] 
results_folder = joinpath(PROJECT_ROOT, "results", "Table6_Sec6_3_Cyclic")
logs_folder = joinpath(results_folder, "logs")
mkpath(logs_folder)  

json_path = joinpath(PROJECT_ROOT, "data","Cyclic.json")
test_data = open( json_path, "r") do io
    JSON.parse(read(io, String))
end



for M in Mlist   
    for tn in 1:length(Nlist)
        N = Nlist[tn]

        for k in 1:testn 
            r = test_data[(tn-1)*5 + k ][1]
            u = test_data[(tn-1)*5 + k ][2] 
            beta = zeros(N,M) 
            s = zeros(N,M) 
    
            # unconstrained 
            cardinal = N 
            showtime = 1 

            # obtain BF-infinite 
            res12 = BoundFree(N,M, r,u,v0,beta, 0 ,stop_gap,time_bound,output, "",s,cardinal ) 

            # obtain BF-K 
            lpbf0 = BoundFreewithSepa(N,M,r,u,v0,beta, 0 ,0, stop_gap, time_bound,  "", s ,cardinal)
            lpbf1 = BoundFreewithSepa(N,M,r,u,v0,beta, 0 ,1, stop_gap, time_bound,  "", s,cardinal)
            lpbf2 = BoundFreewithSepa(N,M,r,u,v0,beta, 0 ,2, stop_gap, time_bound,  "", s ,cardinal)
            lpbf3 = BoundFreewithSepa(N,M,r,u,v0,beta, 0 ,3, stop_gap, time_bound,  "", s,cardinal )

            
            push!(Result, [M,N,k, res12[1], lpbf0[1], lpbf1[1], lpbf2[1], lpbf3[1] ] )

            # solve BF-1
            log_filename = "bf1_N"*string(N)*"_M"*string(M)*"_inst"*string(k)*".txt" 
            logbf1 = joinpath(logs_folder, log_filename)
            resbf1 = BoundFreewithSepa(N,M,r,u,v0,beta, IP ,1, stop_gap, time_bound, logbf1, s ,cardinal)
            push!(BF1RES, [M,N,k, resbf1[1], resbf1[3], resbf1[4], resbf1[5]])
        
        end 
    end 
end 
 

temp_matrix  = tupe2mat(Result) 
f1 = joinpath(results_folder, "LP_detail.csv")
temp_data = DataFrame(temp_matrix, :auto) 
rename!(temp_data, ["M", "N", "k","obj_bf","obj_base","obj_r1", "obj_r2", "obj_r3" ])
CSV.write(f1,  temp_data, writeheader=false)


temp_matrix2  = tupe2mat(BF1RES) 
f2 = joinpath(results_folder,"IP_detail.csv")
temp_data2 = DataFrame(temp_matrix2, :auto) 
rename!(temp_data2, ["M", "N", "k","obj_ip", "runtime","total_time", "gap" ])
CSV.write(f2,  temp_data2, writeheader=false)


# =============== compute table 6 (a) ======================= 
temp_data.close1 = (temp_data.obj_base  .- temp_data.obj_r1) ./ (temp_data.obj_base  .- temp_data.obj_bf) 
temp_data.close2 = (temp_data.obj_base  .- temp_data.obj_r2) ./ (temp_data.obj_base  .- temp_data.obj_bf) 
temp_data.close3 = (temp_data.obj_base  .- temp_data.obj_r3) ./ (temp_data.obj_base  .- temp_data.obj_bf) 


sumdata = [ ] 
for N in Nlist
    for M in Mlist     
        filterdata = filter(row -> row.M == M && row.N == N , temp_data)
        push!(sumdata, [N,M, round(100*mean(filterdata.close1),digits=4), round(100*mean(filterdata.close2),digits=4), round(100*mean(filterdata.close3),digits=4) ])
    end 
end 

# Get current time
t = Dates.now() 
timestr = Dates.format(t, "HHMM")

sumdata = tupe2mat(sumdata) 
sumdata = DataFrame(sumdata, :auto)   
rename!(sumdata, ["N" , "M", "GClosed_1", "GClosed_2", "GClosed_3"] )
table6_1 = joinpath(results_folder,"Table6a_close_gap_"*timestr*".xlsx")
XLSX.writetable(table6_1, sumdata)

# =============== compute table 6 (b) ======================= 

summary = [ ] 
for N in Nlist
    for M in Mlist   
        filterdata = filter(row -> row.M == M && row.N == N , temp_data2)

        solved = 0 
        total_time = 0 
        total_endgap = 0 
        for (index, row) in enumerate(eachrow(filterdata))
            if row.gap <= stop_gap 
                solved +=1 
                total_time += row.runtime  
            else 
                total_endgap += row.gap
            end
        end 
        if solved ==0 
            push!(summary, [N,M, solved, time_bound, round(100*total_endgap/testn, digits=4)] )  
        elseif solved == testn 
            push!(summary, [N,M, solved, round(total_time/testn, digits=4), round(100*total_endgap/testn ) ] )  
        else
            push!(summary, [N,M, solved, round(total_time/solved, digits=4), round(100*total_endgap/(testn-solved), digits=4)] ) 
        end
    end 
end 

summary = tupe2mat(summary) 
summary = DataFrame(summary, :auto)   
rename!(summary, ["N" , "M", "solved", "time", "end_gap"] )
table6_2 = joinpath(results_folder,"Table6b_IPsolu_"*timestr*".xlsx")
XLSX.writetable(table6_2, summary)

println("Finish numerical study ", testname, "!")