function [isValid, newSpec] = checkSldvSpecificationType(spec, classS, varargin)
%

%   Copyright 2007-2023 The MathWorks, Inc.

    % Validate the type of an Sldv specification
    % The specification is expected to be already syntactically
    % valid, that is, validated by checkSldvSpecification.
    %
    % This is called from several places in the following manner:
    %
    % checkSldvSpecificationType(spec, blockH)
    %   - early check on the sldv blocks.
    %
    % checkSldvSpecificationType(spec, class, dimensions, blockH)
    %   - deeper compatibility check and eventually vectorization on the blocks
    %
    % checkSldvSpecificationType(spec, class, dimensions, param, modelReferencingParam)
    %   - check on a parameter configuration. No vectorization. dimensions
    %     is the internal dimension.

    isValid = true;
    allowResizing = true;
    param_value = [];
    param_runtime_type = '';
    isSimulinkParameterAuto = false;
    parameterTuningWorkflow = false;
    if nargin == 5
        % The call checkSldvSpecificationType(spec, class, dimensions, ...
        %                                         param, modelReferencingParam)
        % is performed only during parameter tuning workflow.
        parameterTuningWorkflow = true;
    end

    if nargin < 3
        dimensions = [];
    else
        dimensions = varargin{1};
    end

    if nargin < 4
        obj = classS;
        modelH = getModelHandle(obj);
    else
        obj = varargin{2};
        if ischar(obj)
            if nargin == 4
                modelH = getModelHandle(obj);
            else
                modelH = varargin{3};
            end
            % eval obj in global scope. So, if model has associated Data
            % Dictionary(DD), then the evaluation will occur in the context of
            % DD, otherwise it will occur in base Workspace.
            % For struct/Bus Param, we have stored source information
            % for topParamName. Thus we get source information for
            % leaf params using their topParamName.
            % For example, for obj = 'carToplvlParam.EnginesubBus.speed',
            % topParamName = 'carToplvlParam'.
            hierarchyNames = split(obj, '.');
            topParamName = hierarchyNames{1};
            testComp = Sldv.Token.get.getTestComponent;
            paramInfo = testComp.mdlFlatIOInfo.Parameter;
            paramMap = testComp.mdlFlatIOInfo.ParamNameMap;
            paramIdx = paramMap(topParamName);
            if ~strcmp(paramInfo(paramIdx).SourceType, 'model workspace')
                param_value = getVarFromGlobalContext(modelH, obj);
            else
                paramObj = getVariableFromModel(modelH, topParamName);
                if isa(paramObj, 'Simulink.Parameter') && contains(obj, '.')
                   childParam = replaceBetween(obj,1,length(topParamName), 'paramObj');
                   param_value = eval(childParam);
                else
                    param_value = paramObj;
                end
            end

            if isa(param_value, 'Simulink.Parameter')
                modelH = varargin{3};
                if strcmpi(param_value.DataType,'auto')
                    % if data type is auto, we should cast using RunTime
                    % type
                    isSimulinkParameterAuto = true;
                end
                param_value = slResolve(obj,modelH);
            end
            if isscalar(param_value)
                allowResizing = false;
            end
            sldvdllexist = exist('slavteng','builtin')==5;
            if sldvdllexist
                testComp = Sldv.Token.get.getTestComponent;
            end
            if ~isempty(testComp) && ...
                    isfield(testComp.analysisInfo,'paramsRunTimeTypes') && ...
                    ~isempty(testComp.analysisInfo.paramsRunTimeTypes) && ...
                    isfield(testComp.analysisInfo.paramsRunTimeTypes,obj)
                if (isempty(testComp.analysisInfo.paramsRunTimeTypes.(obj)) || ...
                        length(unique(testComp.analysisInfo.paramsRunTimeTypes.(obj))) ~= 1) && ...
                        parameterTuningWorkflow && util_is_simulink_builtin(classS) 
                    % This could occur in two cases
                    % Case 1: When we are unable to get the runtime data type of
                    %         the parameter (g2951792)
                    % Case 2: When the parameter has two unique data types(g2962865)
                    % 
                    % In both these case, we assign the data type of CG::var to 
                    % this variable 
                    % We verify nargin == 5 here to make sure that we use
                    % it only for the workflow 'check on a parameter configuration'
                    testComp.analysisInfo.paramsRunTimeTypes.(obj) = {classS};
                end
                param_runtime_type = ...
                    testComp.analysisInfo.paramsRunTimeTypes.(obj);
                if isSimulinkParameterAuto && ~isempty(param_runtime_type)
                    param_value = getTypedValue(param_value, param_runtime_type{1}, true, modelH);
                end
            end
        else
            modelH = getModelHandle(obj);
        end
    end

    % If the second argument is a block handle, we go back to the block and
    % get the class and dimensions from there.
    if isnumeric(classS)
        ssBlk = [];
        while isempty(ssBlk)
            maskType = get_param(classS, 'MaskType');
            if (strcmpi(maskType, 'design verifier test objective') || ...
                       strcmpi(maskType, 'design verifier proof objective') || ...
                       strcmpi(maskType, 'design verifier test condition') || ...
                       strcmpi(maskType, 'design verifier assumption'))
                   ssBlk = classS;
            else
                classS = get_param(classS, 'Parent');
                if isempty(classS)
                    isValid = false;
                    return;
                end
            end
        end
        ph = get_param(ssBlk, 'porthandles');
        portH = ph.Inport;
        compiledportPrm = Simulink.CompiledPortInfo(portH);
        classS = compiledportPrm.AliasThruDataType;
        dimensions = compiledportPrm.Dimensions;

        obj = get_param(ssBlk, 'Handle');
    end

    % Explicitly give number of rows for 1 dimensions signals
    if length(dimensions) == 1
        dimensions = [ 1 dimensions ];
    end

    if ~iscell(spec)
        spec = { spec };
    end

    newSpec = spec;

    for i=1:length(spec)
        [isValid, newElem, errId, errMsg] = checkElem(spec{i}, classS, dimensions, obj, allowResizing, ...
            param_value, param_runtime_type, modelH);
        if ~isValid
            newSpec = {errId, errMsg};
            return;
        end
        newSpec{i} = newElem;
    end
