/**
 * exif.c - Functions to generate Exif metadata
 *
 * Copyright (c) 2019, David Imhoff <dimhoff.devel@gmail.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the author nor the names of its contributors may
 *       be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <time.h>
#include <sys/time.h>

#include "exif.h"
#include "exif_defines.h"

// TIFF header byte order
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define TIFF_BYTE_ORDER 0x4949
#else // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define TIFF_BYTE_ORDER 0x4d4d
#endif // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__

// Reimplementation of htons() that can be used to initialize a struct member.
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define htons_macro(x) \
  (((x) & 0x00ff) << 8 | \
   ((x) & 0xff00) >> 8)
#else // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define htons_macro(x) (x)
#endif // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__

/**
 * Type for storing Tiff Rational typed data
 */
#pragma pack(1)
typedef struct {
  uint32_t num;
  uint32_t denom;
} TiffRational;
#pragma pack()

/**
 * Type used for IFD entries
 *
 * This type is used to store a value within the Tiff IFD.
 */
#pragma pack(1)
typedef struct {
  uint16_t tag; // Data tag
  uint16_t type; // Data type
  uint32_t length; // length of data
  // Offset of data from start of TIFF data, or data it self if length <= 4.
  // Always use the IFD_SET_*() macros to modify the value.
  uint32_t value;
} IfdEntry;
#pragma pack()

// Some helper macros to initialize the IFD value's. The types shorter then
// 4-bytes need to be aligned such that they are directly after the previous
// field. These macros take care of this.
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define IFD_SET_BYTE(VAL)   (VAL)
# define IFD_SET_SHORT(VAL)  (VAL)
# define IFD_SET_LONG(VAL)   (VAL)
# define IFD_SET_OFFSET(REF, VAR) offsetof(REF, VAR)
# define IFD_SET_UNDEF(V0, V1, V2, V3) \
                             (((V3) & 0xff) << 24 | \
                              ((V2) & 0xff) << 16 | \
                              ((V1) & 0xff) << 8 | \
                              ((V0) & 0xff))
#else // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define IFD_SET_BYTE(VAL)   (((VAL) & 0xff) << 24)
# define IFD_SET_SHORT(VAL)  (((VAL) & 0xffff) << 16)
# define IFD_SET_LONG(VAL)   (VAL)
# define IFD_SET_OFFSET(REF, VAR) offsetof(REF, VAR)
# define IFD_SET_UNDEF(V0, V1, V2, V3) \
                             (((V0) & 0xff) << 24 | \
                              ((V1) & 0xff) << 16 | \
                              ((V2) & 0xff) << 8 | \
                              ((V3) & 0xff))
#endif // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__

// Amount of entries in the 0th IFD
#ifdef WITH_GNSS
# define IFD0_ENTRY_CNT 11
#else
# define IFD0_ENTRY_CNT 12
#endif

// Amount of entries in the Exif private IFD
#define IFD_EXIF_ENTRY_CNT 6

// Amount of entries in the GPS private IFD
#define IFD_GPS_ENTRY_CNT 12

// GPS map datum, probably always 'WGS-84'
#define GPS_MAP_DATUM "WGS-84"

/**
 * New Jpeg/Exif header
 *
 * This defines the new JPEG/Exif header that is added to the images. To keep
 * it simple, the structure of the header is completely static.
 */
#pragma pack(1)
struct JpegExifHdr {
  uint16_t jpeg_soi; // htons(0xffd8)
  uint16_t marker; // htons(0xffe1)
  uint16_t len;    // htons(length)
  uint8_t exif_identifier[6]; // 'Exif\0\0'

