// 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 com.google.collide.client.util.dom.eventcapture;

import org.waveprotocol.wave.client.common.util.SignalEvent;

import collide.client.util.Elements;

import com.google.collide.client.util.JsIntegerMap;
import com.google.collide.client.util.SignalEventUtils;
import com.google.collide.client.util.input.CharCodeWithModifiers;
import com.google.common.base.Preconditions;

import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;

/**
 * Provides a mean for registering global hot key bindings, particularly
 * spring-loaded hot keys.
 *
 * <p>All hot keys depend on CTRL being pressed, while other modifiers are not,
 * so as not to interfere with regular typing.
 */
public class GlobalHotKey {

  /**
   * Container for one entry in the HotKey database.
   */
  public static class Data {
    private final String description;
    private final Handler handler;
    private final CharCodeWithModifiers key;

    private Data(CharCodeWithModifiers key, Handler handler, String description) {
      this.key = key;
      this.handler = handler;
      this.description = description;
    }

    public String getDescription() {
      return description;
    }

    public Handler getHandler() {
      return handler;
    }

    public CharCodeWithModifiers getKey() {
      return key;
    }
  }

  /**
   * A handler interface to receive event callbacks.
   */
  public interface Handler {
    /**
     * Called when a hot key is initially pressed.
     *
     * @param event the underlying event
     */
    void onKeyDown(SignalEvent event);
  }

  private static int handlerCount = 0;
  private static JsIntegerMap<Data> handlers;

  // Human readable descriptions for key codes.
  private static CaptureReleaser remover;

  /**
   * Registers a handler to receive notification when the key corresponding to
   * {@code keyCode} is used.
   *
   * <p>Only one handler can be tied to a particular key code. Attempting to
   * register a previously registered code will result in an assertion being
   * raised.
   *
   * @param key the key code with modifiers
   * @param handler a callback handler
   * @param description short human readable description for the action.
   */
  public static void register(CharCodeWithModifiers key, Handler handler, String description) {
    if (handlers == null) {
      remover = addEventListeners();
      handlers = JsIntegerMap.create();
    }
    Data handle = new Data(key, handler, description);
    int keyDigest = key.getKeyDigest();
    Preconditions.checkState(
        handlers.get(keyDigest) == null, "Only one handler can be registered per a key");
    handlers.put(keyDigest, handle);
    ++handlerCount;
  }

  /**
   * Unregisters a previously registered handler for a particular keyCode.
   */
  public static void unregister(CharCodeWithModifiers key) {
    int keyDigest = key.getKeyDigest();
    Preconditions.checkState(
        handlers.get(keyDigest) != null, "No handler is register for this key");
    handlers.erase(keyDigest);
    if (--handlerCount == 0) {
      remover.release();
      remover = null;
      handlers = null;
    }
  }

  private static CaptureReleaser addEventListeners() {

    final EventListener downListener = new EventListener() {
      @Override
      public void handleEvent(Event event) {
        SignalEvent signalEvent = SignalEventUtils.create(event, false);
        if (signalEvent == null) {
          return;
        }
        int keyDigest = CharCodeWithModifiers.computeKeyDigest(signalEvent);
        final Data data = handlers.get(keyDigest);
        if (data == null) {
          return;
        }

        Handler handler = data.getHandler();
        handler.onKeyDown(signalEvent);
        event.preventDefault();
      }
    };

    // Attach the listeners.
    final Element documentElement = Elements.getDocument().getDocumentElement();
    documentElement.addEventListener(Event.KEYDOWN, downListener, true);

    final CaptureReleaser downRemover = new CaptureReleaser() {
      @Override
      public void release() {
        documentElement.removeEventListener(Event.KEYDOWN, downListener, true);
      }
    };

    return new CaptureReleaser() {
      @Override
      public void release() {
        downRemover.release();
      }
    };
  }

  /**
   * This class is automatically instantiated as a singleton through the
   * {@link #register} method.
   */
  private GlobalHotKey() {
    // Do nothing
  }
}
