/*
 * Copyright 2010 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.i18n.shared;

import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck;

/**
 * Base class for {@link BidiFormatter} and {@link SafeHtmlBidiFormatter} that
 * contains their common implementation.
 */
public abstract class BidiFormatterBase {

  /**
   * Abstract factory class for BidiFormatterBase.
   * BidiFormatterBase subclasses will usually have a non-abstract inner Factory
   * class subclassed from this one, and use a static member of that class in
   * order to prevent the needless creation of objects. For example, see
   * {@link BidiFormatter}.
   */
  protected abstract static class Factory<T extends BidiFormatterBase> {
    private T[] instances;

    @SuppressWarnings("unchecked")
    public Factory() {
      instances = (T[]) new BidiFormatterBase[6];
    }

    public abstract T createInstance(Direction contextDir,
        boolean alwaysSpan);

    public T getInstance(Direction contextDir,
        boolean alwaysSpan) {
      int index = calculateIndex(contextDir, alwaysSpan);
      T formatter = instances[index];
      if (formatter == null) {
        formatter = createInstance(contextDir, alwaysSpan);
        instances[index] = formatter;
      }
      return formatter;
    }

    // Index should be in the range [0, 5].
    private int calculateIndex(Direction contextDir, boolean alwaysSpan) {
      int i = contextDir == Direction.LTR ? 0 : contextDir == Direction.RTL ? 1
          : 2;
      if (alwaysSpan) {
        i += 3;
      }
      return i;
    }
  }

  /**
   * A container class for direction-related string constants, e.g. Unicode
   * formatting characters.
   */
  static final class Format {
    /**
     * "left" string constant.
     */
    public static final String LEFT = "left";

    /**
     * Unicode "Left-To-Right Embedding" (LRE) character.
     */
    public static final char LRE = '\u202A';

    /**
     * Unicode "Left-To-Right Mark" (LRM) character.
     */
    public static final char LRM = '\u200E';

    /**
     * String representation of LRM.
     */
    public static final String LRM_STRING = Character.toString(LRM);

    /**
     * Unicode "Pop Directional Formatting" (PDF) character.
     */
    public static final char PDF = '\u202C';

    /**
     * "right" string constant.
     */
    public static final String RIGHT = "right";

    /**
     * Unicode "Right-To-Left Embedding" (RLE) character.
     */
    public static final char RLE = '\u202B';

    /**
     * Unicode "Right-To-Left Mark" (RLM) character.
     */
    public static final char RLM = '\u200F';

    /**
     * String representation of RLM.
     */
    public static final String RLM_STRING = Character.toString(RLM);

    // Not instantiable.
    private Format() {
    }
  }

  private boolean alwaysSpan;
  private Direction contextDir;

  protected BidiFormatterBase(Direction contextDir, boolean alwaysSpan) {
    this.contextDir = contextDir;
    this.alwaysSpan = alwaysSpan;
  }

  /**
   * Like {@link #estimateDirection(String, boolean)}, but assumes {@code
   * isHtml} is false.
   *
   * @param str String whose direction is to be estimated
   * @return {@code str}'s estimated overall direction
   */
  public Direction estimateDirection(String str) {
    return BidiUtils.get().estimateDirection(str);
  }

  /**
   * Estimates the direction of a string using the best known general-purpose
   * method, i.e. using relative word counts. Direction.DEFAULT return value
   * indicates completely neutral input.
   *
   * @param str String whose direction is to be estimated
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @return {@code str}'s estimated overall direction
   */
  public Direction estimateDirection(String str, boolean isHtml) {
    return BidiUtils.get().estimateDirection(str, isHtml);
  }

  /**
   * Returns whether the span structure added by the formatter should be stable,
   * i.e., spans added even when the direction does not need to be declared.
   */
  public boolean getAlwaysSpan() {
    return alwaysSpan;
  }

  /**
   * Returns the context direction.
   */
  public Direction getContextDir() {
    return contextDir;
  }

  /**
   * Returns whether the context direction is RTL.
   */
  public boolean isRtlContext() {
    return contextDir == Direction.RTL;
  }

  /**
   * @see BidiFormatter#dirAttr(String, boolean)
   *
   * @param str String whose direction is to be estimated
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @return "dir=rtl" for RTL text in non-RTL context; "dir=ltr" for LTR text
   *         in non-LTR context; else, the empty string.
   */
  protected String dirAttrBase(String str, boolean isHtml) {
    return knownDirAttrBase(BidiUtils.get().estimateDirection(str, isHtml));
  }

  /**
   * @see BidiFormatter#endEdge
   */
  protected String endEdgeBase() {
    return contextDir == Direction.RTL ? Format.LEFT : Format.RIGHT;
  }

  /**
   * @see BidiFormatter#knownDirAttr(HasDirection.Direction)
   *
   * @param dir Given direction
   * @return "dir=rtl" for RTL text in non-RTL context; "dir=ltr" for LTR text
   *         in non-LTR context; else, the empty string.
   */
  protected String knownDirAttrBase(Direction dir) {
    if (dir != contextDir) {
      return dir == Direction.LTR ? "dir=ltr" : dir == Direction.RTL
          ? "dir=rtl" : "";
    }
    return "";
  }

