import time
import ctypes as C
import numpy as np

class Edge:
    def __init__(self, pco_edge_type='4.2', verbose=True, very_verbose=False):
        assert pco_edge_type in ('4.2', '5.5')
        self.pco_edge_type = pco_edge_type
        self.verbose = verbose
        self.very_verbose = very_verbose
        self.camera_handle = C.c_void_p(0)
        if verbose: print("Opening pco.edge camera...")
        try:
            assert self.camera_handle.value is None
            dll.open_camera(self.camera_handle, 0)
            assert self.camera_handle.value is not None
        except (WindowsError, AssertionError):
            print("Failed to open pco.edge camera.")
            print(" *Is the camera on, and plugged into the computer?")
            print(" *Is CamWare running? It shouldn't be!")
            print(" *Is sc2_cl_me4.dll in the same directory as SC2_Cam.dll?")
            raise
        if self.verbose: print(" Camera open.")
        self.disarm()
        self._refresh_camera_setting_attributes()
        return None                            

    def close(self):
        if self.armed: self.disarm()
        if self.verbose: print("Closing pco.edge camera...")
        dll.close_camera(self.camera_handle)
        if self.verbose: print(" Camera closed.")
        return None

    def apply_settings(
        self,
        trigger='auto_trigger',
        exposure_time_microseconds=2200,
        region_of_interest={'left': 1,
                            'right': 2060,
                            'top': 1,
                            'bottom': 2048}
        ):
        """
        * 'trigger' can be 'auto_trigger' or 'external_trigger' See the
          comment block in _get_trigger_mode() for further details.
        * 'exposure_time_microseconds' can be as low as 107 and as high
          as 1000000.
        * 'region_of_interest' will be adjusted to match the nearest
          legal ROI that the camera supports. See _legalize_roi() for
          details.
        """    
        if self.armed: self.disarm()
        if self.verbose: print("Applying settings to camera...")
        """
        These settings matter, but we don't expose their functionality
        through apply_settings():
        """
        dll.reset_settings_to_default(self.camera_handle)        
        self._set_sensor_format('standard')
        self._set_acquire_mode('auto')
        self._set_pixel_rate({'4.2': 272250000,
                              '5.5': 286000000
                              }[self.pco_edge_type])
        """
        I think these settings don't matter for the pco.edge, but just
        in case...
        """
        self._set_storage_mode('recorder')
        self._set_recorder_submode('ring_buffer')
        """
        These settings change all the time:
        """
        self._set_trigger_mode(trigger)
        self._set_exposure_time(exposure_time_microseconds)
        self._set_roi(region_of_interest)
        """
        It's good to check the camera health periodically. Now's as good
        a time as any, especially since the expected result is
        predictable: it should all be zeros.
        """
        camera_health = self._get_camera_health()
        for v in camera_health.values():
            assert v == 0
        return None

    def arm(self, num_buffers=2):
        assert 1 <= num_buffers <= 16
        if self.armed:
            if self.verbose:
                print('Arm requested, but the pco.edge camera'
                      'is already armed. Disarming...')
            self.disarm()
        if self.verbose: print("Arming camera...") 
        dll.arm_camera(self.camera_handle)
        wXRes, wYRes, wXResMax, wYResMax = (
            C.c_uint16(), C.c_uint16(), C.c_uint16(), C.c_uint16())
        dll.get_sizes(self.camera_handle, wXRes, wYRes, wXResMax, wYResMax)
        self.width, self.height = wXRes.value, wYRes.value
        self.bytes_per_image = self.width * self.height * 2 #16 bit images
        if self.very_verbose:
            print(" Camera ROI dimensions:",
                  self.width, "(l/r) by", self.height, "(u/d)")
        """
        Allocate buffers that the camera will use to hold images.
        """
        self.buffer_pointers = []
        for i in range(num_buffers):
            buffer_number = C.c_int16(-1)
            self.buffer_pointers.append(C.POINTER(C.c_uint16)()) #Woo!
            buffer_event = C.c_void_p(0)
            dll.allocate_buffer(
                self.camera_handle,
                buffer_number,
                self.bytes_per_image,
                self.buffer_pointers[-1],
                buffer_event)
            assert buffer_number.value == i
            if self.very_verbose:
                print(" Buffer number ", i, " allocated, pointing to ",
                      self.buffer_pointers[-1].contents,
                      ", linked to event ", buffer_event.value, sep='')
        dll.set_image_parameters(self.camera_handle, self.width, self.height)
        dll.set_recording_state(self.camera_handle, 1)
        self.armed = True
        if self.verbose: print(" Camera armed.")
        """
        Add our allocated buffers to the camera's 'driver queue'
        """
        self.added_buffers = []
        for buf_num in range(len(self.buffer_pointers)):
            dll.add_buffer(
                self.camera_handle,
                0,
                0,
                buf_num,
                self.width,
                self.height,
                16)
            self.added_buffers.append(buf_num)
        self._dll_status = C.c_uint32()
        self._driver_status = C.c_uint32()
        self._image_datatype = C.c_uint16 * self.width * self.height
        return None

    def disarm(self):
        if self.verbose: print("Disarming camera...")
        dll.set_recording_state(self.camera_handle, 0)
        dll.remove_buffer(self.camera_handle)
        if hasattr(self, 'buffer_pointers'): #free allocated buffers
            for buf in range(len(self.buffer_pointers)):
                dll.free_buffer(self.camera_handle, buf)
            self.buffer_pointers = []
        self.armed = False
        if self.verbose: print(" Camera disarmed.")
        return None

    def record_to_memory(
        self,
        num_images,
        preframes=0,
        out=None,
        first_frame=0,
        poll_timeout=5e5,
        sleep_timeout=40,
        first_trigger_timeout_seconds=0,
        ):
        if not self.armed: self.arm()
        """
        We'll store our images in a numpy array. Did the user provide
        one, or should we allocate one ourselves?
        """
        if out is None:
            first_frame = 0
            out = np.ones((num_images - preframes, self.height, self.width),
                          dtype=np.uint16)
        try:
            assert len(out.shape) == 3
            assert (out.shape[0] - first_frame) >= (num_images - preframes)
            assert (out.shape[1], out.shape[2]) == (self.height, self.width)
            assert out.dtype == np.uint16
        except AssertionError:
            print("\nInput argument 'out' must have dimensions:")
            print("(>=num_images - preframes, y-resolution, x-resolution)")
            raise
        except AttributeError:
            print("\nInput argument 'out' must be a numpy array",
                  "(to hold our images)")
            raise
        """
        Try to record some images, and try to tolerate the many possible
        ways this can fail.
        """
        if self.verbose: print("Acquiring", num_images, "images...")
        num_acquired = 0
        for which_im in range(num_images):
            """
            Hassle the camera until it gives us a buffer. The only ways
            we exit this 'while' loop are by getting a buffer or running
            out of patience.
            """
            num_polls = 0
            num_sleeps = 0
            start_time = time.perf_counter()
            while True:
                """
                Check if a buffer is ready
                """
                num_polls += 1
                dll.get_buffer_status(
                    self.camera_handle,
                    self.added_buffers[0],
                    self._dll_status,
                    self._driver_status)
                if self._dll_status.value == 0xc0008000:
                    buffer_number = self.added_buffers.pop(0)#Removed from queue
                    if self.very_verbose:
                        print(" After", num_polls, "polls and", num_sleeps,
                              "sleeps, buffer", buffer_number, "is ready.")
                    break
                """
                The buffer isn't ready. How long should we wait to try
                again? For short exposures, we'd like to poll super
                frequently. For long exposures, we'll use time.sleep()
                to save CPU.
                """
                if self.exposure_time_microseconds > 30e3:
                    time.sleep(self.exposure_time_microseconds * 1e-6 * #seconds
                               2 / sleep_timeout) #Worst case
                    num_sleeps += 1
                """
                At some point we have to admit we probably missed a
                trigger, and give up. Give up after too many polls
                (likely triggered by short exposures) or too many sleeps
                (likely triggered by long exposures)
                """
                if num_polls > poll_timeout or num_sleeps > sleep_timeout:
                    elapsed_time = time.perf_counter() - start_time
                    if which_im == 0: # First image; maybe keep waiting...
                        if elapsed_time < first_trigger_timeout_seconds:
                            continue
                    raise TimeoutError(
                        "After %i polls,"%(num_polls) + 
                        " %i sleeps"%(num_sleeps) + 
                        " and %0.3f seconds,"%(elapsed_time) + 
                        " no buffer. (%i acquired)"%(num_acquired),
                        num_acquired=num_acquired)
            try:
                if self._driver_status.value == 0x0:
                    pass
                elif self._driver_status.value == 0x80332028:
                    # Zero the rest of the buffer
                    out[max(0, first_frame + (which_im - preframes)):, :, :
                        ].fill(0)
                    raise DMAError('DMA error during record_to_memory')
                else:
                    print("Driver status:", self._driver_status.value)
                    raise UserWarning("Buffer status error")
                if self.very_verbose:
                    print(" Record to memory result:",
                          hex(self._dll_status.value),
                          hex(self._driver_status.value))
                if which_im >= preframes:
                    """
                    http://stackoverflow.com/a/13481676
                    """
                    image = np.ctypeslib.as_array( #Temporary!
                        self._image_datatype.from_address(
                            C.addressof(
                                self.buffer_pointers[buffer_number].contents)))
                    out[first_frame + (which_im - preframes), :, :] = image
                    num_acquired += 1
            finally:
                dll.add_buffer(#Put the buffer back in the driver queue
                    self.camera_handle,
                    0,
                    0,
                    buffer_number,
                    self.width,
                    self.height,
                    16)
                self.added_buffers.append(buffer_number)
        if self.verbose: print("Done acquiring.")
        return out

    def _refresh_camera_setting_attributes(self):
        """
        There are two ways to access a camera setting:
        
         1. Ask the camera directly, using a self.get_*() - type method.
        
          This interrogates the camera via a DLL call, updates the
          relevant attribute(s) of the Edge object, and returns the
          relevant value(s). This is slower, because you have to wait for
          round-trip communication, but gets you up-to-date info.

         2. Access an attribute of the Edge object, e.g. self.roi

          This ignores the camera, which is very fast, but the resulting
          value could potentially be inconsistent with the camera's true
          setting (although I hope it isn't!)

        _refresh_camera_setting_attributes() is a convenience function
        to update all the camera attributes at once. Call it if you're
        nervous, I guess.
        """
        if self.verbose: print("Retrieving settings from camera...")
        self._get_sensor_format()
        self._get_trigger_mode()
        self._get_storage_mode()
        self._get_recorder_submode()
        self._get_acquire_mode()
        self._get_pixel_rate()
        self._get_exposure_time()
        self._get_roi()
        self._get_temperature()
        self._get_camera_health()
        return None

    def _get_sensor_format(self):
        wSensor = C.c_uint16(777) #777 is not an expected output
        dll.get_sensor_format(self.camera_handle, wSensor)
        assert wSensor.value in (0, 1) #wSensor.value should change
        mode_names = {0: "standard", 1: "extended"}
        if self.very_verbose:
            print(" Sensor format:", mode_names[wSensor.value])
        self.sensor_format = mode_names[wSensor.value]
        return self.sensor_format

    def _set_sensor_format(self, mode='standard'):
        mode_numbers = {"standard": 0, "extended": 1}
        if self.very_verbose:
            print(" Setting sensor format to:", mode)
        dll.set_sensor_format(self.camera_handle, mode_numbers[mode])
        assert self._get_sensor_format() == mode
        return self.sensor_format
    
    def _get_camera_health(self):
        dwWarn, dwErr, dwStatus = (
            C.c_uint32(), C.c_uint32(), C.c_uint32())
        dll.get_camera_health(self.camera_handle, dwWarn, dwErr, dwStatus)
        if self.verbose:
            print(" Camera health status:", end='')
            print("  Warnings:", dwWarn.value, end='')
            if dwWarn.value == 0:
                print(" (good)", end='')
            else:
                print("***BAD***")
            print(" / Errors:", dwErr.value, end='')
            if dwErr.value == 0:
                print(" (good)", end='')
            else:
                print("***BAD***")
            print(" / Status:", dwStatus.value)
        self.camera_health = {
            'warnings': dwWarn.value,
            'errors': dwErr.value,
            'status': dwStatus.value}
        return self.camera_health

    def _get_temperature(self):
        ccdtemp, camtemp, powtemp = (
            C.c_int16(), C.c_int16(), C.c_int16())
        dll.get_temperature(self.camera_handle, ccdtemp, camtemp, powtemp)
        if self.verbose:
            print(" Temperatures:",
                  "CCD", ccdtemp.value * 0.1, "C /",
                  "camera", camtemp.value, "C /",
                  "power supply", powtemp.value, "C ")
        self.temperature = {
            'ccd_temp': ccdtemp.value * 0.1,
            'camera_temp': camtemp.value,
            'power_supply_temp': powtemp.value}
        return self.temperature

    def _get_trigger_mode(self):
        """
        0x0000 = [auto trigger]
        A new image exposure is automatically started best possible
        compared to the readout of an image. If a CCD is used and the
        images are taken in a sequence, then exposures and sensor readout
        are started simultaneously. Signals at the trigger input (<exp
        trig>) are irrelevant.
        - 0x0001 = [software trigger]:
        An exposure can only be started by a force trigger command.
        - 0x0002 = [extern exposure & software trigger]:
        A delay / exposure sequence is started at the RISING or FALLING
        edge (depending on the DIP switch setting) of the trigger input
        (<exp trig>).
        - 0x0003 = [extern exposure control]:
        The exposure time is defined by the pulse length at the trigger
        input(<exp trig>). The delay and exposure time values defined by
        the set/request delay and exposure command are ineffective.
        (Exposure time length control is also possible for double image
        mode; exposure time of the second image is given by the readout
        time of the first image.)
        """
        trigger_mode_names = {0: "auto_trigger",
                              1: "software_trigger",
                              2: "external_trigger",
                              3: "external_exposure"}
        wTriggerMode = C.c_uint16()
        dll.get_trigger_mode(self.camera_handle, wTriggerMode)
        if self.verbose:
            print(" Trigger mode:", trigger_mode_names[wTriggerMode.value])
        self.trigger_mode = trigger_mode_names[wTriggerMode.value]
        return self.trigger_mode
    
    def _set_trigger_mode(self, mode="auto_trigger"):
        trigger_mode_numbers = {
            "auto_trigger": 0,
            "external_trigger": 2}
        if self.verbose: print(" Setting trigger mode to:", mode)
        dll.set_trigger_mode(self.camera_handle, trigger_mode_numbers[mode])
        assert self._get_trigger_mode() == mode
        return self.trigger_mode

    def _force_trigger(self):
        assert self.trigger_mode in ('software_trigger', 'external_trigger')
        wTriggerMode = C.c_uint16()
        dll.force_trigger(self.camera_handle, wTriggerMode)
        assert wTriggerMode.value in (0, 1)
        return bool(wTriggerMode.value)

    def _get_storage_mode(self):
        wStorageMode = C.c_uint16()
        dll.get_storage_mode(self.camera_handle, wStorageMode)
        storage_mode_names = {0: "recorder",
                              1: "FIFO_buffer"}
        if self.very_verbose:
            print(" Storage mode:", storage_mode_names[wStorageMode.value])
        self.storage_mode = storage_mode_names[wStorageMode.value]
        return self.storage_mode

    def _set_storage_mode(self, mode="recorder"):
        storage_mode_numbers = {"recorder": 0,
                                "FIFO_buffer": 1}
        if self.very_verbose: print(" Setting storage mode to:", mode)
        dll.set_storage_mode(self.camera_handle, storage_mode_numbers[mode])
        assert self._get_storage_mode() == mode
        return self.storage_mode

    def _get_recorder_submode(self):
        wRecSubmode = C.c_uint16(1)
        dll.get_recorder_submode(self.camera_handle, wRecSubmode)
        recorder_submode_names = {0: "sequence",
                                  1: "ring_buffer"}
        if self.very_verbose:
            print(" Recorder submode:",
                  recorder_submode_names[wRecSubmode.value])
        self.recorder_submode = recorder_submode_names[wRecSubmode.value]
        return self.recorder_submode

    def _set_recorder_submode(self, mode="ring_buffer"):
        recorder_mode_numbers = {
            "sequence": 0,
            "ring_buffer": 1}
        if self.very_verbose: print(" Setting recorder submode to:", mode)
        dll.set_recorder_submode(
            self.camera_handle, recorder_mode_numbers[mode])
        assert self._get_recorder_submode() == mode
        return self.recorder_submode

    def _get_acquire_mode(self):
        wAcquMode = C.c_uint16(0)
        dll.get_acquire_mode(self.camera_handle, wAcquMode)
        acquire_mode_names = {0: "auto",
                              1: "external_static",
                              2: "external_dynamic"}
        if self.very_verbose:
            print(" Acquire mode:", acquire_mode_names[wAcquMode.value])
        self.acquire_mode = acquire_mode_names[wAcquMode.value]
        return self.acquire_mode

    def _set_acquire_mode(self, mode='auto'):
        acquire_mode_numbers = {"auto": 0,
                                "external_static": 1,
                                "external_dynamic": 2}
        if self.very_verbose: print(" Setting acquire mode to:", mode)
        dll.set_acquire_mode(self.camera_handle, acquire_mode_numbers[mode])
        assert self._get_acquire_mode() == mode
        return self.acquire_mode

    def _get_pixel_rate(self):
        dwPixelRate = C.c_uint32(0)
        dll.get_pixel_rate(self.camera_handle, dwPixelRate)
        assert dwPixelRate.value != 0
        if self.very_verbose: print(" Pixel rate:", dwPixelRate.value)
        self.pixel_rate = dwPixelRate.value
        return self.pixel_rate

    def _set_pixel_rate(self, rate=272250000):
        if self.very_verbose: print(" Setting pixel rate to:", rate)
        dll.set_pixel_rate(self.camera_handle, rate)
        assert self._get_pixel_rate() == rate
        return self.pixel_rate

    def _get_exposure_time(self):
        dwDelay = C.c_uint32(0)
        wTimeBaseDelay = C.c_uint16(0)
        dwExposure = C.c_uint32(0)
        wTimeBaseExposure = C.c_uint16(1)
        dll.get_delay_exposure_time(
            self.camera_handle,
            dwDelay,
            dwExposure,
            wTimeBaseDelay,
            wTimeBaseExposure)
        time_base_mode_names = {0: "nanoseconds",
                                1: "microseconds",
                                2: "milliseconds"}
        if self.verbose:
            print(" Exposure:", dwExposure.value,
                  time_base_mode_names[wTimeBaseExposure.value])
        if self.very_verbose:
            print(" Delay:", dwDelay.value,
                  time_base_mode_names[wTimeBaseDelay.value])
        self.exposure_time_microseconds = (
            dwExposure.value * 10.**(3*wTimeBaseExposure.value - 3))
        self.delay_time = dwDelay.value
        return self.exposure_time_microseconds

    def _set_exposure_time(self, exposure_time_microseconds=2200):
        exposure_time_microseconds = int(exposure_time_microseconds)
        assert 1e2 <= exposure_time_microseconds <= 1e7
        if self.verbose:
            print(" Setting exposure time to", exposure_time_microseconds, "us")
        dll.set_delay_exposure_time(
            self.camera_handle, 0, exposure_time_microseconds, 1, 1)
        assert self._get_exposure_time() == exposure_time_microseconds
        return self.exposure_time_microseconds

    
    def _get_roi(self):
        wRoiX0, wRoiY0, wRoiX1, wRoiY1 = (
            C.c_uint16(), C.c_uint16(),
            C.c_uint16(), C.c_uint16())
        dll.get_roi(self.camera_handle, wRoiX0, wRoiY0, wRoiX1, wRoiY1)
        if self.verbose:
            print(" Camera ROI:");
            print("  From pixel", wRoiX0.value, "to pixel", wRoiX1.value, "(left/right)")
            print("  From pixel", wRoiY0.value, "to pixel", wRoiY1.value, "(up/down)")
        self.roi = {
            'left': wRoiX0.value,
            'top': wRoiY0.value,
            'right': wRoiX1.value,
            'bottom': wRoiY1.value}
        """
        How long do we expect the chip to spend rolling? Both the 4.2
        and the 5.5 take ~10 ms to roll the full chip. Calculate the
        fraction of the chip we're using and estimate the rolling time.
        """
        if self.pco_edge_type == '4.2':
            max_lines = 1024
        elif self.pco_edge_type == '5.5':
            max_lines = 1080
        full_chip_rolling_time = 1e4
        chip_fraction = max(wRoiY1.value - max_lines,
                            max_lines + 1 - wRoiY0.value) / max_lines
        self.rolling_time_microseconds =  full_chip_rolling_time * chip_fraction
        return self.roi

    def _legalize_roi(self, roi):
        """This just calls the _legalize_roi function defined below.

        Note that this method fills in some of the arguments for you.
        """
        return legalize_roi(roi, self.pco_edge_type, self.roi, self.verbose)
    
    def _set_roi(self, region_of_interest):
        roi = self._legalize_roi(region_of_interest)
        dll.set_roi(self.camera_handle,
                    roi['left'], roi['top'], roi['right'], roi['bottom'])
        assert self._get_roi() == roi
        return self.roi

