#!/usr/bin/python
# -*- coding: latin1 -*-
#    ASDIMO: ASynchronous DIstributed MOdeling
#    Copyright (C) 2009 Thierry EXCOFFIER, Universite Claude Bernard
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#    Contact: Thierry.EXCOFFIER@bat710.univ-lyon1.fr


import os
import sys

time = 0 # For DOT display

class AnID(object):
    idents = {}
    the_type = ''
    
    def __new__(cls, ident):
        try:
            return AnID.idents[ident]
        except KeyError:
            a = AnID.idents[ident] = object.__new__(cls, ident)
            return a

    def __init__(self, ident):
        self.ident = ident

    def __str__(self):
        return '%s(%s)' % (self.__class__.__name__, repr( self.ident))

    def __cmp__(self, other):
        return cmp(self.ident, other.ident)

    def __hash__(self):
        return hash(self.ident)

    def dot(self):
        return self.the_type + repr(self.ident).replace("'","").replace("(","").replace(")","").replace(".","_").replace(" ","").replace(',','_')

class ValueID(AnID):
    the_type = 'Val_'
    pass
class SessionID(AnID):
    pass
class VersionID(AnID):
    the_type = 'Ver_'
    def new(self, sessionID):
        return VersionID(self.ident + (sessionID,))
    def subversion_of(self, other):
        length = len(self.ident)
        if length >= len(other.ident):
            return False
        if length == 0:
            return True
        return self.ident == other.ident[0:length]


def remove_subversions_of(set, to_remove):
    version_list = list(set)
    for versionID in set:
        for versionID2 in to_remove:
            if versionID.subversion_of(versionID2):
                version_list.remove(versionID)
                break
    return version_list
    
def remove_old_versions(version_list):
    return remove_subversions_of(version_list, version_list)



