/*
 * Copyright (C) 2018 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.internal.os;

import android.os.BatteryStats;
import android.os.Parcel;
import android.os.StatFs;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ParseUtils;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * BatteryStatsHistory encapsulates battery history files.
 * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
 * {@link #mActiveFile}.
 * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
 * current mActiveFile is closed and a new mActiveFile is open.
 * History files are under directory /data/system/battery-history/.
 * History files have name battery-history-<num>.bin. The file number <num> starts from zero and
 * grows sequentially.
 * The mActiveFile is always the highest numbered history file.
 * The lowest number file is always the oldest file.
 * The highest number file is always the newest file.
 * The file number grows sequentially and we never skip number.
 * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
 * the lowest numbered file is deleted and a new file is open.
 *
 * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
 * locks on BatteryStatsImpl object.
 */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class BatteryStatsHistory {
    private static final boolean DEBUG = false;
    private static final String TAG = "BatteryStatsHistory";
    public static final String HISTORY_DIR = "battery-history";
    public static final String FILE_SUFFIX = ".bin";
    private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;

    private final BatteryStatsImpl mStats;
    private final Parcel mHistoryBuffer;
    private final File mHistoryDir;
    /**
     * The active history file that the history buffer is backed up into.
     */
    private AtomicFile mActiveFile;
    /**
     * A list of history files with incremental indexes.
     */
    private final List<Integer> mFileNumbers = new ArrayList<>();

    /**
     * A list of small history parcels, used when BatteryStatsImpl object is created from
     * deserialization of a parcel, such as Settings app or checkin file.
     */
    private List<Parcel> mHistoryParcels = null;

    /**
     * When iterating history files, the current file index.
     */
    private int mCurrentFileIndex;
    /**
     * When iterating history files, the current file parcel.
     */
    private Parcel mCurrentParcel;
    /**
     * When iterating history file, the current parcel's Parcel.dataSize().
     */
    private int mCurrentParcelEnd;
    /**
     * When iterating history files, the current record count.
     */
    private int mRecordCount = 0;
    /**
     * Used when BatteryStatsImpl object is created from deserialization of a parcel,
     * such as Settings app or checkin file, to iterate over history parcels.
     */
    private int mParcelIndex = 0;

    /**
     * Constructor
     * @param stats BatteryStatsImpl object.
     * @param systemDir typically /data/system
     * @param historyBuffer The in-memory history buffer.
     */
    public BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer) {
        mStats = stats;
        mHistoryBuffer = historyBuffer;
        mHistoryDir = new File(systemDir, HISTORY_DIR);
        mHistoryDir.mkdirs();
        if (!mHistoryDir.exists()) {
            Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
        }

        final Set<Integer> dedup = new ArraySet<>();
        // scan directory, fill mFileNumbers and mActiveFile.
        mHistoryDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                final int b = name.lastIndexOf(FILE_SUFFIX);
                if (b <= 0) {
                    return false;
                }
                final Integer c =
                        ParseUtils.parseInt(name.substring(0, b), -1);
                if (c != -1) {
                    dedup.add(c);
                    return true;
                } else {
                    return false;
                }
            }
        });
        if (!dedup.isEmpty()) {
            mFileNumbers.addAll(dedup);
            Collections.sort(mFileNumbers);
        } else {
            // No file found, default to have file 0.
            mFileNumbers.add(0);
        }
        createActiveFile();
    }

    /**
     * Used when BatteryStatsImpl object is created from deserialization of a parcel,
     * such as Settings app or checkin file.
     * @param stats BatteryStatsImpl object.
     * @param historyBuffer the history buffer inside BatteryStatsImpl
     */
    public BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer) {
        mStats = stats;
        mHistoryDir = null;
        mHistoryBuffer = historyBuffer;
    }
    /**
     * The highest numbered history file is active file that mHistoryBuffer is backed up into.
     * If file does not exists, truncate() creates a empty file.
     */
    private void createActiveFile() {
        final AtomicFile file = getFile(mFileNumbers.get(mFileNumbers.size() - 1));
        if (DEBUG) {
            Slog.d(TAG, "activeHistoryFile:" + file.getBaseFile().getPath());
        }
        if (!file.exists()) {
            try {
                file.truncate();
            } catch (IOException e) {
                Slog.e(TAG, "Error creating history file "+ file.getBaseFile().getPath(), e);
            }
        }
        mActiveFile = file;
    }

    /**
     * Create history AtomicFile from file number.
     * @param num file number.
     * @return AtomicFile object.
     */
    private AtomicFile getFile(int num) {
        return new AtomicFile(
                new File(mHistoryDir,  num + FILE_SUFFIX));
    }

    /**
     * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
     * create next history file.
     */
    public void createNextFile() {
        if (mFileNumbers.isEmpty()) {
            Slog.wtf(TAG, "mFileNumbers should never be empty");
            return;
        }
        // The last number in mFileNumbers is the highest number. The next file number is highest
        // number plus one.
        final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1;
        mFileNumbers.add(next);
        createActiveFile();

        // if free disk space is less than 100MB, delete oldest history file.
        if (!hasFreeDiskSpace()) {
            int oldest = mFileNumbers.remove(0);
            getFile(oldest).delete();
        }

        // if there are more history files than allowed, delete oldest history files.
        // MAX_HISTORY_FILES can be updated by GService config at run time.
        while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) {
            int oldest = mFileNumbers.get(0);
            getFile(oldest).delete();
            mFileNumbers.remove(0);
        }
    }

    /**
     * Delete all existing history files. Active history file start from number 0 again.
     */
    public void resetAllFiles() {
        for (Integer i : mFileNumbers) {
            getFile(i).delete();
        }
        mFileNumbers.clear();
        mFileNumbers.add(0);
        createActiveFile();
    }

    /**
     * Start iterating history files and history buffer.
     * @return always return true.
     */
    public boolean startIteratingHistory() {
        mRecordCount = 0;
        mCurrentFileIndex = 0;
        mCurrentParcel = null;
        mCurrentParcelEnd = 0;
        mParcelIndex = 0;
        return true;
    }

    /**
     * Finish iterating history files and history buffer.
     */
    public void finishIteratingHistory() {
        // setDataPosition so mHistoryBuffer Parcel can be written.
        mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
        if (DEBUG) {
            Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
        }
    }

    /**
     * When iterating history files and history buffer, always start from the lowest numbered
     * history file, when reached the mActiveFile (highest numbered history file), do not read from
     * mActiveFile, read from history buffer instead because the buffer has more updated data.
     * @param out a history item.
     * @return The parcel that has next record. null if finished all history files and history
     *         buffer
     */
    public Parcel getNextParcel(BatteryStats.HistoryItem out) {
        if (mRecordCount == 0) {
            // reset out if it is the first record.
            out.clear();
        }
        ++mRecordCount;

        // First iterate through all records in current parcel.
        if (mCurrentParcel != null)
        {
            if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
                // There are more records in current parcel.
                return mCurrentParcel;
            } else if (mHistoryBuffer == mCurrentParcel) {
                // finished iterate through all history files and history buffer.
                return null;
            } else if (mHistoryParcels == null
                    || !mHistoryParcels.contains(mCurrentParcel)) {
                // current parcel is from history file.
                mCurrentParcel.recycle();
            }
        }

        // Try next available history file.
        // skip the last file because its data is in history buffer.
        while (mCurrentFileIndex < mFileNumbers.size() - 1) {
            mCurrentParcel = null;
            mCurrentParcelEnd = 0;
            final Parcel p = Parcel.obtain();
            AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++));
            if (readFileToParcel(p, file)) {
                int bufSize = p.readInt();
                int curPos = p.dataPosition();
                mCurrentParcelEnd = curPos + bufSize;
                mCurrentParcel = p;
                if (curPos < mCurrentParcelEnd) {
                    return mCurrentParcel;
                }
            } else {
                p.recycle();
            }
        }

        // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
        // of a parcel, such as Settings app or checkin file.
        if (mHistoryParcels != null) {
            while (mParcelIndex < mHistoryParcels.size()) {
                final Parcel p = mHistoryParcels.get(mParcelIndex++);
                if (!skipHead(p)) {
                    continue;
                }
                final int bufSize = p.readInt();
                final int curPos = p.dataPosition();
                mCurrentParcelEnd = curPos + bufSize;
                mCurrentParcel = p;
                if (curPos < mCurrentParcelEnd) {
                    return mCurrentParcel;
                }
            }
        }

        // finished iterator through history files (except the last one), now history buffer.
        if (mHistoryBuffer.dataSize() <= 0) {
            // buffer is empty.
            return null;
        }
        mHistoryBuffer.setDataPosition(0);
        mCurrentParcel = mHistoryBuffer;
        mCurrentParcelEnd = mCurrentParcel.dataSize();
        return mCurrentParcel;
    }

    /**
     * Read history file into a parcel.
     * @param out the Parcel read into.
     * @param file the File to read from.
     * @return true if success, false otherwise.
     */
    public boolean readFileToParcel(Parcel out, AtomicFile file) {
        byte[] raw = null;
        try {
            final long start = SystemClock.uptimeMillis();
            raw = file.readFully();
            if (DEBUG) {
                Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
                        + " duration ms:" + (SystemClock.uptimeMillis() - start));
            }
        } catch(Exception e) {
            Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
            return false;
        }
        out.unmarshall(raw, 0, raw.length);
        out.setDataPosition(0);
        return skipHead(out);
    }

    /**
     * Skip the header part of history parcel.
     * @param p history parcel to skip head.
     * @return true if version match, false if not.
     */
    private boolean skipHead(Parcel p) {
        p.setDataPosition(0);
        final int version = p.readInt();
        if (version != mStats.VERSION) {
            return false;
        }
        // skip historyBaseTime field.
        p.readLong();
        return true;
    }

    /**
     * Read all history files and serialize into a big Parcel. This is to send history files to
     * Settings app since Settings app can not access /data/system directory.
     * Checkin file also call this method.
     * @param out the output parcel
     */
    public void writeToParcel(Parcel out) {
        final long start = SystemClock.uptimeMillis();
        out.writeInt(mFileNumbers.size() - 1);
        for(int i = 0;  i < mFileNumbers.size() - 1; i++) {
            AtomicFile file = getFile(mFileNumbers.get(i));
            byte[] raw = new byte[0];
            try {
                raw = file.readFully();
            } catch(Exception e) {
                Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
            }
            out.writeByteArray(raw);
        }
        if (DEBUG) {
            Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
        }
    }

    /**
     * This is for Settings app, when Settings app receives big history parcel, it call
     * this method to parse it into list of parcels.
     * Checkin file also call this method.
     * @param in the input parcel.
     */
    public void readFromParcel(Parcel in) {
        final long start = SystemClock.uptimeMillis();
        mHistoryParcels = new ArrayList<>();
        final int count = in.readInt();
        for(int i = 0; i < count; i++) {
            byte[] temp = in.createByteArray();
            if (temp.length == 0) {
                continue;
            }
            Parcel p = Parcel.obtain();
            p.unmarshall(temp, 0, temp.length);
            p.setDataPosition(0);
            mHistoryParcels.add(p);
        }
        if (DEBUG) {
            Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start));
        }
    }

    /**
     * @return true if there is more than 100MB free disk space left.
     */
    private boolean hasFreeDiskSpace() {
        final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
        return stats.getAvailableBytes() > MIN_FREE_SPACE;
    }

    public List<Integer> getFilesNumbers() {
        return mFileNumbers;
    }

    public AtomicFile getActiveFile() {
        return mActiveFile;
    }

    /**
     * @return the total size of all history files and history buffer.
     */
    public int getHistoryUsedSize() {
        int ret = 0;
        for(int i = 0; i < mFileNumbers.size() - 1; i++) {
            ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
        }
        ret += mHistoryBuffer.dataSize();
        if (mHistoryParcels != null) {
            for(int i = 0; i < mHistoryParcels.size(); i++) {
                ret += mHistoryParcels.get(i).dataSize();
            }
        }
        return ret;
    }
}
