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

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

  Copyright (c) 2010-11 Ben Fry and Casey Reas
  Copyright (c) 2012-15 The Processing Foundation

  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.mode.java;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import processing.app.*;
import processing.app.ui.Editor;
import processing.app.ui.EditorException;
import processing.app.ui.EditorState;
import processing.mode.java.runner.Runner;
import processing.mode.java.tweak.SketchParser;


public class JavaMode extends Mode {

  public Editor createEditor(Base base, String path,
                             EditorState state) throws EditorException {
    return new JavaEditor(base, path, state, this);
  }


  public JavaMode(Base base, File folder) {
    super(base, folder);

    initLogger();
    loadPreferences();
  }


  public String getTitle() {
    return "Java";
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  public File[] getExampleCategoryFolders() {
    return new File[] {
      new File(examplesFolder, "Basics"),
      new File(examplesFolder, "Topics"),
      new File(examplesFolder, "Demos"),
      new File(examplesFolder, "Books")
    };
  }


  public String getDefaultExtension() {
    return "pde";
  }


  public String[] getExtensions() {
    return new String[] { "pde", "java" };
  }


  public String[] getIgnorable() {
    return new String[] {
      "applet",
      "application.macosx",
      "application.windows",
      "application.linux"
    };
  }


  public Library getCoreLibrary() {
    if (coreLibrary == null) {
      File coreFolder = Platform.getContentFile("core");
      coreLibrary = new Library(coreFolder);
//      try {
//        coreLibrary = getLibrary("processing.core");
//        System.out.println("core found at " + coreLibrary.getLibraryPath());
//      } catch (SketchException e) {
//        Base.log("Serious problem while locating processing.core", e);
//      }
    }
    return coreLibrary;
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  /*
  public Runner handleRun(Sketch sketch,
                          RunnerListener listener) throws SketchException {
    final JavaEditor editor = (JavaEditor) listener;
    editor.errorCheckerService.quickErrorCheck();
//    if (enableTweak) {
//      enableTweak = false;
//      return handleTweak(sketch, listener, false);
//    } else {
    return handleLaunch(sketch, listener, false);
//    }
  }


  public Runner handlePresent(Sketch sketch,
                              RunnerListener listener) throws SketchException {
    final JavaEditor editor = (JavaEditor) listener;
    editor.errorCheckerService.quickErrorCheck();
//    if (enableTweak) {
//      enableTweak = false;
//      return handleTweak(sketch, listener, true);
//    } else {
    return handleLaunch(sketch, listener, true);
//    }
  }
  */


  /** Handles the standard Java "Run" or "Present" */
  public Runner handleLaunch(Sketch sketch, RunnerListener listener,
                             final boolean present) throws SketchException {
    JavaBuild build = new JavaBuild(sketch);
//    String appletClassName = build.build(false);
    String appletClassName = build.build(true);
    if (appletClassName != null) {
      final Runner runtime = new Runner(build, listener);
      new Thread(new Runnable() {
        public void run() {
          // these block until finished
          if (present) {
            runtime.present(null);
          } else {
            runtime.launch(null);
          }
        }
      }).start();
      return runtime;
    }
    return null;
  }


  /** Start a sketch in tweak mode */
  public Runner handleTweak(Sketch sketch,
                            RunnerListener listener) throws SketchException {
//                            final boolean present) throws SketchException {
    final JavaEditor editor = (JavaEditor) listener;
//    editor.errorCheckerService.quickErrorCheck();  // done in prepareRun()

    if (isSketchModified(sketch)) {
      editor.deactivateRun();
      Messages.showMessage(Language.text("menu.file.save"),
                           Language.text("tweak_mode.save_before_tweak"));
      return null;
    }

    // first try to build the unmodified code
    JavaBuild build = new JavaBuild(sketch);
//    String appletClassName = build.build(false);
    String appletClassName = build.build(true);
    if (appletClassName == null) {
      // unmodified build failed, so fail
      return null;
    }

    // if compilation passed, modify the code and build again
    // save the original sketch code of the user
    editor.initBaseCode();
    // check for "// tweak" comment in the sketch
    boolean requiresTweak = SketchParser.containsTweakComment(editor.baseCode);
    // parse the saved sketch to get all (or only with "//tweak" comment) numbers
    final SketchParser parser = new SketchParser(editor.baseCode, requiresTweak);

    // add our code to the sketch
    final boolean launchInteractive = editor.automateSketch(sketch, parser);

    build = new JavaBuild(sketch);
    appletClassName = build.build(false);

    if (appletClassName != null) {
      final Runner runtime = new Runner(build, listener);
      new Thread(new Runnable() {
        public void run() {
          // these block until finished
//          if (present) {
//            runtime.present(null);
//          } else {
          runtime.launch(null);
//          }
          // next lines are executed when the sketch quits
          if (launchInteractive) {
            editor.initEditorCode(parser.allHandles, false);
            editor.stopTweakMode(parser.allHandles);
          }
        }
      }).start();

      if (launchInteractive) {
        // replace editor code with baseCode
        editor.initEditorCode(parser.allHandles, false);
        editor.updateInterface(parser.allHandles, parser.colorBoxes);
        editor.startTweakMode();
      }
      return runtime;
    }
    return null;
  }


  // TODO Why is this necessary? Why isn't Sketch.isModified() used?
  static private boolean isSketchModified(Sketch sketch) {
    for (SketchCode sc : sketch.getCode()) {
      if (sc.isModified()) {
        return true;
      }
    }
    return false;
  }


//  public void handleStop() {
//    if (runtime != null) {
//      runtime.close();  // kills the window
//      runtime = null; // will this help?
//    }
//  }


//  public boolean handleExportApplet(Sketch sketch) throws SketchException, IOException {
//    JavaBuild build = new JavaBuild(sketch);
//    return build.exportApplet();
//  }


  public boolean handleExportApplication(Sketch sketch) throws SketchException, IOException {
    JavaBuild build = new JavaBuild(sketch);
    return build.exportApplication();
  }


  /**
   * Any modes that extend JavaMode can override this method to add additional jars to be
   * included in the classpath for code completion and error checking
   * @return searchPath: file-paths separated by File.pathSeparatorChar
   */
  public String getSearchPath() {
    // Java Mode doesn't need any default external jars at the moment.
    return "";
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

  // Merged from ExperimentalMode


  void initLogger() {
    final boolean VERBOSE_LOGGING = true;
    final int LOG_SIZE = 512 * 1024; // max log file size (in bytes)

    Logger globalLogger = Logger.getLogger("");
    //Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); // doesn't work on os x
    if (VERBOSE_LOGGING) {
      globalLogger.setLevel(Level.INFO);
    } else {
      globalLogger.setLevel(Level.WARNING);
    }

    // enable logging to file
    try {
      // settings is writable for built-in modes, mode folder is not writable
      File logFolder = Base.getSettingsFile("debug");
      if (!logFolder.exists()) {
        logFolder.mkdir();
      }
      File logFile = new File(logFolder, "DebugMode.%g.log");
      Handler handler = new FileHandler(logFile.getAbsolutePath(), LOG_SIZE, 10, false);
      globalLogger.addHandler(handler);

    } catch (IOException ex) {
      Logger.getLogger(JavaMode.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SecurityException ex) {
      Logger.getLogger(JavaMode.class.getName()).log(Level.SEVERE, null, ex);
    }
  }


  //ImageIcon classIcon, fieldIcon, methodIcon, localVarIcon;

//  protected void loadIcons() {
//    String iconPath = getContentFile("data").getAbsolutePath() + File.separator + "icons";
//    classIcon = new ImageIcon(iconPath + File.separator + "class_obj.png");
//    methodIcon = new ImageIcon(iconPath + File.separator + "methpub_obj.png");
//    fieldIcon = new ImageIcon(iconPath + File.separator + "field_protected_obj.png");
//    localVarIcon = new ImageIcon(iconPath + File.separator + "field_default_obj.png");
//  }


  static public volatile boolean errorCheckEnabled = true;
  static public volatile boolean warningsEnabled = true;
  static public volatile boolean codeCompletionsEnabled = true;
  static public volatile boolean debugOutputEnabled = false;
  static public volatile boolean errorLogsEnabled = false;
  static public volatile boolean autoSaveEnabled = true;
  static public volatile boolean autoSavePromptEnabled = true;
  static public volatile boolean defaultAutoSaveEnabled = true;
  static public volatile boolean ccTriggerEnabled = false;
//  static public volatile boolean importSuggestEnabled = true;
  static public int autoSaveInterval = 3; //in minutes


  /**
   * After how many typed characters, code completion is triggered
   */
  volatile public static int codeCompletionTriggerLength = 1;

  static public final String prefErrorCheck = "pdex.errorCheckEnabled";
  static public final String prefWarnings = "pdex.warningsEnabled";
  static public final String prefDebugOP = "pdex.dbgOutput";
  static public final String prefErrorLogs = "pdex.writeErrorLogs";
  static public final String prefAutoSaveInterval = "pdex.autoSaveInterval";
  static public final String prefAutoSave = "pdex.autoSave.autoSaveEnabled";
  static public final String prefAutoSavePrompt = "pdex.autoSave.promptDisplay";
  static public final String prefDefaultAutoSave = "pdex.autoSave.autoSaveByDefault";
  static public final String suggestionsFileName = "suggestions.txt";

  static public final String COMPLETION_PREF = "pdex.completion";
  static public final String COMPLETION_TRIGGER_PREF = "pdex.completion.trigger";
  static public final String SUGGEST_IMPORTS_PREF = "pdex.suggest.imports";

//  static volatile public boolean enableTweak = false;

  /**
   * Stores the white list/black list of allowed/blacklisted imports. These are defined in
   * suggestions.txt in java mode folder.
   */
  static public final Map<String, Set<String>> suggestionsMap = new HashMap<>();

  public void loadPreferences() {
    Messages.log("Load PDEX prefs");
    ensurePrefsExist();
    errorCheckEnabled = Preferences.getBoolean(prefErrorCheck);
    warningsEnabled = Preferences.getBoolean(prefWarnings);
    codeCompletionsEnabled = Preferences.getBoolean(COMPLETION_PREF);
//    DEBUG = Preferences.getBoolean(prefDebugOP);
    errorLogsEnabled = Preferences.getBoolean(prefErrorLogs);
    autoSaveInterval = Preferences.getInteger(prefAutoSaveInterval);
//    untitledAutoSaveEnabled = Preferences.getBoolean(prefUntitledAutoSave);
    autoSaveEnabled = Preferences.getBoolean(prefAutoSave);
    autoSavePromptEnabled = Preferences.getBoolean(prefAutoSavePrompt);
    defaultAutoSaveEnabled = Preferences.getBoolean(prefDefaultAutoSave);
    ccTriggerEnabled = Preferences.getBoolean(COMPLETION_TRIGGER_PREF);
//    importSuggestEnabled = Preferences.getBoolean(prefImportSuggestEnabled);
    loadSuggestionsMap();
  }


  public void savePreferences() {
    Messages.log("Saving PDEX prefs");
    Preferences.setBoolean(prefErrorCheck, errorCheckEnabled);
    Preferences.setBoolean(prefWarnings, warningsEnabled);
    Preferences.setBoolean(COMPLETION_PREF, codeCompletionsEnabled);
//    Preferences.setBoolean(prefDebugOP, DEBUG);
    Preferences.setBoolean(prefErrorLogs, errorLogsEnabled);
    Preferences.setInteger(prefAutoSaveInterval, autoSaveInterval);
//    Preferences.setBoolean(prefUntitledAutoSave,untitledAutoSaveEnabled);
    Preferences.setBoolean(prefAutoSave, autoSaveEnabled);
    Preferences.setBoolean(prefAutoSavePrompt, autoSavePromptEnabled);
    Preferences.setBoolean(prefDefaultAutoSave, defaultAutoSaveEnabled);
    Preferences.setBoolean(COMPLETION_TRIGGER_PREF, ccTriggerEnabled);
//    Preferences.setBoolean(prefImportSuggestEnabled, importSuggestEnabled);
  }

  public void loadSuggestionsMap() {
    File suggestionsListFile = new File(getFolder() + File.separator
        + suggestionsFileName);
    if (!suggestionsListFile.exists()) {
      Messages.loge("Suggestions file not found! "
          + suggestionsListFile.getAbsolutePath());
      return;
    }

    try {
      BufferedReader br = new BufferedReader(
                                             new FileReader(suggestionsListFile));
      while (true) {
        String line = br.readLine();
        if (line == null) {
          break;
        }
        line = line.trim();
        if (line.startsWith("#")) {
          continue;
        } else {
          if (line.contains("=")) {
            String key = line.split("=")[0];
            String val = line.split("=")[1];
            if (suggestionsMap.containsKey(key)) {
              suggestionsMap.get(key).add(val);
            } else {
              HashSet<String> set = new HashSet<>();
              set.add(val);
              suggestionsMap.put(key, set);
            }
          }
        }
      }
      br.close();
    } catch (IOException e) {
      Messages.loge("IOException while reading suggestions file:"
          + suggestionsListFile.getAbsolutePath());
    }
  }


  public void ensurePrefsExist() {
    //TODO: Need to do a better job of managing prefs. Think lists.
    if (Preferences.get(prefErrorCheck) == null)
      Preferences.setBoolean(prefErrorCheck, errorCheckEnabled);
    if (Preferences.get(prefWarnings) == null)
      Preferences.setBoolean(prefWarnings, warningsEnabled);
    if (Preferences.get(COMPLETION_PREF) == null)
      Preferences.setBoolean(COMPLETION_PREF, codeCompletionsEnabled);
    if (Preferences.get(prefDebugOP) == null)
//      Preferences.setBoolean(prefDebugOP, DEBUG);
    if (Preferences.get(prefErrorLogs) == null)
      Preferences.setBoolean(prefErrorLogs, errorLogsEnabled);
    if (Preferences.get(prefAutoSaveInterval) == null)
      Preferences.setInteger(prefAutoSaveInterval, autoSaveInterval);
//    if(Preferences.get(prefUntitledAutoSave) == null)
//      Preferences.setBoolean(prefUntitledAutoSave,untitledAutoSaveEnabled);
    if (Preferences.get(prefAutoSave) == null)
      Preferences.setBoolean(prefAutoSave, autoSaveEnabled);
    if (Preferences.get(prefAutoSavePrompt) == null)
      Preferences.setBoolean(prefAutoSavePrompt, autoSavePromptEnabled);
    if (Preferences.get(prefDefaultAutoSave) == null)
      Preferences.setBoolean(prefDefaultAutoSave, defaultAutoSaveEnabled);
    if (Preferences.get(COMPLETION_TRIGGER_PREF) == null)
      Preferences.setBoolean(COMPLETION_TRIGGER_PREF, ccTriggerEnabled);
//    if (Preferences.get(prefImportSuggestEnabled) == null)
//      Preferences.setBoolean(prefImportSuggestEnabled, importSuggestEnabled);
  }


  static public void main(String[] args) {
    processing.app.Base.main(args);
  }
}
