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

    #include "lz77_buffer_kernel_abstract.h"
    #include "../algs.h"

    namespace dlib
    {

        template <
            typename sliding_buffer
            >
        class lz77_buffer_kernel_2
        {
            /*!
                REQUIREMENTS ON sliding_buffer
                    sliding_buffer must be an implementation of sliding_buffer/sliding_buffer_kernel_abstract.h
                    and must be instantiated to contain unsigned char data

                INITIAL VALUE
                    history_limit == defined by constructor arguments
                    lookahead_limit == defined by constructor arguments
                    history_size == 0
                    lookahead_size == 0
                    buffer.size() == history_limit + lookahead_limit
                    buffer[i] == 0 for all valid i

                    nodes == an array of history_limit-3 nodes
                    id_table == an array of buffer.size() pointers
                    hash_table == an array of buffer.size() pointers and all are set to 0
                    mask == buffer.size() - 1
                    next_free_node == 0

                CONVENTION
                    history_limit == get_history_buffer_limit()
                    lookahead_limit == get_lookahead_buffer_limit()
                    history_size == get_history_buffer_size()
                    lookahead_limit == get_lookahead_buffer_size()

                    buffer.size() == history_limit + lookahead_limit

                    lookahead_buffer(i) == buffer[lookahead_limit-1-i]
                    history_buffer(i) == buffer[lookahead_limit+i]

                    hash_table[hash(a,b,c,d)] points to the head of a linked list.
                        Each node in this linked list tells the location in the buffer
                        of a string that begins with abcd or a string who's first four
                        letters have the same hash.  The linked list is terminated by a
                        node with a null next pointer.

                    hash_table[i] == 0 if there is no linked list for this element of the hash
                        table.

                    each node in the hash table is allocated from the array nodes.
                    When adding a node to hash_table:
                        if (if all nodes aren't already in the hash_table) then
                        {
                            the next node to use is nodes[next_free_node].
                        }
                        else
                        {
                            recycle nodes from the hash_table itself.  This works because
                            when we add new nodes we also have to remove nodes.
                        }

                    if (there is a node defined with an id of i) then
                    {
                        if (id_table[i] != 0) then
                            id_table[i]->next->id == i
                        else
                            hash_table[some_hash]->id == i
                    }
            !*/

        public:

            lz77_buffer_kernel_2 (
                unsigned long total_limit_,
                unsigned long lookahead_limit_
            );

            virtual ~lz77_buffer_kernel_2 (
            );

            void clear(
            );

            void add (
                unsigned char symbol
            );

            void find_match (
                unsigned long& index,
                unsigned long& length,
                unsigned long min_match_length
            );

            inline unsigned long get_history_buffer_limit (
            ) const { return history_limit; }

            inline unsigned long get_lookahead_buffer_limit (
            ) const { return lookahead_limit; }

            inline unsigned long get_history_buffer_size (
            ) const { return history_size; }

            inline unsigned long get_lookahead_buffer_size (
            ) const { return lookahead_size; }

            inline unsigned char lookahead_buffer (
                unsigned long index
            ) const { return buffer[lookahead_limit-1-index]; }

            inline unsigned char history_buffer (
                unsigned long index
            ) const { return buffer[lookahead_limit+index]; }

            inline void shift_buffers (
                unsigned long N
            ) { shift_buffer(N); }

        private:

            inline unsigned long hash (
                unsigned char a,
                unsigned char b,
                unsigned char c,
                unsigned char d
            ) const
            /*!
                ensures
                    - returns a hash of the 4 arguments and the hash is in the range
            !*/
            {
                unsigned long B = b << 3;
                unsigned long C = c << 6;
                unsigned long D = d << 9;

                unsigned long temp = a + B;
                temp += C;
                temp += D;

                return (temp&mask); /**/
            }

            void shift_buffer (
                unsigned long N
            );
            /*!
                requires
                    - N <= lookahead_size
                ensuers
                    - #lookahead_size == lookahead_size - N
                    - if (history_size+N < history_limit) then
                        - #history_size == history_size+N
                    - else
                        - #history_size == history_limit
                    - for all i where 0 <= i < N:
                      #history_buffer(N-1-i) == lookahead_buffer(i)
                    - for all i where 0 <= i < #history_size-N:
                      #history_buffer(N+i) == history_buffer(i)
                    - for all i where 0 <= i < #lookahead_size
                      #lookahead_buffer(i) == lookahead_buffer(N+i)
            !*/

            // member data
            sliding_buffer buffer;
            unsigned long lookahead_limit;
            unsigned long history_limit;

            struct node
            {
                unsigned long id;
                node* next;
            };

            node** hash_table;
            node* nodes;
            node** id_table;
            unsigned long next_free_node;
            unsigned long mask;

            unsigned long lookahead_size;
            unsigned long history_size;

            // restricted functions
            lz77_buffer_kernel_2(lz77_buffer_kernel_2<sliding_buffer>&);        // copy constructor
            lz77_buffer_kernel_2<sliding_buffer>& operator=(lz77_buffer_kernel_2<sliding_buffer>&);    // assignment operator
        };

    // ----------------------------------------------------------------------------------------
    // ----------------------------------------------------------------------------------------
        // member function definitions
    // ----------------------------------------------------------------------------------------
    // ----------------------------------------------------------------------------------------

        template <
            typename sliding_buffer
            >
        lz77_buffer_kernel_2<sliding_buffer>::
        lz77_buffer_kernel_2 (
            unsigned long total_limit_,
            unsigned long lookahead_limit_
        ) :
            lookahead_size(0),
            history_size(0)
        {
            buffer.set_size(total_limit_);
            lookahead_limit = lookahead_limit_;
            history_limit = buffer.size() - lookahead_limit_;

            nodes = new node[history_limit-3];

            try { id_table = new node*[buffer.size()]; }
            catch (...) { delete [] nodes; throw; }

            try { hash_table = new node*[buffer.size()]; }
            catch (...) { delete [] id_table; delete [] nodes; throw; }

            mask = buffer.size()-1;
            next_free_node = 0;

            node** start = hash_table;
            node** end = hash_table + buffer.size();
            while (start != end)
            {
                *start = 0;
                ++start;
            }

            for (unsigned long i = 0; i < buffer.size(); ++i)
                buffer[i] = 0;

        }

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

        template <
            typename sliding_buffer
            >
        lz77_buffer_kernel_2<sliding_buffer>::
        ~lz77_buffer_kernel_2 (
        )
        {
            delete [] nodes;
            delete [] hash_table;
            delete [] id_table;
        }

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

        template <
            typename sliding_buffer
            >
        void lz77_buffer_kernel_2<sliding_buffer>::
        clear(
        )
        {
            lookahead_size = 0;
            history_size = 0;
            next_free_node = 0;

            node** start = hash_table;
            node** end = hash_table + buffer.size();
            while (start != end)
            {
                *start = 0;
                ++start;
            }
        }

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

        template <
            typename sliding_buffer
            >
        void lz77_buffer_kernel_2<sliding_buffer>::
        shift_buffer (
            unsigned long N
        )
        {
            unsigned long old_history_size = history_size;
            unsigned long temp = history_size+N;
            unsigned long new_nodes; // the number of nodes to pull from the nodes array
            unsigned long recycled_nodes; // the number of nodes to pull from hash_table
            lookahead_size -= N;
            if (temp <= history_limit)
            {
                if (history_size <= 3)
                {
                    if ((3-history_size) >= N)
                        new_nodes = 0;
                    else
                        new_nodes = N - (3-history_size);
                }
                else
                {
                    new_nodes = N;
                }

                recycled_nodes = 0;
                history_size = temp;
            }
            else
            {
                if (history_size != history_limit)
                {
                    new_nodes = history_limit - history_size;
                    recycled_nodes = temp - history_limit;
                    history_size = history_limit;
                }
                else
                {
                    new_nodes = 0;
                    recycled_nodes = N;
                }
            }

            unsigned long i = lookahead_limit + 2;

            // if there are any "new" nodes to add to the hash table
            if (new_nodes != 0)
            {
                unsigned long stop = i - new_nodes;
                for (; i > stop; --i)
                {
                    nodes[next_free_node].next = 0;
                    nodes[next_free_node].id = buffer.get_element_id(i);
                    id_table[nodes[next_free_node].id] = 0;

                    unsigned long new_hash = hash(buffer[i],buffer[i-1],buffer[i-2],buffer[i-3]);

                    if (hash_table[new_hash] != 0)
                        id_table[hash_table[new_hash]->id] = &nodes[next_free_node];
                    nodes[next_free_node].next = hash_table[new_hash];
                    hash_table[new_hash] = &nodes[next_free_node];

                    ++next_free_node;
                }
            } // if (new_nodes != 0)

            unsigned long stop = i - recycled_nodes;
            unsigned long old = old_history_size-1+lookahead_limit;
            for (; i > stop; --i)
            {
                // find the next node to recycle in hash_table
                node* recycled_node;

                unsigned long old_id = buffer.get_element_id(old);

                // find the node with id old_id
                if (id_table[old_id] == 0)
                {
                    unsigned long old_hash = hash(buffer[old],buffer[old-1],buffer[old-2],buffer[old-3]);
                    recycled_node = hash_table[old_hash];

                    // fill the gap left by removing this node
                    hash_table[old_hash] = recycled_node->next;
                }
                else
                {
                    recycled_node = id_table[old_id]->next;

                    // fill the gap left by removing this node
                    id_table[old_id]->next = recycled_node->next;
                }

                --old;

                recycled_node->next = 0;
                recycled_node->id = buffer.get_element_id(i);
                id_table[recycled_node->id] = 0;

                unsigned long new_hash = hash(buffer[i],buffer[i-1],buffer[i-2],buffer[i-3]);

                if (hash_table[new_hash] != 0)
                    id_table[hash_table[new_hash]->id] = recycled_node;

                recycled_node->next = hash_table[new_hash];
                hash_table[new_hash] = recycled_node;

            } // for (; i > stop; --i)

            buffer.rotate_left(N);
        }

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

        template <
            typename sliding_buffer
            >
        void lz77_buffer_kernel_2<sliding_buffer>::
        add (
            unsigned char symbol
        )
        {
            if (lookahead_size == lookahead_limit)
            {
                shift_buffer(1);
            }
            buffer[lookahead_limit-1-lookahead_size] = symbol;
            ++lookahead_size;
        }

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

        template <
            typename sliding_buffer
            >
        void lz77_buffer_kernel_2<sliding_buffer>::
        find_match (
            unsigned long& index,
            unsigned long& length,
            unsigned long min_match_length
        )
        {
            unsigned long match_length = 0;   // the length of the longest match we find
            unsigned long match_index = 0;    // the index of the longest match we find

            const unsigned long hash_value = hash(lookahead_buffer(0),
                                                  lookahead_buffer(1),
                                                  lookahead_buffer(2),
                                                  lookahead_buffer(3)
                                                  );

            node* temp = hash_table[hash_value];
            while (temp != 0)
            {
                // current position in the history buffer
                unsigned long hpos = buffer.get_element_index(temp->id)-lookahead_limit;
                // current position in the lookahead buffer
                unsigned long lpos = 0;

                // find length of this match
                while (history_buffer(hpos) == lookahead_buffer(lpos))
                {
                    ++lpos;
                    if (hpos == 0)
                        break;
                    --hpos;
                    if (lpos == lookahead_size)
                        break;
                }

                if (lpos > match_length)
                {
                    match_length = lpos;
                    match_index = buffer.get_element_index(temp->id)-lookahead_limit;
                    // if this is the longest possible match then stop looking
                    if (lpos == lookahead_limit)
                        break;
                }

                temp = temp->next;
            } // while (temp != 0)

            // if we found a match that was long enough then report it
            if (match_length >= min_match_length)
            {
                shift_buffer(match_length);
                index = match_index;
                length = match_length;
            }
            else
            {
                length = 0;
            }
        }

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

    }

    #endif // DLIB_LZ77_BUFFER_KERNEl_2_