function t = timeseries2timetable(ts, varargin)
%TIMESERIES2TIMETABLE Convert timeseries to timetable.
%   TT = TIMESERIES2TIMETABLE(TS) converts the timeseries TS to a
%   timetable TT containing one variable.
%﻿
%   TT = TIMESERIES2TIMETABLE(TSARRAY) converts the timeseries array TSARRAY to a
%   timetable TT containing one variable for each element of the array.
%   Each element of the timeseries array must have matching time vectors.
%
%   TT = TIMESERIES2TIMETABLE(TS1, TS2, ...) converts the timeseries TS1, TS2, ..., to a
%   timetable TT containing one variable for each timeseries. All inputs
%   must have matching time vectors.
%
%   See also TIMETABLE, ARRAY2TIMETABLE, TABLE2TIMETABLE, TIMETABLE2TABLE.

%   Copyright 2020 The MathWorks, Inc.

if ~isa(ts,'timeseries')
    error(message("MATLAB:timeseries2timetable:InputType"))
end

numTS = nargin;
if ~isscalar(ts) && numTS > 1
    error(message("MATLAB:timeseries2timetable:NonscalarInput"));
end

if ~isscalar(ts) && numTS == 1
    if isempty(ts)
        error(message("MATLAB:timeseries2timetable:EmptyTimeseriesArray"));
    end

    varargin = num2cell(ts(2:end));
    numTS = numel(ts);
    ts = ts(1);
end

% preallocate
varnames = cell(1,numTS);
vars = cell(1,numTS);
variableContinuity = matlab.tabular.Continuity("unset");
varUnits = cell(1,numTS);
hasUnits = false;
userData = [];

[varnames, variableContinuity, varUnits, hasUnits, vars, userData] = extractAndValidateTS(ts, 1, varnames, variableContinuity,varUnits, hasUnits, vars,userData);
numRows = size(vars{1},1);

for i = 1:(numTS-1)
    ts_i = varargin{i};

    % Array inputs are only supported for the single input syntax.
    if ~isscalar(ts_i) || ~isa(ts_i, 'timeseries')
        error(message("MATLAB:timeseries2timetable:NonscalarInput"));
    end

    [varnames, variableContinuity, varUnits, hasUnits, vars, userData] = extractAndValidateTS(ts_i, i+1, varnames, variableContinuity,varUnits, hasUnits, vars,userData,numRows);

    % Check that time vector matches all inputs.
    if ~isequal(ts.Time, ts_i.Time) || (~isequal(ts.TimeInfo.Units ,ts_i.TimeInfo.Units) || ~isequal(ts.TimeInfo.StartDate,ts_i.TimeInfo.StartDate))
        error(message("MATLAB:timeseries2timetable:TimeVectorMismatch"));
    end
end

varnames = matlab.lang.makeUniqueStrings(varnames);
varnames = matlab.internal.tabular.private.varNamesDim.makeValidName(varnames, 'warnLength');

if isnan(ts.TimeInfo.Increment) % explicit rowtime timetable
    time = tsTimeToDuration(ts.Time,ts.TimeInfo.Units,ts.TimeInfo.Format);
    if ~isempty(ts.TimeInfo.StartDate)
        % timeseries validates that StartDate must be a datestr.
        d = datetime(ts.TimeInfo.StartDate);
        time = d+time;
    end
    t = timetable.init(vars,numRows,time,numTS,varnames);
else % optimized regular timetable
    starttime = tsTimeToDuration(ts.TimeInfo.Start,ts.TimeInfo.Units,ts.TimeInfo.Format);
    if ~isempty(ts.TimeInfo.StartDate)
        starttime = datetime(ts.TimeInfo.StartDate)+starttime;
    end
    timestep = tsTimeToDuration(ts.TimeInfo.Increment,ts.TimeInfo.Units,ts.TimeInfo.Format);

    % Always use timestep rather than sample rate.
    t = timetable.initRegular(vars,numRows,starttime,timestep,[],numTS,varnames);
end

