#include <deque>
#include <cstring>
#include <cstdio>
#include <cassert>
#include <ctime>
#include <rtosc/rtosc.h>
#include <rtosc/undo-history.h>

using std::pair;
using std::make_pair;

namespace rtosc {
class UndoHistoryImpl
{
    public:
        UndoHistoryImpl(void)
            :max_history_size(20)
        {}
        ~UndoHistoryImpl(void)
        {
            clear();
        }
        std::deque<pair<time_t, const char *>> history;
        long history_pos;
        unsigned max_history_size;//XXX Expose this via a public API
        std::function<void(const char*)> cb;

        void rewind(const char *msg);
        void replay(const char *msg);
        bool mergeEvent(time_t t, const char *msg, char *buf, size_t N);
        void clear(void);
};

UndoHistory::UndoHistory(void)
{
    impl = new UndoHistoryImpl;
    impl->history_pos  = 0;
}

UndoHistory::~UndoHistory(void)
{
    delete impl;
}

void UndoHistory::recordEvent(const char *msg)
{
    //TODO Properly account for when you have traveled back in time.
    //while this could result in another branch of history, the simple method
    //would be to kill off any future redos when new history is recorded
    if(impl->history.size() != (unsigned) impl->history_pos) {
        impl->history.resize(impl->history_pos);
    }

    size_t len = rtosc_message_length(msg, -1);
    char *data = new char[len];
    time_t now = time(NULL);
    //printf("now = '%ld'\n", now);
    if(!impl->mergeEvent(now, msg, data, len)) {
        memcpy(data, msg, len);
        impl->history.push_back(make_pair(now, data));
        impl->history_pos++;
        if(impl->history.size() > impl->max_history_size)
        {
            delete[] impl->history[0].second;
            impl->history.pop_front();
            impl->history_pos--;
        }
    }

}

void UndoHistory::showHistory(void) const
{
    int i = 0;
    for(auto s : impl->history)
        printf("#%d type: %s dest: %s arguments: %s\n", i++,
                s.second, rtosc_argument(s.second, 0).s, rtosc_argument_string(s.second));
}

static char tmp[256];
void UndoHistoryImpl::rewind(const char *msg)
{
    memset(tmp, 0, sizeof(tmp));
    rtosc_arg_t arg = rtosc_argument(msg,1);
    rtosc_amessage(tmp, 256, rtosc_argument(msg,0).s,
            rtosc_argument_string(msg)+2,
            &arg);
    cb(tmp);
}

void UndoHistoryImpl::replay(const char *msg)
{
    rtosc_arg_t arg = rtosc_argument(msg,2);
    int len = rtosc_amessage(tmp, 256, rtosc_argument(msg,0).s,
            rtosc_argument_string(msg)+2,
            &arg);
    
    if(len)
        cb(tmp);
}

const char *getUndoAddress(const char *msg)
{
    return rtosc_argument(msg,0).s;
}

bool UndoHistoryImpl::mergeEvent(time_t now, const char *msg, char *buf, size_t N)
{
    if(history_pos == 0)
        return false;
    for(int i=history_pos-1; i>=0; --i) {
        if(difftime(now, history[i].first) > 2)
            break;
        if(!strcmp(getUndoAddress(msg),
                    getUndoAddress(history[i].second)))
        {
            //We can splice events together, merging them into one event
            rtosc_arg_t args[3];
            args[0] = rtosc_argument(msg, 0);
            args[1] = rtosc_argument(history[i].second,1);
            args[2] = rtosc_argument(msg, 2);

            rtosc_amessage(buf, N, msg, rtosc_argument_string(msg), args);

            delete [] history[i].second;
            history[i].second = buf;
            history[i].first = now;
            return true;
        }
    }
    return false;
}

void UndoHistoryImpl::clear(void)
{
    for(auto elm : history)
        delete [] elm.second;
    history.clear();
    history_pos = 0;
}



void UndoHistory::seekHistory(int distance)
{
    //TODO print out the events that would need to take place to get to the
    //final destination
    
    //TODO limit the distance to be to applicable sizes
    //ie ones that do not exceed the known history/future
    long dest = impl->history_pos + distance;
    if(dest < 0)
        distance -= dest;
    if(dest > (long) impl->history.size())
        distance  = impl->history.size() - impl->history_pos;
    if(!distance)
        return;
    
    //TODO account for traveling back in time
    if(distance<0)
        while(distance++)
            impl->rewind(impl->history[--impl->history_pos].second);
    else
        while(distance--)
            impl->replay(impl->history[impl->history_pos++].second);
}

unsigned UndoHistory::getPos(void) const
{
    return impl->history_pos;
}

const char *UndoHistory::getHistory(int i) const
{
    return impl->history[i].second;
}

size_t UndoHistory::size() const
{
    return impl->history.size();
}

void UndoHistory::setCallback(std::function<void(const char*)> cb)
{
    impl->cb = cb;
}
};
