/* -------------------------------------------------------------------------- */
/* -                 Astronomical CCD Camera Control                        - */
/* -                    ASI Cameras Wrapper for C                           - */
/* -------------------------------------------------------------------------- */
/*                                                                            */
/* Copyright (c) 2021 John Kielkopf                                           */
/* kielkopf@louisville.edu                                                    */
/*                                                                            */
/* Library to make ASI camera operations available to xmccd in C              */
/* This file is part of XmCCD.                                                */
/*                                                                            */
/* -------------------------------------------------------------------------- */


#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <time.h>
#include <string>

/* Needed for ASI library */

#include <ASICamera2.h>

/* Needed for xmccd */

#include "xmccd_asi_wrap.h"

extern "C" {

  /* Global in this scope */

  static int exp_width = 0;
  static int exp_height = 0;
  static int exp_gain = 0;
  static int exp_bin = 1;
  static int camera_id;
  double exp_duration;



  /* Connect to a camera and perform a one-time setup */
  /* Returns True on success and False on failure */
  /* Prints diagnostics to stdout */

  int asi_open(int selected_camera)
  {
    int i;
    int numDevices = ASIGetNumOfConnectedCameras();
    if(numDevices <= 0)
    {
      printf("XmCCD cannot find a camera. Press any key to exit.\n");
      getchar();
      return 0;
    }
    else
    {
      printf("XmCCD found these attached cameras: \n");
    }
    
    ASI_CAMERA_INFO ASICameraInfo;

    for(i = 0; i < numDevices; i++)
    {
      ASIGetCameraProperty(&ASICameraInfo, i);
      printf("%d %s\n",i, ASICameraInfo.Name);
    }
    
    camera_id = selected_camera;
    
    ASI_ERROR_CODE asi_error;
    
    asi_error = ASIOpenCamera(camera_id);
    
    if (asi_error != ASI_SUCCESS)
    {
      printf("ASI reports error # %d opening camera %d \n", asi_error, camera_id);
      return 0;
    }
    
    asi_error = ASIInitCamera(camera_id);
    if (asi_error != ASI_SUCCESS)
    {
      printf("ASI reports error # %d initializing camera %d \n", asi_error, camera_id);
      return 0;
    }

    printf("Camera %d selected, opened, and intialized. \n", camera_id);  
    return 1 ;

    
  }

  /* Return available camera parameters for the selected camera id */

  void asi_get_values(int *width, int *height, double *temperature) 
  {

    /* Get the sensor size from the camera info function*/

    ASI_CAMERA_INFO ASICameraInfo;
    ASIGetCameraProperty(&ASICameraInfo, camera_id);
    
    *width = ASICameraInfo.MaxWidth;
    *height = ASICameraInfo.MaxHeight;
    
    /* Get the temperature from the get control value function */  
    
    /* This is the long integer sensor raw value in ADU  that is 10x the temperature */
    
    long temperature_adu = 0;
    ASI_BOOL temperature_auto;
    
    ASIGetControlValue(camera_id, ASI_TEMPERATURE, &temperature_adu, &temperature_auto);
    *temperature = (double)temperature_adu/10.0; 
   
  }

  /* Start an unbinned exposure allowing for a region of interest */

  int asi_expose(double exposure, int x, int y, int height, int width, 
    int shutter_flag)
  {

    int exp_duration_microsec;
    
    ASI_ERROR_CODE asi_error; 
    ASI_EXPOSURE_STATUS asi_status; 
    ASI_IMG_TYPE asi_image_type; 
    
    /* ASI requires width%8=0 and height%2=0 */
    
    exp_width = 8*(width/8);
    exp_height = 2*(height/2);  
    
    /* ASI times exposure in integer microsecond ticks */
    
    exp_duration = exposure;
    exp_duration_microsec = (int)1000000.0*exp_duration;
    
    /* The duration must be >= 0  */
    
    if (exp_duration_microsec < 0)
    {
      exp_duration_microsec = 0;
    }  

    /* Always use 16-bit raw to be compatible with XmCCD image buffers */
    
    asi_image_type = ASI_IMG_RAW16;
    
    /* Set the region of interest width and height centered on the detector */

    asi_error = ASISetROIFormat(camera_id, exp_width, exp_height, exp_bin, asi_image_type);
    if (ASI_SUCCESS != asi_error)
    {
      return 0;
    }  

    /* Adjust the region of interest starting pixel */
     
    asi_error = ASISetStartPos(camera_id, x, y);
    if (ASI_SUCCESS != asi_error)
    {
      return 0;
    }  
      
    /* Set default gain (usually 0) for science images */
    
    ASISetControlValue(camera_id, ASI_GAIN, exp_gain, ASI_FALSE);
    
    /* Start an exposure without on-camera dark subtraction */
          
    ASISetControlValue(camera_id, ASI_EXPOSURE, exp_duration_microsec, ASI_FALSE);
    ASISetControlValue(camera_id, ASI_BANDWIDTHOVERLOAD, 40, ASI_FALSE);
    ASIStartExposure(camera_id, ASI_FALSE);
    asi_status = ASI_EXP_WORKING;
    
    /* Check the status of the camera */
    
    ASIGetExpStatus(camera_id, &asi_status);
    
    /* Pause 10000 microseconds for the  camera to start an exposure */
    
    usleep(10000);

    /* Check the exposure status which may already be done for a short exposure */

    ASIGetExpStatus(camera_id, &asi_status);
    if ( asi_status == ASI_EXP_WORKING)
    {
      return 1;
    }
    else if (asi_status == ASI_EXP_SUCCESS)
    {
      return 1;
    }
    else
    {
      return 0;
    }
                
  }

  /* Stop an exposure without reading out */
  /* This allows for reading out an exposure later */

  int asi_stop_exposure(void)
  {
    ASI_EXPOSURE_STATUS  exp_status;

    ASIGetExpStatus(camera_id, &exp_status);
    
    if (ASI_EXP_IDLE == exp_status)
    {
      /* Return 1 because the exposure is stopped  */
      return 1;
    }  
     

    if (ASI_EXP_FAILED == exp_status)
    {
      /* Return 1 because the previous exposure failed */
      return 1;
    }  
     

    ASIStopExposure(camera_id);
    ASIGetExpStatus(camera_id, &exp_status);

    /* Return 2 for success and that the camera may be read out at this time */
   
    if (ASI_EXP_SUCCESS == exp_status)
    { 
      return 2 ;
    }

    /* Unknown response from the request to stop the exposure */
    
    return 0;      
  }

  /* Check camera exposure status */

  int asi_get_status(void)
  {

    ASI_EXPOSURE_STATUS exp_status;
    
    /* Test the ASI flag for whether an image is in progress */
    /*   Possible responses are                         */
    /*     ASI_EXP_IDLE  ready to start an exposure     */
    /*     ASI_EXP_WORKING taking an exposure           */
    /*     ASI_EXP_SUCCESS finished and ready to read   */
    /*     ASI_EXP_FAILED  must start an exposure again */
      
    ASIGetExpStatus(camera_id, &exp_status);
    
    /* Is the camera still taking an exposure? */
    
    if (ASI_EXP_WORKING == exp_status)
    {
      /* Return 1 if exposure is integrating */
      return 1;
    }  
    
    /* Is an image is ready to download?  */

    if  (ASI_EXP_SUCCESS == exp_status)
    {
      
      /* Return 2 if the current exposure is complete */
      return 2;
    }
    
    /* Return 0 if the camera is ready for another exposure */
    
    return 0;  
  }

  /* Return the image based on data size from exposure request */

  int asi_get_image(unsigned short int *data)
  {  
    
    ASI_ERROR_CODE data_error;
    
    /* Create a pointer to the downloaded 16-bit image data space */
    
    long image_size = exp_width*exp_height*2;
    unsigned char* image_buffer = new unsigned char[image_size];
          
    /* Get the image from the camera in driver format */
    
    data_error = ASIGetDataAfterExp(camera_id, image_buffer, image_size);

    if (data_error != ASI_SUCCESS)
    {
      return 0;
    }  
    
    /* Copy the image to the calling dats array address */
    
    bcopy(image_buffer, data, image_size);

    return 1 ;
  }

  /* Set cooler target temperature */

  void asi_set_cooler_temperature(double new_temperature)
  {    
    long target_temperature= (long)new_temperature;
    ASI_BOOL temperature_auto = ASI_TRUE;
    ASISetControlValue(camera_id,  ASI_TARGET_TEMP, target_temperature, temperature_auto);
  }


  /* Set cooler and states */

  void asi_set_cooler_state(int state)
  {
    ASI_BOOL cooler_auto = ASI_FALSE;
    ASI_BOOL fan_auto = ASI_FALSE;
    long cooler_adu = 0;
    long fan_adu = 0;
    printf("Setting cooler state %d. \n", state);
    if (state == 1) 
    {
      cooler_auto = ASI_TRUE;
      fan_auto = ASI_TRUE;
      cooler_adu = 100;
      fan_adu = 100;
      ASISetControlValue(camera_id, ASI_COOLER_ON, cooler_adu, cooler_auto);
      ASISetControlValue(camera_id, ASI_FAN_ON, fan_adu, fan_auto);
    } 
    else 
    {    
      cooler_auto = ASI_FALSE;
      fan_auto = ASI_FALSE; 
      cooler_adu = 0;
      fan_adu = 0;   
      ASISetControlValue(camera_id, ASI_COOLER_ON, cooler_adu,  cooler_auto);
      ASISetControlValue(camera_id, ASI_FAN_ON, fan_adu, fan_auto);
    }
  }


  /* Get cooler states              */
  /*   0 = off                      */
  /*   1 = on and not at set point  */              
  /*   2 = at set point             */                     

  int asi_get_cooler_temperature(double *cooler_temperature)
  {

    long fan_adu = 0;
    ASI_BOOL fan_auto = ASI_FALSE;
    ASIGetControlValue(camera_id, ASI_FAN_ON, &fan_adu, &fan_auto);

    long cooler_adu = 0;
    ASI_BOOL cooler_auto = ASI_FALSE;
    ASIGetControlValue(camera_id, ASI_COOLER_ON, &cooler_adu, &cooler_auto);

    long target_temperature_adu = 0;
    ASI_BOOL target_temperature_auto = ASI_TRUE;
    ASIGetControlValue(camera_id, ASI_TARGET_TEMP, &target_temperature_adu, &target_temperature_auto);
    
    long temperature_adu = 0;
    double temperature = 0.0;
    ASI_BOOL temperature_auto = ASI_TRUE;
    ASIGetControlValue(camera_id, ASI_TEMPERATURE, &temperature_adu, &temperature_auto);
    temperature = temperature_adu;
    *cooler_temperature = temperature/10.0;

    if (ASI_TRUE == cooler_auto)
    {
    
      return 1;

    }
    else if (ASI_FALSE == cooler_auto)
    {
      return 0;
    }

    return 0;
  }


  /* Actuate a guider relay */
  /* This works only for ASI cameras with an ST4 type guider output */

  int asi_guide(int relay, double ontime)
  {
    
    int delay_us = 1000;
    delay_us = 1000000.0*ontime;
    if (delay_us < 1000) 
    {
      delay_us = 1000;
    }  
    else if (delay_us > 5000000)
    {
      delay_us = 5000000;
    }   
    
    if (relay == 0)
    {
      ASIPulseGuideOn(camera_id, ASI_GUIDE_NORTH);
    }
    else if (relay == 1)
    {
      ASIPulseGuideOn(camera_id, ASI_GUIDE_SOUTH);
    }
    else if (relay == 2)
    {
      ASIPulseGuideOn(camera_id, ASI_GUIDE_EAST);
    }
    else if (relay == 3)
    {
      ASIPulseGuideOn(camera_id, ASI_GUIDE_WEST);
    }
   
    /* Relays on for a timed delay from 1 ms to 5 seconds */
    
    usleep(delay_us);
    
    /* All relays turned off */
    
    ASIPulseGuideOff(camera_id, ASI_GUIDE_NORTH);  
    ASIPulseGuideOff(camera_id, ASI_GUIDE_SOUTH);
    ASIPulseGuideOff(camera_id, ASI_GUIDE_EAST);
    ASIPulseGuideOff(camera_id, ASI_GUIDE_WEST);
    
    /* ASIPulseGuide commands return error flags that could be trapped here */
    /* This routine returns TRUE if it has run successfully */
    
    return 1;
  }


  /* Return pointers to stings with sensor information and camera model */

  void asi_get_info(char *sensor, char *camera)
  {  

    double electron_per_adu;
    char sensor_buffer[64];
    
    ASI_CAMERA_INFO ASICameraInfo;
    ASIGetCameraProperty(&ASICameraInfo, camera_id);
    electron_per_adu = ASICameraInfo.ElecPerADU;
    strncpy(camera, ASICameraInfo.Name, 64);
    snprintf(sensor_buffer, sizeof sensor_buffer, "%.3f", electron_per_adu);
    strncpy(sensor, sensor_buffer, 64);

  }


}
