/*
 * Copyright (C) 2014 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.exoplayer.smoothstreaming;

import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.upstream.UriLoadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;

import android.util.Base64;
import android.util.Pair;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

/**
 * Parses SmoothStreaming client manifests.
 *
 * @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
 * IIS Smooth Streaming Client Manifest Format</a>
 */
public class SmoothStreamingManifestParser implements UriLoadable.Parser<SmoothStreamingManifest> {

  private final XmlPullParserFactory xmlParserFactory;

  public SmoothStreamingManifestParser() {
    try {
      xmlParserFactory = XmlPullParserFactory.newInstance();
    } catch (XmlPullParserException e) {
      throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
    }
  }

  @Override
  public SmoothStreamingManifest parse(String connectionUrl, InputStream inputStream)
      throws IOException, ParserException {
    try {
      XmlPullParser xmlParser = xmlParserFactory.newPullParser();
      xmlParser.setInput(inputStream, null);
      SmoothStreamMediaParser smoothStreamMediaParser =
          new SmoothStreamMediaParser(null, connectionUrl);
      return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
    } catch (XmlPullParserException e) {
      throw new ParserException(e);
    }
  }

  /**
   * Thrown if a required field is missing.
   */
  public static class MissingFieldException extends ParserException {

    public MissingFieldException(String fieldName) {
      super("Missing required field: " + fieldName);
    }

  }

  /**
   * A base class for parsers that parse components of a smooth streaming manifest.
   */
  private static abstract class ElementParser {

    private final String baseUri;
    private final String tag;

    private final ElementParser parent;
    private final List<Pair<String, Object>> normalizedAttributes;

    public ElementParser(ElementParser parent, String baseUri, String tag) {
      this.parent = parent;
      this.baseUri = baseUri;
      this.tag = tag;
      this.normalizedAttributes = new LinkedList<Pair<String, Object>>();
    }

    public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException,
        ParserException {
      String tagName;
      boolean foundStartTag = false;
      int skippingElementDepth = 0;
      while (true) {
        int eventType = xmlParser.getEventType();
        switch (eventType) {
          case XmlPullParser.START_TAG:
            tagName = xmlParser.getName();
            if (tag.equals(tagName)) {
              foundStartTag = true;
              parseStartTag(xmlParser);
            } else if (foundStartTag) {
              if (skippingElementDepth > 0) {
                skippingElementDepth++;
              } else if (handleChildInline(tagName)) {
                parseStartTag(xmlParser);
              } else {
                ElementParser childElementParser = newChildParser(this, tagName, baseUri);
                if (childElementParser == null) {
                  skippingElementDepth = 1;
                } else {
                  addChild(childElementParser.parse(xmlParser));
                }
              }
            }
            break;
          case XmlPullParser.TEXT:
            if (foundStartTag && skippingElementDepth == 0) {
              parseText(xmlParser);
            }
            break;
          case XmlPullParser.END_TAG:
            if (foundStartTag) {
              if (skippingElementDepth > 0) {
                skippingElementDepth--;
              } else {
                tagName = xmlParser.getName();
                parseEndTag(xmlParser);
                if (!handleChildInline(tagName)) {
                  return build();
                }
              }
            }
            break;
          case XmlPullParser.END_DOCUMENT:
            return null;
          default:
            // Do nothing.
            break;
        }
        xmlParser.next();
      }
    }

    private ElementParser newChildParser(ElementParser parent, String name, String baseUri) {
      if (TrackElementParser.TAG.equals(name)) {
        return new TrackElementParser(parent, baseUri);
      } else if (ProtectionElementParser.TAG.equals(name)) {
        return new ProtectionElementParser(parent, baseUri);
      } else if (StreamElementParser.TAG.equals(name)) {
        return new StreamElementParser(parent, baseUri);
      }
      return null;
    }

