package commons.application.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import basics.application.Binder;
import basics.profiler.AppWatcher;
import basics.profiler.Profiler;
import basics.unexpected.Failure;
import basics.unexpected.Problem;
import basics.utl.ClassUtl;
import basics.utl.SysUtl;
import commons.application.GLOBAL;
import commons.application.Application;
import commons.application.ctx.Context;
import commons.application.ctx.ContextParameterChangedListener;
import commons.cmd.set;
import commons.script.Processor;

/**
 * author: ks
 */
public class Engine implements EngineInterface
{
private Set<String>                  gotoTargets                 = new HashSet<String>();
protected Map<String, Command>       registry                    = Collections
                                                                    .synchronizedMap(new HashMap<String, Command>());
protected boolean                    state;
protected static final boolean       STOP                        = true;
protected static final boolean       CONT                        = false;
protected static final String        KEY_STOP                    = "stop";
protected static Map<String, String> constants                   = new HashMap<String, String>();
protected static List<String>        constantNames               = new ArrayList<String>();
ContextParameterChangedListener      ctxParameterChangedListener = null;
static Processor                     processor                   = null;

private static synchronized void instantiateProcessor(Context ctx)
{
   if(processor == null)
      processor = new Processor(ctx, constantNames, constants);
}

/**
 * trigger a service by calling its name. Note: this catches all throwables, creates a new Context
 * if the context was lost, and puts the error into the (old or new) context.
 * @param ctx - containing state information
 * @param call - related to service in registry, can contain an arg0 (no key)
 */
public/*synchronized?*/void call(String input, final Context ctx,
   Collection<Callable<Void>> tasks, final String docid)
{
   instantiateProcessor(ctx);
   processor.process(input, tasks, docid);
}
public/*synchronized?*/void callBack(final String cmd, final Context ctx,
   Collection<Callable<Void>> tasks, final String docid)
{
   try
   {
      if(tasks == null) // single thread
         lookupAndExecute(cmd, ctx, docid);
      else
      // multi threaded
      {
         tasks.add(new Callable<Void>()
         {
            public Void call()
            {
               try
               {
                  lookupAndExecute(cmd, ctx, docid);
               }
               catch(Problem e)
               {
                  e.printStackTrace();
                  throw new Failure(e);
               }
               return null;
            }
         });
      }
   }
   catch(Throwable p)
   {
      ctx.addError(p.getMessage());
   }
   return;
}

private void lookupAndExecute(String cmd, Context ctx, String docid) throws Problem
{
   Command command = lookup(cmd);
   try
   {
      command.setDocId(docid);
      execute(command, ctx);
   }
   catch(Throwable e)
   {
      e.printStackTrace();
      String reason = e.getMessage();
      reason = reason == null ? "" : ": " + reason;
      throw new Problem("ERR(" + command.getSignature() + ")" + reason);
   }
}

private void execute(Command command, Context ctx) throws Throwable
{
   command.setContext(ctx);
   command.setupDebug();
   command.init(); // initialize or reset the service
   if(!preProcessCheck(command, ctx))
      return;
   Profiler.resume(command.getId());
   AppWatcher.step(command.getId());
   command.process();
   Profiler.increment(command.getId());
   Profiler.stop(command.getId());
   //   SysUtl.tracesp("%", command.getId(), Profiler.report(command.getId()));
   if(command.isDebugLevel(1) && ctx.getr(GLOBAL.CTX_KEY_TRACE_PROFILER, true))
      SysUtl.trace(Profiler.report());
   if(ctx.env().get(KEY_STOP, false))
   {
      state = STOP;
      ctx.env().remove(KEY_STOP);
      command = null;
      return;
   }
   postProcessCheck(command, ctx);
   command = null;
}

public boolean isRegistered(String id)
{
   return registry.containsKey(id);
}

protected void register(Command service)
{
   if(service == null)
      throw new Failure("Service must not be null when registering.");
   service.engine = this;
   String keyLow = service.getClass().getSimpleName();
   service.setId(keyLow);
   registry.put(keyLow, service);
}

protected void register(String id, Command service)
{
   if(service == null)
      throw new Failure("Service '" + id + "' is null.");
   service.engine = this;
   String keyLow = id.trim();
   service.setId(keyLow);
   registry.put(keyLow, service);
}

public List<String> getList()
{
   List<String> list = new ArrayList<String>();
   for(String id : registry.keySet())
      list.add(id);
   Collections.sort(list);
   return list;
}

public List<String> getList(String prefix)
{
   prefix = prefix.toLowerCase();
   List<String> newlist = new ArrayList<String>();
   for(String id : registry.keySet())
      if(id.startsWith(prefix))
         newlist.add(id);
   return newlist;
}

public Command lookup(String id) throws Problem
{
   //   SysUtl.tracesp(">>> % %", id, ((registry.get(id.trim()) == null) ? " UNKNWON" : " REGISTERED"));
   Command service = registry.get(id.trim());
   if(service == null)
   {
      service = createService(id);
      if(service == null)
      {
         // create a service out of a binding set at run time in the context:
         // The run time binding is usually done in script with the command:
         // bind <name> <package-and-service-class>
         service = (Command)Binder.createNew(id);
         if(service != null)
         {
            service.engine = this;
            service.signContract();
            //            register(id, service);
         }
         else
         {
            throw new Problem("Unknown service-id(" + id
               + "), use 'dir' to check available services.");
         }
      }
   }
   //   else
   //      SysUtl.tracesp("Reusing %: %", id, ObjectUtl.getId(service));
   return service;
}

@SuppressWarnings("unchecked")
private Command createService(String serviceName)
{
   String p = ClassUtl.getPackage(set.class);
   String cname = p + "." + serviceName;
   try
   {
      Class c = Class.forName(cname);
      Command s = (Command)c.newInstance();
      s.engine = this;
      return s;
   }
   catch(Exception e)
   {
      //      OsUtl.error("Can't create service: '" + cname + "'", e);
      return null;
   }
}

protected boolean preProcessCheck(Command service, Context ctx)
{
   if(service.contract != null && !service.contract.preconditionsFullfilled(ctx))
   {
      ctx.addError(service.getSignature() + ": precondition failed, key '"
         + service.contract.getKeyThatBrokeTheContract() + "'.");
      state = STOP;
      return false;
   }
   return true;
}

protected boolean postProcessCheck(Command service, Context ctx)
{
   if(service.contract != null && !service.contract.postconditionsFullfilled(ctx))
   {
      ctx.addError(service.getSignature() + ": postcondition failed, key '"
         + service.contract.getKeyThatBrokeTheContract() + "'.");
      state = STOP;
      return false;
   }
   return true;
}

public boolean isStopped()
{
   return state == STOP;
}

/**
 * Enable CMD to stop skipping (when user is entering a new cmd).
 */
public void resume()
{
   gotoTargets.clear();
   state = CONT;
}

public void contextParamsChanged()
{
   try
   {
      if(ctxParameterChangedListener != null)
         ctxParameterChangedListener.fireCtxHasChanged(Application.ctx().cloneCurrent());
   }
   catch(Throwable t)
   {
      t.printStackTrace();
   }
}

public void registerContextParameterChangedListener(ContextParameterChangedListener e)
{
   SysUtl.trace(this + " registers " + e);
   ctxParameterChangedListener = e;
}

public String constantsToString()
{
   StringBuilder s = new StringBuilder();
   for(String name : constantNames)
   {
      s.append(name);
      s.append(":");
      s.append(constants.get(name));
      s.append(";");
   }
   return s.toString();
}

public String toString()
{
   return this.getClass().getName();
}
}
