module Mainrun
    using Dates
    using InteractiveUtils
    import ..LTK_universe:Universe,show_parameters,make_WdagWmatrix,calc_Action,set_β!,set_βs!,get_β,
                            Wilsonloops_actions,calc_looptrvalues,calc_trainingdata,calc_looptrvalues_site
    import ..Actions:Setup_Gauge_action,Setup_Fermi_action,GaugeActionParam_autogenerator
    import ..Measurements:calc_plaquette,measure_correlator,Measurement,calc_polyakovloop,measure_chiral_cond,calc_topological_charge,
                measurements,Measurement_set
    import  ..MD:md_initialize!,MD_parameters_standard,md!,metropolis_update!,construct_MD_parameters
    import ..System_parameters:Params,print_parameters,parameterloading,Params_set#,parameterloading2
    import ..Print_config:write_config
    import ..Smearing:gradientflow!,calc_fatlink_APE,calc_stout
    import ..ILDG_format:ILDG,load_gaugefield,load_gaugefield!,save_binarydata
    import ..Bridge_format:load_BridgeText!,save_textdata
    import ..Heatbath:heatbath!,overrelaxation!
    import ..Wilsonloops:make_plaq,make_loopforactions,make_plaqloops,make_rectloops,make_polyakovloops
    import ..IOmodule:saveU,loadU,loadU!
    import ..SLMC:SLMC_data,show_effbeta,update_slmcdata!
    import ..Gaugefields:calc_GaugeAction

    import ..Actions:GaugeActionParam_standard,
                    GaugeActionParam,
                    GaugeActionParam_autogenerator
    import ..Verbose_print:println_verbose1,println_verbose2,Verbose_1

    import ..System_parameters:system,actions,md,cg,wilson,staggered,measurement

    function run_LQCD(filenamein::String)
        #if isdemo
        #    params_set = Params_set(Demo.system,Demo.actions,Demo.md,Demo.cg,Demo.wilson,Demo.staggered,Demo.measurement)
        #else
        filename = filenamein
        include(abspath(filename))
        params_set = Params_set(system,actions,md,cg,wilson,staggered,measurement)
        #end

        plaq = run_LQCD(params_set)
    end

    #=
    function run_LQCD()
        parameters = parameterloading()

        load_gaugefield()

        univ = Universe(parameters)

        run_LQCD!(univ,parameters)
        return
    end
    =#

    function run_LQCD(parameters::Params)
        univ = Universe(parameters)
        plaq = run_LQCD!(univ,parameters)

        return plaq
    end

    function run_LQCD(params_set::Params_set)
        parameters = parameterloading(params_set)
        univ = Universe(parameters)
        plaq = run_LQCD!(univ,parameters)

        return  plaq
    end

    function run_LQCD!(univ::Universe,parameters::Params)
        verbose = univ.kind_of_verboselevel
        #println("# ",pwd())
        #println("# ",Dates.now())
        println_verbose1(verbose,"# ",pwd())
        println_verbose1(verbose,"# ",Dates.now())
        versioninfo(verbose)

        #show_parameters(univ)

        mdparams = construct_MD_parameters(parameters)

        measset = Measurement_set(univ,parameters.measuredir,measurement_methods=parameters.measurement_methods)

        #if isdemo
        #run_demo!(parameters,univ,measset)
        #else
        plaq = run_core!(parameters,univ,mdparams,measset)
        #end

    end

    function checkfile(filename,list)
        for name in list

        end
    end

    function run_init_Fileloading!(parameters,univ,mdparams,meas,verbose)
        ildg = nothing
        println_verbose1(verbose,"load U from ",parameters.loadU_dir)
        if parameters.loadU_format == "JLD"
            datatype = "JLD"
            filename_load =  filter(f -> contains(f,".jld2"),readdir("./$(parameters.loadU_dir)"))
            #filename_load =  filter(f -> contains(f,".jld"),readdir("./$(parameters.loadU_dir)"))
        elseif parameters.loadU_format == "ILDG"
            datatype = "ILDG"
            filename_load =  filter(f -> contains(f,"ildg"),readdir("./$(parameters.loadU_dir)"))
        elseif parameters.loadU_format == "BridgeText"
            datatype = "BridgeText"
            filename_load =  filter(f -> contains(f,"txt"),readdir("./$(parameters.loadU_dir)"))
        else
            error("loadU_format should be JLD or ILDG")
        end

        if parameters.loadU_fromfile
            data = readlines("./$(parameters.loadU_dir)/$(parameters.loadU_filename)")
            datafromlist = String[]

            for (i,datai) in enumerate(data)
                havesharp = findfirst('#',datai)
                #println("havesharp: $havesharp")
                if havesharp == 1
                    datau = ""
                elseif havesharp != nothing
                    datau = datai[begin:havesharp-1]
                else
                    datau = datai
                end

                datau = strip(datau)

                if datatype == "JLD"
                    doescontains = contains(datau,".jld2")
                    #doescontains = contains(datau,".jld")
                elseif datatype == "ILDG"
                    doescontains = contains(datau,".ildg")
                elseif datatype == "BridgeText"
                    doescontains = contains(datau,".txt")
                end

                #println(doescontains)
                if doescontains
                    if isfile("./$(parameters.loadU_dir)/$(datau)")
                        push!(datafromlist,datau)
                    else
                        println("Warning! $(datau) does not exist in $(parameters.loadU_dir)! We ignore this.")
                    end
                    #println(datau)

                end
            end
            filename_load = datafromlist
            #println(datafromlist)

        end

        #exit()

        #filename = filter(f -> isfile(f), readdir("./$(parameters.loadU_dir)"))
        #println(filename)
        numfiles = length(filename_load)
        println_verbose1(verbose,"Num of files = $numfiles")
        for file in filename_load
            println_verbose1(verbose,"$file")
        end
        #println("Num of files = $numfiles")
        Nsteps = numfiles-1
        filename_i = filename_load[1]
        if parameters.loadU_format == "JLD"
            loadU!(parameters.loadU_dir*"/"*filename_i,univ.U)
        elseif parameters.loadU_format == "ILDG"
            ildg = ILDG(parameters.loadU_dir*"/"*filename_i)
            i = 1
            load_gaugefield!(univ.U,i,ildg,parameters.L,parameters.NC)
        elseif parameters.loadU_format == "BridgeText"
            load_BridgeText!(parameters.loadU_dir*"/"*filename_i,univ.U,parameters.L,parameters.NC)
        end
        return Nsteps,numfiles,filename_load,ildg

    end

    function print_trainingdata(trs,Sg,Sf)
        for i=1:length(trs)
            print(real(trs[i]),"\t",imag(trs[i]),"\t")
        end
        println(Sg,"\t",Sf,"\t #trainingdata")
    end

    function print_trainingdata(fp,trs,Sg,Sf)
        for i=1:length(trs)
            print(fp,real(trs[i]),"\t",imag(trs[i]),"\t")
        end
        println(fp,Sg,"\t",Sf,"\t #trainingdata")
    end

    function print_trainingdata_site(fp,trs,Sg,Sf)
        return

        numloops,NX,NY,NZ,NT = size(trs)
        println(fp,Sg,"\t",Sf," $numloops $NX $NY $NZ $NT")
        for it = 1:NT
            for iz = 1:NZ
                for iy = 1:NY
                    for ix=1:NX
                        print(fp,"$ix $iy $iz $it ")
                        for i=1:numloops
                            print(fp,real(trs[i,ix,iy,iz,it]),"\t",imag(trs[i,ix,iy,iz,it]),"\t")
                        end
                        println(fp,"\t")

                    end
                end
            end
        end
        #println(fp,Sg,"\t",Sf,"\t #trainingdata_site")

    end

    function run_core!(parameters,univ,mdparams,meas)

        # If an algorithm uses fermion integration (trlog(D+m)),
        # it is flagged
        if parameters.update_method == "IntegratedHMC" ||
                    parameters.update_method == "SLHMC" ||
                    parameters.update_method == "IntegratedHB" ||
                    parameters.update_method == "SLMC"
            isIntegratedFermion = true
        else
            isIntegratedFermion = false
        end
        verbose = univ.kind_of_verboselevel

        if parameters.integratedFermionAction
            loopactions = Wilsonloops_actions(univ)
            trainingfp = open(parameters.training_data_name,"w")
            trainingfp2 = open("trainset.txt","w")
            couplinglist = loopactions.couplinglist
            print(trainingfp,"# ")
            for i=1:loopactions.numloops
                print(trainingfp,"Re($(loopactions.couplinglist[i])) Im($(loopactions.couplinglist[i])) ")
            end
            println(trainingfp,"Sg Sf")
            #println(trainingfp,"#Re(plaq) Im(plaq) Re(rect) Im(rect) Re(polyx) Im(polyy) Re(polyz) Im(polyz) Re(polyt) Im(polyt) Sg Sf")
        end

        Nsteps = parameters.Nsteps
        if parameters.update_method == "Fileloading"
            Nsteps,numfiles,filename_load,ildg = run_init_Fileloading!(parameters,univ,mdparams,meas,verbose)
        elseif parameters.update_method == "SLHMC" || parameters.update_method == "SLMC"
            #slmc_data = SLMC_data(1,univ.NC)
            if typeof(univ.gparam) == GaugeActionParam_autogenerator
                slmc_data = SLMC_data(length(univ.gparam.couplinglist),univ.NC)
            else
                slmc_data = SLMC_data(1,univ.NC)
            end
        end

        numaccepts = 0

        if (parameters.Nthermalization ≤ 0 && parameters.initialtrj == 1) || parameters.update_method == "Fileloading"
            plaq,poly = measurements(0,univ.U,univ,meas;verbose = univ.kind_of_verboselevel) # check consistency of preparation.
        end

        if parameters.integratedFermionAction
            trs,Sg,Sf = calc_trainingdata(loopactions,univ)
            print_trainingdata(trs,Sg,Sf)
            print_trainingdata(trainingfp,trs,Sg,Sf)
            trs_site = calc_looptrvalues_site(loopactions,univ)
            print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
        end

        if parameters.saveU_format != nothing && parameters.update_method != "Fileloading"
            itrj = 0
            itrjstring = lpad(itrj,8,"0")
            itrjsavecount = 0
            println_verbose1(verbose,"save gaugefields U every $(parameters.saveU_every) trajectory")
            #println("save gaugefields U every $(parameters.saveU_every) trajectory")
        end

        if isIntegratedFermion
            Sfold = nothing
        end

        for itrj=parameters.initialtrj:Nsteps
            println("# itrj = $itrj")
            # Update for different updaters
            # HMC: Hybrid Monte-Carlo
            if parameters.update_method == "HMC"
                Hold = md_initialize!(univ)

                @time Hnew = md!(univ,mdparams)

                if parameters.integratedFermionAction
                    trs,Sg,Sf = calc_trainingdata(loopactions,univ)
                    print_trainingdata(trs,Sg,Sf)
                    print_trainingdata(trainingfp,trs,Sg,Sf)
                    trs_site = calc_looptrvalues_site(loopactions,univ)
                    print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
                end

                accept = metropolis_update!(univ,Hold,Hnew)
                numaccepts += ifelse(accept,1,0)
                #println("Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")
                println_verbose1(verbose,"Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")

            # Heatbath
            elseif parameters.update_method == "Heatbath"
                @time heatbath!(univ)
                if parameters.useOR
                    for ior=1:parameters.numOR
                        @time overrelaxation!(univ)
                    end
                end

                if parameters.integratedFermionAction
                    trs,Sg,Sf = calc_trainingdata(loopactions,univ)
                    print_trainingdata(trs,Sg,Sf)
                    print_trainingdata(trainingfp,trs,Sg,Sf)
                    trs_site = calc_looptrvalues_site(loopactions,univ)
                    print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
                end

            # Integrated HMC
            # HMC with S = -tr(log(D+m)), instead of the pseudo-fermins
            elseif parameters.update_method == "IntegratedHMC"
                Sgold = md_initialize!(univ)

                @time Sgnew,Sfnew,Sgold,Sfold = md!(univ,Sfold,Sgold,mdparams)
                Sold = Sgold + Sfold
                Snew = Sgnew + Sfnew

                if parameters.integratedFermionAction
                    trs,Sg,Sf = calc_trainingdata(loopactions,univ)
                    print_trainingdata(trs,Sg,Sf)
                    print_trainingdata(trainingfp,trs,Sg,Sf)
                    trs_site = calc_looptrvalues_site(loopactions,univ)
                    print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
                end

                accept = metropolis_update!(univ,Sold,Snew)
                numaccepts += ifelse(accept,1,0)
                #println("Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")
                println_verbose1(verbose,"Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")

                Sfold = ifelse(accept,Sfnew,Sfold)

            # File loading
            # This actually loads files, and performs measurements
            elseif parameters.update_method == "Fileloading"
                filename_i = filename_load[itrj+1]
                if parameters.loadU_format == "JLD"
                    loadU!(parameters.loadU_dir*"/"*filename_i,univ.U)
                elseif parameters.loadU_format == "ILDG"
                    ildg = ILDG(parameters.loadU_dir*"/"*filename_i)
                    i = 1
                    load_gaugefield!(univ.U,i,ildg,parameters.L,parameters.NC)
                elseif parameters.loadU_format == "BridgeText"
                    load_BridgeText!(parameters.loadU_dir*"/"*filename_i,univ.U,parameters.L,parameters.NC)
                end

                if parameters.integratedFermionAction
                    trs,Sg,Sf = calc_trainingdata(loopactions,univ)
                    print_trainingdata(trs,Sg,Sf)
                    print_trainingdata(trainingfp,trs,Sg,Sf)
                    trs_site = calc_looptrvalues_site(loopactions,univ)
                    print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
                end

                #loadU!(parameters.loadU_dir*"/"*filename_i,univ.U)

            # SLHMC: Self-learing Hybrid Monte-Carlo
            # This uses gluonic effective action for MD
            elseif parameters.update_method == "SLHMC"

                Sgold = md_initialize!(univ)

                @time Sgnew,Sfnew,Sgold,Sfold,plaq = md!(univ,Sfold,Sgold,mdparams)
                Sold = Sgold + Sfold
                Snew = Sgnew + Sfnew

            elseif parameters.update_method == "SLMC"
                Sgold = md_initialize!(univ)

                @time Sgnew,Sfnew,Sgeffnew,Sgold,Sfold,Sgeffold = md!(univ,Sfold,Sgold,mdparams)
                Sold = Sgold + Sfold -Sgeffold
                Snew = Sgnew + Sfnew -Sgeffnew

            elseif parameters.update_method == "IntegratedHB"
                Sgold = md_initialize!(univ)

                @time Sgnew,Sfnew,Sgeffnew,Sgold,Sfold,Sgeffold = md!(univ,Sfold,Sgold,mdparams)
                Sold = Sgold + Sfold -Sgeffold
                Snew = Sgnew + Sfnew -Sgeffnew

                if parameters.integratedFermionAction
                    trs,Sg,Sf = calc_trainingdata(loopactions,univ)
                    print_trainingdata(trs,Sg,Sf)
                    print_trainingdata(trainingfp,trs,Sg,Sf)
                    trs_site = calc_looptrvalues_site(loopactions,univ)
                    print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
                end

                accept = metropolis_update!(univ,Sold,Snew)
                numaccepts += ifelse(accept,1,0)
                #println("Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")
                println_verbose1(verbose,"Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")

                Sfold = ifelse(accept,Sfnew,Sfold)
            end

            if parameters.update_method == "SLHMC" || parameters.update_method == "SLMC"
                S2,plaqetc = calc_GaugeAction(univ)
                plaq = plaqetc
                #S2,plaq = calc_Action(univ)

                if typeof(plaqetc) == Float64
                    plaqetc = [plaq]
                end
                #println(plaq)
                #exit()
                Sf = Sfnew

                outputdata = univ.gparam.β*plaqetc[1]*slmc_data.factor + Sf

                update_slmcdata!(slmc_data,plaqetc,outputdata)
                βeffs,Econst,IsSucs = show_effbeta(slmc_data,univ.gparam)
                println("βeffs = ",βeffs)

                println_verbose1(verbose,"#S = ",outputdata)
                println_verbose1(verbose,"#Estimated Seff = ",Econst + sum(βeffs[:].*plaqetc[:])*slmc_data.factor)
                if IsSucs && itrj ≥ parameters.firstlearn
                    mdparams.βeff = βeffs[:]
                end

                if parameters.integratedFermionAction
                    trs,Sg,Sf = calc_trainingdata(loopactions,univ)
                    print_trainingdata(trs,Sg,Sf)
                    print_trainingdata(trainingfp,trs,Sg,Sf)
                    trs_site = calc_looptrvalues_site(loopactions,univ)
                    print_trainingdata_site(trainingfp2,trs_site,Sg,Sf)
                end

                accept = metropolis_update!(univ,Sold,Snew)
                numaccepts += ifelse(accept,1,0)
                println_verbose1(verbose,"Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")
                #println("Acceptance $numaccepts/$itrj : $(round(numaccepts*100/itrj)) %")
                Sfold = ifelse(accept,Sfnew,Sfold)
            end# update end

            if (itrj ≥ parameters.Nthermalization) || parameters.update_method == "Fileloading"
                plaq,poly = measurements(itrj,univ.U,univ,meas;verbose = univ.kind_of_verboselevel)
            end

            if itrj % parameters.saveU_every == 0 && parameters.saveU_format != nothing && parameters.update_method != "Fileloading"
                itrjsavecount += 1

                itrjstring = lpad(itrj,8,"0")
                #itrjstring = lpad(itrjsavecount,8,"0")
                if parameters.saveU_format == "JLD"
                    #filename = parameters.saveU_dir*"/conf_$(itrjstring).jld"
                    filename = parameters.saveU_dir*"/conf_$(itrjstring).jld2"
                    saveU(filename,univ.U)
                elseif parameters.saveU_format == "ILDG"
                    filename = parameters.saveU_dir*"/conf_$(itrjstring).ildg"
                    save_binarydata(univ.U,filename)
                elseif parameters.saveU_format == "BridgeText"
                    filename = parameters.saveU_dir*"/conf_$(itrjstring).txt"
                    save_textdata(univ.U,filename)
                else
                    error("$(parameters.saveU_format) is not supported")
                end
            end

            println_verbose1(verbose,"-------------------------------------")
            #println("-------------------------------------")
            flush(stdout)
            flush(verbose)
            if parameters.integratedFermionAction
                flush(trainingfp)
                flush(trainingfp2)
            end
        end
        if parameters.integratedFermionAction
            close(trainingfp)
        end
        return plaq
    end