class Session:
    nr = 0
    
    def __init__(self, ident=None, values=None):
        if ident == None:
            self.ident = "hostname+time+PID+%d" % Session.nr
            Session.nr += 1
        else:
            self.ident = ident
        self.nr_created = 0
        if values == None:
            values = {}
        self.values = values

    def add_parents(self, valueID, data):
        for c_valueID, c_versionID in data.dict_of_versions.items():
            parents = self.values[c_valueID].versions[c_versionID].parents
            if valueID in parents:
                parents[valueID] += 1
            else:
                parents[valueID] = 1

    def new_value(self, data):
        self.nr_created += 1
        valueID = ValueID((self.ident, self.nr_created))
        versionID = VersionID(())
        versions = dict(((versionID, data),))
        v = self.values[valueID] = Value(versions, versionID)

        # Find current version
        for c_valueID, c_versionID in data.dict_of_versions.items():
            if c_versionID != None:
                continue
            data.dict_of_versions[c_valueID] = \
                                      self.values[c_valueID].current_versionID
        # Reverse pointers
        self.add_parents(valueID, data)

        return valueID

    def display_html(self):
        write_file('xxx.html', str(self))
        os.system('links -dump xxx.html')

    def change_value(self, valueID, data, versionID=None, remove=True):
        # self.check()
        self.values[valueID].set(self, data, valueID, versionID)
        self.add_parents(valueID, data)
        
        if remove:
            self.remove_unused()
        self.check()

    def get(self, valueID, versionID=None):
        return self.values[valueID].get(versionID)

    def get_data(self, valueID, versionID=None):
        return self.values[valueID].get_data(self, versionID)

    def update_version(self):
        """
        On veut pouvoir choisir la version d'une valeur
        et diffuser ce changement partout en restant cohérent.
        """

        for value in self.values.values():
            value.computed_version = False

        # Fixed version because not upgradable
        for valueID, value in self.values.items():
            if value.has_parents():
                continue
            if value.computed_version:
                continue
            if value.upgradable_to() == value.current_versionID:
                value.set_version_to(self, valueID, value.upgradable_to())

        for valueID, value in self.values.items():
            if value.has_parents():
                continue
            if value.computed_version:
                continue
            
            value.set_version_to(self, valueID, value.upgradable_to())

    def mark_needed(self):
        for value in self.values.values():
            for versionID, data in value.versions.items():
                data.needed = False
        for valueID, value in self.values.items():
                value.mark_needed(self, valueID)

    def update_with(self, session):
        self.check()

        #print 'UPDATE';self.display_html();print 'WITH';session.display_html()
        for value in session.values.values():
            value.other_current_versionID = None
        add_parents_to = []
        for valueID, value in session.values.items():
            if valueID in self.values:
                add_parents_to += self.values[valueID].update_with(valueID,
                                                                   value)
            else:
                self.values[valueID] = value.copy()
                for versionID in self.values[valueID].versions:
                    add_parents_to.append((valueID, versionID))
            self.values[valueID].other_current_versionID = value.current_versionID
        #print 'AFTER MERGE';self.display_html()
        for valueID, versionID in add_parents_to:
            # print valueID, versionID
            value = self.values[valueID]
            self.add_parents(valueID, value.versions[versionID])

        #print 'AFTER ADD PARENT';self.display_html()
        self.update_version()
        #print 'AFTER FORCE VERSION';self.display_html()
        self.remove_unused()
        #print 'AFTER REMOVE UNUSED';self.display_html()
        self.check()

    def synchronize(self, other):
        #print 'S1<=S2 ' * 80
        self.update_with(other)
        #print 'S2<=S1 ' * 80
        other.update_with(self)

    def check(self):
        for v in self.values.values():
            v.check(self)

    def remove_unused(self):
        self.mark_needed()
        j = 0
        i = True
        while i:
            i = 0
            for valueID, value in self.values.items():
                i += value.remove_unused(self, valueID)
            j += i
        return j

    def content(self):
        k = self.values.keys()
        k.sort()
        return [(valueID, self.values[valueID]) for valueID in k]
    
    def __str__(self):
        t = ['%s %s\n<ul>' % (self.__class__.__name__, self.ident)]
        for valueID, value in self.content():
            t.append( '%s: %s' % (valueID, value) )
        return '%s\n</ul>' % ('\n<li>'.join(t))

    def dot(self, old_time):
        t = ['digraph %s {' % self.ident,
             'node[];',
             'edge[];',
             'graph[charset="Latin1"];',
             ]
        for valueID, value in self.content():
            t.append('subgraph cluster_%s {' % valueID.dot())
            t.append('label="%s" ;' % valueID.dot())
            t.append('color=black ;')
            
            for versionID, data in value.content():
                current = versionID == value.current_versionID
                t.append('%s__%s [ %s ] ;' % (
                    valueID.dot(), versionID.dot(),
                    data.dot(versionID, current, old_time),
                    ))
            t.append('}')

        for valueID, value in self.content():
            for versionID, data in value.content():
                t.append(data.edges('%s__%s' % (valueID.dot(),
                                               versionID.dot())))

        t.append('}')
        return '\n'.join(t)

class Data(object):
    dict_of_versions = {}
    needed_because = 'Is used ?'
    def __init__(self):
        self.parents = {}
        self._time_ = time
    def check(self, session):
        pass
    def copy(self):
        raise ValueError('virtual copy')
    def dot(self, versionID, current, old_time):
        s = 'label="%s: %s"' % (self.title(versionID), self.data)
        if current:
            s += ',style=filled,fillcolor=green'
        if self._time_ >= old_time:
            s += ',pencolor=blue,penwidth=6'
        return s
    def edges(self, frome):
        return ''
    def title(self, versionID):
        return versionID.dot()
        return versionID.dot() + '(' + self.needed_because + ')'
    def __str__(self):
        s = self.str()
        # s += '<p>%s' % id(self)
        if self.parents:
            # s += '(%s)' % id(self.parents)
            s += '<br>Parents = ' + \
                 ', '.join(['%s:%d' % (valueID,nr)
                            for valueID,nr in self.parents.items()])
        return s

class DataSimple(Data):
    def __init__(self, data):
        self.data = data
        super(self.__class__,self).__init__()
    def get(self, session):
        return self.data
    def copy(self):
        return DataSimple(self.data)
    def str(self):
        return '%s(%s)' % (self.__class__.__name__, repr( self.data))

class DataDeleted(Data):
    data = 'DELETED'
    def __init__(self):
        super(self.__class__,self).__init__()
    def copy(self):
        return DataDeleted()
    def __str__(self):
        return '%s()' % (self.__class__.__name__,)
        
