/*
 * Copyright (C) 2015 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.repository.testframework;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.repository.io.FileOpUtils;
import com.android.repository.io.impl.FileOpImpl;
import com.android.repository.io.impl.FileSystemFileOp;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.List;


/**
 * Mock version of {@link FileOpImpl} that wraps some common {@link File}
 * operations on files and folders.
 * <p>
 * This version does not perform any file operation. Instead it records a textual
 * representation of all the file operations performed.
 * <p>
 * To avoid cross-platform path issues (e.g. Windows path), the methods here should
 * always use rooted (aka absolute) unix-looking paths, e.g. "/dir1/dir2/file3".
 * When processing {@link File}, you can convert them using {@link #getAgnosticAbsPath(File)}.
 */
public class MockFileOp extends FileSystemFileOp {
    private FileSystem mFileSystem = createFileSystem();

    public MockFileOp() {
        mIsWindows = FileOpUtils.create().isWindows();
    }

    private static FileSystem createFileSystem() {
        // TODO: use the current platform configuration and get rid of all the agnostic path stuff.
        Configuration config = Configuration.unix();
        config = config.toBuilder()
                .setWorkingDirectory("/")
                .setAttributeViews("posix")
                .build();
        return Jimfs.newFileSystem(config);
    }

    @Override
    public FileSystem getFileSystem() {
        return mFileSystem;
    }

    /** Resets the internal state, as if the object had been newly created. */
    public void reset() {
        mFileSystem = createFileSystem();
    }

    @Override
    public void deleteOnExit(File file) {
        // nothing
    }

    public void setIsWindows(boolean isWindows) {
        mIsWindows = isWindows;
    }

    @Override
    public boolean canWrite(@NonNull File file) {
        try {
            return !Sets.intersection(
                    Files.getPosixFilePermissions(toPath(new File(getAgnosticAbsPath(file)))),
                    ImmutableSet.of(
                            PosixFilePermission.OTHERS_WRITE,
                            PosixFilePermission.GROUP_WRITE,
                            PosixFilePermission.OWNER_WRITE))
                    .isEmpty();
        }
        catch (IOException e) {
            return false;
        }
    }

    @NonNull
    public String getAgnosticAbsPath(@NonNull File file) {
        return getAgnosticAbsPath(file.getAbsolutePath());
    }

    @NonNull
    public String getAgnosticAbsPath(@NonNull String path) {
        if (isWindows()) {
            // Try to convert the windows-looking path to a unix-looking one
            path = path.replace('\\', '/');
            path = path.replaceAll("^[A-Z]:", "");
        }
        return path;
    }

    /**
     * Records a new absolute file path.
     * Parent folders are automatically created.
     */
    public void recordExistingFile(@NonNull File file) {
        recordExistingFile(getAgnosticAbsPath(file), 0, (byte[])null);
    }

    /**
     * Records a new absolute file path.
     * Parent folders are automatically created.
     * <p>
     * The syntax should always look "unix-like", e.g. "/dir/file".
     * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}.
     * @param absFilePath A unix-like file path, e.g. "/dir/file"
     */
    public void recordExistingFile(@NonNull String absFilePath) {
        recordExistingFile(absFilePath, 0, (byte[])null);
    }

    /**
     * Records a new absolute file path and its input stream content.
     * Parent folders are automatically created.
     * <p>
     * The syntax should always look "unix-like", e.g. "/dir/file".
     * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}.
     * @param absFilePath A unix-like file path, e.g. "/dir/file"
     * @param inputStream A non-null byte array of content to return
     *                    via {@link #newFileInputStream(File)}.
     */
    public void recordExistingFile(@NonNull String absFilePath, @Nullable byte[] inputStream) {
        recordExistingFile(absFilePath, 0, inputStream);
    }

    /**
     * Records a new absolute file path and its input stream content.
     * Parent folders are automatically created.
     * <p>
     * The syntax should always look "unix-like", e.g. "/dir/file".
     * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}.
     * @param absFilePath A unix-like file path, e.g. "/dir/file"
     * @param content A non-null UTF-8 content string to return
     *                    via {@link #newFileInputStream(File)}.
     */
    public void recordExistingFile(@NonNull String absFilePath, @NonNull String content) {
        recordExistingFile(absFilePath, 0, content.getBytes(Charsets.UTF_8));
    }