end


"""
    struct DirichletFESpace <: SingleFieldFESpace
      space::SingleFieldFESpace
    end
"""
struct DirichletFESpace{B} <: SingleFieldFESpace
  space::SingleFieldFESpace
  constraint_style::Val{B}
  function DirichletFESpace(space::SingleFieldFESpace)
    cs = constraint_style(space)
    B = get_val_parameter(cs)
    new{B}(space,cs)
  end
end

constraint_style(::Type{DirichletFESpace{B}}) where B = Val{B}()

get_cell_axes(t::DirichletFESpace)= get_cell_axes(t.space)

get_cell_axes_with_constraints(t::DirichletFESpace)= get_cell_axes_with_constraints(t.space)

CellData.CellField(t::DirichletFESpace,cell_vals) = CellField(t.space,cell_vals)

get_cell_isconstrained(f::DirichletFESpace) = get_cell_isconstrained(f.space)

get_cell_constraints(f::DirichletFESpace) = get_cell_constraints(f.space)

function num_free_dofs(f::DirichletFESpace)
  num_dirichlet_dofs(f.space)
end

function zero_free_values(f::DirichletFESpace)
  zero_dirichlet_values(f.space)
end

function get_cell_dofs(f::DirichletFESpace)
  apply(elem(-),get_cell_dofs(f.space))
