// Copyright (C) 2010  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_RANDOM_SUBSeT_SELECTOR_H_
#define DLIB_RANDOM_SUBSeT_SELECTOR_H_

#include "random_subset_selector_abstract.h"
#include "../rand.h"
#include <vector>
#include "../algs.h"
#include "../string.h"
#include "../serialize.h"
#include "../matrix/matrix_mat.h"
#include <iostream>

namespace dlib
{
    template <
        typename T,
        typename Rand_type = dlib::rand
        >
    class random_subset_selector
    {
        /*!
            INITIAL VALUE
                - _max_size == 0
                - items.size() == 0
                - count == 0
                - _next_add_accepts == false

            CONVENTION
                - count == the number of times add() has been called since the last
                  time this object was empty.
                - items.size() == size()
                - max_size() == _max_size
                - next_add_accepts() == _next_add_accepts
        !*/
    public:
        typedef T type;
        typedef T value_type;
        typedef default_memory_manager mem_manager_type;
        typedef Rand_type rand_type;

        typedef typename std::vector<T>::iterator iterator;
        typedef typename std::vector<T>::const_iterator const_iterator;


        random_subset_selector (
        )
        {
            _max_size = 0;
            make_empty();
        }

        void set_seed(const std::string& value)
        {
            rnd.set_seed(value);
        }

        void make_empty (
        )
        {
            items.resize(0);
            count = 0;
            update_next_add_accepts();
        }

        const std::vector<T>& to_std_vector(
        ) const { return items; }

        size_t size (
        ) const 
        {
            return items.size();
        }

        void set_max_size (
            unsigned long new_max_size
        )
        {
            items.reserve(new_max_size);
            make_empty();
            _max_size = new_max_size;
            update_next_add_accepts();
        }

        unsigned long max_size (
        ) const
        {
            return _max_size;
        }

        T& operator[] (
            unsigned long idx
        ) 
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(idx < size(),
                "\tvoid random_subset_selector::operator[]()"
                << "\n\t idx is out of range"
                << "\n\t idx:    " << idx 
                << "\n\t size(): " << size() 
                << "\n\t this:   " << this
                );

            return items[idx];
        }

        const T& operator[] (
            unsigned long idx
        ) const
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(idx < size(),
                "\tvoid random_subset_selector::operator[]()"
                << "\n\t idx is out of range"
                << "\n\t idx:    " << idx 
                << "\n\t size(): " << size() 
                << "\n\t this:   " << this
                );

            return items[idx];
        }

        iterator                begin()                         { return items.begin(); }
        const_iterator          begin() const                   { return items.begin(); }
        iterator                end()                           { return items.end(); }
        const_iterator          end() const                     { return items.end(); }

        bool next_add_accepts (
        ) const 
        {
            return _next_add_accepts;
        }

        void add (
            const T& new_item
        )
        {
            if (items.size() < _max_size)
            {
                items.push_back(new_item);
                // swap into a random place
                exchange(items[rnd.get_random_32bit_number()%items.size()], items.back());
            }
            else if (_next_add_accepts)
            {
                // pick a random element of items and replace it.
                items[rnd.get_random_32bit_number()%items.size()] = new_item;
            }

            update_next_add_accepts();
            ++count;
        }

        void add (
        )
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(next_add_accepts() == false,
                "\tvoid random_subset_selector::add()"
                << "\n\t You should be calling the version of add() that takes an argument"
                << "\n\t this: " << this
                );

            update_next_add_accepts();
            ++count;
        }

        void swap (
            random_subset_selector& a
        )
        {
            items.swap(a.items);
            std::swap(_max_size, a._max_size);
            std::swap(count, a.count);
            rnd.swap(a.rnd);
            std::swap(_next_add_accepts, a._next_add_accepts);
        }

        template <typename T1, typename T2>
        friend void serialize (
            const random_subset_selector<T1,T2>& item,
            std::ostream& out
        );

        template <typename T1, typename T2>
        friend void deserialize (
            random_subset_selector<T1,T2>& item,
            std::istream& in 
        );

    private:

        void update_next_add_accepts (
        )
        {
            if (items.size() < _max_size)
            {
                _next_add_accepts = true;
            }
            else if (_max_size == 0)
            {
                _next_add_accepts = false;
            }
            else
            {
                // At this point each element of items has had an equal chance of being in this object.   
                // In particular, the probability that each arrived here is currently items.size()/count.    
                // We need to be able to say that, after this function ends, the probability of any 
                // particular object ending up in items is items.size()/(count+1).  So this means that 
                // we should decide to add a new item into items with this probability.  Also, if we do 
                // so then we pick one of the current items and replace it at random with the new item.

                // Make me a random 64 bit number.   This might seem excessive but I want this object
                // to be able to handle an effectively infinite number of calls to add().  So count
                // might get very large and we need to deal with that properly.
                const unsigned long num1 = rnd.get_random_32bit_number();
                const unsigned long num2 = rnd.get_random_32bit_number();
                uint64 num = num1;
                num <<= 32;
                num |= num2;

                num %= (count+1);

                _next_add_accepts = (num < items.size());
            }

        }

        std::vector<T> items;
        unsigned long _max_size;
        uint64 count; 

        rand_type rnd;

        bool _next_add_accepts;

    };

    template <
        typename T,
        typename rand_type 
        >
    void swap (
        random_subset_selector<T,rand_type>& a,
        random_subset_selector<T,rand_type>& b
    ) { a.swap(b); }

