cosmo meeg dataset

function ds=cosmo_meeg_dataset(filename, varargin)
% Returns a dataset structure based on MEEG data
%
% ds=cosmo_meeg_dataset(filename, varargin)
%
% Inputs:
%   filename          filename of MEEG data to be loaded. Currently
%                     supported are files with extensions:
%                       .mat :        FieldTrip time-locked or
%                                     time-frequency  data at  either the
%                                     sensor or source level.
%                       .txt :        exported EEGLab with time-locked
%                                     data.
%                       .daterp       time-locked               }
%                       .icaerp       ICA time-locked           } EEGLab
%                       .dattimef     time-freq                 }
%                       .icatimef     ICA time-freq             }
%                       .datitc       inter-trial coherence     }
%                       .icaitc       ICA inter-trial coherence }
%                       .datersp      ERSP data                 }
%                       .icaersp      ICA ERSP data             }
%                     Alternatively it can be a FieldTrip or EEGLab struct
%                     with time-locked or time-frequency data
%   'targets', t      Px1 targets for P samples; these will be stored in
%                     the output as ds.sa.targets (optional)
%   'chunks', c       Px1 chunks for P samples; these will be stored in the
%                     the output as ds.sa.chunks (optional)
%   'data_field', f   - For FieldTrip MEEG source dataset with multiple
%                       data fields (such as 'pow' and 'mom'), this sets
%                       which data is returned. (only for source data)
%                     - For EEGLAB 'ersp' data
%                        f='ersp'       the original data is returned
%                                       (without baseline correction),
%                                       with data for each channel,
%                                       frequency and time point.
%                                       Based on datasets generated by
%                                       EEGLAB that were inspected when
%                                       writing this function, it seems
%                                       that this data represents raw power
%                                       values.
%                        f='erspbase'   the baseline is returned, with data
%                                       for each channel and frequency
%                                       (but not time point)
%                        Note: this function does currently not support
%                              returning baseline-corrected data.
%   'trials', idx     Mx1 array with indices of trials to load (optional).
%                     If not provided then all trials are loaded. The
%                     output has a .samples field with the number of rows
%                     equal to numel(idx).
%
% Returns:
%   ds                dataset struct with the following fields
%     .samples        PxQ for P samples and Q features.
%     .sa.targets     Px1 sample targets (if provided)
%     .sa.chunks      Px1 sample chunks (if provided)
%     .a
%       .meeg
%         .sample_field   name of sample field. One of 'fourierspctrm',
%                         'powspctrm', or 'trial'.
%         .samples_type   'timelock' or 'timefreq'.
%         .samples_label  Usually 'rpt'; or the first field of .dimord
%                         for FieldTrip data
%       .dim
%         .labels     1xS cell struct with labels for the feature
%                     dimensions of the input. Usually this is
%                     {'chan','time'} or {'chan','freq','time'}.
%         .values     1xS cell struct with values associated with .labels.
%                     If the K-th value has N_K values, this means that
%                     the feature dimension .labels{K} takes the
%                     values in .values{K}. For example, if
%                     .labels{1}=='chan', then .values{1} contains the
%                     channel labels.
%     .fa
%       .{D}          if D==a.fdim.labels{K} is the label for the K-th
%                     feature dimension, then .{D} contains the
%                     indices referencing a.fdim.values. Thus, all values in
%                     .{D} are in the range 1:N_K if a.fdim.values{K} has
%                     N_K values, and the J-th feature has dimension value
%                     .dim.values{K}(.{D}(J)) in the K-th dimension.
%
% Notes:
%  - The resulting dataset can be mapped back to MEEG format using
%    cosmo_map2meeg
%  - if the input contains data from a single sample (such as an average)
%    the .sample_field is set to .trial, and mapping back to MEEG format
%    adds a singleton dimension to the .trial data output field.
%  - For single-subject MVPA of single trials using data preprocessed with
%    FieldTrip, consider setting, depending on the data type:
%       * timelock (ft_timelockanalysis): cfg.keeptrials = 'yes'
%       * timefreq (ft_timefreqanalysis): cfg.keeptrials = 'yes'
%       * source   (ft_sourceanalysis)  : cfg.keeptrials = 'yes' *and*
%                                                   cfg.rawtrials = 'yes'
%  - Most MVPA applications require that .sa.targets (experimental
%    condition of each sample) and .sa.chunks (partitioning of the samples
%    in independent sets) are set, either by using this function or
%    manually afterwards.
%  - If the input is a FieldTrip struct with a field .trialinfo, then this
%    field is present in .sa.trialinfo. Depending on the contents of
%    .trialinfo, this could be used to specify conditions in each trial.
%    For example, if the third column of .trialinfo contains an integer
%    specifying the condition of each trial, after running this function
%    one can do
%
%       ds.sa.targets=ds.sa.trialinfo(:,3)
%
%    to set the trial conditions.
%  - Implementation note: when loading EEGLAB data from a file, using the
%    'trials' option means that data from different channels are loaded
%    through different 'load' commands. When loading a subset of all
%    trials, the advantage of this implementation is that significant
%    less memory is needed compared to an alternative implementation in
%    which the full dataset is loaded and then the trials of interest
%    are selected through slicing. The disadvantage is that loading may
%    take longer, because the file is opened and closed multiple times. yet
%    this approach allows one to load subsets of trials from data files
%    that are larger than the available RAM.
%    Such memory reductions are currently not available for FieldTrip
%    data, as FieldTrip's data structures do not store data for different
%    channels in different variables.
%
% See also: cosmo_map2meeg
%
% #   For CoSMoMVPA's copyright information and license terms,   #
% #   see the COPYING file distributed with CoSMoMVPA.           #

    % Input parsing stuff

    defaults=struct();
    defaults.targets=[];
    defaults.chunks=[];

    params = cosmo_structjoin(defaults, varargin);

    if cosmo_check_dataset(filename,'meeg',false)
        % it is already a dataset
        ds=filename;
    else
        % get image format and verify it is supported
        img_format=get_img_format(filename);
        supported_image_formats=get_supported_image_formats();

        % check externals
        cosmo_check_external(supported_image_formats.(img_format).externals);

        % get the reader
        reader=supported_image_formats.(img_format).reader;

        % read the dataset
        ds=reader(filename, params);
    end

    % set targets and chunks
    ds=set_vec_sa(ds,'targets',params.targets);
    ds=set_vec_sa(ds,'chunks',params.chunks);


    % check consistency
    cosmo_check_dataset(ds,'meeg');

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% general helper functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function ds=set_vec_sa(ds, label, values)
    if isempty(values)
        return;
    end
    if numel(values)==1
        nsamples=size(ds.samples,1);
        values=repmat(values,nsamples,1);
    end
    ds.sa.(label)=values(:);


