/*  Copyright (C) 2010 Joshua Judson Rosen <rozzin@geekspace.com>.

    This program is free software: you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public License
    as published by the Free Software Foundation, either version 3
    of the License, or (at your option) any later version..

    This program 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; see the file COPYING. If not, see
    <http://www.gnu.org/licenses/> or write to:

        The Free Software Foundation, inc.
        51 Franklin Street, Fifth Floor
        Boston, MA 02110-1301
        USA
*/

#include <glib.h>
#include <glib/grand.h>
#include <glib-object.h>

#include "generator.h"



/* ^There's no `ref and unref' for GRand, so any GObject that has one
   needs to own it, which may be problematic: there's no way for
   multiple generators to share a single pool of entropy, unless the
   GRand is specified as a parameter to every generator-function
   instead of being a proper property of the generator, or unless each
   generator can hold (weak) references to other generators and update
   the others' PRNG-pointers when it finalizes and destroys the shared
   PRNG.

   The easiest way out of this is to just define a GObject type to
   wrap around our GRands, give ownership of the GRand to *that*, and
   have VirandGenerator objects hold references to the wrapper:
*/

typedef struct
{
  GObject parent_instance;

  GRand *PRNG;
} VirandState;

typedef struct
{
  GObjectClass parent_class;
} VirandStateClass;

enum {
  STATE_PROP_PRNG = 1
};

G_DEFINE_TYPE (VirandState, virand_state, G_TYPE_OBJECT)

#define VIRAND_TYPE_STATE (virand_state_get_type ())
#define VIRAND_STATE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), VIRAND_TYPE_STATE, VirandState))