end

function modelH = getModelHandle(obj)
    try
        if ishandle(obj)
            modelH = bdroot(obj);
        else
            modelH = bdroot(find_equiv_handle(obj)); % sf element
        end
    catch
        % 'obj' may be the sfId of an external EML script. In this case,
        % the above call to bdroot(find_equiv_handle(obj)) will fail. Then
        % try to get modelH from TestComponent. Note: this is reachable
        % from cvsim too, in which case TestComponent will not be available.
        try
            modelH = Sldv.Token.get.getTestComponent.analysisInfo.analyzedModelH;
        catch
            modelH = [];   % This is for use by util_is_fxp_type. So '[]' is ok as modelH.
        end
    end
end

% An element is valid if:
% - it's empty
% - it's a point and it's value is valid
% - it's an interval and both bounds are either valid
%   or infinite
% We assume that the spec is already syntactically valid
% (i.e. either [], Sldv.Point, or Sldv.Interval)
function [isValid, newSpec, errId, errMsg] = checkElem(spec, classS, dimensions, obj, allowResizing, ...
                                                       pvalue, param_runtime_type, modelH)
    isValid = true;
    newSpec = spec;
    errId = [];
    errMsg = [];
    if isa(spec, 'Sldv.Point')
        [isValid, newValue, errId, errMsg] = checkValue(spec.value, classS, dimensions, obj, allowResizing, pvalue, param_runtime_type, modelH);
        if ~isValid
            return;
        end
        newSpec = Sldv.Point(newValue);
    elseif isa(spec,'Sldv.Interval')
        [isValidLow, newLow, errId, errMsg] = checkValue(spec.low, classS, dimensions, obj, allowResizing, pvalue, param_runtime_type, modelH);
        if ~isValidLow
            isValid = false;
            return;
        end
        [isValidHigh, newHigh, errId, errMsg] = checkValue(spec.high, classS, dimensions, obj, allowResizing, pvalue, param_runtime_type, modelH);
        if ~isValidHigh
           isValid = false;
           return;
        end

        if spec.lowIncluded
            modL = '[';
        else
            modL = '(';
        end
        if spec.highIncluded
            modH = ']';
        else
            modH = ')';
        end
        newSpec = Sldv.Interval(newLow, newHigh, [ modL modH ]);
    end
