/*
 * 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.store;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.wsdl.Definition;
import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.activityRecovery.FailureHandlingDocument.FailureHandling;
import org.apache.ode.bpel.dd.TCleanup;
import org.apache.ode.bpel.dd.TDeployment;
import org.apache.ode.bpel.dd.TInvoke;
import org.apache.ode.bpel.dd.TMexInterceptor;
import org.apache.ode.bpel.dd.TProcessEvents;
import org.apache.ode.bpel.dd.TProvide;
import org.apache.ode.bpel.dd.TSchedule;
import org.apache.ode.bpel.dd.TScopeEvents;
import org.apache.ode.bpel.dd.TService;
import org.apache.ode.bpel.evt.BpelEvent;
import org.apache.ode.bpel.iapi.Cache;
import org.apache.ode.bpel.iapi.ContextException;
import org.apache.ode.bpel.iapi.Endpoint;
import org.apache.ode.bpel.iapi.EndpointReference;
import org.apache.ode.bpel.iapi.EndpointReferenceContext;
import org.apache.ode.bpel.iapi.ProcessConf;
import org.apache.ode.bpel.iapi.ProcessState;
import org.apache.ode.bpel.iapi.Scheduler.JobDetails;
import org.apache.ode.bpel.o.OFailureHandling;
import org.apache.ode.store.DeploymentUnitDir.CBPInfo;
import org.apache.ode.utils.CollectionUtils;
import org.apache.ode.utils.CronExpression;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.HierarchicalProperties;
import org.apache.ode.utils.WatchDog;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Implementation of the {@link org.apache.ode.bpel.iapi.ProcessConf} interface.
 * Provides configuration information for a process. Note that this class should
 * be immutable, that is the engine expects it to return consistent results!
 *
 * @author mriou <mriou at apache dot org>
 */
public class ProcessConfImpl implements ProcessConf {
	private static final Log __log = LogFactory.getLog(ProcessConfImpl.class);

    private final Date _deployDate;
    private File _configDir;
    private final Map<QName, Node> _props;
    private final HashMap<String, Endpoint> _partnerRoleInitialValues = new HashMap<String, Endpoint>();
    private final HashMap<String, PartnerRoleConfig> _partnerRoleConfig = new HashMap<String, PartnerRoleConfig>();

    private final HashMap<String, Endpoint> _myRoleEndpoints = new HashMap<String, Endpoint>();
    private final ArrayList<QName> _sharedServices = new ArrayList<QName>();
    private final Map<String, Set<BpelEvent.TYPE>> _events = new HashMap<String, Set<BpelEvent.TYPE>>();
    private final ArrayList<String> _mexi = new ArrayList<String>();
    private final TDeployment.Process _pinfo;
    private final DeploymentUnitDir _du;
    private long _version = 0;
    private QName _pid;
    private QName _type;

    // cache the inMemory flag because XMLBeans objects are heavily synchronized (guarded by a coarse-grained lock)
    private volatile boolean _inMemory = false;

    // monitor the IL property file and reload it if necessary
    private  WatchDog<Map<File, Long>, PropertiesObserver> propertiesWatchDog;

    private EndpointReferenceContext eprContext;

    private final ProcessCleanupConfImpl processCleanupConfImpl;
    
    private final boolean generateProcessEventsAll;
    
    private Cache<String, String> _statesCache;

    ProcessConfImpl(QName pid, QName type, long version, DeploymentUnitDir du, TDeployment.Process pinfo, Date deployDate,
                    Map<QName, Node> props, Cache<String, String> states, EndpointReferenceContext eprContext, File configDir, boolean generateProcessEventsAll) {
        _pid = pid;
        _version = version;
        _du = du;
        _pinfo = pinfo;
        _deployDate = deployDate;
        _configDir = configDir;
        _props = Collections.unmodifiableMap(props);
        _statesCache = states;
        _type = type;
        _inMemory = _pinfo.isSetInMemory() && _pinfo.getInMemory();
        this.generateProcessEventsAll = generateProcessEventsAll;
        this.eprContext = eprContext;

        propertiesWatchDog = new WatchDog<Map<File, Long>, PropertiesObserver>(new PropertiesMutable(), new PropertiesObserver());

        initLinks();
        initMexInterceptors();
        initEventList();

        processCleanupConfImpl = new ProcessCleanupConfImpl(pinfo);
        
        initSchedules();
    }