function img_format=get_img_format(filename)
    % helper functgion to detect image format based on filename.
    % uses 'get_supported_image_formats'.
    img_formats=get_supported_image_formats();

    fns=fieldnames(img_formats);
    for k=1:numel(fns)
        fn=fns{k};

        img_spec=img_formats.(fn);
        if img_spec.file_matcher(filename)
            img_format=fn;
            return
        end
    end
    error('Unknown image format');

function img_formats=get_supported_image_formats()
    % helper function to return the image format based on the filename
    img_formats=struct();

    % helper function to see if a filename ends with a certain string.
    % uses currying - who doesn't like curry?
    ends_with=@(ext) @(fn) ischar(fn) && ...
                        isempty(cosmo_strsplit(fn,ext,-1));
    ends_with_any=@(exts) @(fn) ischar(fn) && any(...
                            cellfun(@(x)isempty(...
                                        cosmo_strsplit(fn,x,-1)),exts));

    % eeglab txt files
    img_formats.eeglab_txt.file_matcher=ends_with('.txt');
    img_formats.eeglab_txt.reader=@read_eeglab_txt;
    img_formats.eeglab_txt.externals={};

    img_formats.eeglab.file_matcher=ends_with_any({'.daterp',...
                                                   '.icaerp',...
                                                   '.dattimef',...
                                                   '.icatimef',...
                                                   '.datitc',...
                                                   '.icaitc',...
                                                   '.datersp',...
                                                   '.icaersp'});
    img_formats.eeglab.reader=@read_eeglab;
    img_formats.eeglab.externals={};

    img_formats.eeglab_struct.file_matcher=@is_eeglab_struct;
    img_formats.eeglab_struct.reader=@convert_eeglab_struct;
    img_formats.eeglab_struct.externals={};



    % fieldtrip
    % XXX any .mat file is currently assumed to be a fieldtrip struct
    img_formats.ft.file_matcher=ends_with('.mat');
    img_formats.ft.reader=@read_ft;
    img_formats.ft.externals={};

    % fieldtrip matlab struct
    img_formats.ft_struct.file_matcher=@(x) is_ft_struct(x);
    img_formats.ft_struct.reader=@convert_ft;
    img_formats.ft_struct.externals={};