class DataList(Data):
    def __init__(self, *list_of_valueID):
        self.list_of_valueID = list_of_valueID
        super(self.__class__,self).__init__()
    def str(self):
        return '%s<ul><li>%s</ul>' % (self.__class__.__name__, '<li>'.join([
            str(i) for i in self.list_of_valueID]))
    def get(self, session):
        return [session.get(i).get(session) for i in self.list_of_valueID]
    def check(self, session):
        for i in self.list_of_valueID:
            assert(i in session.values)
    def copy(self):
        return DataList(*self.list_of_valueID)
    def dot(self, versionID, current, old_time):
        s = ['label=<<TABLE']
        if current:
            s.append(' BGCOLOR="green"')
        if self._time_ >= old_time:
            s.append(' BORDER="6"')
        s.append('><TR><TD BORDER="1">%s</TD></TR>' % self.title(versionID))
        for i in self.list_of_valueID:
            s.append('<TR><TD BGCOLOR="white" BORDER="1">' + i.dot() + '</TD></TR>')
        s.append('</TABLE>>, shape=none')
        return ''.join(s)
    def edges(self, frome):
        t = []
        for i in self.list_of_valueID:
            t.append('%s -> cluster_%s' % ( frome, i.dot() ) )
        return '\n'.join(t)

class DataListVersion(Data):
    def __init__(self, *list_of_valueID):
        d = {}
        for valueID in list_of_valueID:
            d[valueID] = None
        self.dict_of_versions = d
        super(self.__class__,self).__init__()

    def content(self):
        v = self.dict_of_versions.keys()
        v.sort()
        return [(valueID, self.dict_of_versions[valueID])
                for valueID in v]
    def str(self):
        return '%s<ul><li>%s</ul>' % (self.__class__.__name__, '<li>'.join([
            '%s:%s' % (valueID, versionID)
            for valueID, versionID in self.content()]))
    def get(self, session):
        return [session.get(valueID, versionID).get(session)
                for valueID, versionID in self.content()]
    def check(self, session):
        for valueID, versionID in self.dict_of_versions.items():
            if valueID not in session.values:
                print valueID, versionID
                raise ValueError('Unknown valueID')
            if versionID not in session.values[valueID].versions:
                print valueID, versionID
                raise ValueError('Unknown versionID')
            
    def copy(self):
        c = DataListVersion()
        c.dict_of_versions = dict(self.dict_of_versions)
        return c

    def dot(self, versionID, current, old_time):
        s = ['label=<<TABLE']
        if current:
            s.append(' BGCOLOR="green"')
        if self._time_ >= old_time:
            s.append(' BORDER="6"')
        s.append('><TR><TD COLSPAN="2" BORDER="1">%s</TD></TR>' % self.title(versionID))
        for valueID, versionID in self.content():
            s.append('<TR><TD BORDER="1" BGCOLOR="white">%s</TD><TD BORDER="1" BGCOLOR="white">%s</TD></TR>' % (
                valueID.dot(), versionID.dot()))
        s.append('</TABLE>>, shape=none')
        return ''.join(s)

    def edges(self, frome):
        t = []
        for valueID, versionID in self.dict_of_versions.items():
            t.append('%s -> %s__%s' % ( frome,
                                        valueID.dot(), versionID.dot() ) )
        return '\n'.join(t)

