/*
 * $Id: ModuleManager.java 8150 2012-05-05 22:34:07Z uckelman $
 *
 * Copyright (c) 2000-2008 by Rodney Kinney, Joel Uckelman
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
package VASSAL.launch;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;

import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.SwingUtilities;

import org.apache.commons.io.FileUtils;

import VASSAL.Info;
import VASSAL.build.module.metadata.AbstractMetaData;
import VASSAL.build.module.metadata.MetaDataFactory;
import VASSAL.build.module.metadata.SaveMetaData;
import VASSAL.configure.IntConfigurer;
import VASSAL.configure.LongConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslateVassalWindow;
import VASSAL.preferences.Prefs;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.ThrowableUtils;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.logging.LogOutputStreamAdapter;
import VASSAL.tools.logging.LoggedOutputStream;
import VASSAL.tools.logging.Logger;
import VASSAL.tools.menu.MacOSXMenuManager;
import VASSAL.tools.menu.MenuBarProxy;
import VASSAL.tools.menu.MenuManager;

/**
 * Tracks recently-used modules and builds the main GUI window for 
 * interacting with modules.
 * 
 * @author rodneykinney
 * @since 3.1.0
 */
public class ModuleManager {

  private static final String NEXT_VERSION_CHECK = "nextVersionCheck";

  public static final String MAXIMUM_HEAP = "maximumHeap"; //$NON-NLS-1$
  public static final String INITIAL_HEAP = "initialHeap"; //$NON-NLS-1$

