package commons.application.ctx;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import basics.application.Binder;
import basics.collections.TypedValueMap;
import basics.unexpected.Failure;
import basics.unexpected.Problem;
import basics.utl.Empty;
import basics.utl.FormatterUtl;
import basics.utl.SUtl;

/**
 * 
 * @author ks
 */
public class Context
{
private static final long     serialVersionUID     = 1L;
protected List<Object>        uniqueAutoBoundBeans = null;
protected List<Object>        errors               = null;
protected String              ctxname              = "root";
protected TypedValueMap       environment          = null;
protected TypedValueMap       map                  = null;
protected Context             parent               = null;
//protected String              resultValue        = "";
protected List<String>        aliasNames           = new ArrayList<String>();
protected Map<String, String> aliasses             = new HashMap<String, String>();
private String                lastError            = null;
public static final String    CTX_ENVKEY_DEBUG     = "DEBUG";

public Context()
{
   map = new TypedValueMap();
}

private Context(String name)
{
   this.ctxname = name;
   map = new TypedValueMap();
}

public TypedValueMap env()
{
   Context ctx = getRoot();
   if(ctx.environment == null)
      ctx.environment = new TypedValueMap();
   return ctx.environment;
}

public boolean isRoot()
{
   return parent == null;
}

public Context getRoot()
{
   Context ctx = this;
   while(ctx.getParent() != null)
   {
      ctx = ctx.getParent();
   }
   return ctx;
}

public void setContextName(String name)
{
   this.ctxname = name;
}

public List<Object> getAutoBoundBeans()
{
   Context ctx = getRoot();
   if(ctx.uniqueAutoBoundBeans == null)
      uniqueAutoBoundBeans = new ArrayList<Object>();
   return ctx.uniqueAutoBoundBeans;
}

/**
 * The objects stored as autobound beans behold their state!
 * With other words, two calls return the same object in one session.
 * @param c
 * @return
 */
@SuppressWarnings("unchecked")
public Object getOrCreateAutoBoundBean(Class c)
{
   Context ctx = getRoot();
   if(ctx.uniqueAutoBoundBeans == null)
   {
      uniqueAutoBoundBeans = new ArrayList<Object>();
   }
   // try to find an appropriate object in the context...
   for(Object b : ctx.uniqueAutoBoundBeans)
      if(c.isInstance(b))
         return b;
   // try to find a class that can be used...
   Object object = Binder.createNew(c);
   if(object == null)
      throw new Failure("Can't resolve dependency, no implementation found for " + c.getName());
   ctx.uniqueAutoBoundBeans.add(object);
   return object;
}

public int envSize()
{
   TypedValueMap env = env();
   return env == null ? 0 : env.size();
}

public List<Object> getErrors()
{
   Context ctx = getRoot();
   if(ctx.errors == null)
      ctx.errors = new ArrayList<Object>();
   return ctx.errors;
}

public void clearErrors()
{
   Context ctx = getRoot();
   ctx.getErrors().clear();
}

/**
 * Retrieves map from session or create one in the session. If no session available, then an empty
 * new map is returned.
 */
public boolean containsKey(String key)
{
   return map.containsKey(key);
}

public boolean containsAlias(String key)
{
   return getRoot().aliasses.containsKey(key);
}

public boolean containsKeyInHierarchy(String key)
{
   return rf(key).map.containsKey(key);
}

/**
 * remove keys in keyList, matter if key is a alias or a key
 * @param key
 */
public Context removeAll(String keyList)
{
   String [] keys = SUtl.split(keyList, ",");
   List<String> delete = new ArrayList<String>();
   Context r = getRoot();
   for(String k : keys)
   {
      for(String a : r.aliasNames)
      {
         if(k.equalsIgnoreCase(r.aliasses.get(a)))
         {
            if(!delete.contains(a))
               delete.add(a);
         }
      }
      map.remove(k);
   }
   for(String a : delete)
   {
      r.aliasses.remove(a);
      r.aliasNames.remove(a);
   }
   return this;
}

/**
 * remove key, no matter if it is a alias or a key
 * @param key
 */
public void remove(String key)
{
   List<String> delete = new ArrayList<String>();
   Context r = getRoot();
   for(String a : r.aliasNames)
   {
      if(key.equalsIgnoreCase(r.aliasses.get(a)))
      {
         if(!delete.contains(a))
            delete.add(a);
      }
   }
   map.remove(key);
   for(String a : delete)
   {
      r.aliasses.remove(a);
      r.aliasNames.remove(a);
   }
}

/**
 * removes keys and alias that point to the keys
 * @param key
 */
public void removeInHierarchy(String key)
{
   List<String> delete = new ArrayList<String>();
   Context r = getRoot();
   while(containsKeyInHierarchy(key))
   {
      for(String a : r.aliasNames)
      {
         if(key.equalsIgnoreCase(r.aliasses.get(a)))
         {
            if(!delete.contains(a))
               delete.add(a);
         }
      }
      rf(key).map.remove(key);
   }
   for(String a : delete)
   {
      r.aliasses.remove(a);
      r.aliasNames.remove(a);
   }
}

// ------ recursive access, go through ctx hierarchy ----------
/**
 * deep read, scan hierarchy, use aliases.
 * if not found, set the value (using the key not the alias).
 */
public Object getOrSet(String key, Object object)
{
   String akey = returnKeyOrAlias(key);
   Object obj = rf(akey).map.get(akey);
   if(obj != null)
      return obj;
   put(key, object);
   return object;
}

/**
 * deep read, scan hierarchy, use aliases
 */
public Object getr(String key)
{
   key = returnKeyOrAlias((String)key);
   return rf((String)key).map.get(key);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public String getr(String key, String dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public Object getr(String key, Object dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public int getr(String key, int dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public long getr(String key, long dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public boolean getr(String key, boolean dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public double getr(String key, double dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public float getr(String key, float dv)
{
   key = returnKeyOrAlias((String)key);
   return rf(key).map.get(key, dv);
}

/**
 * deep read, scan hierarchy, use aliases
 */
public Context getr(String key, Context dv)
{
   key = returnKeyOrAlias((String)key);
   Context v = (Context)rf(key).map.get(returnKeyOrAlias(key), dv);
   return (v == null) ? dv : v;
}

// nicht recursive----------------------------
public void rename(String oldKey, String newKey) throws Problem
{
   if(map.containsKey(oldKey))
   {
      if(map.containsKey(newKey))
         throw new Problem("Key is in use: '" + newKey + "', remove first.");
      List<String> delete = new ArrayList<String>();
      for(String a : aliasNames)
      {
         String k = aliasses.get(a);
         if(k.equalsIgnoreCase(oldKey))
         {
            delete.add(a);
         }
      }
      for(String a : delete)
      {
         aliasses.remove(a);
         aliasNames.remove(a);
      }
      map.put(newKey, map.get(oldKey));
      map.remove(oldKey);
   }
   else
      throw new Problem("No such key: '" + oldKey + "'");
}

public void put(String key, String value)
{
   map.put(key, value);
}

public void put(String key, int value)
{
   map.put(key, value);
}

public void put(String key, boolean value)
{
   map.put(key, value);
}

public void put(String key, double value)
{
   map.put(key, value);
}

public void put(String key, float value)
{
   map.put(key, value);
}

public void put(String key, Object value)
{
   map.put(key, value);
}

public void put(String key, Context value)
{
   map.put(key, value);
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public String consume(String key, String dv)
{
   key = returnKeyOrAlias((String)key);
   String v = map.get(key, dv);
   remove(key);
   return v;
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public Object consume(String key, Object dv)
{
   key = returnKeyOrAlias((String)key);
   Object v = map.get(key, dv);
   remove(key);
   return v;
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public int consume(String key, int dv)
{
   key = returnKeyOrAlias((String)key);
   int v = map.get(key, dv);
   remove(key);
   return v;
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public boolean consume(String key, boolean dv)
{
   key = returnKeyOrAlias((String)key);
   boolean v = map.get(key, dv);
   remove(key);
   return v;
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public double consume(String key, double dv)
{
   key = returnKeyOrAlias((String)key);
   double v = map.get(key, dv);
   remove(key);
   return v;
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public float consume(String key, float dv)
{
   key = returnKeyOrAlias((String)key);
   float v = map.get(key, dv);
   remove(key);
   return v;
}

/**
 * retrieve value and delete key,
 * use aliases. but non recursive.
 */
public long consume(String key, long dv)
{
   key = returnKeyOrAlias((String)key);
   long v = map.get(key, dv);
   remove(key);
   return v;
}

public void addError(Throwable e)
{
   String m = e.getMessage();
   if(env().get(Context.CTX_ENVKEY_DEBUG, false))
      addError(FormatterUtl.stackTraceToString(e) + " [" + m + "]");
   else
      addError(m);
}

public synchronized void addError(String error)
{
   if(lastError != null && lastError.equals(error))
      return;
   lastError = error;
   getErrors().add(error);
}

@SuppressWarnings("unchecked")
public Set entrySet()
{
   return map.entrySet();
}

public Set<String> keySet()
{
   return map.keySet();
}

public Iterator<String> keyIterator()
{
   return map.keySet().iterator();
}

@SuppressWarnings("unchecked")
public void putAll(Map map)
{
   Iterator pairs = map.entrySet().iterator();
   while(pairs.hasNext())
   {
      Map.Entry pair = (Map.Entry)pairs.next();
      put((String)pair.getKey(), pair.getValue());
   }
}

@SuppressWarnings("unchecked")
public void putAll(TypedValueMap map)
{
   Iterator pairs = map.entrySet().iterator();
   while(pairs.hasNext())
   {
      Map.Entry pair = (Map.Entry)pairs.next();
      put((String)pair.getKey(), pair.getValue());
   }
}

@SuppressWarnings("unchecked")
public Collection values()
{
   return map.entrySet();
}

//public String toString ()
//{
//   StringBuilder s = new StringBuilder();
//   if (getParent() != null)
//   {
//      s.append(getParent().eigenName);
//      s.append("/");
//   }
//   s.append(eigenName.toUpperCase());
//   s.append("{");
//   if (map != null && map.keySet().size() > 0)
//   {
//      Iterator<String> iter = map.keySet().iterator();
//      while (iter.hasNext())
//      {
//         String k = (String) iter.next();
//         Object o = map.get(k);
//         if ((o instanceof Context))
//         {
//            String v = o == null ? "null" : ((Context) o).toStringFlat();
//            s.append(k.toUpperCase());
//            s.append(":");
//            s.append(v);
//         }
//         else
//         {
//            String v = o == null ? "null" : o.toString();
//            s.append(k);
//            s.append(":");
//            s.append(v);
//         }
//         s.append(";");
//      }
//   }
//   s.append("[");
//   s.append(aliassesToString());
//   return SU.removeTail(s.toString(), ";").concat("]}");
//}
public String toString()
{
   StringBuilder s = new StringBuilder();
   if(getParent() != null)
   {
      s.append(getParent().ctxname);
      s.append("/");
   }
   s.append(ctxname.toUpperCase());
   s.append("{");
   if(map != null)
   {
      Set<String> keys = map.keySet();
      Iterator<String> iter = keys.iterator();
      while(iter.hasNext())
      {
         String k = (String)iter.next();
         Object o = map.get(k);
         if(o instanceof Context)
            s.append(k.toUpperCase());
         else
            if(o instanceof TypedValueMap)
               s.append(k.toUpperCase());
            else
               if(o instanceof Map)
                  s.append(k.toUpperCase());
               else
                  if(o instanceof Collection)
                     s.append(k.toUpperCase());
                  else
                     s.append(k);
         s.append(";");
      }
   }
   return SUtl.removeSuffix(s.toString(), ";").concat("}");
}

public boolean hasErrors()
{
   return getErrors().size() > 0;
}

public Iterator<Object> errorIterator()
{
   return getErrors().iterator();
}

public String getErrorsAsString()
{
   Iterator<Object> I = errorIterator();
   List<String> unique = new ArrayList<String>();
   while(I.hasNext())
   {
      Object e = I.next();
      String msg = e == null ? "NULL" : e.toString();
      if(!unique.contains(msg))
         unique.add(msg);
   }
   return SUtl.join(unique, "; ");
}

/**
 * does not clone errors and environment!
 */
public Context cloneCurrent()
{
   Context ctx = new Context(this.ctxname);
   ctx.map = this.map.clone();
   if(!isRoot())
      ctx.parent = this.parent;
   else
   {
      ctx.environment = environment.clone();
      ctx.uniqueAutoBoundBeans = new ArrayList<Object>();
      for(Object obj : this.getAutoBoundBeans())
         ctx.uniqueAutoBoundBeans.add(obj);
      ctx.aliasses = new HashMap<String, String>();
      ctx.aliasNames = new ArrayList<String>();
      for(String name : this.aliasNames)
      {
         ctx.aliasNames.add(name);
         ctx.aliasses.put(name, ctx.aliasses.get(name));
      }
   }
   return ctx;
}

/**
 * find the first context that contains the given key in the context hierarchy. return never null
 * but an empty context.
 */
public Context rf(String key)
{
   Context ctx = this;
   while(ctx != null)
   {
      if(ctx.map.containsKey(key))
         return ctx;
      ctx = ctx.getParent();
   }
   return this; //new Context();
}

/**
 * lookup key that points to subctx of current context or environment. retrieves the subctx
 * accordingly:
 * @param key point to key store under this name
 * @return subctx - may be null
 */
public Context getChildCtxByLookup(String key) throws Problem
{
   String subkey = getr(key, (String)null);
   if(subkey == null)
      return null;
   Object object = getr(key);
   if(object == null)
      return null;
   if(!(object instanceof Context))
   {
      throw new Failure(key + " does not point to a Context, but to "
         + object.getClass().getSimpleName());
   }
   return key == null ? null : (Context)getr(key, (Context)null);
}

/**
 * access child ctx directly
 * @return subctx - is never null
 */
public Context getChildCtxByName(String key) throws Problem
{
   Object object = getr(key);
   if(object == null)
      return null;
   if(!(object instanceof Context))
   {
      throw new Problem(key + " does not point to a Context, but to "
         + object.getClass().getSimpleName());
   }
   return (Context)object;// key == null ? null : (Context) get(key,
   // (Context)null);
}

public Context getParent()
{
   return parent;
}

public void setParent(Context parent)
{
   this.parent = parent;
}

public int size()
{
   return map.size();
}

public boolean isEmpty()
{
   return map == null || map.size() < 1;
}

//public String getResultValue ()
//{
//   Context ctx = getRoot();
//   return ctx.resultValue;
//}
//
//public void setResultValue (String resultValue)
//{
//   Context ctx = getRoot();
//   ctx.resultValue = resultValue;
//}
//
//public void resetResultValue ()
//{
//   Context ctx = getRoot();
//   ctx.resultValue = "";
//}
// ---------------------------------------------
public void setAlias(String name, String value)
{
   Context ctx = getRoot();
   if(Empty.is(value))
   {
      ctx.aliasNames.remove(name);
      ctx.aliasses.remove(value);
   }
   else
   {
      ctx.aliasses.put(name, value);
      if(!ctx.aliasNames.contains(name))
         ctx.aliasNames.add(name);
   }
}

/**
 * return the input when it is NOT an alias else return the key the alias stands for.
 */
public String returnKeyOrAlias(String key)
{
   return !containsAlias(key) ? key : getRoot().aliasses.get(key);
}

public String aliassesToString()
{
   Context ctx = getRoot();
   StringBuilder s = new StringBuilder();
   for(String name : ctx.aliasNames)
   {
      s.append(name);
      s.append(":");
      s.append(ctx.aliasses.get(name));
      s.append(";");
   }
   return s.toString();
}
}
