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

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

  Copyright (c) 2012 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.experimental;

import galsasson.mode.tweak.SketchParser;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;

import processing.app.Base;
import processing.app.Editor;
import processing.app.EditorState;
import processing.app.Mode;
import processing.app.Preferences;
import processing.app.RunnerListener;
import processing.app.Sketch;
import processing.app.SketchCode;
import processing.app.SketchException;
import processing.mode.java.JavaBuild;
import processing.mode.java.JavaMode;
import processing.mode.java.runner.Runner;


/**
 * Experimental Mode for Processing, combines Debug Mode and XQMode and
 * starts us working toward our next generation editor/debugger setup.
 */
public class ExperimentalMode extends JavaMode {
  public static final boolean VERBOSE_LOGGING = true;
  //public static final boolean VERBOSE_LOGGING = false;
  public static final int LOG_SIZE = 512 * 1024; // max log file size (in bytes)
  public static boolean DEBUG = !true;

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

    // use libraries folder from javamode. will make sketches using core libraries work, as well as import libraries and examples menus
    for (Mode m : base.getModeList()) {
      if (m.getClass() == JavaMode.class) {
        JavaMode jMode = (JavaMode) m;
        librariesFolder = jMode.getLibrariesFolder();
        rebuildLibraryList();
        break;
      }
    }

    // Fetch examples and reference from java mode
    // thx to Manindra (https://github.com/martinleopold/DebugMode/issues/4)
    examplesFolder = Base.getContentFile("modes/java/examples");
    // https://github.com/martinleopold/DebugMode/issues/6
    referenceFolder = Base.getContentFile("modes/java/reference");

    // set logging level
    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(ExperimentalMode.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SecurityException ex) {
      Logger.getLogger(ExperimentalMode.class.getName()).log(Level.SEVERE, null, ex);
    }

