function varargout = gtReconstructGrainCluster(grain_ids, phase_id, parameters, varargin)
% FUNCTION gtReconstructGrainCluster cluster 6D reconstructions on a GPU machine
%     gtReconstructGrainCluster(grainIDs, phaseID, [parameters])
%     -------------------------------------------------------
    if (~exist('parameters', 'var') || isempty(parameters))
        parameters = gtLoadParameters();
    end

    conf = struct( ...
        'ospace_resolution', [], ...
        'ospace_lims', [], ...
        'extra_output', false, ...
        'save', true );
    [conf, ~] = parse_pv_pairs(conf, varargin);

    rec_opts = gtReconstruct6DGetParamenters(parameters);
    if (~isempty(conf.ospace_resolution))
        rec_opts.ospace_resolution = conf.ospace_resolution;
    end

    % handling input, and making sure that things are called by their name
    if (isstruct(grain_ids))
        cl = grain_ids;
    else
        fprintf('Loading the cluster file..')
        c = tic();
        cl = gtLoadCluster(phase_id, grain_ids);
        fprintf('\b\b: Done in %g seconds.\n', toc(c))
    end

    if (isfield(cl, 'samp_ors'))
        % this means it is a twin cluster, but this is the only place where
        % it matters
        if (isfield(cl, 'blobs'))
            blobs = cl.blobs;
        else
            error([mfilename 'wrong_argument'], ...
                'Old twin cluster reconstructions not supported any more! Please, re-produce the twin cluster file')
        end
        cl = cl.samp_ors;
        grain_ids = [cl(:).id];
    else
        grain_ids = cl.gids;
        blobs = [];
    end

    num_sub_ors = numel(cl);
    if ((num_sub_ors > 1) && (numel(rec_opts.ospace_resolution) == 1))
        rec_opts.ospace_resolution(2:num_sub_ors) = rec_opts.ospace_resolution;
    end

    for ii_g = 1:num_sub_ors
        sampler(ii_g) = GtOrientationSampling(parameters, cl(ii_g)); %#ok<AGROW>

        if (isempty(conf.ospace_lims) || (size(conf.ospace_lims, 1) < ii_g))
            bb_gvdm = cat(1, cl(ii_g).bb_ors(:).R_vector);
            ospace_oversize = rec_opts.ospace_oversize;
        else
            diff_r_vecs = tand(conf.ospace_lims(ii_g, :) / 2);
            bb_gvdm = [cl(ii_g).R_vector + diff_r_vecs(1:3); cl(ii_g).R_vector + diff_r_vecs(4:6)];
            ospace_oversize = 1;
        end
        sampler(ii_g).make_grid_resolution(rec_opts.ospace_resolution(ii_g), ...
            rec_opts.max_grid_edge_points, bb_gvdm', 'oversize', ospace_oversize);

        if (rec_opts.ospace_super_sampling > 1)
            sampler(ii_g).make_supersampling_simple_grid([1 2 3], ...
                rec_opts.ospace_super_sampling);
        end
    end

    algo = gtReconstruct6DLaunchAlgorithm(sampler, rec_opts, parameters, 'blobs', blobs);

    vols = algo.getCurrentSolution();

    ors_ranges = algo.orientation_groups;

    gam = zeros(num_sub_ors, 1);
    gos = zeros(num_sub_ors, 1);

    ODF6D = gtReconstructDataStructureDefinition('VOL6D', num_sub_ors);

    fprintf('Producing output data-structures: ')
    c = tic();
    for ii_g = 1:num_sub_ors
        num_chars = fprintf('%03d/%03d', ii_g, num_sub_ors);

        ODF6D(ii_g).options = rec_opts;
        ODF6D(ii_g).compute_statistics = algo.get_statistics();

        or_sizes = sampler(ii_g).get_orientation_sampling_size();
        gr_vols = vols(ors_ranges(ii_g, 1):ors_ranges(ii_g, 2));

        [avg_R_vecs, avg_R_vecs_int, stddev_R_vecs] = sampler(ii_g).getAverageOrientations(gr_vols);
        avg_R_vec = sampler(ii_g).getAverageOrientation(gr_vols);
        s_g_odf = reshape(sampler(ii_g).getODF(gr_vols), or_sizes);

        [kam, gam(ii_g)] = gtDefComputeKernelAverageMisorientation(avg_R_vecs, avg_R_vecs_int);
        [igm, gos(ii_g)] = gtDefComputeIntraGranularMisorientation(avg_R_vecs, avg_R_vecs_int, 'R_vector', cl(ii_g).R_vector);

        % Restoring initial volume size (depending on the rounding)
        if (rec_opts.volume_downscaling > 1)
            avg_R_vecs_int = gtMathsUpsampleVolume(avg_R_vecs_int, rec_opts.volume_downscaling);
            avg_R_vecs = gtMathsUpsampleVolume(avg_R_vecs, rec_opts.volume_downscaling);
            stddev_R_vecs = gtMathsUpsampleVolume(stddev_R_vecs, rec_opts.volume_downscaling);
            kam = gtMathsUpsampleVolume(kam, rec_opts.volume_downscaling);
            igm = gtMathsUpsampleVolume(igm, rec_opts.volume_downscaling);
        end

        vol_size = size(avg_R_vecs_int);
        shift = gtFwdSimComputeVolumeShifts(cl(ii_g).proj, parameters, vol_size);

        ODF6D(ii_g).voxels_avg_R_vectors = avg_R_vecs;
        ODF6D(ii_g).intensity = avg_R_vecs_int;
        ODF6D(ii_g).shift = shift;
        ODF6D(ii_g).R_vectors = sampler(ii_g).get_R_vectors();
        ODF6D(ii_g).voxels_stddev_R_vectors = stddev_R_vecs;
        ODF6D(ii_g).single_grain_ODF = s_g_odf;
        ODF6D(ii_g).single_grain_avg_R_vector = avg_R_vec;
        ODF6D(ii_g).kernel_average_misorientation = kam;
        ODF6D(ii_g).intra_granular_misorientation = igm;

        fprintf(repmat('\b', [1, num_chars]));
    end
    fprintf('Done (%d) in %g seconds.\n', num_sub_ors, toc(c))

    if (conf.save)
        fprintf('Saving the cluster reconstruction file..')
        c = tic();
        % Saving and cleaning at the same time
        cl_rec = struct('ODF6D', ODF6D);
        gtSaveClusterRec(phase_id, grain_ids, cl_rec, 'clean', true);
        fprintf('\b\b: Done in %g seconds.\n', toc(c))
    end

    if (nargout > 0)
        varargout{1} = ODF6D;
    end

    if (conf.extra_output)
        clear('ODF6D')

        % Restoring initial volume size (depending on the rounding)
        if (rec_opts.volume_downscaling > 1)
            fprintf('Expanding volumes..')
            c = tic();
            vols = gtMathsUpsampleVolume(vols, rec_opts.volume_downscaling);
            fprintf('\b\b: Done (%f seconds).\n', toc(c))
        end

        current_solution = algo.getCurrentSolution();
        for ii_g = num_sub_ors:-1:1
            algo.currentSolution = gtMathsGetSameSizeZeros(current_solution);
            algo.currentSolution(ors_ranges(ii_g, 1):ors_ranges(ii_g, 2)) = current_solution(ors_ranges(ii_g, 1):ors_ranges(ii_g, 2));
            [proj_blobs, proj_spots] = algo.getProjectionOfCurrentSolution();
            gr_vols = vols(ors_ranges(ii_g, 1):ors_ranges(ii_g, 2));

            ODF6D(ii_g) = struct( ...
                'orientation_volumes', {gr_vols}, ...
                'fwd_projected_blobs', {proj_blobs}, ...
                'fwd_projected_spots', {proj_spots}, ...
                'grain_average_misorientation', {gam(ii_g)}, ...
                'grain_orientation_spread', {gos(ii_g)} );
        end

        if (conf.save)
            phase_dir = fullfile(parameters.acq.dir, '4_grains', ...
                sprintf('phase_%02d', phase_id));
            grains_str_ids = sprintf('_%04d', sort(grain_ids));
            grain_full_details_file = fullfile(phase_dir, ...
                sprintf('grain_cluster_full_details%s.mat', grains_str_ids));
            save(grain_full_details_file, 'ODF6D', '-v7.3');
        end
        if (nargout > 1)
            varargout{2} = ODF6D;
        end
    end
end