    /**
     * Stash an attribute that may be normalized at this level. In other words, an attribute that
     * may have been pulled up from the child elements because its value was the same in all
     * children.
     * <p>
     * Stashing an attribute allows child element parsers to retrieve the values of normalized
     * attributes using {@link #getNormalizedAttribute(String)}.
     *
     * @param key The name of the attribute.
     * @param value The value of the attribute.
     */
    protected final void putNormalizedAttribute(String key, Object value) {
      normalizedAttributes.add(Pair.create(key, value));
    }

    /**
     * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with
     * the provided name, the parent element parser will be queried, and so on up the chain.
     *
     * @param key The name of the attribute.
     * @return The stashed value, or null if the attribute was not be found.
     */
    protected final Object getNormalizedAttribute(String key) {
      for (int i = 0; i < normalizedAttributes.size(); i++) {
        Pair<String, Object> pair = normalizedAttributes.get(i);
        if (pair.first.equals(key)) {
          return pair.second;
        }
      }
      return parent == null ? null : parent.getNormalizedAttribute(key);
    }

    /**
     * Whether this {@link ElementParser} parses a child element inline.
     *
     * @param tagName The name of the child element.
     * @return Whether the child is parsed inline.
     */
    protected boolean handleChildInline(String tagName) {
      return false;
    }

    /**
     * @param xmlParser The underlying {@link XmlPullParser}
     * @throws ParserException
     */
    protected void parseStartTag(XmlPullParser xmlParser) throws ParserException {
      // Do nothing.
    }

    /**
     * @param xmlParser The underlying {@link XmlPullParser}
     * @throws ParserException
     */
    protected void parseText(XmlPullParser xmlParser) throws ParserException {
      // Do nothing.
    }

    /**
     * @param xmlParser The underlying {@link XmlPullParser}
     * @throws ParserException
     */
    protected void parseEndTag(XmlPullParser xmlParser) throws ParserException {
      // Do nothing.
    }

    /**
     * @param parsedChild A parsed child object.
     */
    protected void addChild(Object parsedChild) {
      // Do nothing.
    }

    protected abstract Object build();

    protected final String parseRequiredString(XmlPullParser parser, String key)
        throws MissingFieldException {
      String value = parser.getAttributeValue(null, key);
      if (value != null) {
        return value;
      } else {
        throw new MissingFieldException(key);
      }
    }

    protected final int parseInt(XmlPullParser parser, String key, int defaultValue)
        throws ParserException {
      String value = parser.getAttributeValue(null, key);
      if (value != null) {
        try {
          return Integer.parseInt(value);
        } catch (NumberFormatException e) {
          throw new ParserException(e);
        }
      } else {
        return defaultValue;
      }
    }

    protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException {
      String value = parser.getAttributeValue(null, key);
      if (value != null) {
        try {
          return Integer.parseInt(value);
        } catch (NumberFormatException e) {
          throw new ParserException(e);
        }
      } else {
        throw new MissingFieldException(key);
      }
    }

    protected final long parseLong(XmlPullParser parser, String key, long defaultValue)
        throws ParserException {
      String value = parser.getAttributeValue(null, key);
      if (value != null) {
        try {
          return Long.parseLong(value);
        } catch (NumberFormatException e) {
          throw new ParserException(e);
        }
      } else {
        return defaultValue;
      }
    }

    protected final long parseRequiredLong(XmlPullParser parser, String key)
        throws ParserException {
      String value = parser.getAttributeValue(null, key);
      if (value != null) {
        try {
          return Long.parseLong(value);
        } catch (NumberFormatException e) {
          throw new ParserException(e);
        }
      } else {
        throw new MissingFieldException(key);
      }
    }

    protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) {
      String value = parser.getAttributeValue(null, key);
      if (value != null) {
        return Boolean.parseBoolean(value);
      } else {
        return defaultValue;
      }
    }

  }

  private static class SmoothStreamMediaParser extends ElementParser {

    public static final String TAG = "SmoothStreamingMedia";

    private static final String KEY_MAJOR_VERSION = "MajorVersion";
    private static final String KEY_MINOR_VERSION = "MinorVersion";
    private static final String KEY_TIME_SCALE = "TimeScale";
    private static final String KEY_DVR_WINDOW_LENGTH = "DVRWindowLength";
    private static final String KEY_DURATION = "Duration";
    private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
    private static final String KEY_IS_LIVE = "IsLive";

    private int majorVersion;
    private int minorVersion;
    private long timescale;
    private long duration;
    private long dvrWindowLength;
    private int lookAheadCount;
    private boolean isLive;
    private ProtectionElement protectionElement;
    private List<StreamElement> streamElements;

    public SmoothStreamMediaParser(ElementParser parent, String baseUri) {
      super(parent, baseUri, TAG);
      lookAheadCount = -1;
      protectionElement = null;
      streamElements = new LinkedList<StreamElement>();
    }

    @Override
    public void parseStartTag(XmlPullParser parser) throws ParserException {
      majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);
      minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);
      timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L);
      duration = parseRequiredLong(parser, KEY_DURATION);
      dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0);
      lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1);
      isLive = parseBoolean(parser, KEY_IS_LIVE, false);
      putNormalizedAttribute(KEY_TIME_SCALE, timescale);
    }

    @Override
    public void addChild(Object child) {
      if (child instanceof StreamElement) {
        streamElements.add((StreamElement) child);
      } else if (child instanceof ProtectionElement) {
        Assertions.checkState(protectionElement == null);
        protectionElement = (ProtectionElement) child;
      }
    }

    @Override
    public Object build() {
      StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
      streamElements.toArray(streamElementArray);
      return new SmoothStreamingManifest(majorVersion, minorVersion, timescale, duration,
          dvrWindowLength, lookAheadCount, isLive, protectionElement, streamElementArray);
    }

  }

  private static class ProtectionElementParser extends ElementParser {

    public static final String TAG = "Protection";
    public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";

    public static final String KEY_SYSTEM_ID = "SystemID";

    private boolean inProtectionHeader;
    private UUID uuid;
    private byte[] initData;

    public ProtectionElementParser(ElementParser parent, String baseUri) {
      super(parent, baseUri, TAG);
    }

    @Override
    public boolean handleChildInline(String tag) {
      return TAG_PROTECTION_HEADER.equals(tag);
    }

    @Override
    public void parseStartTag(XmlPullParser parser) {
      if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
        inProtectionHeader = true;
        String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
        uuid = UUID.fromString(uuidString);
      }
    }

    @Override
    public void parseText(XmlPullParser parser) {
      if (inProtectionHeader) {
        initData = Base64.decode(parser.getText(), Base64.DEFAULT);
      }
    }

    @Override
    public void parseEndTag(XmlPullParser parser) {
      if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
        inProtectionHeader = false;
      }
    }

    @Override
    public Object build() {
      return new ProtectionElement(uuid, initData);
    }

  }

  private static class StreamElementParser extends ElementParser {

    public static final String TAG = "StreamIndex";
    private static final String TAG_STREAM_FRAGMENT = "c";

    private static final String KEY_TYPE = "Type";
    private static final String KEY_TYPE_AUDIO = "audio";
    private static final String KEY_TYPE_VIDEO = "video";
    private static final String KEY_TYPE_TEXT = "text";
    private static final String KEY_SUB_TYPE = "Subtype";
    private static final String KEY_NAME = "Name";
    private static final String KEY_QUALITY_LEVELS = "QualityLevels";
    private static final String KEY_URL = "Url";
    private static final String KEY_MAX_WIDTH = "MaxWidth";
    private static final String KEY_MAX_HEIGHT = "MaxHeight";
    private static final String KEY_DISPLAY_WIDTH = "DisplayWidth";
    private static final String KEY_DISPLAY_HEIGHT = "DisplayHeight";
    private static final String KEY_LANGUAGE = "Language";
    private static final String KEY_TIME_SCALE = "TimeScale";

    private static final String KEY_FRAGMENT_DURATION = "d";
    private static final String KEY_FRAGMENT_START_TIME = "t";
    private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";

    private final String baseUri;
    private final List<TrackElement> tracks;

    private int type;
    private String subType;
    private long timescale;
    private String name;
    private int qualityLevels;
    private String url;
    private int maxWidth;
    private int maxHeight;
    private int displayWidth;
    private int displayHeight;
    private String language;
    private ArrayList<Long> startTimes;

    private long lastChunkDuration;

    public StreamElementParser(ElementParser parent, String baseUri) {
      super(parent, baseUri, TAG);
      this.baseUri = baseUri;
      tracks = new LinkedList<TrackElement>();
    }

    @Override
    public boolean handleChildInline(String tag) {
      return TAG_STREAM_FRAGMENT.equals(tag);
    }

    @Override
    public void parseStartTag(XmlPullParser parser) throws ParserException {
      if (TAG_STREAM_FRAGMENT.equals(parser.getName())) {
        parseStreamFragmentStartTag(parser);
      } else {
        parseStreamElementStartTag(parser);
      }
    }

    private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
      int chunkIndex = startTimes.size();
      long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
      if (startTime == -1L) {
        if (chunkIndex == 0) {
          // Assume the track starts at t = 0.
          startTime = 0;
        } else if (lastChunkDuration != -1L) {
          // Infer the start time from the previous chunk's start time and duration.
          startTime = startTimes.get(chunkIndex - 1) + lastChunkDuration;
        } else {
          // We don't have the start time, and we're unable to infer it.
          throw new ParserException("Unable to infer start time");
        }
      }
      chunkIndex++;
      startTimes.add(startTime);
      lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
      // Handle repeated chunks.
      long repeatCount = parseLong(parser, KEY_FRAGMENT_REPEAT_COUNT, 1L);
      if (repeatCount > 1 && lastChunkDuration == -1L) {
        throw new ParserException("Repeated chunk with unspecified duration");
      }
      for (int i = 1; i < repeatCount; i++) {
        chunkIndex++;
        startTimes.add(startTime + (lastChunkDuration * i));
      }
    }

    private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
      type = parseType(parser);
      putNormalizedAttribute(KEY_TYPE, type);
      if (type == StreamElement.TYPE_TEXT) {
        subType = parseRequiredString(parser, KEY_SUB_TYPE);
      } else {
        subType = parser.getAttributeValue(null, KEY_SUB_TYPE);
      }
      name = parser.getAttributeValue(null, KEY_NAME);
      qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1);
      url = parseRequiredString(parser, KEY_URL);
      maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1);
      maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1);
      displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1);
      displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1);
      language = parser.getAttributeValue(null, KEY_LANGUAGE);
      timescale = parseInt(parser, KEY_TIME_SCALE, -1);
      if (timescale == -1) {
        timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
      }
      startTimes = new ArrayList<Long>();
    }

    private int parseType(XmlPullParser parser) throws ParserException {
      String value = parser.getAttributeValue(null, KEY_TYPE);
      if (value != null) {
        if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) {
          return StreamElement.TYPE_AUDIO;
        } else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) {
          return StreamElement.TYPE_VIDEO;
        } else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) {
          return StreamElement.TYPE_TEXT;
        } else {
          throw new ParserException("Invalid key value[" + value + "]");
        }
      }
      throw new MissingFieldException(KEY_TYPE);
    }

    @Override
    public void addChild(Object child) {
      if (child instanceof TrackElement) {
        tracks.add((TrackElement) child);
      }
    }

    @Override
    public Object build() {
      TrackElement[] trackElements = new TrackElement[tracks.size()];
      tracks.toArray(trackElements);
      return new StreamElement(baseUri, url, type, subType, timescale, name, qualityLevels,
          maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes,
          lastChunkDuration);
    }

  }

  private static class TrackElementParser extends ElementParser {

    public static final String TAG = "QualityLevel";

    private static final String KEY_INDEX = "Index";
    private static final String KEY_BITRATE = "Bitrate";
    private static final String KEY_CODEC_PRIVATE_DATA = "CodecPrivateData";
    private static final String KEY_SAMPLING_RATE = "SamplingRate";
    private static final String KEY_CHANNELS = "Channels";
    private static final String KEY_FOUR_CC = "FourCC";
    private static final String KEY_TYPE = "Type";
    private static final String KEY_MAX_WIDTH = "MaxWidth";
    private static final String KEY_MAX_HEIGHT = "MaxHeight";

    private final List<byte[]> csd;

    private int index;
    private int bitrate;
    private String mimeType;
    private int maxWidth;
    private int maxHeight;
    private int samplingRate;
    private int channels;

    public TrackElementParser(ElementParser parent, String baseUri) {
      super(parent, baseUri, TAG);
      this.csd = new LinkedList<byte[]>();
    }

    @Override
    public void parseStartTag(XmlPullParser parser) throws ParserException {
      int type = (Integer) getNormalizedAttribute(KEY_TYPE);
      String value;

      index = parseInt(parser, KEY_INDEX, -1);
      bitrate = parseRequiredInt(parser, KEY_BITRATE);

      if (type == StreamElement.TYPE_VIDEO) {
        maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
        maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
        mimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC));
      } else {
        maxHeight = -1;
        maxWidth = -1;
        String fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
        // If fourCC is missing and the stream type is audio, we assume AAC.
        mimeType = fourCC != null ? fourCCToMimeType(fourCC)
            : type == StreamElement.TYPE_AUDIO ? MimeTypes.AUDIO_AAC : null;
      }

      if (type == StreamElement.TYPE_AUDIO) {
        samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE);
        channels = parseRequiredInt(parser, KEY_CHANNELS);
      } else {
        samplingRate = -1;
        channels = -1;
      }

      value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
      if (value != null && value.length() > 0) {
        byte[] codecPrivateData = hexStringToByteArray(value);
        byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData);
        if (split == null) {
          csd.add(codecPrivateData);
        } else {
          for (int i = 0; i < split.length; i++) {
            csd.add(split[i]);
          }
        }
      }
    }

    @Override
    public Object build() {
      byte[][] csdArray = null;
      if (!csd.isEmpty()) {
        csdArray = new byte[csd.size()][];
        csd.toArray(csdArray);
      }
      return new TrackElement(index, bitrate, mimeType, csdArray, maxWidth, maxHeight, samplingRate,
          channels);
    }

    private static String fourCCToMimeType(String fourCC) {
      if (fourCC.equalsIgnoreCase("H264") || fourCC.equalsIgnoreCase("X264")
          || fourCC.equalsIgnoreCase("AVC1") || fourCC.equalsIgnoreCase("DAVC")) {
        return MimeTypes.VIDEO_H264;
      } else if (fourCC.equalsIgnoreCase("AAC") || fourCC.equalsIgnoreCase("AACL")
          || fourCC.equalsIgnoreCase("AACH") || fourCC.equalsIgnoreCase("AACP")) {
        return MimeTypes.AUDIO_AAC;
      } else if (fourCC.equalsIgnoreCase("TTML")) {
        return MimeTypes.APPLICATION_TTML;
      }
      return null;
    }

    private static byte[] hexStringToByteArray(String hexString) {
      int length = hexString.length();
      byte[] data = new byte[length / 2];
      for (int i = 0; i < data.length; i++) {
        int stringOffset = i * 2;
        data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
            + Character.digit(hexString.charAt(stringOffset + 1), 16));
      }
      return data;
    }

  }

}