class Value:
    def __init__(self, versions, current_versionID):
        self.versions = versions
        self.current_versionID = current_versionID

    def copy(self):
        v = {}
        for versionID, data in self.versions.items():
            v[versionID] = data.copy()
        return Value(v, self.current_versionID)
        
    def set(self, session, data, valueID, versionID=None):
        if not versionID:
            versionID = self.current_versionID
            
        old_data = self.versions[versionID]
        new_versionID = versionID.new(session.ident)
        self.current_versionID = new_versionID
        self.versions[new_versionID] = data
        data.parents = {}
        
        # My parents are DataListVersion
        for parent_valueID in old_data.parents:
            parent_value = session.get(parent_valueID).copy()
            parent_value.dict_of_versions[valueID] = new_versionID
            session.change_value(parent_valueID, parent_value, remove=False)

    def get_data(self, session, versionID=None):
        return self.get(versionID).get(session)
    
    def get(self, versionID=None):
        if not versionID:
            versionID = self.current_versionID
        return self.versions[versionID]

    def remove_unused(self, session, my_valueID):
        to_remove = []
        for versionID, data in self.versions.items():
            if data.parents:
                continue
            if data.needed:
                continue
            to_remove.append(versionID)
        for versionID in to_remove:
            data = self.versions[versionID]
            for d_valueID, d_versionID in data.dict_of_versions.items():
                child_data = session.values[d_valueID].versions[d_versionID]
                child_data.parents[my_valueID] -= 1
                if child_data.parents[my_valueID] == 0:
                    del child_data.parents[my_valueID]

            del self.versions[versionID]
        return len(to_remove)
            
        
    def update_with(self, valueID, value):
        add_parent_to = []
        for versionID in value.versions.keys():
            if versionID in self.versions:
                continue
            # Add its version to ours even if it is older
            # Because it may be used in a versionned list
            self.versions[versionID] = value.versions[versionID].copy()
            add_parent_to.append((valueID, versionID))
        return add_parent_to

    def upgradable_to(self):
        t = []
        for versionID in self.versions.keys():
            if self.current_versionID.subversion_of(versionID):
                t.append(versionID)            
        if len(t) == 1:
            return t[0]
        if len(t) == 0:
            return self.current_versionID
        # Multiple choices
        if self.other_current_versionID in t:
            return self.other_current_versionID
        raise ValueError('Multiple choice 2')

    def mark_needed_version(self, session, valueID, versionID, needed_because):
        data = self.versions[versionID]
        if data.needed:
            return
        data.needed_because = needed_because
        data.needed = True

        for valueID2, versionID2 in data.dict_of_versions.items():
            session.values[valueID2].mark_needed_version(session,valueID2,
                                                         versionID2,
                                                         'Child of ' + str(valueID))
            
        for valueID2 in data.parents:
            value = session.values[valueID2]
            t = []
            for versionID2, data in value.versions.items():
                if data.dict_of_versions.get(valueID) == versionID:
                    t.append(versionID2)
            tt = remove_subversions_of(t, value.versions)
            if tt:
                t = tt
            # t = remove_old_versions(t)
            for versionID2 in t:
                value.mark_needed_version(session, valueID2, versionID2,
                                          'Parent of ' + str(valueID))

    def mark_needed(self, session, valueID):
        self.mark_needed_version(session, valueID, self.current_versionID, 'Current Version')
        for versionID in remove_old_versions(self.versions):
            self.mark_needed_version(session, valueID, versionID,
                                     'No better version')

    def parents(self):
        return self.versions[self.current_versionID].parents

    def has_parents(self):
        return len(self.parents()) != 0

    def set_version_to(self, session, valueID, versionID):
        if self.computed_version:
            assert(self.current_versionID == versionID)
            return
        
        self.computed_version = True
        
        old_versionID = self.current_versionID
        self.current_versionID = versionID
        for valueID2, versionID2 in self.versions[versionID].dict_of_versions.items():
            session.values[valueID2].set_version_to(session,
                                                    valueID2,  versionID2)

        for valueID2 in self.versions[old_versionID].parents:
            parent = session.values[valueID2]
            t = []
            for versionID2, value in parent.versions.items():
                if (valueID in value.dict_of_versions
                    and value.dict_of_versions[valueID] == versionID):
                    t.append(versionID2)
            if t:
                if parent.computed_version:
                    if parent.current_versionID in t:
                        continue
                    raise ValueError('Conflicting parent version')
                if parent.upgradable_to() in t:
                    parent.set_version_to(session, valueID2,
                                          parent.upgradable_to() )
                    continue
                if len(t) != 1:
                    t = remove_old_versions(t)
                    if len(t) != 1:
                        print 'Choix aléatoire'
                        print 't:', ' '.join(str(tt) for tt in t)
                        print parent.other_current_versionID
                        print valueID, 'parent:', valueID2
                        print ['%s ' % x for x in t]
                        t = [ max(t) ]
                    
                parent.set_version_to(session, valueID2, t[0])
            else:
                raise ValueError('No new version')

    def content(self):
        versions = self.versions.keys()
        versions.sort()
        return [(versionID, self.versions[versionID])
                for versionID in versions]

    def check(self, session):
        assert(self.current_versionID in self.versions)
        for v2 in self.versions.values():
            v2.check(session)

    def __str__(self):
        t = ['Current=%s<ul>' % self.current_versionID] + \
            ['%s: %s'%(versionID, data) for versionID, data in self.content()]
        return '%s\n</ul>' % '\n<li>'.join(t)


