/*
  function.c
  Ruby/GSL: Ruby extension library for GSL (GNU Scientific Library)
    (C) Copyright 2001-2006 by Yoshiki Tsunesada

  Ruby/GSL is free software: you can redistribute it and/or modify it
  under the terms of the GNU General Public License.
  This library is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY.
*/

#include "include/rb_gsl_function.h"

VALUE cgsl_function;
VALUE cgsl_function_fdf;

void gsl_function_free(gsl_function *f);
double rb_gsl_function_f(double x, void *p);
ID RBGSL_ID_call, RBGSL_ID_arity;

static VALUE rb_gsl_function_set_f(int argc, VALUE *argv, VALUE obj)
{
  gsl_function *F = NULL;
  VALUE ary, ary2;
  size_t i;
  Data_Get_Struct(obj, gsl_function, F);
  if (F->params == NULL) {
    ary = rb_ary_new2(2);
    /*    (VALUE) F->params = ary;*/
    F->params = (void *) ary;
  } else {
    ary = (VALUE) F->params;
  }
  rb_ary_store(ary, 1, Qnil);

  switch (argc) {
  case 0:
    break;
  case 1:
    CHECK_PROC(argv[0]);
    rb_ary_store(ary, 0, argv[0]);
    break;
  case 2:
    CHECK_PROC(argv[0]);
    rb_ary_store(ary, 0, argv[0]);
    rb_ary_store(ary, 1, argv[1]);
    break;
  default:
    CHECK_PROC(argv[0]);
    rb_ary_store(ary, 0, argv[0]);
    ary2 = rb_ary_new2(argc-1);
    for (i = 1; (int) i < argc; i++) rb_ary_store(ary2, i-1, argv[i]);
    rb_ary_store(ary, 1, ary2);
    break;
  }
  if (rb_block_given_p()) rb_ary_store(ary, 0, rb_block_proc());
  return obj;
}

void gsl_function_free(gsl_function *f)
{
  if (f) free((gsl_function *) f);
}

void gsl_function_mark(gsl_function *f)
{
  rb_gc_mark((VALUE) f->params);
}

/*
 * Create a Function object
 */
static VALUE rb_gsl_function_alloc(int argc, VALUE *argv, VALUE klass)
{
  gsl_function *f = NULL;
  VALUE obj;
  f = ALLOC(gsl_function);
  f->function = &rb_gsl_function_f;
  /*  (VALUE) f->params = rb_ary_new2(2);*/
  f->params = (void *) rb_ary_new2(2);
  rb_ary_store((VALUE) f->params, 1, Qnil);
  obj = Data_Wrap_Struct(klass, gsl_function_mark, gsl_function_free, f);
  rb_gsl_function_set_f(argc, argv, obj);
  return obj;
}

double rb_gsl_function_f(double x, void *p)
{
  VALUE result, ary, proc, params;
  ary = (VALUE) p;
  proc = rb_ary_entry(ary, 0);
  params = rb_ary_entry(ary, 1);
  if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, rb_float_new(x));
  else result = rb_funcall(proc, RBGSL_ID_call, 2, rb_float_new(x), params);
  return NUM2DBL(result);
}

/*
 * Calculates a function at x, and returns the rusult.
 */
static VALUE rb_gsl_function_eval(VALUE obj, VALUE x)
{
  gsl_function *F = NULL;
  VALUE ary, proc, params, result, arynew, x2;
  gsl_vector *v = NULL, *vnew = NULL;
  gsl_matrix *m = NULL, *mnew = NULL;
  size_t i, j, n;
  Data_Get_Struct(obj, gsl_function, F);
  ary = (VALUE) F->params;
  proc = rb_ary_entry(ary, 0);
  params = rb_ary_entry(ary, 1);
  if (CLASS_OF(x) == rb_cRange) x = rb_gsl_range2ary(x);
  switch (TYPE(x)) {
  case T_FIXNUM:
  case T_BIGNUM:
  case T_FLOAT:
    if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, x);
    else result = rb_funcall(proc, RBGSL_ID_call, 2, x, params);
    return result;
    break;
  case T_ARRAY:
    //    n = RARRAY(x)->len;
    n = RARRAY_LEN(x);
    arynew = rb_ary_new2(n);
    for (i = 0; i < n; i++) {
      x2 = rb_ary_entry(x, i);
      Need_Float(x2);
      if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, x2);
      else result = rb_funcall(proc, RBGSL_ID_call, 2, x2, params);
      rb_ary_store(arynew, i, result);
    }
    return arynew;
    break;
  default:
#ifdef HAVE_NARRAY_H
    if (NA_IsNArray(x)) {
      double *ptr1, *ptr2;
      struct NARRAY *na;
      GetNArray(x, na);
      ptr1 = (double *) na->ptr;
      n = na->total;
      ary = na_make_object(NA_DFLOAT, na->rank, na->shape, CLASS_OF(x));
      ptr2 = NA_PTR_TYPE(ary, double*);
      for (i = 0; i < n; i++) {
        x2 = rb_float_new(ptr1[i]);
        if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, x2);
        else result = rb_funcall(proc, RBGSL_ID_call, 2, x2, params);
        ptr2[i] = NUM2DBL(result);
      }
      return ary;
    }
#endif
    if (VECTOR_P(x)) {
      Data_Get_Struct(x, gsl_vector, v);
      vnew = gsl_vector_alloc(v->size);
      for (i = 0; i < v->size; i++) {
        x2 = rb_float_new(gsl_vector_get(v, i));
        if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, x2);
        else result = rb_funcall(proc, RBGSL_ID_call, 2, x2, params);
        gsl_vector_set(vnew, i, NUM2DBL(result));
      }
      return Data_Wrap_Struct(cgsl_vector, 0, gsl_vector_free, vnew);
    } else if (MATRIX_P(x)) {
      Data_Get_Struct(x, gsl_matrix, m);
      mnew = gsl_matrix_alloc(m->size1, m->size2);
      for (i = 0; i < m->size1; i++) {
        for (j = 0; j < m->size2; j++) {
          x2 = rb_float_new(gsl_matrix_get(m, i, j));
          if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, x2);
          else result = rb_funcall(proc, RBGSL_ID_call, 2, x2, params);
          gsl_matrix_set(mnew, i, j, NUM2DBL(result));
        }
      }
      return Data_Wrap_Struct(cgsl_matrix, 0, gsl_matrix_free, mnew);
    } else {
      rb_raise(rb_eTypeError, "wrong argument type");
    }
    break;
  }
  /* never reach here */
  return Qnil;
}

static VALUE rb_gsl_function_arity(VALUE obj)
{
  gsl_function *F = NULL;
  VALUE proc;
  Data_Get_Struct(obj, gsl_function, F);
  proc = rb_ary_entry((VALUE) F->params, 0);
  return INT2FIX(rb_funcall(proc, RBGSL_ID_arity, 0));
}

static VALUE rb_gsl_function_proc(VALUE obj)
{
  gsl_function *F = NULL;
  Data_Get_Struct(obj, gsl_function, F);
  return rb_ary_entry((VALUE) F->params, 0);
}

static VALUE rb_gsl_function_params(VALUE obj)
{
  gsl_function *F = NULL;
  Data_Get_Struct(obj, gsl_function, F);
  return rb_ary_entry((VALUE) F->params, 1);
}

static VALUE rb_gsl_function_set_params(int argc, VALUE *argv, VALUE obj)
{
  gsl_function *F = NULL;
  VALUE ary, ary2;
  size_t i;
  if (argc == 0) return obj;
  Data_Get_Struct(obj, gsl_function, F);
  ary = (VALUE) F->params;
  if (argc == 1) {
    rb_ary_store(ary, 1, argv[0]);
  } else {
    ary2 = rb_ary_new2(argc);
    for (i = 0; (int) i < argc; i++) rb_ary_store(ary2, i, argv[i]);
    rb_ary_store(ary, 1, ary2);
  }
  return obj;
}

