/*
 * 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.android.builder.internal.utils;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.Objects;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;

import java.io.File;
import java.io.IOException;

/**
 * A cache for file contents. The cache allows closing a file and saving in memory its contents
 * (or some related information). It can then be used to check if the contents are still valid
 * at some later time. Typical usage flow is:
 *
 * <p>
 * <pre>
 *    Object fileRepresentation = // ...
 *    File toWrite = // ...
 *    // Write file contents and update in memory representation
 *    CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);
 *    contents.closed(fileRepresentation);
 *
 *    // Later, when data is needed:
 *    if (contents.isValid()) {
 *        fileRepresentation = contents.getCache();
 *    } else {
 *        // Re-read the file and recreate the file representation
 *    }
 * </pre>
 * @param <T> the type of cached contents
 */
public class CachedFileContents<T> {

    /**
     * The file.
     */
    @NonNull
    private File mFile;

    /**
     * Time when last closed (time when {@link #closed(Object)} was invoked).
     */
    private long mLastClosed;

    /**
     * Size of the file when last closed.
     */
    private long mSize;

    /**
     * Hash of the file when closed. {@code null} if hashing failed for some reason.
     */
    @Nullable
    private HashCode mHash;

    /**
     * Cached data associated with the file.
     */
    @Nullable
    private T mCache;

    /**
     * Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked
     * to set the cache.
     *
     * @param file the file
     */
    public CachedFileContents(@NonNull File file) {
        mFile = file;
    }

    /**
     * Should be called when the file's contents are set and the file closed. This will save the
     * cache and register the file's timestamp to later detect if it has been modified.
     * <p>
     * This method can be called as many times as the file has been written.
     *
     * @param cache an optional cache to save
     */
    public void closed(@Nullable T cache) {
        mCache = cache;
        mLastClosed = mFile.lastModified();
        mSize = mFile.length();
        mHash = hashFile();
    }

    /**
     * Are the cached contents still valid? If this method determines that the file has been
     * modified since the last time {@link #closed(Object)} was invoked.
     *
     * @return are the cached contents still valid? If this method returns {@code false}, the
     * cache is cleared
     */
    public boolean isValid() {
        boolean valid = true;

        if (!mFile.exists()) {
            valid = false;
        }

        if (valid && mFile.lastModified() != mLastClosed) {
            valid = false;
        }

        if (valid && mFile.length() != mSize) {
            valid = false;
        }

        if (valid && !Objects.equal(mHash, hashFile())) {
            valid = false;
        }

        if (!valid) {
            mCache = null;
        }

        return valid;
    }

    /**
     * Obtains the cached data set with {@link #closed(Object)} if the file has not been modified
     * since {@link #closed(Object)} was invoked.
     *
     * @return the last cached data or {@code null} if the file has been modified since
     * {@link #closed(Object)} has been invoked
     */
    @Nullable
    public T getCache() {
        return mCache;
    }

    /**
     * Computes the hashcode of the cached file.
     *
     * @return the hash code
     */
    @Nullable
    private HashCode hashFile() {
        try {
            return Files.hash(mFile, Hashing.crc32());
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * Obtains the file used for caching.
     *
     * @return the file; this file always exists and contains the old (cached) contents of the
     * file
     */
    @NonNull
    public File getFile() {
        return mFile;
    }
}
