/*
 * 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.upstream.cache;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Writes data into a cache.
 */
public final class CacheDataSink implements DataSink {

  private final Cache cache;
  private final long maxCacheFileSize;

  private DataSpec dataSpec;
  private File file;
  private FileOutputStream outputStream;
  private long outputStreamBytesWritten;
  private long dataSpecBytesWritten;

  /**
   * Thrown when IOException is encountered when writing data into sink.
   */
  public static class CacheDataSinkException extends CacheException {

    public CacheDataSinkException(IOException cause) {
      super(cause);
    }

  }

  /**
   * @param cache The cache into which data should be written.
   * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
   *    a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
   *    multiple cache files.
   */
  public CacheDataSink(Cache cache, long maxCacheFileSize) {
    this.cache = Assertions.checkNotNull(cache);
    this.maxCacheFileSize = maxCacheFileSize;
  }

  @Override
  public void open(DataSpec dataSpec) throws CacheDataSinkException {
    this.dataSpec = dataSpec;
    if (dataSpec.length == C.LENGTH_UNSET) {
      return;
    }
    dataSpecBytesWritten = 0;
    try {
      openNextOutputStream();
    } catch (IOException e) {
      throw new CacheDataSinkException(e);
    }
  }

  @Override
  public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
    if (dataSpec.length == C.LENGTH_UNSET) {
      return;
    }
    try {
      int bytesWritten = 0;
      while (bytesWritten < length) {
        if (outputStreamBytesWritten == maxCacheFileSize) {
          closeCurrentOutputStream();
          openNextOutputStream();
        }
        int bytesToWrite = (int) Math.min(length - bytesWritten,
            maxCacheFileSize - outputStreamBytesWritten);
        outputStream.write(buffer, offset + bytesWritten, bytesToWrite);
        bytesWritten += bytesToWrite;
        outputStreamBytesWritten += bytesToWrite;
        dataSpecBytesWritten += bytesToWrite;
      }
    } catch (IOException e) {
      throw new CacheDataSinkException(e);
    }
  }

  @Override
  public void close() throws CacheDataSinkException {
    if (dataSpec == null || dataSpec.length == C.LENGTH_UNSET) {
      return;
    }
    try {
      closeCurrentOutputStream();
    } catch (IOException e) {
      throw new CacheDataSinkException(e);
    }
  }

  private void openNextOutputStream() throws IOException {
    file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten,
        Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize));
    outputStream = new FileOutputStream(file);
    outputStreamBytesWritten = 0;
  }

  private void closeCurrentOutputStream() throws IOException {
    if (outputStream == null) {
      return;
    }

    boolean success = false;
    try {
      outputStream.flush();
      outputStream.getFD().sync();
      success = true;
    } finally {
      Util.closeQuietly(outputStream);
      if (success) {
        cache.commitFile(file);
      } else {
        file.delete();
      }
      outputStream = null;
      file = null;
    }
  }

}

@@ -996,16 +996,53 @@
 l.Util;%0A
+import java.io.BufferedOutputStream;%0A
 import j
@@ -1054,16 +1054,16 @@
 o.File;%0A
-
 import j
@@ -1114,16 +1114,45 @@
 ception;
+%0Aimport java.io.OutputStream;
 %0A%0A/**%0A *
@@ -1305,16 +1305,48 @@
 ileSize;
+%0A  private final int bufferSize;
 %0A%0A  priv
@@ -1389,16 +1389,53 @@
 e file;%0A
+  private OutputStream outputStream;%0A
   privat
@@ -1445,33 +1445,47 @@
 ileOutputStream 
-o
+underlyingFileO
 utputStream;%0A  p
@@ -2160,24 +2160,574 @@
 FileSize) %7B%0A
+    this(cache, maxCacheFileSize, 0);%0A  %7D%0A%0A  /**%0A   * @param cache The cache into which data should be written.%0A   * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for%0A   *    a %7B@link DataSpec%7D whose size exceeds this value, then the data will be fragmented into%0A   *    multiple cache files.%0A   * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative%0A   *    value disables buffering.%0A   */%0A  public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) %7B%0A
     this.cac
@@ -2809,16 +2809,50 @@
 leSize;%0A
+    this.bufferSize = bufferSize;%0A
   %7D%0A%0A  @
@@ -4499,25 +4499,39 @@
 Size));%0A    
-o
+underlyingFileO
 utputStream 
@@ -4556,24 +4556,171 @@
 ream(file);%0A
+    outputStream = bufferSize %3E 0%0A        ? new BufferedOutputStream(underlyingFileOutputStream, bufferSize)%0A        : underlyingFileOutputStream;%0A
     outputSt
@@ -4931,25 +4931,39 @@
 sh();%0A      
-o
+underlyingFileO
 utputStream.

Update CacheDataSink to optionally use a BufferedOutputStream for writing