def legalize_roi(
    roi,
    pco_edge_type='4.2',
    current_roi=None,
    verbose=True):
    """
    There are lots of ways a requested region of interest (ROI) can
    be illegal. This utility function returns a nearby legal ROI.

    Optionally, you can leave keys of 'roi' unspecified, and
    _legalize_roi() tries to return reasonable choices based on
    the values in current_roi.
    """
    left = roi.get('left')
    right = roi.get('right')
    bottom = roi.get('bottom')
    top = roi.get('top')
    if verbose:
        print(" Requested camera ROI:")
        print("  From pixel", left, "to pixel", right, "(left/right)")
        print("  From pixel", top, "to pixel", bottom, "(up/down)")
    min_lr, min_ud, min_height = 1, 1, 10
    if pco_edge_type == '4.2':
        max_lr, max_ud, min_width, step_lr = 2060, 2048, 40, 20
    elif pco_edge_type == '5.5':
        max_lr, max_ud, min_width, step_lr = 2560, 2160, 160, 160
    if current_roi is None:
        current_roi = {'left': min_lr, 'right':  max_lr,
                       'top':  min_ud, 'bottom': max_ud}
    """
    Legalize left/right
    """
    if left is None and right is None:
        """
        User isn't trying to change l/r ROI; use existing ROI.
        """
        left, right = current_roi['left'], current_roi['right']
    elif left is not None:
        """
        'left' is specified, 'left' is the master.
        """
        if left < min_lr: #Legalize 'left'
            left = min_lr
        elif left > max_lr - min_width + 1:
            left = max_lr - min_width + 1
        else:
            left = 1 + step_lr*((left - 1) // step_lr)
        if right is None: #Now legalize 'right'
            right = current_roi['right']
        if right < left + min_width - 1:
            right = left + min_width - 1
        elif right > max_lr:
            right = max_lr
        else:
            right = left - 1 + step_lr*((right - (left - 1)) // step_lr)
    else:
        """
        'left' is unspecified, 'right' is specified. 'right' is the master.
        """
        if right > max_lr: #Legalize 'right'
            right = max_lr
        elif right < min_lr - 1 + min_width:
            right = min_width
        else:
            right = step_lr * (right  // step_lr)
        left = current_roi['left'] #Now legalize 'left'
        if left > right - min_width + 1:
            left = right - min_width + 1
        elif left < min_lr:
            left = min_lr
        else:
            left = right + 1 - step_lr * ((right - (left - 1)) // step_lr)
    assert min_lr <= left < left + min_width - 1 <= right <= max_lr
    """
    Legalize top/bottom
    """
    if top is None and bottom is None:
        """
        User isn't trying to change u/d ROI; use existing ROI.
        """
        top, bottom = current_roi['top'], current_roi['bottom']
    elif top is not None:
        """
        'top' is specified, 'top' is the master.
        """
        if top < min_ud: #Legalize 'top'
            top = min_ud
        if top > (max_ud - min_height)//2 + 1:
            top = (max_ud - min_height)//2 + 1
        bottom = max_ud - top + 1 #Now bottom is specified
    else:
        """
        'top' is unspecified, 'bottom' is specified, 'bottom' is the
        master.
        """
        if bottom > max_ud: #Legalize 'bottom'
            bottom = max_ud
        if bottom < (max_ud + min_height)//2:
            bottom = (max_ud + min_height)//2
        top = max_ud - bottom + 1 #Now 'top' is specified
    assert min_ud <= top < top + min_height - 1 <= bottom <= max_ud
    new_roi = {'left': left, 'top': top, 'right': right, 'bottom': bottom}
    if verbose and new_roi != roi:
        print(" ***Requested ROI must be adjusted to match the camera***")
    return new_roi

def pco_edge_camera_child_process(
    data_buffers,
    buffer_shape,
    input_queue,
    output_queue,
    commands,
    pco_edge_type='4.2' #Change this if you're using a 5.5
    ):
    """For use with image_data_pipeline.py

    https://github.com/AndrewGYork/tools/blob/master/image_data_pipeline.py
    Debugged for the 4.2, but might work for the 5.5, with some TLC...
    """
    from image_data_pipeline import info, Q, sleep, clock
    try:
        import pco
    except ImportError:
        info("Failed to import pco.py; go get it from github:")
        info("https://github.com/AndrewGYork/tools/blob/master/pco.py")
        raise
    buffer_size = np.prod(buffer_shape)
    info("Initializing...")
    camera = pco.Edge(pco_edge_type=pco_edge_type, verbose=False)
    camera.apply_settings(trigger='auto_trigger')
    camera.arm(num_buffers=3)
    info("Done initializing")
    preframes = 3
    first_trigger_timeout_seconds = 0
    status = 'Normal'
    while True:
        if commands.poll():
            cmd, args = commands.recv()
            info("Command received: " + cmd)
            if cmd == 'apply_settings':
                result = camera.apply_settings(**args)
                camera.arm(num_buffers=3)
                commands.send(result)
            elif cmd == 'get_setting':
                setting = getattr(
                    camera, args['setting'], 'unrecognized_setting')
                commands.send(setting)
            elif cmd == 'set_buffer_shape':
                buffer_shape = args['shape']
                buffer_size = np.prod(buffer_shape)
                commands.send(buffer_shape)
            elif cmd == 'get_status':
                commands.send(status)
            elif cmd == 'reset_status':
                status = 'Normal'
                commands.send(status)
            elif cmd == 'get_preframes':
                commands.send(preframes)
            elif cmd == 'set_preframes':
                preframes = args['preframes']
                commands.send(preframes)
            elif cmd == 'get_first_trigger_timeout_seconds':
                commands.send(first_trigger_timeout_seconds)
            elif cmd == 'set_first_trigger_timeout_seconds':
                first_trigger_timeout_seconds = args[
                    'first_trigger_timeout_seconds']
                commands.send(first_trigger_timeout_seconds)
            elif cmd == 'force_trigger':
                result = camera._force_trigger()
                commands.send(result)
            else:
                info("Unrecognized command: " + cmd)
                commands.send("unrecognized_command")
                continue
        try:
            permission_slip = input_queue.get_nowait()
        except Q.Empty:
            sleep(0.001) #Non-deterministic sleep time :(
            continue
        if permission_slip is None: #This is how we signal "shut down"
            output_queue.put(permission_slip)
            break #We're done
        else:
            # Fill the data buffer with images from the camera
            time_received = clock()
            process_me = permission_slip['which_buffer']
            ## Trust IDP to request a legal num_slices
            num_slices = permission_slip.get('num_slices', buffer_shape[0])
            info("start buffer %i, acquiring %i frames and %i preframes"%(
                process_me, num_slices, preframes))
            with data_buffers[process_me].get_lock():
                a = np.frombuffer(
                    data_buffers[process_me].get_obj(),
                    dtype=np.uint16)[:buffer_size].reshape(buffer_shape
                                                           )[:num_slices, :, :]
                try:
                    camera.record_to_memory(
                        num_images=a.shape[0] + preframes,
                        preframes=preframes,
                        out=a,
                        first_trigger_timeout_seconds=(
                            first_trigger_timeout_seconds))
                except pco.TimeoutError as e:
                    info('TimeoutError: %s'%(e.value))
                    status = 'TimeoutError'
                    #FIXME: we can do better, probably. Keep trying?
                    #Should we zero the remainder of 'a'?
                except pco.DMAError:
                    info('DMAError')
                    status = 'DMAError'
                else:
                    status = 'Normal'
            info("end buffer %i, %06f seconds elapsed"%(
                process_me, clock() - time_received))
            output_queue.put(permission_slip)
    camera.close()
    return None

"""
A few types of exception we'll use during recording:
"""
class TimeoutError(Exception):
    def __init__(self, value, num_acquired=0):
        self.value = value
        self.num_acquired = num_acquired
    def __str__(self):
        return repr(self.value)

class DMAError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

"""
DLL management
"""
try:
    dll = C.oledll.LoadLibrary("SC2_Cam")
    """
    If you get a WindowsError, read PCO_err.h to decypher it.
    """
except WindowsError:
    print("Failed to load SC2_Cam.dll")
    print("You need this to run pco.py")
    raise

"""
This command opens the next pco camera; if you want to have multiple
cameras, and pick which one you're opening, I'd have to implement
PCO_OpenCameraEx, which would require me to understand PCO_OpenStruct.
"""
dll.open_camera = dll.PCO_OpenCamera
dll.open_camera.argtypes = [C.POINTER(C.c_void_p), C.c_uint16]

dll.close_camera = dll.PCO_CloseCamera
dll.close_camera.argtypes = [C.c_void_p]

dll.arm_camera = dll.PCO_ArmCamera
dll.arm_camera.argtypes = [C.c_void_p]

dll.allocate_buffer = dll.PCO_AllocateBuffer
dll.allocate_buffer.argtypes = [
    C.c_void_p,
    C.POINTER(C.c_int16),
    C.c_uint32,
    C.POINTER(C.POINTER(C.c_uint16)),
    C.POINTER(C.c_void_p)]

dll.add_buffer = dll.PCO_AddBufferEx
dll.add_buffer.argtypes = [
    C.c_void_p,
    C.c_uint32,
    C.c_uint32,
    C.c_int16,
    C.c_uint16,
    C.c_uint16,
    C.c_uint16]

dll.get_buffer_status = dll.PCO_GetBufferStatus
dll.get_buffer_status.argtypes = [
    C.c_void_p,
    C.c_int16,
    C.POINTER(C.c_uint32),
    C.POINTER(C.c_uint32)]

dll.set_image_parameters = dll.PCO_CamLinkSetImageParameters
dll.set_image_parameters.argtypes = [C.c_void_p, C.c_uint16, C.c_uint16]

dll.set_recording_state = dll.PCO_SetRecordingState
dll.set_recording_state.argtypes = [C.c_void_p, C.c_uint16]

dll.get_sizes = dll.PCO_GetSizes
dll.get_sizes.argtypes = [
    C.c_void_p,
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16)]

dll.get_sensor_format = dll.PCO_GetSensorFormat
dll.get_sensor_format.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]

dll.get_camera_health = dll.PCO_GetCameraHealthStatus
dll.get_camera_health.argtypes = [
    C.c_void_p,
    C.POINTER(C.c_uint32),
    C.POINTER(C.c_uint32),
    C.POINTER(C.c_uint32)]

dll.get_temperature = dll.PCO_GetTemperature
dll.get_temperature.argtypes = [
    C.c_void_p,
    C.POINTER(C.c_int16),
    C.POINTER(C.c_int16),
    C.POINTER(C.c_int16)]

dll.get_trigger_mode = dll.PCO_GetTriggerMode
dll.get_trigger_mode.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]

dll.get_storage_mode = dll.PCO_GetStorageMode
dll.get_storage_mode.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]

