#include "private.h"

struct SPARSE_MATRIX /* matrix in compressed-column or triplet form */
{
  scs_int nzmax; /* maximum number of entries */
  scs_int m;     /* number of rows */
  scs_int n;     /* number of columns */
  scs_int *p;    /* column pointers (size n+1) or col indices (size nzmax) */
  scs_int *i;    /* row indices, size nzmax */
  scs_float *x;  /* numerical values, size nzmax */
  scs_int nz;    /* # of entries in triplet matrix, -1 for compressed-col */
};

char *SCS(get_lin_sys_method)(const ScsMatrix *A, const ScsSettings *stgs) {
  char *tmp = (char *)scs_malloc(sizeof(char) * 128);
  sprintf(tmp, "sparse-direct, nnz in A = %li", (long)A->p[A->n]);
  return tmp;
}

char *SCS(get_lin_sys_summary)(ScsLinSysWork *p, const ScsInfo *info) {
  char *str = (char *)scs_malloc(sizeof(char) * 128);
  scs_int n = p->L->n;
  sprintf(str, "\tLin-sys: nnz in L factor: %li, avg solve time: %1.2es\n",
          (long)(p->L->p[n] + n), p->total_solve_time / (info->iter + 1) / 1e3);
  p->total_solve_time = 0;
  return str;
}

/* wrapper for free */
static void *cs_free(void *p) {
  if (p) {
    scs_free(p);
  }                  /* free p if it is not already SCS_NULL */
  return SCS_NULL; /* return SCS_NULL to simplify the use of cs_free */
}

static _cs *cs_spfree(_cs *A) {
  if (!A) {
    return SCS_NULL;
  } /* do nothing if A already SCS_NULL */
  cs_free(A->p);
  cs_free(A->i);
  cs_free(A->x);
  return (_cs *)cs_free(A); /* free the _cs struct and return SCS_NULL */
}

void SCS(free_lin_sys_work)(ScsLinSysWork *p) {
  if (p) {
    if (p->L) {
      cs_spfree(p->L);
    }
    if (p->P) {
      scs_free(p->P);
    }
    if (p->Dinv) {
      scs_free(p->Dinv);
    }
    if (p->bp) {
      scs_free(p->bp);
    }
    scs_free(p);
  }
}

static _cs *cs_spalloc(scs_int m, scs_int n, scs_int nzmax, scs_int values,
                    scs_int triplet) {
  _cs *A = (_cs *)scs_calloc(1, sizeof(_cs)); /* allocate the _cs struct */
  if (!A) {
    return SCS_NULL;
  }         /* out of memory */
  A->m = m; /* define dimensions and nzmax */
  A->n = n;
  A->nzmax = nzmax = MAX(nzmax, 1);
  A->nz = triplet ? 0 : -1; /* allocate triplet or comp.col */
  A->p = (scs_int *)scs_malloc((triplet ? nzmax : n + 1) * sizeof(scs_int));
  A->i = (scs_int *)scs_malloc(nzmax * sizeof(scs_int));
  A->x = values ? (scs_float *)scs_malloc(nzmax * sizeof(scs_float)) : SCS_NULL;
  return (!A->p || !A->i || (values && !A->x)) ? cs_spfree(A) : A;
}

static _cs *cs_done(_cs *C, void *w, void *x, scs_int ok) {
  cs_free(w); /* free workspace */
  cs_free(x);
  return ok ? C : cs_spfree(C); /* return result if OK, else free it */
}