  struct TiffData {
    struct {
      uint16_t byte_order; // 'II' || 'MM'
      uint16_t signature; // 0x002A
      uint32_t ifd_offset; // 0x8
    } tiff_hdr;
    struct {
      uint16_t cnt; // amount of entries
      IfdEntry entries[IFD0_ENTRY_CNT];
      uint32_t next_ifd; // Offset of next IFD, or 0x0 if last IFD
    } ifd0;
    struct {
      TiffRational XYResolution;
      char make[sizeof(CAMERA_MAKE)];
      char model[sizeof(CAMERA_MODEL)];
      char software[sizeof(CAMERA_SOFTWARE)];
      char datetime[20];
    } ifd0_data;
    struct {
      uint16_t cnt; // amount of entries
      IfdEntry entries[IFD_EXIF_ENTRY_CNT];
      uint32_t next_ifd; // Offset of next IFD, or 0x0 if last IFD
    } ifd_exif;
#ifdef WITH_GNSS
    struct {
      uint16_t cnt; // amount of entries
      IfdEntry entries[IFD_GPS_ENTRY_CNT];
      uint32_t next_ifd; // Offset of next IFD, or 0x0 if last IFD
    } ifd_gps;
    struct {
      TiffRational latitude[3];
      TiffRational longitude[3];
      TiffRational altitude;
      TiffRational time_stamp[3];
      char map_datum[sizeof(GPS_MAP_DATUM)];
      uint8_t processing_method[8 + 3];
      char date_stamp[11];
    } ifd_gps_data;
#endif //WITH_GNSS
  } tiff_data;
} exif_hdr = {
  htons_macro(0xffd8),
  htons_macro(0xffe1),
  htons_macro(sizeof(JpegExifHdr) - offsetof(JpegExifHdr, len)),
  { 'E', 'x', 'i', 'f', 0, 0 },
  {
    .tiff_hdr = { TIFF_BYTE_ORDER, 0x002A, 0x8 },
    .ifd0 = {
      .cnt = IFD0_ENTRY_CNT,
      .entries = {
        { TagTiffMake,
          TiffTypeAscii,
          sizeof(exif_hdr.tiff_data.ifd0_data.make),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.make) },
        { TagTiffModel,
          TiffTypeAscii,
          sizeof(exif_hdr.tiff_data.ifd0_data.model),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.model) },
#define TAG_IFD0_ORIENTATION_IDX 2
        { TagTiffOrientation,
          TiffTypeShort, 1,
          IFD_SET_SHORT(1) },
        { TagTiffXResolution,
          TiffTypeRational, 1,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data) },
        { TagTiffYResolution,
          TiffTypeRational, 1,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data) },
        { TagTiffResolutionUnit,
          TiffTypeShort, 1,
          IFD_SET_SHORT(0x0002) },
        { TagTiffSoftware,
          TiffTypeAscii,
          sizeof(exif_hdr.tiff_data.ifd0_data.software),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.software) },
        { TagTiffDateTime,
          TiffTypeAscii,
          sizeof(exif_hdr.tiff_data.ifd0_data.datetime),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.datetime) },
        { TagTiffYCbCrPositioning,
          TiffTypeShort, 1,
          IFD_SET_SHORT(0x0001) },
        { TagTiffExifIFD,
          TiffTypeLong, 1,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_exif) },
#ifdef WITH_GNSS
        { TagTiffGPSIFD,
          TiffTypeLong, 1,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps) },
#endif // WITH_GNSS
      },
      .next_ifd = 0
    },
    .ifd0_data = {
      { 72, 1 },
      CAMERA_MAKE,
      CAMERA_MODEL,
      CAMERA_SOFTWARE,
      "    :  :     :  :  ",
    },
    .ifd_exif = {
      .cnt = IFD_EXIF_ENTRY_CNT,
      .entries = {
        { TagExifVersion,
          TiffTypeUndef, 4,
          IFD_SET_UNDEF(0x30, 0x32, 0x33, 0x30) },
        { TagExifComponentsConfiguration,
          TiffTypeUndef, 4,
          IFD_SET_UNDEF(0x01, 0x02, 0x03, 0x00) },
#define TAG_EXIF_SUBSEC_TIME_IDX 2
        { TagExifSubSecTime,
          TiffTypeAscii, 4,
          IFD_SET_UNDEF(0x20, 0x20, 0x20, 0x00) },
        { TagExifColorSpace,
          TiffTypeShort, 1,
          IFD_SET_SHORT(1) },
#define TAG_EXIF_PIXEL_X_DIMENSION_IDX 4
        { TagExifPixelXDimension,
          TiffTypeShort, 1,
          IFD_SET_SHORT(1600) },
#define TAG_EXIF_PIXEL_Y_DIMENSION_IDX (TAG_EXIF_PIXEL_X_DIMENSION_IDX + 1)
        { TagExifPixelYDimension,
          TiffTypeShort, 1,
          IFD_SET_SHORT(1200) },
      },
      .next_ifd = 0
    },