    private List<File> collectEndpointConfigFiles() {
        // please mind the order: process-level files must be before system-level files
        List<File> propFiles = new ArrayList<File>();

        propFiles.addAll(_du.getEndpointConfigFiles());
        if (_configDir == null) {
            if (__log.isDebugEnabled()) __log.debug("No config directory set up.");
        } else if (_configDir.isDirectory()) {
            // list and sort endpoint config files
            File[] files = _configDir.listFiles(new FileFilter() {
                public boolean accept(File path) {
                    return path.getName().endsWith(".endpoint") && path.isFile();
                }
            });
            if (files != null) {
                Arrays.sort(files);
                propFiles.addAll(Arrays.asList(files));
            } else {
                if (__log.isErrorEnabled()) __log.error(_configDir + " does not exist or is not a directory");
            }
        } else {
            if (__log.isErrorEnabled()) __log.error(_configDir + " does not exist or is not a directory");
        }
        return propFiles;
    }

    private void initMexInterceptors() {
        if (_pinfo.getMexInterceptors() != null) {
            for (TMexInterceptor mexInterceptor : _pinfo.getMexInterceptors().getMexInterceptorList()) {
                _mexi.add(mexInterceptor.getClassName());
            }
        }
    }

    private void initLinks() {
        if (_pinfo.getInvokeList() != null) {
            for (TInvoke invoke : _pinfo.getInvokeList()) {
                String plinkName = invoke.getPartnerLink();
                TService service = invoke.getService();
                // NOTE: service can be null for partner links
                if (service == null)
                    continue;
                __log.debug("Processing <invoke> element for process " + _pinfo.getName() + ": partnerlink " + plinkName + " --> "
                        + service);
                _partnerRoleInitialValues.put(plinkName, new Endpoint(service.getName(), service.getPort()));
                
                {
                    OFailureHandling g = null;
                    
                    if (invoke.isSetFailureHandling()) {
                        FailureHandling f = invoke.getFailureHandling();
                        g = new OFailureHandling();
                        if (f.isSetFaultOnFailure()) g.faultOnFailure = f.getFaultOnFailure();
                        if (f.isSetRetryDelay()) g.retryDelay = f.getRetryDelay();
                        if (f.isSetRetryFor()) g.retryFor = f.getRetryFor();
                    }
                    
                    PartnerRoleConfig c = new PartnerRoleConfig(g, invoke.getUsePeer2Peer());
                    __log.debug("PartnerRoleConfig for " + plinkName + " " + c.failureHandling + " usePeer2Peer: " + c.usePeer2Peer);
                    _partnerRoleConfig.put(plinkName, c);
                }
            }
        }

        if (_pinfo.getProvideList() != null) {
            for (TProvide provide : _pinfo.getProvideList()) {
                String plinkName = provide.getPartnerLink();
                TService service = provide.getService();
                if (service == null) {
                    String errmsg = "Error in <provide> element for process " + _pinfo.getName() + "; partnerlink " + plinkName
                            + "did not identify an endpoint";
                    __log.error(errmsg);
                    throw new ContextException(errmsg);
                }
                __log.debug("Processing <provide> element for process " + _pinfo.getName() + ": partnerlink " + plinkName + " --> "
                        + service.getName() + " : " + service.getPort());
                _myRoleEndpoints.put(plinkName, new Endpoint(service.getName(), service.getPort()));

                if (provide.isSetEnableSharing()) {
                    _sharedServices.add(service.getName());
                }
            }
        }
    }

    public Date getDeployDate() {
        return _deployDate;
    }

    public String getDeployer() {
        return "";
    }

    public List<File> getFiles() {
        return _du.allFiles();
    }

    public QName getProcessId() {
        return _pid;
    }

    public QName getType() {
        return _pinfo.getType() == null ? _type : _pinfo.getType();
    }

