const inte_one = Int8(1);
const inte_two = Int8(2);
const empty_symbol = Symbol("");

const default_log_location = "./data/"


available_modalities = ["add", "mul"];


# ^^^^^^^^^^^^^^^^^^^^^^^^^^
# ====> Back up Layers <====
# ^^^^^^^^^^^^^^^^^^^^^^^^^^
struct SelectIndex
    index::Integer
end
struct SelectRange{R}
    range::R
end



function SelectRange(start::Integer; stop::Integer = start, step::Integer = 1)
    rng = range(start = start, stop = stop, step = step);
    return SelectRange(rng);
end



Base.show(io::IO, slct::SelectIndex) = print(io, "Get value at index $(slct.index)");
Base.show(io::IO, slct::SelectRange) = print(io, "Select values from range $(slct.range)");



function (slct::SelectIndex)(obj::O) where {O}
    # println("Select $(slct.index) within $obj")
    got = getindex(obj, slct.index);
    # println("Selected: $got");
    return got
end
function (slct::SelectRange{R})(obj::O) where {O, R}
    # println("Select $(slct.range) within $obj")
    got = getindex(obj, slct.range);
    # println("Selected: $got");
    return got
end
function (slct::SelectRange{R})(obj::NamedTuple{O}) where {O, R}
    # println("Select $(slct.range) within $obj")
    got = getindex(values(obj), slct.range);
    # println("Selected: $got");
    return got
end



@functor SelectIndex
@functor SelectRange



trainable(::SelectIndex) = ()
trainable(::SelectRange) = ()


# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# ====> Back up Functions <====
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_check_columns(expected::Vector{Symbol}, df::Union{DF, GroupedDataFrame{DF}}; kwargs...) where {DF<:AbstractDataFrame} =
    _check_columns(expected, propertynames(first(df)); kwargs...);
_check_columns(expected::Vector{Symbol}, inputs::Vector{Symbol}; message::AbstractString = "") = 
    size(intersect(expected, inputs))[1] == 0 ? throw(ArgumentError(message)) : nothing;


function add_labels!(df::Union{DF, GroupedDataFrame{DF}}) where {DF<:AbstractDataFrame}
    _check_columns([:Qty], df, message = ":Qty is mandatory to create labels");
    transform!(df, :Qty => ByRow(qty -> Float32.(qty)) => :Labels);
    return nothing;
end


function add_time_explanatory!(df::Union{DF, GroupedDataFrame{DF}},
                               modes::AbstractString, frequencies::AbstractString,
                               delimiter::AbstractString = ":") where {DF<:AbstractDataFrame}
    _check_columns([:Date], df, message = ":Date is mandatory to add time explanatory variables");

    if isempty(modes)
        throw(ArgumentError("argument `modes` can not be empty"));
    end

    if isempty(frequencies)
        throw(ArgumentError("argument `frequencies` can not be empty"));
    end
    
    lwr_modes = lowercase(modes);
    lwr_frequencies = lowercase(frequencies);
    symbs_mds = to_symbols(lwr_modes, Ses.available_modalities, delimiter);
    symbs_frqs = to_symbols(lwr_frequencies, Ses.available_frequencies, delimiter);

    nmodes = length(symbs_mds);
    nfrequencies = length(symbs_frqs);
    if nmodes != nfrequencies
        throw(DimensionMismatch("found $nmodes modes and $nfrequencies frequencies. Modes and frequencies have to be aligned"));
    end

    return add_time_explanatory!(df, symbs_mds, symbs_frqs);
