/*
 * Copyright (C) 2011 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.tradefed.result;

import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.StreamUtil;

import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A utility class that checks the device for files and sends them to
 * {@link ITestInvocationListener#testLog(String, LogDataType, InputStreamSource)} if found.
 */
public class DeviceFileReporter {
    private final Map<String, LogDataType> mFilePatterns = new LinkedHashMap<>();
    private final ITestInvocationListener mListener;
    private final ITestDevice mDevice;

    /** Whether to ignore files that have already been captured by a prior Pattern */
    private boolean mSkipRepeatFiles = true;
    /** The files which have already been reported */
    private Set<String> mReportedFiles = new HashSet<>();

    /** Whether to attempt to infer data types for patterns with {@code UNKNOWN} data type */
    private boolean mInferDataTypes = true;

    private LogDataType mDefaultFileType = LogDataType.UNKNOWN;

    private static final Map<String, LogDataType> DATA_TYPE_REVERSE_MAP = new HashMap<>();

    static {
        // Make it easy to map backward from file extension to LogDataType
        for (LogDataType type : LogDataType.values()) {
            // Extracted extension will contain a leading dot
            final String ext = "." + type.getFileExt();
            if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) {
                continue;
            }

            DATA_TYPE_REVERSE_MAP.put(ext, type);
        }
    }
    /**
     * Initialize a new DeviceFileReporter with the provided {@link ITestDevice}
     */
    public DeviceFileReporter(ITestDevice device, ITestInvocationListener listener) {
        // Do a null check here, since otherwise that error would be asynchronous
        if (device == null || listener == null) {
            throw new NullPointerException();
        }
        mDevice = device;
        mListener = listener;
    }

    /**
     * Add patterns with the log data type set to the default.
     *
     * @param patterns a varargs array of {@link String} filename glob patterns. Should be absolute.
     * @see #setDefaultLogDataType
     */
    public void addPatterns(String... patterns) {
        addPatterns(Arrays.asList(patterns));
    }

    /**
     * Add patterns with the log data type set to the default.
     *
     * @param patterns a {@link List} of {@link String} filename glob patterns. Should be absolute.
     * @see #setDefaultLogDataType
     */
    public void addPatterns(List<String> patterns) {
        for (String pat : patterns) {
            mFilePatterns.put(pat, mDefaultFileType);
        }
    }

    /**
     * Add patterns with the respective log data types
     *
     * @param patterns a {@link Map} of {@link String} filename glob patterns to their respective
     *        {@link LogDataType}s.  The globs should be absolute.
     * @see #setDefaultLogDataType
     */
    public void addPatterns(Map<String, LogDataType> patterns) {
        mFilePatterns.putAll(patterns);
    }

    /**
     * Set the default log data type set for patterns that don't have an associated type.
     *
     * @param type the {@link LogDataType}
     * @see #addPatterns(List)
     */
    public void setDefaultLogDataType(LogDataType type) {
        if (type == null) {
            throw new NullPointerException();
        }
        mDefaultFileType = type;
    }

    /**
     * Whether or not to skip files which have already been reported.  This is only relevant when
     * multiple patterns are being used, and two or more of those patterns match the same file.
     * <p />
     * Note that this <emph>must only</emph> be called prior to calling {@link #run()}. Doing
     * otherwise will cause undefined behavior.
     */
    public void setSkipRepeatFiles(boolean skip) {
        mSkipRepeatFiles = skip;
    }

    /**
     * Whether to <emph>attempt to</emph> infer the data types of {@code UNKNOWN} files by checking
     * the file extensions against a list.
     * <p />
     * Note that, when enabled, these inferences will only be made for patterns with file type
     * {@code UNKNOWN} (which includes patterns added without a specific type, and without the)
     * default type having been set manually).  If the inference fails, the data type will remain
     * as {@code UNKNOWN}.
     */
    public void setInferUnknownDataTypes(boolean infer) {
        mInferDataTypes = infer;
    }

    /**
     * Actually search the filesystem for the specified patterns and send them to
     * {@link ITestInvocationListener#testLog} if found
     */
    public List<String> run() throws DeviceNotAvailableException {
        List<String> filenames = new LinkedList<>();
        CLog.d(String.format("Analyzing %d patterns.", mFilePatterns.size()));
        for (Map.Entry<String, LogDataType> pat : mFilePatterns.entrySet()) {
            final String searchCmd = String.format("ls %s", pat.getKey());
            final String fileList = mDevice.executeShellCommand(searchCmd);
            // Early bail out if we don't find anything
            if (fileList.contains(": No such file or directory")) {
                continue;
            }

            for (String line : fileList.split("\r?\n")) {
                line = line.trim();
                if (line.isEmpty()) {
                    continue;
                }
                // If the command output files on the same line.
                for (String filename : line.split("\\s+")) {
                    if (mSkipRepeatFiles && mReportedFiles.contains(filename)) {
                        CLog.v("Skipping already-reported file %s", filename);
                        continue;
                    }

                    File file = null;
                    InputStreamSource iss = null;
                    try {
                        CLog.d(
                                "Trying to pull file '%s' from device %s",
                                filename, mDevice.getSerialNumber());
                        file = mDevice.pullFile(filename);
                        iss = createIssForFile(file);
                        final LogDataType type = getDataType(filename, pat.getValue());
                        CLog.d(
                                "Local file %s has size %d and type %s",
                                file, file.length(), type.getFileExt());
                        mListener.testLog(filename, type, iss);
                        filenames.add(filename);
                        mReportedFiles.add(filename);
                    } finally {
                        StreamUtil.cancel(iss);
                        iss = null;
                        FileUtil.deleteFile(file);
                    }
                }
            }
        }
        return filenames;
    }

    /**
     * Returns the data type to use for a given file.  Will attempt to infer the data type from the
     * file's extension IFF inferences are enabled, and the current data type is {@code UNKNOWN}.
     */
    LogDataType getDataType(String filename, LogDataType defaultType) {
        if (!mInferDataTypes) {
            return defaultType;
        }
        if (!LogDataType.UNKNOWN.equals(defaultType)) {
            return defaultType;
        }

        CLog.d("Running type inference for file %s with default type %s", filename, defaultType);
        String ext = FileUtil.getExtension(filename);
        CLog.v("Found raw extension \"%s\"", ext);

        // Normalize the extension
        if (ext == null) {
            return defaultType;
        }
        ext = ext.toLowerCase();

        if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) {
            final LogDataType newType = DATA_TYPE_REVERSE_MAP.get(ext);
            CLog.d("Inferred data type %s", newType);
            return newType;
        } else {
            CLog.v("Failed to find a reverse map for extension \"%s\"", ext);
            return defaultType;
        }
    }

    /**
     * Create an {@link InputStreamSource} for a file
     *
     * <p>Exposed for unit testing
     */
    InputStreamSource createIssForFile(File file) {
        return new FileInputStreamSource(file);
    }
}
