% This utility converts test cases logged in Simulink in Simulink.SimulationData.Dataset format to SLDV compatible one.
function sldvData = convertSimulationDataSetToSldvDataBatch(tcDataStructList, modelH, portInfo)
% tcDataStructList: This is array of structure objects with fields
%   - dataValues:     specifies test inputs in 'Simulink.SimulationData.Dataset' format
%   - expectedOutput: specifies baseline output data for test case in 'Simulink.SimulationData.Dataset' format
%   - paramValues:    specifies parameter values

%   Copyright 2022 The MathWorks, Inc.

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

    InputPortInfo  = portInfo.inputPortInfo;
    OutputPortInfo = portInfo.outputPortInfo;
    flatInfo       = portInfo.flatInfo;

    % Sldv.DataUtils.getInterfaceInfo crashes without analysis info field
    sldvData.AnalysisInformation.Status         = '';
    sldvData.AnalysisInformation.InputPortInfo  = InputPortInfo;
    sldvData.AnalysisInformation.OutputPortInfo = OutputPortInfo;
    sldvData.AnalysisInformation.SampleTimes    = flatInfo.SampleTimes;
    sldvData.AnalysisInformation.Options        = sldvoptions;

    % Add model information
    sldvData.ModelInformation = Sldv.DataUtils.getModelInformation(modelH, 'dataconvert');

    modelSampleTimes = flatInfo.SampleTimes;
    % 'modelSampleTimes' may be an array, if model has multiple rates.
    % Assumed to be sorted in increasing order
    funTimeStep = sldvshareprivate('mdl_derive_sampletime_for_sldvdata', modelSampleTimes);

    % Add each test case in sldvData structure
    for tcIdx = length(tcDataStructList):-1:1    % Loop backwards for allocation efficiency

        % Process 'dataValues' field
        [ds, timeSamples] = fixAndResampleData(tcDataStructList(tcIdx).dataValues, funTimeStep);
        if ds.getLength <= 0
            ds = cell(0, 1);    % If dataset is empty convert it to empty cell
        end
        sldvData.TestCases(tcIdx).dataValues = ds;
        sldvData.TestCases(tcIdx).timeValues = timeSamples;

        % Process 'expectedOutput' field
        [ds, ~] = fixAndResampleData(tcDataStructList(tcIdx).expectedOutput, funTimeStep);
        if ds.getLength <= 0
            ds = cell(0, 0);    % If dataset is empty convert it to empty cell
        end
        sldvData.TestCases(tcIdx).expectedOutput = ds;

        % Process 'paramValues' field
        sldvData.TestCases(tcIdx).paramValues = tcDataStructList(tcIdx).paramValues;
    end

    sldvData = dropDegenerateTestCases(sldvData);
    sldvData = Sldv.DataUtils.convertSLDataSetToCellArray(sldvData);

    % Remove 'TestCases' field if all test cases are degenerate
    if isempty(sldvData.TestCases)
        sldvData = rmfield(sldvData, 'TestCases');
    else
        % Add 'dataNoEffect' field
        for tcIdx = 1:length(sldvData.TestCases)
            sldvData.TestCases(tcIdx).dataNoEffect = createDataNoEffect(sldvData.TestCases(tcIdx).dataValues);
            sldvData.TestCases(tcIdx).stepValues = [];
            sldvData.TestCases(tcIdx).objectives = [];
        end
    end
    sldvData.Objectives = [];
end

% Fix sampling issues in the given dataset and resample the data if needed.
% Resampling is needed if model base rate differs from given dataset sampling rate.
function [dataset, timeResampled] = fixAndResampleData(dataset, funTimeStep)
    maxLogTime = findMaxLogTime(dataset, funTimeStep);
    timeResampled = findTimeLoggedData(maxLogTime, funTimeStep);

    % Iterate over each element in dataset and fix the data in-place
    for elemIdx = 1:dataset.getLength
        dataset{elemIdx} = fixDataSamples(dataset{elemIdx}, timeResampled, maxLogTime);
    end
end