function ds=posthoc_slice_dataset_if_necessary(ds,opt)
    if ~isfield(opt,'trials')
        return;
    end

    trial_idx=opt.trials;
    verify_trial_params(size(ds.samples,1),trial_idx);
    ds=cosmo_slice(ds,trial_idx);

function verify_trial_params(nsamples,trial_idx)
    if ~isnumeric(trial_idx)
        error('.trials option must be numeric');
    end

    bad_msk=trial_idx<1 | ...
                trial_idx>nsamples | ...
                round(trial_idx)~=trial_idx;

    if any(bad_msk)
        bad_pos=find(bad_msk,1);
        error(['.trials option has illegal value at position %d; '...
                'all values must be in (1:%d)'],...
                bad_pos,nsamples);
    end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% fieldtrip helper function
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function ds=read_ft(filename, opt)
    % reads it from a .mat data file
    ft=importdata(filename);
    ds=convert_ft(ft, opt);


function ds=convert_ft(ft, opt)
    [data, samples_field, fdim]=get_ft_data(ft, opt);

    ds=cosmo_flatten(data, fdim.labels, fdim.values,2,...
                                'matrix_labels',get_ft_matrix_labels());
    ds.a.meeg.samples_field=samples_field;

    if is_ft_source_struct(ft)
        if isfield(ft,'inside')
            ds=apply_ft_source_inside(ds,fdim.labels,fdim.values,...
                                        ft.inside);
        end

        optional_fields={'dim','tri'};
        ds.a.meeg=copy_optional_fields(ds.a.meeg,ft,optional_fields);
    end

    ds.a.meeg=set_samples_label_explicitly_if_necessary(ds.a.meeg,ft);

    nsamples=size(ds.samples,1);
    ft_fields={'rpt','trialinfo','cumtapcnt'};
    ds.sa=copy_fields_for_matching_sample_size(ft,nsamples,ft_fields);

    ds=posthoc_slice_dataset_if_necessary(ds,opt);


function s=copy_optional_fields(s,source,optional_fields)
    for k=1:numel(optional_fields)
        key=optional_fields{k};
        if isfield(source,key)
            s.(key)=source.(key);
        end
    end

function a_meeg=set_samples_label_explicitly_if_necessary(a_meeg,ft)
    % deal with grand average data
    if ~cosmo_isfield(ft,'dimord')
        return;
    end

    first_label=cosmo_strsplit(ft.dimord,'_',1);

    if cosmo_match({first_label},{'subj'})
        a_meeg.samples_label=first_label;
    end


function [data,samples_field,fdim]=get_ft_data(ft,opt)
    [data, samples_field]=get_data_ft(ft,opt);
    [dim_labels,ft_dim_labels]=get_ft_dim_labels(ft, samples_field);

    nlabels=numel(dim_labels);
    dim_values=cell(nlabels,1);
    matrix_labels=get_ft_matrix_labels();
    for k=1:nlabels
        label=ft_dim_labels{k};
        value=ft.(label);
        if cosmo_match({label},matrix_labels)
            dim_values{k}=value';
        else
            dim_values{k}=value(:)';
        end
    end

    fdim.labels=dim_labels;
    fdim.values=dim_values;

    if is_ft_source_struct(ft)
        fdim=add_ft_mom_field_if_present(fdim, samples_field, size(data));
        [data,fdim]=fix_ft_lcmv_if_necessary(data,fdim);
    end


