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

import collide.client.util.CssUtils;
import collide.client.util.Elements;

import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;

import elemental.css.CSSStyleDeclaration;
import elemental.css.CSSStyleDeclaration.Unit;
import elemental.dom.Element;

/**
 * A class that computes the height and width of a character.
 */
public class FontDimensionsCalculator {

  public interface FontDimensions {
    float getBaseWidth();

    float getBaseHeight();

    float getWidthZoomFactor();

    float getHeightZoomFactor();

    float getCharacterWidth();

    float getCharacterHeight();
  }

  private class FontDimensionsImpl implements FontDimensions {

    private float baseWidth;
    private float baseHeight;
    private float widthFactor = 1;
    private float heightFactor = 1;

    /**
     * Updates the width and height internally. If this has never been set
     * before this will set the baseWidth and baseHeight. Otherwise it will
     * adjust the zoom factor and the current character width and height.
     *
     * @return false if no update was required.
     */
    private boolean update(float width, float height) {
      if (baseWidth == 0 && baseHeight == 0) {
        baseWidth = width;
        baseHeight = height;
        return true;
      } else if (baseWidth * widthFactor != width || baseHeight * heightFactor != height) {
        widthFactor = width / baseWidth;
        heightFactor = height / baseHeight;
        return true;
      }

      return false;
    }

    @Override
    public float getBaseWidth() {
      return baseWidth;
    }

    @Override
    public float getBaseHeight() {
      return baseHeight;
    }

    @Override
    public float getWidthZoomFactor() {
      return widthFactor;
    }

    @Override
    public float getHeightZoomFactor() {
      return heightFactor;
    }

    @Override
    public float getCharacterWidth() {
      return widthFactor * baseWidth;
    }

    @Override
    public float getCharacterHeight() {
      return heightFactor * baseHeight;
    }
  }

  private static final int POLLING_CUTOFF = 10000;

  private static final String SAMPLE_TEXT = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  private static final int SAMPLE_ROWS = 30;

  public interface Callback {
    void onFontDimensionsChanged(FontDimensions fontDimensions);
  }

  private static FontDimensionsCalculator INSTANCE;

  public static FontDimensionsCalculator get(String fontClassName) {
    if (INSTANCE == null) {
      INSTANCE = new FontDimensionsCalculator(fontClassName);
    }

    return INSTANCE;
  }

  private final Element dummyElement;

  /*
   * This timer is called during the first 12s. When using a web font it will be
   * loaded asynchronously and our initial measurements will be incorrect. By
   * polling we remeasure after this font has been loaded and updated our size.
   */
  private final Timer repeater = new Timer() {
    @Override
    public void run() {
      if (pollingDelay >= POLLING_CUTOFF) {
        // Terminate polling rescheduling once we cross the cutoff.
        return;
      }

      measureAndDispatch();

      schedule(pollingDelay);
      pollingDelay *= 2;
    }
  };

  private int pollingDelay = 500;

  private JsonArray<Callback> callbacks = JsonCollections.createArray();

  private final FontDimensionsImpl fontDimensions;
  private final String fontClassName;

  private FontDimensionsCalculator(String fontClassName) {
    this.fontClassName = fontClassName;
    // This handler will be called when the browser window zooms
    Window.addResizeHandler(new ResizeHandler() {
      @Override
      public void onResize(ResizeEvent arg0) {
        measureAndDispatch();
      }
    });

    // Build a multirow text block so we can measure it
    StringBuilder htmlContent = new StringBuilder(SAMPLE_TEXT);
    for (int i = 1; i < SAMPLE_ROWS; i++) {
      htmlContent.append("<br/>");
      htmlContent.append(SAMPLE_TEXT);
    }

    dummyElement = Elements.createSpanElement(fontClassName);
    dummyElement.setInnerHTML(htmlContent.toString());
    dummyElement.getStyle().setVisibility(CSSStyleDeclaration.Visibility.HIDDEN);
    dummyElement.getStyle().setPosition(CSSStyleDeclaration.Position.ABSOLUTE);
    dummyElement.getStyle().setLeft(0, Unit.PX);
    dummyElement.getStyle().setTop(0, Unit.PX);
    Elements.getBody().appendChild(dummyElement);

    fontDimensions = new FontDimensionsImpl();

    repeater.schedule(pollingDelay);

    /*
     * Force an initial measure (the dispatch won't happen since no one is
     * attached)
     */
    measureAndDispatch();
  }

  public void addCallback(final Callback callback) {
    callbacks.add(callback);
  }

  public void removeCallback(final Callback callback) {
    callbacks.remove(callback);
  }

  /**
   * Returns a font dimensions that will be updated as the font dimensions
   * change.
   */
  public FontDimensions getFontDimensions() {
    return fontDimensions;
  }

  public String getFontClassName() {
    return fontClassName;
  }

  public String getFont() {
    return CssUtils.getComputedStyle(dummyElement).getPropertyValue("font");
  }

  private void measureAndDispatch() {
    float curWidth = dummyElement.getOffsetWidth() / ((float) SAMPLE_TEXT.length());
    float curHeight = dummyElement.getOffsetHeight() / ((float) SAMPLE_ROWS);

    if (fontDimensions.update(curWidth, curHeight)) {
      dispatchToCallbacks();
    }
  }

  private void dispatchToCallbacks() {
    for (int i = 0, n = callbacks.size(); i < n; i++) {
      callbacks.get(i).onFontDimensionsChanged(fontDimensions);
    }
  }
}
