#ifndef TUT_RESTARTABLE_H_GUARD
#define TUT_RESTARTABLE_H_GUARD

#include <tut/tut.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>

/**
 * Template Unit Tests Framework for C++.
 * http://tut.dozen.ru
 *
 * Optional restartable wrapper for test_runner. Allows to restart test runs
 * finished due to abnormal test application termination (such as segmentation
 * fault or math error).
 *
 * @author Vladimir Dyuzhev, Vladimir.Dyuzhev@gmail.com
 */

namespace tut
{
    
namespace util
{
    
/**
 * Escapes non-alphabetical characters in string.
 */
std::string escape(const std::string& orig)
{
    std::string rc;
    std::string::const_iterator i,e;
    i = orig.begin();
    e = orig.end();

    while (i != e)
    {
        if ((*i >= 'a' && *i <= 'z') ||
                (*i >= 'A' && *i <= 'Z') ||
                (*i >= '0' && *i <= '9') )
        {
            rc += *i;
        }
        else
        {
            rc += '\\';
            rc += ('a'+(((unsigned int)*i) >> 4));
            rc += ('a'+(((unsigned int)*i) & 0xF));
        }

        ++i;
    }
    return rc;
}

/**
 * Un-escapes string.
 */
std::string unescape(const std::string& orig)
{
    std::string rc;
    std::string::const_iterator i,e;
    i = orig.begin();
    e = orig.end();

    while (i != e)
    {
        if (*i != '\\')
        {
            rc += *i;
        }
        else
        {
            ++i;
            if (i == e)
            {
                throw std::invalid_argument("unexpected end of string");
            }
            unsigned int c1 = *i;
            ++i;
            if (i == e)
            {
                throw std::invalid_argument("unexpected end of string");
            }
            unsigned int c2 = *i;
            rc += (((c1 - 'a') << 4) + (c2 - 'a'));
        }

        ++i;
    }
    return rc;
}

/**
 * Serialize test_result avoiding interfering with operator <<.
 */
void serialize(std::ostream& os, const tut::test_result& tr)
{
    os << escape(tr.group) << std::endl;
    os << tr.test << ' ';
    switch(tr.result)
    {
    case test_result::ok:
        os << 0;
        break;
    case test_result::fail:
        os << 1;
        break;
    case test_result::ex:
        os << 2;
        break;
    case test_result::warn:
        os << 3;
        break;
    case test_result::term:
        os << 4;
        break;
    default:
        throw std::logic_error("operator << : bad result_type");
    }
    os << ' ' << escape(tr.message) << std::endl;
}

/**
 * deserialization for test_result
 */
void deserialize(std::istream& is, tut::test_result& tr)
{
    std::getline(is,tr.group);
    if (is.eof())
    {
        throw tut::no_more_tests();
    }
    tr.group = unescape(tr.group);

    tr.test = -1;
    is >> tr.test;
    if (tr.test < 0)
    {
        throw std::logic_error("operator >> : bad test number");
    }

    int n = -1;
    is >> n;
    switch(n)
    {
    case 0:
        tr.result = test_result::ok;
        break;
    case 1:
        tr.result = test_result::fail;
        break;
    case 2:
        tr.result = test_result::ex;
        break;
    case 3:
        tr.result = test_result::warn;
        break;
    case 4:
        tr.result = test_result::term;
        break;
    default:
        throw std::logic_error("operator >> : bad result_type");
    }

    is.ignore(1); // space
    std::getline(is,tr.message);
    tr.message = unescape(tr.message);
    if (!is.good())
    {
        throw std::logic_error("malformed test result");
    }
}
};

/**
 * Restartable test runner wrapper.
 */
class restartable_wrapper
{
    test_runner& runner_;
    callback* callback_;

    std::string dir_;
    std::string log_; // log file: last test being executed
    std::string jrn_; // journal file: results of all executed tests

public:
    /**
     * Default constructor.
     * @param dir Directory where to search/put log and journal files
     */
    restartable_wrapper(const std::string& dir = ".")
        : runner_(runner.get()), 
          callback_(0), 
          dir_(dir)
    {
        // dozen: it works, but it would be better to use system path separator
        jrn_ = dir_ + '/' + "journal.tut";
        log_ = dir_ + '/' + "log.tut";
    }

