/**
 * Copyright (C) 2012 - 2014  Eric Van Dewoestine
 *
 * 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.eclim.eclipse;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;

import java.net.BindException;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLClassLoader;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclim.Services;

import org.eclim.command.ReloadCommand;

import org.eclim.logging.Logger;

import org.eclim.plugin.PluginResources;

import org.eclim.util.IOUtils;

import org.eclim.util.file.FileUtils;

import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;

import org.eclipse.core.runtime.jobs.IJobManager;
import org.eclipse.core.runtime.jobs.Job;

import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;

import com.google.gson.Gson;

import com.martiansoftware.nailgun.NGServer;

/**
 * Class which handles starting/stopping of the eclim nailgun daemon along with
 * the loading/unloading of eclim plugins.
 *
 * @author Eric Van Dewoestine
 */
public class EclimDaemon
  implements FrameworkListener
{
  private static final Logger logger =
    Logger.getLogger(EclimDaemon.class);

  private static String BASE = "org.eclim";
  private static String CORE = "org.eclim.core";
  private static EclimDaemon instance = new EclimDaemon();

  private boolean started;
  private boolean starting;
  private boolean stopping;
  private NGServer server;

  private EclimDaemon()
  {
  }

  public static EclimDaemon getInstance()
  {
    return instance;
  }

  public void start()
  {
    if (started || starting){
        return;
    }

    String host = Services.getPluginResources("org.eclim")
      .getProperty("nailgun.server.host");
    String portString = Services.getPluginResources("org.eclim")
      .getProperty("nailgun.server.port");
    try{
      String workspace = getWorkspace();
      logger.info("Workspace: " + workspace);

      String home = getHome();
      logger.info("Home: " + home);

      starting = true;
      logger.info("Starting eclim...");

      int port = Integer.parseInt(portString);

      InetAddress address = InetAddress.getByName(host);
      server = new NGServer(address, port, getExtensionClassLoader());
      server.setCaptureSystemStreams(false);

      logger.info("Loading plugin org.eclim");
      PluginResources defaultResources = Services.getPluginResources("org.eclim");
      defaultResources.registerCommand(ReloadCommand.class);

      logger.info("Loading plugin org.eclim.core");

      Bundle bundle = Platform.getBundle(CORE);
      if(bundle == null){
        logger.error(Services.getMessage("plugin.load.failed", CORE));
        return;
      }

      bundle.start(Bundle.START_TRANSIENT);
      bundle.getBundleContext().addFrameworkListener(this);

      // wait up to 10 seconds for bundles to activate.
      synchronized(this){
        wait(10000);
      }

      // headless
      if (EclimApplication.isEnabled()){
        logger.info("Waiting on running jobs before starting eclimd...");
        long timeout = System.currentTimeMillis() + (30 * 1000);
        IJobManager manager = Job.getJobManager();
        while(System.currentTimeMillis() < timeout &&
          jobsRunning(manager))
        {
          try{
            Thread.sleep(1000);
          }catch(InterruptedException ie){
            // ignore
          }
        }
      }

      starting = false;
      started = true;
      logger.info("Eclim Server Started on: {}:{}", host, port);
      registerInstance(home, workspace, port);
      server.run();
    }catch(NumberFormatException nfe){
      logger.error("Error starting eclim:",
          new RuntimeException("Invalid port number: '" + portString + "'"));
    }catch(BindException be){
      logger.error("Error starting eclim on {}:{}", host, portString, be);
    }catch(Throwable t){
      logger.error("Error starting eclim:", t);
    }finally{
      try{
        unregisterInstance();
      }catch(Exception e){
        throw new RuntimeException(e);
      }
      starting = false;
      started = false;
    }
  }

  public void stop()
    throws Exception
  {
    if(started && !stopping){
      try{
        stopping = true;
        logger.info("Shutting down eclim...");

        if(server != null && server.isRunning()){
          server.shutdown(false /* exit vm */);
        }

        unregisterInstance();

        logger.info("Stopping plugin " + CORE);
        Bundle bundle = Platform.getBundle(CORE);
        if (bundle != null){
          try{
            bundle.stop();
          }catch(IllegalStateException ise){
            // thrown because eclipse osgi BaseStorage attempts to register a
            // shutdown hook during shutdown.
          }
        }

        logger.info("Eclim stopped.");
      }finally{
        stopping = false;
      }
    }
  }

  /**
   * Register the current instance in the eclimd instances file for use by vim.
   */
  private void registerInstance(String home, String workspace, int port)
    throws Exception
  {
    File instances = new File(FileUtils.concat(
          System.getProperty("user.home"), ".eclim/.eclimd_instances"));

    Gson gson = new Gson();
    FileWriter out = null;
    try{
      List<Instance> entries = readInstances();
      if (entries != null){
        boolean updated = false;

        // remove any stale entries
        for (Iterator<Instance> i = entries.iterator(); i.hasNext();){
          Instance entry = i.next();
          if (!entry.exists()){
            i.remove();
            updated = true;
            logger.info("Removed stale instance entry: " +
                entry.home + ':' + entry.port);
          }
        }

        // register new instance
        Instance instance = new Instance(home, workspace, port);
        if (!entries.contains(instance)){
          entries.add(instance);
          updated = true;
        }

        // update the instances file
        if (updated){
          out = new FileWriter(instances);
          for (Instance entry : entries) {
            out.write(gson.toJson(entry) + '\n');
          }
        }
      }
    }catch(IOException ioe){
      logger.error(
          "\nError writing to eclimd instances file: " + ioe.getMessage() +
          "\n" + instances);
    }finally{
      IOUtils.closeQuietly(out);
    }
  }

  /**
   * Unregister the current instance in the eclimd instances file for use by vim.
   */
  private synchronized void unregisterInstance()
    throws Exception
  {
    File instances = new File(FileUtils.concat(
          System.getProperty("user.home"), ".eclim/.eclimd_instances"));

    Gson gson = new Gson();
    FileWriter out = null;
    try{
      List<Instance> entries = readInstances();
      if (entries == null){
        return;
      }

      Instance instance = new Instance(getHome(), getWorkspace(), getPort());
      entries.remove(instance);

      if (entries.size() == 0){
        if (!instances.delete()){
          logger.error("Error deleting eclimd instances file: " + instances);
        }
      }else{
        out = new FileWriter(instances);
        for (Instance entry : entries) {
          out.write(gson.toJson(entry) + '\n');
        }
      }
    }catch(IOException ioe){
      logger.error(
          "\nError writing to eclimd instances file: " + ioe.getMessage() +
          "\n" + instances);
      return;
    }finally{
      IOUtils.closeQuietly(out);
    }
  }

  @SuppressWarnings("unchecked")
  private List<Instance> readInstances()
    throws Exception
  {
    File doteclim =
      new File(FileUtils.concat(System.getProperty("user.home"), ".eclim"));
    if (!doteclim.exists()){
      if (!doteclim.mkdirs()){
        logger.error("Error creating ~/.eclim directory: " + doteclim);
        return null;
      }
    }
    File instances = new File(FileUtils.concat(
          doteclim.getAbsolutePath(), ".eclimd_instances"));
    if (!instances.exists()){
      try{
        instances.createNewFile();
      }catch(IOException ioe){
        logger.error(
            "\nError creating eclimd instances file: " + ioe.getMessage() +
            "\n" + instances);
        return null;
      }
    }

    Gson gson = new Gson();
    FileInputStream in = null;
    try{
      in = new FileInputStream(instances);
      List<String> lines = IOUtils.readLines(in);
      List<Instance> entries = new ArrayList<Instance>();
      for (String line : lines){
        if (!line.startsWith("{")) {
          continue;
        }
        entries.add(gson.fromJson(line, Instance.class));
      }
      return entries;
    }finally{
      IOUtils.closeQuietly(in);
    }
  }

  private String getWorkspace()
  {
    return ResourcesPlugin.getWorkspace()
      .getRoot().getRawLocation().toOSString().replace('\\', '/');
  }

  private String getHome()
    throws IOException
  {
    Bundle bundle = Platform.getBundle(BASE);
    IPath p = Path.fromOSString(FileLocator.getBundleFile(bundle).getPath());
    return p.addTrailingSeparator().toOSString();
  }

  private int getPort()
  {
    String portString = Services.getPluginResources("org.eclim")
      .getProperty("nailgun.server.port");
    return Integer.parseInt(portString);
  }

  /**
   * Builds the classloader used for third party nailgun extensions dropped into
   * eclim's ext dir.
   *
   * @return The classloader.
   */
  private ClassLoader getExtensionClassLoader()
    throws Exception
  {
    File extdir = new File(FileUtils.concat(Services.DOT_ECLIM, "resources/ext"));
    if (extdir.exists()){
      FileFilter filter = new FileFilter(){
        public boolean accept(File file){
          return file.isDirectory() || file.getName().endsWith(".jar");
        }
      };

      ArrayList<URL> urls = new ArrayList<URL>();
      listFileUrls(extdir, filter, urls);
      return new URLClassLoader(
          urls.toArray(new URL[urls.size()]),
          this.getClass().getClassLoader());
    }
    return null;
  }

  private void listFileUrls(File dir, FileFilter filter, ArrayList<URL> results)
    throws Exception
  {
    File[] files = dir.listFiles(filter);
    for (File file : files) {
      if(file.isFile()){
        results.add(file.toURI().toURL());
      }else{
        listFileUrls(file, filter, results);
      }
    }
  }

  @Override
  public synchronized void frameworkEvent(FrameworkEvent event)
  {
    // We are using a framework INFO event to announce when all the eclim
    // plugins bundles have been started (but not necessarily activated yet).
    Bundle bundle = event.getBundle();
    if (event.getType() == FrameworkEvent.INFO &&
        CORE.equals(bundle.getSymbolicName()))
    {
      logger.info("Loaded plugin org.eclim.core");
      notify();
    }
  }

  private boolean jobsRunning(IJobManager manager)
  {
    Job[] jobs = manager.find(null);
    for (Job job : jobs){
      if (job.getState() == Job.RUNNING){
        logger.debug("Waiting on job: " + job);
        return true;
      }
    }
    logger.info("Jobs finished.");
    return false;
  }

  @SuppressWarnings("unused")
  private class Instance
  {
    private String home;
    private String workspace;
    private int port;

    public Instance(String home, String workspace, int port)
    {
      this.home = home;
      this.workspace = workspace;
      this.port = port;
    }

    public boolean exists()
    {
      return new File(home).exists();
    }

    public boolean equals(Object other)
    {
      if (!(other instanceof Instance)){
        return false;
      }
      Instance otheri = (Instance)other;
      return workspace.equals(otheri.workspace) &&
        home.equals(otheri.home) &&
        port == otheri.port;
    }
  }
}
