/*
 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "core/fileapi/File.h"

#include "bindings/core/v8/ExceptionState.h"
#include "core/dom/ExceptionCode.h"
#include "core/fileapi/FilePropertyBag.h"
#include "core/frame/UseCounter.h"
#include "platform/FileMetadata.h"
#include "platform/MIMETypeRegistry.h"
#include "platform/blob/BlobData.h"
#include "public/platform/Platform.h"
#include "public/platform/WebFileUtilities.h"
#include "wtf/CurrentTime.h"
#include "wtf/DateMath.h"
#include <memory>

namespace blink {

static String getContentTypeFromFileName(const String& name,
                                         File::ContentTypeLookupPolicy policy) {
  String type;
  int index = name.reverseFind('.');
  if (index != -1) {
    if (policy == File::WellKnownContentTypes) {
      type = MIMETypeRegistry::getWellKnownMIMETypeForExtension(
          name.substring(index + 1));
    } else {
      ASSERT(policy == File::AllContentTypes);
      type =
          MIMETypeRegistry::getMIMETypeForExtension(name.substring(index + 1));
    }
  }
  return type;
}

static std::unique_ptr<BlobData> createBlobDataForFileWithType(
    const String& path,
    const String& contentType) {
  std::unique_ptr<BlobData> blobData =
      BlobData::createForFileWithUnknownSize(path);
  blobData->setContentType(contentType);
  return blobData;
}

static std::unique_ptr<BlobData> createBlobDataForFile(
    const String& path,
    File::ContentTypeLookupPolicy policy) {
  return createBlobDataForFileWithType(
      path, getContentTypeFromFileName(path, policy));
}

static std::unique_ptr<BlobData> createBlobDataForFileWithName(
    const String& path,
    const String& fileSystemName,
    File::ContentTypeLookupPolicy policy) {
  return createBlobDataForFileWithType(
      path, getContentTypeFromFileName(fileSystemName, policy));
}

static std::unique_ptr<BlobData> createBlobDataForFileWithMetadata(
    const String& fileSystemName,
    const FileMetadata& metadata) {
  std::unique_ptr<BlobData> blobData = BlobData::create();
  blobData->setContentType(
      getContentTypeFromFileName(fileSystemName, File::WellKnownContentTypes));
  blobData->appendFile(metadata.platformPath, 0, metadata.length,
                       metadata.modificationTime / msPerSecond);
  return blobData;
}

static std::unique_ptr<BlobData> createBlobDataForFileSystemURL(
    const KURL& fileSystemURL,
    const FileMetadata& metadata) {
  std::unique_ptr<BlobData> blobData = BlobData::create();
  blobData->setContentType(getContentTypeFromFileName(
      fileSystemURL.path(), File::WellKnownContentTypes));
  blobData->appendFileSystemURL(fileSystemURL, 0, metadata.length,
                                metadata.modificationTime / msPerSecond);
  return blobData;
}

// static
File* File::create(
    ExecutionContext* context,
    const HeapVector<ArrayBufferOrArrayBufferViewOrBlobOrUSVString>& fileBits,
    const String& fileName,
    const FilePropertyBag& options,
    ExceptionState& exceptionState) {
  ASSERT(options.hasType());
  if (!options.type().containsOnlyASCII()) {
    exceptionState.throwDOMException(
        SyntaxError, "The 'type' property must consist of ASCII characters.");
    return nullptr;
  }

  double lastModified;
  if (options.hasLastModified())
    lastModified = static_cast<double>(options.lastModified());
  else
    lastModified = currentTimeMS();
  ASSERT(options.hasEndings());
  bool normalizeLineEndingsToNative = options.endings() == "native";
  if (normalizeLineEndingsToNative)
    UseCounter::count(context, UseCounter::FileAPINativeLineEndings);

  std::unique_ptr<BlobData> blobData = BlobData::create();
  blobData->setContentType(options.type().lower());
  populateBlobData(blobData.get(), fileBits, normalizeLineEndingsToNative);

  long long fileSize = blobData->length();
  return File::create(fileName, lastModified,
                      BlobDataHandle::create(std::move(blobData), fileSize));
}

File* File::createWithRelativePath(const String& path,
                                   const String& relativePath) {
  File* file = new File(path, File::AllContentTypes, File::IsUserVisible);
  file->m_relativePath = relativePath;
  return file;
}

File::File(const String& path,
           ContentTypeLookupPolicy policy,
           UserVisibility userVisibility)
    : Blob(BlobDataHandle::create(createBlobDataForFile(path, policy), -1)),
      m_hasBackingFile(true),
      m_userVisibility(userVisibility),
      m_path(path),
      m_name(Platform::current()->fileUtilities()->baseName(path)),
      m_snapshotSize(-1),
      m_snapshotModificationTimeMS(invalidFileTime()) {}

File::File(const String& path,
           const String& name,
           ContentTypeLookupPolicy policy,
           UserVisibility userVisibility)
    : Blob(BlobDataHandle::create(
          createBlobDataForFileWithName(path, name, policy),
          -1)),
      m_hasBackingFile(true),
      m_userVisibility(userVisibility),
      m_path(path),
      m_name(name),
      m_snapshotSize(-1),
      m_snapshotModificationTimeMS(invalidFileTime()) {}

File::File(const String& path,
           const String& name,
           const String& relativePath,
           UserVisibility userVisibility,
           bool hasSnapshotData,
           uint64_t size,
           double lastModified,
           PassRefPtr<BlobDataHandle> blobDataHandle)
    : Blob(std::move(blobDataHandle)),
      m_hasBackingFile(!path.isEmpty() || !relativePath.isEmpty()),
      m_userVisibility(userVisibility),
      m_path(path),
      m_name(name),
      m_snapshotSize(hasSnapshotData ? static_cast<long long>(size) : -1),
      m_snapshotModificationTimeMS(hasSnapshotData ? lastModified
                                                   : invalidFileTime()),
      m_relativePath(relativePath) {}

File::File(const String& name,
           double modificationTimeMS,
           PassRefPtr<BlobDataHandle> blobDataHandle)
    : Blob(std::move(blobDataHandle)),
      m_hasBackingFile(false),
      m_userVisibility(File::IsNotUserVisible),
      m_name(name),
      m_snapshotSize(Blob::size()),
      m_snapshotModificationTimeMS(modificationTimeMS) {}

File::File(const String& name,
           const FileMetadata& metadata,
           UserVisibility userVisibility)
    : Blob(BlobDataHandle::create(
          createBlobDataForFileWithMetadata(name, metadata),
          metadata.length)),
      m_hasBackingFile(true),
      m_userVisibility(userVisibility),
      m_path(metadata.platformPath),
      m_name(name),
      m_snapshotSize(metadata.length),
      m_snapshotModificationTimeMS(metadata.modificationTime) {}

File::File(const KURL& fileSystemURL,
           const FileMetadata& metadata,
           UserVisibility userVisibility)
    : Blob(BlobDataHandle::create(
          createBlobDataForFileSystemURL(fileSystemURL, metadata),
          metadata.length)),
      m_hasBackingFile(false),
      m_userVisibility(userVisibility),
      m_name(decodeURLEscapeSequences(fileSystemURL.lastPathComponent())),
      m_fileSystemURL(fileSystemURL),
      m_snapshotSize(metadata.length),
      m_snapshotModificationTimeMS(metadata.modificationTime) {}

File::File(const File& other)
    : Blob(other.blobDataHandle()),
      m_hasBackingFile(other.m_hasBackingFile),
      m_userVisibility(other.m_userVisibility),
      m_path(other.m_path),
      m_name(other.m_name),
      m_fileSystemURL(other.m_fileSystemURL),
      m_snapshotSize(other.m_snapshotSize),
      m_snapshotModificationTimeMS(other.m_snapshotModificationTimeMS),
      m_relativePath(other.m_relativePath) {}

File* File::clone(const String& name) const {
  File* file = new File(*this);
  if (!name.isNull())
    file->m_name = name;
  return file;
}

double File::lastModifiedMS() const {
  if (hasValidSnapshotMetadata() &&
      isValidFileTime(m_snapshotModificationTimeMS))
    return m_snapshotModificationTimeMS;

  double modificationTimeMS;
  if (hasBackingFile() && getFileModificationTime(m_path, modificationTimeMS) &&
      isValidFileTime(modificationTimeMS))
    return modificationTimeMS;

  return currentTimeMS();
}

long long File::lastModified() const {
  double modifiedDate = lastModifiedMS();

  // The getter should return the current time when the last modification time
  // isn't known.
  if (!isValidFileTime(modifiedDate))
    modifiedDate = currentTimeMS();

  // lastModified returns a number, not a Date instance,
  // http://dev.w3.org/2006/webapi/FileAPI/#file-attrs
  return floor(modifiedDate);
}

double File::lastModifiedDate() const {
  double modifiedDate = lastModifiedMS();

  // The getter should return the current time when the last modification time
  // isn't known.
  if (!isValidFileTime(modifiedDate))
    modifiedDate = currentTimeMS();

  // lastModifiedDate returns a Date instance,
  // http://www.w3.org/TR/FileAPI/#dfn-lastModifiedDate
  return modifiedDate;
}

unsigned long long File::size() const {
  if (hasValidSnapshotMetadata())
    return m_snapshotSize;

  // FIXME: JavaScript cannot represent sizes as large as unsigned long long, we
  // need to come up with an exception to throw if file size is not
  // representable.
  long long size;
  if (!hasBackingFile() || !getFileSize(m_path, size))
    return 0;
  return static_cast<unsigned long long>(size);
}

Blob* File::slice(long long start,
                  long long end,
                  const String& contentType,
                  ExceptionState& exceptionState) const {
  if (isClosed()) {
    exceptionState.throwDOMException(InvalidStateError,
                                     "File has been closed.");
    return nullptr;
  }

  if (!m_hasBackingFile)
    return Blob::slice(start, end, contentType, exceptionState);

  // FIXME: This involves synchronous file operation. We need to figure out how
  // to make it asynchronous.
  long long size;
  double modificationTimeMS;
  captureSnapshot(size, modificationTimeMS);
  clampSliceOffsets(size, start, end);

  long long length = end - start;
  std::unique_ptr<BlobData> blobData = BlobData::create();
  blobData->setContentType(contentType);
  if (!m_fileSystemURL.isEmpty()) {
    blobData->appendFileSystemURL(m_fileSystemURL, start, length,
                                  modificationTimeMS / msPerSecond);
  } else {
    ASSERT(!m_path.isEmpty());
    blobData->appendFile(m_path, start, length,
                         modificationTimeMS / msPerSecond);
  }
  return Blob::create(BlobDataHandle::create(std::move(blobData), length));
}

void File::captureSnapshot(long long& snapshotSize,
                           double& snapshotModificationTimeMS) const {
  if (hasValidSnapshotMetadata()) {
    snapshotSize = m_snapshotSize;
    snapshotModificationTimeMS = m_snapshotModificationTimeMS;
    return;
  }

  // Obtains a snapshot of the file by capturing its current size and
  // modification time. This is used when we slice a file for the first time.
  // If we fail to retrieve the size or modification time, probably due to that
  // the file has been deleted, 0 size is returned.
  FileMetadata metadata;
  if (!hasBackingFile() || !getFileMetadata(m_path, metadata)) {
    snapshotSize = 0;
    snapshotModificationTimeMS = invalidFileTime();
    return;
  }

  snapshotSize = metadata.length;
  snapshotModificationTimeMS = metadata.modificationTime;
}

void File::close(ExecutionContext* executionContext,
                 ExceptionState& exceptionState) {
  if (isClosed()) {
    exceptionState.throwDOMException(InvalidStateError,
                                     "Blob has been closed.");
    return;
  }

  // Reset the File to its closed representation, an empty
  // Blob. The name isn't cleared, as it should still be
  // available.
  m_hasBackingFile = false;
  m_path = String();
  m_fileSystemURL = KURL();
  invalidateSnapshotMetadata();
  m_relativePath = String();
  Blob::close(executionContext, exceptionState);
}

void File::appendTo(BlobData& blobData) const {
  if (!m_hasBackingFile) {
    Blob::appendTo(blobData);
    return;
  }

  // FIXME: This involves synchronous file operation. We need to figure out how
  // to make it asynchronous.
  long long size;
  double modificationTimeMS;
  captureSnapshot(size, modificationTimeMS);
  if (!m_fileSystemURL.isEmpty()) {
    blobData.appendFileSystemURL(m_fileSystemURL, 0, size,
                                 modificationTimeMS / msPerSecond);
    return;
  }
  ASSERT(!m_path.isEmpty());
  blobData.appendFile(m_path, 0, size, modificationTimeMS / msPerSecond);
}

bool File::hasSameSource(const File& other) const {
  if (m_hasBackingFile != other.m_hasBackingFile)
    return false;

  if (m_hasBackingFile)
    return m_path == other.m_path;

  if (m_fileSystemURL.isEmpty() != other.m_fileSystemURL.isEmpty())
    return false;

  if (!m_fileSystemURL.isEmpty())
    return m_fileSystemURL == other.m_fileSystemURL;

  return uuid() == other.uuid();
}

}  // namespace blink