def read_file(filename):
    f = open(filename, 'r')
    c = f.read()
    f.close()
    return c

def write_file(filename, content):
    f = open(filename, 'w')
    f.write(content)
    f.close()

def read_yes():
    try:
        return read_yes.ret
    except AttributeError:
        pass
    
    print '\n' + '='*79
    print 'Store the current version ?'
    a = sys.stdin.readline()
    if a.strip() in ('y', 'yes', 'o', 'oui'):
        return True
    if a.strip().lower() in ('y', 'yes', 'o', 'oui'):
        read_yes.ret = True
        return True
    if a.strip() in ('N', 'NO', 'NON'):
        read_yes.ret = False
        return False
    return False

class Tester:
    def __init__(self, filename, n=2):
        self.n = n
        self.s = []
        for i in range(n):
            self.s.append(Session('S%d' % i))
        self.filename = filename

    def check_ok(self, string, filename, session=None):
        fn = os.path.join('DUMPS','%s.%s' % (self.filename, filename))
        print fn
        sys.stdout.flush()
        if os.path.exists(fn):
            content = read_file(fn)
            if content == string:
                return True

            write_file('xxx_current.html', string)
            write_file('xxx_expected.html', content)
            os.system('''
            links -dump xxx_current.html >xxx_current.txt
            links -dump xxx_expected.html >xxx_expected.txt
            diff --unified=10 xxx_expected.txt xxx_current.txt
            ''')
            if session:
                display(session.dot(time), 'xxx')
                os.system('display xxx.png &')
            if read_yes():
                write_file(fn, string)
                return True
            else:
                return False
        write_file('xxx_current.html', string)
        os.system('links -dump xxx_current.html')
        if read_yes():
            write_file(fn, string)
            return True
        else:
            return False

    def checkpoint(self, name):
        for i in range(self.n):
            self.check_ok( str(self.s[i]), 's%d.%s' % (i, name), self.s[i])

    def check_data(self, name, valueID):
        for i in range(self.n):
            self.check_ok( repr(self.s[i].get_data(valueID)),
                           's%d.%s' % (i, name) )


def display(dot, base_filename):
    f = open(base_filename + '.dot', 'w')
    f.write(dot)
    f.close()
    os.system('fdp -Tpng %s.dot -o%s.png' % (base_filename, base_filename))


t = Tester('create_simple')
t.s[0].new_value(DataSimple('A_VALUE'))
t.checkpoint('in_S0')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')

t = Tester('create_simple_modify')
v = t.s[0].new_value(DataSimple('A_VALUE'))
t.checkpoint('in_S0')
t.s[0].change_value(v, DataSimple('NEW_VALUE') )
t.checkpoint('modify_in_S0')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')

t = Tester('propagate_simple')
v = t.s[0].new_value(DataSimple('A_VALUE'))
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.s[0].change_value(v, DataSimple('NEW_VALUE') )
t.checkpoint('modify_in_S0')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')

t = Tester('conflict')
v = t.s[0].new_value(DataSimple('A_VALUE'))
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.s[0].change_value(v, DataSimple('VALUE ONE') )
t.s[1].change_value(v, DataSimple('VALUE TWO') )
t.checkpoint('modify_in_S0_S1')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.s[0].change_value(v, DataSimple('VALUE ONE.ONE') )
t.s[1].change_value(v, DataSimple('VALUE TWO.TWO') )
t.checkpoint('modify_in_S0_S1_2')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync2')

t = Tester('list')
v = t.s[0].new_value(DataSimple('v'))
w = t.s[0].new_value(DataSimple('w'))
x = t.s[0].new_value( DataList(v, w) )
t.checkpoint('init')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('x', x)