end

function num_dirichlet_dofs(f::DirichletFESpace)
  num_free_dofs(f.space)
end

function zero_dirichlet_values(f::DirichletFESpace)
  zero_free_values(f.space)
end

function num_dirichlet_tags(f::DirichletFESpace)
  1
end

function get_dirichlet_dof_tag(f::DirichletFESpace)
  ones(Int8,num_dirichlet_dofs(f))
end

function scatter_free_and_dirichlet_values(f::DirichletFESpace,fv,dv)
  scatter_free_and_dirichlet_values(f.space,dv,fv)
end

function gather_free_and_dirichlet_values!(fv,dv,f::DirichletFESpace,cv)
  gather_free_and_dirichlet_values!(dv,fv,f.space,cv)
  (fv,dv)
end

function TrialFESpace(f::DirichletFESpace)
  U = TrialFESpace(f.space)
  DirichletFESpace(U)
end

function get_cell_basis(f::DirichletFESpace)
  get_cell_basis(f.space)
end

function get_cell_dof_basis(f::DirichletFESpace)
  get_cell_dof_basis(f.space)
end


"""
$(TYPEDEF)

A `Storage` can be used to store simulation results for a [`Mechanism`](@ref).

Indexing example:
    storage.x[body_id][time_step_number][2] # returns the y-position of the specified body at the specified time step.

# Important attributes
* `x`: Contains the position for each body and each time step.
* `q`: Contains the orientation for each body and each time step.
* `v`: Contains the velocity for each body and each time step.
* `ω`: Contains the angluar velocity for each body and each time step.

# Constructors
    Storage{T}(step_range, nbodies)
    Storage(step_range, nbodies)
    Storage{T}(nsteps, nbodies)
    Storage(nsteps, nbodies)
"""
struct Storage{T,N}
    x::Vector{Vector{SVector{3,T}}}
    q::Vector{Vector{UnitQuaternion{T}}}
    v::Vector{Vector{SVector{3,T}}}
    ω::Vector{Vector{SVector{3,T}}}

    function Storage{T}(steps, nbodies) where T
        x = [[szeros(T, 3) for i = steps] for j = 1:nbodies]
        q = [[one(UnitQuaternion{T}) for i = steps] for j = 1:nbodies]
        v = [[szeros(T, 3) for i = steps] for j = 1:nbodies]
        ω = [[szeros(T, 3) for i = steps] for j = 1:nbodies]
        new{T,length(steps)}(x, q, v, ω)
    end

    Storage(steps, nbodies) = Storage{Float64}(steps, nbodies)
    Storage{T}(nsteps::Integer, nbodies) where T = Storage{T}(Base.OneTo(nsteps), nbodies)
    Storage(nsteps::Integer, nbodies) = Storage{Float64}(nsteps, nbodies)

    function Storage(x::Vector{<:Vector{<:AbstractVector{T}}},q::Vector{Vector{UnitQuaternion{T}}}) where T
        steps = Base.OneTo(length(x[1]))
        nbodies = length(x)

        v = [[szeros(T, 3) for i = steps] for j = 1:nbodies]
        ω = [[szeros(T, 3) for i = steps] for j = 1:nbodies]

        new{T,length(steps)}(x, q, v, ω)
    end

    Storage{T}() where T = Storage{T}(Base.OneTo(0),0)
end

function Base.show(io::IO, mime::MIME{Symbol("text/plain")}, storage::Storage{T,N}) where {T,N}
    summary(io, storage)
end


using BHAtp, Statistics

ProjDir = @__DIR__
!isdir(joinpath(ProjDir, "plots")) && mkdir(joinpath(ProjDir, "plots"))

ProjName = split(ProjDir, "/")[end]
bhaj = BHAtp.BHAJ(ProjName, ProjDir)
bhaj.ratio = 2.3

segs = [
	# Element type,  Material,    Length,     ID,   OD,    fc
	(:bit,           :steel,        0.00,   2.000, 17.500,  0.0),
  (:collar,        :steel,        1.44,   2.000,  9.500,  0.0),
  (:collar,        :steel,        3.00,   3.000,  9.500,  0.0),
  (:collar,        :steel,        1.24,   2.938,  9.500,  0.0),
	(:stabilizer,    :steel,        0.00,   2.938, 17.500,  0.0),
  (:collar,        :steel,        4.04,   2.938,  9.500,  0.0),
	(:collar,        :monel,       37.45,   2.875,  9.500,  0.0),
  (:collar,        :monel,       27.09,   3.000,  9.625,  0.0),
  (:collar,        :monel,       30.01,   2.875,  9.500,  0.0),
  (:collar,        :steel,        1.87,   3.000,  9.500,  0.0),
  (:stabilizer,    :steel,        0.00,   3.000, 17.500,  0.0),
  (:collar,        :steel,        3.73,   3.000,  9.500,  0.0),
	(:collar,        :steel,       28.73,   2.875,  9.500,  0.0),
  (:collar,        :steel,        4.22,   3.000,  9.125,  0.0),
  (:collar,        :steel,       27.98,   2.750,  7.875,  0.0),
  (:collar,        :steel,       29.45,   2.875,  7.563,  0.0),
  (:collar,        :steel,       30.87,   2.875,  7.625,  0.0),
  (:collar,        :steel,       31.10,   2.750,  8.000,  0.0),
  (:collar,        :steel,       32.18,   3.000,  8.000,  0.0),
  (:collar,        :steel,       30.88,   2.750,  7.938,  0.0),
  (:collar,        :steel,        3.50,   2.750,  8.000,  0.0)
]

traj = [
	#Heading,  Diameter
	( 60.0,      17.5)
]

wobs = 30:5:40
incls = 30:5:60  # Or e.g. incls = [5 10 20 30 40 45 50]

@time bhaj(segs, traj, wobs, incls)

display("Fetch tp=false, wob=35, incl@bit=30 solution:")
df,df_tp = show_solution(ProjDir, 35, 30, show=false, tp=false)
df[:,[2; 5:6; 9:12]] |> display
println()

println("Fetch tp=true, wob=35, incl@bit=30 solution:")
df,df_tp = show_solution(ProjDir, 35, 30, show=false);
df[:,[2; 5:6; 9:12]] |> display
println()

tpf = create_final_tp_df(ProjDir, wobs, incls; ofu=false)
tpf |> display
println()

# Collect computed side forces on NBS @ 45ft

sfs = []
for wob in wobs
  for incl in incls
    df, df_tp = show_solution(ProjDir, wob, incl, show=false);
    sf = @linq df |> where(:dtb .== 5.68)
    append!(sfs, sf.fx)
  end
end

# Actual tps

actual = DataFrame(
  incls = [40.0, 42.3, 44.0, 44.65, 45.7, 47.25, 49.5],
  atp = [2.55, 2.65, 0.4, 1.15, 1.18, 2.15, 2.35],
)

rpm = DataFrame(
  incls = [35, 43.5,43.6,44.2,44.3,44.9, 45, 48,48.1, 55],
  rpm = [ 100,  100,  60,  60,  70,  70, 80, 80, 120,120]
)

name = "figure_03"

