package app.controllers;

import java.net.URL;
import java.util.Collection;
import java.util.ResourceBundle;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.TwoDimensional.Bias;
import org.fxmisc.richtext.model.TwoDimensional.Position;

import app.events.CopyEvent;
import app.events.CutEvent;
import app.events.EditorChangedEvent;

import app.events.LanguageChangedEvent;
import app.events.OpenFileEvent;
import app.events.PasteEvent;
import app.events.RedoEvent;
import app.events.SaveFileEvent;
import app.events.ToggleCommentEvent;
import app.events.ToggleWrapTextEvent;
import app.events.UndoEvent;
import app.events.FileSaveStateChangedEvent;
import app.model.Model;
import app.service.FileOperations;
import app.service.LanguageOperations;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.stage.Stage;

/**
 * A FXML controller that controls the editor component of the UI
 */
public class EditorController implements Initializable, Controller {

  @FXML
  private CodeArea editor;

  private EventBus eventBus;

  /**
   * Initializes the controller, and binds the event of change in editor content
   * to {@link #editorChanged() editorChanged}
   */
  @Override
  public void initialize(URL url, ResourceBundle resourceBundle) {
    editor.setParagraphGraphicFactory(LineNumberFactory.get(editor));

    editor.textProperty().addListener((obs, oldV, newV) -> this.editorChanged());
  }

  @Override
  public void setEventBus(EventBus eventBus) {
    this.eventBus = eventBus;
    this.eventBus.register(this);
  }

  // TODO: document
  public CodeArea getEditor() {
    return editor;
  }

  /**
   * Applies highlighting to the editor.
   * 
   * @param highlighting highlighting data
   */
  private void setHighlighting(StyleSpans<Collection<String>> highlighting) {
    this.editor.setStyleSpans(0, highlighting);
  }

  /**
   * Recalculates and refreshes the syntax highlighting of the editor.
   */
  private void refreshHighlighting() {
    this.setHighlighting(LanguageOperations.syntaxHighlight(this.editor.getText(), Model.getLanguage()));
  }

  /**
   * Uses the {@link app.model.ProgrammingLanguage ProgrammingLanguage} in
   * {@link app.model.Model Model} to determine whether the current line/selection
   * is commented or not, and toggles the comment.
   * 
   * @see app.model.ProgrammingLanguage#commentLine(String)
   *      ProgrammingLanguage.commentLine(line)
   */
  private void toggleComment() {
    // TODO: This logic might need to be moved to LanguageOperations
    if (editor.getSelectedText().equals("")) {
      String currentLine = editor.getText(editor.getCurrentParagraph());

      String newText;
      if (Model.getLanguage().isCommentedLine(currentLine))
        newText = Model.getLanguage().unCommentLine(currentLine);
      else
        newText = Model.getLanguage().commentLine(currentLine);

      editor.replaceText(editor.getCurrentParagraph(), 0, editor.getCurrentParagraph(), currentLine.length(), newText);

    } else { // Comment selection

      String newText;
      if (Model.getLanguage().isCommentedSelection(editor.getSelectedText()))
        newText = Model.getLanguage().unCommentSelection(editor.getSelectedText());
      else
        newText = Model.getLanguage().commentSelection(editor.getSelectedText());

      editor.replaceSelection(newText);

    }
  }

  /**
   * Updates the wraptext setting of the code area
   * 
   * @param isWrapText The new value for the setting
   */
  private void setWrapText(boolean isWrapText) {
    this.editor.setWrapText(isWrapText);
  }

  /**
   * Handles the event whenever the content of the editor is changed.
   */
  private void editorChanged() {
    int offset = this.editor.getCaretPosition();
    Position pos = this.editor.offsetToPosition(offset, Bias.Forward);
    this.eventBus.post(new EditorChangedEvent(pos.getMajor() + 1, pos.getMinor()));

    if (Model.getFileIsSaved())
      this.eventBus.post(new FileSaveStateChangedEvent(false));

    this.refreshHighlighting();
  }

  /**
   * Updates the content of the editor to a specific filepath. Otherwise it will
   * open an error dialog to give the user feedback about what has happened.
   * 
   * @param filePath The path of the file
   */
  public void setEditorContent(String newContent) {
      editor.clear();
      editor.appendText(newContent);
  }

  /**
   * Saving/Writing to the file based on the spesific filepath. Otherwise it will
   * open an error dialog to give the user feedback about what has happened.
   */
  public void saveCodeArea(boolean isNewFile) {
    Stage stage = (Stage) editor.getScene().getWindow();

    isNewFile = Model.getActiveFilePath().isEmpty();

    if (isNewFile && FileOperations.saveFileWithDialog(stage, editor.getText())) {
      this.eventBus.post(new OpenFileEvent(Model.getActiveFilePath()));
      this.eventBus.post(new FileSaveStateChangedEvent(true));
    }
    else if (FileOperations.saveFile(Model.getActiveFilePath().orElseThrow(), editor.getText())) {
      this.eventBus.post(new FileSaveStateChangedEvent(true));
    }    
  }

  /**
   * Checking if all is saved before closing the app. The user can either choose
   * to exit or go back to the application and save.
   */

  /* ------------------------------------------------------------------------ */
  /* SUBSCRIPTIONS */
  /* ------------------------------------------------------------------------ */

  /**
   * Updates Code Area (read from file) whenever the FileSelected is changed
   */
  @Subscribe
  public void handle(OpenFileEvent event) {
    String newContent =
      event
        .getPath()
        .map(path -> FileOperations.readFile(path))
        .orElse("");
    this.setEditorContent(newContent);
  }

  /**
   * Save file (write to file) whenever the save in the menubare is selected
   */
  @Subscribe
  private void handle(SaveFileEvent event) {
    this.saveCodeArea(event.getIsNewFile());
  }

  @Subscribe
  private void handle(LanguageChangedEvent event) {
    this.refreshHighlighting();
  }

  @Subscribe
  public void handle(ToggleCommentEvent event) {
    this.toggleComment();
  }

  @Subscribe
  private void handle(ToggleWrapTextEvent event) {
    this.setWrapText(event.getIsWrapped());
  }

  @Subscribe
  private void handle(UndoEvent event) {
    if (this.editor.isFocused())
      this.editor.undo();
  }

  @Subscribe
  private void handle(RedoEvent event) {
    if (this.editor.isFocused())
      this.editor.redo();
  }

  @Subscribe
  private void handle(CopyEvent event) {
    if (this.editor.isFocused())
      this.editor.copy();
  }

  @Subscribe
  private void handle(CutEvent event) {
    if (this.editor.isFocused())
      this.editor.cut();
  }

  @Subscribe
  private void handle(PasteEvent event) {
    if (this.editor.isFocused())
      this.editor.paste();
  }

}
