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

import java.io.File;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.RemoteObject;
import java.util.Properties;

import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

/**
 * Class to run remote tests from the client JMeter and collect remote samples
 */
public class ClientJMeterEngine implements JMeterEngine {
    private static final Logger log = LoggingManager.getLoggerForClass();

    private static final Object LOCK = new Object();

    private RemoteJMeterEngine remote;

    private HashTree test;

    private final String host;

    private static RemoteJMeterEngine getEngine(String h) throws MalformedURLException, RemoteException,
            NotBoundException {
       final String name = "//" + h + "/" + RemoteJMeterEngineImpl.JMETER_ENGINE_RMI_NAME; // $NON-NLS-1$ $NON-NLS-2$
       Remote remobj = Naming.lookup(name);
       if (remobj instanceof RemoteJMeterEngine){
           final RemoteJMeterEngine rje = (RemoteJMeterEngine) remobj;
           if (remobj instanceof RemoteObject){
               RemoteObject robj = (RemoteObject) remobj;
               System.out.println("Using remote object: "+robj.getRef().remoteToString());
           }
           return rje;
       }
       throw new RemoteException("Could not find "+name);
    }

    public ClientJMeterEngine(String host) throws MalformedURLException, NotBoundException, RemoteException {
        this.remote = getEngine(host);
        this.host = host;
    }

    /** {@inheritDoc} */
    @Override
    public void configure(HashTree testTree) {
        TreeCloner cloner = new TreeCloner(false);
        testTree.traverse(cloner);
        test = cloner.getClonedTree();
    }

    /** {@inheritDoc} */
    @Override
    public void stopTest(boolean now) {
        log.info("about to "+(now ? "stop" : "shutdown")+" remote test on "+host);
        try {
            remote.rstopTest(now);
        } catch (Exception ex) {
            log.error("", ex); // $NON-NLS-1$
        }
    }

    /** {@inheritDoc} */
    @Override
    public void reset() {
        try {
            try {
                remote.rreset();
            } catch (java.rmi.ConnectException e) {
                log.info("Retry reset after: "+e);
                remote = getEngine(host);
                remote.rreset();
            }
        } catch (Exception ex) {
            log.error("Failed to reset remote engine", ex); // $NON-NLS-1$
        }
    }

    @Override
    public void runTest() throws JMeterEngineException {
        log.info("running clientengine run method");
        
        // See https://issues.apache.org/bugzilla/show_bug.cgi?id=55510
        JMeterContextService.clearTotalThreads();
        HashTree testTree = test;

        synchronized(testTree) {
            testTree.traverse(new PreCompiler(true));  // limit the changes to client only test elements
            testTree.traverse(new TurnElementsOn());
            testTree.traverse(new ConvertListeners());
        }

        String methodName="unknown";
        try {
            JMeterContextService.startTest();
            /*
             * Add fix for Deadlocks, see:
             * 
             * See https://issues.apache.org/bugzilla/show_bug.cgi?id=48350
            */
            File baseDirRelative = FileServer.getFileServer().getBaseDirRelative();
            String scriptName = FileServer.getFileServer().getScriptName();
            synchronized(LOCK)
            {
                methodName="rconfigure()";
                remote.rconfigure(testTree, host, baseDirRelative, scriptName);
            }
            log.info("sent test to " + host + " basedir='"+baseDirRelative+"'"); // $NON-NLS-1$
            if(savep == null) {
                savep = new Properties();
            }
            log.info("Sending properties "+savep);
            try {
                methodName="rsetProperties()";
                remote.rsetProperties(savep);
            } catch (RemoteException e) {
                log.warn("Could not set properties: " + e.toString());
            }
            methodName="rrunTest()";
            remote.rrunTest();
            log.info("sent run command to "+ host);
        } catch (IllegalStateException ex) {
            log.error("Error in "+methodName+" method "+ex); // $NON-NLS-1$ $NON-NLS-2$
            tidyRMI(log);
            throw ex; // Don't wrap this error - display it as is
        } catch (Exception ex) {
            log.error("Error in "+methodName+" method "+ex); // $NON-NLS-1$ $NON-NLS-2$
            tidyRMI(log);
            throw new JMeterEngineException("Error in "+methodName+" method "+ex, ex); // $NON-NLS-1$ $NON-NLS-2$
        }
    }

    /**
     * Tidy up RMI access to allow JMeter client to exit.
     * Currently just interrups the "RMI Reaper" thread.
     * @param logger where to log the information
     */
    public static void tidyRMI(Logger logger) {
        String reaperRE = JMeterUtils.getPropDefault("rmi.thread.name", "^RMI Reaper$");
        for(Thread t : Thread.getAllStackTraces().keySet()){
            String name = t.getName();
            if (name.matches(reaperRE)) {
                logger.info("Interrupting "+name);
                t.interrupt();
            }
        }
    }

    /** {@inheritDoc} */
    // Called by JMeter ListenToTest if remoteStop is true
    @Override
    public void exit() {
        log.info("about to exit remote server on "+host);
        try {
            remote.rexit();
        } catch (RemoteException e) {
            log.warn("Could not perform remote exit: " + e.toString());
        }
    }

    private Properties savep;
    /** {@inheritDoc} */
    @Override
    public void setProperties(Properties p) {
        savep = p;
        // Sent later
    }

    @Override
    public boolean isActive() {
        return true;
    }

    public String getHost() {
        return host;
    }
}
