package de.tu_dresden.diplom.richter_mirko_mat2628335.common.handler;

import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
import org.apache.axis.MessageContext;
import org.apache.axis.AxisFault;
import org.apache.axis.Message;
import org.apache.axis.client.Service;
import org.apache.axis.client.Call;
import org.w3c.dom.Element;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.handler.data.IFHandlingInfo;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.handler.ex.ContextManagerInstantiationException;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.context.IFCountingContext;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.context.IFContextManager;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.context.IFDigestAgreementStateInfo;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.digestAgreement.DigestAgreementRequest;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.digestAgreement.DigestAgreementResponse;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.account.AccountIdentification;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.account.Amount;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.account.IFCurrency;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.evidence.BodyEvidence;
import de.tu_dresden.diplom.richter_mirko_mat2628335.common.NS;
import de.tu_dresden.diplom.richter_mirko_mat2628335.Configuration;

import javax.xml.namespace.QName;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.ServiceException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.StringWriter;

import com.ibm.dom.util.Digest;

/**
 * 
 */
public class DigestAgreementRequester extends BaseHandler {

   private static final Logger logger = Logger.getLogger(DigestAgreementRequester.class);

   public static final String OPTION_FORK = "fork";
   public static final String OPTION_CONF_FILE = "confFile";

   public static final String OPTION_SLEEP_TIME = "sleepTime";
   public static final String OPTION_MAX_SLEEP_TIME = "maxSleepTime";

   public static final String OPTION_CONTEXT_METHOD = "contextMethod";
   public static final String OPTION_CONTEXT_FACTORY_CLASS = "contextFactoryClass";
   public static final String OPTION_CONTEXT_FACTORY_METHOD = "contextFactoryMethod";

   private int sleepTime = 100;
   private int maxSleepTime = 3000;

   /**
    * @see org.apache.axis.Handler#invoke(org.apache.axis.MessageContext)
    */
   public void invoke(MessageContext messageContext) throws AxisFault {
      String fn = "[invoke] ";
      Configuration conf = new Configuration(Configuration.APPLICATION_MODULES_BASE);
      IFContextManager contextManager = determineContextManager();
      if (contextManager == null) {
         throw new ContextManagerInstantiationException("Couldn't determine the ContextManager-Implementation");
      }
      String logInfo = (String) getOption(OPTION_LOG_INFO);
      if (logInfo != null) {
         //fn = fn + " -> " + logInfo + "::";
         MDC.put(OPTION_LOG_INFO, logInfo);
      }
      if (logger.isDebugEnabled()) logger.debug(fn + " --> DIGESTAGREEMENTREQUESTER");
      String sleepTimeStr = (String) getOption(OPTION_SLEEP_TIME);
      if (sleepTimeStr != null) {
         try {
            sleepTime = Integer.parseInt(sleepTimeStr);
         } catch (NumberFormatException e) {
            logger.error(fn + "Error while parsing parameter '" + OPTION_SLEEP_TIME + "' into int ... using default '" + sleepTime + "'!");
         }
      } else {
         if (logger.isInfoEnabled()) logger.info(fn + "No option '" + OPTION_SLEEP_TIME + "' given... using default -> '" + sleepTime + "'");
      }
      String maxSleepTimeStr = (String) getOption(OPTION_MAX_SLEEP_TIME);
      if (maxSleepTimeStr != null) {
         try {
            maxSleepTime = Integer.parseInt(maxSleepTimeStr);
         } catch (NumberFormatException e) {
            logger.error(fn + "Error while parsing parameter '" + OPTION_MAX_SLEEP_TIME + "' into int ... using default '" + maxSleepTime + "'!");
         }
      } else {
         if (logger.isInfoEnabled()) logger.info(fn + "No option '" + OPTION_MAX_SLEEP_TIME + "' given... using default -> '" + maxSleepTime + "'");
      }
      String forkString = (String) getOption(OPTION_FORK);
      boolean fork = false;
      if (forkString.equals("true")) {
         fork = true;
      }
      String confFile = (String) getOption(OPTION_CONF_FILE);
      if (confFile == null) {
         logger.error(fn + " configuration file for digest agreement process needed!");
      } else {
         confFile = conf.resolveProperty(confFile);
         if (logger.isDebugEnabled()) logger.debug(fn + "Using configurationfile '" + confFile + "' for communication");
         IFHandlingInfo handlingInfo = getHandlingInfo();
         Message message = messageContext.getCurrentMessage();
         if (handlingInfo.getCountingContext().getStepContext().isPerformDigestAgreementInStep()) {
            if(logger.isInfoEnabled())logger.info(fn + "Performing DigestAgreement as requested in the incomming SOAP-Message!");
            if (fork) {
               if (logger.isDebugEnabled()) logger.debug(fn + " forking digest agreement request into a new Thread (asynchron)");
               new Thread(new DigestAgreementRequesterThread(handlingInfo, contextManager, messageContext, confFile)).start();
            } else {
               if (logger.isDebugEnabled()) logger.debug(fn + " performing digest agreement request in the same Thread (synchron)");
               requestDigestAgreement(handlingInfo, contextManager, messageContext, confFile);
            }
         }else{
            if(logger.isInfoEnabled())logger.info(fn + "Skipping DigestAgreement as requested in the incomming SOAP-Message");
         }
      }
      if (logger.isDebugEnabled()) logger.debug(fn + " <-- DIGESTAGREEMENTREQUESTER");
      MDC.remove(OPTION_LOG_INFO);
   }

