function sldvData = spreadsheetToSldvData(filename, modelH)
    % Read Excel file and load test cases as sldvData object

%   Copyright 2022-2023 The MathWorks, Inc.

    assert(~isempty(modelH), 'Sldv:shared:DataUtils:ModelHandleEmpty', getString(message('Sldv:shared:DataUtils:ModelHandleEmpty')));
    assert(ishandle(modelH), 'Sldv:shared:DataUtils:InvalidModelHandle', getString(message('Sldv:shared:DataUtils:InvalidModelHandle')));

    % Compile the model and get port information
    [portInfo.inputPortInfo, portInfo.outputPortInfo, portInfo.flatInfo] = Sldv.DataUtils.generateIOportInfo(modelH);


    % Create a structure array as expected by Sldv.DataUtils.convertSimulationDataSetToSldvDataBatch() API
    testDataList = constructTestDataStructure(filename, modelH, portInfo);

    % Vector implementation of Sldv.DataUtils.convertSimulationDataSetToSldvData()
    sldvData = Sldv.DataUtils.convertSimulationDataSetToSldvDataBatch(testDataList, modelH, portInfo);
end

function testDataList = constructTestDataStructure(filename, modelH, portInfo)
    try
        excelSheets = sheetnames(filename);    % List of all sheets
    catch MEx
        if (strcmp(MEx.identifier, 'MATLAB:spreadsheet:book:invalidFormat')|| ...
                strcmp(MEx.identifier, 'MATLAB:spreadsheet:book:invalidFormatUnix'))
            errorId = 'Sldv:shared:DataUtils:InvalidFormat';
            errorStr = getString(message(errorId));
            MExToThrow = MException(errorId, '%s', errorStr);
            MExToThrow.addCause(MEx);
            throw(MExToThrow);
        end
        throw(MEx);
    end
     nSheets = length(excelSheets);

     simIndex = 1;                               % SLDV supports only first simulation
    modelName = get_param(modelH, 'Name');

    testDataList(nSheets) = struct('dataValues', [], 'expectedOutput', [], 'paramValues', []);

    % Process backwards for execution efficiency. First iteration pre-allocates the memory for all elements.
    for tcIdx = nSheets:-1:1
        % Construct ReadTable objects for reading every sheet
        rtObj = xls.internal.ReadTable(filename, ...
            'Sheets', excelSheets(tcIdx), ...
            'Model',  modelName);

        % contextInfo structure store the current context. This is needed to form error message
        contextInfo = struct(...
            'fileName', filename, ...           % Name of the spreadsheet file
            'sheet', excelSheets(tcIdx), ...    % Name of the sheet being processed
            'model', modelName, ...             & Model name
            'portType', 'inport');              % Type of the port being processed

        % Read inputs from Excel file
        sourceType = xls.internal.SourceTypes.Input;

        try
            testDataList(tcIdx).dataValues = rtObj.readMetadata(sourceType, simIndex);
        catch MEx
            errorId = 'Sldv:shared:DataUtils:FailedToReadSheet';
            errorStr = getString(message(errorId, contextInfo.fileName, contextInfo.sheet));
            MExToThrow = MException(errorId, '%s', errorStr);
            MExToThrow.addCause(MEx);
            throw(MExToThrow);
        end

        % If input data is not specified, user may not be interested in converting input data
        % Skip sanity checking in such cases
        if testDataList(tcIdx).dataValues.getLength > 0
            % Sanity check for inports
            checkInterfaceSanity(testDataList(tcIdx).dataValues, portInfo.inputPortInfo, contextInfo);   % Throws exception
        end

        % Read expected output from Excel file
        sourceType = xls.internal.SourceTypes.Output;
        testDataList(tcIdx).expectedOutput = rtObj.readMetadata(sourceType, simIndex);

        % Sanity check for outports
        contextInfo.portType = 'outport';
        % If expected output is not specified, user may not be interested in converting it
        % Skip sanity checking in such cases
        if testDataList(tcIdx).expectedOutput.getLength > 0
            checkInterfaceSanity(testDataList(tcIdx).expectedOutput, portInfo.outputPortInfo, contextInfo);   % Throws exception
        end

        % Read parameters from Excel file
        tcParams = rtObj.readParameters(simIndex);

        if ~isempty(tcParams)
            % Evaluate parameter constants and read metadata
            paramInfo = xls.internal.SaveVariablesToScript.toValue(tcParams);

            % Pre-allocate memory for struct array. SLDV stores parameters as a column vector
            paramCount = length(paramInfo);
            paramValues(paramCount, 1) = struct('name', '', ...
                'value', 0, ...
                'noEffect', false, ...
                'type', '', ...
                'sourceType', '', ...
                'source', '');

            % Create sldvData formatted structure
            for idx = 1:paramCount
                currParamVal = paramInfo(idx).Value;

                % Limitation: Spreadsheet format does not support storage of 'noEffect' for parameters.
                % This limitation is not significant as SLDV analysis discovers correct value of 'noEffect' field during
                % the analysis.

                % Keeping source and sourceType empty here and populating
                % it in loadTestCase during test case extension workflow
                % from excel file
                paramValues(idx) = struct('name', paramInfo(idx).Name, ...
                    'value', currParamVal, ...
                    'noEffect', false(size(currParamVal)), ...
                    'type', char(Sldv.utils.SLDVParamType.Parameter), ...
                    'sourceType', '', ...
                    'source', '');
            end

            testDataList(tcIdx).paramValues = paramValues;
        end
    end