dll.get_recorder_submode = dll.PCO_GetRecorderSubmode
dll.get_recorder_submode.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]

dll.get_acquire_mode = dll.PCO_GetAcquireMode
dll.get_acquire_mode.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]

dll.get_pixel_rate = dll.PCO_GetPixelRate
dll.get_pixel_rate.argtypes = [C.c_void_p, C.POINTER(C.c_uint32)]

dll.set_pixel_rate = dll.PCO_SetPixelRate
dll.set_pixel_rate.argtypes = [C.c_void_p, C.c_uint32]

dll.get_delay_exposure_time = dll.PCO_GetDelayExposureTime
dll.get_delay_exposure_time.argtypes = [
    C.c_void_p,
    C.POINTER(C.c_uint32),
    C.POINTER(C.c_uint32),
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16)]

dll.set_delay_exposure_time = dll.PCO_SetDelayExposureTime
dll.set_delay_exposure_time.argtypes = [
    C.c_void_p,
    C.c_uint32,
    C.c_uint32,
    C.c_uint16,
    C.c_uint16]

dll.get_roi = dll.PCO_GetROI
dll.get_roi.argtypes = [
    C.c_void_p,
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16),
    C.POINTER(C.c_uint16)]

dll.set_roi = dll.PCO_SetROI
dll.set_roi.argtypes = [
    C.c_void_p,
    C.c_uint16,
    C.c_uint16,
    C.c_uint16,
    C.c_uint16]