/* C = compressed-column form of a triplet matrix T */
static _cs *cs_compress(const _cs *T) {
  scs_int m, n, nz, p, k, *Cp, *Ci, *w, *Ti, *Tj;
  scs_float *Cx, *Tx;
  _cs *C;
  m = T->m;
  n = T->n;
  Ti = T->i;
  Tj = T->p;
  Tx = T->x;
  nz = T->nz;
  C = cs_spalloc(m, n, nz, Tx != SCS_NULL, 0); /* allocate result */
  w = (scs_int *)scs_calloc(n, sizeof(scs_int));    /* get workspace */
  if (!C || !w) {
    return cs_done(C, w, SCS_NULL, 0);
  } /* out of memory */
  Cp = C->p;
  Ci = C->i;
  Cx = C->x;
  for (k = 0; k < nz; k++) w[Tj[k]]++; /* column counts */
  SCS(cumsum)(Cp, w, n);            /* column pointers */
  for (k = 0; k < nz; k++) {
    Ci[p = w[Tj[k]]++] = Ti[k]; /* A(i,j) is the pth entry in C */
    if (Cx) {
      Cx[p] = Tx[k];
    }
  }
  return cs_done(C, w, SCS_NULL, 1); /* success; free w and return C */
}

static _cs *form_kkt(const ScsMatrix *A, const ScsSettings *s) {
  /* ONLY UPPER TRIANGULAR PART IS STUFFED
   * forms column compressed KKT matrix
   * assumes column compressed form A matrix
   *
   * forms upper triangular part of [I A'; A -I]
   */
  scs_int j, k, kk;
  _cs *K_cs;
  /* I at top left */
  const scs_int Anz = A->p[A->n];
  const scs_int Knzmax = A->n + A->m + Anz;
  _cs *K = cs_spalloc(A->m + A->n, A->m + A->n, Knzmax, 1, 1);

#if EXTRA_VERBOSE > 0
  scs_printf("forming KKT\n");
#endif

  if (!K) {
    return SCS_NULL;
  }
  kk = 0;
  for (k = 0; k < A->n; k++) {
    K->i[kk] = k;
    K->p[kk] = k;
    K->x[kk] = s->rho_x;
    kk++;
  }
  /* A^T at top right : CCS: */
  for (j = 0; j < A->n; j++) {
    for (k = A->p[j]; k < A->p[j + 1]; k++) {
      K->p[kk] = A->i[k] + A->n;
      K->i[kk] = j;
      K->x[kk] = A->x[k];
      kk++;
    }
  }
  /* -I at bottom right */
  for (k = 0; k < A->m; k++) {
    K->i[kk] = k + A->n;
    K->p[kk] = k + A->n;
    K->x[kk] = -1;
    kk++;
  }
  /* assert kk == Knzmax */
  K->nz = Knzmax;
  K_cs = cs_compress(K);
  cs_spfree(K);
  return K_cs;
}

static scs_int _ldl_init(_cs *A, scs_int *P, scs_float **info) {
  *info = (scs_float *)scs_malloc(AMD_INFO * sizeof(scs_float));
  return amd_order(A->n, A->p, A->i, P, (scs_float *)SCS_NULL, *info);
}

static scs_int _ldl_factor(_cs *A, _cs **L, scs_float **Dinv) {
  scs_int factor_status, n = A->n;
  scs_int *etree= (scs_int *)scs_malloc(n * sizeof(scs_int));
  scs_int *Lnz = (scs_int *)scs_malloc(n * sizeof(scs_int));
  scs_int *iwork= (scs_int *)scs_malloc(3 * n * sizeof(scs_int));
  scs_float *D, *fwork;
  scs_int *bwork;
  (*L)->p = (scs_int *)scs_malloc((1 + n) * sizeof(scs_int));
  (*L)->nzmax = QDLDL_etree(n, A->p, A->i, iwork, Lnz, etree);
  if ((*L)->nzmax < 0) {
    scs_printf("Error in elimination tree calculation.\n");
    scs_free(Lnz);
    scs_free(iwork);
    scs_free(etree);
    scs_free((*L)->p);
    return (*L)->nzmax;
  }

  (*L)->x = (scs_float *)scs_malloc((*L)->nzmax * sizeof(scs_float));
  (*L)->i = (scs_int *)scs_malloc((*L)->nzmax * sizeof(scs_int));
  *Dinv = (scs_float *)scs_malloc(n * sizeof(scs_float));
  D = (scs_float *)scs_malloc(n * sizeof(scs_float));
  bwork = (scs_int *)scs_malloc(n * sizeof(scs_int));
  fwork = (scs_float *)scs_malloc(n * sizeof(scs_float));

#if EXTRA_VERBOSE > 0
  scs_printf("numeric factorization\n");
#endif
  factor_status = QDLDL_factor(n, A->p, A->i, A->x,
                                 (*L)->p, (*L)->i, (*L)->x,
                                 D, *Dinv, Lnz,
                                 etree, bwork, iwork, fwork);
#if EXTRA_VERBOSE > 0
  scs_printf("finished numeric factorization\n");
#endif

  scs_free(Lnz);
  scs_free(iwork);
  scs_free(etree);
  scs_free(D);
  scs_free(bwork);
  scs_free(fwork);
  return factor_status;
}