  /**
   * @see BidiFormatter#markAfter(String, boolean)
   *
   * @param str String after which the mark may need to appear
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context;
   *         else, the empty string.
   */
  protected String markAfterBase(String str, boolean isHtml) {
    str = BidiUtils.get().stripHtmlIfNeeded(str, isHtml);
    return dirResetIfNeeded(str, BidiUtils.get().estimateDirection(str), false,
        true);
  }

  /**
   * @see BidiFormatter#mark()
   */
  protected String markBase() {
    return contextDir == Direction.LTR ? Format.LRM_STRING
        : contextDir == Direction.RTL ? Format.RLM_STRING : "";
  }

  /**
   * @see BidiFormatter#spanWrap(String, boolean, boolean)
   *
   * @param str The input string
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @param dirReset Whether to append a trailing unicode bidi mark matching the
   *          context direction, when needed, to prevent the possible garbling
   *          of whatever may follow {@code str}
   * @return Input string after applying the above processing.
   */
  @IsSafeHtml
  protected String spanWrapBase(@IsSafeHtml String str, boolean isHtml, boolean dirReset) {
    Direction dir = BidiUtils.get().estimateDirection(str, isHtml);
    return spanWrapWithKnownDirBase(dir, str, isHtml, dirReset);
  }

  /**
   * @see BidiFormatter#spanWrapWithKnownDir(HasDirection.Direction, String, boolean, boolean)
   *
   * @param dir {@code str}'s direction
   * @param str The input string
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @param dirReset Whether to append a trailing unicode bidi mark matching the
   *          context direction, when needed, to prevent the possible garbling
   *          of whatever may follow {@code str}
   * @return Input string after applying the above processing.
   */
  @SuppressIsSafeHtmlCastCheck
  @IsSafeHtml
  protected String spanWrapWithKnownDirBase(
      Direction dir, @IsSafeHtml String str, boolean isHtml, boolean dirReset) {
    boolean dirCondition = dir != Direction.DEFAULT && dir != contextDir;
    String origStr = str;
    if (!isHtml) {
      str = SafeHtmlUtils.htmlEscape(str);
    }

    StringBuilder result = new StringBuilder();
    if (alwaysSpan || dirCondition) {
      result.append("<span");
      if (dirCondition) {
        result.append(" ");
        result.append(dir == Direction.RTL ? "dir=rtl" : "dir=ltr");
      }
      result.append(">" + str + "</span>");
    } else {
      result.append(str);
    }
    // origStr is passed (more efficient when isHtml is false).
    result.append(dirResetIfNeeded(origStr, dir, isHtml, dirReset));
    return result.toString();
  }

  /**
   * @see BidiFormatter#startEdge
   */
  protected String startEdgeBase() {
    return contextDir == Direction.RTL ? Format.RIGHT : Format.LEFT;
  }

  /**
   * @see BidiFormatter#unicodeWrap(String, boolean, boolean)
   *
   * @param str The input string
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @param dirReset Whether to append a trailing unicode bidi mark matching the
   *          context direction, when needed, to prevent the possible garbling
   *          of whatever may follow {@code str}
   * @return Input string after applying the above processing.
   */
  protected String unicodeWrapBase(String str, boolean isHtml,
      boolean dirReset) {
    Direction dir = BidiUtils.get().estimateDirection(str, isHtml);
    return unicodeWrapWithKnownDirBase(dir, str, isHtml, dirReset);
  }

  /**
   * @see BidiFormatter#unicodeWrapWithKnownDir(HasDirection.Direction, String, boolean, boolean)
   *
   * @param dir {@code str}'s direction
   * @param str The input string
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @param dirReset Whether to append a trailing unicode bidi mark matching the
   *          context direction, when needed, to prevent the possible garbling
   *          of whatever may follow {@code str}
   * @return Input string after applying the above processing.
   */
  protected String unicodeWrapWithKnownDirBase(Direction dir, String str,
      boolean isHtml, boolean dirReset) {
    StringBuilder result = new StringBuilder();
    if (dir != Direction.DEFAULT && dir != contextDir) {
      result.append(dir == Direction.RTL ? Format.RLE : Format.LRE);
      result.append(str);
      result.append(Format.PDF);
    } else {
      result.append(str);
    }

    result.append(dirResetIfNeeded(str, dir, isHtml, dirReset));
    return result.toString();
  }

  /**
   * Returns a unicode BiDi mark matching the context direction (LRM or RLM) if
   * {@code dirReset}, and if the overall direction or the exit direction of
   * {@code str} are opposite to the context direction. Otherwise returns the
   * empty string.
   *
   * @param str The input string
   * @param dir {@code str}'s overall direction
   * @param isHtml Whether {@code str} is HTML / HTML-escaped
   * @param dirReset Whether to perform the reset
   * @return A unicode BiDi mark or the empty string.
   */
  private String dirResetIfNeeded(String str, Direction dir, boolean isHtml,
      boolean dirReset) {
    // endsWithRtl and endsWithLtr are called only if needed (short-circuit).
    if (dirReset
        && ((contextDir == Direction.LTR &&
            (dir == Direction.RTL ||
             BidiUtils.get().endsWithRtl(str, isHtml))) ||
            (contextDir == Direction.RTL &&
            (dir == Direction.LTR ||
             BidiUtils.get().endsWithLtr(str, isHtml))))) {
      return contextDir == Direction.LTR ? Format.LRM_STRING
          : Format.RLM_STRING;
    } else {
      return "";
    }
  }
}
