/*
* 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.stanbol.enhancer.jobmanager.event.impl;

import static org.apache.stanbol.enhancer.jobmanager.event.Constants.PROPERTY_EXECUTION;
import static org.apache.stanbol.enhancer.jobmanager.event.Constants.PROPERTY_JOB_MANAGER;
import static org.apache.stanbol.enhancer.jobmanager.event.Constants.TOPIC_JOB_MANAGER;
import static org.apache.stanbol.enhancer.servicesapi.helper.ExecutionPlanHelper.getEngine;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.clerezza.rdf.core.NonLiteral;
import org.apache.stanbol.enhancer.servicesapi.EngineException;
import org.apache.stanbol.enhancer.servicesapi.EnhancementEngine;
import org.apache.stanbol.enhancer.servicesapi.EnhancementEngineManager;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EnhancementJobHandler implements EventHandler {

    private EnhancementEngineManager engineManager;
    private EventAdmin eventAdmin;

    /*
     * NOTE on debug level Loggings
     * 
     *  ++ ... init some activity
     *  >> ... started some activity (thread has the requested lock)
     *  << ... completed some activity (thread has released the lock)
     *  
     *  n: ... no lock
     *  r: ... read lock
     *  w: ... write lock
     */
    private Logger log = LoggerFactory.getLogger(EnhancementJobHandler.class);
    /**
     * Keys are {@link EnhancementJob}s currently asynchronously enhancing 
     * contentItems and the values are the objects used to interrupt the 
     * requesting thread as soon as the enhancement process has finished. 
     */
    private Map<EnhancementJob,EnhancementJobObserver> processingJobs;
    private final ReadWriteLock processingLock = new ReentrantReadWriteLock();
    private Thread observerDaemon;
    
    public EnhancementJobHandler(EventAdmin eventAdmin, 
                                 EnhancementEngineManager engineManager) {
        if(eventAdmin == null){
            throw new IllegalArgumentException("The parsed EventAdmin service MUST NOT be NULL!");
        }
        if(engineManager == null){
            throw new IllegalArgumentException("The parsed EnhancementEngineManager MUST NOT be NULL!");
        }
        this.eventAdmin = eventAdmin;
        this.engineManager = engineManager;
        processingLock.writeLock().lock();
        try {
            processingJobs = new LinkedHashMap<EnhancementJob,EnhancementJobObserver>();
        } finally{
            processingLock.writeLock().unlock();
        }
        observerDaemon = new Thread(new EnhancementJobObserverDaemon());
        observerDaemon.setName("Event Job Manager Observer Daemon");
        observerDaemon.setDaemon(true);
        observerDaemon.start();
        
    }
    /**
     * Closes this Handler and notifies all components that wait for still
     * running jobs
     */
    public void close(){
        log.info("deactivate {}",getClass().getName());
        processingLock.writeLock().lock();
        try {
            for(Object o : processingJobs.values()){
                synchronized (o) {
                    o.notifyAll();
                }
            }
            processingJobs = null;
        } finally {
            processingLock.writeLock().unlock();
        }
        observerDaemon = null;
    }
    
    /**
     * Registers an EnhancementJob and will start the enhancement process.
     * When the process is finished or this service is deactivated the
     * returned oject will be notified. Therefore callers that need to 
     * wait for the completion of the parsed job will want to
     * <code><pre>
     *   Object object = enhancementJobHandler.register();
     *   while(!job.isFinished() & enhancementJobHandler != null){
     *       synchronized (object) {
     *           try {
     *               object.wait();
     *           } catch (InterruptedException e) {}
     *       }
     *   }
     * </pre></code>
     * @param enhancementJob the enhancement job to register
     * @return An object that will get {@link Object#notifyAll()} as soon as
     * {@link EnhancementJob#isFinished()} or this instance is deactivated
     */
    public EnhancementJobObserver register(EnhancementJob enhancementJob){
        final boolean init;
        EnhancementJobObserver observer;
        processingLock.writeLock().lock();
        try {
            if(enhancementJob == null || processingJobs == null){
                return null;
            }
            observer = processingJobs.get(enhancementJob);
            if(observer == null){
                observer = new EnhancementJobObserver(enhancementJob);
                if(log.isDebugEnabled()){
                    logJobInfo(log, enhancementJob, "Add EnhancementJob:",log.isTraceEnabled());
                }
                processingJobs.put(enhancementJob, observer);
                init = true;
            } else {
                log.warn("Request to register an EnhancementJob for an ContentItem {} that is" +
                		"already registered "+enhancementJob.getContentItem().getUri());
                init = false;
            }
        } finally {
            processingLock.writeLock().unlock();
        }
        if(init){
            observer.acquire();
            enhancementJob.startProcessing();
            log.trace("++ w: {}","init execution");
            enhancementJob.getLock().writeLock().lock();
            try {
                log.trace(">> w: {}","init execution");
                if(!executeNextNodes(enhancementJob)){
                    String message = "Unable to start Execution of "+enhancementJob.getContentItem().getUri();
                    log.warn(message);
                    logJobInfo(log, enhancementJob, null, true);
                    log.warn("finishing job ...");
                    finish(enhancementJob);
                }
            } finally {
                log.trace("<< w: {}","init execution");
                enhancementJob.getLock().writeLock().unlock();
            }
        }
        return observer;
    }

    @Override
    public void handleEvent(Event event) {
        EnhancementJob job = (EnhancementJob)event.getProperty(PROPERTY_JOB_MANAGER);
        NonLiteral execution = (NonLiteral)event.getProperty(PROPERTY_EXECUTION);
        if(job == null || execution == null){
            log.warn("Unable to process EnhancementEvent where EnhancementJob " +
            		"{} or Execution node {} is null -> ignore",job,execution);
        }
        try {
            processEvent(job, execution);
        } catch (Throwable t) {
            String message = String.format("Unexpected Exception while processing " +
            		"ContentItem %s with EnhancementJobManager: %s",
                    job.getContentItem().getUri(),EventJobManagerImpl.class);
            //this ensures that an runtime exception does not 
           job.setFailed(execution, null, new IllegalStateException(message,t));
           log.error(message,t);
        }
        //(2) trigger the next actions
        log.trace("++ w: {}","check for next Executions");
        job.getLock().writeLock().lock();
        log.trace(">> w: {}","check for next Executions");
        try {
            if(job.isFinished()){
                finish(job);
            } else if(!job.isFailed()){
                if(!executeNextNodes(job) && job.getRunning().isEmpty()){
                    log.warn("Unexpected state in the Execution of ContentItem {}:"
                        + " Job is not finished AND no executions are running AND"
                        + " no further execution could be started! -> finishing"
                        + " this job :(");
                    finish(job);
                } //else execution started of other jobs are running
            } else {
                if(log.isInfoEnabled()){
                    Collection<String> running = new ArrayList<String>(3);
                    for(NonLiteral runningNode : job.getRunning()){
                        running.add(getEngine(job.getExecutionPlan(), job.getExecutionNode(runningNode)));
                    }
                    log.info("Job {} failed, but {} still running!",
                        job.getContentItem().getUri(),running);
                }
            }
        } finally {
            log.trace("<< w: {}","check for next Executions");
            job.getLock().writeLock().unlock();
        }
    }
    /**
     * @param job
     * @param execution
     */
    private void processEvent(EnhancementJob job, NonLiteral execution) {
        String engineName = getEngine(job.getExecutionPlan(), 
            job.getExecutionNode(execution));
        //(1) execute the parsed ExecutionNode
        EnhancementEngine engine = engineManager.getEngine(engineName);
        if(engine != null){
            //execute the engine
            Exception exception = null;
            int engineState;
            try {
                engineState = engine.canEnhance(job.getContentItem());
            } catch (EngineException e) {
                exception = e;
                log.warn("Unable to check if engine '" + engineName
                    + "'(type: " + engine.getClass() + ") can enhance ContentItem '"
                    + job.getContentItem().getUri()+ "'!",e);
                engineState = EnhancementEngine.CANNOT_ENHANCE;
            }
            if(engineState == EnhancementEngine.ENHANCE_SYNCHRONOUS){
                //ensure that this engine exclusively access the content item
                log.trace("++ w: {}: {}","start sync execution", engine.getName());
                job.getLock().writeLock().lock();
                log.trace(">> w: {}: {}","start sync execution", engine.getName());
                try {
                    engine.computeEnhancements(job.getContentItem());
                    job.setCompleted(execution);
                } catch (EngineException e){
                    log.warn(e.getMessage(),e);
                    job.setFailed(execution, engine, e);
                } catch (RuntimeException e){
                    log.warn(e.getMessage(),e);
                    job.setFailed(execution, engine, e);
                } finally{
                    log.trace("<< w: {}: {}","finished sync execution", engine.getName());
                    job.getLock().writeLock().unlock();
                }
            } else if(engineState == EnhancementEngine.ENHANCE_ASYNC){
                try {
                    log.trace("++ n: start async execution of Engine {}",engine.getName());
                    engine.computeEnhancements(job.getContentItem());
                    log.trace("++ n: finished async execution of Engine {}",engine.getName());
                    job.setCompleted(execution);
                } catch (EngineException e) {
                    log.warn(e.getMessage(),e);
                    job.setFailed(execution, engine, e);
                } catch (RuntimeException e) {
                    log.warn(e.getMessage(),e);
                    job.setFailed(execution, engine, e);
                }
            } else { //CANNOT_ENHANCE
                if(exception != null){
                    job.setFailed(execution,engine,exception);
                } else { //can not enhance is not an error
                    //it just says this engine can not enhance this content item
                    job.setCompleted(execution);
                }
            }
        } else { //engine with that name is not available
            job.setFailed(execution, null, null);
        }
    }
    /**
     * Removes a finished job from {@link #processingJobs} and notifies
     * all waiting components
     * @param job the finished job
     */
    private void finish(EnhancementJob job){
        processingLock.writeLock().lock();
        EnhancementJobObserver observer;
        try {
            observer = processingJobs.remove(job);
        } finally {
            processingLock.writeLock().unlock();
        }
        if(observer != null) {
            try {
                if(log.isDebugEnabled()){
                    logJobInfo(log, job, "Finished EnhancementJob:",log.isTraceEnabled());
                }
                log.trace("++ n: finished processing ContentItem {} with Chain {}",
                    job.getContentItem().getUri(),job.getChainName());
            } finally {
                //release the semaphore to send signal to the EventJobManager waiting
                //for the results
                observer.release();
            }
        } else {
            log.warn("EnhancementJob for ContentItem {} is not " +
                    "registered with {}. Will not send notification!",
                    job.getContentItem().getUri(), getClass().getName());
        }            
    }
    /**
     * triggers the execution of the next nodes or if 
     * {@link EnhancementJob#isFinished()} notifies the one who registered 
     * the {@link EnhancementJob} with this component.
     * @param job the enhancement job to process
     * @return if an Execution event was sent
     */
    protected boolean executeNextNodes(EnhancementJob job) {
        //getExecutable returns an snapshot so we do not need to lock
        boolean startedExecution = false;
        for(NonLiteral executable : job.getExecutable()){
            if(log.isTraceEnabled()){
                log.trace("PREPARE execution of Engine {}",
                    getEngine(job.getExecutionPlan(), job.getExecutionNode(executable)));
            }
            Dictionary<String,Object> properties = new Hashtable<String,Object>();
            properties.put(PROPERTY_JOB_MANAGER, job);
            properties.put(PROPERTY_EXECUTION, executable);
            job.setRunning(executable);
            if(log.isTraceEnabled()){
                log.trace("SHEDULE execution of Engine {}",
                    getEngine(job.getExecutionPlan(), job.getExecutionNode(executable)));
            }
            eventAdmin.postEvent(new Event(TOPIC_JOB_MANAGER,properties));
            startedExecution = true;
        }
        return startedExecution;
    }
    
    /**
     * Logs basic infos about the Job as INFO and detailed infos as DEBUG
     * @param job
     */
    protected static void logJobInfo(Logger log, EnhancementJob job, String header, boolean logExecutions) {
        if(header != null){
            log.info(header);
        }
        log.info("   finished:     {}",job.isFinished());
        log.info("   state:        {}",job.isFailed()?"failed":"processing");
        log.info("   chain:        {}",job.getChainName());
        log.info("   content-item: {}", job.getContentItem().getUri());
        if(logExecutions){
            log.info("  executions:");
            for(NonLiteral completedExec : job.getCompleted()){
                log.info("    - {} completed",getEngine(job.getExecutionMetadata(), 
                    job.getExecutionNode(completedExec)));
            }
            for(NonLiteral runningExec : job.getRunning()){
                log.info("    - {} running",getEngine(job.getExecutionMetadata(), 
                    job.getExecutionNode(runningExec)));
            }
            for(NonLiteral executeable : job.getExecutable()){
                log.info("    - {} executeable",getEngine(job.getExecutionMetadata(), 
                    job.getExecutionNode(executeable)));
            }
        }
        if(job.getErrorMessage() != null){
            log.info("Error Message: {}",job.getErrorMessage());
        }
        if(job.getError() != null){
            log.info("Reported Exception:",job.getError());
        }
    }
    public class EnhancementJobObserver{
        
        private static final int MIN_WAIT_TIME = 500;
        private final EnhancementJob enhancementJob;
        private final Semaphore semaphore;
        
        private EnhancementJobObserver(EnhancementJob job){
            if(job == null){
                throw new IllegalArgumentException("The parsed EnhancementJob MUST NOT be NULL!");
            }
            this.enhancementJob = job;
            this.semaphore = new Semaphore(1);
        }

        protected void acquire() {
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                log.warn("Interrupted while acquireing Semaphore for EnhancementJob "
                        + enhancementJob + "!",e);
            }
        }
        
        protected void release() {
            semaphore.release();
        }

        public boolean hasCompleted() {
            enhancementJob.getLock().readLock().lock();
            try {
                return enhancementJob.isFinished();
            } finally {
                enhancementJob.getLock().readLock().unlock();
            }
        }

        public boolean waitForCompletion(int maxEnhancementJobWaitTime) {
            boolean acquire = false;
            if(semaphore.availablePermits() < 1){
                // The only permit is taken by the EnhancementJobHander
                try {
                    acquire = semaphore.tryAcquire(1,
                        Math.max(MIN_WAIT_TIME, maxEnhancementJobWaitTime),TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    //interupted
                    acquire = false;
                }
            } else if(!hasCompleted()){
                int wait = Math.max(100, maxEnhancementJobWaitTime/10);
                log.warn("Unexpected permit available for Semaphore of "
                    + "EnhancementJob of ContentItem {}. Fallback to wait({})"
                    + "for detecting if Job has finished. While the fallback "
                    + "should ensure correct Enhancement results this indicates a "
                    + "Bug in the EventHobManager. Please feel free to report "
                    + "This on dev@stanbol.apache.org or the Apache Stanbol "
                    + "Issue Tracker.",enhancementJob.getContentItem().getUri(),wait);
                try {
                    Thread.currentThread().wait(wait);
                } catch (InterruptedException e) {
                    //interupted
                }
                acquire = true;
            }// else completed
            return acquire;
        }
    }
    
    
    /**
     * Currently only used to debug the number of currently registered
     * Enhancements Jobs (if there are some)
     * @author Rupert Westenthaler
     */
    private class EnhancementJobObserverDaemon implements Runnable {

        /**
         * The logger of the Observer. Can be used to configure Loglevel specificly
         * 
         */
        private Logger observerLog = LoggerFactory.getLogger(EnhancementJobObserverDaemon.class);
        
        @Override
        public void run() {
            observerLog.debug(" ... init EnhancementJobObserver");
            while(processingJobs != null){
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                }
                Collection<EnhancementJob> jobs;
                Lock readLock = processingLock.readLock();
                readLock.lock();
                try {
                    if(processingJobs != null){
                        jobs = new ArrayList<EnhancementJob>(processingJobs.keySet());
                    } else {
                        jobs = Collections.emptyList();
                    }
                } finally {
                    readLock.unlock();
                }
                if(!jobs.isEmpty()){
                    observerLog.debug(" -- {} active Enhancement Jobs",jobs.size());
                    if(observerLog.isDebugEnabled()){
                        for(EnhancementJob job : jobs){
                            Lock jobLock = job.getLock().readLock();
                            jobLock.lock();
                            try {
                                logJobInfo(observerLog,job,null,true);
                            } finally {
                                jobLock.unlock();
                            }
                        }
                    }
                } else {
                    log.debug(" -- No active Enhancement Jobs");
                }
            }
            
        }
        
    }
    
    
}