/*
 Author: Michael Droettboom
         mdroe@stsci.edu
*/

#include "astropy_wcs/sip.h"

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

#include <wcserr.h>

#define SIP_ERRMSG(status) WCSERR_SET(status)

void
sip_clear(
    sip_t* sip) {

  assert(sip != NULL);

  sip->a_order = 0;
  sip->a = NULL;
  sip->b_order = 0;
  sip->b = NULL;
  sip->ap_order = 0;
  sip->ap = NULL;
  sip->bp_order = 0;
  sip->bp = NULL;
  sip->crpix[0] = 0.0;
  sip->crpix[1] = 0.0;
  sip->scratch = NULL;
  sip->err = NULL;
}

int
sip_init(
    sip_t* sip,
    const unsigned int a_order, const double* a,
    const unsigned int b_order, const double* b,
    const unsigned int ap_order, const double* ap,
    const unsigned int bp_order, const double* bp,
    const double* crpix /* [2] */) {

  size_t             a_size       = 0u;
  size_t             b_size       = 0u;
  size_t             ap_size      = 0u;
  size_t             bp_size      = 0u;
  size_t             scratch_size = 0u;
  int                status       = 0;
  struct wcserr**    err          = NULL;
  static const char *function     = "sip_init";

  assert(sip != NULL);
  sip_clear(sip);
  err = &(sip->err);

  /* We we have one of A/B or AP/BP, we must have both. */
  if ((a == NULL) ^ (b == NULL)) {
    return wcserr_set(
      SIP_ERRMSG(WCSERR_BAD_COORD_TRANS),
      "Both A and B SIP transform must be defined");
  }

  if ((ap == NULL) ^ (bp == NULL)) {
    return wcserr_set(
      SIP_ERRMSG(WCSERR_BAD_COORD_TRANS),
      "Both AP and BP SIP transform must be defined");
  }


  if (a != NULL) {
    sip->a_order = a_order;
    a_size = (size_t)(a_order + 1u) * (a_order + 1u) * sizeof(double);
    sip->a = malloc(a_size);
    if (sip->a == NULL) {
      sip_free(sip);
      status = wcserr_set(
        SIP_ERRMSG(WCSERR_MEMORY), "Memory allocation failed");
      goto exit;
    }
    memcpy(sip->a, a, a_size);
    if (a_order > scratch_size) {
      scratch_size = a_order;
    }

    sip->b_order = b_order;
    b_size = (size_t)(b_order + 1u) * (b_order + 1u) * sizeof(double);
    sip->b = malloc(b_size);
    if (sip->b == NULL) {
      sip_free(sip);
      status = wcserr_set(
        SIP_ERRMSG(WCSERR_MEMORY), "Memory allocation failed");
      goto exit;
    }
    memcpy(sip->b, b, b_size);
    if (b_order > scratch_size) {
      scratch_size = b_order;
    }
  }

  if (ap != NULL) {
    sip->ap_order = ap_order;
    ap_size = (size_t)(ap_order + 1u) * (ap_order + 1u) * sizeof(double);
    sip->ap = malloc(ap_size);
    if (sip->ap == NULL) {
      sip_free(sip);
      status = wcserr_set(
        SIP_ERRMSG(WCSERR_MEMORY), "Memory allocation failed");
      goto exit;
    }
    memcpy(sip->ap, ap, ap_size);
    if (ap_order > scratch_size) {
      scratch_size = ap_order;
    }

    sip->bp_order = bp_order;
    bp_size = (size_t)(bp_order + 1u) * (bp_order + 1u) * sizeof(double);
    sip->bp = malloc(bp_size);
    if (sip->bp == NULL) {
      sip_free(sip);
      status = wcserr_set(
        SIP_ERRMSG(WCSERR_MEMORY), "Memory allocation failed");
      goto exit;
    }
    memcpy(sip->bp, bp, bp_size);
    if (bp_order > scratch_size) {
      scratch_size = bp_order;
    }
  }

  scratch_size = (scratch_size + 1) * sizeof(double);
  sip->scratch = malloc(scratch_size);
  if (sip->scratch == NULL) {
    sip_free(sip);
    status = wcserr_set(
      SIP_ERRMSG(WCSERR_MEMORY), "Memory allocation failed");
    goto exit;
  }

  sip->crpix[0] = crpix[0];
  sip->crpix[1] = crpix[1];

 exit:

  return status;
}

void
sip_free(sip_t* sip) {
  free(sip->a);
  sip->a = NULL;
  free(sip->b);
  sip->b = NULL;
  free(sip->ap);
  sip->ap = NULL;
  free(sip->bp);
  sip->bp = NULL;
  free(sip->scratch);
  sip->scratch = NULL;
  free(sip->err);
  sip->err = NULL;
}