  public static void main(String[] args) {
    // parse command-line arguments
    LaunchRequest lr = null;
    try {
      lr = LaunchRequest.parseArgs(args);
    }
    catch (LaunchRequestException e) {
// FIXME: should be a dialog...
      System.err.println("VASSAL: " + e.getMessage());
      System.exit(1);
    }

    // do this before the graphics subsystem fires up or it won't stick
    System.setProperty("swing.boldMetal", "false");

    if (lr.mode == LaunchRequest.Mode.TRANSLATE) {
      // show the translation window in translation mode
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          // FIXME: does this window exit on close?
          new TranslateVassalWindow(null).setVisible(true);
        }
      });
      return;
    }

    //
    // How we start exactly one request server:
    //
    // To ensure that exactly one process acts as the request server, we
    // acquire a lock on the ~/VASSAL/key file, and then attempt to acquire
    // a lock on the ~/VASSAL/lock file. If we cannot lock ~/VASSAL/lock,
    // then there is already a server running; in that case, we read the
    // port number and security key from ~/VASSAL/key. If we can lock
    // ~/VASSAL/lock, then we start the server, write the port number and
    // key to ~/VASSAL/key, and continue to hold the lock on ~/VASSAL/lock.
    // Finally, we unlock ~/VASSAL/key and proceed to act as a client,
    // sending requests over localhost:port using the security key.
    //
    // The advantages of this method are: 
    //
    // (1) No race conditions between processes started at the same time.
    // (2) No port collisions, because we don't use a predetermined port.
    //

    final File keyfile = new File(Info.getConfDir(), "key");
    final File lockfile = new File(Info.getConfDir(), "lock");

    int port = 0;
    long key = 0;

    RandomAccessFile kraf = null;
    FileLock klock = null;
    try {
      // acquire an exclusive lock on the key file
      kraf = new RandomAccessFile(keyfile, "rw");
      
      try {
        klock = kraf.getChannel().lock();
      }
      catch (OverlappingFileLockException e) {
        throw (IOException) new IOException().initCause(e);
      }
      
      // determine whether we are the server or a client

      // Note: We purposely keep lout open in the case where we are the
      // server, because closing lout will release the lock.
      FileLock lock = null;
      final FileOutputStream lout = new FileOutputStream(lockfile);
      try {
        lock = lout.getChannel().tryLock();
      }
      catch (OverlappingFileLockException e) {
        throw (IOException) new IOException().initCause(e);
      }

      if (lock != null) {
        // we have the lock, so we will be the request server

        // bind to an available port on the loopback device
        final ServerSocket serverSocket =
          new ServerSocket(0, 0, InetAddress.getByName(null));
    
        // write the port number where we listen to the key file
        port = serverSocket.getLocalPort(); 
        kraf.writeInt(port);

        // create new security key and write it to the key file
        key = (long) (Math.random() * Long.MAX_VALUE);
        kraf.writeLong(key);

        // create a new Module Manager
        new ModuleManager(serverSocket, key, lout, lock);
      }
      else {
        // we do not have the lock, so we will be a request client
        lout.close();

        // read the port number we will connect to from the key file
        port = kraf.readInt();

        // read the security key from the key file
        key = kraf.readLong();
      }

      kraf.close();
    }
    catch (IOException e) {
// FIXME: should be a dialog...
      System.err.println("VASSAL: IO error");
      e.printStackTrace();
      System.exit(1);
    }
    finally {
      // this will also release the lock on the key file
      IOUtils.closeQuietly(kraf);
    }

    lr.key = key;
    
    // pass launch parameters on to the ModuleManager via the socket
    Socket clientSocket = null;
    ObjectOutputStream out = null;
    InputStream in = null;
    try {
      clientSocket = new Socket((String) null, port);

      out = new ObjectOutputStream(
              new BufferedOutputStream(clientSocket.getOutputStream()));
      out.writeObject(lr);
      out.flush();

      in = clientSocket.getInputStream();
      IOUtils.copy(in, System.err);
    }
    catch (IOException e) {
// FIXME: should be a dialog...
      System.err.println("VASSAL: Problem with socket on port " + port);
      e.printStackTrace();
      System.exit(1);
    }
    finally {
      IOUtils.closeQuietly(in);
      IOUtils.closeQuietly(out);
      IOUtils.closeQuietly(clientSocket);
    }
  }

  private static ModuleManager instance = null;

  public static ModuleManager getInstance() {
    return instance;
  }

  private final long key;

  private FileOutputStream lout;
  private FileLock lock;

  private final ServerSocket serverSocket;

  public ModuleManager(ServerSocket serverSocket, long key,
                       FileOutputStream lout, FileLock lock)
                                                           throws IOException {

    if (instance != null) throw new IllegalStateException();
    instance = this;

    this.serverSocket = serverSocket;
    this.key = key;

    // we hang on to these to prevent the lock from being lost
    this.lout = lout;
    this.lock = lock;

    final StartUp start = Info.isMacOSX() ?
      new ModuleManagerMacOSXStartUp() : new StartUp();

    // start logging to the errorLog
    final File errorLog = new File(Info.getHomeDir(), "errorLog");
    try {
      Logger.addLogListener(
        new LogOutputStreamAdapter(
          new FileOutputStream(errorLog)));
    }
    catch (IOException e) {
      WriteErrorDialog.error(e, errorLog);
    }

    start.startErrorLog();

    // log everything which comes across our stderr
    System.setErr(
      new PrintStream(new LoggedOutputStream(Info.getInstanceID()), true));

    Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());

    start.initSystemProperties();

    // check whether we need to migrate pre-3.1.19 preferences
    final File oldprefs =
      new File(System.getProperty("user.home"), "VASSAL/Preferences");
    if (oldprefs.exists()) {
      final File newprefs = new File(Info.getHomeDir(), "Preferences");
      if (!newprefs.exists()) {
        FileUtils.copyFile(oldprefs, newprefs);
      }
    }

    if (Info.isMacOSX()) new MacOSXMenuManager();
    else new ModuleManagerMenuManager();

    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        launch();
      }
    });

    // ModuleManagerWindow.getInstance() != null now, so listen on the socket
    new Thread(new SocketListener(serverSocket), "socket listener").start();

    final Prefs globalPrefs = Prefs.getGlobalPrefs();

    // determine when we should next check on the current version of VASSAL
    final LongConfigurer nextVersionCheckConfig =
      new LongConfigurer(NEXT_VERSION_CHECK, null, -1L);
    globalPrefs.addOption(null, nextVersionCheckConfig);

    long nextVersionCheck = nextVersionCheckConfig.getLongValue(-1L);
    if (nextVersionCheck < System.currentTimeMillis()) {
        new UpdateCheckRequest().execute();
    }

    // set the time for the next version check
    if (nextVersionCheck == -1L) {
      // this was our first check; randomly check after 0-10 days to
      // to spread version checks evenly over a 10-day period
      nextVersionCheck = System.currentTimeMillis() + 
                         (long) (Math.random() * 10 * 86400000);
    }
    else {
      // check again in 10 days
      nextVersionCheck += 10 * 86400000;
    }

    nextVersionCheckConfig.setValue(nextVersionCheck);