t.Properties.VariableContinuity = variableContinuity;
if hasUnits
    t.Properties.VariableUnits = varUnits;
end

t.Properties.UserData = userData;
end




function [varnames, variableContinuity, varUnits, hasUnits, vars, userData] = extractAndValidateTS(ts, i, varnames, variableContinuity,varUnits,hasUnits, vars,userData,numRows)
if ~isempty(ts.Events)
    warning(message("MATLAB:timeseries2timetable:IgnoringEvents"));
end

if ~isempty(ts.Quality)
    warning(message("MATLAB:timeseries2timetable:IgnoringQuality"));
end

varname = (ts.Name);
if isempty(varname) || varname == "unnamed"
    varname = 'Data';
end
varnames{i} = varname;

data = ts.Data;
if ~ts.IsTimeFirst % For timetable, time is always the first dimension.
    data = tsReorientData(data, length(ts.Time));
end

variableContinuity(i) = interpolation2Continuity(getinterpmethod(ts));
if isempty(userData) % Take first non-empty
    userData = ts.UserData;
end
units = ts.DataInfo.Units;

if isempty(units)
    varUnits{i} = '';
else
    hasUnits = true;
    varUnits(i) = transferUnits(units);
end

vars{i} = data;

if isempty(ts.Time)
    error(message("MATLAB:timeseries2timetable:EmptyTimeVector"));
end

if nargin > 8
    if size(data,1) ~= numRows
        error(message("MATLAB:timeseries2timetable:NumberOfRowsMismatch"));
    end
end
end


function time = tsTimeToDuration(time,units,fmt)
if isempty(units)
    time = seconds(time);
else
    % Timeseries.TimeInfo.Units can be set to anything, but has a
    % documented list of allowed values.
    if ~matlab.internal.datatypes.isScalarText(units)
        error(message('MATLAB:timeseries2timetable:InvalidTimeUnit'))
    elseif ~isnumeric(time)
        error(message('MATLAB:timeseries2timetable:InvalidTime'));
    end
    switch units
        case 'weeks'
            time = days(7*time);
        case 'days'
            time = days(time);
        case 'hours'
            time = hours(time);
        case 'minutes'
            time = minutes(time);
        case 'seconds'
            time = seconds(time);
        case 'milliseconds'
            time = milliseconds(time);
        case 'microseconds'
            time = milliseconds(1e-3*time);
        case 'nanoseconds'
            time = milliseconds(1e-6*time);
        otherwise
            error(message('MATLAB:timeseries2timetable:InvalidTimeUnit'))
    end
end
if ~isempty(fmt)
    time.Format = fmt;
end
end

function cont = interpolation2Continuity(interp)
switch interp
    case ""
        cont = matlab.tabular.Continuity("unset");
    case "linear"
        cont = matlab.tabular.Continuity("continuous");
    case "zoh"
        cont = matlab.tabular.Continuity("step");
    otherwise
        error(message('MATLAB:timeseries2timetable:InvalidInterpMethod'))
end
end

function data = tsReorientData(data, len)
if len > 1
    data = permute(data, circshift(1:ndims(data),1));
else
    % For a single sample, the last dimension (representing time) is
    % missing because trailing singleton dimensions are cropped. For
    % example, logging a row vector for one sample results in 1x3x1 and the
    % trailing singleton dimension is cropped: 1x3. Adding the leading
    % dimension to represent time results in a 1x1x3. Note that aligning
    % time with the first dimension results in a 1xN array for column
    % vectors because we are left with a 1x3x1 and the last singleton
    % dimension is cropped.
    data = reshape(data, [1 size(data)]);
end
end

function ttUnit = transferUnits(units)

if isa(units,"Simulink.SimulationData.Unit")
    if isscalar(units)
        ttUnit = {units.Name};
    else
        error(message("MATLAB:timeseries2timetable:InvalidUnit"))
    end
elseif matlab.internal.datatypes.isScalarText(units)
    ttUnit = {convertStringsToChars(units)};
else
    error(message("MATLAB:timeseries2timetable:InvalidUnit"))
end
end