figure_03 = @pgf TikzPicture(
  Axis({
      height = "6cm", width = " 8cm",
      xmin = 30, xmax = 90,
      ymin = 0, ymax = 6.5,
      axis_y_line = "left",
      xlabel = "Inclination [°]",
      ylabel = "TP [°/100ft]",
      title = "Figure 3: Actual vs. Isotropic TP",
      legend_pos = "south east",
    },
    Plot({ color="blue", mark = "." },
      Table(tpf.incl[1:7], tpf.tp[1:7])),
    LegendEntry("TP wob=30"),
    Plot({ color="gray", mark = "." },
      Table(tpf.incl[8:14], tpf.tp[8:14])),
    LegendEntry("TP wob=35"),
    Plot({ color="green", mark = "." },
      Table(tpf.incl[15:21], tpf.tp[15:21])),
    LegendEntry("TP wob=40"),
    Plot({ thick, color="black", mark = "+" },
      Table(actual.incls, actual.atp)),
    LegendEntry("TP, actual")),
  Axis({
      height = "6cm", width = " 8cm",
      ymin = 0, ymax = 160,
      xmin = 30, xmax = 90,
      hide_x_axis,
      axis_y_line = "right",
      ylabel = "RPM [revs/min]",
      legend_pos = "north east",
    },
    Plot({ color="red", mark = "." },
      Table(rpm.incls, rpm.rpm)),
    LegendEntry("RPM")),
)

savefig(joinpath(ProjDir, "plots", name), figure_03, ProjDir)


module AbstractGPs

using Distributions
using FillArrays
using LinearAlgebra
using PDMats: AbstractPDMat, ScalMat
using Reexport
@reexport using KernelFunctions
using Random
using Statistics
using StatsBase
using RecipesBase
using IrrationalConstants: log2π

using KernelFunctions: ColVecs, RowVecs

using ChainRulesCore: ChainRulesCore

export GP,
    rand!,
    mean,
    cov,
    var,
    std,
    mean_and_cov,
    mean_and_var,
    marginals,
    logpdf,
    elbo,
    dtc,
    posterior,
    VFE,
    DTC,
    update_posterior,
    LatentGP,
    ColVecs,
    RowVecs

# Various bits of utility functionality.
include("util/common_covmat_ops.jl")

# AbstractGP interface.
include("abstract_gp.jl")

# FiniteGP object that describes the projection of a GP at points x.
include("finite_gp_projection.jl")

# Basic GP object, e.g. to define the prior.
include("mean_function.jl")
include("base_gp.jl")

# Efficient exact posterior GP implementation.
include("exact_gpr_posterior.jl")

# Approximate sparse GP inference for Gaussian likelihood.
include("sparse_approximations.jl")

# LatentGP and LatentFiniteGP objects to accommodate GPs with non-Gaussian likelihoods.
include("latent_gp.jl")

# Plotting utilities.
include("util/plotting.jl")

# Testing utilities.
include("util/TestUtils.jl")

# Deprecations.
include("deprecations.jl")

end # module


"""
**SegyToSeis**

*Convert SEGY or SU data to seis format.*

**IN**

* filename_in
* filename_out
* format="segy" (or "su")
* swap_bytes=true (flag to swap bytes)
* input_type="ibm" (or "ieee")

**OUT**

*Credits: AS, 2015*

"""
function SegyToSeis(filename_in,filename_out;format="segy",swap_bytes=true,input_type="ibm")

	if (format=="su")
		file_hsize = 0
	else
		file_hsize = 3600
		# add commands here to read text and binary headers and write them out to
		# filename_out.thead and filename_out.bhead
		stream     = open(filename_in)
		position = 3200
		seek(stream, position)
		fh = GrabFileHeader(stream)
		ntfh = swap_bytes == true ? bswap(fh.netfh) : fh.netfh
		if ntfh == -1
			error("add instructions to deal with variable extended text header")
		end
		if ntfh == 0
			file_hsize = 3600
		elseif ntfh > 0
			# file_hsize = 3200 * (ntfh+1) + 400
			file_hsize = 3200 * 1 + 400
		else
			error("unknown data format")
		end
	end
	stream = open(filename_in)
	seek(stream, segy_count["ns"] + file_hsize)
	if (swap_bytes==true)
		nt = bswap(read(stream,Int16))
	else
		nt = read(stream,Int16)
	end
	total = 60 + nt
	nx = round(Int,(filesize(stream)-file_hsize)/4/total)
	println("number of traces: ",nx)
	println("number of samples per trace: ",nt)
	h_segy = Array{SegyHeader}(undef,1)
	h_seis = Array{Header}(undef,1)
	seek(stream,file_hsize + segy_count["trace"])
	h_segy[1] = GrabSegyHeader(stream,swap_bytes,nt,file_hsize,1)
	dt = h_segy[1].dt/1000000
	extent = Extent(convert(Int32,nt),convert(Int32,nx),convert(Int32,1),convert(Int32,1),convert(Int32,1),
		   convert(Float32,0),convert(Float32,1),convert(Float32,0),convert(Float32,0),convert(Float32,0),
		   convert(Float32,dt),convert(Float32,1),convert(Float32,1),convert(Float32,1),convert(Float32,1),
		   "Time","Trace Number","","","",
		   "s","index","","","",
		   "")
	for j=1:nx
		position = file_hsize + total*(j-1)*4 + segy_count["trace"]
		seek(stream,position)
		if (input_type == "ieee")
			d = Array{Float32}(undef,nt);
			read!(stream,d)
		else
			d = Array{IBMFloat32}(undef,nt);
			read!(stream,d)
		end
		if (swap_bytes==true && input_type == "ieee")
			d = bswap_vector(d)
		end
		if (input_type != "ieee")
			d = convert(Array{Float32,1},d)
		end
		h_segy[1] = GrabSegyHeader(stream,swap_bytes,nt,file_hsize,j)
		h_seis[1] = MapHeaders(h_segy,j,"SegyToSeis")
		SeisWrite(filename_out,d,h_seis,extent,itrace=j)
	end
	close(stream)

end

function bswap_vector(a)
	for i = 1 : length(a)
		a[i] = bswap(a[i]);
	end
	return a
end


@testset "Simulation Unit Tests:" begin

    s = Simulation(test_model, test_driver)

    @test isa(trace(s), BlockOptTrace)

    @test isa(backend(s), BlockOptBackend)

    @testset "inhereted methods" begin

        @test isa(trs_timer(s), EvaluationTimer)

        @test Δt(trs_timer(s)) ≡ 0.0

        @test isa(trs_counter(s), EvaluationCounter)

        @test evaluations(trs_counter(s)) ≡ 0

        @test isa(ghs_timer(s), EvaluationTimer)

        @test Δt(ghs_timer(s)) ≡ 0.0

        @test isa(ghs_counter(s), EvaluationCounter)

        @test evaluations(ghs_counter(s)) ≡ 0

        @test weave!(s, :f_vals, 0.0) ≈ [0.0]

        @test weave!(s, :∇f_norms, 1.0) ≈ [1.0]

        @test weave!(s, :Δ_vals, 2.0) ≈ [2.0]

        @test weave!(s, :p_norms, 3.0) ≈ [3.0]

        @test weave!(s, :ρ_vals, 4.0) ≈ [4.0]

        @test pop!(f_vals(s)) ≈ 0.0

        @test pop!(∇f_norms(s)) ≈ 1.0

        @test pop!(Δ_vals(s)) ≈ 2.0

        @test pop!(p_norms(s)) ≈ 3.0

        @test pop!(ρ_vals(s)) ≈ 4.0

        @test weave!(s, f_vals, 0.0) ≈ [0.0]

        @test weave!(s, ∇f_norms, 1.0) ≈ [1.0]

        @test weave!(s, Δ_vals, 2.0) ≈ [2.0]

        @test weave!(s, p_norms, 3.0) ≈ [3.0]

        @test weave!(s, ρ_vals, 4.0) ≈ [4.0]

        @test weave_level(s) ≡ weave_level(test_driver)

        @test fₖ(s) ≈ ∇fₖ_norm(s) ≈ pₖ_norm(s) ≈ Δₖ(s) ≈ ρ(s) ≈ 0.0

        @test log_level(s) ≡ log_level(test_driver)

        @test isfile(io(s))

        info!(s, "This is an info! message.")

        debug!(s, "This is an debug! message.")

        warn!(s, "This is an warn! message.")

        error!(s, "This is an error! message.")

        # Currently, the statements below print to console. Fix.

        # info!(s,  "Simulation initilaized by:\\n", test_model, test_driver)

        # info!(s,  "Simulation initilized trace and backend:\\n", trace(s), backend(s))

        restricted_type_test(s)
    end

    @testset "edge case testing" begin

    end

    nothing
end

