content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * 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.android.exoplayer2.text.ttml;
  
  import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
  
  import android.text.Spannable;
  import android.text.SpannableStringBuilder;
  import android.text.Spanned;
  import android.text.style.AbsoluteSizeSpan;
  import android.text.style.BackgroundColorSpan;
  import android.text.style.ForegroundColorSpan;
  import android.text.style.RelativeSizeSpan;
  import android.text.style.StrikethroughSpan;
  import android.text.style.StyleSpan;
  import android.text.style.TypefaceSpan;
  import android.text.style.UnderlineSpan;
  import androidx.annotation.Nullable;
  import com.google.android.exoplayer2.text.Cue;
  import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
  import com.google.android.exoplayer2.text.span.RubySpan;
  import com.google.android.exoplayer2.text.span.SpanUtil;
  import com.google.android.exoplayer2.text.span.TextAnnotation;
  import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
  import com.google.android.exoplayer2.util.Log;
  import com.google.android.exoplayer2.util.Util;
  import java.util.ArrayDeque;
  import java.util.Deque;
  import java.util.Map;
  
  /** Package internal utility class to render styled <code>TtmlNode</code>s. */
  /* package */ final class TtmlRenderUtil {
  
    private static final String TAG = "TtmlRenderUtil";
  
    @Nullable
    public static TtmlStyle resolveStyle(
        @Nullable TtmlStyle style, @Nullable String[] styleIds, Map<String, TtmlStyle> globalStyles) {
      if (style == null) {
        if (styleIds == null) {
          // No styles at all.
          return null;
        } else if (styleIds.length == 1) {
          // Only one single referential style present.
          return globalStyles.get(styleIds[0]);
        } else if (styleIds.length > 1) {
          // Only multiple referential styles present.
          TtmlStyle chainedStyle = new TtmlStyle();
          for (String id : styleIds) {
            chainedStyle.chain(globalStyles.get(id));
          }
          return chainedStyle;
        }
      } else /* style != null */ {
        if (styleIds != null && styleIds.length == 1) {
          // Merge a single referential style into inline style.
          return style.chain(globalStyles.get(styleIds[0]));
        } else if (styleIds != null && styleIds.length > 1) {
          // Merge multiple referential styles into inline style.
          for (String id : styleIds) {
            style.chain(globalStyles.get(id));
          }
          return style;
        }
      }
      // Only inline styles available.
      return style;
    }
  
    public static void applyStylesToSpan(
        Spannable builder,
        int start,
        int end,
        TtmlStyle style,
        @Nullable TtmlNode parent,
        Map<String, TtmlStyle> globalStyles,
        @Cue.VerticalType int verticalType) {
  
      if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
        builder.setSpan(
            new StyleSpan(style.getStyle()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      if (style.isLinethrough()) {
        builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      if (style.isUnderline()) {
        builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      if (style.hasFontColor()) {
        SpanUtil.addOrReplaceSpan(
            builder,
            new ForegroundColorSpan(style.getFontColor()),
            start,
            end,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      if (style.hasBackgroundColor()) {
        SpanUtil.addOrReplaceSpan(
            builder,
            new BackgroundColorSpan(style.getBackgroundColor()),
            start,
            end,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      if (style.getFontFamily() != null) {
        SpanUtil.addOrReplaceSpan(
            builder,
            new TypefaceSpan(style.getFontFamily()),
            start,
            end,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      if (style.getTextEmphasis() != null) {
        TextEmphasis textEmphasis = checkNotNull(style.getTextEmphasis());
        @TextEmphasisSpan.MarkShape int markShape;
        @TextEmphasisSpan.MarkFill int markFill;
        if (textEmphasis.markShape == TextEmphasis.MARK_SHAPE_AUTO) {
          // If a vertical writing mode applies, then 'auto' is equivalent to 'filled sesame';
          // otherwise, it's equivalent to 'filled circle':
          // https://www.w3.org/TR/ttml2/#style-value-emphasis-style
          markShape =
              (verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL)
                  ? TextEmphasisSpan.MARK_SHAPE_SESAME
                  : TextEmphasisSpan.MARK_SHAPE_CIRCLE;
          markFill = TextEmphasisSpan.MARK_FILL_FILLED;
        } else {
          markShape = textEmphasis.markShape;
          markFill = textEmphasis.markFill;
        }
  
        @TextEmphasis.Position int position;
        if (textEmphasis.position == TextEmphasis.POSITION_OUTSIDE) {
          // 'outside' is not supported by TextEmphasisSpan, so treat it as 'before':
          // https://www.w3.org/TR/ttml2/#style-value-annotation-position
          position = TextAnnotation.POSITION_BEFORE;
        } else {
          position = textEmphasis.position;
        }
  
        SpanUtil.addOrReplaceSpan(
            builder,
            new TextEmphasisSpan(markShape, markFill, position),
            start,
            end,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      switch (style.getRubyType()) {
        case TtmlStyle.RUBY_TYPE_BASE:
          // look for the sibling RUBY_TEXT and add it as span between start & end.
          @Nullable TtmlNode containerNode = findRubyContainerNode(parent, globalStyles);
          if (containerNode == null) {
            // No matching container node
            break;
          }
          @Nullable TtmlNode textNode = findRubyTextNode(containerNode, globalStyles);
          if (textNode == null) {
            // no matching text node
            break;
          }
          String rubyText;
          if (textNode.getChildCount() == 1 && textNode.getChild(0).text != null) {
            rubyText = Util.castNonNull(textNode.getChild(0).text);
          } else {
            Log.i(TAG, "Skipping rubyText node without exactly one text child.");
            break;
          }
  
          @Nullable
          TtmlStyle textStyle = resolveStyle(textNode.style, textNode.getStyleIds(), globalStyles);
  
          // Use position from ruby text node if defined.
          @TextAnnotation.Position
          int rubyPosition =
              textStyle != null ? textStyle.getRubyPosition() : TextAnnotation.POSITION_UNKNOWN;
  
          if (rubyPosition == TextAnnotation.POSITION_UNKNOWN) {
            // If ruby position is not defined, use position info from container node.
            @Nullable
            TtmlStyle containerStyle =
                resolveStyle(containerNode.style, containerNode.getStyleIds(), globalStyles);
            rubyPosition = containerStyle != null ? containerStyle.getRubyPosition() : rubyPosition;
          }
  
          builder.setSpan(
              new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          break;
        case TtmlStyle.RUBY_TYPE_DELIMITER:
          // TODO: Add support for this when RubySpan supports parenthetical text. For now, just
          // fall through and delete the text.
        case TtmlStyle.RUBY_TYPE_TEXT:
          // We can't just remove the text directly from `builder` here because TtmlNode has fixed
          // ideas of where every node starts and ends (nodeStartsByRegion and nodeEndsByRegion) so
          // all these indices become invalid if we mutate the underlying string at this point.
          // Instead we add a special span that's then handled in TtmlNode#cleanUpText.
          builder.setSpan(new DeleteTextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          break;
        case TtmlStyle.RUBY_TYPE_CONTAINER:
        case TtmlStyle.UNSPECIFIED:
        default:
          // Do nothing
          break;
      }
      if (style.getTextCombine()) {
        SpanUtil.addOrReplaceSpan(
            builder,
            new HorizontalTextInVerticalContextSpan(),
            start,
            end,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
      switch (style.getFontSizeUnit()) {
        case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
          SpanUtil.addOrReplaceSpan(
              builder,
              new AbsoluteSizeSpan((int) style.getFontSize(), true),
              start,
              end,
              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          break;
        case TtmlStyle.FONT_SIZE_UNIT_EM:
          SpanUtil.addOrReplaceSpan(
              builder,
              new RelativeSizeSpan(style.getFontSize()),
              start,
              end,
              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          break;
        case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
          SpanUtil.addOrReplaceSpan(
              builder,
              new RelativeSizeSpan(style.getFontSize() / 100),
              start,
              end,
              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          break;
        case TtmlStyle.UNSPECIFIED:
          // Do nothing.
          break;
      }
    }
  
    @Nullable
    private static TtmlNode findRubyTextNode(
        TtmlNode rubyContainerNode, Map<String, TtmlStyle> globalStyles) {
      Deque<TtmlNode> childNodesStack = new ArrayDeque<>();
      childNodesStack.push(rubyContainerNode);
      while (!childNodesStack.isEmpty()) {
        TtmlNode childNode = childNodesStack.pop();
        @Nullable
        TtmlStyle style = resolveStyle(childNode.style, childNode.getStyleIds(), globalStyles);
        if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_TEXT) {
          return childNode;
        }
        for (int i = childNode.getChildCount() - 1; i >= 0; i--) {
          childNodesStack.push(childNode.getChild(i));
        }
      }
  
      return null;
    }
  
    @Nullable
    private static TtmlNode findRubyContainerNode(
        @Nullable TtmlNode node, Map<String, TtmlStyle> globalStyles) {
      while (node != null) {
        @Nullable TtmlStyle style = resolveStyle(node.style, node.getStyleIds(), globalStyles);
        if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_CONTAINER) {
          return node;
        }
        node = node.parent;
      }
      return null;
    }
  
    /**
     * Called when the end of a paragraph is encountered. Adds a newline if there are one or more
     * non-space characters since the previous newline.
     *
     * @param builder The builder.
     */
    /* package */ static void endParagraph(SpannableStringBuilder builder) {
      int position = builder.length() - 1;
      while (position >= 0 && builder.charAt(position) == ' ') {
        position--;
      }
      if (position >= 0 && builder.charAt(position) != '\n') {
        builder.append('\n');
      }
    }
  
    /**
     * Applies the appropriate space policy to the given text element.
     *
     * @param in The text element to which the policy should be applied.
     * @return The result of applying the policy to the text element.
     */
    /* package */ static String applyTextElementSpacePolicy(String in) {
      // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
      String out = in.replaceAll("\r\n", "\n");
      // Apply suppress-at-line-break="auto" and
      // white-space-treatment="ignore-if-surrounding-linefeed"
      out = out.replaceAll(" *\n *", "\n");
      // Apply linefeed-treatment="treat-as-space"
      out = out.replaceAll("\n", " ");
      // Apply white-space-collapse="true"
      out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
      return out;
    }
  
    private TtmlRenderUtil() {}
  }
  