static INLINE double
lu(
    const unsigned int order,
    const double* const matrix,
    const int x,
    const int y) {

  int index;
  assert(x >= 0 && x <= (int)order);
  assert(y >= 0 && y <= (int)order);

  index = x * ((int)order + 1) + y;
  assert(index >= 0 && index < ((int)order + 1) * ((int)order + 1));

  return matrix[index];
}

static int
sip_compute(
    /*@unused@*/ const unsigned int naxes,
    const unsigned int nelem,
    const unsigned int m,
    /*@null@*/ const double* a,
    const unsigned int n,
    /*@null@*/ const double* b,
    const double* crpix /* [2] */,
    /*@null@*/ double* tmp,
    /*@null@*/ const double* input /* [NAXES][nelem] */,
    /*@null@*/ double* output /* [NAXES][nelem] */) {

  unsigned int  i;
  int           j, k;
  double        x, y;
  double        sum;
  const double* input_ptr;
  double*       output_ptr;

  assert(a != NULL);
  assert(b != NULL);
  assert(crpix != NULL);
  assert(tmp != NULL);
  assert(input != NULL);
  assert(output != NULL);

  /* Avoid segfaults */
  if (input == NULL || output == NULL || tmp == NULL || crpix == NULL) {
    return 1;
  }

  /* If we have one, we must have both... */
  if ((a == NULL) ^ (b == NULL)) {
    return 6;
  }

  /* If no distortion, just return values */
  if (a == NULL /* && b == NULL ... implied */) {
    return 0;
  }

  input_ptr = input;
  output_ptr = output;
  for (i = 0; i < nelem; ++i) {
    x = *input_ptr++ - crpix[0];
    y = *input_ptr++ - crpix[1];

    for (j = 0; j <= (int)m; ++j) {
      tmp[j] = lu(m, a, (int)m-j, j);
      for (k = j-1; k >= 0; --k) {
        tmp[j] = (y * tmp[j]) + lu(m, a, (int)m-j, k);
      }
    }

    sum = tmp[0];
    for (j = (int)m; j > 0; --j) {
      sum = x * sum + tmp[(int)m - j + 1];
    }
    *output_ptr++ += sum;

    for (j = 0; j <= (int)n; ++j) {
      tmp[j] = lu(n, b, (int)n-j, j);
      for (k = j-1; k >= 0; --k) {
          tmp[j] = (y * tmp[j]) + lu(n, b, (int)n-j, k);
      }
    }

    sum = tmp[0];
    for (j = (int)n; j > 0; --j) {
      sum = x * sum + tmp[n - j + 1];
    }
    *output_ptr++ += sum;
  }

  return 0;
}

int
sip_pix2deltas(
    const sip_t* sip,
    const unsigned int naxes,
    const unsigned int nelem,
    const double* pix /* [NAXES][nelem] */,
    double* deltas /* [NAXES][nelem] */) {

  if (sip == NULL) {
    return 1;
  }

  return sip_compute(naxes, nelem,
                     sip->a_order, sip->a,
                     sip->b_order, sip->b,
                     sip->crpix,
                     (double *)sip->scratch,
                     pix, deltas);
}

int
sip_foc2deltas(
    const sip_t* sip,
    const unsigned int naxes,
    const unsigned int nelem,
    const double* foc /* [NAXES][nelem] */,
    double* deltas /* [NAXES][nelem] */) {

  if (sip == NULL) {
    return 1;
  }

  return sip_compute(naxes, nelem,
                     sip->ap_order, sip->ap,
                     sip->bp_order, sip->bp,
                     sip->crpix,
                     (double *)sip->scratch,
                     foc, deltas);
}

int
sip_pix2foc(
    const sip_t* sip,
    const unsigned int naxes,
    const unsigned int nelem,
    const double* pix /* [NAXES][nelem] */,
    double* foc /* [NAXES][nelem] */) {
  assert(pix);
  assert(foc);

  if (pix != foc) {
      memcpy(foc, pix, sizeof(double) * naxes * nelem);
  }

  return sip_pix2deltas(sip, naxes, nelem, pix, foc);
}

int
sip_foc2pix(
    const sip_t* sip,
    const unsigned int naxes,
    const unsigned int nelem,
    const double* foc /* [NAXES][nelem] */,
    double* pix /* [NAXES][nelem] */) {
  assert(pix);
  assert(foc);

  if (pix != foc) {
      memcpy(pix, foc, sizeof(double) * naxes * nelem);
  }

  return sip_foc2deltas(sip, naxes, nelem, foc, pix);
}