end

% A scalar is valid if casting it to the required class doesn't
% imply any loss of precision. The single case is special.
%
% This should check things for any dimension matrix. The isnan
% check lets infinities pass (Inf - Inf == NaN).
function [isValid, newValue, errId, errMsg] = checkValue(value, classS, dimensions, obj, allowResizing, pvalue, param_runtime_type, modelH)

    errMsg = [];

    if ischar(obj)
        % Parameter configuration
        % Check that a parameter used by multiple locations has a single data-type
        if ~isempty(param_runtime_type) && length(unique(param_runtime_type)) ~= 1
            % This error would never occur.
            % Simulink:Parameters:TransTuneRTPClashingUses expected to be thrown.
            isValid = false;
            errId = 'Sldv:Compatibility:DataTypeAutoIsUsedByMultipleLocations';
        else
            if ~isempty(param_runtime_type)
                param_runtime_type = param_runtime_type{1};
            else
                param_runtime_type = '';
            end
            [isValid, value, errId] = fixValueParam(value, pvalue, param_runtime_type, classS, modelH);
        end
    else
        % SLDV block
        [isValid, value, errId] = fixValueSldvBlocks(value, classS, modelH);
    end
    if isValid
        % We have a new specification, we need to check its dimensions
        %   . if the dimensions are the same as the expected ones, we're
        %     good
        %   . if they are different, and the spec is a scalar and we allow
        %     resizing, we vectorize the spec
        %   . otherwise, error out. Two errors - either we don't allow
        %     resizing, or it's allowed but we couldn't reconcile the
        %     dimensions.

        valueSize = size(value);
        pvalueSize = size(pvalue);

        dim1 = simplify_dimensions(valueSize);
        dim2 = simplify_dimensions(dimensions);

        if ~isempty(dim1) && ~isempty(dim2) && ...
           (ndims(dim1) == ndims(dim2)) && ...
           (numel(dim1) == numel(dim2)) && ...
           all(dim1 == dim2)
            newValue = value;
        elseif ~isempty(pvalue) && ...
                length(valueSize)== length(pvalueSize) && all(valueSize==pvalueSize)
            newValue = reshape(value, dimensions);
        else
            if isscalar(value)
                % The specification is a scalar, and is applied to a matrix
                % if the specification comes from a parameter, we don't do
                % vectorization. If it's coming from a block, we do.
                if allowResizing
                    newValue = repmat(value, dimensions);
                else
                    isValid = false;
                    errID = 'Sldv:Compatibility:ParameterSizeMismatch';
                    errMsg = getString(message(errID, obj, num2str(dimensions)));
                    avtcgirunsupcollect('push', -1, 'sldv', errMsg, errID);
                end
            else
                newValue = value;
                isValid = false;
                errObj = obj;
                if ischar(obj)
                    errID = 'Sldv:Compatibility:CustomBlockDimensionError';
                    errMsg = getString(message(errID, obj, num2str(dimensions), num2str(size(value))));
                    errObj = -1;
                else
                    errID = 'Sldv:Compatibility:CustomInputSignalDimensionError';
                    errMsg = getString(message(errID, num2str(dimensions), num2str(size(value))));
                end
                avtcgirunsupcollect('push', errObj, 'sldv', errMsg, errID);
            end
        end
    else
        newValue = value;
        errObj = obj;
        % Value type
        try
            if isa(value, 'embedded.fi')
                configTypeStr = fixdt(value.numerictype);
            else
                configTypeStr = class(value);
            end
        catch ME %#ok<NASGU>
            configTypeStr = class(value);
        end

        % Runtime-type
        if ischar(obj) && ~isempty(param_runtime_type)
            if sldvshareprivate('util_is_fxp_type', param_runtime_type, modelH)
                fixdtType = fixdt(param_runtime_type);
                runtimeTypeStr = fixdtType.tostring;
            else
                runtimeTypeStr = param_runtime_type;
            end
        else
            runtimeTypeStr = classS;
        end
        switch errId
            case 'Sldv:Compatibility:MultiwordType'
                errMsg = getString(message(errId));
            case 'Sldv:Compatibility:CustomBlockBusTypeError'
                errMsg = getString(message(errId, getfullname(obj)));
            case 'Sldv:Compatibility:FixedPtMismatch'
                errMsg = getString(message(errId));
                errObj = -1;
            case 'Sldv:Compatibility:IncompatibleTypes'
                 if ischar(obj)
                    errMsg = getString(message('Sldv:Compatibility:IncompatibleTypesChar', obj, runtimeTypeStr, configTypeStr));
                    errObj = -1;
                else
                    errMsg = getString(message('Sldv:Compatibility:IncompatibleTypesInputSignal', getfullname(obj), runtimeTypeStr, configTypeStr));
                 end
            case 'Sldv:Compatibility:IncompatibleParamTypes'
                errMsg = getString(message(errId, obj, runtimeTypeStr, configTypeStr));
                errObj = -1;
            case 'Sldv:Compatibility:RuntimeParamTypeMismatch'
                errMsg = getString(message(errId, obj, runtimeTypeStr, runtimeTypeStr));
                errObj = -1;
            case 'Sldv:Compatibility:PrecisionLoss'
                if ischar(obj)
                    errMsg = getString(message('Sldv:Compatibility:PrecisionLossObj', obj, runtimeTypeStr, configTypeStr));
                    errObj = -1;
                else
                    errMsg = getString(message('Sldv:Compatibility:PrecisionLossSignal', getfullname(obj), runtimeTypeStr, configTypeStr));
                end
            case 'Sldv:Compatibility:IncompatibleFixedPointParamType'
                errMsg = getString(message(errId, errObj));
                errObj = -1;
            case 'Sldv:Compatibility:NaNvalue'
                errMsg = getString(message(errId, getfullname(obj)));
        end

        % We lose information on the error ID on purpose.
        errID = 'Sldv:Compatibility:CustomBlockTypeError';
        avtcgirunsupcollect('push', errObj, 'sldv', errMsg, errID);
    end
