/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.ode.bpel.engine.replayer;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.xml.namespace.QName;
import javax.xml.xquery.XQConnection;
import javax.xml.xquery.XQConstants;
import javax.xml.xquery.XQDataSource;
import javax.xml.xquery.XQPreparedExpression;
import javax.xml.xquery.XQResultSequence;

import net.sf.saxon.xqj.SaxonXQConnection;
import net.sf.saxon.xqj.SaxonXQDataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.ode.bpel.dao.ProcessDAO;
import org.apache.ode.bpel.dao.ProcessInstanceDAO;
import org.apache.ode.bpel.engine.BpelEngineImpl;
import org.apache.ode.bpel.engine.BpelProcess;
import org.apache.ode.bpel.engine.MyRoleMessageExchangeImpl;
import org.apache.ode.bpel.engine.PartnerLinkMyRoleImpl;
import org.apache.ode.bpel.engine.PartnerLinkMyRoleImpl.RoutingInfo;
import org.apache.ode.bpel.iapi.MessageExchange.Status;
import org.apache.ode.bpel.pmapi.CommunicationType;
import org.apache.ode.bpel.pmapi.ExchangeType;
import org.apache.ode.bpel.pmapi.MockQueryRequestDocument;
import org.apache.ode.bpel.pmapi.MockQueryResponseDocument;
import org.apache.ode.bpel.pmapi.ResponseType;
import org.apache.ode.bpel.pmapi.CommunicationType.Exchange;
import org.apache.ode.bpel.pmapi.CommunicationType.ServiceConfig;
import org.apache.ode.bpel.runtime.PROCESS;
import org.apache.ode.utils.DOMUtils;
import org.apache.xmlbeans.XmlAnySimpleType;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Context holding replayer state (eg. invoke answers) for single instance during replaying.
 * 
 * @author Rafal Rusin
 *
 */
public class ReplayerContext {
    private static final Logger __log = LoggerFactory.getLogger(ReplayerContext.class);

    public ReplayerScheduler scheduler;

    public BpelEngineImpl bpelEngine;
    public ReplayerBpelRuntimeContextImpl runtimeContext;
    
    public Map<QName, ServiceConfig> servicesConfig = new HashMap<QName, ServiceConfig>();
    public CommunicationType replayerConfig;

    public final Date replayStartDate;

    public Answers answers = new Answers();

    public class Answers {
        public Map<String, AnswersForKey> answersMap = new HashMap<String, AnswersForKey>();

        public String getAnswersKey(QName service, String operation) {
            return service.toString() + ";" + operation;
        }

        public void add(Exchange e) {
            ServiceConfig cfg = getServiceConfig(e.getService());
            if (cfg.getReplayType().isSetMock()) {
                String key = getAnswersKey(e.getService(), e.getOperation());
                AnswersForKey v = answersMap.get(key);
                if (v == null) {
                    v = new AnswersForKey();
                    answersMap.put(key, v);
                }
                v.answers.add(e);
            }
        }

        public AnswerResult fetchAnswer(QName service, String operation, Element outgoingMessage, Date currentEventDateTime) {
            __log.debug("fetching answer for " + service + " " + operation);
            
            ServiceConfig cfg = getServiceConfig(service);
            
            if (cfg.getReplayType().isSetMock()) {
                String key = getAnswersKey(service, operation);
                AnswersForKey v = answersMap.get(key);
                Exchange e = v == null ? null : v.answerPos < v.answers.size() ? v.answers.get(v.answerPos) : null;
                if (e == null) {
                    throw new IllegalStateException("answer for " + service + " " + operation + " at time " + currentEventDateTime + " not found, outgoing message was " + DOMUtils.domToString(outgoingMessage));
                }
                v.answerPos++;
                __log.debug("fetched " + e);
                return new AnswerResult(false, e);
            } else if (cfg.getReplayType().isSetMockQuery()) {
                return new AnswerResult(false, fetchMockQuery(service, operation, outgoingMessage, cfg));
            } else if (cfg.getReplayType().isSetLive()) {
                return new AnswerResult(true, null);
            } else assert(false);
            return null;
        }

        public void remainingExchanges(List<Exchange> e) {
            for (AnswersForKey v : answersMap.values()) {
                v.remainingExchanges(e);
            }
        }
        
        private Exchange fetchMockQuery(QName service, String operation, Element outgoingMessage, org.apache.ode.bpel.pmapi.CommunicationType.ServiceConfig serviceConfig) {
            try {
                MockQueryRequestDocument request = MockQueryRequestDocument.Factory.newInstance();
                request.addNewMockQueryRequest().addNewIn().set(XmlObject.Factory.parse(outgoingMessage));
                String xquery = serviceConfig.getReplayType().getMockQuery();
                
                XQDataSource xqds = new SaxonXQDataSource();
                XQConnection xqconn = xqds.getConnection();
    
                net.sf.saxon.Configuration configuration = ((SaxonXQConnection) xqconn).getConfiguration();
                configuration.setHostLanguage(net.sf.saxon.Configuration.XQUERY);
//                XQStaticContext staticEnv = xqconn.getStaticContext();
                XQPreparedExpression exp = xqconn.prepareExpression(xquery);
                Node requestNode = DOMUtils.parse(request.newXMLStreamReader());
                if (__log.isDebugEnabled()) {
                    __log.debug("request " + request.toString());
                }
                exp.bindItem(XQConstants.CONTEXT_ITEM, xqconn.createItemFromNode(requestNode, xqconn.createNodeType()));
                XQResultSequence result = exp.executeQuery();
                MockQueryResponseDocument response = MockQueryResponseDocument.Factory.parse(result.getSequenceAsStream());
                {
                    XmlOptions opts = new XmlOptions();
                    List<Object> errors = new ArrayList<Object>();
                    opts.setErrorListener(errors);
                    if (!response.validate(opts)) {
                        __log.error("MockQuery response doesn't validate. Errors: " + errors + " Request: " + request.toString() + " Response: " + response.toString(), new Exception());
                        throw new IllegalStateException("MockQuery response doesn't validate.");
                    }
                }
                ResponseType response2 = response.getMockQueryResponse();
                
                if (__log.isDebugEnabled()) {
                    __log.debug("mockQuery result " + response);
                }
                
                
                Exchange answer = Exchange.Factory.newInstance();
                {
                    if (response2.isSetOut()) {
                        answer.setOut(response2.getOut());
                    }
                    if (response2.isSetFault()) {
                        answer.setFault(response2.getFault());
                    }
                    if (response2.isSetFailure()) {
                        answer.setFailure(response2.getFailure());
                    }
                }
                
                return answer;
            } catch (Exception e) {
                __log.error("", e);
                throw new IllegalStateException(e.getMessage());
            }
        }
    }

