/*
Copyright (c) 2013 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Author: Leonardo de Moura
*/
#pragma once
#include <algorithm>
#include <cstring>
#include "runtime/debug.h"
#include "runtime/optional.h"

namespace lean {
/**
    \brief Very similar to std::vector, but stores elements on the
    system stack when collection has less than \c INITIAL_SIZE.
    This collection is useful when writing functions that need
    temporary storage.
*/
template<typename T, size_t INITIAL_SIZE = 16>
class buffer {
protected:
    T *      m_buffer;
    size_t   m_pos;
    size_t   m_capacity;
    char     m_initial_buffer[INITIAL_SIZE * sizeof(T)];

    void free_memory() {
        if (m_buffer != reinterpret_cast<T*>(m_initial_buffer))
            delete[] reinterpret_cast<char*>(m_buffer);
    }

    void set_capacity(size_t new_capacity) {
        lean_assert(new_capacity > m_capacity);
        char * new_buffer_char = new char[sizeof(T) * new_capacity];
        T * new_buffer         = reinterpret_cast<T*>(new_buffer_char);
        std::uninitialized_copy(m_buffer, m_buffer + m_pos, new_buffer);
        destroy();
        m_buffer              = new_buffer;
        m_capacity            = new_capacity;
    }

    void expand() {
        set_capacity(m_capacity << 1);
    }

    void destroy_elements() {
        std::for_each(begin(), end(), [](T & e) { e.~T(); });
    }

    void destroy() {
        destroy_elements();
        free_memory();
    }

public:
    typedef T value_type;
    typedef T * iterator;
    typedef T const * const_iterator;

    buffer():
        m_buffer(reinterpret_cast<T *>(m_initial_buffer)),
        m_pos(0),
        m_capacity(INITIAL_SIZE) {
    }

    buffer(buffer const & source):
        m_buffer(reinterpret_cast<T *>(m_initial_buffer)),
        m_pos(0),
        m_capacity(INITIAL_SIZE) {
        std::for_each(source.begin(), source.end(), [=](T const & e) { push_back(e); });
    }

    void ensure_capacity(size_t new_capacity) {
        if (new_capacity > m_capacity) {
            set_capacity(new_capacity);
        }
    }

    ~buffer() { destroy(); }

    buffer & operator=(buffer const & source) {
        clear();
        std::for_each(source.begin(), source.end(), [=](T const & e) { push_back(e); });
        return *this;
    }

    bool operator==(buffer const & other) const {
        if (size() != other.size()) {
            return false;
        } else {
            for (size_t i = 0; i < size(); i++) {
                if (operator[](i) != other[i])
                    return false;
            }
            return true;
        }
    }

    bool operator!=(buffer const & other) const {
        return !operator==(other);
    }

    T const & back() const { lean_assert(!empty() && m_pos > 0); return m_buffer[m_pos - 1]; }
    T & back() { lean_assert(!empty() && m_pos > 0); return m_buffer[m_pos - 1]; }
    T & operator[](size_t idx) { lean_assert(idx < size());  return m_buffer[idx]; }
    T const & operator[](size_t idx) const { lean_assert(idx < size()); return m_buffer[idx]; }
    void clear() { destroy_elements(); m_pos = 0; }
    size_t size() const { return m_pos; }
    T const * data() const { return m_buffer; }
    T * data() { return m_buffer; }
    bool empty() const { return m_pos == 0;  }
    iterator begin() { return m_buffer; }
    iterator end() { return m_buffer + size(); }
    const_iterator begin() const { return m_buffer; }
    const_iterator end() const { return m_buffer + size(); }
    size_t capacity() const { return m_capacity; }

    void push_back(T const & elem) {
        if (m_pos >= m_capacity)
            expand();
        new (m_buffer + m_pos) T(elem);
        m_pos++;
    }

    template<typename... Args>
    void emplace_back(Args&&... args) {
        if (m_pos >= m_capacity)
            expand();
        new (m_buffer + m_pos) T(args...);
        m_pos++;
    }

    void pop_back() {
        back().~T();
        m_pos--;
    }

    void append(size_t sz, T const * elems) {
        ensure_capacity(size() + sz);
        for (size_t i = 0; i < sz; i++)
            push_back(elems[i]);
    }

    template<typename C>
    void append(C const & c) {
        append(c.size(), c.data());
    }

    void resize(size_t nsz, T const & elem = T()) {
        size_t sz = size();
        if (nsz > sz) {
            ensure_capacity(nsz);
            for (size_t i = sz; i < nsz; i++)
                push_back(elem);
        } else if (nsz < sz) {
            for (size_t i = nsz; i < sz; i++)
                pop_back();
        }
        lean_assert(size() == nsz);
    }

    void erase(size_t idx) {
        lean_assert(idx < size());
        for (size_t i = idx+1; i < size(); i++)
            m_buffer[i-1] = std::move(m_buffer[i]);
        pop_back();
    }

    void erase_elem(T const & elem) {
        for (size_t i = 0; i < size(); i++) {
            if (m_buffer[i] == elem) {
                erase(i);
                return;
            }
        }
    }

    optional<size_t> index_of(T const & elem) const {
        for (size_t i = 0; i < size(); i++)
            if (m_buffer[i] == elem) return optional<size_t>(i);
        return optional<size_t>();
    }

    void insert(size_t idx, T const & elem) {
        using std::swap;
        lean_assert(idx <= size());
        push_back(elem);
        size_t i = size();
        while (i > idx + 1) {
            --i;
            swap(m_buffer[i], m_buffer[i-1]);
        }
    }

    void shrink(size_t nsz) {
        size_t sz = size();
        lean_assert(nsz <= sz);
        for (size_t i = nsz; i < sz; i++)
            pop_back();
        lean_assert(size() == nsz);
    }
};

}