end

function [isValid, newValue, errId] = fixValueSldvBlocks(value, classS, modelH)
    errId = '';
    
    classS = changeTypeForSpecialCases(classS);
    
    if sldvshareprivate('util_is_bus_type', classS)
        % Bus input types are not supported for Sldv blocks
        isValid = false;
        errId = 'Sldv:Compatibility:CustomBlockBusTypeError';
    elseif sldvshareprivate('util_is_fxp_type', classS, modelH)
        % In case of fixed-point, specification needs to be exact match
        numericType = numerictype(classS);
        if isa(value, 'embedded.fi') && isequivalent(numericType, value.numerictype)
            if numericType.WordLength <= 128   % g1482055, g1586816
                isValid = true;
            else
                isValid = false;
                errId = 'Sldv:Compatibility:MultiwordType';
            end
        elseif isa(value, 'double')
            [isValid, errId] = checkPrecisionLoss(value, classS, modelH);
        else
            errId = 'Sldv:Compatibility:IncompatibleTypes';
            isValid = false;
        end
    else
        if isa(value, 'embedded.fi')
            % Allow to use fi object to specify built-in integer type
            % ex. use fi(1,1,8,0) instead of int8(1)
            if (value.isscalingslopebias && value.Slope == 1 && value.Bias == 0) ...
                    || (value.isscalingbinarypoint && value.FractionLength == 0)
                isValid = false;
                switch classS
                    case 'int8'
                        if value.WordLength == 8 && value.Signed
                            isValid = true;
                        end
                    case 'uint8'
                        if value.WordLength == 8 && ~value.Signed
                            isValid = true;
                        end
                    case 'int16'
                        if value.WordLength == 16 && value.Signed
                            isValid = true;
                        end
                    case 'uint16'
                        if value.WordLength == 16 && ~value.Signed
                            isValid = true;
                        end
                    case 'int32'
                        if value.WordLength == 32 && value.Signed
                            isValid = true;
                        end
                    case 'uint32'
                        if value.WordLength == 32 && ~value.Signed
                            isValid = true;
                        end
                end
                if ~isValid
                    errId = 'Sldv:Compatibility:IncompatibleTypes';
                end
            else
                errId = 'Sldv:Compatibility:IncompatibleTypes';
                isValid = false;
            end
        else
            [isValid, errId] = checkPrecisionLoss(value, classS, modelH);
        end
    end

    if isValid
        newValue = getTypedValue(value, classS, true, modelH);
    else
        newValue = value; % newValue is used for error message
        return;
    end