#ifdef WITH_GNSS
    .ifd_gps = {
      .cnt = IFD_GPS_ENTRY_CNT,
      .entries = {
        { TagGPSVersionID,
          TiffTypeByte, 4,
          IFD_SET_UNDEF(2, 3, 0, 0) },
#define TAG_GPS_LATITUDE_REF_IDX 1
        { TagGPSLatitudeRef,
          TiffTypeAscii, 2,
          IFD_SET_UNDEF(0x00, 0x00, 0x00, 0x00) },
        { TagGPSLatitude,
          TiffTypeRational, 3,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.latitude) },
#define TAG_GPS_LONGITUDE_REF_IDX 3
        { TagGPSLongitudeRef,
          TiffTypeAscii, 2,
          IFD_SET_UNDEF(0x00, 0x00, 0x00, 0x00) },
        { TagGPSLongitude,
          TiffTypeRational, 3,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.longitude) },
#define TAG_GPS_ALTITUDE_REF_IDX 5
        { TagGPSAltitudeRef,
          TiffTypeByte, 1,
          IFD_SET_UNDEF(0x00, 0x00, 0x00, 0x00) },
        { TagGPSAltitude,
          TiffTypeRational, 1,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.altitude) },
        { TagGPSTimeStamp,
          TiffTypeRational, 3,
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.time_stamp) },
#define TAG_GPS_STATUS_IDX 8
        { TagGPSStatus,
          TiffTypeAscii, 2,
          IFD_SET_UNDEF('V', 0x00, 0x00, 0x00) },
/*TODO: currently not available from MicroNMEA
        { TagGPSMeasureMode,
          TiffTypeAscii, 2,
          IFD_SET_UNDEF('2', 0x00, 0x00, 0x00) },*/
        { TagGPSMapDatum,
          TiffTypeAscii,
          sizeof(exif_hdr.tiff_data.ifd_gps_data.map_datum),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.map_datum) },
        { TagGPSProcessingMethod,
          TiffTypeUndef,
          sizeof(exif_hdr.tiff_data.ifd_gps_data.processing_method),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.processing_method) },
        { TagGPSDateStamp,
          TiffTypeAscii,
          sizeof(exif_hdr.tiff_data.ifd_gps_data.date_stamp),
          IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.date_stamp) },
      },
      .next_ifd = 0
    },
    .ifd_gps_data = {
      { { 0, 1000*1000 }, { 0, 1 }, { 0, 1 } },
      { { 0, 1000*1000 }, { 0, 1 }, { 0, 1 } },
      { 0, 1000 },
      { { 0, 1 }, { 0, 1 }, { 0, 1 } },
      GPS_MAP_DATUM,
      { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, 'G', 'P', 'S' },
      "    :  :  ",
    }
#endif // WITH_GNSS
  }
};
#pragma pack()

bool update_exif_from_cfg(const uint8_t c)
{
  exif_hdr.tiff_data.ifd0.entries[TAG_IFD0_ORIENTATION_IDX].value = IFD_SET_SHORT(c);

  return true;
}

