/*
 * File    : SingleInstanceJFrame.java
 * Created : 19-feb-2004 14:01
 * By      : fbusquets
 *
 * JClic - Authoring and playing system for educational activities
 *
 * Copyright (C) 2000 - 2018 Francesc Busquets & Departament
 * d'Educacio de la Generalitat de Catalunya
 *
 * 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 2 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 (see the LICENSE file).
 */

package edu.xtec.jclic;

import edu.xtec.util.Check;
import edu.xtec.util.Messages;
import edu.xtec.util.Options;
import edu.xtec.util.ResourceManager;
import edu.xtec.util.StrUtils;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author Francesc Busquets (fbusquets@xtec.cat)
 * @version 13.09.10
 */
public class SingleInstanceJFrame extends javax.swing.JFrame implements Constants {

  public static final String OK = "OK", CANCEL = "CANCEL";
  public static final int DEFAULT_TIMEOUT = 1000;

  RunnableComponent rc;
  javax.swing.JLabel splashLabel;
  String project;
  Options options;
  boolean trace;
  String playerClass;
  SocketThread socketThread;
  boolean initializing;
  boolean armed;

  /** Creates a new instance of SingleInstanceJFrame */
  public SingleInstanceJFrame(String playerClass, String[] args, String windowTitle, String logoIcon, String frameIcon,
      int port) {

    initializing = true;
    setDefaultCloseOperation(javax.swing.JFrame.DO_NOTHING_ON_CLOSE);
    this.playerClass = playerClass;
    options = new Options(this);
    project = loadArgs(args, options);

    if (!checkOtherInstance(port)) {
      build(playerClass, args, windowTitle, frameIcon, logoIcon);
      armed = true;
    }
  }

  public boolean isArmed() {
    return armed;
  }

  public static String loadArgs(String[] args, Options options) {
    String result = null;
    for (String arg : args) {
      if (arg != null && arg.length() > 0) {
        if (arg.startsWith("-")) {
          String key, value = null;
          int k = arg.indexOf('=');
          if (k > 0) {
            key = arg.substring(1, k);
            value = arg.substring(k + 1);
          } else
            key = arg.substring(1);
          options.put(key, value);
        } else
          result = arg;
      }
    }
    return result;
  }

  protected void build(String playerClass, String[] args, String windowTitle, String frameIcon, String logoIcon) {

    trace = options.getBoolean(TRACE);
    if (trace) {
      for (String arg : args)
        System.out.println(arg);
    }

    initComponents();
    setTitle(windowTitle);
    if (frameIcon != null)
      setIconImage(ResourceManager.getImageIcon(frameIcon).getImage());

    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    int scrW = (int) screenSize.getWidth();
    int scrH = (int) screenSize.getHeight();

    splashLabel = new javax.swing.JLabel(" ", ResourceManager.getImageIcon(logoIcon),
        javax.swing.SwingConstants.CENTER);
    splashLabel.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    splashLabel.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    splashLabel.setBackground(BG_COLOR);
    splashLabel.setOpaque(true);

    getContentPane().add(splashLabel, java.awt.BorderLayout.CENTER);
    pack();
    setBounds((scrW - getWidth()) / 2, (scrH - getHeight()) / 3, scrW - 40, scrH - 80);
    setLocation((scrW - getWidth()) / 2, (scrH - getHeight()) / 3);

    if (Check.checkSignature(options, true))
      init();
    else
      System.exit(0);
  }

