package processing.app.ui;

import java.awt.EventQueue;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.swing.JOptionPane;

import processing.app.Messages;
import processing.app.Preferences;
import processing.app.Sketch;
import processing.app.SketchCode;


public class ChangeDetector implements WindowFocusListener {
  private final Sketch sketch;
  private final Editor editor;

  private List<String> ignoredAdditions = new ArrayList<>();
  private List<SketchCode> ignoredRemovals = new ArrayList<>();

  // Windows and others seem to have a few hundred ms difference in reported
  // times, so we're arbitrarily setting a gap in time here.
  // Mac OS X has an (exactly) one second difference. Not sure if it's a Java
  // bug or something else about how OS X is writing files.
  static private final int MODIFICATION_WINDOW_MILLIS =
    Preferences.getInteger("editor.watcher.window");

  // Debugging this feature is particularly difficult, adding an option for it
  static private final boolean DEBUG =
    Preferences.getBoolean("editor.watcher.debug");


  public ChangeDetector(Editor editor) {
    this.sketch = editor.sketch;
    this.editor = editor;
  }


  @Override
  public void windowGainedFocus(WindowEvent e) {
    if (Preferences.getBoolean("editor.watcher")) {
      if (sketch != null) {
        // make sure the sketch folder exists at all.
        // if it does not, it will be re-saved, and no changes will be detected
        sketch.ensureExistence(); // <- touches UI, stay on EDT

        // TODO: Not sure if we even need to run this async. Usually takes
        //   just a few ms and we probably want to prevent any changes from
        //   users until the external changes are sorted out. [jv 2016-12-05]

        // Run task in common pool, starting threads directly is so Java 6
        ForkJoinPool.commonPool().execute(this::checkFiles);
      }
    }
  }


  @Override
  public void windowLostFocus(WindowEvent e) {
    // Shouldn't need to do anything here, and not storing anything here b/c we
    // don't want to assume a loss of focus is required before change detection
  }


  // Synchronize, we are running async and touching fields
  private synchronized void checkFiles() {

    List<String> filenames = new ArrayList<>();
    sketch.getSketchCodeFiles(filenames, null);

    SketchCode[] codes = sketch.getCode();

    // Separate codes with and without files
    Map<Boolean, List<SketchCode>> existsMap = Arrays.stream(codes)
        .collect(Collectors.groupingBy(code -> filenames.contains(code.getFileName())));


    // ADDED FILES

    List<String> codeFilenames = Arrays.stream(codes)
        .map(SketchCode::getFileName)
        .collect(Collectors.toList());

    // Get filenames which are in filesystem but don't have code
    List<String> addedFilenames = filenames.stream()
        .filter(f -> !codeFilenames.contains(f))
        .collect(Collectors.toList());

    // Show prompt if there are any added files which were not previously ignored
    boolean added = addedFilenames.stream()
        .anyMatch(f -> !ignoredAdditions.contains(f));


    // REMOVED FILES

    // Get codes which don't have file
    List<SketchCode> removedCodes = Optional.ofNullable(existsMap.get(Boolean.FALSE))
        .orElse(Collections.emptyList());

    // Show prompt if there are any removed codes which were not previously ignored
    boolean removed = removedCodes.stream()
        .anyMatch(code -> !ignoredRemovals.contains(code));


    /// MODIFIED FILES

    // Get codes which have file with different modification time
    List<SketchCode> modifiedCodes = Optional.ofNullable(existsMap.get(Boolean.TRUE))
        .orElse(Collections.emptyList())
        .stream()
        .filter(code -> {
          long fileLastModified = code.getFile().lastModified();
          long codeLastModified = code.getLastModified();
          long diff = fileLastModified - codeLastModified;
          return fileLastModified == 0L || diff > MODIFICATION_WINDOW_MILLIS;
        })
        .collect(Collectors.toList());

    // Show prompt if any open codes were modified
    boolean modified = !modifiedCodes.isEmpty();


    boolean ask = added || removed || modified;

    if (DEBUG) {
      System.out.println("ask: " + ask + "\n" +
                             "added filenames: " + addedFilenames + ",\n" +
                             "ignored added: " + ignoredAdditions + ",\n" +
                             "removed codes: " + removedCodes + ",\n" +
                             "ignored removed: " + ignoredRemovals + ",\n" +
                             "modified codes: " + modifiedCodes);
    }


    // This has to happen in one go and also touches UI everywhere. It has to
    // run on EDT, otherwise windowGainedFocus callback runs again right after
    // dismissing the prompt and we get another prompt before we even finished.
    try {
      // Wait for EDT to finish its business
      // We need to stay in synchronized scope because of ignore lists
      EventQueue.invokeAndWait(() -> {
        // Show prompt if something interesting happened
        if (ask && showReloadPrompt()) {
          // She said yes!!!
          if (sketch.getMainFile().exists()) {
            sketch.reload();
            editor.rebuildHeader();
          } else {
            // If the main file was deleted, and that's why we're here,
            // then we need to re-save the sketch instead.
            // Mark everything as modified so that it saves properly
            for (SketchCode code : codes) {
              code.setModified(true);
            }
            try {
              sketch.save();
            } catch (Exception e) {
              //if that didn't work, tell them it's un-recoverable
              Messages.showError("Reload Failed", "The main file for this sketch was deleted\n" +
                  "and could not be rewritten.", e);
            }
          }

          // Sketch was reloaded, clear ignore lists
          ignoredAdditions.clear();
          ignoredRemovals.clear();

          return;
        }

        // Update ignore lists to get rid of old stuff
        ignoredAdditions = addedFilenames;
        ignoredRemovals = removedCodes;

        // If something changed, set modified flags and modification times
        if (!removedCodes.isEmpty() || !modifiedCodes.isEmpty()) {
          Stream.concat(removedCodes.stream(), modifiedCodes.stream())
              .forEach(code -> {
                code.setModified(true);
                code.setLastModified();
              });

          // Not sure if this is needed
          editor.rebuildHeader();
        }
      });
    } catch (InterruptedException ignore) {
    } catch (InvocationTargetException e) {
      Messages.loge("exception in ChangeDetector", e);
    }

  }


  /**
   * Prompt the user whether to reload the sketch. If the user says yes,
   * perform the actual reload.
   * @return true if user said yes, false if they hit No or closed the window
   */
  private boolean showReloadPrompt() {
    int response = Messages
        .showYesNoQuestion(editor, "File Modified",
                           "Your sketch has been modified externally.<br>" +
                               "Would you like to reload the sketch?",
                           "If you reload the sketch, any unsaved changes will be lost.");
    return response == JOptionPane.YES_OPTION;
  }
}