dll.reset_settings_to_default = dll.PCO_ResetSettingsToDefault
dll.reset_settings_to_default.argtypes = [C.c_void_p]

dll.set_recording_state = dll.PCO_SetRecordingState
dll.set_recording_state.argtypes = [C.c_void_p, C.c_uint16]

dll.remove_buffer = dll.PCO_RemoveBuffer
dll.remove_buffer.argtypes = [C.c_void_p]

dll.free_buffer = dll.PCO_FreeBuffer
dll.free_buffer.argtypes = [C.c_void_p, C.c_int16]

dll.set_sensor_format = dll.PCO_SetSensorFormat
dll.set_sensor_format.argtypes = [C.c_void_p, C.c_uint16]

dll.set_trigger_mode = dll.PCO_SetTriggerMode
dll.set_trigger_mode.argtypes = [C.c_void_p, C.c_uint16]

dll.force_trigger = dll.PCO_ForceTrigger
dll.force_trigger.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]

dll.set_recorder_submode = dll.PCO_SetRecorderSubmode
dll.set_recorder_submode.argtypes = [C.c_void_p, C.c_uint16]

dll.set_acquire_mode = dll.PCO_SetAcquireMode
dll.set_acquire_mode.argtypes = [C.c_void_p, C.c_uint16]

