package basics.application;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import basics.filesystem.file.LineReader;
import basics.testing.Test;
import basics.unexpected.Failure;
import basics.utl.SysUtl;

/**
 * This class binds classed either for dependency injection or for a named retrieval. Binding can be
 * defined in a property file: bind java.util.ArrayList // binds a list implementation date
 * java.util.Date // binds a name 'date' to a class. Also, bindings can be done programmatically.
 * Use: Binder.register(<class>); // to bind an interface implementation Binder.register("name",
 * <class>); // to bind to a name Rebinding is possible. New Auto-Bindings will outdate older
 * interface bindings. Note, that the entries are not beeing deleted from the list. The createNew(<class>)
 * method simply selects the last entry. Bindings to a name, however, will be overwritten, if a new
 * binding is definded for the same name. Definitions a read from the file that the constant
 * {@link BINDINGS} points to. The location of the file can be set a system property and depends on
 * two settings: basedir and libdir.
 * @author kst
 */
public class Binder
{
private static String         basedirString    = null;
private static String         libdirString     = null;
private File                  basedir          = null;
private String                libdir           = null;
private File                  configfolder     = null;
// Binder as singleton:
private static Binder         instance         = null;
public static final String    BINDINGS         = "binding.properties";
private List<Class<?>>        autoboundclasses = Collections
                                                  .synchronizedList(new ArrayList<Class<?>>());
private Map<String, Class<?>> namedclasses     = new HashMap<String, Class<?>>();

private Binder()
{
   SysUtl.trace("Creating binder, basedirString %",
      (basedirString == null ? "NULL" : basedirString));
}

/**
 * Use setup to define the basedir programmatically. I.E. from an init servlet. Must be called
 * before first reading access to the binder.
 * @param basedir absolute path to basedir
 * @return
 */
public static synchronized String setup(String basedir)
{
   return setup(basedir, null);
}

/**
 * Use setup to define the basedir programmatically. I.E. from an init servlet. Must be called
 * before first reading access to the binder.
 * @param basedir absolute path to basedir
 * @param libdir relative to basedir
 * @return
 */
public static synchronized String setup(String basedir, String libdir)
{
   if(instance != null) return instance.basedirString;
   basedirString = basedir;
   if(libdir != null) libdirString = libdir;
   return basedirString;
}

private static synchronized Binder instance()
{
   if(instance == null) instance = initBinder();
   return instance;
}

private static Binder initBinder()
{
   Binder binder = new Binder();
   if(basedirString == null)
   {
      basedirString = System.getProperty("basedir", null);
      if(basedirString == null)
      {
         if (!System.getProperty("os.name").toUpperCase(). startsWith("WINDOWS")) {
        	 basedirString = "./";
         } else {
        	 basedirString = "";
         }
         SysUtl.trace("Basedir was not set, using current folder: %", basedirString);
      }
      else SysUtl.trace("Got basedir from System Properties: %", basedirString);
   }
   binder.basedir = new File(basedirString);
   SysUtl.trace("Absolute path to basedir is: % ", binder.basedir.getAbsolutePath());
   if(libdirString == null)
   {
      libdirString = System.getProperty("libdir", null);
      if(libdirString == null)
      {
         libdirString = "/lib/";
         SysUtl.trace("Libdir was not set, using default folder: %", libdirString);
      }
      else SysUtl.trace("Got libdir from System Properties: %", libdirString);
   }
   binder.libdir = libdirString;
   ////////////////////////////////
   if(!binder.basedir.exists() || !binder.basedir.canRead()) throw new Failure(
      "Invalid basedir: %", binder.basedir.getAbsolutePath());
   binder.configfolder = new File(binder.basedir, binder.libdir);
   if(!binder.configfolder.exists() || !binder.configfolder.canRead()) throw new Failure(
      "Invalid config folder: %", binder.configfolder.getAbsolutePath());
   SysUtl.trace("basedir:  " + binder.basedir.getAbsolutePath());
   SysUtl.trace("configs: " + binder.configfolder.getAbsolutePath());
   File file = new File(binder.configfolder, BINDINGS);
   if(!file.exists() || !file.canRead())
   {
      SysUtl.terror(null, "No bindings found. Config folder (%),  filename (%).",
         binder.configfolder.getAbsolutePath(), BINDINGS);
   }
   else
   {
      SysUtl.trace("Reading bindings from " + file.getAbsolutePath());
      int count = 0;
      LineReader r = new LineReader(file);
      if(r.open())
      {
         while(r.hasLine())
         {
            String line = r.nextLineTrimNotNull();
            if(line.length() > 0 && !line.startsWith("#"))
            {
               String [] def = line.split(" ");
               if(def[0].equals("bind"))
               {
                  SysUtl.trace("binding " + def[1]);
                  binder.registerInternal(def[1]);
               }
               else
               {
                  SysUtl.trace("binding " + def[1] + " to name " + def[0]);
                  binder.registerInternal(def[0], def[1]);
               }
               count++;
            }
         }
      }
      if(count == 0) SysUtl.terror(null, "No bindings found in: " + file.getAbsolutePath());
   }
   return binder;
}

/**
 * Returns the basedir of the installation. This is normally the current folder ("./"), but can be
 * changed to a user defined value by setting a System property "basedir".
 * @return
 */
public static File getBasedir()
{
   return instance().basedir;
}

/**
 * This is used to give other object a means to load properties. Properties are read from a config
 * folder which normally is the folder where the jar is (lib).
 * @param fileId
 * @return
 */
public static Properties loadProperties(String fileId)
{
   File file = new File(instance().configfolder, fileId);
   if(!file.exists() || !file.canRead())
   {
      SysUtl.terror(null, "Properties not found. Config folder (" + instance().configfolder
         + "),  filename (" + fileId + ").");
      return new Properties();
   }
   return new Config(file).getProperties();
}

public static File configFile(String fileId)
{
   return new File(instance().configfolder, fileId);
}

public static File basedirFile(String fileId)
{
   return new File(instance().basedir, fileId);
}

@SuppressWarnings("unchecked")
public static void register(String key, String className)
{
   try
   {
      register(key, Class.forName(className));
   }
   catch(Exception e)
   {
      throw new Failure(e, "Class " + className);
   }
}

@SuppressWarnings("unchecked")
public static void register(String key, Class c)
{
   if(c == null) throw new Failure("Attempting to register class Null.");
   instance().registerInternal(key, c);
}

@SuppressWarnings("unchecked")
public static void register(String className)
{
   try
   {
      register(Class.forName(className));
   }
   catch(Exception e)
   {
      throw new Failure(e, "Can't register, unknown class " + className);
   }
}

@SuppressWarnings("unchecked")
public static void register(Class clazz)
{
   if(clazz == null) throw new Failure("Attempting to register class Null.");
   try
   {
      instance().registerInternal(clazz);
   }
   catch(Exception e)
   {
      throw new Failure(e, "Class " + clazz.getName());
   }
}

@SuppressWarnings("unchecked")
private void registerInternal(String key, String className)
{
   try
   {
      registerInternal(key, Class.forName(className));
   }
   catch(Exception e)
   {
      throw new Failure(e, "Can't register, unknown class " + className);
   }
}

/**
 * Bind a name to a class. Overwrite previous entries.
 * @param key
 * @param c
 */
@SuppressWarnings("unchecked")
private void registerInternal(String key, Class c)
{
   if(c == null) throw new Failure("Attempt to register class Null.");
   namedclasses.put(key, c);
}

@SuppressWarnings("unchecked")
private void registerInternal(String className)
{
   try
   {
      Class c = Class.forName(className);
      if(c == null) throw new Failure("Class " + className + " not found.");
      registerInternal(c);
   }
   catch(Exception e)
   {
      throw new Failure(e, "Registering failed: " + className);
   }
}

/**
 * Register a clazz. Prevents double entries. Append to list.
 * @param clazz
 */
@SuppressWarnings("unchecked")
private void registerInternal(Class clazz)
{
   if(clazz == null) throw new Failure("Attempt to register class Null.");
   try
   {
      for(Class current : autoboundclasses)
         if(clazz.equals(current)) return;
      autoboundclasses.add(clazz);
   }
   catch(Exception e)
   {
      SysUtl.terror(e, clazz.getName());
      throw new Failure(e, "Class " + clazz.getName());
   }
}

// ---------------------------------------------------
/**
 * Create a bean of class {@link c}.
 */
@SuppressWarnings("unchecked")
public static Object createNew(Class clazz) throws Failure
{
   boolean notfound = true;
   Object object = null;
   for(Class current : instance().autoboundclasses)
   {
      if(clazz.isAssignableFrom(current))
      {
         try
         {
            Object test = current.newInstance();
            if(test != null)
            {
               object = test;
               notfound = notfound ? false : notfound;
            }
         }
         catch(InstantiationException e)
         {
            e.printStackTrace();
            throw new Failure(e);
         }
         catch(IllegalAccessException e)
         {
            e.printStackTrace();
            throw new Failure(e);
         }
         break;
      }
   }
   if(notfound) SysUtl.terror(null, "No class bound to interface " + clazz.getName());
   return object;
}

/**
 * Try to create an bean, don't throw an exception but fail silently and return null.
 * @param name
 * @return
 */
public static Object createNewFailSilently(Class<?> clazz) throws Failure
{
   Object object = null;
   try
   {
      boolean notfound = true;
      for(Class<?> current : instance().autoboundclasses)
      {
         if(clazz.isAssignableFrom(current))
         {
            try
            {
               Object test = current.newInstance();
               if(test != null)
               {
                  object = test;
                  notfound = notfound ? false : notfound;
               }
            }
            catch(InstantiationException e)
            {
            }
            catch(IllegalAccessException e)
            {
            }
            break;
         }
      }
   }
   catch(Throwable e)
   {
   }
   return object;
}

@SuppressWarnings("unchecked")
public static Object createNew(String name) throws Failure
{
   Class<?> c = instance().namedclasses.get(name);
   if(c == null) throw new Failure("Binder.instance: No class bound to name '" + name + "'");
   Object object = null;
   try
   {
      object = c.newInstance();
   }
   catch(Exception e)
   {
      throw new Failure(e, "Can't create object of class " + name);
   }
   return object;
}

/**
 * Try to create an bean, don't throw an exception but fail silently and return null.
 * @param name
 * @return
 */
@SuppressWarnings("unchecked")
public static Object createNewFailSilently(String name)
{
   Object object = null;
   try
   {
      Class<?> c = instance().namedclasses.get(name);
      if(c == null) return object;
      try
      {
         object = c.newInstance();
      }
      catch(Exception e)
      {
      }
   }
   catch(Throwable e)
   {
   }
   return object;
}

public static Iterator<String> bindingNamesIterator()
{
   return instance().namedclasses.keySet().iterator();
}

public static Iterator<Class<?>> autoBoundClassesIterator()
{
   return new Iterator<Class<?>>()
   {
      final Iterator<Class<?>> iter = instance().autoboundclasses.iterator();

      public boolean hasNext()
      {
         return iter.hasNext();
      }

      public Class<?> next()
      {
         return iter.next();
      }

      public void remove()
      {
      }
   };
}

public static void unittest()
{
   Binder.register(java.util.ArrayList.class);
   Binder.register("date", java.util.Date.class);
   List<?> list = (List<?>)Binder.createNew(java.util.List.class);
   Test.assertNotNull(list, "List impl not loaded.");
   java.util.Date date = (java.util.Date)Binder.createNew("date");
   Test.assertNotNull(date, "util.Date impl not loaded.");
   java.lang.String string = (java.lang.String)Binder.createNewFailSilently("string");
   if(string == null) Test.assertEqualsTrue(list.getClass(), java.util.ArrayList.class,
      "Wrong list impl (ArrayList expected: binding.properties were NOT used!).");
   else Test.assertEqualsTrue(list.getClass(), java.util.concurrent.CopyOnWriteArrayList.class,
      "Wrong list impl (CopyOnWriteArrayList expected: binding.properties were used!).");
}
}
