/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2013 The Processing Foundation
  Copyright (c) 2011-12 Ben Fry and Casey Reas

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.

  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, write to the Free Software Foundation, Inc.
  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package processing.app.contrib;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import processing.app.Base;
import processing.app.Mode;
import processing.app.Util;


public class ModeContribution extends LocalContribution {
  private Mode mode;


  static public ModeContribution load(Base base, File folder) {
    return load(base, folder, null);
  }


  static public ModeContribution load(Base base, File folder,
                                      String searchName) {
    try {
      return new ModeContribution(base, folder, searchName);

    } catch (IgnorableException ig) {
      Base.log(ig.getMessage());

    } catch (Throwable err) {
      // Throwable to catch Exceptions or UnsupportedClassVersionError et al
      if (searchName == null) {
        //err.printStackTrace(System.out);
        // for 3.0b1, pass this through to the Contribution Manager so that
        // we can provide better error messages
        throw new RuntimeException(err);

      } else {
        // For the built-in modes, don't print the exception, just log it
        // for debugging. This should be impossible for most users to reach,
        // but it helps us load experimental mode when it's available.
        Base.loge("ModeContribution.load() failed for " + searchName, err);
      }
    }
    return null;
  }


  /**
   *
   * @param base the base object that this will be tied to
   * @param folder location inside the sketchbook modes folder or contrib
   * @param className name of class and full package, or null to use default
   * @throws Exception
   */
  private ModeContribution(Base base, File folder,
                           String className) throws Exception {
    super(folder);
    className = initLoader(base, className);
    if (className != null) {
      Class<?> modeClass = loader.loadClass(className);
      Base.log("Got mode class " + modeClass);
      Constructor con = modeClass.getConstructor(Base.class, File.class);
      mode = (Mode) con.newInstance(base, folder);
      mode.setClassLoader(loader);
      if (base != null) {
        mode.setupGUI();
      }
    }
  }