@testset "Simulation Behavior Tests:" begin

    max_iterations!(test_driver, 4)

    s = Simulation(test_model, test_driver)

    @testset "initialize subroutine" begin

        @test evaluations(ghs_counter(s)) ≡ 0

        initialize(s)

        @test evaluations(ghs_counter(s)) ≡ 1
    end

    i = 0

    while !terminal(s)

        build_trs(s)

        @testset "solve_trs subroutine" begin

            k = evaluations(trs_counter(s))

            elapsed = Δt(trs_timer(s))

            solve_trs(s)

            @test evaluations(trs_counter(s)) ≡ k + 1

            @test Δt(trs_timer(s)) > elapsed
        end

        build_trial(s)

        successful_trial = accept_trial(s)

        @testset "accept_trial subroutine" begin

            if successful_trial

                @test last(Δ_vals(s)) ≈ Δₖ(s)

                @test last(ρ_vals(s)) ≈ ρ(s)

                @test last(p_norms(s)) ≈ pₖ_norm(s)
            end

            @test last(f_vals(s)) ≈ fₖ(s) # always holds

            @test last(∇f_norms(s)) ≈ ∇fₖ_norm(s) # always holds
        end

        successful_trial && pflag(s) && secantQN(s)

        successful_trial && update_Sₖ(s)

        successful_trial && @testset "gHS subroutine" begin

            cost = evaluations(ghs_counter(s))

            elapsed = Δt(ghs_timer(s))

            ∇f_norm = ∇fₖ_norm(s)

            gHS(s)

            @test !(last(∇f_norms(s)) ≈ ∇f_norm)

            @test evaluations(ghs_counter(s)) ≡ cost + 1

            @test Δt(ghs_timer(s)) > elapsed
        end

        successful_trial && blockQN(s)

        update_Δₖ(s)

        i += 1
    end

    @test last(∇f_norms(s)) ≈ ∇fₖ_norm(s)

    @test last(f_vals(s)) ≈ fₖ(s)

    if max_iterations(backend(s)) > evaluations(trs_counter(s)) # implying convergence

        @test last(Δ_vals(s)) ≈ Δₖ(s)

        @test last(ρ_vals(s)) ≈ ρ(s)

        @test last(p_norms(s)) ≈ pₖ_norm(s)

        @test ∇fₖ_norm(s) < ϵ_tol(backend(s))
    end

    return nothing
end


"""
`PSSFSS` is a software package for the analysis of polarization and frequency selective surfaces (PSSs and FSSs).
The user specifies the geometry to be analyzed as a `Vector` containing two or more dielectric [`Layer`](@ref)s
and zero or more [`Sheet`](@ref) objects denoting the PSS/FSS surfaces.  After also specifying the scan angles or
unit cell incremental phasings, frequencies to be considered, and optionally some output parameters to be written
to CSV file(s),
the user then invokes the [`analyze`](@refs) function to perform the analysis.  FSS/PSS triangulations
can be conveniently visualized using the `plot` command of the `Plots` package.

"""
module PSSFSS

if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel"))
    @eval Base.Experimental.@optlevel 3
end

using Reexport
using PkgVersion
using InteractiveUtils: versioninfo
using Dates: now
using DelimitedFiles: writedlm
using Printf: @sprintf
using LinearAlgebra: ×, norm, ⋅, factorize, lu!, ldiv!
using StaticArrays: StaticArrays, SVector, SArray, @SVector
using Unitful: ustrip, @u_str
using Logging: with_logger
using ProgressMeter

include("Constants.jl")
include("Log.jl")
include("PSSFSSLen.jl")
include("Rings.jl")
include("Layers.jl")
include("Sheets.jl")
include("Meshsub.jl")
include("Elements.jl")
include("RWG.jl")
include("PGF.jl")
include("Zint.jl")
include("FillZY.jl")
include("GSMs.jl")
include("Modes.jl")
include("Outputs.jl")

using .Rings
@reexport using .Sheets: Sheet, RWGSheet, read_sheet_data
using .RWG: setup_rwg, rwgbfft!, RWGData
using .GSMs: GSM, cascade, cascade!, gsm_electric_gblock, gsm_magnetic_gblock,
             gsm_slab_interface, translate_gsm!, choose_gblocks, Gblock, pecgsm, pmcgsm
using .FillZY: fillz, filly
using .Modes: zhatcross, choose_layer_modes!, setup_modes!
using .Constants: twopi, c₀, tdigits, dbmin
using .Log: pssfss_logger, @logfile
@reexport using .PSSFSSLen
@reexport using .Layers: Layer
@reexport using .Elements: rectstrip, diagstrip, polyring, meander, loadedcross,
                           jerusalemcross, pecsheet, pmcsheet
@reexport using .Outputs: @outputs, extract_result_file, extract_result
using .Outputs: Result, append_result_data

export analyze

Base.isfile(f::Base.DevNull) = false
Base.open(f::Base.DevNull,::AbstractString) = f

"""
    analyze(strata::Vector, flist, steering; outlist=[], logfile="pssfss.log", resultfile="pssfss.res", showprogress::Bool=true)

Analyze a full FSS/PSS structure over a range of frequencies and steering angles/phasings.
Generate output files as specified in `outlist`.

## Positional Arguments
- `strata`:  A vector of `Layer` and `Sheet` objects. The first and last entries must be of type `Layer`.

- `flist`: An iterable containing the analysis frequencies in GHz.

- `steering`: A length 2 `NamedTuple` containing as keys the steering parameter labels and as values
  the iterables that define the values of steering parameters to be analyzed.

    - one of {`:phi` ,`:ϕ`} and one of {`:theta`, `:θ`}, or

    - one of {`:psi1` ,`:ψ₁`} and one of {`:psi2`, `:ψ₂`}.

  All steering parameters are input in degrees.

  The program will analyze while iterating over a triple loop over the two steering
  parameters and frequency, with frequency in the innermost loop (i.e. varying the fastest).
  The steering parameter listed first will be in the outermost loop and will therefore
  vary most slowly.

## Keyword Arguments

- `outlist`:  A matrix with 2 columns.  The first column in each row is a string
  containing the name of the CSV file to write the output to.  The second entry in
  each row is a tuple generated by the `@outputs` macro of the `Outputs` module. The
  contents of the specified file(s) will be updated as the program completes each analysis
  frequency.

- `logfile`:  A string containing the name of the log file to which timing and other
  information about the run is written. Defaults to `"pssfss.log"`.
  If this file already exists, it will be overwritten.

- `resultfile`:  A string containing the name of the results file. Defaults to `pssfss.res`.
  If this file already exists, it will be overwritten.  It is a binary
  file that contains information (including the generalized scattering matrix) from
  the analysis performed for each scan condition and frequency. The result file can be
  post-processed to produce similar or additional outputs that were requested at run time
  via the `outlist` argument.

- `showprogress`: If true (default), then show progress bar during execution.

"""
function analyze(strata::Vector, flist, steering; outlist=[], logfile="pssfss.log",
    resultfile="pssfss.res", showprogress::Bool=true)
    layers = Layer[deepcopy(s) for s in strata if s isa Layer]
    sheets = RWGSheet[s for s in strata if s isa Sheet]
    islayer = map(x -> x isa Layer, strata)
    issheet = map(x -> x isa RWGSheet, strata)
    nl = length(layers)
    nj = nl - 1
    ns = length(sheets)
    sint = cumsum(islayer)[issheet] # sint[k] contains dielectric interface number of k'th sheet
    junc = zeros(Int, nj)
    junc[sint] = 1:ns #  junc[i] is the sheet number present at interface i, or 0 if no sheet is there
    freqstemp = float.(collect(flist))
    if length(freqstemp) < 2
        freqs = Float64[freqstemp]
    else
        freqs::Vector{Float64} = freqstemp
    end

    stkeys::Tuple{Symbol, Symbol} = keys(steering)
    stvaluestemp = [float.(collect(s)) for s in steering]
    stvalues = Vector{Float64}[]
    for (i,s) in pairs(stvaluestemp)
        if length(s) < 2
            push!(stvalues, Float64[s])
        else
            push!(stvalues, s)
        end
    end

    with_logger(pssfss_logger(logfile)) do
        _analyze(layers, sheets, junc, freqs, stkeys, stvalues;
                 outlist, resultfile, showprogress)
    end
end # function