    // disable initial chattiness for now
//    // output version from manifest file
//    Package p = ExperimentalMode.class.getPackage();
//    String titleAndVersion = p.getImplementationTitle() + " (v" + p.getImplementationVersion() + ")";
//    //log(titleAndVersion);
//    Logger.getLogger(ExperimentalMode.class.getName()).log(Level.INFO, titleAndVersion);
    loadPreferences();
    loadIcons();
  }


  @Override
  public String getTitle() {
    //return "PDE X";
    return "Java";
  }


  public File[] getKeywordFiles() {
    return new File[] {
      Base.getContentFile("modes/java/keywords.txt")
    };
  }

  public File getContentFile(String path) {
    File file = new File(folder, path);
    if (!file.exists()) {
      // check to see if it's part of the parent Java Mode
      file = new File(Base.getContentFile("modes/java"), path);
    }
    return file;
  }

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

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

  static public final String prefErrorCheck = "pdex.errorCheckEnabled";
  static public final String prefWarnings = "pdex.warningsEnabled";
  static public final String prefCodeCompletionEnabled = "pdex.completion";
  static public final String prefCCTriggerEnabled = "pdex.completion.trigger";
  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 prefImportSuggestEnabled = "pdex.importSuggestEnabled";

  static volatile public boolean enableTweak = false;

  public void loadPreferences() {
    log("Load PDEX prefs");
    ensurePrefsExist();
    errorCheckEnabled = Preferences.getBoolean(prefErrorCheck);
    warningsEnabled = Preferences.getBoolean(prefWarnings);
    codeCompletionsEnabled = Preferences.getBoolean(prefCodeCompletionEnabled);
    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(prefCCTriggerEnabled);
    importSuggestEnabled = Preferences.getBoolean(prefImportSuggestEnabled);
  }

  public void savePreferences() {
    log("Saving PDEX prefs");
    Preferences.setBoolean(prefErrorCheck, errorCheckEnabled);
    Preferences.setBoolean(prefWarnings, warningsEnabled);
    Preferences.setBoolean(prefCodeCompletionEnabled, 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(prefCCTriggerEnabled, ccTriggerEnabled);
    Preferences.setBoolean(prefImportSuggestEnabled, importSuggestEnabled);
  }

  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(prefCodeCompletionEnabled) == null)
      Preferences.setBoolean(prefCodeCompletionEnabled, 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(prefCCTriggerEnabled) == null)
      Preferences.setBoolean(prefCCTriggerEnabled, ccTriggerEnabled);
    if (Preferences.get(prefImportSuggestEnabled) == null)
      Preferences.setBoolean(prefImportSuggestEnabled, importSuggestEnabled);

  }


  /**
   * Create a new editor associated with this mode.
   */
  @Override
  public Editor createEditor(Base base, String path, EditorState state) {
    return new DebugEditor(base, path, state, this);
  }


  /**
   * Load a String value from theme.txt
   *
   * @param attribute the attribute key to load
   * @param defaultValue the default value
   * @return the attributes value, or the default value if the attribute
   * couldn't be loaded
   */
  public String loadThemeString(String attribute, String defaultValue) {
    String newString = theme.get(attribute);
    if (newString != null) {
      return newString;
    }
    Logger.getLogger(getClass().getName()).log(Level.WARNING, "Error loading String: {0}", attribute);
    return defaultValue;
  }


  /**
   * Load a Color value from theme.txt
   *
   * @param attribute the attribute key to load
   * @param defaultValue the default value
   * @return the attributes value, or the default value if the attribute
   * couldn't be loaded
   */
  public Color getThemeColor(String attribute, Color defaultValue) {
    Color newColor = theme.getColor(attribute);
    if (newColor != null) {
      return newColor;
    }
    log("error loading color: " + attribute);
    Logger.getLogger(ExperimentalMode.class.getName()).log(Level.WARNING, "Error loading Color: {0}", attribute);
    return defaultValue;
  }

  protected 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");
    // log("Icons loaded");
  }


  public ClassLoader getJavaModeClassLoader() {
    for (Mode m : base.getModeList()) {
      if (m.getClass() == JavaMode.class) {
        JavaMode jMode = (JavaMode) m;
        return jMode.getClassLoader();
      }
    }
    // badness
    return null;
  }

  /**
   * System.out.println()
   */
  public static final void log(Object message){
    if(ExperimentalMode.DEBUG)
      System.out.println(message);
  }

  /**
   * System.err.println()
   */
  public static final void logE(Object message){
    if(ExperimentalMode.DEBUG)
      System.err.println(message);
  }

  /**
   * System.out.print
   */
  public static final void log2(Object message){
    if(ExperimentalMode.DEBUG)
      System.out.print(message);
  }

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

  // TweakMode code
        @Override
        public Runner handleRun(Sketch sketch, RunnerListener listener) throws SketchException
        {
                if (enableTweak) {
                  enableTweak = false;
                        return handleTweakPresentOrRun(sketch, listener, false);
                }
                else {
                        /* Do the usual (JavaMode style) */
                    JavaBuild build = new JavaBuild(sketch);
                    String appletClassName = build.build(false);
                    if (appletClassName != null) {
                      final Runner runtime = new Runner(build, listener);
                      new Thread(new Runnable() {
                        public void run() {
                          runtime.launch(false);  // this blocks until finished
                        }
                      }).start();
                      return runtime;
                    }
                    return null;
                }
        }

        @Override
        public Runner handlePresent(Sketch sketch, RunnerListener listener) throws SketchException
        {
                if (enableTweak) {
                  enableTweak = false;
                        return handleTweakPresentOrRun(sketch, listener, true);
                }
                else {
                        /* Do the usual (JavaMode style) */
                    JavaBuild build = new JavaBuild(sketch);
                    String appletClassName = build.build(false);
                    if (appletClassName != null) {
                      final Runner runtime = new Runner(build, listener);
                      new Thread(new Runnable() {
                        public void run() {
                          runtime.launch(true);
                        }
                      }).start();
                      return runtime;
                    }
                    return null;
                }
        }

        public Runner handleTweakPresentOrRun(Sketch sketch, RunnerListener listener, boolean present) throws SketchException
        {
                final DebugEditor editor = (DebugEditor)listener;
                final boolean toPresent = present;

                boolean launchInteractive = false;

                if (isSketchModified(sketch)) {
                        editor.deactivateRun();
                        Base.showMessage("Save", "Please save the sketch before running in Tweak Mode.");
                        return null;
                }

                /* first try to build the unmodified code */
                JavaBuild build = new JavaBuild(sketch);
                String appletClassName = build.build(false);
                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
                launchInteractive = editor.automateSketch(sketch, parser.allHandles);

                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() {
                                        runtime.launch(toPresent);  // this blocks until finished

                                        // executed when the sketch quits
                                        editor.initEditorCode(parser.allHandles, false);
                                        editor.stopInteractiveMode(parser.allHandles);
                                }

                        }).start();

                        if (launchInteractive) {

                                // replace editor code with baseCode
                                editor.initEditorCode(parser.allHandles, false);
                                editor.updateInterface(parser.allHandles, parser.colorBoxes);
                                editor.startInteractiveMode();
                        }

                        return runtime;
                }

                return null;
        }

        private boolean isSketchModified(Sketch sketch)
        {
                for (SketchCode sc : sketch.getCode()) {
                        if (sc.isModified()) {
                                return true;
                        }
                }
                return false;
        }

}