    /**
     * Records a new absolute file path and its input stream content.
     * Parent folders are automatically created.
     * <p>
     * The syntax should always look "unix-like", e.g. "/dir/file".
     * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}.
     * @param absFilePath A unix-like file path, e.g. "/dir/file"
     * @param inputStream A non-null byte array of content to return
     *                    via {@link #newFileInputStream(File)}.
     */
    public void recordExistingFile(@NonNull String absFilePath,
      long lastModified,
      @Nullable byte[] inputStream) {
        try {
            Path path = mFileSystem.getPath(getAgnosticAbsPath(absFilePath));
            Files.createDirectories(path.getParent());
            Files.write(path,
              inputStream == null ? new byte[0] : inputStream);
            Files.setLastModifiedTime(path, FileTime.fromMillis(lastModified));
        } catch (IOException e) {
            assert false : e.getMessage();
        }
    }

    /**
     * Records a new absolute file path and its input stream content.
     * Parent folders are automatically created.
     * <p>
     * The syntax should always look "unix-like", e.g. "/dir/file".
     * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}.
     * @param absFilePath A unix-like file path, e.g. "/dir/file"
     * @param content A non-null UTF-8 content string to return
     *                    via {@link #newFileInputStream(File)}.
     */
    public void recordExistingFile(@NonNull String absFilePath,
      long lastModified,
      @NonNull String content) {
        recordExistingFile(absFilePath, lastModified, content.getBytes(Charsets.UTF_8));
    }

    /**
     * Records a new absolute folder path.
     * Parent folders are automatically created.
     */
    public void recordExistingFolder(File folder) {
        recordExistingFolder(getAgnosticAbsPath(folder));
    }

    /**
     * Records a new absolute folder path.
     * Parent folders are automatically created.
     * <p>
     * The syntax should always look "unix-like", e.g. "/dir/file".
     * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}.
     * @param absFolderPath A unix-like folder path, e.g. "/dir/file"
     */
    public void recordExistingFolder(String absFolderPath) {
        try {
            Files.createDirectories(mFileSystem.getPath(getAgnosticAbsPath(absFolderPath)));
        } catch (IOException e) {
            assert false : e.getMessage();
        }
    }

    /**
     * Returns true if a folder with the given path has been recorded.
     */
    public boolean hasRecordedExistingFolder(File folder) {
        return exists(folder) && isDirectory(folder);
    }

    /**
     * Returns the list of paths added using {@link #recordExistingFile(String)}
     * and eventually updated by {@link #delete(File)} operations.
     * <p>
     * The returned list is sorted by alphabetic absolute path string.
     */
    @NonNull
    public String[] getExistingFiles() {
        List<String> result = new ArrayList<>();
        mFileSystem.getRootDirectories().forEach(path -> {
            try {
                Files.find(path, 100, (p, a) -> true).filter(p -> Files.isRegularFile(p))
                  .forEach(p -> result.add(p.toString()));
            } catch (IOException e) {
                assert false : e.getMessage();
            }
        });
        return result.toArray(new String[result.size()]);
    }

    /**
     * Returns the list of folder paths added using {@link #recordExistingFolder(String)}
     * and eventually updated {@link #delete(File)} or {@link #mkdirs(File)} operations.
     * <p>
     * The returned list is sorted by alphabetic absolute path string.
     */
    @NonNull
    public String[] getExistingFolders() {
        List<String> result = new ArrayList<>();
        mFileSystem.getRootDirectories().forEach(path -> {
            try {
                Files.find(path, 100, (p, a) -> true).filter(p -> Files.isDirectory(p))
                  .forEach(p -> result.add(p.toString()));
            } catch (IOException e) {
                assert false : e.getMessage();
            }
        });
        return result.toArray(new String[result.size()]);
    }

    @Override
    public File ensureRealFile(@NonNull File in) throws IOException {
        if (!exists(in)) {
            return in;
        }
        File result = File.createTempFile("MockFileOp", null);
        result.deleteOnExit();
        OutputStream os = new FileOutputStream(result);
        try {
            ByteStreams.copy(newFileInputStream(in), os);
        }
        finally {
            os.close();
        }
        return result;
    }

    public byte[] getContent(File file) {
        try {
            return Files.readAllBytes(toPath(file));
        }
        catch (IOException e) {
            return new byte[0];
        }
    }

    @NonNull
    @Override
    public Path toPath(@NonNull File file) {
        return getFileSystem().getPath(getAgnosticAbsPath(file.getPath()));
    }
}
