content: /**
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 org.apache.weex.layout.measurefunc;
  
  import android.graphics.Canvas;
  import android.os.Build;
  import android.os.Looper;
  import android.support.annotation.NonNull;
  import android.support.annotation.Nullable;
  import android.text.Editable;
  import android.text.Layout;
  import android.text.Spannable;
  import android.text.SpannableString;
  import android.text.SpannableStringBuilder;
  import android.text.Spanned;
  import android.text.SpannedString;
  import android.text.StaticLayout;
  import android.text.TextPaint;
  import android.text.TextUtils;
  import android.text.style.AlignmentSpan;
  import android.support.annotation.RestrictTo;
  import android.support.annotation.RestrictTo.Scope;
  import android.support.annotation.WorkerThread;
  
  import org.apache.weex.WXSDKManager;
  import org.apache.weex.common.Constants;
  import org.apache.weex.dom.TextDecorationSpan;
  import org.apache.weex.dom.WXAttr;
  import org.apache.weex.dom.WXCustomStyleSpan;
  import org.apache.weex.dom.WXLineHeightSpan;
  import org.apache.weex.dom.WXStyle;
  import org.apache.weex.layout.ContentBoxMeasurement;
  import org.apache.weex.layout.MeasureMode;
  import org.apache.weex.ui.component.WXComponent;
  import org.apache.weex.ui.component.WXText;
  import org.apache.weex.ui.component.WXTextDecoration;
  import org.apache.weex.utils.WXDomUtils;
  import org.apache.weex.utils.WXLogUtils;
  import org.apache.weex.utils.WXResourceUtils;
  
  import java.util.Map;
  import java.util.concurrent.atomic.AtomicReference;
  
  import static org.apache.weex.dom.WXStyle.UNSET;
  import static org.apache.weex.utils.WXUtils.isUndefined;
  
  /**
   * Created by miomin on 2018/3/9.
   */
  
  public class TextContentBoxMeasurement extends ContentBoxMeasurement {
  
    private static final Canvas DUMMY_CANVAS = new Canvas();
  
    public TextContentBoxMeasurement(WXComponent component) {
      super(component);
    }
  
    class SetSpanOperation {
  
      protected final int start, end, flag;
      protected final Object what;
  
      SetSpanOperation(int start, int end, Object what) {
        this(start, end, what, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
      }
  
      SetSpanOperation(int start, int end, Object what, int flag) {
        this.start = start;
        this.end = end;
        this.what = what;
        this.flag = flag;
      }
  
      public void execute(Spannable sb) {
        sb.setSpan(what, start, end, flag);
      }
    }
  
    private static final String ELLIPSIS = "\u2026";
    private boolean mIsColorSet = false;
    private boolean hasBeenMeasured = false;
    private int mColor;
    private int mFontStyle = UNSET;
    private int mFontWeight = UNSET;
    private int mNumberOfLines = UNSET;
    private int mFontSize = UNSET;
    private int mLineHeight = UNSET;
    private float previousWidth = Float.NaN;
    private String mFontFamily = null;
    private String mText = null;
    private TextUtils.TruncateAt textOverflow;
    private Layout.Alignment mAlignment;
    private WXTextDecoration mTextDecoration = WXTextDecoration.NONE;
    private TextPaint mTextPaint;
    private @Nullable
    Spanned spanned;
    private @Nullable
    Layout layout;
    private AtomicReference<Layout> atomicReference = new AtomicReference<>();
  
    /**
     * uiThread = false
     **/
    @Override
    public void layoutBefore() {
      mTextPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
      hasBeenMeasured = false;
      updateStyleAndText();
      spanned = createSpanned(mText);
    }
  
    /**
     * uiThread = false
     **/
    @Override
    public void measureInternal(float width, float height, int widthMeasureMode, int heightMeasureMode) {
      float measureWidth = width, measureHeight = height;
      hasBeenMeasured = true;
      float textWidth = getTextWidth(mTextPaint, width, widthMeasureMode == MeasureMode.EXACTLY);
  
      if (textWidth > 0 && spanned != null) {
        layout = createLayout(textWidth,null);
        previousWidth = layout.getWidth();
        if (Float.isNaN(width)) {
          measureWidth = layout.getWidth();
        } else {
          measureWidth = Math.min(layout.getWidth(), measureWidth);
        }
  
        if (Float.isNaN(height)) {
          measureHeight = layout.getHeight();
        }
      } else {
        if (widthMeasureMode == MeasureMode.UNSPECIFIED) {
          measureWidth = 0;
        }
        if (heightMeasureMode == MeasureMode.UNSPECIFIED) {
          measureHeight = 0;
        }
      }
      mMeasureWidth = measureWidth;
      mMeasureHeight = measureHeight;
    }
  
    /**
     * uiThread = false
     **/
    @Override
    public void layoutAfter(float computedWidth, float computedHeight) {
      if(mComponent!=null) {
        if (hasBeenMeasured) {
          if (layout != null &&
              WXDomUtils
                  .getContentWidth(mComponent.getPadding(), mComponent.getBorder(), computedWidth)
                  != previousWidth) {
            recalculateLayout(computedWidth);
          }
        } else {
          updateStyleAndText();
          recalculateLayout(computedWidth);
        }
        hasBeenMeasured = false;
        if (layout != null && !layout.equals(atomicReference.get()) &&
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
          if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
            warmUpTextLayoutCache(layout);
          }
        }
        swap();
        WXSDKManager.getInstance().getWXRenderManager().postOnUiThread(new Runnable() {
          @Override
          public void run() {
            if(mComponent!=null) {
              mComponent.updateExtra(atomicReference.get());
            }
          }
        }, mComponent.getInstanceId());
      }
    }
  
    private void updateStyleAndText() {
      updateStyleImp(mComponent.getStyles());
      mText = WXAttr.getValue(mComponent.getAttrs());
    }
  
    /**
     * Force relayout the text, the text must layout before invoke this method.
     *
     * Internal method, do not invoke unless you what what you are doing
     * @param isRTL
     */
    @RestrictTo(Scope.LIBRARY)
    @WorkerThread
    public void forceRelayout(){
      //Generate Spans
      layoutBefore();
  
      //Measure
      measure(previousWidth, Float.NaN, MeasureMode.EXACTLY, MeasureMode.UNSPECIFIED);
  
      //Swap text layout to UI Thread
      layoutAfter(previousWidth, Float.NaN);
    }
  
    /**
     * Record the property according to the given style
     *
     * @param style the give style.
     */
    private void updateStyleImp(Map<String, Object> style) {
      if (style != null) {
        if (style.containsKey(Constants.Name.LINES)) {
          int lines = WXStyle.getLines(style);
          mNumberOfLines = lines > 0 ? lines : UNSET;
        }
        if (style.containsKey(Constants.Name.FONT_SIZE)) {
          mFontSize = WXStyle.getFontSize(style, mComponent.getViewPortWidth());
        }
        if (style.containsKey(Constants.Name.FONT_WEIGHT)) {
          mFontWeight = WXStyle.getFontWeight(style);
        }
        if (style.containsKey(Constants.Name.FONT_STYLE)) {
          mFontStyle = WXStyle.getFontStyle(style);
        }
        if (style.containsKey(Constants.Name.COLOR)) {
          mColor = WXResourceUtils.getColor(WXStyle.getTextColor(style));
          mIsColorSet = mColor != Integer.MIN_VALUE;
        }
        if (style.containsKey(Constants.Name.TEXT_DECORATION)) {
          mTextDecoration = WXStyle.getTextDecoration(style);
        }
        if (style.containsKey(Constants.Name.FONT_FAMILY)) {
          mFontFamily = WXStyle.getFontFamily(style);
        }
        mAlignment = WXStyle.getTextAlignment(style, mComponent.isLayoutRTL());
        textOverflow = WXStyle.getTextOverflow(style);
        int lineHeight = WXStyle.getLineHeight(style, mComponent.getViewPortWidth());
        if (lineHeight != UNSET) {
          mLineHeight = lineHeight;
        }
      }
    }
  
    /**
     * Update {@link #spanned} according to the give charSequence and styles
     *
     * @param text the give raw text.
     * @return an Spanned contains text and spans
     */
    protected
    @NonNull
    Spanned createSpanned(String text) {
      if (!TextUtils.isEmpty(text)) {
        SpannableString spannable = new SpannableString(text);
        updateSpannable(spannable, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return spannable;
      }
      return new SpannableString("");
    }
  
    protected void updateSpannable(Spannable spannable, int spanFlag) {
      int end = spannable.length();
      if (mFontSize == UNSET) {
        mTextPaint.setTextSize(WXText.sDEFAULT_SIZE);
      }
      else{
        mTextPaint.setTextSize(mFontSize);
      }
  
      if (mLineHeight != UNSET) {
        setSpan(spannable, new WXLineHeightSpan(mLineHeight), 0, end, spanFlag);
      }
  
      setSpan(spannable, new AlignmentSpan.Standard(mAlignment), 0, end, spanFlag);
  
      if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) {
        setSpan(spannable, new WXCustomStyleSpan(mFontStyle, mFontWeight, mFontFamily), 0, end, spanFlag);
      }
  
      if (mIsColorSet) {
        mTextPaint.setColor(mColor);
      }
  
      if (mTextDecoration == WXTextDecoration.UNDERLINE || mTextDecoration == WXTextDecoration.LINETHROUGH) {
        setSpan(spannable, new TextDecorationSpan(mTextDecoration), 0, end, spanFlag);
      }
    }
  
    private void setSpan(Spannable spannable, Object what, int start, int end, int spanFlag){
      spannable.setSpan(what, start, end, spanFlag);
    }
  
    /**
     * Get text width according to constrain of outerWidth with and forceToDesired
     *
     * @param textPaint      paint used to measure text
     * @param outerWidth     the width that css-layout desired.
     * @param forceToDesired if set true, the return value will be outerWidth, no matter what the width
     *                       of text is.
     * @return if forceToDesired is false, it will be the minimum value of the width of text and
     * outerWidth in case of outerWidth is defined, in other case, it will be outer width.
     */
    private float getTextWidth(TextPaint textPaint, float outerWidth, boolean forceToDesired) {
      if (mText == null) {
        if (forceToDesired) {
          return outerWidth;
        }
        return 0;
      }
      else {
        float textWidth;
        if (forceToDesired) {
          textWidth = outerWidth;
        } else {
          float desiredWidth = Layout.getDesiredWidth(spanned, textPaint);
          if (isUndefined(outerWidth) || desiredWidth < outerWidth) {
            textWidth = desiredWidth;
          } else {
            textWidth = outerWidth;
          }
        }
        return textWidth;
      }
    }
  
    /**
     * Update layout according to {@link #mText} and span
     *
     * @param width          the specified width.
     * @param forceWidth     If true, force the text width to the specified width, otherwise, text width
     *                       may equals to or be smaller than the specified width.
     * @param previousLayout the result of previous layout, could be null.
     */
    private
    @NonNull
    Layout createLayout(final float textWidth, @Nullable Layout previousLayout) {
      Layout layout;
      if (previousWidth != textWidth || previousLayout == null) {
        layout = new StaticLayout(spanned, mTextPaint, (int) Math.ceil(textWidth),
                Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
      } else {
        layout = previousLayout;
      }
      if (mNumberOfLines != UNSET && mNumberOfLines > 0 && mNumberOfLines < layout.getLineCount()) {
        int lastLineStart, lastLineEnd;
        lastLineStart = layout.getLineStart(mNumberOfLines - 1);
        lastLineEnd = layout.getLineEnd(mNumberOfLines - 1);
        if (lastLineStart < lastLineEnd) {
          SpannableStringBuilder builder = null;
          if (lastLineStart > 0) {
            builder = new SpannableStringBuilder(spanned.subSequence(0, lastLineStart));
          } else {
            builder = new SpannableStringBuilder();
          }
          Editable lastLine = new SpannableStringBuilder(spanned.subSequence(lastLineStart, lastLineEnd));
          builder.append(truncate(lastLine, mTextPaint, (int) Math.ceil(textWidth), textOverflow));
          adjustSpansRange(spanned, builder);
          spanned = builder;
          return new StaticLayout(spanned, mTextPaint, (int) Math.ceil(textWidth),
                  Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
        }
      }
      return layout;
    }
  
    /**
     * Truncate the source span to the specified lines.
     * Caller of this method must ensure that the lines of text is <strong>greater than desired lines and need truncate</strong>.
     * Otherwise, unexpected behavior may happen.
     *
     * @param source     The source span.
     * @param paint      the textPaint
     * @param desired    specified lines.
     * @param truncateAt truncate method, null value means clipping overflow text directly, non-null value means using ellipsis strategy to clip
     * @return The spans after clipped.
     */
    private
    @NonNull
    Spanned truncate(@Nullable Editable source, @NonNull TextPaint paint,
                     int desired, @Nullable TextUtils.TruncateAt truncateAt) {
      Spanned ret = new SpannedString("");
      if (!TextUtils.isEmpty(source) && source.length() > 0) {
        if (truncateAt != null) {
          source.append(ELLIPSIS);
          Object[] spans = source.getSpans(0, source.length(), Object.class);
          for (Object span : spans) {
            int start = source.getSpanStart(span);
            int end = source.getSpanEnd(span);
            if (start == 0 && end == source.length() - 1) {
              source.removeSpan(span);
              source.setSpan(span, 0, source.length(), source.getSpanFlags(span));
            }
          }
        }
  
        StaticLayout layout;
        int startOffset;
  
        while (source.length() > 1) {
          startOffset = source.length() - 1;
          if (truncateAt != null) {
            startOffset -= 1;
          }
          source.delete(startOffset, startOffset + 1);
          layout = new StaticLayout(source, paint, desired, Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
          if (layout.getLineCount() <= 1) {
            ret = source;
            break;
          }
        }
      }
      return ret;
    }
  
    /**
     * Adjust span range after truncate due to the wrong span range during span copy and slicing.
     *
     * @param beforeTruncate The span before truncate
     * @param afterTruncate  The span after truncate
     */
    private void adjustSpansRange(@NonNull Spanned beforeTruncate, @NonNull Spannable afterTruncate) {
      Object[] spans = beforeTruncate.getSpans(0, beforeTruncate.length(), Object.class);
      for (Object span : spans) {
        int start = beforeTruncate.getSpanStart(span);
        int end = beforeTruncate.getSpanEnd(span);
        if (start == 0 && end == beforeTruncate.length()) {
          afterTruncate.removeSpan(span);
          afterTruncate.setSpan(span, 0, afterTruncate.length(), beforeTruncate.getSpanFlags(span));
        }
      }
    }
  
    private void recalculateLayout(float computedWidth) {
      float contentWidth = WXDomUtils.getContentWidth(mComponent.getPadding(), mComponent.getBorder(), computedWidth);
      if (contentWidth > 0) {
        spanned = createSpanned(mText);
        if (spanned != null) {
          layout = createLayout(contentWidth, layout);
          previousWidth = layout.getWidth();
        } else {
          previousWidth = 0;
        }
      }
    }
  
    /**
     * As warming up TextLayoutCache done in the DOM thread may manipulate UI operation,
     * there may be some exception, in which case the exception is ignored. After all,
     * this is just a warm up operation.
     *
     * @return false for warm up failure, otherwise returns true.
     */
    private boolean warmUpTextLayoutCache(Layout layout) {
      boolean result;
      try {
        layout.draw(DUMMY_CANVAS);
        result = true;
      } catch (Exception e) {
        WXLogUtils.eTag("TextWarmUp", e);
        result = false;
      }
      return result;
    }
  
    /**
     * Move the reference of current layout to the {@link AtomicReference} for further use,
     * then clear current layout.
     */
    private void swap() {
      if (layout != null) {
        atomicReference.set(layout);
        layout = null;
      }
      hasBeenMeasured = false;
    }
  }
  