/*
Copyright (c) by respective owners including Yahoo!, Microsoft, and
individual contributors. All rights reserved.  Released under a BSD
license as described in the file LICENSE.
*/
#pragma once

#include <stdio.h>
#include <float.h>
#include <stdlib.h>
#include "v_array.h"

// TODO: special case the version where beam_size == 1
// TODO: *maybe* special case the version where beam_size <= 10

#define BEAM_CONSTANT_SIZE   0

namespace Beam
{

template <class T> struct beam_element
{ uint32_t hash;   // a cached hash value -- if a ~= b then h(a) must== h(b)
  float    cost;   // cost of this element
  T*       data;   // pointer to element data -- rarely accessed!
  bool     active; // is this currently active
  //  bool     recombined;                 // if we're not the BEST then we've been recombined
  //  v_array<T*> * recomb_friends;   // if we're the BEST (among ~= elements), then recomb_friends is everything that's equivalent to us but worse... NOT USED if we're not doing k-best predictions
};

inline int compare_on_cost(const void *void_a, const void *void_b)
{ if (void_a == void_b) return 0;
  const beam_element<void> *a = (const beam_element<void>*) void_a;
  const beam_element<void> *b = (const beam_element<void>*) void_b;
  if      ( a->active && !b->active) return -1;   // active things come before inactive things
  else if (!a->active &&  b->active) return  1;
  else if (!a->active && !b->active) return  0;
  else if (a->cost < b->cost) return -1;          // otherwise sort by cost
  else if (a->cost > b->cost) return  1;
  else return 0;
}

inline int compare_on_hash_then_cost(const void *void_a, const void *void_b)
{ if (void_a == void_b) return 0;
  const beam_element<void> *a = (const beam_element<void>*) void_a;
  const beam_element<void> *b = (const beam_element<void>*) void_b;
  if      ( a->active && !b->active) return -1;   // active things come before inactive things
  else if (!a->active &&  b->active) return  1;
  else if (!a->active && !b->active) return  0;
  else if (a->hash < b->hash) return -1;          // if the hashes are different, sort by hash
  else if (a->hash > b->hash) return  1;
  else if (a->cost < b->cost) return -1;          // otherwise sort by cost
  else if (a->cost > b->cost) return  1;
  else return 0;
}

template<class T> class beam
{
private:
  size_t beam_size;   // the beam size -- how many active elements can we have
  size_t count;       // how many elements do we have currently -- should be == to A.size()
  float  pruning_coefficient;  // prune anything with cost >= pruning_coefficient * best, set to FLT_MAX to not do coefficient-based pruning
  float  worst_cost;  // what is the cost of the worst (highest cost) item in the beam
  float  best_cost;   // what is the cost of the best (lowest cost) item in the beam
  float  prune_if_gt; // prune any element with cost greater than this
  T*     best_cost_data;  // easy access to best-cost item
  bool   do_kbest;
  v_array< beam_element<T> > A; // the actual data
  //  v_array<v_array<beam_element<T>*>> recomb_buckets;

  //  static size_t NUM_RECOMB_BUCKETS = 10231;

  bool (*is_equivalent)(T*,T*);  // test if two items are equivalent; nullptr means don't do hypothesis recombination

public:
  beam(size_t beam_size, float prune_coeff=FLT_MAX, bool (*test_equiv)(T*,T*)=nullptr, bool kbest=false)
    : beam_size(beam_size)
    , pruning_coefficient(prune_coeff)
    , do_kbest(kbest)
    , is_equivalent(test_equiv)
  { count = 0;
    worst_cost  = -FLT_MAX;
    best_cost   =  FLT_MAX;
    prune_if_gt =  FLT_MAX;
    best_cost_data = nullptr;
    A = v_init<beam_element<T>>();
    if (beam_size <= BEAM_CONSTANT_SIZE)
      A.resize(beam_size, true);
    else
      A.resize((beam_size+1) * 4, true);
    if (beam_size == 1) do_kbest = false;  // automatically turn of kbest
  }

  inline bool might_insert(float cost) { return (cost <= prune_if_gt) && ((count < beam_size) || (cost < worst_cost)); }

  bool insert(T*data, float cost, uint32_t hash)   // returns TRUE iff element was actually added
  { if (!might_insert(cost)) return false;

    //bool we_were_worse = false;
    // if (is_equivalent) {
    //   size_t mod = recomb_buckets.size();
    //   size_t id  = hash % mod;
    //   size_t equiv_pos = bucket_contains_equiv(recomb_buckets[i], data, hash);
    //   if (equiv_pos != (size_t) -1) { // we can recombing at equiv_pos
    //     if (cost >= recomb_buckets[i][equiv_pos].cost) {
    //       // we are more expensive, so ignore
    //       we_were_worse = true;
    //       beam_element<T> * be = new beam_element<T>;
    //       be->hash = hash; be->cost = cost; be->data = data; be->active = true; be->recombined = false; be->recomb_friends = nullptr;
    //       add_recomb_friend(recomb_buckets[i][equiv_pos], be);
    //   }
    // }

    if (beam_size < BEAM_CONSTANT_SIZE)
    { // find the worst item and directly replace it
      size_t worst_idx = 0;
      float  worst_idx_cost = A[0].cost;
      for (size_t i=1; i<beam_size; i++)
        if (A[i].cost > worst_idx_cost)
        { worst_idx = i;
          worst_idx_cost = A[i].cost;
          if (worst_idx_cost <= worst_cost)
            break;
        }
      if (cost >= worst_idx_cost)
        return false;

      A[worst_idx].hash = hash;
      A[worst_idx].cost = cost;
      A[worst_idx].data = data;
      A[worst_idx].active = true;
      // A[worst_idx].recombined = false;
      // A[worst_idx].recomb_friends = nullptr;  // TODO: free it if it isn't nullptr
      worst_cost = cost;
    }
    else
    { beam_element<T> be;
      be.hash = hash;
      be.cost = cost;
      be.data = data;
      be.active = true;
      // be.recombined = false;
      // be.recomb_friends = nullptr;

      A.push_back(be);
      count++;
    }

    if (cost < best_cost)
    { best_cost = cost;
      best_cost_data = data;
    }
    if (cost > worst_cost)
    { worst_cost  = cost;
      prune_if_gt = max(1.f, best_cost) * pruning_coefficient;
    }
    return true;
  }

  beam_element<T>* get_best_item()
  { if (count == 0) return nullptr;
    beam_element<T> *ret = A.begin;
    while ((ret != A.end) && (!ret->active)) ++ret;
    return (ret == A.end) ? nullptr : ret;
  }

  beam_element<T>* pop_best_item()
  { if (count == 0)
      return nullptr;

    beam_element<T> *ret = nullptr;
    float next_best_cost = FLT_MAX;
    for (beam_element<T> *el = A.begin; el!=A.end; el++)
      if ((ret == nullptr) && el->active && (el->cost <= best_cost))
        ret = el;
      else if (el->active && (el->cost < next_best_cost))
      { next_best_cost = el->cost;
        best_cost_data = el->data;
      }

    if (ret != nullptr)
    { best_cost = next_best_cost;
      prune_if_gt = max(1.f, best_cost) * pruning_coefficient;
      ret->active = false;
      count--;
    }
    else
    { best_cost = FLT_MAX;
      prune_if_gt = FLT_MAX;
      best_cost_data = nullptr;
    }

    return ret;
  }

  void do_recombination()
  { qsort(A.begin, A.size(), sizeof(beam_element<T>), compare_on_hash_then_cost);
    size_t start = 0;
    while (start < A.size() - 1)
    { size_t end = start+1;
      for (; (end < A.size()) && (A[start].hash == A[end].hash); end++);
      assert(start < A.size());
      assert(end <= A.size());
      //cerr << "start=" << start << " end=" << end << endl;
      // go over all pairs
      for (size_t i=start; i<end; i++)
      { if (! A[i].active) continue;
        assert(i < A.size());
        for (size_t j=i+1; j<end; j++)
        { if (! A[j].active) continue;
          assert(j < A.size());
          //cerr << "te " << i << "," << j << endl;
          if (is_equivalent(A[i].data, A[j].data))
          { A[j].active = false; // TODO: if kbest is on, do recomb_friends
            //cerr << "equivalent " << i << "," << j << ": " << ((size_t)A[i].data) << " and " << ((size_t)A[j].data) << endl;
          }
        }
      }
      start = end;
    }
  }

  void compact(void (*free_data)(T*)=nullptr)
  { if (is_equivalent) do_recombination();
    qsort(A.begin, A.size(), sizeof(beam_element<T>), compare_on_cost); // TODO: quick select

    if (count <= beam_size) return;

    count = beam_size;
    if (is_equivalent) // we might be able to get rid of even more
      while ((count > 1) && !A[count-1].active) count--;

    if (free_data)
      for (beam_element<T> * be = A.begin+count; be != A.end; ++be)
        free_data(be->data);

    A.end = A.begin + count;

    best_cost = A[0].cost;
    worst_cost = A[count-1].cost;
    prune_if_gt = max(1.f, best_cost) * pruning_coefficient;
    best_cost_data = A[0].data;
  }

  void maybe_compact(void (*free_data)(T*)=nullptr)
  { if (count >= beam_size * 10)
      compact(free_data);
  }

  void erase(void (*free_data)(T*)=nullptr)
  { if (free_data)
      for (beam_element<T> * be = A.begin; be != A.end; ++be)
        free_data(be->data);
    A.erase();
    count = 0;
    worst_cost  = -FLT_MAX;
    best_cost   =  FLT_MAX;
    prune_if_gt =  FLT_MAX;
    best_cost_data = nullptr;
  }

  ~beam()
  { assert(A.size() == 0);
    A.delete_v();
  }

  beam_element<T> * begin() { return A.begin; }
  beam_element<T> * end()   { return A.end; }
  size_t         size()  { return count; }
  bool           empty() { return A.empty(); }
  size_t         get_beam_size() { return beam_size; }

private:
  // void add_recomb_friend(beam_element<T> *better, beam_element<T> *worse) {
  //   assert( better->cost <= worse->cost );
  //   if (better->recomb_friends == nullptr) {
  //     if (worse->recomb_friends != nullptr) {
  //       better->recomb_friends = worse->recomb_friends;
  //       worse->recomb_friends = nullptr;
  //     } else
  //       better->recomb_friends = new vector<beam_element<T>*>;
  //   } else {
  //     assert(worse->recomb_friends == nullptr);
  //   }
  // }
};



}