#ifdef WITH_GNSS
void update_exif_gps(const MicroNMEA& nmea)
{
  // Latitude
  long lat = nmea.getLatitude();
  if (lat < 0) {
    exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LATITUDE_REF_IDX].value = IFD_SET_BYTE('S');
    lat *= -1;
  } else {
    exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LATITUDE_REF_IDX].value = IFD_SET_BYTE('N');
  }
  exif_hdr.tiff_data.ifd_gps_data.latitude[0].num = lat;

  // Longitude
  long lon = nmea.getLongitude();
  if (lon < 0) {
    exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LONGITUDE_REF_IDX].value = IFD_SET_BYTE('W');
    lon *= -1;
  } else {
    exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LONGITUDE_REF_IDX].value = IFD_SET_BYTE('E');
  }
  exif_hdr.tiff_data.ifd_gps_data.longitude[0].num = lon;

  // Altitude
  long alt;
  if (nmea.getAltitude(alt)) {
    if (alt < 0) {
      exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_ALTITUDE_REF_IDX].value = IFD_SET_BYTE(1);
      alt *= -1;
    } else {
      exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_ALTITUDE_REF_IDX].value = IFD_SET_BYTE(0);
    }
    exif_hdr.tiff_data.ifd_gps_data.altitude.num = alt;
  } else {
    exif_hdr.tiff_data.ifd_gps_data.altitude.num = 0;
  }

  // time stamp
  exif_hdr.tiff_data.ifd_gps_data.time_stamp[0].num = nmea.getHour();
  exif_hdr.tiff_data.ifd_gps_data.time_stamp[1].num = nmea.getMinute();
  exif_hdr.tiff_data.ifd_gps_data.time_stamp[2].num = nmea.getSecond();

  // GPS Status
  if (nmea.isValid()) {
    exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_STATUS_IDX].value = IFD_SET_BYTE('A');
  } else {
    exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_STATUS_IDX].value = IFD_SET_BYTE('V');
  }

  // date stamp
  snprintf(exif_hdr.tiff_data.ifd_gps_data.date_stamp,
      sizeof(exif_hdr.tiff_data.ifd_gps_data.date_stamp),
      "%04u:%02u:%02u",
      nmea.getYear(),
      nmea.getMonth(),
      nmea.getDay());
}
#endif // WITH_GNSS

const uint8_t *get_exif_header(camera_fb_t *fb, const uint8_t **exif_buf, size_t *exif_len)
{
  // TODO: pass config to function and use that to set some of the image
  // taking conditions. Or do this only once, with a update config
  // function????

  // Get current time
  struct timeval now_tv;
  if (gettimeofday(&now_tv, NULL) != 0) {
    now_tv.tv_sec = time(NULL);
    now_tv.tv_usec = 0;
  }

  // Set date time
  struct tm timeinfo;
  localtime_r(&now_tv.tv_sec, &timeinfo);
  strftime(exif_hdr.tiff_data.ifd0_data.datetime, sizeof(exif_hdr.tiff_data.ifd0_data.datetime), "%Y:%m:%d %H:%M:%S", &timeinfo);

  // Set sub-seconds time
  snprintf( (char *) &(exif_hdr.tiff_data.ifd_exif.entries[TAG_EXIF_SUBSEC_TIME_IDX].value), 9, "%03ld", now_tv.tv_usec/1000);

  // Update image dimensions
  exif_hdr.tiff_data.ifd_exif.entries[TAG_EXIF_PIXEL_X_DIMENSION_IDX].value = IFD_SET_SHORT(fb->width);
  exif_hdr.tiff_data.ifd_exif.entries[TAG_EXIF_PIXEL_Y_DIMENSION_IDX].value = IFD_SET_SHORT(fb->height);

  *exif_len = sizeof(exif_hdr);

  if (exif_buf != NULL) {
    *exif_buf = (uint8_t *) &exif_hdr;
  }

  return (uint8_t *) &exif_hdr;
}

size_t get_jpeg_data_offset(camera_fb_t *fb)
{
  if (fb->len < 6) {
    return 0;
  }

  // Check JPEG SOI
  if (fb->buf[0] != 0xff || fb->buf[1] != 0xd8) {
    return 0;
  }
  size_t data_offset = 2; // Offset to first JPEG segment after header

  // Check if JFIF header
  if (fb->buf[2] == 0xff && fb->buf[3] == 0xe0) {
    uint16_t jfif_len = fb->buf[4] << 8 | fb->buf[5];

    data_offset += 2 + jfif_len;
  }

  if (data_offset >= fb->len) {
    return 0;
  }

  return data_offset;
}