   /**
    * Locates the IFContextManager using the Handler-Configuration in the WSDD-File for the management
    * of digest request agreements.
    *
    * @return
    */
   private IFContextManager determineContextManager() {
      final String fn = "[determineContextManager] ";
      IFContextManager result = null;
      String contextMethod = (String) getOption(OPTION_CONTEXT_METHOD);
      if (contextMethod != null && contextMethod.length() > 0) {
         if (contextMethod.equals("factory")) {
            String contextFactoryClass = (String) getOption(OPTION_CONTEXT_FACTORY_CLASS);
            if (contextFactoryClass != null && contextFactoryClass.length() > 0) {
               try {
                  Class cfClass = getClass().getClassLoader().loadClass(contextFactoryClass);
                  String contextFactoryMethod = (String) getOption(OPTION_CONTEXT_FACTORY_METHOD);
                  if (contextFactoryMethod != null && contextFactoryMethod.length() > 0) {
                     Method cfMethod = cfClass.getMethod(contextFactoryMethod, null);
                     Object cfInst = cfMethod.invoke(null, null);
                     if (cfInst instanceof IFContextManager) {
                        result = (IFContextManager) cfInst;
                     } else {
                        logger.error(fn + "created instance by contextFactory '" + contextFactoryClass + "' and method '" + contextFactoryMethod + "' is not of type IFContextManager!");
                        throw new UnsupportedOperationException("created instance by contextFactory '" + contextFactoryClass + "' and method '" + contextFactoryMethod + "' is not of type IFContextManager!");
                     }
                  } else {
                     logger.error(fn + "no contextFactoryMethod given!");
                     throw new UnsupportedOperationException("no contextFactoryMethod given!");
                  }
               } catch (ClassNotFoundException e) {
                  e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
               } catch (NoSuchMethodException e) {
                  e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
               } catch (IllegalAccessException e) {
                  e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
               } catch (InvocationTargetException e) {
                  e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
               }
            } else {
               logger.error(fn + "no contextFactoryClass given!");
               throw new UnsupportedOperationException("no contextFactoryClass given!");
            }
         } else {
            logger.error(fn + "contextMethod '" + contextMethod + "' not supported!");
            throw new UnsupportedOperationException("contextMethod '" + contextMethod + "' not supported!");
         }
      } else {
         logger.error(fn + "no ContextMethod given in the conf-file!");
      }
      return result;
   }