"""
function _analyze(layers, sheets, junc, freqs, stkeys, stvalues;
    outlist=Any[], resultfile="pssfss.res", showprogress::Bool=true)

## Positional Arguments
- `layers`:  A vector of `Layer` objects.

- `sheets`:  A vector of `RWGSheet` objects.

- `junc`: `junc[k]` contains the sheet number present at layer interface `k` or `0` if no sheet
  is present there.

- `freqs`: A vector containing the analysis frequencies in GHz.

- `stkeys`: A length 2 `Tuple` containing as the steering angle labels as `Symbols` Either

    - one of {`:phi` ,`:ϕ`} and one of {`:theta`, `:θ`}, or

    - one of {`:psi1` ,`:ψ₁`} and one of {`:psi2`, `:ψ₂`}.

  The angular steering parameters are input in degrees, while the incremental phase shift
  parameters are input in radians.

  The program will analyze while iterating over a triple loop over the two steering
  parameters and frequency, with frequency in the innermost loop (i.e. varying the fastest).
  The steering parameter listed first will be in the outermost loop and will therefore
  vary most slowly.

## Keyword Arguments

- `outlist`:  A matrix with 2 columns.  The first column in each row is a string
  containing the name of the CSV file to write the output to.  The second entry in
  each row is a tuple generated by the `@outputs` macro of the `Outputs` module. The
  contents of the specified file(s) will be updated as the program completes each analysis
  frequency.

- `resultfile`:  A string containing the name of the results file. Defaults to `pssfss.res`.
  If this file already exists, it will be overwritten.  It is a binary
  file that contains information (including the generalized scattering matrix) from
  the analysis performed for each scan condition and frequency. The result file can be
  post-processed to produce similar or additional outputs that were requested at run time
  via the `outlist` argument.

- `showprogress`: If true, use ProgressMeter to show execution progress
"""
function _analyze(layers, sheets, junc, freqs, stkeys, stvalues;
    outlist=[], resultfile="pssfss.res", showprogress::Bool=true)
    showprogress && println("Beginning PSSFSS Analysis")
    ncount = 0 # Number of analyses performed
    ntotal = length(freqs) * length(stvalues[1]) * length(stvalues[2])
    showprogress && (progress = Progress(ntotal,1))
    showprogress && update!(progress, 0)
    isfile(resultfile) && rm(resultfile)
    date, clock = split(string(now()),'T')
    pssfssv = PkgVersion.Version(PSSFSS)
    ss = "Environment"
    io=IOBuffer(); versioninfo(io); juliainfo = String(take!(io)) * ss
    i = findfirst(ss, juliainfo)
    juliainfo = juliainfo[1:first(i)-1]
    juliainfo = juliainfo * "  Threads.nthreads() = $(Threads.nthreads())\n"
    @logfile "\n\nStarting PSSFSS $(pssfssv) analysis on $(date) at $(clock)\n$(juliainfo)\n\n"
    check_inputs(layers, sheets, junc, freqs, stkeys, stvalues, outlist)
    k0min, k0max = twopi*1e9/c₀ .* extrema(freqs)
    gbls = choose_gblocks(layers, sheets, junc, k0min)
    gsm_save = Vector{GSM}(undef, length(gbls)) # Storage for reusable GSMs
    choose_layer_modes!(layers, sheets, junc, gbls, k0max, dbmin)
    gbldup = get_gbldup(gbls, layers, sheets, junc)
    usi = unique_indices(sheets)
    rwgdat = RWGData[setup_rwg(sheet) for sheet in sheets]
    report_layers_sheets(layers, sheets, junc, rwgdat, usi)
    uvec::Vector{Float64} = map(sheets) do sh  # Green's function smoothing factors
        sh.style == "NULL" && (return 0.0)
        ufactor = 0.5 * ustrip(Float64, sh.units, 1u"m")
        ufactor * max(norm(sh.β₁), norm(sh.β₂))
    end
    # Begin analysis loops over steering angles and frequency
    firstoutput = true
    results = Result[]
    for stout in stvalues[1], stin in stvalues[2]
        steer = getsttuple(stkeys, stout, stin)
        if keys(steer)[1] == :ψ₁
            ψ₁, ψ₂ = deg2rad.([steer...]) # radians
            upm::Float64 = ustrip(Float64, sheets[1].units, 1u"m")
            β₁, β₂ = sheets[1].β₁*upm, sheets[1].β₂*upm
            β⃗₀₀ = (ψ₁*β₁ + ψ₂*β₂) / twopi # Eq. (2.13b)
        else
            θ, ϕ = steer # degrees
            st = sind(θ)
            sp, cp = sincosd(ϕ)
        end
        @logfile "Beginning $(steer)"
        for fghz in freqs
            @logfile "  $(fghz) GHz"
            t_freq = time()
            k0 = twopi*fghz*1e9/c₀
            if keys(steer)[1] == :θ
                k1 = k0 * sqrt(real(layers[1].ϵᵣ * layers[1].μᵣ))
                β⃗₀₀ = @SVector([k1*st*cp, k1*st*sp])
            end
            setup_modes!.(layers, k0, Ref(β⃗₀₀))
            if !(angle(layers[begin].γ[1]) ≈ angle(layers[end].γ[1]) ≈ π/2)
                @logfile "  Skipping $(fghz) GHz due to cutoff principal modes in ambient medium"
                continue
            end
            # Initialize overall GSM and propagate it through layer 1's width:
            n1 = length(layers[1].P)
            gsma = GSM(n1,n1)
            gsmc = GSM(n1,n1)
            cascade!(gsma, layers[1])
            for (ig,gbl) in pairs(gbls) # Walk through the Gblocks
                i1 = first(gbl.rng) # Index of layer to left of Gblock
                i2 = 1 + last(gbl.rng) # Index of layer to right of Gblock
                i_junc = gbl.j # junction where FSS is located, or 0 if no sheet
                i_sheet = i_junc == 0 ? 0 : junc[i_junc]
                if i_sheet ≠ 0
                    if gbldup[ig] > 0
                        gsmb = deepcopy(gsm_save[gbldup[ig]]) # Use previously calculated GSM
                    else
                        region = @view layers[i1:i2]
                        sheet = sheets[i_sheet]
                        s = gbl.j - i1 + 1 # sheet interface location within `region`
                        if sheet.class == 'J'
                            gsmb = calculate_jtype_gsm(region, sheet, uvec[i_sheet],
                                                       rwgdat[i_sheet], s, k0, β⃗₀₀, usi[i_sheet])
                        elseif sheet.class == 'M'
                            gsmb = calculate_mtype_gsm(region, sheet, uvec[i_sheet],
                                                       rwgdat[i_sheet], s, k0, β⃗₀₀, usi[i_sheet])
                        elseif sheet.class == 'E'
                            gsmb = pecgsm(length(region[1].P), length(region[end].P))
                        elseif sheet.class == 'H'
                            gsmb = pmcgsm(length(region[1].P), length(region[end].P))
                        else
                            error("Illegal sheet class: $(sheet.class)")
                        end

                        gbldup[ig] < 0 && (gsm_save[ig] = deepcopy(gsmb))
                    end
                    # Apply translations if requested:
                    sheet = sheets[i_sheet]
                    if sheet.dx ≠ 0 || sheet.dy ≠ 0
                        upm = ustrip(Float64, sheet.units, 1u"m")
                        dx = sheet.dx / upm
                        dy = sheet.dy / upm
                        translate_gsm!(gsmb, dx, dy, first(region), last(region))
                    end

                else # no sheet
                    @assert i2 - i1 == 1
                    gsmb = gsm_slab_interface(layers[i1], layers[i2], k0)
                end
                gsmc = cascade(gsma, gsmb)
                cascade!(gsmc, layers[i2])
                gsma = gsmc
            end # Gblock loop
            t_freq = round(time()-t_freq, digits=tdigits)
            @logfile "  $(t_freq) seconds total at $(fghz) GHz"

            result = Result(gsmc, steer, β⃗₀₀, fghz, layers[1].ϵᵣ, layers[1].μᵣ,
            layers[1].β₁, layers[1].β₂, layers[end].ϵᵣ, layers[end].μᵣ,
            layers[end].β₁, layers[end].β₂)
            push!(results, result)
            ncount += 1
            # Write to output files
            append_result_data(resultfile,string(ncount),result)
            for row in eachrow(outlist)
                if firstoutput
                    open(row[1], "w") do io
                        writedlm(io, permutedims([r.label for r in row[2]]), ',')
                    end
                end
                open(row[1], "a") do io
                    writedlm(io, permutedims([r(result) for r in row[2]]), ',')
                end
            end
            firstoutput = false
            showprogress && next!(progress) # Bump progress meter
        end # Frequency loop
    end # steering angle loop

    date, clock = split(string(now()),'T')
    @logfile "\n\n PSSFSS analysis exiting on $(date) at $(clock)\n\n"
    return results
end # function