  protected void init() {

    final javax.swing.RootPaneContainer rpc = this;

    edu.xtec.util.SwingWorker sw = new edu.xtec.util.SwingWorker() {

      final StringBuilder sb = new StringBuilder();

      @Override
      public Object construct() {
        try {
          // build messages
          Messages messages = edu.xtec.util.PersistentSettings.getMessages(options, DEFAULT_BUNDLE);
          messages.addBundle(COMMON_SETTINGS);

          if (splashLabel != null)
            splashLabel.setText(messages.get("LOADING"));

          Class<?> c = Class.forName(playerClass);
          java.lang.reflect.Constructor cons = c.getConstructor(new Class[] { edu.xtec.util.Options.class });
          rc = (RunnableComponent) cons.newInstance(new Object[] { options });
        } catch (Exception ex) {
          sb.append("ERROR: Unable to start!\n").append(ex);
          ex.printStackTrace(System.err);
        }
        return rc;
      }

      @Override
      public void finished() {
        if (getValue() == null) {
          // no player build!
          if (splashLabel != null) {
            String s = sb.substring(0);
            splashLabel.setText(s);
            System.err.println(s);
          }
        } else {
          // remove label and place player
          getContentPane().removeAll();
          splashLabel = null;
          rc.addTo(rpc, java.awt.BorderLayout.CENTER);
          getRootPane().revalidate();
          // load project
          javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
              rc.start(project, null);
              initializing = false;
            }
          });
        }
      }
    };

    if (trace)
      System.out.println(">>> initializing...");

    // launch swingWorker
    sw.start();
  }

  private void initComponents() {
    addWindowListener(new java.awt.event.WindowAdapter() {
      @Override
      public void windowClosing(java.awt.event.WindowEvent evt) {
        if (rc == null || rc.windowCloseRequested()) {
          if (socketThread != null)
            socketThread.stopSocketThread();
          if (rc != null) {
            rc.end();
            rc = null;
          }
          dispose();
        }
      }

      @Override
      public void windowActivated(java.awt.event.WindowEvent evt) {
        if (rc != null)
          rc.activate();
      }

      @Override
      public void windowClosed(java.awt.event.WindowEvent evt) {
        exitForm(evt);
      }
    });
  }

  /** Exit the Application */
  private void exitForm(java.awt.event.WindowEvent evt) {
    if (socketThread != null)
      socketThread.stopSocketThread();
    if (rc != null) {
      rc.end();
      rc = null;
    }
    while (socketThread != null) {
      Thread.yield();
    }
    System.exit(0);
  }

  protected boolean checkOtherInstance(int port) {
    boolean result = false;
    try {
      socketThread = new SocketThread(port, DEFAULT_TIMEOUT);
    } catch (Exception ex) {
      // socket already created!
      // eat exception
    }
    if (socketThread == null) {
      try {
        Socket socket = new Socket(InetAddress.getLocalHost(), port);
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
        pw.println(playerClass);
        pw.println(StrUtils.secureString(project));
        pw.println("");
        pw.flush();
        String response = in.readLine();
        result = OK.equals(response);
        socket.close();
      } catch (Exception ex) {
        System.err.println("Socket error: " + ex);
      }
    } else {
      result = false;
      socketThread.start();
    }
    return result;
  }

  protected class SocketThread extends Thread {

    boolean running;
    boolean inService;
    boolean forceSocketClose;
    ServerSocket ss;
    int socketTimeOut;

    SocketThread(int port, int timeOut) throws IOException {
      ss = new ServerSocket(port);
      socketTimeOut = timeOut;
      running = false;
    }

    @Override
    public void run() {
      try {
        running = true;
        ss.setSoTimeout(1000);
        while (running) {
          try {
            inService = false;
            Socket socket = ss.accept();
            inService = true;
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
            String skPlayerClass = StrUtils.secureString(in.readLine());
            String skArg1 = StrUtils.nullableString(in.readLine());
            String skArg2 = StrUtils.nullableString(in.readLine());

            boolean result = rc != null && !initializing && skPlayerClass.equals(playerClass) && skArg1 != null;

            pw.println(result ? OK : CANCEL);
            pw.flush();
            socket.close();
            if (result)
              rc.newInstanceRequest(skArg1, skArg2);
          } catch (InterruptedIOException ioex) {
            // Timeout. start again...
          } catch (Exception ex) {
            if (!forceSocketClose)
              System.err.println("Socket error: " + ex);
            running = false;
          }
        }
        forceSocketClose = true;
        ss.close();
      } catch (IOException ex) {
        if (!forceSocketClose)
          System.err.println("Server socket error: " + ex);
      }
      running = false;
      inService = false;
      socketThread = null;
    }

    public void stopSocketThread() {
      if (inService) {
        running = false;
      } else {
        try {
          forceSocketClose = true;
          ss.close();
        } catch (IOException ex) {
          // eat exception
        } finally {
          socketThread = null;
          running = false;
        }
      }
    }
  }
}