static VALUE rb_gsl_function_graph(int argc, VALUE *argv, VALUE obj)
{
#ifdef HAVE_GNU_GRAPH
  gsl_function *F = NULL;
  gsl_vector *v = NULL;
  double x, y;
  char opt[256] = "", command[1024];
  size_t i, n;
  int flag = 0;
  FILE *fp = NULL;
  VALUE ary, params, proc;
  switch (argc) {
  case 2:
    Check_Type(argv[1], T_STRING);
    strcpy(opt, STR2CSTR(argv[1]));
  /* no break, do next */
  case 1:
    if (CLASS_OF(argv[0]) == rb_cRange) argv[0] = rb_gsl_range2ary(argv[0]);
    if (TYPE(argv[0]) == T_ARRAY) {
      //      n = RARRAY(argv[0])->len;
      n = RARRAY_LEN(argv[0]);
      v = gsl_vector_alloc(n);
      flag = 1;
      for (i = 0; i < n; i++)
        gsl_vector_set(v, i, NUM2DBL(rb_ary_entry(argv[0], i)));
    } else if (rb_obj_is_kind_of(argv[0], cgsl_vector)) {
      Data_Get_Struct(argv[0], gsl_vector, v);
      n = v->size;
      flag = 0;
    } else {
      rb_raise(rb_eTypeError,
               "wrong argument type %s (Array or GSL::Vector expected)",
               rb_class2name(CLASS_OF(argv[0])));
    }
    break;
  default:
    rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
    break;
  }
  Data_Get_Struct(obj, gsl_function, F);
  ary = (VALUE) F->params;
  proc = rb_ary_entry(ary, 0);
  params = rb_ary_entry(ary, 1);
  sprintf(command, "graph -T X -g 3 %s", opt);
  fp = popen(command, "w");
  if (fp == NULL)
    rb_raise(rb_eIOError, "GNU graph not found.");
  for (i = 0; i < n; i++) {
    x = gsl_vector_get(v, i);
    if (NIL_P(params)) y = NUM2DBL(rb_funcall(proc, RBGSL_ID_call, 1, rb_float_new(x)));
    else y = NUM2DBL(rb_funcall(proc, RBGSL_ID_call, 2, rb_float_new(x), params));
    fprintf(fp, "%e %e\n", x, y);
  }
  fflush(fp);
  pclose(fp);
  fp = NULL;
  if (flag == 1) gsl_vector_free(v);
  return Qtrue;
#else
  rb_raise(rb_eNoMethodError, "not implemented");
  return Qfalse;
#endif
}


static double rb_gsl_function_fdf_f(double x, void *p);
static void gsl_function_fdf_free(gsl_function_fdf *f);

static double rb_gsl_function_fdf_f(double x, void *p);
static double rb_gsl_function_fdf_df(double x, void *p);
static void rb_gsl_function_fdf_fdf(double x, void *p, double *f, double *df);

static void setfunc(int i, VALUE *argv, gsl_function_fdf *F);
static void setfunc(int i, VALUE *argv, gsl_function_fdf *F)
{
  VALUE ary;
  if (F->params == NULL) {
    ary = rb_ary_new2(4);
    /*    (VALUE) F->params = ary;*/
    F->params = (void *) ary;
  } else {
    ary = (VALUE) F->params;
  }
  if (rb_obj_is_kind_of(argv[i], rb_cProc)) {
    rb_ary_store(ary, i, argv[i]);
  } else if (TYPE(argv[i]) == T_ARRAY || rb_obj_is_kind_of(argv[i], cgsl_vector)
             || TYPE(argv[i]) == T_FIXNUM || TYPE(argv[i]) == T_FLOAT) {
    rb_ary_store(ary, 3, argv[i]);
  } else {
    rb_raise(rb_eArgError,
             "wrong type argument (Proc, Array, GSL::Vector or a number)");
  }
}

static void gsl_function_fdf_mark(gsl_function_fdf *f);
static VALUE rb_gsl_function_fdf_new(int argc, VALUE *argv, VALUE klass)
{
  gsl_function_fdf *F = NULL;
  VALUE ary;
  size_t i;
  F = ALLOC(gsl_function_fdf);
  F->f = &rb_gsl_function_fdf_f;
  F->df = &rb_gsl_function_fdf_df;
  F->fdf = &rb_gsl_function_fdf_fdf;
  ary = rb_ary_new2(4);
  /*  (VALUE) F->params = ary;*/
  F->params = (void *) ary;
  rb_ary_store(ary, 2, Qnil);
  rb_ary_store(ary, 3, Qnil);
  for (i = 0; (int) i < argc; i++) setfunc(i, argv, F);
  return Data_Wrap_Struct(klass, gsl_function_fdf_mark, gsl_function_fdf_free, F);
}