static void
virand_state_set_property (GObject *object,
                           guint property_id,
                           const GValue *value,
                           GParamSpec *pspec)
{
  VirandState *state = VIRAND_STATE (object);

  switch (property_id)
    {
    case STATE_PROP_PRNG:
      {
        GRand *new_prng = g_value_get_pointer (value);

        if (state->PRNG && state->PRNG != new_prng) {
          g_rand_free (state->PRNG);
        }

        state->PRNG = new_prng;
      }
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
virand_state_get_property (GObject *object,
                           guint property_id,
                           GValue *value,
                           GParamSpec *pspec)
{
  VirandState *state = VIRAND_STATE (object);

  switch (property_id)
    {
    case STATE_PROP_PRNG:
      g_value_set_pointer (value, state->PRNG);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
virand_state_constructed (GObject *self)
{
  VirandState *state = VIRAND_STATE (self);

  if (state->PRNG == NULL) {
    state->PRNG = g_rand_new ();
  }
}

static void
virand_state_finalize (GObject *self)
{
  VirandState *state = VIRAND_STATE (self);

  g_rand_free (state->PRNG);

  G_OBJECT_CLASS (virand_state_parent_class)->finalize (self);
}

void virand_state_init (VirandState *state)
{
}

void
virand_state_class_init (VirandStateClass *class)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (class);

  gobject_class->set_property = virand_state_set_property;
  gobject_class->get_property = virand_state_get_property;
  gobject_class->constructed = virand_state_constructed;
  gobject_class->finalize = virand_state_finalize;

  g_object_class_install_property
    (gobject_class, STATE_PROP_PRNG,
     g_param_spec_pointer ("prng",
                           "Pseudo-Random Number Generator",
                           "A GRand pointer",
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}



struct _VirandGenerator
{
  GObject parent_instance;

  VirandState *state;

  GHashTable *map;
  GSequence *sequence;
  /* ^GHashTable is iterable via g_hash_table_iter(), but there's no
     guarantees of traversal always happening in the same order.

     Using a GTree instead of a GHashTable would guarantee consistent
     traversal-order within a given run of the program, but might
     *not* be consistent across runs of the program if the item-values
     are dynamically-generated; the order would also not necessarily
     be predictable by the programmer.

     In the case of dynamically-generated item-values (as for GTypes),
     it only makes sense to traverse in insertion-order.

     We could also do this with just a pair of linked lists, but
     item-lookup would be more expensive; realistically, though, we're
     almost never going to do lookups anway--only when we want to
     change the weight of an item that's already been added to the
     generator, or insert a new item.
  */

  int total; /* Combined weight of all items. */
  int default_weight;
};

struct _VirandGeneratorClass
{
  GObjectClass parent_class;
};

G_DEFINE_TYPE (VirandGenerator, virand_generator, G_TYPE_OBJECT)



VirandGenerator *
virand_generator_new (gpointer item0, ...)
{
  VirandGenerator *generator = g_object_new (VIRAND_TYPE_GENERATOR,
                                             NULL);
  gpointer item;
  va_list ap;
  va_start (ap, item0);
  for (item = item0; item != NULL; item = va_arg (ap, gpointer)) {
    virand_generator_set_weight (generator, item, va_arg (ap, int));
  }
  va_end (ap);

  return generator;
}

GRand *
virand_generator_state (VirandGenerator *generator)
{
  if (generator->state) {
    return generator->state->PRNG;
  } else {
    return NULL;
  }
}

void
virand_generator_share_state (VirandGenerator *self, VirandGenerator *other)
{
  if (other != self && self->state != other->state) {
    if (self->state) {
      g_object_unref (self->state);
    }

    self->state = g_object_ref (other->state);
  }
}

gpointer
virand_generate_discrete (VirandGenerator *generator)
{
  int selector = g_rand_int_range (virand_generator_state (generator),
                                   0, generator->total);
  int accumulator = 0;
  gpointer output = NULL;

  GSequenceIter *iter;
  for (iter = g_sequence_get_begin_iter (generator->sequence);
       ! g_sequence_iter_is_end (iter);
       iter = g_sequence_iter_next (iter))
  {
    gpointer seq_item = g_sequence_get (iter);
    int weight =
      GPOINTER_TO_INT (g_hash_table_lookup (generator->map, seq_item));

    if (weight < 0) {
      accumulator += generator->default_weight;
    } else {
      accumulator += weight;
    }

    if (selector < accumulator) {
      output = seq_item;
      break;
    }
  }

  return output;
}

void
virand_generator_set_weight (VirandGenerator *generator,
                             gpointer item, int weight)
{
  gpointer mapval;

  if (g_hash_table_lookup_extended (generator->map, item,
                                    NULL, &mapval)) {
    int w_eld = GPOINTER_TO_INT (mapval);

    if (w_eld < 0) {
      w_eld = generator->default_weight;
    }

    generator->total -= w_eld;
  } else {
    g_sequence_append (generator->sequence, item);
  }

  g_hash_table_insert (generator->map, item, GINT_TO_POINTER (weight));

  if (weight < 0) {
    weight = generator->default_weight;
  }

  generator->total += weight;
}

void
virand_generator_set_weights (VirandGenerator *generator,
                              ...)
{
  gpointer item;

  va_list ap;
  va_start (ap, generator);

  while ((item = va_arg (ap, void *))) {
    virand_generator_set_weight (generator, item, va_arg (ap, int));
  }

  va_end (ap);
}

enum {
  PROP_STATE = 1,
  PROP_DEFWEIGHT
};

static void
set_property (GObject *object,
              guint property_id,
              const GValue *value,
              GParamSpec *pspec)
{
  VirandGenerator *generator = VIRAND_GENERATOR (object);

  switch (property_id)
    {
    case PROP_STATE:
      {
        GRand *new_prng = g_value_get_pointer (value);

        if (generator->state && generator->state->PRNG != new_prng) {
          g_object_unref (generator->state);
          generator->state = NULL;
        }

        if (generator->state == NULL && new_prng != NULL) {
          generator->state = g_object_new (VIRAND_TYPE_STATE,
                                           "prng", new_prng,
                                           NULL);
        }
      }
      break;

    case PROP_DEFWEIGHT:
      {
        int w_eld = generator->default_weight;
        int w_new = g_value_get_int (value);

        GHashTableIter iter;
        gpointer key, value;

        g_hash_table_iter_init (&iter, generator->map);

        while (g_hash_table_iter_next (&iter, &key, &value)) {
          if (GPOINTER_TO_INT (value) < 0) {
            generator->total -= w_eld;
            generator->total += w_new;
          }
        }

        generator->default_weight = w_new;
      }
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
get_property (GObject *object,
              guint property_id,
              GValue *value,
              GParamSpec *pspec)
{
  VirandGenerator *generator = VIRAND_GENERATOR (object);

  switch (property_id)
    {
    case PROP_STATE:
      g_value_set_pointer (value, generator->state->PRNG);
      break;

    case PROP_DEFWEIGHT:
      g_value_set_int (value, generator->default_weight);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

void
virand_generator_init (VirandGenerator *generator)
{
  generator->default_weight = 1;

  generator->map = g_hash_table_new (g_direct_hash, g_direct_equal);
  generator->sequence = g_sequence_new (NULL);
}

void
virand_generator_constructed (GObject *self)
{
  VirandGenerator *generator = VIRAND_GENERATOR (self);
  GObjectClass *parent_class = G_OBJECT_CLASS (virand_generator_parent_class);

  if (parent_class->constructed) {
    parent_class->constructed (self);
  }

  if (generator->state == NULL) {
    generator->state = g_object_new (VIRAND_TYPE_STATE,
                                     "prng", g_rand_new (),
                                     NULL);
  }
}

void
virand_generator_dispose (GObject *self)
{
  VirandGenerator *generator = VIRAND_GENERATOR (self);

  if (generator->state) {
    g_object_unref (generator->state);
    generator->state = NULL;
  }

  G_OBJECT_CLASS (virand_generator_parent_class)->dispose (self);
}

void
virand_generator_class_init (VirandGeneratorClass *class)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (class);

  gobject_class->set_property = set_property;
  gobject_class->get_property = get_property;
  gobject_class->dispose = virand_generator_dispose;
  gobject_class->constructed = virand_generator_constructed;

  g_object_class_install_property
    (gobject_class, PROP_STATE,
     g_param_spec_pointer ("state",
                           "Pseudo-Random Number Generator state",
                           "A GRand pointer",
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property
    (gobject_class, PROP_DEFWEIGHT,
     g_param_spec_int ("default-item-weight",
                       "Default item-weight",
                       "The default item-weight, used for items with `unspecified' (negative) weights.",
                       -1, G_MAXINT, 1,
                       G_PARAM_READWRITE));
}