   public void requestDigestAgreement(IFHandlingInfo handlingInfo, IFContextManager contextManager, MessageContext messageContext, String confFile) {
      final String fn = "[requestDigestAgreement] ";
      Message message = messageContext.getCurrentMessage();
      if (handlingInfo != null && handlingInfo.isSuccess() && message != null) {
         try {
            logger.debug(fn + "requesting digest agreement from witness");
            IFCountingContext countingContext = handlingInfo.getCountingContext();
            String endPoint = countingContext.getCoordinationContext().getRegistrationServiceAddress();
            System.setProperty("axis.ClientConfigFile", confFile);
            Service service = new Service();
            QName qnameRequest = new QName("http://diplom.compago.de", "DigestAgreementRequest");
            QName qnameResponse = new QName("http://diplom.compago.de", "DigestAgreementResponse");
            Call call = (Call) service.createCall();
            call.setTargetEndpointAddress(new URL(endPoint));
            call.setOperationName("performDigestAgreement");
            call.addParameter("request", qnameRequest, ParameterMode.IN);
            call.setReturnType(qnameResponse);
            DigestAgreementRequest request = new DigestAgreementRequest();
            String digest = calculateDigest(countingContext, message);
            request.setDigest(digest);
            request.setDependency(countingContext.getStepContext().getDependency());
            request.setContextID(countingContext.getIdentification());
            request.setCurrentTs(System.currentTimeMillis());
            //request.setAgreementReference();
            //@TODO set account identification and amount dynamically
            request.setAccountIdentification(new AccountIdentification("default"));
            request.setAmount(new Amount(10, IFCurrency.CURRENCY_COUNTER_INST));
            if (messageContext.getPastPivot()) {
               if (logger.isDebugEnabled()) logger.debug(fn + "RESPONSE PHASE!");
               request.setResponsePhase();
               boolean reloop = true;
               int waiting = 0;
               while (reloop) {
                  if (waiting <= maxSleepTime) {
                     IFDigestAgreementStateInfo stateInfo = contextManager.getStateInfo4ResponsePhaseDigestAgreementRequest(countingContext.getIdentification(), countingContext.getStepContext().getNonce());
                     if (stateInfo.isSuccess()) {
                        request.setNonce(stateInfo.getReferenciationNonce());
                        if (logger.isDebugEnabled()) logger.debug(fn + "got response of digestAgreementProcess in request-phase at " + System.currentTimeMillis());
                        reloop = false;
                     } else if (stateInfo.isWaiting()) {
                        if (logger.isDebugEnabled()) logger.debug(fn + "waiting for response of digestAgreementProcess in request-phase");
                        try {
                           Thread.sleep(sleepTime);
                           waiting += sleepTime;
                        } catch (InterruptedException e) {
                           e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                        }
                     } else {
                        //@TODO do something to cancel the communication
                     }
                  } else {
                     logger.warn(fn + "Maximum time ('" + maxSleepTime + "') to wait for the digestAgreementResponse in Request-Phase has passed!");
                     reloop = false;
                     messageContext.dispose();
                  }
               }
            } else {
               if (logger.isDebugEnabled()) logger.debug(fn + "REQUEST PHASE!");
               request.setRequestPhase();
               request.setNonce(countingContext.getStepContext().getNonce());
            }
            contextManager.registerDigestAgreementStarted(countingContext.getIdentification(), request, countingContext.getStepContext().getNonce());
            DigestAgreementResponse response = (DigestAgreementResponse) call.invoke(new Object[]{request});
            if (response.isSuccess()) {
               if (logger.isDebugEnabled()) logger.debug(fn + "registering successful digestAgreementResponse '" + response + "'");
               contextManager.registerSuccessfullDigestAgreement(countingContext.getIdentification(), response, new BodyEvidence(BaseHandler.getHandlingInfo()));
            } else {
               logger.warn(fn + "digestAgreementResponse '" + response + "' wasn't successful!");
            }
         } catch (ServiceException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
         } catch (MalformedURLException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
         } catch (RemoteException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
         }
      } else {
         StringBuffer err = new StringBuffer("CHECK FOR PARAMS FAILED: ");
         if (handlingInfo == null) {
            err.append("handlingInfo was NULL!");
         } else if (!handlingInfo.isSuccess()) {
            err.append("handlingInfo was NOT SUCESSFULL!");
         } else if (message == null) {
            err.append("message was NULL!");
         }
         logger.error(fn + err);
      }
   }

   private String calculateDigest(IFCountingContext countingContext, Message message) {
      final String fn = "[calculateDigest]";
      if (logger.isDebugEnabled()) logger.debug(fn + " calculating digest ...");
      String result = null;
      try {
         Element rootElement = message.getSOAPEnvelope().getAsDocument().getDocumentElement();
         Element body = (Element) rootElement.getElementsByTagNameNS(NS.SOAP_ENVELOPE_NAMESPACE, "Body").item(0);
         byte[] digest = Digest.getDigest(MessageDigest.getInstance("SHA-1"), body);
         StringWriter sw = new StringWriter();
         Digest.printByteArray(sw, digest);
         result = sw.toString();
         if (logger.isDebugEnabled()) logger.debug(fn + " ... '" + result + "'");
      } catch (NoSuchAlgorithmException e) {
         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
      } catch (AxisFault axisFault) {
         axisFault.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
      } catch (Exception e) {
         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
      }
      return result.trim();
   }

   private class DigestAgreementRequesterThread implements Runnable {

      private String confFile = null;
      private MessageContext messageContext = null;
      private IFHandlingInfo handlingInfo = null;
      private IFContextManager contextManager = null;

      public DigestAgreementRequesterThread(IFHandlingInfo handlingInfo, IFContextManager contextManager, MessageContext messageContext, String confFile) {
         this.confFile = confFile;
         this.messageContext = messageContext;
         this.handlingInfo = handlingInfo;
         this.contextManager = contextManager;
      }

      public void run() {
         requestDigestAgreement(handlingInfo, contextManager, messageContext, confFile);
      }

   }
}
