/*
 * Scenic View, 
 * Copyright (C) 2012 Jonathan Giles, Ander Ruiz, Amy Fowler 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fxconnector.remote;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URISyntaxException;
import java.net.URL;
import java.rmi.ConnectException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import org.fxconnector.AppController;
import org.fxconnector.AppControllerImpl;
import org.fxconnector.Configuration;
import org.fxconnector.StageController;
import org.fxconnector.StageID;
import org.fxconnector.details.DetailPaneType;
import org.fxconnector.event.FXConnectorEvent;
import org.fxconnector.event.FXConnectorEventDispatcher;
import org.fxconnector.node.SVNode;
import org.scenicview.utils.ExceptionLogger;
import org.scenicview.utils.Logger;
import org.scenicview.utils.Platform;

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

class RemoteConnectorImpl extends UnicastRemoteObject implements RemoteConnector, FXConnector {

    private static final long serialVersionUID = -8263538629805832734L;

    private final Map<Integer, String> vmInfo = new HashMap<>();
    private final Map<String, RemoteApplication> applications = new HashMap<>();
    private FXConnectorEventDispatcher dispatcher;
    private final List<FXConnectorEvent> previous = new ArrayList<>();
    private List<AppController> apps;
    private final AtomicInteger count = new AtomicInteger();
    private final int port;
    private final List<String> attachError = new ArrayList<>();

    private File agentFile;

    RemoteConnectorImpl() throws RemoteException {
        super();
        this.port = getValidPort();
        RMIUtils.bindScenicView(this, port);
    }

    @Override public void dispatchEvent(final FXConnectorEvent event) {
        if (dispatcher != null) {
            javafx.application.Platform.runLater(() -> {
                synchronized (previous) {
                    if (!previous.isEmpty()) {
                        for (int i = 0; i < previous.size(); i++) {
                            dispatcher.dispatchEvent(previous.get(i));
                        }
                        previous.clear();
                    }
                }
                dispatcher.dispatchEvent(event);
            });
        } else {
            synchronized (previous) {
                previous.add(event);
            }
        }
    }

    @Override public void onAgentStarted(final int port) {
        Logger.print("Remote agent started on port:" + port);
        RMIUtils.findApplication(port, application -> {
            applications.put(vmInfo.get(port), application);
            try {
                final int appsID = Integer.parseInt(vmInfo.get(port));
                final StageID[] ids = application.getStageIDs();
                addStages(appsID, ids, application);
            } catch (final RemoteException e) {
                ExceptionLogger.submitException(e);
            }
            count.decrementAndGet();
        });
    }

    private void addStages(final int appsID, final StageID[] ids, final RemoteApplication application) {
        final AppControllerImpl impl = new AppControllerImpl(appsID, Integer.toString(appsID)) {
            @Override public void close() {
                super.close();
                try {
                    application.close();
                } catch (final RemoteException e) {
                    // Nothing to do
                }
            }
        };
        
        for (int i = 0; i < ids.length; i++) {
            Logger.print("RemoteApp connected on:" + port + " stageID:" + ids[i]);
            final int cont = i;
            impl.getStages().add(new StageController() {
                StageID id = new StageID(appsID, ids[cont].getStageID());
                private boolean isOpened;
                
                {
                    id.setName(ids[cont].getName());
                }

                @Override public StageID getID() {
                    return id;
                }

                @Override public void update() {
                    try {
                        application.update(getID());
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void configurationUpdated(final Configuration configuration) {
                    try {
                        application.configurationUpdated(getID(), configuration);
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void close() {
                    try {
                        isOpened = false;
                        application.close(getID());
                    } catch (final ConnectException e2) {
                        // Nothing to do
                    } catch (final Exception e) {
                        ExceptionLogger.submitException(e);
                    }

                }

                @Override public boolean isOpened() {
                    return isOpened;
                }

                @Override public void setEventDispatcher(final FXConnectorEventDispatcher dispatcher) {
                    isOpened = true;
                    RemoteConnectorImpl.this.dispatcher = dispatcher;
                    try {
                        application.setEventDispatcher(getID(), null);
                    } catch (final RemoteException e) {
                        // TODO Auto-generated catch block
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void setSelectedNode(final SVNode value) {
                    try {
                        application.setSelectedNode(getID(), value);
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void removeSelectedNode() {
                    try {
                        application.removeSelectedNode(getID());
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public AppController getAppController() {
                    return impl;
                }

                @Override public void setDetail(final DetailPaneType detailType, final int detailID, final String value) {
                    try {
                        application.setDetail(getID(), detailType, detailID, value);
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void animationsEnabled(final boolean enabled) {
                    try {
                        application.animationsEnabled(getID(), enabled);
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void updateAnimations() {
                    try {
                        application.updateAnimations(getID());
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }

                @Override public void pauseAnimation(final int animationID) {
                    try {
                        application.pauseAnimation(getID(), animationID);
                    } catch (final RemoteException e) {
                        ExceptionLogger.submitException(e);
                    }
                }
            });
        }
        
        if (!impl.getStages().isEmpty()) {
            apps.add(impl);
        } else {
            /**
             * Keep the agent connected
             */
        }
    }

    /**
     * This method is periodically call to connect to remote VM that may have JavaFX Application running on them
     */
    @Override public List<AppController> connect() {
        apps = new ArrayList<>();
        vmInfo.clear();
        final List<VirtualMachine> machines = getRunningJavaFXApplications();
        Logger.print(machines.size() + " JavaFX applications found");
        count.set(machines.size());
        if (agentFile == null) {
            agentFile = findAgent();
        }
        try {
            final List<String> validIDs = new ArrayList<>();
            for (final VirtualMachine machine : machines) {
                validIDs.add(machine.id());
                final VirtualMachine temp = machine;

                boolean connected = false;
                if (applications.containsKey(temp.id())) {
                    final RemoteApplication application = applications.get(temp.id());
                    try {
                        final int appsID = Integer.parseInt(temp.id());
                        final StageID[] ids = application.getStageIDs();
                        addStages(appsID, ids, application);
                        connected = true;
                        count.decrementAndGet();
                    } catch (final Exception e) {
                        ExceptionLogger.submitException(e, "Failure connecting to machine.");
                        applications.remove(temp.id());
                    }
                }
                if (!connected) {
                    Thread agentThread = new Thread() {
                        {
                            setDaemon(true);
                        }
                        
                        @Override public void run() {
                            loadAgent(temp, agentFile);
                        }
                    };
                    agentThread.start();
                }
            }
            /**
             * Remove obsolete VM
             */
            for (Iterator<String> iterator = applications.keySet().iterator(); iterator.hasNext();) {
                String ids = (String) iterator.next();
                if(!validIDs.contains(ids)) {
                    iterator.remove();
                }
            }
        } catch (final Exception e) {
            ExceptionLogger.submitException(e);
        }
        final long initial = System.currentTimeMillis();
        /**
         * MAC Seems to be slower using attach API
         */
        final long timeout = Platform.getCurrent() == Platform.OSX ? 30000 : 10000;
        while (count.get() != 0 && System.currentTimeMillis() - initial < timeout) {
            try {
                Thread.sleep(50);
            } catch (final InterruptedException e) {
                // no-op
            }
        }
        Logger.setEnabled(false);
        return apps;
    }

    @Override public void close() {
        try {
            RMIUtils.unbindScenicView(port);
        } catch (final Exception e) {
            ExceptionLogger.submitException(e);
        }
    }

    private int getValidPort() {
        int port = RMIUtils.getClientPort();
        boolean valid = false;
        do {
            try {
                final Socket socket = new Socket();
                socket.connect(new InetSocketAddress("127.0.0.1", port), 100);
                socket.close();
                valid = true;
                port = RMIUtils.getClientPort();
            } catch (final Exception e) {
                valid = false;
            }

        } while (valid);
        return port;
    }

    private void loadAgent(final VirtualMachine machine, final File agentFile) {
        try {
            final long start = System.currentTimeMillis();
            final int port = getValidPort();
            Logger.print("Loading agent for:" + machine + " ID:" + machine.id() + " on port:" + port + " took:" + (System.currentTimeMillis() - start) + "ms using agent defined in " + agentFile.getAbsolutePath());
            vmInfo.put(port, machine.id());
            machine.loadAgent(agentFile.getAbsolutePath(), Integer.toString(port) + ":" + this.port + ":" + machine.id() + ":" + Logger.isEnabled());
            machine.detach();
        } catch (final Exception e) {
            ExceptionLogger.submitException(e);
        }
    }

    private static final String JAVAFX_SYSTEM_PROPERTIES_KEY = "javafx.version";

    private List<VirtualMachine> getRunningJavaFXApplications() {
        final List<VirtualMachineDescriptor> machines = VirtualMachine.list();
        Logger.print("Number of running Java applications found: " + machines.size());
        final List<VirtualMachine> javaFXMachines = new ArrayList<>();

        final Map<String, Properties> vmsProperties = new HashMap<>(machines.size());

        String currentPid = String.valueOf(ProcessHandle.current().pid());
        for (int i = 0; i < machines.size(); i++) {
            final VirtualMachineDescriptor vmd = machines.get(i);
            if (vmd != null && currentPid.equals(vmd.id())) {
                continue;
            }
            try {
                final VirtualMachine virtualMachine = VirtualMachine.attach(vmd);
                Logger.print("Obtaining properties for Java application with PID:" + virtualMachine.id());
                final Properties sysPropertiesMap = virtualMachine.getSystemProperties();
                vmsProperties.put(virtualMachine.id(), sysPropertiesMap);
                if (sysPropertiesMap != null && sysPropertiesMap.containsKey(JAVAFX_SYSTEM_PROPERTIES_KEY)/* && !sysPropertiesMap.containsKey(SCENIC_VIEW_VM)*/) {
                    javaFXMachines.add(virtualMachine);
                } else {
                    virtualMachine.detach();
                }
//                Logger.print("JVM:" + virtualMachine.id() + " detection finished");
            } catch (final AttachNotSupportedException ex) {
                dumpAttachError(vmd, ex);
            } catch (final IOException ex) {
                dumpAttachError(vmd, ex);
            } catch (final InternalError ex) {
                dumpAttachError(vmd, ex);
            }

        }
//        if (debug && javaFXMachines.isEmpty() && machines.size() > 1) {
//            debug("No running JavaFX applications found.");
//            for (final Iterator<String> iterator = vmsProperties.keySet().iterator(); iterator.hasNext();) {
//                final String id = iterator.next();
//
//                final Properties properties = vmsProperties.get(id);
//                if (!properties.containsKey(JAVAFX_SYSTEM_PROPERTIES_KEY)) {
//                    debug("ID:" + id);
//                    for (@SuppressWarnings("rawtypes") final Iterator iterator2 = properties.keySet().iterator(); iterator2.hasNext();) {
//                        final String value = (String) iterator2.next();
//                        debug("\t" + value + "=" + properties.getProperty(value));
//                    }
//                }
//            }
//        }

        return javaFXMachines;
    }

    private void dumpAttachError(final VirtualMachineDescriptor vmd, final Throwable ex) {
        if (!attachError.contains(vmd.id())) {
            attachError.add(vmd.id());
            System.err.println("Error while obtaining properties for JVM:" + vmd);
            ex.printStackTrace();
        }
    }
    
    private File findAgent() {
        File tempf = null;

        try {
            URL url = RuntimeAttach.class.getResource("/org/fxconnector/remote/RuntimeAttach.class");
            if (url.getProtocol().equals("jar")) {
                String urlFile = url.getFile();
                String fileUrl = urlFile.substring(0, urlFile.indexOf('!'));
                tempf = new File(new URL(fileUrl).toURI());
            } else if (url.getProtocol().equals("jrt")) {
                /**
                 * Find jar distributed in custom image
                 */
                tempf = new File(new URL("file:///" + System.getProperty("java.home") + "/lib/scenicview.jar").toURI());
            }
        } catch (MalformedURLException | URISyntaxException e) {
            ExceptionLogger.submitException(e, "Attempting to get agent jar.");
        }

        if (tempf == null || !tempf.exists()) {
            /**
             * Find jar file in the classpath
             */
            final String classPath = System.getProperty("java.class.path");
            final String pathSeparator = System.getProperty("path.separator");
            if (classPath != null && ! classPath.isEmpty()) {
                final String[] files = classPath.split(pathSeparator);
                for (int i = 0; i < files.length; i++) {
                    if (files[i].toLowerCase().indexOf("scenicview.jar") != -1) {
                        tempf = new File(files[i]);
                        break;
                    }
                }
            }
        }
        if (tempf == null || !tempf.exists()) {
            /**
             * Find jar file in the modulepath
             */
            final String modulePath = System.getProperty("jdk.module.path");
            final String pathSeparator = System.getProperty("path.separator");
            if (modulePath != null && ! modulePath.isEmpty()) {
                final String[] files = modulePath.split(pathSeparator);
                for (int i = 0; i < files.length; i++) {
                    if (files[i].toLowerCase().indexOf("scenicview.jar") != -1) {
                        tempf = new File(files[i]);
                        break;
                    }
                }
            }
        }

        if (tempf == null || !tempf.exists()) {
            // if we are here, lets check the development location, and try to get the jar from there
            final File buildLibsDir = new File("build/libs/");
            if (buildLibsDir.exists()) {
                File[] jarFiles = buildLibsDir.listFiles(new FilenameFilter() {
                    @Override public boolean accept(File f, String name) {
                        return name.endsWith(".jar");
                    }
                });

                for (File jarFile : jarFiles) {
                    if (tempf == null || jarFile.length() > tempf.length()) {
                        tempf = jarFile;
                    }
                }
            }

            // System.err.println("Cannot load the agent, ScenicView.jar not found here:" + tempf.getAbsolutePath());
        }

        if (tempf == null) {
            Logger.print("Error: Unable to find agent jar file. Exiting Scenic View.");
            System.exit(-1);
        } else {
            Logger.print("Loading agent from: " + tempf.getAbsolutePath());
        }
        return tempf;
    }

}