end
function add_time_explanatory!(df::Union{DF, GroupedDataFrame{DF}},
                               modes::Union{Symbol, Vector{Symbol}},
                               frequencies::Union{Symbol, Vector{Symbol}}) where {DF<:AbstractDataFrame}
    _check_columns([:Date], df, message = ":Date is mandatory to add time explanatory variables");

    if any(isempty.(modes))
        throw(ArgumentError("argument `modes` can not be empty"));
    end

    if any(isempty.(frequencies))
        throw(ArgumentError("argument `frequencies` can not be empty"));
    end

    if isa(modes, Symbol) && !isa(frequencies, Symbol)
        throw(DimensionMismatch("found 1 mode and $(length(frequencies)) frequencies. Modes and frequencies have to be aligned"));
    elseif !isa(modes, Symbol) && isa(frequencies, Symbol)
        throw(DimensionMismatch("found $(length(modes)) modes and 1 frequency. Modes and frequencies have to be aligned"));
    else
        nmodes = length(modes);
        nfrequencies = length(frequencies);
        if nmodes != nfrequencies
            throw(DimensionMismatch("found $nmodes modes and $nfrequencies frequencies. Modes and frequencies have to be aligned"));
        end
    end


    lwr_modes = Symbol.(lowercase.(String.(modes)));
    lwr_frequencies = Symbol.(lowercase.(String.(frequencies)));
    mode_ints = collect_values(Ses.to_intmode, lwr_modes);
    date_fns = collect_values(Ses.to_datefn, lwr_frequencies);
    transform!(df, :Date
                    => ByRow(dts -> (d -> d .|> date_fns).(dts)
                             |> (vs -> hcat(vs...))
                             |> (m -> mapslices(v -> [v], m, dims = 2))
                             |> (vs -> Dict(zip(lwr_frequencies, vs))))
                    => :TExplanatory);
    transform!(df, :TExplanatory
                    => ByRow(dct -> Dict(zip(keys(dct), mode_ints)))
                    => :TMode);
    return nothing;
end


collect_values(ref, ref_keys::D) where {D<:AbstractDict} = collect_values(ref, keys(ref_keys));
function collect_values(ref, ref_keys)
    rkeys = keys(ref);
    ckeys = intersect(rkeys, ref_keys);
    return getindex.(Ref(ref), ckeys);
end



concatenate(mx::M, x::X) where {M, X} = (mx, x);
concatenate(mx::M, x::AbstractVecOrMat{X}) where {M, X} = (mx, x...);
concatenate(mx::AbstractVecOrMat{M}, x::X) where {M, X} = (mx..., x);
concatenate(mx::AbstractVecOrMat{M}, x::AbstractVecOrMat{X}) where {M, X} = (mx..., x...);



is_additive(mode::Real) = mode == inte_one;



Base.isempty(smb::Symbol) = smb == empty_symbol;



function load_model(name::AbstractString; use_bson = false)
    if use_bson
        bson = BSON.load(abspath(name))
        return bson[:model];
    else
        return Serialization.deserialize(name);
    end
end



function log_loss(loss, logger, name = "Loss")
    TensorBoardLogger.log_value(logger, name, loss);
end
function log_loss_and_increase!(loss, logger, name = "Loss")
    TensorBoardLogger.log_value(logger, name, loss);
    TensorBoardLogger.set_step!(logger, TensorBoardLogger.step(logger) + logger.step_increment);
end


Base.lowercase(vs::AbstractVector{Symbol}) = lowercase.(vs);
Base.lowercase(s::Symbol) = Symbol(lowercase(String(s)))


passtrough(t::Union{Tuple{T1, T2}, NamedTuple{T1, T2}}; idx::I = 1) where {I<:Integer, T1, T2} = t[idx]


function preprocess(df::Union{DF, GroupedDataFrame{DF}}; byDate::Bool = false) where {DF<:AbstractDataFrame}
    expected_cols = [:Id, :Date, :Qty];

    _check_columns(expected_cols, df,
                   message = "$(expected_cols) expected in source DataFrame!");

    tmp_df = combine(df, expected_cols);
    if byDate
        tmp_df = groupby(tmp_df, :Date);
        tmp_df = combine(tmp_df, :Id => Ref => :Id, :Qty => Ref => :Qty)
    else
        tmp_df = groupby(tmp_df, :Id);
        tmp_df = combine(tmp_df, [:Date, :Qty] =>
                                ((d, qty) -> [(Array(d), Array(qty))]) =>
                                [:Date, :Qty]);
    end

    add_labels!(tmp_df);

    return tmp_df;
end


# save_model(name::AbstractString, model) = @save name model
function save_model(name::AbstractString, model; use_bson = false)
    if use_bson
        BSON.@save name model
    else
        Serialization.serialize(name, model);
    end

    return nothing
end


function to_symbols(inpt_list::AbstractString, reference_list::AbstractVector{<:AbstractString}, delimiter = ":")
    lst = Vector{Symbol}();
    for elem in split(inpt_list, delimiter)
        if elem in reference_list
            push!(lst, Symbol(elem));
        end
    end
    return lst;
end