function signal = fixDataSamples(signal, timeResampled, maxLogTime)
    if isa(signal, 'Simulink.SimulationData.Signal')
        % Update only 'Values' field in place. This preserves other metadata like block paths.
        signal.Values = fixDataSamples(signal.Values, timeResampled, maxLogTime);
    elseif isa(signal,'timeseries')
        timeInfo = signal.TimeInfo;

        if timeResampled(end) > maxLogTime
            maxLogTime = timeResampled(end);
        end

        if timeInfo.End ~= maxLogTime
            % Logged data is missing the log for the last step. Repeat the
            % last value to extrapolate
            ds = signal.getdatasamples(timeInfo.Length);
            signal = signal.addsample('Time',maxLogTime,'Data',ds);
        end

        if timeInfo.Start ~= 0
            % If logging starts from non-zero time, we add t=0 and use the
            % same data value with the first logged step.
            % Subsystem logging can reach this code.
            signal = signal.addsample('Time',0,'Data',signal.getdatasamples(1));
        end

        % Why resample needs to change?
        % Example: Consider a model with single inport 'In1'
        % Consider a case where the signal data is obtained from Excel file and data looks like this:
        % ------------------
        % |  time  |  In1  |
        % ------------------
        % |   0    |   1   |
        % |   10   |   1   |
        % ------------------
        % For this case signal.TimeInfo is a column vector [0; 10]
        % But if a model has sampling rate of 3, then sampling times should be [0; 3; 6; 9]
        % This necessitates signal.TimeInfo resampling.

        timeInfo = signal.TimeInfo;
        if timeInfo.Length ~= length(timeResampled)
            signal = signal.resample(timeResampled, 'zoh');
        else
            signal.Time = timeResampled;
        end
    else
        % signal is a struct (bus) or a struct array (bus array)
        if all(size(signal) == 1)
            % For bus signal, iterate for each bus element
            fields = fieldnames(signal);
            numElements = length(fields);
            for idx=1:numElements
                signal.(fields{idx}) = fixDataSamples(signal.(fields{idx}), timeResampled, maxLogTime);
            end
        else
            % For bus array signal, iterate for each bus
            for idx = 1:numel(signal)
                signal(idx) = fixDataSamples(signal(idx), timeResampled, maxLogTime);
            end
        end
    end
end

function timeResampled = findTimeLoggedData(maxLogTime, funTimeStep)
    % Assumption: Test case for SLDV always starts with time zero
    timeResampled = Sldv.DataUtils.expandTimeForTimeseries([0 maxLogTime], funTimeStep);
end

% Get maximum logging time for a input dataset
function maxLogTime = findMaxLogTime(inDataset, funTimeStep)
    % inDataset : object of 'Simulink.SimulationData.Dataset'
    assert(isa(inDataset, 'Simulink.SimulationData.Dataset'));

    numSignals = inDataset.getLength;
    if numSignals <= 0
        maxLogTime = funTimeStep;
        return;
    end
    maxLogTimeTs = zeros(numSignals,1); % Column vector

    % Iterate over each signal in dataset
    for idx=1:numSignals
        currDataElement = inDataset.getElement(idx);
        if ~isempty(currDataElement)
            maxLogTimeTs(idx) = findMaxLogTimeForElement(currDataElement);
        end
    end
    maxLogTime = max(maxLogTimeTs);
end

% Use structural induction to get maximum logging time
function maxLogTimeTs = findMaxLogTimeForElement(dataElement)
    if isa(dataElement, 'timeseries')
        % Induction rule: max logging time for 'timeseries' => timeseries.TimeInfo.End
        % Usage scenario: dataElement is a leaf element in the logged signal hierarchy
        maxLogTimeTs = dataElement.TimeInfo.End;
    elseif isa(dataElement, 'Simulink.SimulationData.Signal')
        % Induction rule: max logging time for 'Simulink.SimulationData.Signal' => max logging time for 'Values'
        % Usage scenario: dataElement is a signal container
        maxLogTimeTs = findMaxLogTimeForElement(dataElement.Values);
    else
        fields = fieldnames(dataElement);
        if all(size(dataElement) == 1)
            % Induction rule: max logging time for 'struct' => max logging time for any 'field' in 'struct'
            % Optimization: Don't probe every 'field' in 'struct'. Just use the first 'field'.
            % Usage scenario: dataElement is a struct. It corresponds to a bus.
            component = dataElement.(fields{1});
        else
            % Induction rule: max logging time for 'struct' array => max logging time for any element of array
            % Optimization: Don't probe every index in array 'field' in 'struct'. Just use the first 'field' of the
            % first element.
            % Usage scenario: dataElement is a struct array. It corresponds to a bus array.
            component = dataElement(1).(fields{1});
        end
        maxLogTimeTs = findMaxLogTimeForElement(component);
    end
end

% Removes degenerate test cases from sldvData
% Definition: A test case is said to degenerate if 'dataValues', 'expectedOutput' and 'paramValues' are all empty
function sldvData = dropDegenerateTestCases(sldvData)
    isDegenTestCase = arrayfun(@(tc) isempty(tc.dataValues) & isempty(tc.expectedOutput) & isempty(tc.paramValues), ...
        sldvData.TestCases);

    sldvData.TestCases = sldvData.TestCases(~isDegenTestCase);
end

function dataNoEffect = createDataNoEffect(dataValues)
    switch class(dataValues)
        case 'cell'
            dataNoEffect = cell(size(dataValues));
            for idx=1:length(dataValues)
                dataNoEffect{idx} = createDataNoEffect(dataValues{idx});
            end
        otherwise
            dataNoEffect = false(size(dataValues));
    end
end

% LocalWords:  tcDataStructList