    public String getPackage() {
        return _du.getName();
    }

    public Map<QName, Node> getProcessProperties() {
        return _props;
    }

    public long getVersion() {
        return _version;
    }

    public InputStream getCBPInputStream() {
        CBPInfo cbpInfo = _du.getCBPInfo(getType());
        if (cbpInfo == null)
            throw new ContextException("CBP record not found for type " + getType());
        try {
            return new FileInputStream(cbpInfo.cbp);
        } catch (FileNotFoundException e) {
            throw new ContextException("File Not Found: " + cbpInfo.cbp, e);
        }
    }

    public long getCBPFileSize() {
        CBPInfo cbpInfo = _du.getCBPInfo(getType());
        if (cbpInfo == null)
            throw new ContextException("CBP record not found for type " + getType());
        return cbpInfo.cbp.length();
    }
    
    public String getBpelDocument() {
        CBPInfo cbpInfo = _du.getCBPInfo(getType());
        if (cbpInfo == null)
            throw new ContextException("CBP record not found for type " + getType());
        try {
            String relative = getRelativePath(_du.getDeployDir(), cbpInfo.cbp).replaceAll("\\\\", "/");
            if (!relative.endsWith(".cbp"))
                throw new ContextException("CBP file must end with .cbp suffix: " + cbpInfo.cbp);
            relative = relative.replace(".cbp", ".bpel");
            File bpelFile = new File(_du.getDeployDir(), relative);
            if (!bpelFile.exists()) __log.warn("BPEL file does not exist: " + bpelFile);
            return relative;
        } catch (IOException e) {
            throw new ContextException("IOException in getBpelRelativePath: " + cbpInfo.cbp, e);
        }
    }

    public URI getBaseURI() {
        return _du.getDeployDir().toURI();
    }

    public ProcessState getState() {
        return ProcessState.valueOf(_statesCache.get(_pid.toString()));
    }

    void setState(ProcessState state) {
        _statesCache.put(_pid.toString(), state.toString());
    }

    public List<String> getMexInterceptors(QName processId) {
        return Collections.unmodifiableList(_mexi);
    }

    public Definition getDefinitionForService(QName serviceName) {
        return _du.getDefinitionForService(serviceName);
    }

    public Definition getDefinitionForPortType(QName portTypeName) {
        return _du.getDefinitionForPortType(portTypeName);
    }

    public Map<String, Endpoint> getInvokeEndpoints() {
        return Collections.unmodifiableMap(_partnerRoleInitialValues);
    }

    public Map<String, PartnerRoleConfig> getPartnerRoleConfig() {
        return Collections.unmodifiableMap(_partnerRoleConfig);
    }

    public Map<String, Endpoint> getProvideEndpoints() {
        return Collections.unmodifiableMap(_myRoleEndpoints);
    }

    public boolean isSharedService(QName serviceName) {
        return _sharedServices.contains(serviceName);
    }

    @SuppressWarnings("unused")
    private void handleEndpoints() {
        // for (TProvide provide : _pinfo.getProvideList()) {
        // OPartnerLink pLink = _oprocess.getPartnerLink(provide.getPartnerLink());
        // if (pLink == null) {
        // String msg = __msgs.msgDDPartnerLinkNotFound(provide.getPartnerLink());
        // __log.error(msg);
        // throw new BpelEngineException(msg);
        // }
        // if (!pLink.hasMyRole()) {
        // String msg = __msgs.msgDDMyRoleNotFound(provide.getPartnerLink());
        // __log.error(msg);
        // throw new BpelEngineException(msg);
        // }
        // }
        // for (TInvoke invoke : _pinfo.getInvokeList()) {
        // OPartnerLink pLink = _oprocess.getPartnerLink(invoke.getPartnerLink());
        // if (pLink == null) {
        // String msg = __msgs.msgDDPartnerLinkNotFound(invoke.getPartnerLink());
        // __log.error(msg);
        // throw new BpelEngineException(msg);
        // }
        // if (!pLink.hasPartnerRole()) {
        // String msg = __msgs.msgDDPartnerRoleNotFound(invoke.getPartnerLink());
        // __log.error(msg);
        // throw new BpelEngineException(msg);
        // }
        // TODO Handle non initialize partner roles that just provide a binding
        // if (!pLink.initializePartnerRole && _oprocess.version.equals(Namespaces.WS_BPEL_20_NS)) {
        // String msg = ProcessDDInitializer.__msgs.msgDDNoInitiliazePartnerRole(invoke.getPartnerLink());
        // ProcessDDInitializer.__log.error(msg);
        // throw new BpelEngineException(msg);
        // }
        // }
    }

