package com.marinilli.b2.c6.bank;

import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.io.*;
import java.net.MalformedURLException;
import java.net.Socket;
import java.util.Properties;

/**
 * Chapter 6 - The Application Helper
 *
 * @author Mauro Marinilli
 * @version 1.0
 */

public class ApplicationHelper {
  private BankClientLoader loader;
  private Socket socket;
  private DataOutputStream out;
  private DataInputStream in;
  private String thisClientId;
  private Properties appHelperSettings;
  private final static String PROPS_FILE_NAME = "appHelper.properties";
  public final static String CLIENT_DIR = "clientdir/";
  public final static String CACHE_DIR = CLIENT_DIR + "cached/";
  private final static String DEFAULT_MAIN_CLASS_NAME = "com.marinilli.b2.c6.bank.BankClient";
  private String OFFLINE_ALLOWED = "offline-allowed";
  private String CLIENT_ID = "id";
  private String launchClassName;
  private boolean offline = false;

  /**
   * Constructor
   */
  public ApplicationHelper() {
    loadProperties();
    thisClientId = appHelperSettings.getProperty(CLIENT_ID, "client0");
    try {
      socket = new Socket("localhost", 3333);
      out = new DataOutputStream(socket.getOutputStream());
      in = new DataInputStream(socket.getInputStream());
    } catch (Exception e) {
      System.out.println("ApplicationHelper()- Couldn't work out the connection: "+e);
      offline = true;
      if (appHelperSettings.getProperty(OFFLINE_ALLOWED).equalsIgnoreCase("false")){
        System.out.println("ApplicationHelper()- Cannot execute offline.");
        System.exit(-1);
      }
    }
    try {
      if (!offline)
        check();
    } catch (Exception e) {
      System.out.println("ApplicationHelper()- check()"+e);
    }
  }

  /**
   * It implements the check command
   */
  public void check() throws IOException{
    try {
      out.writeChar('c');
      out.writeUTF(thisClientId);
      int newItems = in.readInt();
      if (newItems!=0) {
        //there is a new version in sight
        //we must download it
        byte[] b;
        for (int i = 0; i < newItems; i++) {
          // download a new file in cache
          String fileName = in.readUTF();
          int size = in.readInt();
          b = new byte[size];
          in.readFully(b);
          cache(fileName, b);
        }
        //read the class to be launched
        String launch = in.readUTF();
        if (launch!="")
          launchClassName = launch;
      }//-(newItems!=0)
    } catch (Exception e) {
      System.out.println("ApplicationHelper- check() "+e);
    }
  }

  /**
   * check if a file is in cache
   */
  protected boolean isCached(String item){
    File file = new File(CACHE_DIR);
    if(file.exists() && file.isDirectory()) {
      File[] dirContent = file.listFiles();
      for(int i = 0; i < dirContent.length; i++) {
        if(dirContent[i].getName().equals(item))
          return true;
      }//-for
    }
    return false;
  }

  /**
   * It stores a file in the local cache
   */
  protected void cache(String name, byte[] b) {
    try {
      FileOutputStream fos = new FileOutputStream(CACHE_DIR+name);
      fos.write(b);
      fos.close();
    } catch (IOException exce) {
      System.out.println("ApplicationHelper- cache("+name+") "+exce);
    }
    System.out.println("ApplicationHelper- saved in cache: "+name);
  }

  /**
   * It gets a file from the cache directory
   * @return a File
   */
  public File getCachedFile(String filename){
    File file = null;
    try {
      file = new File(CACHE_DIR, filename);
    } catch (Exception e) {
      System.out.println("ApplicationHelper- getFile(): " + e);
    }
    return file;
  }


  /**
   * transform a fully qual. class name in a simpler name
   */
  protected String getCacheClassFileName(String className) {
    String result = className;
    if (className.indexOf('.')!=-1)
      result =className.substring(className.lastIndexOf('.')+1);
    return result;
  }