function [data,fdim]=fix_ft_lcmv_if_necessary(data,fdim)
    % FT LCMV does something weird for average data; the .avg.pow field
    % is 1xNCHAN for a NCHAN x NTIME field.
    % To address this:
    % - reshape the data
    % - average the time field
    dim_labels=fdim.labels;
    data_size=size(data);

    pos_pos=find(cosmo_match(dim_labels,'pos'));
    if ~isempty(pos_pos) && ... % has pos field
                pos_pos<numel(dim_labels)

        npos=size(fdim.values{pos_pos},2);
        next_dim_pos=pos_pos+1;

        if data_size(pos_pos+1)==1 && data_size(next_dim_pos+1)==npos
            new_data_size=data_size;

            % fix with next dimension
            new_data_size(pos_pos+1)=npos;
            new_data_size(next_dim_pos+1)=1;

            data=reshape(data,new_data_size);

            % the next dimension (typically time) is averaged if
            % necessary
            next_dim_value=fdim.values{next_dim_pos};
            if numel(next_dim_value)~=1
                fdim.values{next_dim_pos}=mean(next_dim_value);
            end

        end
    end



function fdim=add_ft_mom_field_if_present(fdim, samples_field, size_data)
    % insert .mom field for source struct
    samples_field_split=cosmo_strsplit(samples_field,'.');
    has_mom=numel(samples_field_split)==2 && ...
                    strcmp(samples_field_split{2},'mom');
    if has_mom
        ndim=size_data(3);
        fdim.labels=[fdim.labels(1);...
                                {'mom'};...
                                fdim.labels(2:end)];
        switch ndim
            case 1
                values={'xyz'};
            case 3
                values={'x','y','z'};
            otherwise
                error('Unsupported number of dimensions for %s: %d',...
                            samples_field,ndim);
        end
        fdim.values=[fdim.values(1);...
                                {values};...
                                fdim.values(2:end)];
    end


function labels=get_ft_matrix_labels()
    labels={'pos'};

function [cosmo_dim_labels,ft_dim_labels]=get_ft_dim_labels(ft, ...
                                                        samples_field)
    cosmo_ft_dim_labels={ 'pos','pos';...
                          'chan','label';...
                          'freq','freq';...
                          'time','time'};

    is_source=is_ft_source_struct(ft);

    if is_source==isfield(ft,'dimord')
        error('Weird fieldtrip data: .pos and .dimord are not compatible');
    end

    if is_source
        % source data
        keys=fieldnames(ft);
        labels_msk=cosmo_match(cosmo_ft_dim_labels(:,2),keys);
    else


        dimord_labels=cosmo_strsplit(ft.dimord,'_');

        sample_field=get_sample_dimord_field(ft);
        sample_msk=cosmo_match(dimord_labels, sample_field);

        dimord_labels_without_sample=dimord_labels(~sample_msk);

        labels_msk=cosmo_match(cosmo_ft_dim_labels(:,1),...
                                dimord_labels_without_sample);
    end

    ft_dim_labels=cosmo_ft_dim_labels(labels_msk,2);
    cosmo_dim_labels=cosmo_ft_dim_labels(labels_msk,1);





function sample_field=get_sample_dimord_field(ft)
    sample_field='';

    sample_dimord_fields={'rpt','trial','subj'};
    if isfield(ft,'dimord')
        ft_dimord_fields=cosmo_strsplit(ft.dimord,'_');
        msk=cosmo_match(ft_dimord_fields,sample_dimord_fields);
        if any(msk)
            assert(sum(msk)==1);
            sample_field=sample_dimord_fields{msk};
        end
    end


