"""
       scan_xyz(scanner, x_axis_range, x_axis_num_scans, y_axis_range, y_axis_num_scans, z_axis_range, z_axis_num_scans; kwargs..)

 This function grabs data along the xyz-axis. It holds the z-position constant and calls the
 scan_xy function. This function scans along the x axis until it reaches the last x point
 and then moves along the y axis and repeats the process. After completing the xy scan, the
 z-position is changed and then the process is repeated. There are five mantadory inputs and
 one optional input that is used only when this function is called by other functions and
 the printed messages should be surpressed. The user usually doesn't have to use the
 variable called.

# Arguments
- `scanner`: the structure for all controller connections.
- `x_axis_range::Array`: An array that contains the start and end point of the desired range
                         in the x-direction. (Must be 1 or 2 elements long)
- `x_axis_num_scans::Int`: The number of points/scans the user desires to record along the
                           x-axis.
- `y_axis_range::Array`: Contains the start and end point of the desired range in the
                         y-direction. (Must be 1 or 2 elements long)
- `y_axis_num_scans::Int`: The number of points/scans the user desires to record along the
                           y-axis.
- `z_axis_range::Array`: Contains the start and end point of the desired range in the
                         z-direction. (Must be 1 or 2 elements long)
- `z_axis_num_scans::Int`: The number of points/scans the user desires to record along the
                           z-axis.

# Keywords
- `verbose`::Bool: Optional, defaults to: true

# Returns
- `ScanData`

# Example:
```
ps = initialize(PositionerSystem)
scope = initialize(AgilentDSOX4034A)

scanner = IntensityScan(
xyz = ps, 
scope = scope, 
channel = 1, 
post_move_delay = 0ms,
precapture_delay = 0µs,
)

wave_xyz = scan_xyz(scanner, [-5mm, 5mm], 3, [-5mm, 5mm], 3, [-5mm, 5mm], 3)
```
"""
function scan_xyz(scanner, x_range, x_num_scans, y_range, y_num_scans, z_range, z_num_scans; verbose=true)
    check_xyz_limits(scanner, "x", x_range)
    check_xyz_limits(scanner, "y", y_range)
    check_xyz_limits(scanner, "z", z_range)
    positions = create_positions_vector(z_range, z_num_scans)
    
    # construct the ScanData struct
    scope_info = TcpInstruments.scope_waveform_info_get(scanner.scope, scanner.channel)
    dummy_data = TcpInstruments.scope_parse_raw_waveform(zeros(scope_info.num_points), scope_info)
    time_points = dummy_data.time
    num_points_per_axis = [x_num_scans, y_num_scans, z_num_scans]
    scan_data = ScanData("xyz", scope_info, time_points, scope_info.num_points, num_points_per_axis)

    pm = Progress(z_num_scans; desc="Scanning 3D volume: ", enabled=verbose)
    for scan_index in 1:z_num_scans
        move_z_abs(scanner.xyz, positions[scan_index])
        ProgressMeter.update!(pm)

        scan_data_xy = scan_xy(scanner, x_range, x_num_scans, y_range, y_num_scans, verbose=false)
        scan_data.waveform[:, :, :, scan_index] = scan_data_xy.waveform
        scan_data.coordinates[:, :, :, scan_index] = scan_data_xy.coordinates

        ProgressMeter.next!(pm)
    end

    return scan_data
end