  /**
   * Loads properties file
   */
  private void loadProperties(){
    try {
      appHelperSettings = new Properties();
      FileInputStream in = new FileInputStream(CLIENT_DIR+PROPS_FILE_NAME);
      appHelperSettings.load(in);
      launchClassName = appHelperSettings.getProperty("launchClass", DEFAULT_MAIN_CLASS_NAME);
      in.close();
    }
    catch (IOException ex) {
      System.out.println("ApplicationHelper- loadProperties(): "+ex);
    }
  }

  /**
   * this method downloads and caches a file, taking advantage of the GET command
   *
   */
  public File getRemoteFile(String serverFilename, boolean isClass) throws IOException {
    out.writeChar('g');
    out.writeUTF(serverFilename);
    int size = in.readInt();
    byte[] resBytes = new byte[size];
    in.readFully(resBytes);

    if (resBytes.length!=0) {
      String cacheName = serverFilename;
      if (isClass)
        cacheName += ".class";
      cache(cacheName, resBytes);
      return getCachedFile(cacheName);
    }
    // file not found
    throw new FileNotFoundException("File " + serverFilename +
              " not found neither in cache nor in the server.");
  }

  /**
   * launch the app via main() method
   */
  private boolean mainMethodLaunch(Class mainClass){
    boolean launched = true;
    try {
      Class[] types = { Class.forName("[Ljava.lang.String;") };
      Method mainMethod = mainClass.getDeclaredMethod("main", types);
      Object[] p = new Object[0];
      mainMethod.invoke(null, p);
      System.out.println("ApplicationHelper- appl. launched invoking: "+mainMethod);
    } catch (Exception ex) {
      // ok, no main() method
      launched = false;
    }
    return launched;
  }

  /**
   * launch the app via constructor(s)
   */
  private boolean creationLaunch(Class mainClass){
    boolean launched = true;
    // first, try default constructor
    try {
      Object ob = mainClass.newInstance();
      System.out.println("ApplicationHelper- appl. launched creating: "+mainClass+"( )");
    }
    catch (Exception ex) {
      // it doesn't support the default constructor
      // just use the first of its constructors
      try {
        Constructor cons = mainClass.getConstructors()[0];
        int paramNumber = cons.getParameterTypes().length;
        Object[] arg = new Object[paramNumber];
        Class[] param = cons.getParameterTypes();
        for (int i = 0; i < paramNumber ; i++) {
          //for each parameter, initializes it using its default constructor
          arg[i] = param[i].newInstance();
        }
        cons.newInstance(arg);
        System.out.println("ApplicationHelper- appl. launched creating: "+cons);
      } catch (Exception exc){
        launched = false;
      }
    }
    return launched;
  }

  /**
   * This method launches the whole application
   *
   */
  public void launch() {
    boolean successfullyLaunched = false;
    loader = new BankClientLoader(this);
    try {
      Class mainClass =
          loader.loadClass(launchClassName, loader.getClass(launchClassName));

      // first, try to use the main() method
      successfullyLaunched = mainMethodLaunch(mainClass);

      // if not present, try to create an instance
      if (!successfullyLaunched)
        successfullyLaunched = creationLaunch(mainClass);

      if (!successfullyLaunched)
        System.out.println("ApplicationHelper- unable to launch application.");

      // after launched class finished, exit application
      shutDown();

    } catch (Exception e) {
      System.out.println("ApplicationHelper- Launch Failed. "+e);
      e.printStackTrace();
    }
  }

  /**
   * This method shuts down the whole application
   *
   */
  private void shutDown() throws IOException {
    System.out.println("ApplicationHelper-  closing application.");
    out.writeChar('q');
    out.close();
    in.close();
    socket.close();
    saveProperties();
  }

  /**
   * save deployment properties
   */
  private void saveProperties(){
    try {
      FileOutputStream out = new FileOutputStream(CLIENT_DIR+PROPS_FILE_NAME);
      appHelperSettings.store(out, "-- Application Helper Properties file --");
      out.close();
    }
    catch (IOException ex) {
      System.out.println("ApplicationHelper- saveProperties(): "+ex);
    }
  }

  /**
   * Main method that launches the appl. helper and consequently,
   * the whole application
   */
  public static void main(String[] args) {
    ApplicationHelper applicationHelper = new ApplicationHelper();
    applicationHelper.launch();
  }
}