end

function [isValid, newValue, errId] = fixValueParam(confValue, wsValue, runtimeType, classS, modelH)

    % fixValueParam is used to fix spec (confValue) to a fixed point
    % value if runtimeType is fixed point value or to expected class of
    % parameter(classS)
    
    % wsValue = initial value assigned to the parameter
    % runtimeType = runtimeType of the parameter
    % classS = expected class of parameter 
    % confValue = parameter specification value (constraint value)
    errId = '';
    runtimeTypeEmpty = false;

    if isempty(runtimeType)
        % No runtime_type use classS
        runtimeTypeEmpty = true;
        runtimeType = classS;
    end
    
    previousRunTimeType = runtimeType;
    runtimeType = changeTypeForSpecialCases(runtimeType);
    
    % For 'Special cases' ('int64' and 'uint64') we use the
    % changeTypeForSpecialCases which causes the runtimeType to change to sfix64 and ufix64 correspondingly.
    % The confValue would be converted to these types as required.
    
    % For example, consider a tunable int64 parameter with the 
    % initial value int64(10). 'changeTypeForSpecialCases' changes the
    % runtimeType to sfix64 with the confValue still being a int64(10)
    % fixValueParam wants to convert this value to sfix64. So, precision
    % loss should be checked accordingly.
    
    % The check 'isSpecialCase' covers this scenario. 
    isSpecialCase = strcmp(previousRunTimeType,'int64') || strcmp(previousRunTimeType,'uint64');
    % isConfValueFixpt = isa(confValue, 'embedded.fi');
    % isIntSpecialCase = isSpecialCase && ~isConfValueFixpt;

    if sldvshareprivate('util_is_fxp_type', runtimeType, modelH)
        numericType = numerictype(runtimeType);
        isValid = false;
        if isa(confValue, 'embedded.fi') && isequivalent(numericType, confValue.numerictype)
            if isa(wsValue, 'double')
                [isValid, errId] = checkPrecisionLoss(confValue, runtimeType, modelH);
            elseif isa(wsValue, 'embedded.fi')
                if isequivalent(numericType, wsValue.numerictype)
                    if numericType.WordLength <= 128   % g1482055, g1586816
                        isValid = true;
                    end
                end
            end
        elseif isSpecialCase
            if ~isa(confValue, 'embedded.fi')
                % Converting a non fixed point data type to a fixed point
                % data type. So, check precision loss for this conversion
                [isValid, errId] = checkPrecisionLoss(confValue, runtimeType, modelH);
            end
        end
        if ~isValid
            errId ='Sldv:Compatibility:IncompatibleFixedPointParamType';
        end
    else
        
        % When value is specified through Simulink.Parameter variables, runtime type may
        % not be detected as fixed point, But wsValue would be resolved to a fi
        % variable. Do not error directly in this case.
        if isa(wsValue, 'embedded.fi') && ~runtimeTypeEmpty
            isValid = false;
            errId ='Sldv:Compatibility:IncompatibleFixedPointParamType';
        else
            [isValid, errId] = checkPrecisionLoss(confValue, runtimeType, modelH);
        end
    end

    if ~isValid
        newValue = confValue; % newValue is used for error message
        return;
    end

    if sldvshareprivate('util_is_fxp_type', runtimeType, modelH)
        if isa(confValue, 'embedded.fi')
            newValue = confValue.int;
        else
            % In case of non-fixed-point, use stored integer obtained by
            % casting to runtime data-type
            tValue = fi(confValue, numerictype(runtimeType));
            newValue = tValue.int;
        end
    else
        newValue = getTypedValue(confValue, classS, false, modelH);
    end