static void _ldl_perm(scs_int n, scs_float * x,	scs_float * b, scs_int * P) {
    scs_int j;
    for (j = 0 ; j < n ; j++) x[j] = b[P[j]];
}

static void _ldl_permt(scs_int n, scs_float * x,	scs_float * b, scs_int * P) {
    scs_int j;
    for (j = 0 ; j < n ; j++) x[P[j]] = b[j];
}

static void _ldl_solve(scs_float *x, scs_float *b, _cs *L, scs_float *Dinv,
                       scs_int *P, scs_float *bp) {
  /* solves PLDL'P' x = b for x */
  scs_int n = L->n;
  if (!P) {
    if (x != b) { /* if they're different addresses */
      memcpy(x, b, n * sizeof(scs_float));
    }
    QDLDL_solve(n, L->p, L->i, L->x, Dinv, x);
  } else {
    _ldl_perm(n, bp, b, P);
    QDLDL_solve(n, L->p, L->i, L->x, Dinv, bp);
    _ldl_permt(n, x, bp, P);
  }
}

void SCS(accum_by_atrans)(const ScsMatrix *A, ScsLinSysWork *p,
                          const scs_float *x, scs_float *y) {
  SCS(_accum_by_atrans)(A->n, A->x, A->i, A->p, x, y);
}

void SCS(accum_by_a)(const ScsMatrix *A, ScsLinSysWork *p, const scs_float *x,
                     scs_float *y) {
  SCS(_accum_by_a)(A->n, A->x, A->i, A->p, x, y);
}

static scs_int *cs_pinv(scs_int const *p, scs_int n) {
  scs_int k, *pinv;
  if (!p) {
    return SCS_NULL;
  } /* p = SCS_NULL denotes identity */
  pinv = (scs_int *)scs_malloc(n * sizeof(scs_int)); /* allocate result */
  if (!pinv) {
    return SCS_NULL;
  }                                       /* out of memory */
  for (k = 0; k < n; k++) pinv[p[k]] = k; /* invert the permutation */
  return pinv;                          /* return result */
}