static void gsl_function_fdf_free(gsl_function_fdf *f)
{
  free((gsl_function_fdf *) f);
}

static void gsl_function_fdf_mark(gsl_function_fdf *f)
{
  rb_gc_mark((VALUE) f->params);
}

static VALUE rb_gsl_function_fdf_set(int argc, VALUE *argv, VALUE obj)
{
  gsl_function_fdf *F = NULL;
  VALUE ary;
  size_t i;
  Data_Get_Struct(obj, gsl_function_fdf, F);
  ary = (VALUE) F->params;
  rb_ary_store(ary, 2, Qnil);
  rb_ary_store(ary, 3, Qnil);
  for (i = 0; (int) i < argc; i++) setfunc(i, argv, F);
  return obj;
}

static VALUE rb_gsl_function_fdf_set_f(VALUE obj, VALUE procf)
{
  gsl_function_fdf *F = NULL;
  VALUE ary;
  CHECK_PROC(procf);
  Data_Get_Struct(obj, gsl_function_fdf, F);
  if (F->params == NULL) {
    ary = rb_ary_new2(4);
    /*    (VALUE) F->params = ary;*/
    F->params = (void *) ary;
  } else {
    ary = (VALUE) F->params;
  }
  rb_ary_store(ary, 0, procf);
  return obj;
}

static VALUE rb_gsl_function_fdf_set_df(VALUE obj, VALUE procdf)
{
  gsl_function_fdf *F = NULL;
  VALUE ary;
  CHECK_PROC(procdf);
  Data_Get_Struct(obj, gsl_function_fdf, F);
  if (F->params == NULL) {
    ary = rb_ary_new2(4);
    /*    (VALUE) F->params = ary;*/
    F->params = (void *) ary;
  } else {
    ary = (VALUE) F->params;
  }
  rb_ary_store(ary, 1, procdf);
  return obj;
}

static VALUE rb_gsl_function_fdf_set_fdf(VALUE obj, VALUE procfdf)
{
  gsl_function_fdf *F = NULL;
  VALUE ary;
  CHECK_PROC(procfdf);
  Data_Get_Struct(obj, gsl_function_fdf, F);
  if (F->params == NULL) {
    ary = rb_ary_new2(4);
    /*    (VALUE) F->params = ary;*/
    F->params = (void *) ary;
  } else {
    ary = (VALUE) F->params;
  }
  rb_ary_store(ary, 2, procfdf);
  return obj;
}

static VALUE rb_gsl_function_fdf_set_params(int argc, VALUE *argv, VALUE obj)
{
  gsl_function_fdf *F = NULL;
  VALUE ary, ary2;
  size_t i;
  Data_Get_Struct(obj, gsl_function_fdf, F);
  ary = (VALUE) F->params;
  if (argc == 0) return obj;
  if (argc == 1) {
    rb_ary_store(ary, 3, argv[0]);
  } else {
    ary2 = rb_ary_new2(argc);
    for (i = 0; (int) i < argc; i++) rb_ary_store(ary2, i, argv[i]);
    rb_ary_store(ary, 3, ary2);
  }
  return obj;
}

static double rb_gsl_function_fdf_f(double x, void *p)
{
  VALUE result, params, proc, ary;
  ary = (VALUE) p;
  proc = rb_ary_entry(ary, 0);
  params = rb_ary_entry(ary, 3);
  if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, rb_float_new(x));
  else result = rb_funcall(proc, RBGSL_ID_call, 2, rb_float_new(x), params);
  return NUM2DBL(result);
}

static double rb_gsl_function_fdf_df(double x, void *p)
{
  VALUE result, params, proc, ary;
  ary = (VALUE) p;
  proc = rb_ary_entry(ary, 1);
  params = rb_ary_entry(ary, 3);
  if (NIL_P(params)) result = rb_funcall(proc, RBGSL_ID_call, 1, rb_float_new(x));
  else result = rb_funcall(proc, RBGSL_ID_call, 2, rb_float_new(x), params);
  return NUM2DBL(result);
}