function sa=copy_fields_for_matching_sample_size(ft,nsamples,keys)
    n=numel(keys);

    sa=struct();
    for k=1:n
        key=keys{k};
        if isfield(ft,key)
            value=ft.(key);
            if size(value,1)==nsamples
                sa.(key)=value;
            end
        end
    end







function ds=apply_ft_source_inside(ds,dim_labels,dim_values,ft_inside)
    ndim=numel(dim_labels);
    dim_sizes=cellfun(@numel,dim_values);

    pos_idx=find(cosmo_match(dim_labels,'pos'),1);
    assert(~isempty(pos_idx),['this function should only be called '...
                                'with source datasets']);

    if isnumeric(ft_inside)
        pos=ds.a.fdim.values{pos_idx};
        [three,npos]=size(pos);
        assert(three==3);
        inside_mask=false(npos,1);
        inside_mask(ft_inside)=true;
    elseif islogical(ft_inside)
        inside_mask=ft_inside;
    else
        error('.inside must either be numeric or logical');
    end


    inside_vec_size=[1 ones(1,ndim)];
    inside_vec_size(1+pos_idx)=numel(inside_mask);
    inside_array_vec=reshape(inside_mask, inside_vec_size);

    other_dim_size=[1 dim_sizes(:)'];
    other_dim_size(1+pos_idx)=1;

    inside_array=repmat(inside_array_vec,other_dim_size);
    inside_ds=cosmo_flatten(inside_array, dim_labels, dim_values,2,...
                                         'matrix_labels',{'pos'});
    ds=cosmo_slice(ds,inside_ds.samples,2);




function [data, sample_field]=get_data_ft(ft,opt)
    if is_ft_source_struct(ft)
        [data, sample_field]=get_source_data_ft(ft, opt);
    else
        [data, sample_field]=get_sensor_data_ft(ft);
    end

function [data, sample_field]=get_source_data_ft(ft, opt)
    main_fields={'trial','avg'};
    sub_fields={'pow','mom'};

    msk_main=cosmo_match(main_fields,fieldnames(ft));
    switch sum(msk_main)
        case 0
            error('No data found in source struct');

        case 1
            % ok

        otherwise
            error('Multiple data fields found in source struct');
    end

    main_field=main_fields{msk_main};
    main_data=ft.(main_field);

    sub_field_option='data_field';
    if isfield(opt,sub_field_option)
        sub_field=opt.(sub_field_option);
    else
        msk_sub=cosmo_match(sub_fields,fieldnames(main_data));

        switch sum(msk_sub)
            case 0
                error('No data found in .%s source struct', main_field);

            case 1
                sub_field=sub_fields{msk_sub};

            otherwise
                error(['Multiple data fields found in .%s source '...
                        'struct: ''%s''. To select one of these, use '...
                        'the ''%s'' option'],...
                        main_field,...
                        cosmo_strjoin(sub_fields(msk_sub),''', '''),...
                        sub_field_option);
        end
    end

    data=extract_source_data_array_ft(main_data, sub_field);
    sample_field=sprintf('%s.%s',main_field,sub_field);

function data=extract_source_data_array_ft(main_data, sub_field)
    nsamples=numel(main_data);
    first_data=main_data(1).(sub_field);
    data_cell=cell(nsamples,1);


    switch sub_field
        case 'mom'
            npos=numel(first_data);
            data_inside_pos=find(~cellfun(@isempty,first_data));
            ninside=numel(data_inside_pos);

            [nmom, nfeatures]=size(first_data{data_inside_pos(1)});

            data_arr_empty=NaN(1,npos,nmom,nfeatures);

            for j=1:nsamples
                data_cell_cell=main_data(j).(sub_field);
                data_arr=data_arr_empty;

                for k=1:ninside
                    pos=data_inside_pos(k);
                    data_arr(1,pos,:,:)=data_cell_cell{pos};
                end

                data_cell{j}=data_arr;
            end


        case 'pow'
            feature_size=size(first_data);

            for j=1:nsamples
                data_arr=main_data(j).(sub_field);
                data_cell{j}=reshape(data_arr,[1 feature_size]);
            end

        otherwise
            error('not supported sub_field: %s', sub_field);
    end

    data=cat(1,data_cell{:});




function [data, sample_field]=get_sensor_data_ft(ft)
    % order precedence: if .trial and .avg both exist, take .trial
    sample_fields_in_order={'trial','individual',...
                            'fourierspctrm','powspctrm','avg'};
    msk=cosmo_match(sample_fields_in_order, fieldnames(ft));

    if ~any(msk)
        error(['No supported data field found in sensor data struct. '...
                'If this data struct was generated by FieldTrip, '...
                'consider contacting CoSMoMVPA''s authors.']);
    end

    i=find(msk,1);
    sample_field=sample_fields_in_order{i};

    data=ft.(sample_field);

    if isempty(get_sample_dimord_field(ft))
                        % no 'rpt_...' or 'subj_'
        data=reshape(data,[1 size(data)]);
    end



function tf=is_ft_source_struct(ft)
    tf=isstruct(ft) && isfield(ft,'pos');

function tf=is_ft_sensor_struct(ft)
    tf=isfield(ft,'dimord') && isfield(ft,'label');

function tf=is_ft_struct(ft)
    tf=is_ft_source_struct(ft) || is_ft_sensor_struct(ft);



%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% eeglab struct helper function
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function tf=is_eeglab_struct(s)
    tf=isstruct(s) && ...
            isfield(s,'times') && ...
            isfield(s,'datatype');


function ds=read_eeglab(fn,opt)
    if isfield(opt,'trials')
        % can have significant reduction in memory requirements
        % at the expense of loading different parts of the same file
        % multiple times.
        s=eeglab_load_with_trials(fn,opt.trials);
        opt=rmfield(opt,'trials');
    else
        s=load(fn,'-mat');
    end
    ds=convert_eeglab_struct(s,opt);

function s=eeglab_load_with_trials(fn,trial_idx)
    % can save memory by loaded each channel seperately
    s_minimal=load(fn,'-mat','datatype');
    has_freq=helper_eeglab_get_freq_info(s_minimal);

    w=whos('-file',fn);
    s_surrogate=struct();
    for k=1:numel(w)
        s_surrogate.(w(k).name)=[];
    end

    [chan_prefix,chan_suffix]=eeglab_get_chan_pre_suffix(s_surrogate);
    [chan_labels]=eeglab_get_chan_labels(s_surrogate,...
                                    chan_prefix,chan_suffix);
    other_labels=setdiff({w.name},chan_labels);
    assert(numel(other_labels)<numel(w));

    s=load(fn,'-mat',other_labels{:});
    n_chan=numel(chan_labels);
    for k=1:n_chan
        label=chan_labels{k};
        data_struct=load(fn,'-mat',label);
        data=data_struct.(label);

        sliced_data=helper_eeglab_slice_chan(data,trial_idx,has_freq);
        s.(label)=sliced_data;
    end


function result=helper_eeglab_slice_chan(data, trial_idx, has_freq)
    assert(islogical(has_freq));

    if has_freq
        trial_dim=3;
    else
        trial_dim=1;
    end

    % ensure enough trials
    n_trials=size(data,trial_dim);
    verify_trial_params(n_trials,trial_idx);

    if has_freq
        result=data(:,:,trial_idx);
    else
        assert(numel(size(data))<=2);
        result=data(trial_idx,:);
    end

function [has_freq,freq_label,freq_values]=helper_eeglab_get_freq_info(s)
    samples_type=eeglab_get_samples_type(s);
    has_freq=strcmp(samples_type,'timefreq');
    if nargout<=1
        return;
    end

    if has_freq
        freq_label={'freq'};
        freq_values={s.freqs};
    else
        freq_label={};
        freq_values={};
    end



function ds=convert_eeglab_struct(s, opt)
    samples_type=eeglab_get_samples_type(s);
    [has_freq,freq_label,freq_values]=helper_eeglab_get_freq_info(s);
    freq_size=cellfun(@numel,freq_values);

    [chan_prefix,chan_suffix]=eeglab_get_chan_pre_suffix(s, opt);

    is_baseline=~isempty(regexp(chan_suffix,'base$','once'));
    if is_baseline
        % because the baseline is computed over time
        time_size=[];
        time_values={};
        time_label={};
    else
        time_values={s.times};
        time_label={'time'};
        time_size=numel(time_values{1});
    end

    % set channels
    [eeglab_labels,chan_values]=eeglab_get_chan_labels(...
                                        s,chan_prefix,chan_suffix);
    n_chan=numel(chan_values);

    if isfield(s,'chanlabels')
        assert(numel(s.chanlabels)==n_chan);
    end



    data_cell=cell(n_chan,1);
    for k=1:n_chan
        % load data for each channel
        key=eeglab_labels{k};
        value=s.(key);

        if has_freq
            % it seems that for freq data, single trial data is the last
            % dimension, whereas for erp data, single trial data is the first
            % dimension. In any case we want to make single trial data the
            % first dimension
            has_trial_dim=size(value,3)~=1;
            if has_trial_dim
                value=shiftdim(value,2);
            else
                value=shiftdim(value,-1); % insert singleton dimension
            end
        end

        n_samples=size(value,1);
        size_chan_singleton=[n_samples,1,[freq_size],time_size];

        value_rs=reshape(value,size_chan_singleton);
        data_cell{k}=value_rs;
    end

    meeg_parameters=s.parameters;
    clear s;

    data=cat(2,data_cell{:});
    clear data_cell;

    ds=cosmo_flatten(data,[{chan_prefix},freq_label,time_label],...
                          [{chan_values},freq_values,time_values]);
    clear data;

    ds.sa=struct();
    ds.a.meeg.samples_field='trial';
    ds.a.meeg.samples_type=samples_type;
    ds.a.meeg.samples_label='rpt';

    ds.a.meeg.parameters=meeg_parameters;

    ds=posthoc_slice_dataset_if_necessary(ds,opt);


function [chan_prefix,chan_suffix]=eeglab_get_chan_pre_suffix(s,opt)
    keys=fieldnames(s);

    numeric_infix='1';
    pat=['^(\D+)' numeric_infix '([\D_]*$)'];
    matches=regexp(keys,pat,'once','tokens');

    match_idx=find(~cellfun(@isempty,matches));

    if numel(match_idx)==1
        unique_match=matches{match_idx};
    else
        unique_match=eeglab_select_idx_chan_pref_suffix(matches,opt);
    end

    chan_prefix=unique_match{1};
    chan_suffix=unique_match{2};


function match=eeglab_select_idx_chan_pref_suffix(all_matches,opt)
% typical use case is data with *_erspbase and _ersp data
    key='data_field';

    % 'erspboot' are bootstrap estimates - no idea how to deal with these
    % so for now they are not supported
    not_supported_values={'erspboot'};

    % get fieldnames to keep
    get_match_name=@(x)x{2}(2:end);
    match_func=@(x)~isempty(x) && ...
                    ~cosmo_match({get_match_name(x)},not_supported_values);
    match_msk=cellfun(match_func,all_matches);
    matches=all_matches(match_msk);

    valid_values=cellfun(get_match_name,matches,...
                           'UniformOutput',false);

    suffix=sprintf('Valid options for the ''%s'' option are ''%s''.',...
                         key,cosmo_strjoin(valid_values,''', '''));

    % try to be helpful
    is_ersp=all(cosmo_match({'erspbase';'ersp'}, valid_values));
    if is_ersp
        suffix=sprintf(['%s\nThis data looks like an EEGLAB '...
                    'ERSP data structure. Note '...
                    'that the ''ersp'' option returns a '...
                    'raw dataset (presumably without baseline, ',...
                    'correction), whereas ''erspbase'' returns the '...
                    ' baseline '...
                    'values themselves (that can be used for baseline '...
                    'correction). It is currently not possible to '...
                    'return baseline corrected data with this '...
                    'function.'],suffix);
    end

    if ~isfield(opt,key)
        error('The ''%s'' option is required. %s', key, suffix);
    end

    value=opt.(key);
    if ~ischar(value)
        error('The ''%s'' option must be a string. %s',key, suffix);
    end

    idx=find(cosmo_match(valid_values,value));

    if isempty(idx)
        error(suffix);
    end
    match=matches{idx};



function [eeglab_labels,cosmo_labels]=eeglab_get_chan_labels(...
                                s,chan_prefix,chan_suffix)

    make_eeglab_label=@(idx)sprintf('%s%d%s',chan_prefix,idx,chan_suffix);
    % see how many labels there are
    count=0;
    while true
        label=make_eeglab_label(count+1);
        if ~isfield(s,label)
            break
        end
        count=count+1;
    end

    idxs=1:count;

    eeglab_labels=arrayfun(make_eeglab_label,idxs,'UniformOutput',false);
    if strcmp(chan_prefix,'comp')
        assert(~isfield(s,'chanlabels'));
        make_comp_label=@(idx)sprintf('comp%d',idx);
        cosmo_labels=arrayfun(make_comp_label,idxs,'UniformOutput',false);
    else
        cosmo_labels=s.chanlabels;
    end






function samples_type=eeglab_get_samples_type(s)
    mapping={'erp','timelock';...
            'timef','timefreq';...
            'itc','timefreq';...
            'ersp','timefreq';...
            'erspbase','baseline'};
    for k=1:size(mapping,1)
        row=mapping(k,:);
        if strcmp(lower(s.datatype),row{1})
            samples_type=row{2};
            return;
        end
    end

    error('unsupported datatype mapping ''%s''',s.datatype);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% eeglab txt helper function
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function ds=read_eeglab_txt(fn, opt)
    % reads eeglab time series data. returns data in fieldtrip-like format
    fid=fopen(fn);

    header_line=fgetl(fid); % read header
    chan_labels=cosmo_strsplit(header_line,'\t');
    chan_labels=chan_labels(2:(end-1)); % omit first & last bogus element

    nchan=numel(chan_labels);
    data_pat=cosmo_strjoin(repmat({'%n'},1,nchan+1),' ');

    % read data from file
    cell_data=textscan(fid,data_pat);

    % check all data was read
    neg_one=fgetl(fid);
    if neg_one~=-1
        error('Could not read all data from %s', fn);
    end

    %%%%%%%%%%%%%%%
    % data consistency checks

    % timepoints are in the first column, data in other columns
    timepoints=cell_data{1};
    nrows=numel(timepoints);

    [unused,t_trial]=cosmo_index_unique(timepoints);
    ntime=numel(t_trial);

    if mod(nrows,ntime)~=0 || ...
                ~all(all(bsxfun(@eq,reshape(timepoints,ntime,[]),...
                                  t_trial)))
        error('Data not contiguous or unexpected order of time points');
    end

    % number of trials
    ntrial=nrows/ntime;


    %%%%%%%%%%%%%%%
    % put the data in 3D array
    data=zeros(ntrial,nchan,ntime);
    for chan=1:nchan
        chan_data=cell_data{chan+1}; % skip first column as it has timepoints
        data(:,chan,:)=reshape(chan_data,ntime,ntrial)';
    end

    %%%%%%%%%%%%%%%
    % flatten and make it a dataset
    % (convert miliseconds to seconds along the way)
    dim_labels={'chan';'time'};
    dim_values={chan_labels(:)';.001*t_trial(:)'};

    % make a dataset
    ds=cosmo_flatten(data, dim_labels, dim_values);

    % set datatype to timelock-ish in fieldtrip-compatible way
    ds.a.meeg.samples_field='trial';
    ds.a.meeg.samples_type='timelock';
    ds.a.meeg.samples_label='rpt';

    % set sample info
    ds.sa.(ds.a.meeg.samples_label)=(1:ntrial)';

    ds=posthoc_slice_dataset_if_necessary(ds,opt);