// FIXME: the importer heap size configurers don't belong here
    // the initial heap size for the module importer
    final IntConfigurer initHeapConf = new IntConfigurer(
      INITIAL_HEAP,
      Resources.getString("GlobalOptions.initial_heap"),  //$NON-NLS-1$
      Integer.valueOf(256)
    );
    globalPrefs.addOption("Importer", initHeapConf);

    // the maximum heap size for the module importer
    final IntConfigurer maxHeapConf = new IntConfigurer(
      MAXIMUM_HEAP,
      Resources.getString("GlobalOptions.maximum_heap"),  //$NON-NLS-1$
      Integer.valueOf(512)
    );
    globalPrefs.addOption("Importer", maxHeapConf);
  }
 
  private class SocketListener implements Runnable {
    private final ServerSocket serverSocket;

    public SocketListener(ServerSocket serverSocket) {
      this.serverSocket = serverSocket;
    }

    public void run() {
      try {
        ObjectInputStream in = null;
        PrintStream out = null;
        Socket clientSocket = null;
        while (true) {
          try {
            clientSocket = serverSocket.accept();
            in = new ObjectInputStream(
                  new BufferedInputStream(clientSocket.getInputStream()));
            
            final String message = execute(in.readObject());
            in.close();
            clientSocket.close();

            if (message == null || clientSocket.isClosed()) continue;
           
            out = new PrintStream(
                    new BufferedOutputStream(clientSocket.getOutputStream()));
            out.println(message);
            out.close();
          }
          catch (IOException e) {
            ErrorDialog.showDetails(
              e,
              ThrowableUtils.getStackTrace(e),
              "Error.socket_error"
            );
          }
          catch (ClassNotFoundException e) {
            ErrorDialog.bug(e);
          }
          finally {
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(out);
            IOUtils.closeQuietly(clientSocket);
          }
        }
      }
      finally {
        IOUtils.closeQuietly(serverSocket);
      }
    }
  }

  protected void launch() {
    Logger.log("-- Manager");
    final ModuleManagerWindow window = ModuleManagerWindow.getInstance();
    window.setVisible(true);

    final File prefsFile = new File(Info.getHomeDir(), "Preferences");
    final boolean isFirstTime = !prefsFile.exists();

    if (isFirstTime) new FirstTimeDialog(window).setVisible(true);
  }

  protected String execute(Object req) {
    if (req instanceof LaunchRequest) { 
      final LaunchRequest lr = (LaunchRequest) req;

      if (lr.key != key) {
// FIXME: translate
        return "incorrect key";
      }

      final LaunchRequestHandler handler = new LaunchRequestHandler(lr);
      try {
        SwingUtilities.invokeAndWait(handler);
      }
      catch (InterruptedException e) {
        return "interrupted";   // FIXME 
      }
      catch (InvocationTargetException e) {
        ErrorDialog.bug(e);
        return null;
      }
  
      return handler.getResult();
    }
    else {
      return "unrecognized command";  // FIXME
    }
  }

  private static class LaunchRequestHandler implements Runnable {
    private final LaunchRequest lr;
    private String result;

    public LaunchRequestHandler(LaunchRequest lr) {
      this.lr = lr;
    }   
  
    public void run() {
      result = handle();
    } 

    public String getResult() {
      return result;
    }

    private String handle() {
      final ModuleManagerWindow window = ModuleManagerWindow.getInstance();

      switch (lr.mode) {
      case MANAGE:  
        window.toFront();
        break;
      case LOAD:
        if (Player.LaunchAction.isEditing(lr.module))
          return "module open for editing";   // FIXME
    
        if (lr.module == null && lr.game != null) {
          // attempt to find the module for the saved game or log
          final AbstractMetaData data = MetaDataFactory.buildMetaData(lr.game);
          if (data != null && data instanceof SaveMetaData) {
            // we found save metadata
            final String moduleName = ((SaveMetaData) data).getModuleName();
            if (moduleName != null && moduleName.length() > 0) {
              // get the module file by module name 
              lr.module = window.getModuleByName(moduleName);
            }
            else {
              // this is a pre 3.1 save file, can't tell the module name
// FIXME: show some error here
              return "cannot find module";
            }
          }
        }

        if (lr.module == null) {
          return "cannot find module";
// FIXME: show some error here
        }
        else if (lr.game == null) {
          new Player.LaunchAction(window, lr.module).actionPerformed(null);
        }
        else { 
          new Player.LaunchAction(
            window, lr.module, lr.game).actionPerformed(null);
        }
        break;
      case EDIT:
        if (Editor.LaunchAction.isInUse(lr.module))
          return "module open for play";      // FIXME
        if (Editor.LaunchAction.isEditing(lr.module))
          return "module open for editing";   // FIXME
  
        new Editor.LaunchAction(window, lr.module).actionPerformed(null);
        break;
      case IMPORT:
        new Editor.ImportLaunchAction(
          window, lr.importFile).actionPerformed(null);
        break;
      case NEW:
        new Editor.NewModuleLaunchAction(window).actionPerformed(null);
        break;
      case EDIT_EXT:
        return "not yet implemented";   // FIXME
      case NEW_EXT:
        return "not yet implemented";   // FIXME
      default:
        return "unrecognized mode";     // FIXME
      }

      return null;
    }
  }

  private static class ModuleManagerMenuManager extends MenuManager {
    private final MenuBarProxy menuBar = new MenuBarProxy();

    @Override
    public JMenuBar getMenuBarFor(JFrame fc) {
      return (fc instanceof ModuleManagerWindow) ? menuBar.createPeer() : null;
    }

    @Override
    public MenuBarProxy getMenuBarProxyFor(JFrame fc) {
      return (fc instanceof ModuleManagerWindow) ? menuBar : null;
    }
  }
}