    DeploymentUnitDir getDeploymentUnit() {
        return _du;
    }

    public boolean isTransient() {
        return _inMemory;
    }

    public void setTransient(boolean t) {
        _pinfo.setInMemory(t);
        _inMemory = t;
    }

    public boolean isEventEnabled(List<String> scopeNames, BpelEvent.TYPE type) {
        if (scopeNames != null) {
            for (String scopeName : scopeNames) {
                Set<BpelEvent.TYPE> evtSet = _events.get(scopeName);
                if (evtSet != null) {
                    if (evtSet.contains(type)) return true;
                }
            }
        }
        Set<BpelEvent.TYPE> evtSet = _events.get(null);
        if (evtSet != null) {
            // Default filtering at the process level for some event types
            if (evtSet.contains(type)) return true;
        }
        return false;
    }

    private void initEventList() {
        TProcessEvents processEvents = _pinfo.getProcessEvents();
        // No filtering, using defaults
        if (processEvents == null) {
            if (generateProcessEventsAll) {
                HashSet<BpelEvent.TYPE> all = new HashSet<BpelEvent.TYPE>();
                for (BpelEvent.TYPE t : BpelEvent.TYPE.values()) {
                    if (!t.equals(BpelEvent.TYPE.scopeHandling)) all.add(t);
                }
                _events.put(null, all);
            }
            return;
        }

        // Adding all events
        if (processEvents.getGenerate() != null && processEvents.getGenerate().equals(TProcessEvents.Generate.ALL)) {
            HashSet<BpelEvent.TYPE> all = new HashSet<BpelEvent.TYPE>();
            for (BpelEvent.TYPE t : BpelEvent.TYPE.values())
                all.add(t);
            _events.put(null, all);
            return;
        }

        // Events filtered at the process level
        if (processEvents.getEnableEventList() != null && !processEvents.getEnableEventList().isEmpty()) {
            List<String> enabled = processEvents.getEnableEventList();
            HashSet<BpelEvent.TYPE> evtSet = new HashSet<BpelEvent.TYPE>();
            for (String enEvt : enabled) {
                evtSet.add(BpelEvent.TYPE.valueOf(enEvt));
            }
            _events.put(null, evtSet);
        }

        // Events filtered at the scope level
        if (processEvents.getScopeEventsList() != null) {
            for (TScopeEvents tScopeEvents : processEvents.getScopeEventsList()) {
                HashSet<BpelEvent.TYPE> evtSet = new HashSet<BpelEvent.TYPE>();
                for (String enEvt : tScopeEvents.getEnableEventList()) {
                    evtSet.add(BpelEvent.TYPE.valueOf(enEvt));
                }
                _events.put(tScopeEvents.getName(), evtSet);
            }
        }
    }

    private String getRelativePath(File base, File path) throws IOException {
        String basePath = base.getCanonicalPath();
        String cbpPath = path.getCanonicalPath();
        if (!cbpPath.startsWith(basePath))
            throw new IOException("Invalid relative path: base=" + base + " path=" + path);
        String relative = cbpPath.substring(basePath.length());
        if (relative.startsWith(File.separator)) relative = relative.substring(1);
        return relative;
    }