dll.set_storage_mode = dll.PCO_SetStorageMode
dll.set_storage_mode.argtypes = [C.c_void_p, C.c_uint16]

if __name__ == '__main__':
    """
    Half-assed testing; give randomized semi-garbage inputs, hope the
    plane don't crash.
    """
    camera = Edge()
    for i in range(10000):
        """
        Random exposure time, biased towards shorter exposures
        """
        exposure = min(np.random.randint(1e2, 1e7, size=40))
        """
        Random ROI, potentially with some/all limits unspecified.
        """
        roi = {
            'top': np.random.randint(low=-2000, high=3000),
            'bottom': np.random.randint(low=-2000, high=3000),
            'left': np.random.randint(low=-2000, high=3000),
            'right': np.random.randint(low=-2000, high=3000)}
        roi = {k: v for k, v in roi.items() if v > -10} #Delete some keys/vals
        camera.apply_settings(exposure_time_microseconds=exposure,
                              region_of_interest=roi)
        camera.arm(num_buffers=np.random.randint(1, 16))
        print("Allocating memory...")
        images = np.zeros((np.random.randint(1, 5), camera.height, camera.width),
                          dtype=np.uint16)
        print("Done allocating memory.")
        print("Expected time:",
              images.shape[0] *
              1e-6 * max(camera.rolling_time_microseconds,
                         camera.exposure_time_microseconds))
        start = time.perf_counter()
        camera.record_to_memory(num_images=images.shape[0], out=images)
        print("Elapsed time:", time.perf_counter() - start)
        
        print(images.min(), images.max(), images.shape)
        camera.disarm()
    camera.close()
