/*
 * Copyright 2008 Google Inc.
 * 
 * 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.gwt.widgetideas.client;

import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.WindowResizeListener;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A collection of {@link ResizableWidget} that periodically checks the outer
 * dimensions of a widget and redraws it as necessary. Every
 * {@link ResizableWidgetCollection} uses a timer, so consider the cost when
 * adding one.
 * 
 * Typically, a {@link ResizableWidgetCollection} is only needed if you expect
 * your widgets to resize based on window resizing or other events. Fixed sized
 * Widgets do not need to be added to a {@link ResizableWidgetCollection} as
 * they cannot be resized.
 */
@SuppressWarnings("deprecation")
public class ResizableWidgetCollection implements WindowResizeListener,
    Iterable<ResizableWidget> {
  /**
   * Information about a widgets size.
   */
  private static class ResizableWidgetInfo {
    /**
     * The current clientHeight.
     */
    private int curHeight = 0;

    /**
     * The current clientWidth.
     */
    private int curWidth = 0;

    /**
     * Constructor.
     * 
     * @param widget the widget that will be monitored
     */
    public ResizableWidgetInfo(ResizableWidget widget) {
      curWidth = DOM.getElementPropertyInt(widget.getElement(), "clientWidth");
      curHeight = DOM.getElementPropertyInt(widget.getElement(), "clientHeight");
    }

    /**
     * Set the new dimensions of the widget if they changed.
     * 
     * @param width the new width
     * @param height the new height
     * @return true if the dimensions have changed
     */
    public boolean setClientSize(int width, int height) {
      if (width != curWidth || height != curHeight) {
        this.curWidth = width;
        this.curHeight = height;
        return true;
      } else {
        return false;
      }
    }
  }

  /**
   * The default delay between resize checks in milliseconds.
   */
  private static final int DEFAULT_RESIZE_CHECK_DELAY = 400;

  /**
   * A static {@link ResizableWidgetCollection} that can be used in most cases.
   */
  private static ResizableWidgetCollection staticCollection = null;

  /**
   * Get the globally accessible {@link ResizableWidgetCollection}. In most
   * cases, the global collection can be used for all {@link ResizableWidget}s.
   * 
   * @return the global {@link ResizableWidgetCollection}
   */
  public static ResizableWidgetCollection get() {
    if (staticCollection == null) {
      staticCollection = new ResizableWidgetCollection();
    }
    return staticCollection;
  }

  /**
   * The timer used to periodically compare the dimensions of elements to their
   * old dimensions.
   */
  private Timer resizeCheckTimer = new Timer() {
    @Override
    public void run() {
      // Ignore changes that result from window resize events
      if (windowHeight != Window.getClientHeight()
          || windowWidth != Window.getClientWidth()) {
        windowHeight = Window.getClientHeight();
        windowWidth = Window.getClientWidth();
        schedule(resizeCheckDelay);
        return;
      }

      // Look for elements that have new dimensions
      checkWidgetSize();

      // Start checking again
      if (resizeCheckingEnabled) {
        schedule(resizeCheckDelay);
      }
    }
  };

  /**
   * A hash map of the resizable widgets this collection is checking.
   */
  private Map<ResizableWidget, ResizableWidgetInfo> widgets = new HashMap<>();

  /**
   * The current window height.
   */
  private int windowHeight = 0;

  /**
   * The current window width.
   */
  private int windowWidth = 0;

  /**
   * The hook used to remove the window handler.
   */
  private HandlerRegistration windowHandler;

  /**
   * The delay between resize checks.
   */
  private int resizeCheckDelay = DEFAULT_RESIZE_CHECK_DELAY;

  /**
   * A boolean indicating that resize checking should run.
   */
  private boolean resizeCheckingEnabled;

  /**
   * Create a ResizableWidget.
   */
  public ResizableWidgetCollection() {
    this(DEFAULT_RESIZE_CHECK_DELAY);
  }

  /**
   * Constructor.
   * 
   * @param resizeCheckingEnabled false to disable resize checking
   */
  public ResizableWidgetCollection(boolean resizeCheckingEnabled) {
    this(DEFAULT_RESIZE_CHECK_DELAY, resizeCheckingEnabled);
  }

  /**
   * Constructor.
   * 
   * @param resizeCheckDelay the delay between checks in milliseconds
   */
  public ResizableWidgetCollection(int resizeCheckDelay) {
    this(resizeCheckDelay, true);
  }

  /**
   * Constructor.
   */
  protected ResizableWidgetCollection(int resizeCheckDelay,
      boolean resizeCheckingEnabled) {
    setResizeCheckDelay(resizeCheckDelay);
    setResizeCheckingEnabled(resizeCheckingEnabled);
  }

  /**
   * Add a resizable widget to the collection.
   * 
   * @param widget the resizable widget to add
   */
  public void add(ResizableWidget widget) {
    widgets.put(widget, new ResizableWidgetInfo(widget));
  }

  /**
   * Check to see if any Widgets have been resized and call their handlers
   * appropriately.
   */
  public void checkWidgetSize() {
    for (Map.Entry<ResizableWidget, ResizableWidgetInfo> entry : widgets.entrySet()) {
      ResizableWidget widget = entry.getKey();
      ResizableWidgetInfo info = entry.getValue();
      int curWidth = widget.getElement().getPropertyInt("clientWidth");
      int curHeight = widget.getElement().getPropertyInt("clientHeight");

      // Call the onResize method only if the widget is attached
      if (info.setClientSize(curWidth, curHeight)) {
        if (curWidth > 0 && curHeight > 0 && widget.isAttached()) {
          widget.onResize(curWidth, curHeight);
        }
      }
    }
  }

  /**
   * Get the delay between resize checks in milliseconds.
   * 
   * @return the resize check delay
   */
  public int getResizeCheckDelay() {
    return resizeCheckDelay;
  }

  /**
   * Check whether or not resize checking is enabled.
   * 
   * @return true is resize checking is enabled
   */
  public boolean isResizeCheckingEnabled() {
    return resizeCheckingEnabled;
  }

  public Iterator<ResizableWidget> iterator() {
    return widgets.keySet().iterator();
  }

  /**
   * Called when the browser window is resized.
   * 
   * @param width the width of the window's client area.
   * @param height the height of the window's client area.
   */
  @Deprecated
  public void onWindowResized(int width, int height) {
    checkWidgetSize();
  }

  /**
   * Remove a {@link ResizableWidget} from the collection.
   * 
   * @param widget the widget to remove
   */
  public void remove(ResizableWidget widget) {
    widgets.remove(widget);
  }

  /**
   * Set the delay between resize checks in milliseconds.
   * 
   * @param resizeCheckDelay the new delay
   */
  public void setResizeCheckDelay(int resizeCheckDelay) {
    this.resizeCheckDelay = resizeCheckDelay;
  }

  /**
   * Set whether or not resize checking is enabled. If disabled, elements will
   * still be resized on window events, but the timer will not check their
   * dimensions periodically.
   * 
   * @param enabled true to enable the resize checking timer
   */
  public void setResizeCheckingEnabled(boolean enabled) {
    if (enabled && !resizeCheckingEnabled) {
      resizeCheckingEnabled = true;
      if (windowHandler == null) {
        windowHandler = Window.addResizeHandler(new ResizeHandler() {
          public void onResize(ResizeEvent event) {
            onWindowResized(event.getWidth(), event.getHeight());
          }
        });
      }
      resizeCheckTimer.schedule(resizeCheckDelay);
    } else if (!enabled && resizeCheckingEnabled) {
      resizeCheckingEnabled = false;
      if (windowHandler != null) {
        windowHandler.removeHandler();
        windowHandler = null;
      }
      resizeCheckTimer.cancel();
    }
  }

  /**
   * Inform the {@link ResizableWidgetCollection} that the size of a widget has
   * changed and already been redrawn. This will prevent the widget from being
   * redrawn on the next loop.
   * 
   * @param widget the widget's size that changed
   */
  public void updateWidgetSize(ResizableWidget widget) {
    if (!widget.isAttached()) {
      return;
    }

    ResizableWidgetInfo info = widgets.get(widget);
    if (info != null) {
      int curWidth = widget.getElement().getPropertyInt("clientWidth");
      int curHeight = widget.getElement().getPropertyInt("clientHeight");
      info.setClientSize(curWidth, curHeight);
    }
  }

}