    public List<Element> getExtensionElement(QName qname) {
        try {
            return DOMUtils.findChildrenByName(DOMUtils.stringToDOM(_pinfo.toString()), qname);
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }
    
    @SuppressWarnings("unchecked")
    public Map<String, String> getEndpointProperties(EndpointReference epr) {
        final Map map = eprContext.getConfigLookup(epr);
        final QName service = (QName) map.get("service");
        final String port = (String) map.get("port");

        return(getEndpointProperties(service, port));
    }
    
    @SuppressWarnings("unchecked")
    public Map<String, String> getEndpointProperties(QName service, String port) {

        // update properties if necessary
        // do it manually to save resources (instead of using a thread)
        propertiesWatchDog.check();
        final Map prop = propertiesWatchDog.getObserver().get().getProperties(service, port);
        if(!prop.isEmpty() && __log.isDebugEnabled()) {
            StringBuilder msg = new StringBuilder("Properties for ");
            if(service!=null) msg.append("service ").append(service);
            if(port!=null) msg.append(", port ").append(port);
            msg.append(": {");
            for (Iterator it = prop.entrySet().iterator(); it.hasNext();) {
                Map.Entry e = (Map.Entry) it.next();
                msg.append(e.getKey()).append("=>").append(e.getValue());
                if(it.hasNext()) msg.append(", ");
            }
            msg.append("}");
            __log.debug(msg);
        }
        return prop;
    }


    private class PropertiesMutable implements WatchDog.Mutable<Map<File, Long>> {

        public boolean exists() {
            return true;
        }

        public boolean hasChangedSince(Map<File, Long> since) {
            return !CollectionUtils.equals(lastModified(), since);
        }

        public Map<File, Long> lastModified() {
            List<File> files = collectEndpointConfigFiles();
            Map<File, Long> m = new HashMap<File, Long>(files.size() * 15 / 10);
            for (File f : files) m.put(f, Long.valueOf(f.lastModified()));
            return m;
        }

        @Override
        public String toString() {
            return "Endpoint files for "+_du.toString();
        }
    }

    private class PropertiesObserver extends WatchDog.DefaultObserver<HierarchicalProperties> {

        public void init() {
            try {
                // do not hold a reference on the file list, so that changes are handled
                // and always create a new instance of the HierarchicalProperties
                object = new HierarchicalProperties(collectEndpointConfigFiles());
            } catch (IOException e) {
                throw new ContextException("Integration-Layer Properties cannot be loaded!", e);
            }
        }
    }

    public boolean isCleanupCategoryEnabled(boolean instanceSucceeded, CLEANUP_CATEGORY category) {
        return processCleanupConfImpl.isCleanupCategoryEnabled(instanceSucceeded, category);
    }

    public Set<CLEANUP_CATEGORY> getCleanupCategories(boolean instanceSucceeded) {
        return processCleanupConfImpl.getCleanupCategories(instanceSucceeded);
    }
    
    private void initSchedules() {
        for(TSchedule schedule : _pinfo.getScheduleList()) {
            for(TCleanup cleanup : schedule.getCleanupList()) {
                assert !cleanup.getFilterList().isEmpty();
            }
        }
    }
    
    public List<CronJob> getCronJobs() {
        List<CronJob> jobs = new ArrayList<CronJob>();
        
        for(TSchedule schedule : _pinfo.getScheduleList()) {
            CronJob job = new CronJob();
            try {
                job.setCronExpression(new CronExpression(schedule.getWhen()));
                for(final TCleanup aCleanup : schedule.getCleanupList()) {
                    CleanupInfo cleanupInfo = new CleanupInfo();
                    assert !aCleanup.getFilterList().isEmpty();
                    cleanupInfo.setFilters(aCleanup.getFilterList());
                    ProcessCleanupConfImpl.processACleanup(cleanupInfo.getCategories(), aCleanup.getCategoryList());
                    
                    JobDetails runnableDetails = new JobDetails();
                    runnableDetails.getDetailsExt().put("cleanupInfo", cleanupInfo);
                    runnableDetails.getDetailsExt().put("pid", _pid);
                    runnableDetails.getDetailsExt().put("transactionSize", 10);
                    job.getRunnableDetailList().add(runnableDetails);
                }
                jobs.add(job);
            } catch( ParseException pe ) {
                __log.error("Exception during parsing the schedule cron expression: " + schedule.getWhen() + ", skipped the scheduled job.", pe);
            }
        }
        
        return jobs;
    }
}