  /**
   * Method to close the ClassLoader so that the archives are no longer "locked"
   * and a mode can be removed without restart.
   */
  public void clearClassLoader(Base base) {
    List<ModeContribution> contribModes = base.getModeContribs();
    int botherToRemove = contribModes.indexOf(this);
    if (botherToRemove != -1) { // The poor thing isn't even loaded, and we're trying to remove it...
      contribModes.remove(botherToRemove);

      try {
        ((URLClassLoader) loader).close();
        // The typecast should be safe, since the only case when loader is not of
        // type URLClassLoader is when no archives were found in the first
        // place...
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }


  static public void loadMissing(Base base) {
    File modesFolder = Base.getSketchbookModesFolder();
    List<ModeContribution> contribModes = base.getModeContribs();

    Map<File, ModeContribution> existing = new HashMap<File, ModeContribution>();
    for (ModeContribution contrib : contribModes) {
      existing.put(contrib.getFolder(), contrib);
    }
    File[] potential = ContributionType.MODE.listCandidates(modesFolder);
    // If modesFolder does not exist or is inaccessible (folks might like to
    // mess with folders then report it as a bug) 'potential' will be null.
    if (potential != null) {
      for (File folder : potential) {
        if (!existing.containsKey(folder)) {
          try {
            contribModes.add(new ModeContribution(base, folder, null));
          } catch (NoSuchMethodError nsme) {
            System.err.println(folder.getName() + " is not compatible with this version of Processing");
//            System.err.println(nsme.getMessage());
          } catch (NoClassDefFoundError ncdfe) {
            System.err.println(folder.getName() + " is not compatible with this version of Processing");
//            System.err.println(ncdfe.getMessage());
          } catch (IgnorableException ig) {
            Base.log(ig.getMessage());
          } catch (Throwable e) {
            e.printStackTrace();
          }
        }
      }
    }

    // This allows you to build and test your Mode code from Eclipse.
    // -Dusemode=com.foo.FrobMode:/path/to/FrobMode
    final String useMode = System.getProperty("usemode");
    if (useMode != null) {
      final String[] modeInfo = useMode.split(":", 2);
      final String modeClass = modeInfo[0];
      final String modeResourcePath = modeInfo[1];
      System.out.println("Attempting to load " + modeClass + " with resources at " + modeResourcePath);
      contribModes.add(ModeContribution.load(base, new File(modeResourcePath), modeClass));
    }
  }


  public Mode getMode() {
    return mode;
  }


  public ContributionType getType() {
    return ContributionType.MODE;
  }


  public boolean equals(Object o) {
    if (o == null || !(o instanceof ModeContribution)) {
      return false;
    }
    ModeContribution other = (ModeContribution) o;
    return loader.equals(other.loader) && mode.equals(other.getMode());
  }

  public String initLoader(Base base, String className) throws Exception {

    File modeDirectory = new File(folder, getTypeName());
    if (modeDirectory.exists()) {
      Base.log("checking mode folder regarding " + className);
      // If no class name specified, search the main <modename>.jar for the
      // full name package and mode name.
      if (className == null) {
        String shortName = folder.getName();
        File mainJar = new File(modeDirectory, shortName + ".jar");
        if (mainJar.exists()) {
          className = findClassInZipFile(shortName, mainJar);
        } else {
          throw new IgnorableException(mainJar.getAbsolutePath() + " does not exist.");
        }

        if (className == null) {
          throw new IgnorableException("Could not find " + shortName +
                                       " class inside " + mainJar.getAbsolutePath());
        }
      }

      ArrayList<URL> extraUrls = new ArrayList<>();
      if (imports != null && imports.size() > 0) {
        // if the mode has any dependencies (defined as imports in mode.properties),
        // add the dependencies to the classloader

        HashMap<String, Mode> installedModes = new HashMap<>();
        for(Mode m: base.getModeList()){
          // Base.log("Mode contrib: " + m.getClass().getName() + " : "+ m.getFolder());
          installedModes.put(m.getClass().getName(), m);
        }

        for(String modeImport: imports){
          if (installedModes.containsKey(modeImport)) {
            Base.log("Found mode dependency " + modeImport);
            File modeFolder = installedModes.get(modeImport).getFolder();
            File[] archives = Util.listJarFiles(new File(modeFolder, "mode"));
            if (archives != null && archives.length > 0) {
              for (int i = 0; i < archives.length; i++) {
                // Base.log("Adding jar dependency: " + archives[i].getAbsolutePath());
                extraUrls.add(archives[i].toURI().toURL());
              }
            }
          } else {
            throw new IgnorableException("Dependency mode "+ modeImport + " could not be"
              + " found. Can't load " + className);
          }
        }
      }

      // Add .jar and .zip files from the "mode" folder into the classpath
      File[] archives = Util.listJarFiles(modeDirectory);
      if (archives != null && archives.length > 0) {
        int arrLen = archives.length + extraUrls.size();
        URL[] urlList = new URL[arrLen];

        int j = 0;
        for (; j < extraUrls.size(); j++) {
          //Base.log("Found archive " + archives[j] + " for " + getName());
          urlList[j] = extraUrls.get(j);
        }

        for (int k = 0; k < archives.length; k++,j++) {
          Base.log("Found archive " + archives[k] + " for " + getName());
          urlList[j] = archives[k].toURI().toURL();
        }

        loader = new URLClassLoader(urlList);
        Base.log("loading above JARs with loader " + loader);
//        System.out.println("listing classes for loader " + loader);
//        listClasses(loader);
      }
    }

    // If no archives were found, just use the regular ClassLoader
    if (loader == null) {
      loader = Thread.currentThread().getContextClassLoader();
    }
    return className;
  }


//  static protected List<File> discover(File folder) {
//    File[] folders = listCandidates(folder, "mode");
//    if (folders == null) {
//      return new ArrayList<File>();
//    } else {
//      return Arrays.asList(folders);
//    }
//  }
}