    /**
     * Stores another group for getting by name.
     */
    void register_group(const std::string& name, group_base* gr)
    {
        runner_.register_group(name,gr);
    }

    /**
     * Stores callback object.
     */
    void set_callback(callback* cb)
    {
        callback_ = cb;
    }

    /**
     * Returns callback object.
     */
    callback& get_callback() const
    {
        return runner_.get_callback();
    }

    /**
     * Returns list of known test groups.
     */
    groupnames list_groups() const
    {
        return runner_.list_groups();
    }

    /**
     * Runs all tests in all groups.
     */
    void run_tests() const
    {
        // where last run was failed
        std::string fail_group;
        int fail_test;
        read_log_(fail_group,fail_test);
        bool fail_group_reached = (fail_group == "");

        // iterate over groups
        tut::groupnames gn = list_groups();
        tut::groupnames::const_iterator gni,gne;
        gni = gn.begin();
        gne = gn.end();
        while (gni != gne)
        {
            // skip all groups before one that failed
            if (!fail_group_reached)
            {
                if (*gni != fail_group)
                {
                    ++gni;
                    continue;
                }
                fail_group_reached = true;
            }

            // first or restarted run
            int test = (*gni == fail_group && fail_test >= 0) ? fail_test + 1 : 1;
            while(true)
            {
                // last executed test pos
                register_execution_(*gni,test);

                try
                {
                    tut::test_result tr = runner_.run_test(*gni,test);
                    register_test_(tr);
                }
                catch (const tut::beyond_last_test&)
                {
                    break;
                }
                catch(const tut::no_such_test&)
                {
                    // it's ok
                }

                ++test;
            }

            ++gni;
        }

        // show final results to user
        invoke_callback_();

        // truncate files as mark of successful finish
        truncate_();
    }

private:
    /**
     * Shows results from journal file.
     */
    void invoke_callback_() const
    {
        runner_.set_callback(callback_);
        runner_.get_callback().run_started();

        std::string current_group;
        std::ifstream ijournal(jrn_.c_str());
        while (ijournal.good())
        {
            // read next test result
            try
            {
                tut::test_result tr;
                util::deserialize(ijournal,tr);
                runner_.get_callback().test_completed(tr);
            }
            catch (const no_more_tests&)
            {
                break;
            }
        }

        runner_.get_callback().run_completed();
    }

    /**
     * Register test into journal.
     */
    void register_test_(const test_result& tr) const
    {
        std::ofstream ojournal(jrn_.c_str(), std::ios::app);
        util::serialize(ojournal, tr);
        ojournal << std::flush;
        if (!ojournal.good())
        {
            throw std::runtime_error("unable to register test result in file "
                + jrn_);
        }
    }

    /**
     * Mark the fact test going to be executed
     */
    void register_execution_(const std::string& grp, int test) const
    {
        // last executed test pos
        std::ofstream olog(log_.c_str());
        olog << util::escape(grp) << std::endl << test << std::endl << std::flush;
        if (!olog.good())
        {
            throw std::runtime_error("unable to register execution in file "
                + log_);
        }
    }

    /**
     * Truncate tests.
     */
    void truncate_() const
    {
        std::ofstream olog(log_.c_str());
        std::ofstream ojournal(jrn_.c_str());
    }

    /**
     * Read log file
     */
    void read_log_(std::string& fail_group, int& fail_test) const
    {
        // read failure point, if any
        std::ifstream ilog(log_.c_str());
        std::getline(ilog,fail_group);
        fail_group = util::unescape(fail_group);
        ilog >> fail_test;
        if (!ilog.good())
        {
            fail_group = "";
            fail_test = -1;
            truncate_();
        }
        else
        {
            // test was terminated...
            tut::test_result tr(fail_group, fail_test, "", tut::test_result::term);
            register_test_(tr);
        }
    }
};

}

#endif