    public static class AnswersForKey {
        List<Exchange> answers = new ArrayList<Exchange>();
        int answerPos = 0;

        public boolean isCompleted() {
            return !(answerPos < answers.size());
        }

        public void remainingExchanges(List<Exchange> e) {
            for (int i = answerPos; i < answers.size(); i++) {
                e.add(answers.get(i));
            }
        }

        @Override
        public String toString() {
            return new Integer(answerPos).toString() + " / " + answers.size();
        }
    }

    private void scheduleInvoke(final Exchange e, final MyRoleMessageExchangeImpl mex) {
        final Date time = e.getCreateTime().getTime();
        scheduler.scheduleReplayerJob(new Callable<Void>() {
            public Void call() throws Exception {
                __log.debug("call " + e);
                mex.getDAO().setStatus(Status.ASYNC.toString());
                runtimeContext.handleIncomingRequest(mex, time);
                return null;
            }
        }, time, runtimeContext);
    }

    public void init(final CommunicationType r, ReplayerScheduler scheduler) throws Exception {
        this.scheduler = scheduler;
        
        replayerConfig = r;
        
        for (ServiceConfig s : r.getServiceConfigArray()) {
            servicesConfig.put(s.getService(), s);
        }
        
        final Exchange[] exchanges = r.getExchangeArray();

        for (Exchange e : exchanges) {
            // We skip failures, because INVOKE_CHECK job is not handled by replayer
            if (e.getType() == ExchangeType.P && !e.isSetFailure()) {
                answers.add(e);
            }
        }

        {
            final Exchange e = exchanges[0];

            final Date time = e.getCreateTime().getTime();
            scheduler.scheduleReplayerJob(new Callable<Void>() {
                public Void call() throws Exception {
                    __log.debug("initial call " + e);

                    final BpelProcess p = bpelEngine.getNewestProcessByType(r.getProcessType());
                    final ProcessDAO processDAO = p.getProcessDAO();
                    final MyRoleMessageExchangeImpl mex = ReplayerBpelRuntimeContextImpl.createMyRoleMex(e, bpelEngine);

                    p.invokeProcess(mex,
                    // time,
                            new BpelProcess.InvokeHandler() {
                                public boolean invoke(PartnerLinkMyRoleImpl target, RoutingInfo routing, boolean createInstance) {
                                    if (routing.messageRoute == null && createInstance) {
                                        ProcessInstanceDAO newInstance = processDAO.createInstance(routing.correlator);

                                        runtimeContext = new ReplayerBpelRuntimeContextImpl(p, newInstance, new PROCESS(p.getOProcess()), mex,
                                        // time,
                                                ReplayerContext.this);
                                        runtimeContext.setCurrentEventDateTime(time);
                                        runtimeContext.updateMyRoleMex(mex);
                                        // first receive is matched to provided
                                        // mex
                                        runtimeContext.execute();
                                        return true;
                                    } else if (routing.messageRoute != null) {
                                        throw new IllegalStateException("Instantiating mex causes invocation of existing instance " + mex);
                                    }
                                    return false;
                                }
                            },true,false);

                    for (Exchange e : exchanges) {
                        if (e.getType() == ExchangeType.M) {
                            MyRoleMessageExchangeImpl mex2 = ReplayerBpelRuntimeContextImpl.createMyRoleMex(e, bpelEngine);
                            runtimeContext.updateMyRoleMex(mex2);
                            scheduleInvoke(e, mex2);
                        }
                    }
                    return null;
                }
            }, time, null);
        }

    }

    public ReplayerContext(Date replayStartDate) {
        super();
        this.replayStartDate = replayStartDate;
    }
    
    public ServiceConfig getServiceConfig(QName service) {
        ServiceConfig c = servicesConfig.get(service);
        if (c == null) {
            c = ServiceConfig.Factory.newInstance();
            c.setService(service);
            c.addNewReplayType().setMock(XmlAnySimpleType.Factory.newInstance());
            return c;
        } else return c;
    }
    
    public void checkRollbackOnFault() {
        if (replayerConfig.getRollbackOnFault()) {
            RuntimeException e = new RuntimeException("Process instance run into fault.");
            if (__log.isDebugEnabled()) {
                __log.debug("", e);
            }
            throw e;
        }
    }
    
    public static class AnswerResult {
        public final boolean isLive;
        public final Exchange e;
        public AnswerResult(boolean isLive, Exchange e) {
            super();
            this.isLive = isLive;
            this.e = e;
        }
    }
}
