function S = readlog(filename)
%READ_EYECOIL_CAL_LOG Read eye-coil calibration log (header + trial table).
%
% Output struct S:
%   S.meta   : struct with header fields (e.g., Exp_ExpName, Sig_Fsample, ...)
%   S.table  : table with one row per trial, columns for each stimulus block
%
% Works for files like MARK10.LOG where:
%   - header lines start with '#', contain '='
%   - table header line starts with 'Nr'
%   - table data lines start with '#<trialnr> ...'

    if nargin < 1
        error('Provide a filename.');
    end

    % --- read all lines
    raw = readlines(filename);
    raw = strip(raw, "right");           % keep internal spacing, remove trailing
    raw = raw(raw ~= "");                % drop empty lines

    % --- find the table header line (starts with "Nr")
    iHdr = find(startsWith(raw, "Nr"), 1, "first");
    if isempty(iHdr)
        error('Could not find the table header line starting with "Nr".');
    end

    headerLines = raw(1:iHdr-1);
    tableHeader = raw(iHdr);
    dataLines   = raw(iHdr+1:end);

    % --- parse header into a meta struct
    meta = struct();
    for i = 1:numel(headerLines)
        ln = headerLines(i);
        if ~startsWith(ln, "#") || ~contains(ln, "=")
            continue;
        end
        ln = extractAfter(ln, 1); % remove leading '#'
        parts = split(ln, "=");
        if numel(parts) < 2, continue; end

        key = strtrim(parts(1));
        val = strtrim(join(parts(2:end), "=")); % in case '=' appears in value

        % make a safe MATLAB fieldname: '#Exp.ExpName' -> 'Exp_ExpName'
        key = replace(key, ".", "_");
        key = matlab.lang.makeValidName(key);

        % try numeric conversion; otherwise keep string
        num = str2double(val);
        if ~isnan(num)
            meta.(key) = num;
        else
            meta.(key) = char(val);
        end
    end

    % --- determine number of stimulus "blocks" from Ntarget if present
    nTarget = [];
    if isfield(meta, "Exp_Ntarget")
        nTarget = meta.Exp_Ntarget;
    end

    % --- build variable names (assumes columns: Nr + (Mode R Phi On Off Int Att) per block)
    baseNames = ["Mode","R","Phi","On","Off","Int","Att"];
    if ~isempty(nTarget) && nTarget > 0
        varNames = "Nr";
        for k = 1:nTarget
            varNames = [varNames, baseNames + string(k)]; %#ok<AGROW>
        end
    else
        % fallback: infer number of numeric columns from the first data line
        first = dataLines(find(startsWith(dataLines,"#"),1,"first"));
        first = extractAfter(first, 1); % remove '#'
        nums  = sscanf(first, '%f');
        ncol  = numel(nums);
        % assume first col is Nr, remaining are groups of 7
        nBlocks = floor((ncol-1)/7);
        varNames = "Nr";
        for k = 1:nBlocks
            varNames = [varNames, baseNames + string(k)]; %#ok<AGROW>
        end
        % if there are leftovers, name them generically
        if numel(varNames) < ncol
            for j = (numel(varNames)+1):ncol
                varNames(end+1) = "Var" + j; %#ok<AGROW>
            end
        end
    end

    % --- collect numeric matrix from all data lines
    M = nan(numel(dataLines), numel(varNames));
    keep = false(numel(dataLines),1);

    for i = 1:numel(dataLines)
        ln = dataLines(i);
        if ~startsWith(ln, "#")
            continue; % skip anything non-data
        end
        ln = extractAfter(ln, 1); % remove leading '#'
        nums = sscanf(ln, '%f');

        if isempty(nums)
            continue;
        end

        % accept rows that match, or truncate/pad to fit
        row = nan(numel(varNames),1);
        n = min(numel(nums), numel(varNames));
        row(1:n) = nums(1:n);

        M(i,:) = row;
        keep(i) = true;
    end

    M = M(keep,:);
    T = array2table(M, 'VariableNames', cellstr(varNames));

    % --- package output
    S = struct();
    S.meta  = meta;
    S.table = T;

    % store original header line too (sometimes useful)
    S.tableHeaderLine = char(tableHeader);
end