// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package collide.client.treeview;

import collide.client.util.Elements;

import com.google.gwt.resources.client.CssResource;

import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.KeyboardEvent;
import elemental.html.InputElement;

/**
 * Utility for mutating a tree data model object backing a
 * {@link TreeNodeElement}, in place in a {@link Tree}.
 */
public class TreeNodeMutator<D> {

  /**
   * Encapsulates all needed data to perform a mutation action.
   */
  public interface MutationAction<D> {

    Element getElementForMutation(TreeNodeElement<D> node);

    void onBeforeMutation(TreeNodeElement<D> node);

    void onMutationCommit(TreeNodeElement<D> node, String oldLabel, String newLabel);

    boolean passValidation(TreeNodeElement<D> node, String newLabel);
  }

  /**
   * BaseCss for the mutator.
   */
  public interface Css extends CssResource {
    String nodeNameInput();
  }

  private final EventListener keyListener = new EventListener() {
    @Override
    public void handleEvent(Event evt) {
      KeyboardEvent keyEvent = (KeyboardEvent) evt;
      switch (keyEvent.getKeyCode()) {
        case KeyboardEvent.KeyCode.ENTER:
          commitIfValid(false);
          break;
        case KeyboardEvent.KeyCode.ESC:
          cancel();
          break;
        default:
          return;
      }
      evt.stopPropagation();
    }
  };

  private final EventListener blurListener = new EventListener() {
    @Override
    public void handleEvent(Event evt) {
      commitIfValid(false);
    }
  };

  private static class State<D> {
    final TreeNodeElement<D> node;
    final MutationAction<D> callback;
    final InputElement input;
    final String oldLabel;

    State(TreeNodeElement<D> node, MutationAction<D> callback, InputElement input,
        String oldLabel) {
      this.node = node;
      this.callback = callback;
      this.input = input;
      this.oldLabel = oldLabel;
    }
  }

  private State<D> state;
  private Css css;

  /**
   * @param css can be null
   */
  public TreeNodeMutator(Css css) {
    this.css = css;
  }

  public boolean isMutating() {
    return state != null;
  }

  /**
   * Replaces the nodes text label with an input box to allow the user to
   * rename the node.
   */
  public void enterMutation(TreeNodeElement<D> node, MutationAction<D> callback) {
    // If we are already mutating, return.
    if (isMutating()) {
      return;
    }

    Element element = callback.getElementForMutation(node);
    if (element == null) {
      return;
    }

    String oldLabel = element.getTextContent();
    callback.onBeforeMutation(node);

    // Make a temporary text input box to grab user input, and place it inside
    // the label element.
    InputElement input = Elements.createInputElement();
    if (css != null) {
      input.setClassName(css.nodeNameInput());
    }
    input.setType("text");
    input.setValue(oldLabel);

    // Wipe the content from the element.
    element.setTextContent("");

    // Attach the temporary input box.
    element.appendChild(input);
    input.focus();
    input.select();

    // If we hit enter, commit the action.
    input.addEventListener(Event.KEYUP, keyListener, false);

    // If we lose focus, commit the action.
    input.addEventListener(Event.BLUR, blurListener, false);

    state = new State<D>(node, callback, input, oldLabel);
  }

  /**
   * Cancels the mutation, if any.
   */
  public void cancel() {
    if (!isMutating()) {
      return;
    }
    state.input.setValue(state.oldLabel);
    forceCommit();
  }

  /**
   * Commits the current text if it passes validation, or cancels the mutation.
   */
  public void forceCommit() {
    commitIfValid(true);
  }

  private void commitIfValid(boolean forceCommit) {
    if (!isMutating()) {
      return;
    }

    State<D> oldState = state;
    state = null;

    // Update the node label and commit the change if it passes validation.
    boolean passedValidation =
        oldState.callback.passValidation(oldState.node, oldState.input.getValue());
    if (passedValidation || forceCommit) {

      // Disconnect the handlers first!
      oldState.input.removeEventListener(Event.KEYUP, keyListener, false);
      oldState.input.removeEventListener(Event.BLUR, blurListener, false);

      // Detach the input box. Note that on Chrome this synchronously dispatches
      // a blur event. The guard above saves us.
      oldState.input.removeFromParent();

      String newLabel = passedValidation ? oldState.input.getValue() : oldState.oldLabel;
      oldState.callback.onMutationCommit(oldState.node, oldState.oldLabel, newLabel);
    } else {
      state = oldState;
      state.input.focus();
      state.input.select();
    }
  }
}
