function analysis_results = analyzeG2Structures(g2_results, opts)
%% analyzeG2Structures
% Computes peak anisotropy of g² correlation matrices using ROI and boundary
% analysis, and fits an oriented two-mode Gaussian aligned with the
% structure's principal axis within the ROI.
%
% Inputs:
%   g2_results : struct with fields
%       - g2_matrices : cell array of 2D matrices
%       - dx_phys     : cell array of x-coordinates (µm)
%       - dy_phys     : cell array of y-coordinates (µm)
%   opts : struct with fields
%       - roi.center   : [x0, y0] center of ROI box (µm)
%       - roi.size     : [w, h] width and height (µm)
%       - roi.angle    : rotation angle (radians, CCW)
%       - threshold    : fraction of peak maximum to define core (default 0.5)
%       - skipLivePlot : true/false (default false)
%       - fitDeviationThreshold : max normalized RMS deviation for accepting fit
%       - boundaryPad : extra pixels around boundary to allow Gaussian peaks
%
% Outputs:
%   analysis_results : struct with fields
%       - anisotropy_vals : numeric array
%       - peak_centroid   : {N×1} fitted centroids [x, y]
%       - ellipse_params  : {N×1} fitted parameters + angle
%       - boundary_coords : {N×1} [x, y] boundary points

if ~isfield(opts, "skipLivePlot"), opts.skipLivePlot = false; end
if ~isfield(opts, "threshold"), opts.threshold = 0.5; end
if ~isfield(opts, "font"), opts.font = 'Arial'; end
if ~isfield(opts, "fitDeviationThreshold"), opts.fitDeviationThreshold = 0.2; end
if ~isfield(opts, "boundaryPad"), opts.boundaryPad = 2; end

N_images = numel(g2_results.g2_matrices);

analysis_results                  = struct();
analysis_results.anisotropy_vals  = zeros(1, N_images);
analysis_results.peak_centroid    = cell(1, N_images);
analysis_results.boundary_coords  = cell(1, N_images);
analysis_results.ellipse_params   = cell(1, N_images);

% ROI definition
x0        = opts.roi.center(1);
y0        = opts.roi.center(2);
w         = opts.roi.size(1);
h         = opts.roi.size(2);
roi_theta = opts.roi.angle;