end

function checkInterfaceSanity(dataset, portInfo, contextInfo)
    % Level #1 Check: Length of dataset elements should match number of ports
    if dataset.getLength ~= length(portInfo)
        if strcmp(contextInfo.portType, 'inport')
            errorId  = 'Sldv:shared:DataUtils:InputColMismatch';
        else
            errorId  = 'Sldv:shared:DataUtils:OutputColMismatch';
        end

        errorStr = getString(message(errorId, contextInfo.fileName, contextInfo.sheet, contextInfo.model));

        MEx = MException(errorId, '%s', errorStr);
        throw(MEx);
    end

    % Level #2: Check individual port metadata
    for idx = 1:dataset.getLength
        currElem = dataset.getElement(idx);
        contextInfo.portName = dataset{idx}.Name;

        checkDatasetElementSanity(currElem, portInfo{idx}, contextInfo);     % Throws exception
    end
end

function checkDatasetElementSanity(dsElem, portInfo, contextInfo)
    switch class(dsElem)
        case 'Simulink.SimulationData.Signal'
            checkDatasetElementSanity(dsElem.Values, portInfo, contextInfo);
        case 'timeseries'
            % Check data type
            if ~isCompatibleType(dsElem.Data, portInfo.DataType)
                % Data type mismatch
                if strcmp(contextInfo.portType, 'inport')
                    errorId  = 'Sldv:shared:DataUtils:InportTypeMismatch';
                else
                    errorId  = 'Sldv:shared:DataUtils:OutportTypeMismatch';
                end

                errorStr = getString(message(errorId, contextInfo.portName, contextInfo.fileName, contextInfo.sheet, contextInfo.model));
                MEx = MException(errorId, '%s', errorStr);
                throw(MEx);
            end

        case 'struct'
            subElems = fields(dsElem);
            nSubElems = length(subElems);

            % Check: Number of fields in structure should match with portInfo. portInfo has first member as header, and
            % remaining members contain port information
            if nSubElems ~= (length(portInfo) - 1)
                if strcmp(contextInfo.portType, 'inport')
                    errorId  = 'Sldv:shared:DataUtils:InputColMismatch';
                else
                    errorId  = 'Sldv:shared:DataUtils:OutputColMismatch';
                end

                errorStr = getString(message(errorId, contextInfo.fileName, contextInfo.sheet, contextInfo.model));
                MEx = MException(errorId, '%s', errorStr);
                throw(MEx);
            end

            % Check structure members
            for idx = 1:length(subElems)
                fieldName = subElems{idx};
                checkDatasetElementSanity(dsElem.(fieldName), portInfo{idx+1}, contextInfo);
            end
    end
end

function res = isCompatibleType(value, type)
    res = isa(value, type);     % Most call will pass this check

    % Apply advanced checks if 'res' is false
    if ~res
        if strcmpi(type, 'boolean')
            res = islogical(value);
        elseif startsWith(type, 'sfix')
            res = isfixed(value) & issigned(value);
        elseif startsWith(type, 'ufix')
            res = isfixed(value) & ~issigned(value);
        elseif startsWith(type, 'fixdt')
            res = isfixed(value);
        end
    end
end
