/*
 *******************************************************************************
 * Copyright (C) 1996-2012, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package org.unicode.cldr.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R2;

/**
 * Everything that maps to the same value is part of the same equivalence class
 * @author davis
 *
 */
public class XEquivalenceMap<K,V,R> implements Iterable<Set<K>> {
    
    Map<K,Row.R2<V,Set<R>>> source_target_reasons = new HashMap<K,Row.R2<V,Set<R>>>();

    Map<V,Set<K>> target_sourceSet;
    Map<K,Set<K>> source_Set = new HashMap<K,Set<K>>(); // not really needed: could go source-target-sourceset
    
    public XEquivalenceMap() {
        this(new HashMap<V,Set<K>>());
    }
    
    public XEquivalenceMap(Map<V,Set<K>> storage) {
        target_sourceSet = storage;
    }
    
    public XEquivalenceMap clear() {
        source_target_reasons.clear();
        target_sourceSet.clear();
        source_Set.clear();
        return this;
    }
    
    public XEquivalenceMap add(K source, V target) {
        return add(source, target, null);
    }
    
    public XEquivalenceMap add(K source, V target, R reason) {
        R2<V, Set<R>> target_reasons = source_target_reasons.get(source);
        if (target_reasons == null) {
            Set<R> reasons = new HashSet<R>();
            if (reason != null) {
                reasons.add(reason);
            }
            target_reasons = Row.of(target, reasons);
            source_target_reasons.put(source, target_reasons);
        } else {
            V otherTarget = target_reasons.get0();
            Set<R> reasons = target_reasons.get1();
            if (otherTarget.equals(target)) {
                if (reason != null) {
                    reasons.add(reason);
                }
                return this;
            }
            throw new IllegalArgumentException("Same source mapping to different targets: "
                    + source + " => " + otherTarget + " & " + target);
        }

        Set<K> s = target_sourceSet.get(target);
        if (s == null) target_sourceSet.put(target, s = new HashSet<K>());
        s.add(source);
        source_Set.put(source, s);
        return this;
    }
    
    public Set<K> getEquivalences (K source) {
        Set<K> s = source_Set.get(source);
        if (s == null) return null;
        return Collections.unmodifiableSet(s);
    }
    
    public boolean areEquivalent (K source1, K source2) {
        Set<K> s = (Set) source_Set.get(source1);
        if (s == null) return false;
        return s.contains(source2);
    }
    
    public V getTarget(K source) {
        return source_target_reasons.get(source).get0();
    }
    
    public Set<R> getReasons(K source) {
        return Collections.unmodifiableSet(source_target_reasons.get(source).get1());
    }
    
    public Set<K> getSources(V target) {
        Set<K> s = target_sourceSet.get(target);
        return Collections.unmodifiableSet(s);
    }
    
    public Iterator<Set<K>> iterator() {
        return UnmodifiableIterator.from(target_sourceSet.values());
    }
    
    public int size() {
        return target_sourceSet.size();
    }
    
    public boolean isEmpty() {
        return target_sourceSet.isEmpty();
    }

    // Should be moved out on its own
    public static class UnmodifiableIterator<T> implements Iterator<T> {
        private Iterator<T> source;
        
        public static <T> UnmodifiableIterator<T> from(Iterator<T> source) {
            UnmodifiableIterator<T> result = new UnmodifiableIterator<T>();
            result.source = source;
            return result;
        }
        
        public static <T> UnmodifiableIterator<T> from(Iterable<T> source) {
            return from(source.iterator());
        }
        
        public void remove() {
            throw new UnsupportedOperationException();
        }
        
        public boolean hasNext() {
            return source.hasNext();
        }
        
        public T next() {
            return source.next();
        }
    }
}