"""
    calculate_jtype_gsm(layers, sheet::RWGSheet, u::Real, rwgdat::RWGData, s::Int, k0, k⃗inc, is::Int) -> gsm

Compute the generalized scattering matrix for a sheet of class `'J'`.

### Input Arguments

- `layers`: An iterable of `Layer` instances containing the layers for the `Gblock`
    associated with the `sheet` under consideration.
- `sheet`:  A sheet of class `'J'` for which the GSM is desired.
- `u`: The Green's function smoothing parameter for the `sheet`.
- `rwgdat`: The `RWGData` object associated with the `sheet` argument.
- `s`: The interface number within `layers` at which the sheet is located.
- `k0`: The free-space wavenumber in radians/meter.
- `k⃗inc`: A 2-vector containing the incident field wave vector x and y components. Note
    that by the phase match condition this vector is the same for all layers in the entire FSS
    structure.
- `is`: The sheet index for the `sheet` argument within the global list of sheets.

### Return Value

- `gsm::GSM`  The full GSM for the GBlock including incident fields and scattered fields due
    to currents induced on the sheet surface.
"""
function calculate_jtype_gsm(layers::AbstractVector{Layer}, sheet::RWGSheet, u::Float64,
                                 rwgdat::RWGData, s::Int, k0::Float64, k⃗inc::SVector{2, Float64}, is::Int)
    one_meter = ustrip(Float64, sheet.units, 1u"m")
    area = norm(sheet.s₁ × sheet.s₂) / one_meter^2 # Unit cell area (m^2).
    nmodesmax = max(length(layers[begin].P), length(layers[end].P))
    nbf = size(rwgdat.bfe,2) # Number of basis functions
    if isempty(rwgdat.bfftstore)
        rwgdat.bfftstore = zeros(SArray{Tuple{2},ComplexF64,1,2}, (nbf, 2, nmodesmax))
    end
    bfftstore = rwgdat.bfftstore
    # Compute area correction factors for the mode normalization constants of
    # the two end regions:
    tempvec = zeros(2)
    for (i,l) in enumerate(@view layers[[begin,end]])
      area_i = twopi * twopi / norm(l.β₁ × l.β₂)
      tempvec[i] = √(area_i / area)
    end
    acf = SVector(tempvec[1], tempvec[2])

    # Set up the partial GSM due to incident field
    (gsm, tlgfvi, vincs) = gsm_electric_gblock(layers, s, k0)
    if sheet.style == "NULL"
        return gsm
    end

    # Calculate the scattered field partial scattering matrix of the
    # FSS sheet at this junction. Then add it to GSM already computed
    # for the dielectric discontinuity...

    #  Fill the interaction matrix for the current sheet:
    t_temp = time()
    ψ₁ = k⃗inc ⋅ sheet.s₁ / one_meter
    ψ₂ = k⃗inc ⋅ sheet.s₂ / one_meter
    @logfile "    Beginning matrix fill for sheet $(is)"
    zmat = fillz(k0,u,layers,s,ψ₁,ψ₂,sheet,rwgdat)
    t_fill = round(time() - t_temp, digits=tdigits)
    @logfile "      $(t_fill) seconds total matrix fill time for sheet $(is)"
    # Factor the matrix:
    t_temp = time()
    zmatf = lu!(zmat)
    t_factor = round(time() - t_temp, digits=tdigits)
    @logfile "      $(t_factor) seconds to factor matrix for sheet $(is)"
    t_temp = time()
    # Compute and store the basis function Fourier transforms:
    i_ft = 0
    for (sr,l) in enumerate(@view layers[[begin,end]]) # loop over possible source regions
        for qp = 1:length(l.P)
            kvec = l.β[qp]
            # If desired F.T. has already been computed, then copy it.
            if qp > 1 && kvec ≈ l.β[qp-1]
                bfftstore[:,sr,qp] = bfftstore[:,sr,qp-1]
                continue
            end
            if sr == 2 && length(layers[1].β) ≥ qp && kvec ≈ layers[1].β[qp]
                bfftstore[:,2,qp] = bfftstore[:,1,qp]
                continue
            end
            bfft = @view bfftstore[:,sr,qp]
            rwgbfft!(bfft, rwgdat, sheet, kvec, ψ₁, ψ₂) # Otherwise, compute from scratch
            i_ft += 1
        end
    end
    t_fft = round(time() - t_temp, digits=tdigits)
    #@logfile "      $(t_fft) seconds for basis function Fourier transforms at $(i_ft) points"
    nsolve = 0
    t_extract = 0.0
    i_extract = 0
    t_solve = 0.0
    imat = rwgdat.rhs
    for (sr,ls) in enumerate(@view layers[[begin,end]]) # Loop over source regions
        for qp in 1:length(ls.P) # Loop over srce reg modes
            # Incident field for source layer in absence of the FSS sheet:
            sourcevec = vincs[qp,sr] * ls.c[qp] * acf[sr] * ls.tvec[qp]
            # Compute generalized voltage vector:
            imat .= (b ⋅ sourcevec for b in bfftstore[:,sr,qp]) # Eq. (7.39)
            # Solve the matrix equation
            t_solve1 = time()
            ldiv!(zmatf, imat)
            t_solve2 = time()
            t_solve += t_solve2 - t_solve1
            nsolve += 1
            t_extract1 = time()
            for (or,lo) in enumerate(@view layers[[begin,end]]) # Loop over obs. regions
                smat = gsm[or,sr]
                for q in 1:length(lo.P)  # Loop obs. regn modes
                    # Extract partial scattering parameter due to scattered fields...
                    FTJ = sum((imat[n] * bfftstore[n,or,q] for n in 1:nbf)) # FT of total current
                    smat[q,qp] -= (lo.tvec[q] ⋅ FTJ) * (tlgfvi[q,or] /
                                                (lo.c[q] * acf[or] * area)) # Eq (6.18)
                    i_extract += 1
                end
            end
            t_extract2 = time()
            t_extract = t_extract + (t_extract2 - t_extract1)
        end
    end
    #@logfile "      $(round(t_extract,digits=tdigits)) seconds to extract $(i_extract) GSM entries"
    return gsm
end

"""
    calculate_mtype_gsm(layers, sheet::RWGSheet, u::Real, rwgdat::RWGData, s::Int, k⃗inc, is::Int) -> gsm

Compute the generalized scattering matrix for a sheet of class `'M'`.

### Input Arguments

- `layers`: An iterable of `Layer` instances containing the layers for the `Gblock`
    associated with the `sheet` under consideration.
- `sheet`:  A sheet of class `'M'` for which the GSM is desired.
- `u`: The Green's function smoothing parameter for the `sheet`.
- `rwgdat`: The `RWGData` object associated with the `sheet` argument.
- `s`: The interface number within `layers` at which the sheet is located.
- `k0`: The free-space wavenumber in radians/meter.
- `k⃗inc`: A 2-vector containing the incident field wave vector x and y components. Note
    that by the phase match condition this vector is the same for all layers in the entire FSS
    structure.
- `is`: The sheet index for the `sheet` argument within the global list of sheets.

### Return Value

- `gsm::GSM`  The full GSM for the GBlock including incident fields and scattered fields due
    to magnetic currents induced in the gaps on the sheet surface.
"""
function calculate_mtype_gsm(layers::AbstractVector{Layer}, sheet::RWGSheet, u::Float64,
                                 rwgdat::RWGData, s::Int, k0::Float64, k⃗inc::SVector{2, Float64}, is::Int)
    one_meter = ustrip(Float64, sheet.units, 1u"m")
    area = norm(sheet.s₁ × sheet.s₂) / one_meter^2 # Unit cell area (m^2).
    nmodesmax = max(length(layers[begin].P), length(layers[end].P))
    nbf = size(rwgdat.bfe,2) # Number of basis functions
    if isempty(rwgdat.bfftstore)
        rwgdat.bfftstore = zeros(SArray{Tuple{2},ComplexF64,1,2}, (nbf, 2, nmodesmax))
    end
    bfftstore = rwgdat.bfftstore
    # Compute area correction factors for the mode normalization constants of
    # the two end regions:
    tempvec = zeros(2)
    for (i,l) in enumerate(@view layers[[begin,end]])
      area_i = twopi * twopi / norm(l.β₁ × l.β₂)
      tempvec[i] = √(area_i / area)
    end
    acf = SVector(tempvec[1], tempvec[2])

    # Set up the partial GSM due to incident field
    (gsm, tlgfiv, iincs) = gsm_magnetic_gblock(layers, s, k0)
    if sheet.style == "NULL"
        return gsm
    end

    # Calculate the scattered field partial scattering matrix of the
    # FSS sheet at this junction. Then add it to GSM already computed
    # for the dielectric discontinuity...

    #  Fill the interaction matrix for the current sheet:
    t_temp = time()
    ψ₁ = k⃗inc ⋅ sheet.s₁ / one_meter
    ψ₂ = k⃗inc ⋅ sheet.s₂ / one_meter
    @logfile "    Beginning matrix fill for sheet $(is)"
    ymat = filly(k0,u,layers,s,ψ₁,ψ₂,sheet,rwgdat)
    t_fill = round(time() - t_temp, digits=tdigits)
    @logfile "      $(t_fill) seconds total matrix fill time for sheet $(is)"
    # Factor the matrix:
    t_temp = time()
    ymatf = lu!(ymat)
    t_factor = round(time() - t_temp, digits=tdigits)
    @logfile "      $(t_factor) seconds to factor matrix for sheet $(is)"
    t_temp = time()
    # Compute and store the basis function Fourier transforms:
    i_ft = 0
    for (sr,l) in enumerate(@view layers[[begin,end]]) # loop over possible source regions
        for qp = 1:length(l.P)
            kvec = l.β[qp]
            # If desired F.T. has already been computed, then copy it.
            if qp > 1 && kvec ≈ l.β[qp-1]
                bfftstore[:,sr,qp] = bfftstore[:,sr,qp-1]
                continue
            end
            if sr == 2 && length(layers[1].β) ≥ qp && kvec ≈ layers[1].β[qp]
                bfftstore[:,2,qp] = bfftstore[:,1,qp]
                continue
            end
            bfft = @view bfftstore[:,sr,qp]
            rwgbfft!(bfft, rwgdat, sheet, kvec, ψ₁, ψ₂) # Otherwise, compute from scratch
            i_ft += 1
        end
    end
    t_fft = round(time() - t_temp, digits=tdigits)
    @logfile "      $(t_fft) seconds for basis function Fourier transforms at $(i_ft) points"
    nsolve = 0
    t_extract = 0.0
    t_solve = 0.0
    vmat = rwgdat.rhs
    i_extract = 0
    σ = -1
    for (sr,ls) in enumerate(@view layers[[begin,end]]) # Loop over source regions
        σ *= -1 # 1 for sr == 1, and -1 for sr == 2
        for qp in 1:length(ls.P) # Loop over srce reg modes
            # Incident field for Region sr (Eq. (7.64))
            sourcevec = iincs[qp,sr] * ls.c[qp] * ls.Y[qp] * zhatcross(ls.tvec[qp])
            # Compute generalized current vector:
            vmat .= (b ⋅ sourcevec for b in bfftstore[:,sr,qp]) # Eq. (7.64)
            # Solve the matrix equation
            t_solve1 = time()
            ldiv!(ymatf, vmat)
            t_solve2 = time()
            t_solve += t_solve2 - t_solve1
            nsolve += 1
            t_extract1 = time()
            for (or,lo) in enumerate(@view layers[[begin,end]]) # Loop over obs. regions
                smat = gsm[or,sr]
                for q in 1:length(lo.P)  # Loop obs. regn. modes
                    # Extract partial scattering parameter due to scattered fields...
                    FTM = sum((vmat[n] * bfftstore[n,or,q] for n in 1:nbf)) # FT of total mag. current
                    smat[q,qp] += (zhatcross(lo.tvec[q]) ⋅ FTM) *
                                        (σ * tlgfiv[q,or] * lo.c[q]) # Eq. (6.37)
                    i_extract += 1
                end
            end
            t_extract2 = time()
            t_extract += t_extract2 - t_extract1
        end
    end
    @logfile "      $(t_extract) seconds to extract $(i_extract) GSM entries"
    return gsm