for k = 1:N_images
    g2_matrix = g2_results.g2_matrices{k};
    dx_phys   = g2_results.dx_phys{k};
    dy_phys   = g2_results.dy_phys{k};
    [X, Y] = meshgrid(dx_phys, dy_phys);

    %% ---- Core ROI extraction (rotated rectangle) ----
    Xc = X - x0;
    Yc = Y - y0;
    Xr =  cos(roi_theta)*Xc + sin(roi_theta)*Yc;
    Yr = -sin(roi_theta)*Xc + cos(roi_theta)*Yc;
    roi_mask = (abs(Xr) <= w/2) & (abs(Yr) <= h/2);

    x_roi = Xr(roi_mask);
    y_roi = Yr(roi_mask);
    z_roi = g2_matrix(roi_mask);

    %% ---- Threshold ROI to define core region ----
    g2_roi = zeros(size(g2_matrix));
    g2_roi(roi_mask) = g2_matrix(roi_mask);
    thresh_val = opts.threshold * max(g2_roi(:));
    BW = g2_roi >= thresh_val;

    if ~any(BW(:))
        warning('No peak found in ROI for image %d', k);
        analysis_results.peak_centroid{k} = [NaN, NaN];
        analysis_results.anisotropy_vals(k) = NaN;
        analysis_results.ellipse_params{k} = nan(1,7);
        analysis_results.boundary_coords{k} = [NaN, NaN];
        continue;
    end

    %% ---- Boundary coordinates (edge detection inside ROI) ----
    B = bwboundaries(BW);
    boundary = B{1};
    x_bound = dx_phys(boundary(:,2));
    y_bound = dy_phys(boundary(:,1));
    analysis_results.boundary_coords{k} = [x_bound, y_bound];

    %% ---- Compute centroid and orientation (use PCA on boundary in ROI-local frame) ----
    % Transform boundary points to ROI-local coordinates (same local coords as Xr,Yr)
    Xc_b = x_bound - x0;
    Yc_b = y_bound - y0;
    Xr_b =  cos(roi_theta)*Xc_b + sin(roi_theta)*Yc_b;
    Yr_b = -sin(roi_theta)*Xc_b + cos(roi_theta)*Yc_b;

    % PCA on boundary points in ROI-local frame (robust and simple)
    boundary_mean = [mean(Xr_b), mean(Yr_b)];
    XYb = [Xr_b - boundary_mean(1), Yr_b - boundary_mean(2)];
    try
        coeff = pca(XYb);
        major = coeff(:,1);
        angle_region = atan2(major(2), major(1)); % angle in ROI-local coords (radians)
    catch
        % fallback: use roi_theta as orientation if PCA fails
        angle_region = 0;
    end

    % We'll define longitudinal axis in ROI-local coordinates as angle_region,
    % transverse axis is angle_region + pi/2.

    %% ---- Use points inside boundary for 1D profile (use ROI-local coords) ----
    % We already have axis-aligned ROI samples x_roi, y_roi, z_roi in ROI-local coords.
    xData_local = x_roi;
    yData_local = y_roi;
    zData       = z_roi;
    
    %% ---- Interpolate onto regular axis-aligned grid in ROI-local coords ----
    Nx = ceil(w*10); Ny = ceil(h*10);
    xq = linspace(-w/2, w/2, Nx);
    yq = linspace(-h/2, h/2, Ny);
    [Xq, Yq] = meshgrid(xq, yq);
    Zq = griddata(xData_local, yData_local, zData, Xq, Yq, 'cubic');

    % If griddata produced NaNs in large patches, fill with nearest to avoid empty bins
    if any(isnan(Zq(:)))
        Zq = fillmissing(Zq,'nearest');
    end

    % --- Build rotated coordinates so major axis aligns with new X'
    theta = angle_region; % ROI-local angle of major axis
    Rrot = [cos(theta), sin(theta); -sin(theta), cos(theta)]; % rotation that maps [x;y] -> [x';y'] where x' along major
    XY = [Xq(:)'; Yq(:)'];
    XY_rot = Rrot * XY;
    Xrot = reshape(XY_rot(1,:), size(Xq));
    Yrot = reshape(XY_rot(2,:), size(Yq));

    % --- Compute 1D longitudinal profile by binning along X' (major axis)
    nbins_long = size(Xq,1); % choose number of bins ~ Ny (vertical resolution)
    x_edges_long = linspace(min(Xrot(:)), max(Xrot(:)), nbins_long+1);
    x_centers_long = 0.5*(x_edges_long(1:end-1)+x_edges_long(2:end));
    prof_long = nan(1, nbins_long);
    for ii = 1:nbins_long
        mask_bin = Xrot >= x_edges_long(ii) & Xrot < x_edges_long(ii+1);
        vals = Zq(mask_bin);
        if isempty(vals)
            prof_long(ii) = NaN;
        else
            prof_long(ii) = mean(vals,'omitnan');
        end
    end

    % --- Compute 1D transverse profile by binning along Y' (minor axis)
    nbins_trans = size(Xq,2);
    y_edges_trans = linspace(min(Yrot(:)), max(Yrot(:)), nbins_trans+1);
    y_centers_trans = 0.5*(y_edges_trans(1:end-1)+y_edges_trans(2:end));
    prof_trans = nan(1, nbins_trans);
    for ii = 1:nbins_trans
        mask_bin = Yrot >= y_edges_trans(ii) & Yrot < y_edges_trans(ii+1);
        vals = Zq(mask_bin);
        if isempty(vals)
            prof_trans(ii) = NaN;
        else
            prof_trans(ii) = mean(vals,'omitnan');
        end
    end

    % --- Prepare x-axis positions for profile centers (in ROI-local units)
    xcent_long = x_centers_long;      % positions along major axis (µm)
    xcent_trans = y_centers_trans;    % positions along minor axis (µm)

    % --- Remove NaNs from profiles
    valid_long = ~isnan(prof_long);
    x_long = xcent_long(valid_long);
    y_long = prof_long(valid_long);

    valid_trans = ~isnan(prof_trans);
    x_trans = xcent_trans(valid_trans);
    y_trans = prof_trans(valid_trans);

    % --- If not enough points, skip
    if numel(x_long) < 8 || numel(x_trans) < 8
        warning('Too few points for longitudinal/transverse fits for image %d', k);
        analysis_results.peak_centroid{k} = [NaN, NaN];
        analysis_results.anisotropy_vals(k) = NaN;
        analysis_results.ellipse_params{k} = nan(1,7);
        continue;
    end

    %% ---- Fit two-Gaussian to longitudinal profile (use your working 1D fit) ----
    twoGauss1D = @(p,x) ...
        p(1) * exp(-0.5*((x - p(2))/max(p(3),1e-6)).^2) + ... % Gaussian 1
        p(4) * exp(-0.5*((x - p(5))/max(p(6),1e-6)).^2) + ... % Gaussian 2
        p(7);                                                % offset
    % initial guesses (longitudinal)
    [~, mainIdxL] = max(y_long);
    A1_guessL     = y_long(mainIdxL);
    mu1_guessL    = x_long(mainIdxL);
    sigma1_guessL = range(x_long)/10;
    A2_guessL     = 0.8 * A1_guessL;
    mu2_guessL    = mu1_guessL + range(x_long)/5;
    sigma2_guessL = sigma1_guessL;
    offset_guessL = min(y_long);
    p0L = [A1_guessL, mu1_guessL, sigma1_guessL, A2_guessL, mu2_guessL, sigma2_guessL, offset_guessL];
    lbL = [0, min(x_long), 1e-6, 0, min(x_long), 1e-6, -Inf];
    ubL = [2, max(x_long), range(x_long), 2, max(x_long), range(x_long), Inf];
    opts_lsq = optimoptions('lsqcurvefit','Display','off','MaxFunctionEvaluations',1e4,'MaxIterations',1e4);
    try
        pFitL = lsqcurvefit(twoGauss1D, p0L, x_long, y_long, lbL, ubL, opts_lsq);
    catch
        warning('Longitudinal 1D fit failed for image %d, using initial guess.', k);
        pFitL = p0L;
    end

    %% ---- Fit two-Gaussian to transverse profile (use same logic) ----
    [~, mainIdxT] = max(y_trans);
    A1_guessT     = y_trans(mainIdxT);
    mu1_guessT    = x_trans(mainIdxT);
    sigma1_guessT = range(x_trans)/10;
    A2_guessT     = 0.8 * A1_guessT;
    mu2_guessT    = mu1_guessT + range(x_trans)/5;
    sigma2_guessT = sigma1_guessT;
    offset_guessT = min(y_trans);
    p0T = [A1_guessT, mu1_guessT, sigma1_guessT, A2_guessT, mu2_guessT, sigma2_guessT, offset_guessT];
    lbT = [0, min(x_trans), 1e-6, 0, min(x_trans), 1e-6, -Inf];
    ubT = [2, max(x_trans), range(x_trans), 2, max(x_trans), range(x_trans), Inf];
    try
        pFitT = lsqcurvefit(twoGauss1D, p0T, x_trans, y_trans, lbT, ubT, opts_lsq);
    catch
        warning('Transverse 1D fit failed for image %d, using initial guess.', k);
        pFitT = p0T;
    end

    %% ---- Determine anisotropy from the two fits (ratio of widths) ----
    sigma_long = mean([pFitL(3), pFitL(6)]);
    sigma_trans = mean([pFitT(3), pFitT(6)]);
    anisotropy = sigma_trans / sigma_long; % transverse / longitudinal (ratio)
    % store centroid roughly as ROI-local centroid rotated back to lab coords
    centroid_local_roi = boundary_mean'; % mean of boundary in ROI-local coords
    % rotate back to lab frame
    rotBack = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
    centroid_lab = rotBack * centroid_local_roi + [x0; y0];
    centroid_lab = centroid_lab(:)';

    analysis_results.peak_centroid{k} = centroid_lab;
    analysis_results.anisotropy_vals(k) = anisotropy;
    analysis_results.ellipse_params{k} = [pFitL, pFitT, angle_region];

    %% ---- Visualization ----
    if ~opts.skipLivePlot
        fig = figure(100); clf;
        set(fig,'Color','w','Position',[100 100 1600 450]);
        tiledlayout(1,3,'Padding','compact','TileSpacing','compact');

        % --- Panel 1: 2D g² map + ROI + boundary
        nexttile;
        imagesc(dx_phys, dy_phys, g2_matrix);
        axis image; set(gca,'YDir','normal'); colormap(Colormaps.coolwarm()); colorbar; hold on;

        % ROI bounding box (red dashed)
        rect = [-w/2, -h/2; w/2, -h/2; w/2, h/2; -w/2, h/2; -w/2, -h/2];
        R = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
        rect_rot = (R * rect')';
        plot(rect_rot(:,1)+x0, rect_rot(:,2)+y0, 'r--', 'LineWidth',2);

        % Detected peak boundary (green solid)
        if ~isempty(x_bound)
            plot(x_bound, y_bound, 'g-', 'LineWidth',2);
        end

        xlabel('\Deltax (\mum)'); ylabel('\Deltay (\mum)');
        title(sprintf('Image %d | 2D g^2', k));

        % --- Panel 2: Longitudinal 1D profile and fit
        nexttile;
        plot(x_long, y_long, 'k.-', 'DisplayName','Data'); hold on;
        xFine = linspace(min(x_long), max(x_long), 500);
        yFitL = twoGauss1D(pFitL, xFine);
        plot(xFine, yFitL, 'r-', 'LineWidth',1.5,'DisplayName','Fit');
        title('Longitudinal profile','FontName',opts.font);
        xlabel('x_{longitudinal} (\mum)');
        ylabel('g^2');
        legend('Location','northeast'); grid on;

        % --- Panel 3: Transverse 1D profile and fit
        nexttile;
        plot(x_trans, y_trans, 'b.-', 'DisplayName','Data'); hold on;
        xFineT = linspace(min(x_trans), max(x_trans), 500);
        yFitT = twoGauss1D(pFitT, xFineT);
        plot(xFineT, yFitT, 'r-', 'LineWidth',1.5,'DisplayName','Fit');
        title('Transverse profile','FontName',opts.font);
        xlabel('x_{transverse} (\mum)');
        ylabel('g^2');
        legend('Location','northeast'); grid on;

        drawnow;
    end
end

end
