// 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.status;

import collide.client.util.Elements;

import com.google.collide.json.client.JsoArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;

import elemental.client.Browser;
import elemental.html.AnchorElement;
import elemental.html.SpanElement;

/**
 * This is the base of all message types.
 *
 *  All common message properties are set through this base class. Message
 * events are routed through double-dispatch in the protected do*() methods
 * while their public facing counterparts ensure that the base message state is
 * consistent (e.g. setting canceled).
 */
public class StatusMessage {
  /**
   * A loading message indicates that the application is waiting for a slow
   * operation.
   *
   *  A confirmation message is an informational message to the user.
   *
   *  An error message indicates that there has been a recoverable or transient
   * error.
   *
   *  A fatal message indicates that the application has entered into an
   * unrecoverable state (for example, an uncaught exception). Once a fatal
   * message fires, no other subsequent status messages will fire.
   *
   * NOTE: These message types are in order of priority.
   */
  public enum MessageType {
    LOADING, //
    CONFIRMATION, //
    ERROR, //
    FATAL
  }

  // The default delay to use for avoiding message flicker.
  public static final int DEFAULT_DELAY = 200;

  public static final StatusAction RELOAD_ACTION = new StatusAction() {
    @Override
    public void renderAction(SpanElement actionContainer) {
      actionContainer.setTextContent("Reload Collide");
    }

    @Override
    public void onAction() {
      Browser.getWindow().getLocation().reload();
    }
  };

  public static final StatusAction FEEDBACK_ACTION = new StatusAction() {
    @Override
    public void renderAction(SpanElement actionContainer) {
      AnchorElement a = Elements.createAnchorElement();
      a.setHref(
          "https://groups.google.com/forum/?domain=google.com#!newtopic/collide-discussions");
      a.setTarget("_blank");
      a.setTextContent("Tell us what happened!");   
      a.getStyle().setColor("yellow");
      actionContainer.appendChild(a);
    }

    @Override
    public void onAction() {
      // Nothing. Let the native anchor do its thing.
    }
  };

  private final JsoArray<StatusAction> actions = JsoArray.create();
  private boolean canceled = false;
  private boolean dismissable = false;
  private long expiryTime = 0;
  private String longText;
  private final StatusManager statusManager;
  private String text;

  private final MessageType type;

  public StatusMessage(StatusManager statusManager, MessageType type, String text) {
    this.statusManager = statusManager;
    this.type = type;
    this.text = text;
  }

  /**
   * Cancel a message. Once a message is canceled, all subsequent fires are
   * no-ops.
   */
  public final void cancel() {
    canceled = true;
    statusManager.cancel(this);
  }

  /**
   * Cancel an event in the future.
   *
   * @param milliseconds time to expiry.
   */
  public final void expire(int milliseconds) {
    expiryTime = System.currentTimeMillis() + milliseconds;
    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
      @Override
      public boolean execute() {
        cancel();
        return false;
      }
    }, milliseconds);
  }

  /**
   * Fires a message to the status manager. If the message has been canceled,
   * this is a no-op. If the message has already been fired, then update that
   * message.
   */
  public final void fire() {
    if (!canceled) {
      statusManager.fire(this);
    }
  }

  /**
   * Fire a message with a delay. If the message is canceled before the delay,
   * this is a no-op.
   *
   * @param milliseconds time to delay firing this message.
   */
  public final void fireDelayed(int milliseconds) {
    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
      @Override
      public boolean execute() {
        fire();
        return false;
      }
    }, milliseconds);
  }

  /**
   * @return the actions associated with this message or null
   */
  public JsoArray<StatusAction> getActions() {
    return actions;
  }

  public String getLongText() {
    return longText == null ? "" : longText;
  }

  /**
   * Concrete implementations can reference the {@link StatusManager} through
   * this getter.
   *
   * @return the @{link StatusManager} for this message.
   */
  protected final StatusManager getStatusManager() {
    return statusManager;
  }

  public String getText() {
    return text == null ? "" : text;
  }

  public void setText(String text) {
    this.text = text;
  }

  /**
   * @return return the time in milliseconds left until expiration or 0 if there
   *         is no pending expiration.
   */
  public int getTimeToExpiry() {
    return (int) (expiryTime == 0 ? 0 : Math.max(0, expiryTime - System.currentTimeMillis()));
  }

  public MessageType getType() {
    return type;
  }

  /**
   * @return Can the user manually dismiss this message?
   */
  public boolean isDismissable() {
    return dismissable;
  }

  public void addAction(StatusAction action) {
    this.actions.add(action);
  }

  /**
   * @param dismissable whether or not this message can be dismissed by the
   *        user.
   */
  public void setDismissable(boolean dismissable) {
    this.dismissable = dismissable;
  }

  /**
   * The status message must fit on one line, but the message can be expanded to
   * show more detailed information like a stack trace using long text.
   *
   * @param longText
   */
  public void setLongText(String longText) {
    this.longText = longText;
  }
}
