/*
 * Copyright © 2007-2011 Dynare Team
 *
 * This file is part of Dynare.
 *
 * Dynare is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Dynare is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Dynare.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "mex_interface.hh"
#include <cstring>
#include <sstream>

using namespace std;

map<string, mxArray *> mxglobal;

mxArray_tag::mxArray_tag()
{
  type = mxDOUBLE_CLASS;
  Ir = NULL;
  Jc = NULL;
  Nzmax = 0;
  field_name.clear();
  field_array.clear();
  size_1 = 0;
  size_2 = 0;
  data = NULL;
}

int
mexPrintf(const char *str, ...)
{
  va_list args;
  int retval;

  va_start(args, str);
  retval = vprintf(str, args);
  va_end(args);

  return retval;
}

void
mexErrMsgTxt(const string str)
{
  perror(str.c_str());
  exit(EXIT_FAILURE);
}

void
mexWarnMsgTxt(const string str)
{
  perror(str.c_str());
}

void
mxFree(void *to_release)
{
  free(to_release);
}

void *
mxMalloc(unsigned int amount)
{
  return malloc(amount);
}

void *
mxRealloc(void *to_extend, unsigned int amount)
{
  return realloc(to_extend, amount);
}

void *
mxCalloc(unsigned int nb_elements, unsigned int amount_per_element)
{
  return calloc(nb_elements, amount_per_element);
}

void
mexEvalString(const string str)
{
}

double *
mxGetPr(const mxArray *b_m)
{
  return b_m->data;
}

mxArray *
mxCreateDoubleMatrix(unsigned int rows, unsigned int cols, mxData_type mx_type)
{
  mxArray *Array = new mxArray;
  Array->type = mxDOUBLE_CLASS;
  Array->size_1 = rows;
  Array->size_2 = cols;
  Array->data = (double *) mxMalloc(rows*cols*sizeof(double));
  return (Array);
}

mxArray *
mxCreateCharArray(unsigned int rows, unsigned int cols, mxData_type mx_type)
{
  mxArray *Array = new mxArray;
  Array->type = mxCHAR_CLASS;
  Array->size_1 = rows;
  Array->size_2 = cols;
  Array->data = (double *) mxMalloc(2*rows*cols*sizeof(char));
  return (Array);
}

mxArray *
mxCreateSparse(unsigned int rows, unsigned int cols, unsigned int nz_max, mxData_type mx_type)
{
  mxArray *Array = new mxArray;
  Array->type = mxSPARSE_CLASS;
  Array->size_1 = rows;
  Array->size_2 = cols;
  Array->Nzmax = nz_max;
  Array->data = (double *) mxMalloc(nz_max*sizeof(double));
  Array->Ir = (mwIndex *) mxMalloc(nz_max*sizeof(mwIndex));
  Array->Jc = (mwIndex *) mxMalloc((cols+1)*sizeof(mwIndex));
  return (Array);
}

mxArray *
mxCreateDoubleScalar(double value)
{
  mxArray *Array = new mxArray;
  Array->type = mxSINGLE_CLASS;
  Array->size_1 = 1;
  Array->size_2 = 1;
  Array->data = (double *) mxMalloc(sizeof(double));
  Array->data[0] = value;
  return (Array);
}

mxArray *
mxCreatNULLMatrix()
{
  return NULL;
}

mxArray *
mxCreateStructMatrix(unsigned int rows, unsigned int cols, unsigned int nfields, const vector<string> &fieldnames)
{
  mxArray *Array = new mxArray;
  Array->type = mxSTRUCT_CLASS;
  Array->size_1 = rows;
  Array->size_2 = cols;
  for (unsigned int i = 0; i < nfields; i++)
    Array->field_name.push_back(fieldnames[i]);
  return Array;
}

void
mxDestroyArray(mxArray *A_m)
{
  mxFree(A_m->data);
  if (A_m->Ir)
    mxFree(A_m->Ir);
  if (A_m->Jc)
    mxFree(A_m->Jc);
  mxFree(A_m);
}

mxArray *
mxSetNzmax(mxArray *A_m, mwSize nz_max)
{
  A_m->Nzmax = nz_max;
  A_m->data = (double *) mxRealloc(A_m->data, nz_max*sizeof(double));
  A_m->Ir = (mwIndex *) mxRealloc(A_m->Ir, nz_max*sizeof(mwIndex));
  return (A_m);
}

void
mexCallMATLAB(unsigned int n_lhs, mxArray *matrix_lhs[], unsigned int n_rhs, mxArray *matrix_rhs[], const char *function)
{
  if (strncmp(function, "disp", 4) == 0)
    {
      int cols = mxGetN(matrix_rhs[0]);
      int rows = mxGetM(matrix_rhs[0]);
      if (matrix_rhs[0]->type == mxCHAR_CLASS)
        {
          char *p = (char *) matrix_rhs[0]->data;
          for (int i = 0; i < rows; i++)
            {
              for (int j = 0; j < cols; j++)
                mexPrintf("%c", p[2*(i+j*rows)]);
              mexPrintf("\n");
            }
        }
      else
        for (int i = 0; i < rows; i++)
          {
            for (int j = 0; j < cols; j++)
              mexPrintf("%8.4f ", matrix_rhs[0]->data[i+j*rows]);
            mexPrintf("\n");
          }
    }
}

mxArray *
mexGetVariable(const char *space_name, const char *matrix_name)
{
  if (strncmp(space_name, "global", 6) != 0)
    mexErrMsgTxt("space_name not handle in mexGetVariable\n");
  return mxglobal[matrix_name];
}

int
mxGetFieldNumber(const mxArray *Struct, const char *field_name)
{
  vector<string>::const_iterator it = find(Struct->field_name.begin(), Struct->field_name.end(), field_name);
  if (it == Struct->field_name.end())
    {
      stringstream tmp;
      tmp << "unknown field name '" << field_name << "' in mxGetFieldNumber\n";
      mexErrMsgTxt(tmp.str());
    }
  return it - Struct->field_name.begin();
}

mxArray *
mxGetFieldByNumber(mxArray *Struct, unsigned int pos, unsigned int field_number)
{
  if (pos > Struct->size_1 * Struct->size_2)
    mexErrMsgTxt("index out of range in mxGetFieldByNumber\n");
  if (field_number > Struct->field_name.size())
    mexErrMsgTxt("field_number out of range in mxGetFieldByNumber\n");
  return Struct->field_array[pos][field_number];
}

int
mxAddField(mxArray *Struct, const char *fieldname)
{
  if (!mxIsStruct(Struct))
    return -1;
  else
    {
      Struct->field_name.push_back(fieldname);
      return Struct->field_name.size();
    }
}

void
mxSetFieldByNumber(mxArray *Struct, mwIndex index, unsigned int field_number, mxArray *pvalue)
{
  if (index >= Struct->size_1 * Struct->size_2)
    mexErrMsgTxt("index out of range in mxSetFieldByNumber\n");
  unsigned int nfields = Struct->field_name.size();
  if (field_number >= nfields)
    mexErrMsgTxt("field_number out of range in mxSetFieldByNumber\n");
  while (Struct->field_array.size() <= index)
    Struct->field_array.push_back(vector<mxArray *>(nfields, NULL));
  Struct->field_array[index][field_number] = pvalue;
}

int
mxGetString(const mxArray *array, char *buf, unsigned int buflen)
{
  unsigned int size;
  if (!mxIsChar(array))
    return -1;
  else if (buflen <= (size = mxGetNumberOfElements(array)))
    return -1;
  else
    {
      char *pchar = (char *) array->data;
      for (unsigned int i = 0; i < size; i++)
        buf[i] = pchar[2*i];
      buf[size+1] = ' ';
    }
  return 0;
}

mxArray *
mxDuplicateArray(const mxArray *array)
{
  unsigned int Size;
  unsigned int i;
  vector<mxArray *>::const_iterator it_v_array;
  vector<string>::const_iterator it_v_string;
  mxArray *Array = (mxArray *) mxMalloc(sizeof(mxArray));
  Array->type = array->type;
  Array->size_1 = array->size_1;
  Array->size_2 = Array->size_2;
  switch (array->type)
    {
    case mxDOUBLE_CLASS:
    case mxSINGLE_CLASS:
      Size = array->size_1*array->size_2*sizeof(double);
      Array->data = (double *) mxMalloc(Size);
      memcpy(Array->data, array->data, Size);
      break;
    case mxSTRUCT_CLASS:
      for (i = 0; i < array->size_1 * array->size_2; i++)
        for (it_v_array = array->field_array[i].begin(); it_v_array != array->field_array[i].end(); it_v_array++)
          Array->field_array[i].push_back(mxDuplicateArray(*it_v_array));
      for (it_v_string = array->field_name.begin(); it_v_string != array->field_name.end(); it_v_string++)
        Array->field_name.push_back(*it_v_string);
      break;
    case mxSPARSE_CLASS:
      Array->Nzmax = array->Nzmax;
      Size = array->Nzmax * sizeof(double);
      Array->data = (double *) mxMalloc(Size);
      memcpy(Array->data, array->data, Size);
      Size = array->Nzmax * sizeof(mwIndex);
      Array->Ir = (mwIndex *) mxMalloc(Size);
      memcpy(Array->Ir, array->Ir, Size);
      Size = array->size_2*sizeof(mwIndex);
      Array->Jc = (mwIndex *) mxMalloc(Size);
      memcpy(Array->Jc, array->Jc, Size);
      break;
    case mxCHAR_CLASS:
      Size = 2*array->size_1*array->size_2*sizeof(char);
      Array->data = (double *) mxMalloc(Size);
      memcpy(Array->data, array->data, Size);
      break;
    default:
      ostringstream tmp;
      tmp << "Array type not handle: " << array->type << "\n";
      mexErrMsgTxt(tmp.str());
    }
  return (Array);
}

mxArray *
read_struct(FILE *fid)
{
  unsigned int nfields = 0;
  fscanf(fid, "%d", &nfields);
  unsigned int size_1, size_2;
  fscanf(fid, "%d", &size_1);
  fscanf(fid, "%d", &size_2);
  vector<string> fieldnames;
  vector<mxArray *> v_Array;
  vector<vector<mxArray * >> vv_Array;
  for (unsigned int j = 0; j < size_1 * size_2; j++)
    {
      for (unsigned int i = 0; i < nfields; i++)
        {
          char name[512];
          fscanf(fid, "%s", name);
          if (j == 0)
            fieldnames.push_back(name);
          v_Array.push_back(read_Array(fid));
        }
      vv_Array.push_back(v_Array);
      v_Array.clear();
    }
  mxArray *Struct;
  Struct = mxCreateStructMatrix(size_1, size_2, nfields, fieldnames);
  for (unsigned int j = 0; j < size_1 * size_2; j++)
    {
      v_Array = vv_Array[j];
      for (unsigned int i = 0; i < nfields; i++)
        {
          mxSetFieldByNumber(Struct, j, i, v_Array[i]);
        }
    }
  return Struct;
}

mxArray *
read_double_array(FILE *fid)
{
  int size_1, size_2;
  fscanf(fid, "%d", &size_1);
  fscanf(fid, "%d", &size_2);
  mxArray *A = mxCreateDoubleMatrix(size_1, size_2, mxREAL);
  double *data = mxGetPr(A);
  float f;
  for (int i = 0; i < size_1*size_2; i++)
    {
      fscanf(fid, "%f", &f);
      data[i] = f;
    }
  return A;
}

mxArray *
read_char_array(FILE *fid)
{
  unsigned int size_1, size_2;
  fscanf(fid, "%d", &size_1);
  fscanf(fid, "%d", &size_2);
  mxArray *A = mxCreateCharArray(size_1, size_2, mxREAL);
  char *data = (char *) mxGetPr(A);
  char pchar[size_2+1];
  for (unsigned int i = 0; i < size_1; i++)
    {
      fscanf(fid, "%s", pchar);
      for (unsigned int j = 0; j < strlen(pchar); j++)
        data[2*(i+j*size_1)] = pchar[j];
      for (unsigned int j = strlen(pchar); j < size_2; j++)
        data[2*(i+j*size_1)] = ' ';
    }
  return A;
}

mxArray *
read_Array(FILE *fid)
{
  mxArray *Array;
  mxArray_type array_type;
  unsigned int j;
  fscanf(fid, "%d", &j);
  array_type = static_cast<mxArray_type>(j);
  switch (array_type)
    {
    case mxDOUBLE_CLASS:
    case mxSINGLE_CLASS:
      Array = read_double_array(fid);
      break;
    case mxSTRUCT_CLASS:
      Array = read_struct(fid);
      break;
    case mxCHAR_CLASS:
      Array = read_char_array(fid);
      break;
    default:
      ostringstream tmp;
      tmp << "Array type not handle in read_Array: " << array_type << "\n";
      mexErrMsgTxt(tmp.str());
    }
  return (Array);
}

void
load_global(char *file_name)
{
  FILE *fid;
  ostringstream tmp_out("");
  tmp_out << file_name << "_options.txt";
  fid = fopen(tmp_out.str().c_str(), "r");
  if (fid == NULL)
    {
      string s = "Can't open file ";
      s += tmp_out.str().c_str();
      mexErrMsgTxt(s);
    }
  mxglobal["options_"] = read_struct(fid);
  fclose(fid);

  tmp_out.str("");
  tmp_out << file_name << "_M.txt";
  fid = fopen(tmp_out.str().c_str(), "r");
  if (fid == NULL)
    {
      string s = "Can't open file ";
      s += tmp_out.str().c_str();
      mexErrMsgTxt(s);
    }
  mxglobal["M_"] = read_struct(fid);
  fclose(fid);

  tmp_out.str("");
  tmp_out << file_name << "_oo.txt";
  fid = fopen(tmp_out.str().c_str(), "r");
  if (fid == NULL)
    {
      string s = "Can't open file ";
      s += tmp_out.str().c_str();
      mexErrMsgTxt(s);
    }
  mxglobal["oo_"] = read_struct(fid);
  fclose(fid);
}

void
Free_simple_array(mxArray *array)
{
  mxFree(array->data);
  delete array;
}

void
Free_struct_array(mxArray *array)
{
  for (unsigned int i = 0; i < array->size_1 * array->size_2; i++)
    for (unsigned int j = 0; j < array->field_name.size(); j++)
      Free_Array(array->field_array[i][j]);
  delete array;
}

void
Free_Array(mxArray *array)
{
  switch (array->type)
    {
    case mxDOUBLE_CLASS:
    case mxSINGLE_CLASS:
    case mxCHAR_CLASS:
      Free_simple_array(array);
      break;
    case mxSTRUCT_CLASS:
      Free_struct_array(array);
      break;
    default:
      mexErrMsgTxt("Array type not handle in read_Array\n");
    }
}

void
Free_global()
{
  for (map<string, mxArray *>::iterator it = mxglobal.begin(); it != mxglobal.end(); it++)
    Free_Array(it->second);
}