// ----------------------------------------------------------------------------------------

    template <typename T1, typename T2>
    void serialize (
        const random_subset_selector<T1,T2>& item,
        std::ostream& out
    )
    {
        serialize(item.items, out);
        serialize(item._max_size, out);
        serialize(item.count, out);
        serialize(item.rnd, out);
        serialize(item._next_add_accepts, out);
    }

    template <typename T1, typename T2>
    void deserialize (
        random_subset_selector<T1,T2>& item,
        std::istream& in 
    )
    {
        deserialize(item.items, in);
        deserialize(item._max_size, in);
        deserialize(item.count, in);
        deserialize(item.rnd, in);
        deserialize(item._next_add_accepts, in);
    }

// ----------------------------------------------------------------------------------------

    template <
        typename T,
        typename alloc
        >
    random_subset_selector<T> randomly_subsample (
        const std::vector<T,alloc>& samples,
        unsigned long num
    )
    {
        random_subset_selector<T> subset;
        subset.set_max_size(num);
        for (unsigned long i = 0; i < samples.size(); ++i)
            subset.add(samples[i]);
        return subset;
    }

// ----------------------------------------------------------------------------------------

    template <
        typename T,
        typename alloc,
        typename U
        >
    random_subset_selector<T> randomly_subsample (
        const std::vector<T,alloc>& samples,
        unsigned long num,
        const U& random_seed
    )
    {
        random_subset_selector<T> subset;
        subset.set_seed(cast_to_string(random_seed));
        subset.set_max_size(num);
        for (unsigned long i = 0; i < samples.size(); ++i)
            subset.add(samples[i]);
        return subset;
    }

// ----------------------------------------------------------------------------------------

    template <
        typename T
        >
    random_subset_selector<T> randomly_subsample (
        const random_subset_selector<T>& samples,
        unsigned long num
    )
    {
        random_subset_selector<T> subset;
        subset.set_max_size(num);
        for (unsigned long i = 0; i < samples.size(); ++i)
            subset.add(samples[i]);
        return subset;
    }

// ----------------------------------------------------------------------------------------

    template <
        typename T,
        typename U
        >
    random_subset_selector<T> randomly_subsample (
        const random_subset_selector<T>& samples,
        unsigned long num,
        const U& random_seed
    )
    {
        random_subset_selector<T> subset;
        subset.set_seed(cast_to_string(random_seed));
        subset.set_max_size(num);
        for (unsigned long i = 0; i < samples.size(); ++i)
            subset.add(samples[i]);
        return subset;
    }

// ----------------------------------------------------------------------------------------

    template <
        typename T
        >
    const matrix_op<op_array_to_mat<random_subset_selector<T> > > mat (
        const random_subset_selector<T>& m 
    )
    {
        typedef op_array_to_mat<random_subset_selector<T> > op;
        return matrix_op<op>(op(m));
    }

// ----------------------------------------------------------------------------------------

}

#endif // DLIB_RANDOM_SUBSeT_SELECTOR_H_