// TestSets.cpp
//
// Author David Barrett-Lennard
// (C)opyright Cedanet Pty Ltd 2022

#include "Ceda/cxOT/SetOps.h"
#include "Ceda/cxOT/TOpid.h"
#include "Ceda/cxOT/TVectorTime.h"
#include "Ceda/cxUtils/IsaacRandom.h"
#include "Ceda/cxUtils/Tracer.h"
#include "Ceda/cxUtils/TestTimer.h"

#define TRACE_TestSets 0

using Isaac = ceda::IsaacRandomNumberGen;
using FieldType = bool;
using SiteId = int;
using TimeIndex = int;
using Opid = ceda::TOpid<SiteId,TimeIndex>;
using VectorTime = ceda::TVectorTime<SiteId,TimeIndex>;
using KeyPresentInfo = ceda::TKeyPresentInfo<SiteId,TimeIndex>;

namespace set_operations_test
{
    static FieldType PickValue(Isaac& isaac)
    {
        return isaac.GetUniformDistInteger(0,2) != 0;
    }

    struct Site
    {
        Opid opid;
        VectorTime vout;            // Summaries what operations have been applied on this site
        FieldType field = false;    // Value of the assignable field on this site
        KeyPresentInfo kpi;
    };

    ceda::xostream& operator<<(ceda::xostream& os, const Site& s)
    {
        os << "Site " << s.opid 
           << "  field=" << s.field
           << "  vout=" << s.vout 
           << "  kpi= " << s.kpi;
        return os;
    }

    struct Test
    {
        Test(int numSites);
        SiteId PickSite();
        void DoLocalOp();
        void TrySendOp(SiteId s1, SiteId s2);
        void PickDifferentSites(SiteId& s1, SiteId& s2);
        void SendOp();
        void TestConvergenceOfTwoSites();
        int NumSites() const { return (int) sites.size(); }
    
        ceda::xvector<Site> sites;      // Indexed by SiteId
        Isaac isaac;
    };

    ceda::xostream& operator<<(ceda::xostream& os, const Test& v)
    {
        for (SiteId i=0 ; i < v.sites.size() ; ++i)
        {
            os << v.sites[i] << '\n';
        }
        return os;
    }

    Test::Test(int numSites) :
        sites(numSites)
    {
        for (SiteId i=0 ; i < numSites ; ++i)
        {
            sites[i].opid.s = i;
            sites[i].opid.t = 0;
        }
    }

    SiteId Test::PickSite()
    {
        return isaac.GetUniformDistInteger(0, NumSites());
    }

    void Test::DoLocalOp()
    {
        SiteId si = PickSite();
        Site& site = sites[si];
    
        SiteId s = site.opid.s;
        TimeIndex t = site.opid.t;
    
        cxAlwaysAssert(site.vout(s) == t);
        site.vout.Add(s,t+1);

        if (site.field)
        {
            LocalDelete(site.kpi, s, t);
            site.field = false;
        }
        else
        {
            LocalInsert(site.kpi, s, t);
            site.field = true;
        }

        ++site.opid.t;
    
        #if TRACE_TestSets
            Tracer() << "\nGenerated local operation on S" << s << " : assigned value = " << site.field << '\n';
            ceda::TraceIndenter indent(4);
            Tracer() << *this;
        #endif
    }

    void Test::TrySendOp(SiteId s1, SiteId s2)
    {
        Site& site1 = sites[s1];    // receiver = site1
        Site& site2 = sites[s2];    // sender = site2

        VectorTime vCommon;
        vCommon = Intersection(site1.vout, site2.vout);

        // site1 = merge(site1, site2)
        site1.vout.UnionWith(site2.vout);
    
        KeyPresentInfo rf;
        GetRFactor(rf, site2.kpi, vCommon);

        bool changed = MergeKeyPresentInfo(site1.kpi, rf);
        if (changed)
        {
            // The merge made changes to site1.kpi, so calculate whether the key is present in the set.
            site1.field = KeyIsPresent(site1.kpi);
        }

        #if TRACE_TestSets
            Tracer() << "\nSent operation (rf=" << rf << ") from S" << s2 << " to S" << s1 << '\n';
            ceda::TraceIndenter indent(4);
            Tracer() << *this;
        #endif
    }

    void Test::PickDifferentSites(SiteId& s1, SiteId& s2)
    {
        do
        {
            s1 = PickSite();    // sender
            s2 = PickSite();    // receiver
        } while (s1 == s2);
    }

    void Test::SendOp()
    {
        SiteId s1,s2;
        PickDifferentSites(s1,s2);
        TrySendOp(s1,s2);
    }

    void Test::TestConvergenceOfTwoSites()
    {
        SiteId s1,s2;
        PickDifferentSites(s1,s2);
    
        #if TRACE_TestSets
            Tracer() << "\nVerifying sites S" << s1 << " and S" << s2 << " converge\n";
            ceda::TraceIndenter indent(4);
        #endif
        TrySendOp(s1,s2);
        TrySendOp(s2,s1);
    
        #if TRACE_TestSets
            Tracer() << "sites[s1].vout = " << sites[s1].vout << '\n';
            Tracer() << "sites[s2].vout = " << sites[s2].vout << '\n';
            Tracer() << "sites[s1].kpi = " << sites[s1].kpi << '\n';
            Tracer() << "sites[s2].kpi = " << sites[s2].kpi << '\n';
        #endif
        cxAlwaysAssert(sites[s1].vout == sites[s2].vout);
        cxAlwaysAssert(sites[s1].field == sites[s2].field);

        #if TRACE_TestSets
            Tracer() << "Converged to " << sites[s1].field << '\n';
        #endif
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // RunTest

    void RunTest(int numSites, int count2, double timeForTestInSecs)
    {
        // Need to be able to pick two different sites!
        cxAlwaysAssert(numSites >= 2);
    
        Isaac isaac;
        int numLocalOps = 0;
        int numSendOp = 0;
        int numCheckCgce = 0;

        ceda::HPTimer timeTaken;
        while(timeTaken.GetElapsedTimeInSeconds() < timeForTestInSecs)
        {
            Test test(numSites);
        
            #if TRACE_TestSets
                Tracer() << "\n------------------------------------------------------\n" << test << '\n';
            #endif
        
            for (int c2=0 ; c2 < count2 ; ++c2)
            {
                int c = isaac.GetUniformDistInteger(0,10);
            
                if (c < 4)
                {
                    test.DoLocalOp();
                    ++numLocalOps;
                }
                else if (c < 8)
                {
                    test.SendOp();
                    ++numSendOp;
                }
                else
                {
                    test.TestConvergenceOfTwoSites();
                    ++numCheckCgce;
                }
            }
        }
        Tracer() << "time=" << timeTaken.GetElapsedTimeInSeconds() << "s"
                 << "  sites=" << numSites 
                 << "  localops=" << numLocalOps 
                 << "  sendops=" << numSendOp 
                 << "  checkcgce=" << numCheckCgce 
                 << '\n';
    }
} // namespace set_operations_test

void UnitTestSetOps(double timeForTestInSecs)
{
    Tracer() << "Unit test set operations\n";
    ceda::TraceIndenter indent(4);

    for (int numSites=2 ; numSites < 10 ; ++numSites)
    {
        set_operations_test::RunTest(numSites,200,timeForTestInSecs);
    }
}