static void rb_gsl_function_fdf_fdf(double x, void *p, double *f, double *df)
{
  VALUE result, params, proc_f, proc_df, proc_fdf, ary;
  ary = (VALUE) p;
  proc_f = rb_ary_entry(ary, 0);
  proc_df = rb_ary_entry(ary, 1);
  proc_fdf = rb_ary_entry(ary, 2);
  params = rb_ary_entry(ary, 3);
  if (NIL_P(proc_fdf)) {
    if (NIL_P(params)) {
      result = rb_funcall(proc_f, RBGSL_ID_call, 1, rb_float_new(x));
      *f = NUM2DBL(result);
      result = rb_funcall(proc_df, RBGSL_ID_call, 1, rb_float_new(x));
      *df = NUM2DBL(result);
    } else {
      result = rb_funcall(proc_f, RBGSL_ID_call, 2, rb_float_new(x), params);
      *f = NUM2DBL(result);
      result = rb_funcall(proc_df, RBGSL_ID_call, 2, rb_float_new(x), params);
      *df = NUM2DBL(result);
    }
  } else {
    if (NIL_P(params)) result = rb_funcall(proc_fdf, RBGSL_ID_call, 1, rb_float_new(x));
    else result = rb_funcall(proc_fdf, RBGSL_ID_call, 2, rb_float_new(x), params);
    *f = NUM2DBL(rb_ary_entry(result, 0));
    *df = NUM2DBL(rb_ary_entry(result, 1));
  }
}

void Init_gsl_function(VALUE module)
{
  RBGSL_ID_call = rb_intern("call");
  RBGSL_ID_arity = rb_intern("arity");

  cgsl_function = rb_define_class_under(module, "Function", cGSL_Object);
  cgsl_function_fdf = rb_define_class_under(module, "Function_fdf", cGSL_Object);
  // This Fdf class seems superfluous.  Should probably be deleted?
  rb_define_class_under(cgsl_function_fdf, "Fdf", cgsl_function_fdf);

  /*  rb_define_singleton_method(cgsl_function, "new", rb_gsl_function_new, -1);*/
  rb_define_singleton_method(cgsl_function, "alloc", rb_gsl_function_alloc, -1);

  rb_define_method(cgsl_function, "eval", rb_gsl_function_eval, 1);
  rb_define_alias(cgsl_function, "call", "eval");
  rb_define_alias(cgsl_function, "[]", "eval");
  rb_define_alias(cgsl_function, "at", "eval");
  rb_define_method(cgsl_function, "arity", rb_gsl_function_arity, 0);
  rb_define_method(cgsl_function, "proc", rb_gsl_function_proc, 0);
  rb_define_alias(cgsl_function, "f", "proc");
  rb_define_method(cgsl_function, "params", rb_gsl_function_params, 0);
  rb_define_alias(cgsl_function, "param", "params");
  rb_define_method(cgsl_function, "set", rb_gsl_function_set_f, -1);
  rb_define_method(cgsl_function, "set_params", rb_gsl_function_set_params, -1);
  rb_define_alias(cgsl_function, "set_param", "set_params");
  rb_define_alias(cgsl_function, "params=", "set_params");
  rb_define_alias(cgsl_function, "param=", "set_params");

  rb_define_method(cgsl_function, "graph", rb_gsl_function_graph, -1);
  /*****/
  rb_define_singleton_method(cgsl_function_fdf, "new", rb_gsl_function_fdf_new, -1);
  rb_define_singleton_method(cgsl_function_fdf, "alloc", rb_gsl_function_fdf_new, -1);
  rb_define_method(cgsl_function_fdf, "set", rb_gsl_function_fdf_set, -1);
  rb_define_method(cgsl_function_fdf, "set_f", rb_gsl_function_fdf_set_f, 1);
  rb_define_method(cgsl_function_fdf, "set_df", rb_gsl_function_fdf_set_df, 1);
  rb_define_method(cgsl_function_fdf, "set_fdf", rb_gsl_function_fdf_set_fdf, 1);
  rb_define_method(cgsl_function_fdf, "set_params", rb_gsl_function_fdf_set_params, -1);

}
