# Code that deals with objects and child objects.

PVectorCollection() = PVectorCollection(OrderedDict{ObjectId, ParamVector}());

function PVectorCollection(v :: Vector{ParamVector})
    pvecV = PVectorCollection();
    for pvec in v
        push!(pvecV, pvec);
    end
    return pvecV
end

Base.show(io :: IO, pvecV :: PVectorCollection) = 
    print(io, "PVectorCollection of length $(length(pvecV))");

has_pvector(pvecV :: PVectorCollection, pv :: ParamVector) = 
    has_pvector(pvecV, get_object_id(pv));

has_pvector(pvecV :: PVectorCollection, objId :: ObjectId) = 
    haskey(pvecV.d, objId);

function Base.push!(pvecV :: PVectorCollection, pv :: ParamVector)
    @assert !has_pvector(pvecV, pv)  "$pv already exists.";
    pvecV.d[get_object_id(pv)] = pv;
end

Lazy.@forward PVectorCollection.d (
    Base.iterate, Base.isempty, Base.length
    );

function validate_pvec(pvecV :: PVectorCollection; silent = true)    
    isValid = true;
    for (_, pvec) in pvecV
        isValid = isValid  &&  validate_pvec(pvec; silent);
    end
    return isValid
end


"""
	$(SIGNATURES)

Collect all `ParamVector`s in an object and its child objects.
Returns an empty `ParamVector` if nothing found.
"""
function collect_pvectors(o :: ModelObject)
    pvecV = PVectorCollection();
    objV = collect_model_objects(o);
    if !isempty(objV)
        for i1 = 1 : length(objV)
            if has_pvector(objV[i1])
                push!(pvecV, get_pvector(objV[i1]));
            end
        end
    end
    return pvecV :: PVectorCollection
end


"""
    $(SIGNATURES)

Find a `ParamVector` for a given ObjectId. 
Returns the `ParamVector`. Or `nothing` if not found.
"""
find_pvector(o :: ModelObject, objId) = find_pvector(collect_pvectors(o), objId);

function find_pvector(pvv :: PVectorCollection, objId :: ObjectId)
    if has_pvector(pvv, objId)
        return pvv.d[objId];
    else
        return nothing;
    end
end


"""
	$(SIGNATURES)

Find all occurrences of a given parameter name.
Returns a `Dict` that maps `ObjectId => Param`. Or is empty.

# Example
```julia
d = find_param(o, :alpha);
for (objId, p) in d
    # Since a reference to the `Param` is returned, this changes the calibration status in `o`.
    calibrate!(p);
    pvec = find_pvector(o, objId);
end
```
"""
function find_param(o :: ModelObject, pName)
    pvv = collect_pvectors(o);
    return find_param(pvv, pName);
end

function find_param(pvv :: PVectorCollection, pName)
    d = Dict{ObjectId, Param}();
    for (objId, pvec) in pvv
        if param_exists(pvec, pName)
            p = retrieve(pvec, pName);
            d[objId] = p;
        end
    end
    return d
end



## --------------  Vectors and Dicts



"""
	$(SIGNATURES)

Make a `PVectorCollection` into a `Dict{String, Dict{Symbol, Any}}`.
Each entry is one `ParamVector`. 
They key is the `ObjectId` of each `ParamVector` made into a `String`. Such as "parent > child > grandchild[2, 1]".
The value is the `ParamVector` made into a Dict.
Objects without calibrated params have no entries.

This is a format that can be saved without using user defined types. There is hope this can be serialized.

All values are stored for parameters where some values are calibrated and others are fixed.
"""
function make_dict(pvv :: PVectorCollection; isCalibrated :: Bool = true)
    n = length(pvv);
    if n < 1
        return nothing
    end
    isCalibrated  ?  (vType = :value)  :  (vType = :defaultValue);

    d = nothing;
    for (objId, pv) in pvv.d
        # pv = pvv[j];
        key = make_string(objId);
        pd = make_dict(pv; isCalibrated, valueType = vType);
        if isnothing(d)
            d = Dict([key => pd]);
        else
            push!(d, key => pd);
        end
    end

    return d
end


## -------------  Setting values


"""
	$(SIGNATURES)

Set model values from `Dict{String, Dict}` generated by `make_dict`.
Includes child objects.
"""
function set_values_from_dicts!(
    x :: ModelObject,  
    pvDict :: D1; 
    isCalibrated :: Bool = true,
    skipInvalidSize :: Bool = false
    ) where D1 <: AbstractDict

    # Collect all model objects
    mObjV = collect_model_objects(x);

    # Loop over `ParamVector`s, represented as Dicts
    # `nameStr` is the `ObjectId` converted into a `String`
    # `pd` is the `ParamVector` converted into a `Dict`
    for (nameStr,  pd) in pvDict
        # Make string into ObjectId. Allow for `nameStr` to be something that can be made into `String`
        oId = make_object_id(string(nameStr));
        # Find the matching model object
        obj = find_object(x, oId);
        if !isnothing(obj)  &&  has_pvector(obj)
            set_values_from_dict!(get_pvector(obj), pd; skipInvalidSize);
            set_own_values_from_pvec!(obj, isCalibrated);
        end
    end
    return nothing
end


"""
	$(SIGNATURES)

Copy all values from a vector of `ParamVector` into an object, including child objects.
Only changes values that are `isCalibrated` in object and `v`.

# Arguments
- skipInvalidSize: skip parameters that have the wrong size. Otherwise throw error.
"""
function set_values_from_pvectors!(
    x :: ModelObject, 
    v :: PVectorCollection, 
    isCalibrated :: Bool;
    skipInvalidSize :: Bool = false
    )
    # Collect all model objects
    mObjV = collect_model_objects(x);

    # Loop over `ParamVector`s
    for (objId, pvec) in v.d
        # Find the matching model object
        obj = find_object(x, objId);
        if !isnothing(obj)  &&  has_pvector(obj)
            set_own_values_from_pvec!(get_pvector(obj), pvec, isCalibrated;
                skipInvalidSize);
            set_own_values_from_pvec!(obj, isCalibrated);
        end
    end
    return nothing
end





## --------  Testing

function make_test_pvector_collection(n)
    pvecV = PVectorCollection();
    for j = 1 : n
        oId = Symbol("id$j");
        push!(pvecV, make_test_pvector(n; objId = oId));
    end
    return pvecV
end

# --------------