end

"""
    getsttuple(stkeys::Tuple{Symbol,Symbol}, stout::Float64, stin::Float64) -> NamedTuple

Return a named tuple either of the form `(θ = θ, ϕ = ϕ)` or `(ψ₁ = ψ₁, ψ₂ = ψ₂)` that serves
to define the current steering situation. Actually, the input field names can be spelled out in
English as `:theta`, `:phi`, `:psi1`, and `:psi2`.

### Arguments

- `stkeys`: A 2-tuple containing steering parameters as `Symbols`, either (`:ψ₁` and `ψ₂`) or (`:θ` and `:ϕ`)
  (or their spelled-out English versions as detailed above), either of which
  could be listed in either order.  The order is significant in that the first member of the pair
  defines the outer steering loop.

- `stout` and `stin`: These are the current values of the outer and inner steering variables,
  respectively.
"""
function getsttuple(stkeys::Tuple{Symbol, Symbol}, stout::Float64, stin::Float64)
    if stkeys[1] ∈ (:phi, :ϕ, :Phi, :PHI, :Φ)
        return (θ = stin, ϕ = stout)
    elseif stkeys[2] ∈ (:phi, :ϕ, :Phi, :PHI, :Φ)
        return (θ = stout, ϕ = stin)
    elseif keys(steering)[1] ∈ (:psi1, :Psi1, :PSI1, :ψ₁, :Ψ₁, :ψ1, :Ψ1)
        return (ψ₁ = stout, ψ₂ = stin)
    else
        return (ψ₁ = stin, ψ₂ = stout)
    end
end

function check_inputs(layers, sheets, junc, freqs, stkeys, stvalues, outlist)
    # Check that input and output media are lossless and the same
    imag(first(layers).ϵᵣ) == imag(last(layers).ϵᵣ) ==
    imag(first(layers).μᵣ) == imag(last(layers).μᵣ) == 0 ||
    error("First and last layers must be lossless")
    (first(layers).ϵᵣ == last(layers).ϵᵣ) &&
    (first(layers).μᵣ == last(layers).μᵣ) ||
    error("First and last layers must have identical electrical parameters")

    # todo:

    # Check that ψ₁ and ψ₂ are not specified when there are no nonnull sheets

    return
end

"""
    unique_indices(v::Vector)

Return a vector `ui` of the same length as `v`.
`ui[k]` contains the smallest `i` such that 'i ≤ k` and `ui[i] === ui[k]`
"""
function unique_indices(v::Vector)
    n = length(v)
    ui = collect(1:n)
    for io in 1:n-1
        ui[io] ≠ io && continue
        for it in io+1:n
            ui[it] ≠ it && continue
            v[io] === v[it]  && (ui[it] = io)
        end
    end
    return ui
end

"""
    get_gbldup(gbls::Vector{Gblock}, layers::Vector{Layer}, sheets::Vector{RWGSheet}, junc::Vector{Int})
    -> (gbldup, junc)

Return `gbldup::Vector{Int}` of the same length as `gbls`.
`gbldup[k]` contains `0` for an ordinary Gblock.  `gbldup[k] == -1` means that
`gbls[k]` is the first occurence of a repeated Gblock and that its GSM should be
saved for reuse.  `gbldup[k] == i` where `0<i<k` means that `gbls[k]` is identical
to `gbls[i]` and they can both use the same GSM.
Two `Gblock`s are considered identical if they

1. Contain identical (`===`) `Sheet` objects at the same location within the block.
2. Comprise the same number of dielectric layers with identical widths analyzed
   electrical characteristics.
3. Are embedded within similar adjacent dielectric layers, having identical electrical
   properties and numbers of modes.

`junc::Vector{Int}` is has length `length(Layers)-1`. `junc[i]`` is the sheet number
present at dielectric interface `i`, or `0` if no sheet is present there.
"""
function get_gbldup(gbls::Vector{Gblock}, layers::Vector{Layer}, sheets::Vector{RWGSheet}, junc::Vector{Int})
    gbldup = zeros(Int, length(gbls))
    for (g1,gbl1) in pairs(gbls)
        (gbldup[g1] ≠ 0 || gbl1.j == 0) && continue
        j1, rng1 = gbl1.j, gbl1.rng
        n1 = length(layers[first(rng1)].P) # modes at left side
        n2 = length(layers[last(rng1)+1].P) # modes at right side
        # Examine succeeding Gblocks to see if they match:
        for g2 in g1+1:length(gbls)
            gbl2 = gbls[g2]
            rng2 = gbl2.rng
            j2 = gbl2.j
            (gbldup[g2] ≠ 0 || j2 == 0) && continue
            sheets[junc[j1]] === sheets[junc[j2]] || continue
            last(rng1)-j1 ≠ last(rng2)-j2 && continue
            # Check that layers within blocks gbl1 and gbl2 are identical:
            length(rng1) ≠ length(rng2) && continue
            n1 ≠ length(layers[first(rng2)].P) && continue
            n2 ≠ length(layers[1+last(rng2)].P) && continue
            all(zip(rng1,rng2)) do (i1,i2)
                i1 == first(rng1) && (return true)
                layers[i1] == layers[i2]
            end || continue
            # If we made it to here, the two Gblocks are identical:
            gbldup[g1] = -1  # Indicate that GSM of Gblock g1 is to be saved
            gbldup[g2] = g1  # GSM of Gblock g2 is obtained from saved GSM of block g1
        end
    end
    return gbldup
end # function

function report_layers_sheets(layers, sheets, junc, rwgdat, usi)
    @logfile "Dielectric layer information... \n"
    @logfile " Layer  Width  units  epsr   tandel   mur  mtandel modes  beta1x  beta1y  beta2x  beta2y"
    @logfile " ----- ------------- ------- ------ ------- ------ ----- ------- ------- ------- -------"
    for (jl,l) in pairs(layers)
        eps = real(l.ϵᵣ)
        tandel = -imag(l.ϵᵣ) / eps
        mu = real(l.μᵣ)
        mtandel = -imag(l.μᵣ) / mu
        nmode = length(l.P)
        units = string(unit(l.user_width))
        units == "inch" && (units = "in")
        uw_unitless = ustrip(l.user_width)
        str = @sprintf(" %5i %9.4f %3s %7.2f %6.4f %7.2f %6.4f %5i %7.1f %7.1f %7.1f %7.1f",
              jl,uw_unitless, units, eps, tandel, mu, mtandel, nmode, l.β₁[1], l.β₁[2],
              l.β₂[1], l.β₂[2])
        @logfile "$str"
        if jl < length(layers) && junc[jl] ≠ 0
            js = junc[jl]
            s = sheets[js]
            om = ustrip(Float64, s.units, 1.0u"m")
            str = @sprintf(
                " ==================  Sheet %3i  ======================== %7.1f %7.1f %7.1f %7.1f",
                usi[js], s.β₁[1] * om, s.β₁[2] * om, s.β₂[1] * om, s.β₂[2] * om)
            @logfile "$str"
        end
    end
    sint = findall(junc .≠ 0) # sint[k] contains dielectric interface number of k'th sheet

    @logfile "\n\n\nPSS/FSS sheet information...\n"
    @logfile "Sheet  Loc         Style      Rot  J/M Faces Edges Nodes Unknowns  NUFP"
    @logfile "-----  ---  ---------------- ----- --- ----- ----- ----- -------- ------"
    for (js,s) in pairs(sheets)
        str = @sprintf("%4i   %3i  %16s %5.1f  %1s  %5i %5i %5i  %6i %7i",
             usi[js], sint[js], s.style, s.rot, s.class, size(s.fe,2), length(s.e1),
             length(s.ρ), size(rwgdat[js].bfe,2), length(rwgdat[js].ufp2fp))
        @logfile "$str"
    end
    @logfile "\n\n"
    nothing
end

end # module