/*
 * 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 android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class stores span metadata in filename.
 */
/*package*/ final class SimpleCacheSpan extends CacheSpan {

  private static final String SUFFIX = ".v3.exo";
  private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile(
      "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL);
  private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile(
      "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL);
  private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile(
      "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL);

  /**
   * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code
   * lastAccessTimestamp}.
   *
   * @param cacheDir The parent abstract pathname.
   * @param id The cache file id.
   * @param position The position of the stored data in the original stream.
   * @param lastAccessTimestamp The last access timestamp.
   * @return The cache file.
   */
  public static File getCacheFile(File cacheDir, int id, long position, long lastAccessTimestamp) {
    return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX);
  }

  /**
   * Creates a lookup span.
   *
   * @param key The cache key.
   * @param position The position of the {@link CacheSpan} in the original stream.
   * @return The span.
   */
  public static SimpleCacheSpan createLookup(String key, long position) {
    return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
  }

  /**
   * Creates an open hole span.
   *
   * @param key The cache key.
   * @param position The position of the {@link CacheSpan} in the original stream.
   * @return The span.
   */
  public static SimpleCacheSpan createOpenHole(String key, long position) {
    return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
  }

  /**
   * Creates a closed hole span.
   *
   * @param key The cache key.
   * @param position The position of the {@link CacheSpan} in the original stream.
   * @param length The length of the {@link CacheSpan}.
   * @return The span.
   */
  public static SimpleCacheSpan createClosedHole(String key, long position, long length) {
    return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null);
  }

  /**
   * Creates a cache span from an underlying cache file. Upgrades the file if necessary.
   *
   * @param file The cache file.
   * @param index Cached content index.
   * @return The span, or null if the file name is not correctly formatted, or if the id is not
   *     present in the content index.
   */
  @Nullable
  public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) {
    String name = file.getName();
    if (!name.endsWith(SUFFIX)) {
      file = upgradeFile(file, index);
      if (file == null) {
        return null;
      }
      name = file.getName();
    }

    Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name);
    if (!matcher.matches()) {
      return null;
    }
    long length = file.length();
    int id = Integer.parseInt(matcher.group(1));
    String key = index.getKeyForId(id);
    return key == null ? null : new SimpleCacheSpan(key, Long.parseLong(matcher.group(2)), length,
        Long.parseLong(matcher.group(3)), file);
  }

  /**
   * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}.
   *
   * @param file The cache file.
   * @param index Cached content index.
   * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the
   *     file can not be renamed.
   */
  @Nullable
  private static File upgradeFile(File file, CachedContentIndex index) {
    String key;
    String filename = file.getName();
    Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);
    if (matcher.matches()) {
      key = Util.unescapeFileName(matcher.group(1));
      if (key == null) {
        return null;
      }
    } else {
      matcher = CACHE_FILE_PATTERN_V1.matcher(filename);
      if (!matcher.matches()) {
        return null;
      }
      key = matcher.group(1); // Keys were not escaped in version 1.
    }

    File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key),
        Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)));
    if (!file.renameTo(newCacheFile)) {
      return null;
    }
    return newCacheFile;
  }

  /**
   * @param key The cache key.
   * @param position The position of the {@link CacheSpan} in the original stream.
   * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
   *     open-ended hole.
   * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link
   *     #isCached} is false.
   * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
   */
  private SimpleCacheSpan(
      String key, long position, long length, long lastAccessTimestamp, @Nullable File file) {
    super(key, position, length, lastAccessTimestamp, file);
  }

  /**
   * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This
   * doesn't copy or change the underlying cache file.
   *
   * @param id The cache file id.
   * @return A {@link SimpleCacheSpan} with updated last access time stamp.
   * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false).
   */
  public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) {
    Assertions.checkState(isCached);
    long now = System.currentTimeMillis();
    File newCacheFile = getCacheFile(file.getParentFile(), id, position, now);
    return new SimpleCacheSpan(key, position, length, now, newCacheFile);
  }

}
