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.webvtt;
  
  import android.text.TextUtils;
  import com.google.android.exoplayer2.util.ColorParser;
  import com.google.android.exoplayer2.util.ParsableByteArray;
  import java.util.Arrays;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS
   * features.
   */
  /* package */ final class CssParser {
  
    private static final String PROPERTY_BGCOLOR = "background-color";
    private static final String PROPERTY_FONT_FAMILY = "font-family";
    private static final String PROPERTY_FONT_WEIGHT = "font-weight";
    private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
    private static final String VALUE_BOLD = "bold";
    private static final String VALUE_UNDERLINE = "underline";
    private static final String BLOCK_START = "{";
    private static final String BLOCK_END = "}";
    private static final String PROPERTY_FONT_STYLE = "font-style";
    private static final String VALUE_ITALIC = "italic";
  
    private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
  
    // Temporary utility data structures.
    private final ParsableByteArray styleInput;
    private final StringBuilder stringBuilder;
  
    public CssParser() {
      styleInput = new ParsableByteArray();
      stringBuilder = new StringBuilder();
    }
  
    /**
     * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the
     * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or
     * {@code null} otherwise.
     *
     * @param input The input from which the style block should be read.
     * @return A {@link WebvttCssStyle} that represents the parsed block.
     */
    public WebvttCssStyle parseBlock(ParsableByteArray input) {
      stringBuilder.setLength(0);
      int initialInputPosition = input.getPosition();
      skipStyleBlock(input);
      styleInput.reset(input.data, input.getPosition());
      styleInput.setPosition(initialInputPosition);
      String selector = parseSelector(styleInput, stringBuilder);
      if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
        return null;
      }
      WebvttCssStyle style = new WebvttCssStyle();
      applySelectorToStyle(style, selector);
      String token = null;
      boolean blockEndFound = false;
      while (!blockEndFound) {
        int position = styleInput.getPosition();
        token = parseNextToken(styleInput, stringBuilder);
        blockEndFound = token == null || BLOCK_END.equals(token);
        if (!blockEndFound) {
          styleInput.setPosition(position);
          parseStyleDeclaration(styleInput, style, stringBuilder);
        }
      }
      return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
    }
  
    /**
     * Returns a string containing the selector. The input is expected to have the form
     * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
     *
     * @param input From which the selector is obtained.
     * @return A string containing the target, empty string if the selector is universal
     *     (targets all cues) or null if an error was encountered.
     */
    private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
      skipWhitespaceAndComments(input);
      if (input.bytesLeft() < 5) {
        return null;
      }
      String cueSelector = input.readString(5);
      if (!"::cue".equals(cueSelector)) {
        return null;
      }
      int position = input.getPosition();
      String token = parseNextToken(input, stringBuilder);
      if (token == null) {
        return null;
      }
      if (BLOCK_START.equals(token)) {
        input.setPosition(position);
        return "";
      }
      String target = null;
      if ("(".equals(token)) {
        target = readCueTarget(input);
      }
      token = parseNextToken(input, stringBuilder);
      if (!")".equals(token) || token == null) {
        return null;
      }
      return target;
    }
  
    /**
     * Reads the contents of ::cue() and returns it as a string.
     */
    private static String readCueTarget(ParsableByteArray input) {
      int position = input.getPosition();
      int limit = input.limit();
      boolean cueTargetEndFound = false;
      while (position < limit && !cueTargetEndFound) {
        char c = (char) input.data[position++];
        cueTargetEndFound = c == ')';
      }
      return input.readString(--position - input.getPosition()).trim();
      // --offset to return ')' to the input.
    }
  
    private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style,
        StringBuilder stringBuilder) {
      skipWhitespaceAndComments(input);
      String property = parseIdentifier(input, stringBuilder);
      if ("".equals(property)) {
        return;
      }
      if (!":".equals(parseNextToken(input, stringBuilder))) {
        return;
      }
      skipWhitespaceAndComments(input);
      String value = parsePropertyValue(input, stringBuilder);
      if (value == null || "".equals(value)) {
        return;
      }
      int position = input.getPosition();
      String token = parseNextToken(input, stringBuilder);
      if (";".equals(token)) {
        // The style declaration is well formed.
      } else if (BLOCK_END.equals(token)) {
        // The style declaration is well formed and we can go on, but the closing bracket had to be
        // fed back.
        input.setPosition(position);
      } else {
        // The style declaration is not well formed.
        return;
      }
      // At this point we have a presumably valid declaration, we need to parse it and fill the style.
      if ("color".equals(property)) {
        style.setFontColor(ColorParser.parseCssColor(value));
      } else if (PROPERTY_BGCOLOR.equals(property)) {
        style.setBackgroundColor(ColorParser.parseCssColor(value));
      } else if (PROPERTY_TEXT_DECORATION.equals(property)) {
        if (VALUE_UNDERLINE.equals(value)) {
          style.setUnderline(true);
        }
      } else if (PROPERTY_FONT_FAMILY.equals(property)) {
        style.setFontFamily(value);
      } else if (PROPERTY_FONT_WEIGHT.equals(property)) {
        if (VALUE_BOLD.equals(value)) {
          style.setBold(true);
        }
      } else if (PROPERTY_FONT_STYLE.equals(property)) {
        if (VALUE_ITALIC.equals(value)) {
          style.setItalic(true);
        }
      }
      // TODO: Fill remaining supported styles.
    }
  
    // Visible for testing.
    /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) {
      boolean skipping = true;
      while (input.bytesLeft() > 0 && skipping) {
        skipping = maybeSkipWhitespace(input) || maybeSkipComment(input);
      }
    }
  
    // Visible for testing.
    /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) {
      skipWhitespaceAndComments(input);
      if (input.bytesLeft() == 0) {
        return null;
      }
      String identifier = parseIdentifier(input, stringBuilder);
      if (!"".equals(identifier)) {
        return identifier;
      }
      // We found a delimiter.
      return "" + (char) input.readUnsignedByte();
    }
  
    private static boolean maybeSkipWhitespace(ParsableByteArray input) {
      switch(peekCharAtPosition(input, input.getPosition())) {
        case '\t':
        case '\r':
        case '\n':
        case '\f':
        case ' ':
          input.skipBytes(1);
          return true;
        default:
          return false;
      }
    }
  
    // Visible for testing.
    /* package */ static void skipStyleBlock(ParsableByteArray input) {
      // The style block cannot contain empty lines, so we assume the input ends when a empty line
      // is found.
      String line;
      do {
        line = input.readLine();
      } while (!TextUtils.isEmpty(line));
    }
  
    private static char peekCharAtPosition(ParsableByteArray input, int position) {
      return (char) input.data[position];
    }
  
    private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) {
      StringBuilder expressionBuilder = new StringBuilder();
      String token;
      int position;
      boolean expressionEndFound = false;
      // TODO: Add support for "Strings in quotes with spaces".
      while (!expressionEndFound) {
        position = input.getPosition();
        token = parseNextToken(input, stringBuilder);
        if (token == null) {
          // Syntax error.
          return null;
        }
        if (BLOCK_END.equals(token) || ";".equals(token)) {
          input.setPosition(position);
          expressionEndFound = true;
        } else {
          expressionBuilder.append(token);
        }
      }
      return expressionBuilder.toString();
    }
  
    private static boolean maybeSkipComment(ParsableByteArray input) {
      int position = input.getPosition();
      int limit = input.limit();
      byte[] data = input.data;
      if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') {
        while (position + 1 < limit) {
          char skippedChar = (char) data[position++];
          if (skippedChar == '*') {
            if (((char) data[position]) == '/') {
              position++;
              limit = position;
            }
          }
        }
        input.skipBytes(limit - input.getPosition());
        return true;
      }
      return false;
    }
  
    private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) {
      stringBuilder.setLength(0);
      int position = input.getPosition();
      int limit = input.limit();
      boolean identifierEndFound = false;
      while (position  < limit && !identifierEndFound) {
        char c = (char) input.data[position];
        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#'
            || c == '-' || c == '.' || c == '_') {
          position++;
          stringBuilder.append(c);
        } else {
          identifierEndFound = true;
        }
      }
      input.skipBytes(position - input.getPosition());
      return stringBuilder.toString();
    }
  
    /**
     * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form
     * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
     */
    private void applySelectorToStyle(WebvttCssStyle style, String selector) {
      if ("".equals(selector)) {
        return; // Universal selector.
      }
      int voiceStartIndex = selector.indexOf('[');
      if (voiceStartIndex != -1) {
        Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex));
        if (matcher.matches()) {
          style.setTargetVoice(matcher.group(1));
        }
        selector = selector.substring(0, voiceStartIndex);
      }
      String[] classDivision = selector.split("\\.");
      String tagAndIdDivision = classDivision[0];
      int idPrefixIndex = tagAndIdDivision.indexOf('#');
      if (idPrefixIndex != -1) {
        style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex));
        style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'.
      } else {
        style.setTargetTagName(tagAndIdDivision);
      }
      if (classDivision.length > 1) {
        style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length));
      }
    }
  
  }
  