import coral
import coral.util.search.query as Q
import coral.util.search.util as U

XMIDI=coral.lang.XMIDI20

# MODELCOPY_DONT_CREATE_NEW_UUIDS = 1, 
# MODELCOPY_DONT_ATTACH_TO_PARENTS = 2,
# MODELCOPY_DONT_SKIP_PYOBJECTS = 4, 
# MODELCOPY_DONT_KEEP_ASSOCIATIONS_OUTSIDE_SUBTREE = 32768,

def swap_key_value(dict):
    ret={}
    for k,v in dict.items():
        ret[v]=k
    return ret

def transform(model, lhs, rhs):
    rhs2lhs_map=intersection(rhs, lhs) # RHS /\ LHS
    rhs_to_add=minus(rhs, lhs) # RHS \ LHS

    # The next two might not have to be separate
    # 
    lhs_to_del=minus(lhs, rhs) # LHS \ RHS
    element_propname_items=slot_diff(swap_key_value(rhs2lhs_map)) # Check for slot differences

    deleted_elements=coral.Set()

    matches=Q.search(U.get_all_parts(lhs), model)
    #print len(matches), "matches"
    for match in matches:
        #for k,v in match.items():
        #    print k,"->",v

        ### REMOVE Associations from slots that have changed
        for k,items in element_propname_items.items():
            (lhs_elem,prop_name)=k
            if not match.has_key(lhs_elem):
                continue
            real_elem=match[lhs_elem]
            slot=real_elem.__getSlotByName__(prop_name)
            for remove_item in items:
                slot.drop(match[remove_item])

        ### DELETE Elements that should be deleted, 
        for del_item in lhs_to_del:
            # TODO: remove associations that have changed
            #print "Removing",match[del_item]
            if match[del_item] in deleted_elements:
                print "This element was already removed\n **",match[del_item],"**"
            else:
                deleted_elements.append(match[del_item])
                match[del_item].__parentSlot__().resetSlot()
                
        # 3. Glue copy of R\L (to match[L /\ R])

        ### ADD Associations to slots in elements that should be connected
        copy2orig={}
        rhs_to_add_copy=coral.ModelCopy(rhs_to_add,0,{},copy2orig)

        for new_elem in rhs_to_add_copy:
            original=copy2orig[new_elem]
            if original and original.__parent__() in rhs2lhs_map:
                p=match[rhs2lhs_map[original.__parent__()]]
                new_elem.__parentSlot__().insert(p)
            else:
                new_elem.__parentSlot__().insert(model) # Move element from RHS to model
                # print "Added ",new_elem
            re_connect(new_elem, match, rhs2lhs_map)


def slot_diff(lhs2rhs_map):
    #Check all association property slots if they have changed.
    element_propname_items={}
    for l_elem, r_elem in lhs2rhs_map.items():
        for prop in l_elem.__properties__():
            if prop.isAnonymous():
                continue
            if not prop.isAssociation():
                continue
            l_slot=l_elem.__getSlotByName__(prop.getName())
            r_slot=r_elem.__getSlotByName__(prop.getName())
            diff=slot_diff2(l_slot, r_slot, lhs2rhs_map)
            if diff:
                #print diff
                element_propname_items[(l_elem,prop.getName())]=diff
                
    return element_propname_items

def slot_diff2(slot1, slot2, l2rmap):
    removed_items=[]
    for l_item in slot1:
        if not l_item in l2rmap: # Like 1.A[C] on paper
            continue
        if not l2rmap[l_item] in slot2: # l_item does not exist in rhs:s slot
            removed_items.append(l_item)

    return removed_items
        
def re_connect(new_elem, match, rhs2lhs_map):
    # This needs to be done recursively
    if isinstance(new_elem, coral.ElementPrimitive):
        return

    for prop in new_elem.__properties__():
        if prop.isAnonymous():
            continue
        slot=new_elem.__getSlotByName__(prop.getName())
        if prop.isAssociation():
            for item in slot[:]:
                if item in rhs2lhs_map:
                    slot.remove(item)
                    slot.insert(match[rhs2lhs_map[item]])
        else:
            for item in slot:
                re_connect(item, match, rhs2lhs_map)


def intersection(lhs, rhs, common={}):
    # Elements that exist in both
    
    for lhs_elem in lhs:
        if isinstance(lhs_elem, coral.ElementPrimitive):
            continue
        exists=False
        for rhs_elem in rhs:

            if lhs_elem.name == rhs_elem.name:
                intersect_contained(lhs_elem, rhs_elem, common)
                exists=True
                break

        if exists:
            common[lhs_elem]=rhs_elem
            
    return common

def intersect_contained(lhs_elem, rhs_elem, common):
    for prop in lhs_elem.__properties__():
        if prop.isAssociation():
            continue
        lhs=lhs_elem.__getSlotByName__(prop.getName())
        rhs=rhs_elem.__getSlotByName__(prop.getName())
        intersection(lhs,rhs,common)    
    
def minus(lhs, rhs):
    # Elements that exist in LHS but not in RHS
    diff=coral.Set()
    
    for lhs_elem in lhs:
        if isinstance(lhs_elem, coral.ElementPrimitive):
            continue
        exists=False
        for rhs_elem in rhs:

            if lhs_elem.name == rhs_elem.name:
                exists=True
                diff.unionWith(minus_contained(lhs_elem, rhs_elem))
                break

        if not exists:
            diff.append(lhs_elem)
            
    return diff

def minus_contained(lhs_elem, rhs_elem):
    diff=coral.Set()
    for prop in lhs_elem.__properties__():
        if prop.isAssociation():
            continue
        lhs=lhs_elem.__getSlotByName__(prop.getName())
        rhs=rhs_elem.__getSlotByName__(prop.getName())
        diff.unionWith(minus(lhs,rhs))

    return diff