t = Tester('list_no_conflict')
v = t.s[0].new_value(DataSimple('v'))
w = t.s[0].new_value(DataSimple('w'))
x = t.s[0].new_value( DataList(v, w) )
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.s[0].change_value(v, DataSimple('vvvvvv') )
t.s[1].change_value(w, DataSimple('wwwwww') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('x', x)

t = Tester('list_conflict')
v = t.s[0].new_value(DataSimple('v'))
w = t.s[0].new_value(DataSimple('w'))
x = t.s[0].new_value( DataList(v, w) )
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.s[0].change_value(v, DataSimple('v11111') )
t.s[1].change_value(v, DataSimple('v22222') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('x', x)

t = Tester('list_version')
v = t.s[0].new_value(DataSimple('v'))
w = t.s[0].new_value(DataSimple('w'))
x = t.s[0].new_value( DataListVersion(v, w) )
t.checkpoint('init')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.s[0].change_value(v, DataSimple('v11111') )
t.checkpoint('change_value')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync2')
t.check_data('x', x)

t = Tester('list_version_conflict')
v = t.s[0].new_value(DataSimple('v'))
w = t.s[0].new_value(DataSimple('w'))
x = t.s[0].new_value( DataListVersion(v, w) )
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.s[0].change_value(v, DataSimple('v11111') )
t.s[1].change_value(w, DataSimple('w11111') )
t.checkpoint('change')
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('x', x)
t.s[0].change_value(w, DataSimple('w22222') )
t.s[1].change_value(v, DataSimple('v22222') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync2')
t.check_data('x2', x)

t = Tester('list_version_conflict_same')
v = t.s[0].new_value(DataSimple('v'))
w = t.s[0].new_value(DataSimple('w'))
x = t.s[0].new_value( DataListVersion(v, w) )
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.s[0].change_value(v, DataSimple('S0 modif v') )
t.s[1].change_value(v, DataSimple('S1 modif v') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')

t = Tester('intersection')
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
c = t.s[0].new_value(DataSimple('c'))
x = t.s[0].new_value( DataListVersion(a, b) )
y = t.s[0].new_value( DataListVersion(b, c) )
z = t.s[0].new_value( DataList(x, y) )
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.check_data('z', z)
t.s[0].change_value(b, DataSimple('b PAR S0') )
t.s[0].synchronize(t.s[1])
t.checkpoint('simple_change')
t.check_data('z2', z)
t.s[0].change_value(b, DataSimple('b PAR S0 VERSION DEUX') )
t.s[1].change_value(a, DataSimple('a PAR S1') )
t.checkpoint('conflict_change')
t.check_data('z3', z)
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('z4', z)

t = Tester('intersection_externe')
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
c = t.s[0].new_value(DataSimple('c'))
x = t.s[0].new_value( DataListVersion(a, b) )
y = t.s[0].new_value( DataListVersion(b, c) )
z = t.s[0].new_value( DataList(x, y) )
t.s[0].synchronize(t.s[1])
t.s[0].change_value(a, DataSimple('S0 MODIF a') )
t.s[1].change_value(c, DataSimple('S1 MODIF c') )
t.checkpoint('init')
t.check_data('z', z)
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('z2', z)

t = Tester('pair_pair')
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
c = t.s[0].new_value(DataSimple('c'))
d = t.s[0].new_value(DataSimple('d'))
x = t.s[0].new_value( DataListVersion(a, b) )
y = t.s[0].new_value( DataListVersion(c, d) )
z = t.s[0].new_value( DataListVersion(x, y) )
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.check_data('z', z)
t.s[0].change_value(a, DataSimple('S0 MODIF a') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('z2', z)
t.s[0].change_value(b, DataSimple('S0 MODIF b') )
t.s[1].change_value(c, DataSimple('S1 MODIF c') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync_conflict')
t.check_data('z3', z)

t = Tester('triplet')
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
c = t.s[0].new_value(DataSimple('c'))
x = t.s[0].new_value( DataListVersion(a, b,c) )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.s[0].change_value(a, DataSimple('S0 MODIF a') )
t.s[1].change_value(b, DataSimple('S1 MODIF b') )
t.s[0].synchronize(t.s[1])
t.checkpoint('conflict')
t.check_data('x', x)
t.s[0].change_value(b, DataSimple('S0 MODIF b') )
t.s[1].change_value(a, DataSimple('S1 MODIF a') )
t.s[0].synchronize(t.s[1])
t.checkpoint('conflict2')
t.check_data('x2', x)
t.s[0].change_value(c, DataSimple('S0 MODIF c') )
t.s[0].synchronize(t.s[1])
t.checkpoint('simplechange')
t.check_data('x3', x)


t = Tester('3sessions', n=3)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
c = t.s[0].new_value(DataSimple('c'))
x = t.s[0].new_value( DataListVersion(a, b) )
y = t.s[0].new_value( DataListVersion(b, c) )
z = t.s[0].new_value( DataList(x, y) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.s[0].synchronize(t.s[1])
t.checkpoint('init')
t.check_data('z', z)
t.s[0].change_value(b, DataSimple('b PAR S0') )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.s[0].synchronize(t.s[1])
t.checkpoint('simple_change')
t.check_data('z2', z)
t.s[0].change_value(b, DataSimple('b PAR S0 VERSION DEUX') )
t.s[2].change_value(a, DataSimple('a PAR S2') )
t.checkpoint('conflict_change')
t.check_data('z3', z)
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.check_data('z4', z)

t = Tester('3sessions_choix', n=3)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('sync')

# Same one except that the last sync is not with the same session
# So the update is not the same.
t = Tester('3sessions_choix2', n=3)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[0].synchronize(t.s[1])
t.s[0].synchronize(t.s[2]) # Sync with S0 and not S1
t.checkpoint('sync')

t = Tester('4sessions_choix', n=4)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.s[2].synchronize(t.s[3])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[0].synchronize(t.s[1])
t.s[0].synchronize(t.s[2])
t.s[2].synchronize(t.s[3])
t.checkpoint('sync')


t = Tester('4sessions_choix2', n=4)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.s[2].synchronize(t.s[3])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.s[2].synchronize(t.s[3])
t.checkpoint('sync')

t = Tester('3sessions_2paires', n=3)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
c = t.s[0].new_value(DataSimple('c'))
y = t.s[0].new_value( DataListVersion(b, c) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('sync')

t = Tester('3sessions_2paires_conflit', n=3)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
c = t.s[0].new_value(DataSimple('c'))
y = t.s[0].new_value( DataListVersion(b, c) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[2].change_value(c, DataSimple('c PAR S2') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.s[0].synchronize(t.s[2])
t.checkpoint('sync2')
t.s[1].synchronize(t.s[2])
t.checkpoint('sync3')

t = Tester('3sessions_2paires_conflit2', n=3)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
c = t.s[0].new_value(DataSimple('c'))
y = t.s[0].new_value( DataListVersion(b, c) )
t.s[0].synchronize(t.s[1])
t.s[1].synchronize(t.s[2])
t.checkpoint('init')
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(b, DataSimple('b PAR S1') )
t.s[2].change_value(c, DataSimple('c PAR S2') )
t.s[0].synchronize(t.s[1])
t.checkpoint('sync')
t.s[1].synchronize(t.s[2])
t.checkpoint('sync2')
t.s[0].synchronize(t.s[2])
t.checkpoint('sync3')

t = Tester('4sessions_2paires_conflit', n=4)
a = t.s[0].new_value(DataSimple('a'))
b = t.s[0].new_value(DataSimple('b'))
x = t.s[0].new_value( DataListVersion(a, b) )
c = t.s[0].new_value(DataSimple('c'))
y = t.s[0].new_value( DataListVersion(b, c) )
t.s[0].synchronize(t.s[1])
t.s[0].synchronize(t.s[2])
t.s[0].synchronize(t.s[3])
t.s[0].change_value(a, DataSimple('a PAR S0') )
t.s[1].change_value(a, DataSimple('a PAR S1') )
t.s[2].change_value(b, DataSimple('b PAR S2') )
t.s[3].change_value(c, DataSimple('c PAR S3') )
t.s[0].synchronize(t.s[2])
t.s[1].synchronize(t.s[2])
t.s[0].synchronize(t.s[2]) # S1 vers S0
t.checkpoint('sync')
t.s[2].synchronize(t.s[3])
t.s[1].synchronize(t.s[3])
t.s[0].synchronize(t.s[3])
t.checkpoint('sync2')


# A full example

def affiche(f, message, a=None, b=None, big=False):
    global time
    try:
        affiche.nr += 1
    except AttributeError:
        affiche.nr = 0
    
    display(thierry.dot(time), 'xxx_thierry_%d' % affiche.nr)
    display(eliane.dot(time), 'xxx_eliane_%d' % affiche.nr)

    f.write('<tr><th colspan="2">' + message + "</th></tr>\n")
    if a and b:
        f.write('<tr><td>' + a + '<td>' + b + '</tr>')
    if big:
        colspan = ' colspan="2"'
        tr = '</tr><tr>'
    else:
        colspan = ''
        tr = ''
    f.write('<tr><td%s><img style="width:100%%" src="xxx_thierry_%d.png">%s<td%s><img src="xxx_eliane_%d.png" style="width:100%%"></tr>\n' %
            (colspan, affiche.nr, tr, colspan, affiche.nr)
            )
    time += 1



f = open('xxx.html', 'w')
f.write('<table border width="100%">')





thierry = Session('thierry')
fac = thierry.new_value(DataSimple('FAC'))
tel = thierry.new_value(DataSimple('Téléphone'))
telfac = thierry.new_value(DataSimple("04 72 44 80 00"))
lien = thierry.new_value(DataListVersion(fac, tel, telfac))

eliane  = Session('eliane')
mon_tel = eliane.new_value(DataSimple('04 72 44 XXXX'))
mon_bur = eliane.new_value(DataSimple('Bur. 222'))
moi = eliane.new_value(DataList(mon_tel, mon_bur))

affiche(f, "Initialisation",
        "Thierry: Une liste cohérente",
        "Éliane: Une liste simple")

thierry.synchronize(eliane)
affiche(f, "Synchronisation")

eliane.change_value(mon_bur, DataSimple('Bureau 222'))
thierry.change_value(mon_bur, DataSimple('Bur. 333'))
affiche(f, "Changements conflictuels sur une donnée",
        "Corrige le numéro du bureau",
        "Remplace 'Bur' par 'Bureau'")

thierry.synchronize(eliane)
affiche(f, "Synchronisation")


ordi = thierry.new_value(DataSimple('Ordi: b710ppp'))
thierry.change_value(moi, DataList(mon_tel, mon_bur, ordi))
eliane.change_value(mon_tel, DataSimple('04 72 44 99 99') )
affiche(f, "Changements non conflictuels",
        "Ajoute l'ordi à la liste d'Éliane",
        "Corrige son numéro de téléphone"
        )

thierry.synchronize(eliane)
affiche(f, "Synchronisation")

thierry.change_value(tel, DataSimple('Téléphone Standard'))
eliane.change_value(telfac, DataSimple('04 72 43 13 38'))
affiche(f, "Changements dans liste cohérente",
        "'Téléphone' devient 'Téléphone Standard'",
        "Remplace le numéro du standard par le numéro perso"
        )

thierry.synchronize(eliane)
affiche(f, "Synchronisation", big=True)








thierry = Session('thierry')
eliane  = Session('eliane')
a = thierry.new_value(DataSimple('A'))
b = thierry.new_value(DataSimple('B'))
c = thierry.new_value(DataSimple('C'))
d = thierry.new_value(DataListVersion(a,b))
e = thierry.new_value(DataListVersion(b,c))
thierry.synchronize(eliane)
affiche(f, "On repart sur un autre exemple")

eliane.change_value(a, DataSimple('AA'))
thierry.change_value(c, DataSimple('CC'))
affiche(f, "Changements distants", "Modifie C", "Modifie A")

thierry.synchronize(eliane)
affiche(f, "Synchronisation")


eliane.change_value(a, DataSimple('AAA'))
thierry.change_value(b, DataSimple('BBB'))
affiche(f, "Changements proches", "Modifie B", "Modifie AA")

thierry.synchronize(eliane)
affiche(f, "Synchronisation")



f.write('</table>')
f.close()