end

function [isValid, errId] = checkPrecisionLoss(value, dataTypeStr, modelH)
    errId = '';
    isValid = false;

    if Simulink.data.isSupportedEnumObject(value) && ~sldvshareprivate('util_is_enum_type', dataTypeStr)
        % Disallow use of Enumerated Type to specify non-enumerated type
        errId = 'Sldv:Compatibility:IncompatibleTypes';
        isValid = false;
        return;
    end

    if any(isnan(value(:)))
        isValid = false;
        errId = 'Sldv:Compatibility:NaNvalue';
        return;
    end

    try
        if util_is_simulink_builtin(dataTypeStr) || ...
                strcmp(dataTypeStr, 'logical') || ...
                strcmp(dataTypeStr, 'half')
            value = value(~isinf(value(:)));
            if isempty(value)
                isValid = true;
                return;
            end
            if strcmp(dataTypeStr, 'single')
                if abs(double(single(value)) - double(value)) < eps(single(value))
                    isValid = true;
                else
                    isValid = false;
                    errId = 'Sldv:Compatibility:PrecisionLoss';
                end
            elseif strcmp(dataTypeStr, 'half')
                 if abs(double(half(value)) - double(value)) < eps(half(value))
                    isValid = true;
                else
                    isValid = false;
                    errId = 'Sldv:Compatibility:PrecisionLoss';
                end
            else
                if double(cast(value, dataTypeStr)) == double(value)
                    isValid = true;
                else
                    isValid = false;
                    errId = 'Sldv:Compatibility:PrecisionLoss';
                end
            end
        elseif sldvshareprivate('util_is_fxp_type', dataTypeStr, modelH)
            % fixed-point case
            if any(isinf(value(:)))
                isValid = false;
                errId = 'Sldv:Compatibility:IncompatibleTypes';
                return;
            end
            numericType = numerictype(dataTypeStr);
            if numericType.WordLength > 128   % g1482055, g1586816
                isValid = false;
                errId = 'Sldv:Compatibility:MultiwordType';
            elseif  util_is_simulink_builtin(class(value))
                % base-type -> fixpt is allowed
                if double(fi(value, fixdt(dataTypeStr))) == double(value)
                    isValid = true;
                else
                    isValid = false;
                    errId = 'Sldv:Compatibility:PrecisionLoss';
                end
            elseif isa(value, 'embedded.fi') && ...
                    isequivalent(numericType, value.numerictype)
                isValid = true;
            else
                isValid = false;
                errId = 'Sldv:Compatibility:PrecisionLoss';
            end
        elseif sldvshareprivate('util_is_enum_type', dataTypeStr)
            % Enumeration type
            if strcmp(class(value), dataTypeStr)
                isValid = true;
            else
                % test if given value is available
                feval(dataTypeStr, value);
                isValid = true;
            end
        end
    catch Mex  %#ok<NASGU>
        isValid = false;
        errId = 'Sldv:Compatibility:IncompatibleTypes';
    end
end

function dimensions = simplify_dimensions(dims)
    dimensions = dims(dims ~= 1);
    if isempty(dimensions)
        dimensions =  1 ;
    end
end

% Handle special cases where type change is needed
function newType = changeTypeForSpecialCases(type)
    switch lower(type)   
        case 'boolean'
            newType = 'logical';
        case 'int64'
            newType = 'sfix64';
        case 'uint64'
            newType = 'ufix64';
        otherwise
            newType = type;
    end
end
% LocalWords:  Sldv sldv Toplvl Enginesub RTP Nvalue ws Nvalue