static _cs *cs_symperm(const _cs *A, const scs_int *pinv, scs_int values) {
  scs_int i, j, p, q, i2, j2, n, *Ap, *Ai, *Cp, *Ci, *w;
  scs_float *Cx, *Ax;
  _cs *C;
  n = A->n;
  Ap = A->p;
  Ai = A->i;
  Ax = A->x;
  C = cs_spalloc(n, n, Ap[n], values && (Ax != SCS_NULL), 0); /* alloc result*/
  w = (scs_int *)scs_calloc(n, sizeof(scs_int)); /* get workspace */
  if (!C || !w) {
    return cs_done(C, w, SCS_NULL, 0);
  } /* out of memory */
  Cp = C->p;
  Ci = C->i;
  Cx = C->x;
  for (j = 0; j < n; j++) /* count entries in each column of C */
  {
    j2 = pinv ? pinv[j] : j; /* column j of A is column j2 of C */
    for (p = Ap[j]; p < Ap[j + 1]; p++) {
      i = Ai[p];
      if (i > j) {
        continue;
      }                        /* skip lower triangular part of A */
      i2 = pinv ? pinv[i] : i; /* row i of A is row i2 of C */
      w[MAX(i2, j2)]++;        /* column count of C */
    }
  }
  SCS(cumsum)(Cp, w, n); /* compute column pointers of C */
  for (j = 0; j < n; j++) {
    j2 = pinv ? pinv[j] : j; /* column j of A is column j2 of C */
    for (p = Ap[j]; p < Ap[j + 1]; p++) {
      i = Ai[p];
      if (i > j) {
        continue;
      }                        /* skip lower triangular part of A*/
      i2 = pinv ? pinv[i] : i; /* row i of A is row i2 of C */
      Ci[q = w[MAX(i2, j2)]++] = MIN(i2, j2);
      if (Cx) {
        Cx[q] = Ax[p];
      }
    }
  }
  return cs_done(C, w, SCS_NULL, 1); /* success; free workspace, return C */
}

static scs_int factorize(const ScsMatrix *A, const ScsSettings *stgs,
                         ScsLinSysWork *p) {
  scs_float *info;
  scs_int *Pinv, amd_status, ldl_status;
  _cs *C, *K = form_kkt(A, stgs);
  if (!K) {
    return -1;
  }
  amd_status = _ldl_init(K, p->P, &info);
  if (amd_status < 0) {
    return amd_status;
  }
#if EXTRA_VERBOSE > 0
  if (stgs->verbose) {
    scs_printf("Matrix factorization info:\n");
    amd_info(info);
  }
#endif
  Pinv = cs_pinv(p->P, A->n + A->m);
  C = cs_symperm(K, Pinv, 1);
  ldl_status = _ldl_factor(C, &p->L, &p->Dinv);
  cs_spfree(C);
  cs_spfree(K);
  scs_free(Pinv);
  scs_free(info);
  return ldl_status;
}

ScsLinSysWork *SCS(init_lin_sys_work)(const ScsMatrix *A,
                                      const ScsSettings *stgs) {
  ScsLinSysWork *p = (ScsLinSysWork *)scs_calloc(1, sizeof(ScsLinSysWork));
  scs_int n_plus_m = A->n + A->m;
  p->P = (scs_int *)scs_malloc(sizeof(scs_int) * n_plus_m);
  p->L = (_cs *)scs_malloc(sizeof(_cs));
  p->bp = (scs_float *)scs_malloc(n_plus_m * sizeof(scs_float));
  p->L->m = n_plus_m;
  p->L->n = n_plus_m;
  p->L->nz = -1;

  if (factorize(A, stgs, p) < 0) {
    SCS(free_lin_sys_work)(p);
    return SCS_NULL;
  }
  p->total_solve_time = 0.0;
  return p;
}

scs_int SCS(solve_lin_sys)(const ScsMatrix *A, const ScsSettings *stgs,
                           ScsLinSysWork *p, scs_float *b, const scs_float *s,
                           scs_int iter) {
  /* returns solution to linear system */
  /* Ax = b with solution stored in b */
  SCS(timer) linsys_timer;
  SCS(tic)(&linsys_timer);
  _ldl_solve(b, b, p->L, p->Dinv, p->P, p->bp);
  p->total_solve_time += SCS(tocq)(&linsys_timer);
#if EXTRA_VERBOSE > 0
  scs_printf("linsys solve time: %1.2es\n", SCS(tocq)(&linsys_timer) / 1e3);
#endif
  return 0;
}

void SCS(normalize_a)(ScsMatrix *A, const ScsSettings *stgs,
                       const ScsCone *k, ScsScaling *scal) {
  SCS(_normalize_a)(A, stgs, k, scal);
}

void SCS(un_normalize_a)(ScsMatrix *A, const ScsSettings *stgs,
                         const ScsScaling *scal) {
  SCS(_un_normalize_a)(A, stgs, scal);
}
