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