import time
import ctypes as C
import os
import numpy as np

class Camera:
    def __init__(self, verbose=True, very_verbose=False):
        self.verbose = verbose
        self.very_verbose = very_verbose
        self.camera_handle = C.c_void_p(0)
        if verbose: print("Opening pco camera...")
        try:
            # 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(self.camera_handle, 0)
            assert self.camera_handle.value is not None
        except (WindowsError, AssertionError):
            print("Failed to open pco camera.")
            print(" *Is the camera on, and plugged into the computer?")
            print(" *Is CamWare running? It shouldn't be!")
            print(" *If using a CameraLink camera, is sc2_cl_me4.dll in",
                  "the same directory as SC2_Cam.dll?")
            raise
        self._get_camera_type()
        if self.verbose: print(" pco.%s camera open." % self.camera_type)
        dll.reset_settings_to_default(self.camera_handle)
        self.disarm()
        self._refresh_camera_setting_attributes()
        return None

    def close(self):
        self.disarm()
        if self.verbose: print("Closing pco.%s camera..." % self.camera_type)
        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 100 and as high
          as 10000000.
        * 'region_of_interest' will be adjusted to match the nearest
          legal ROI that the camera supports. See _legalize_roi() for
          details.
        """
        if trigger is None:
            trigger = self.trigger_mode
        if exposure_time_microseconds is None:
            exposure_time_microseconds = self.exposure_time_microseconds
        if region_of_interest is None:
            region_of_interest = self.roi
        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')
        try:
            self._set_pixel_rate({'edge 4.2': 272250000,
                                  'edge 4.2 bi': 46000000,
                                  'edge 5.5': 286000000,
                                  'pixelfly':  24000000, # Read from camera once
                                  'panda 4.2': 44000000, # Read from camera once
                                  }[self.camera_type])
        except WindowsError:
            # TODO we can remove when we don't have any pandas with older firmware
            self._set_pixel_rate(0)
            print('WARNING! Setting pixel rate to zero (for older versions of '
                  ' panda firmware)')
        # 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 k, v in camera_health.items():
            if k == 'status' and self.camera_type == 'panda 4.2':
                # TODO remove tolerance  of status 16. This is only for older
                # firmware versions of the panda.
                assert v == 0 or v == 16, 'Status code %d' % v
            else:
                assert v == 0
        return None

    def arm(self, num_buffers=None):
        if not hasattr(self, '_default_num_buffers'):
            self._default_num_buffers = 2
        if num_buffers is None:
            num_buffers = self._default_num_buffers
        assert 1 <= num_buffers <= 16
        self._default_num_buffers = num_buffers
        if self.armed:
            if self.verbose:
                print('Arm requested, but the pco 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 not hasattr(self, 'armed'):
            self.armed = False
        if not self.armed:
            if self.camera_type == 'pixelfly':
                return None # pixelfly throws an error if disarmed twice
        if self.verbose: print("Disarming camera...")
        dll.set_recording_state(self.camera_handle, 0)
        dll.cancel_images(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.zeros((num_images - preframes, self.height, self.width),
                           'uint16')
            out[:, 1, 1].fill(1) # For error checking empty images
            return_value = out
        else:
            return_value = None # Output is placed in the 'out' array
        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)")
            print(" and dtype='uint16'")
            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.
            self.num_polls = 0
            self.num_sleeps = 0
            start_time = time.perf_counter()
            while True:
                # Check if a buffer is ready
                self.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", self.num_polls, "polls and", self.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
                    self.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 self.num_polls > poll_timeout or self.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,"%(self.num_polls) +
                        " %i sleeps"%(self.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 return_value

    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 camera 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_camera_type()
        self._get_timestamp_mode()
        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_timestamp_mode(self):
        wTimeStamp = C.c_uint16(777) #777 is not an expected output
        dll.get_timestamp_mode(self.camera_handle, wTimeStamp)
        assert wTimeStamp.value in (0, 1, 2, 3) #wTimeStamp.value should change
        mode_names = {0: "off",
                      1: "binary",
                      2: "binary+ASCII",
                      3: "ASCII"}
        if self.verbose:
            print(" Timestamp mode:", mode_names[wTimeStamp.value])
        self.timestamp_mode = mode_names[wTimeStamp.value]
        return self.timestamp_mode

    def _set_timestamp_mode(self, mode='off'):
        mode_numbers = {"off": 0,
                        "binary": 1,
                        "binary+ASCII": 2,
                        "ASCII": 3}
        if self.verbose:
            print(" Setting timestamp mode to:", mode)
        dll.set_timestamp_mode(self.camera_handle, mode_numbers[mode])
        assert self._get_timestamp_mode() == mode
        return self.timestamp_mode

    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,
            "software_trigger": 1,
            "external_trigger": 2,
            "external_exposure": 3}
        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)
        ## TODO: Checking of the reported pixel rate could be greatly improved.
        if self.camera_type == 'panda 4.2':
            # TODO: Older versions of the panda firmware report the  pixel rate
            # as zero. This can be removed once we dont have any cameras with
            # the older firmware.
            assert dwPixelRate.value != 0 or dwPixelRate.value == 0
        else:
            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)
        if self.camera_type in ('edge 4.2', 'edge 5.5', 'pixelfly'):
            assert 1e2 <= exposure_time_microseconds <= 1e7
        elif self.camera_type == 'panda 4.2':
            ## TODO: is the minimum exposure really 9 us?
            assert 9 <= 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)
        self._get_exposure_time()
        if self.camera_type == 'panda 4.2':
            tolerance = 13
        elif self.camera_type == 'edge 4.2 bi':
            tolerance = 6
        else:
            tolerance = 0
        assert abs(self.exposure_time_microseconds -
                   exposure_time_microseconds) <= tolerance
        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}
        self.width =  self.roi['right'] - self.roi['left'] + 1
        self.height = self.roi['bottom'] - self.roi['top'] + 1
        self.rolling_time_microseconds =  self._calculate_rolling_time_us(
            wRoiY0.value, wRoiY1.value)
        return self.roi

    def _calculate_rolling_time_us(self, y0, y1):
        '''How long do we expect the chip to spend rolling, in microseconds?

        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.camera_type == 'edge 4.2':
            max_lines = 1024
            full_chip_rolling_time = 1e4
        elif self.camera_type == 'edge 5.5':
            max_lines = 1080
            full_chip_rolling_time = 1e4
        elif self.camera_type in ('panda 4.2', 'edge 4.2 bi'):
            max_lines = 1024
            full_chip_rolling_time = 2.5e4 # TODO: verify rolling time for panda
        # TODO: calculate rolling time for pixelfly...better
        elif self.camera_type == 'pixelfly':
            full_chip_rolling_time = 7.5e4
            return full_chip_rolling_time
        chip_fraction = max(y1 - max_lines, max_lines + 1 - y0) / max_lines
        return full_chip_rolling_time * chip_fraction

    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.camera_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 _get_camera_type(self):
        camera_name = C.c_char_p(b' '*40)
        dll.get_camera_name(self.camera_handle, camera_name, 40)
        name2type = {'pco.edge rolling shutter 4.2': 'edge 4.2',
                     'pco.edge 4.2 bi': 'edge 4.2 bi',
                     'pco.edge 5.5': 'edge 5.5', # This is probably wrong
                     'pco.panda 4.2': 'panda 4.2',
                     'pco.USB.Pixel.Fly': 'pixelfly'}
        try:
            self.camera_type = name2type[camera_name.value.decode('ascii')]
        except KeyError:
            raise UserWarning('Unexpected camera type - %s' % camera_name.value)
        return self.camera_type

def legalize_roi(
    roi,
    camera_type='edge 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 = 1, 1
    if camera_type == 'edge 4.2':
        min_width, min_height = 40, 10
        max_lr, max_ud, step_lr, = 2060, 2048, 20
    elif camera_type == 'edge 4.2 bi':
        min_width, min_height = 32, 16
        max_lr, max_ud, step_lr, = 2048, 2048, 32
    elif camera_type == 'edge 5.5':
        min_width, min_height = 160, 10
        max_lr, max_ud, step_lr = 2560, 2160, 160
    elif camera_type == 'pixelfly':
        min_width, min_height = 1392, 1040
        max_lr, max_ud, step_lr = 1392, 1040, 1392
    elif camera_type == 'panda 4.2':
        # TODO min_width can be set to 32 when we upgrade the firmware on
        # old pandas.
        min_width, min_height = 192, 10
        max_lr, max_ud, step_lr = 2048, 2048, 32
    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 decode_timestamps(image_stack):
    """Decode PCO image timestamps from binary-coded decimal.
    """
    assert len(image_stack.shape) == 3
    assert image_stack.dtype == 'uint16'
    timestamps = image_stack[:, 0, :14]
    timestamps = (timestamps & 0x0F) + (timestamps >> 4) * 10
    ts = {}
    ts['image_number'] = np.sum(
        timestamps[:, :4] * np.array((1e6, 1e4, 1e2, 1)),
        axis=1, dtype='uint32')
    ts['year'] = np.sum(
        timestamps[:, 4:6] * np.array((1e2, 1)),
        axis=1, dtype='uint32')
    ts['month'] = timestamps[:, 6].astype('uint32')
    ts['day'] = timestamps[:, 7].astype('uint32')
    ts['microseconds'] = np.sum(
        timestamps[:, 8:14] * np.array((3600e6, 60e6, 1e6, 1e4, 1e2, 1)),
        axis=1, dtype='uint64')
    return ts

def reboot_camera():
    """ Reboot the attached camera. 

        While this appears to work and recover the camera from a number
        of error states, until recently, we always ran this as a
        stand-alone script.

        If you need to reboot the camera from within a script, the steps are:
            * dll.reboot_camera
            * dll.close_camera
            * wait for reboot to complete
            * dll.reset_dll
            * dll.open_camera
    """
    print('Connecting to camera...')
    camera_handle = C.c_void_p(0)
    dll.open_camera(camera_handle, 0)
    print('Done connecting.')

    print('Rebooting camera...')
    dll.reboot_camera(camera_handle)
    print('Done rebooting.')

    print('Disconnecting from camera...', flush=True)
    # We know the camera was just rebooted, so no need to check armed/recording
    # state, just close it.
    dll.close_camera(camera_handle)
    print('Done disconnecting.')

    print('Reconnecting to camera...', flush=True)
    t0, timeout = time.perf_counter(), 10
    while True:
        # Reboot time is approximate, keep trying to open the camera until 
        # we are sucessful or timeout has elapsed.
        try:
            dll.reset_dll()
            dll.open_camera(camera_handle, 0)
        except OSError as e:
            if time.perf_counter() - t0 > timeout: 
                raise
            time.sleep(0.2)
        else:
            dll.close_camera(camera_handle)
            print('Done reconnecting.')
            return

def pco_camera_child_process(
    data_buffers,
    buffer_shape,
    input_queue,
    output_queue,
    commands,
    ):
    """For use with image_data_pipeline.py

    Probably will be deprecated soon.

    https://github.com/AndrewGYork/tools/blob/master/image_data_pipeline.py
    Debugged for the edge 4.2, less so for the edge 5.5, pixelfly and panda 4.2.
    """
    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.Camera(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_path = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        'SC2_Cam.dll')
    dll = C.oledll.LoadLibrary(dll_path)
except WindowsError:
    print("Failed to load SC2_Cam.dll")
    print("You need this to run pco.py")
    raise

dll.get_error_text = dll.PCO_GetErrorText
dll.get_error_text.argtypes = [C.c_uint32,
                               C.c_char_p,
                               C.c_uint32]
def check_error(error_code):
    if error_code == 0:
        return 0
    else:
        error_description = C.c_char_p(b'*'*1000)
        dll.get_error_text(error_code, error_description, 1000)
        raise OSError(error_description.value.decode('ascii'))

dll.open_camera = dll.PCO_OpenCamera
dll.open_camera.argtypes = [C.POINTER(C.c_void_p), C.c_uint16]
dll.open_camera.restype = check_error

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

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

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.allocate_buffer.restype = check_error


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.add_buffer.restype = check_error

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.get_buffer_status.restype = check_error

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

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

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_sizes.restype = check_error

dll.get_timestamp_mode = dll.PCO_GetTimestampMode
dll.get_timestamp_mode.argtypes = [C.c_void_p, C.POINTER(C.c_uint16)]
dll.get_timestamp_mode.restype = check_error

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

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_camera_health.restype = check_error

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_temperature.restype = check_error

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

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

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

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

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

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

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.get_delay_exposure_time.restype = check_error

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.set_delay_exposure_time.restype = check_error

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.get_roi.restype = check_error

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.set_roi.restype = check_error

dll.get_camera_name = dll.PCO_GetCameraName
dll.get_camera_name.argtypes = [
    C.c_void_p,
    C.c_char_p,
    C.c_uint16]
dll.get_camera_name.restype = check_error

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

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

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

dll.cancel_images = dll.PCO_CancelImages
dll.cancel_images.argtypes = [C.c_void_p]
dll.cancel_images.restype = check_error

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

dll.set_timestamp_mode = dll.PCO_SetTimestampMode
dll.set_timestamp_mode.argtypes = [C.c_void_p, C.c_uint16]
dll.set_timestamp_mode.restype = check_error

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

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

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

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

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

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

dll.reboot_camera = dll.PCO_RebootCamera
dll.reboot_camera.argtypes = [C.c_void_p]

dll.reset_dll = dll.PCO_ResetLib
dll.reset_dll.restype = check_error

if __name__ == '__main__':
    camera = Camera(verbose=True, very_verbose=True)
    # Half-assed edge testing; give randomized semi-garbage inputs, hope
    # the plane don't crash.
    blank_frames = 0
    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)}
        # Delete some keys/vals
        roi = {k: v for k, v in roi.items() if v > -10}
        camera.apply_settings(exposure_time_microseconds=exposure,
                              region_of_interest=roi)
        num_buffers = np.random.randint(1, 16)
        camera.arm(num_buffers=num_buffers)
        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,
                                first_trigger_timeout_seconds=5)
        print("Elapsed time:", time.perf_counter() - start)
        print(images.min(axis=(1, 2)), images.max(axis=(1, 2)),
              images.shape)
        if not 0 < images.min() < images.max():
            blank_frames += 1
            print('Blank frame received (%d total)' % blank_frames)
        camera.disarm()
    print("%d blank frames received from %s during test" % (blank_frames,
                                                            camera_to_test))
    camera.close()
