/*
 * Copyright (C) 2021 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.server.app;

import android.app.GameManager;
import android.os.FileUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.app.GameManagerService.GamePackageConfiguration;
import com.android.server.app.GameManagerService.GamePackageConfiguration.GameModeConfiguration;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Persists all GameService related settings.
 *
 * @hide
 */
public class GameManagerSettings {
    public static final String TAG = "GameManagerService_GameManagerSettings";
    // The XML file follows the below format:
    // <?xml>
    // <packages>
    //     <package name="" gameMode="">
    //       <gameModeConfig gameMode="" fps="" scaling="" useAngle="" loadingBoost="">
    //       </gameModeConfig>
    //       ...
    //     </package>
    //     ...
    // </packages>
    private static final String GAME_SERVICE_FILE_NAME = "game-manager-service.xml";

    private static final String TAG_PACKAGE = "package";
    private static final String TAG_PACKAGES = "packages";
    private static final String TAG_GAME_MODE_CONFIG = "gameModeConfig";

    private static final String ATTR_NAME = "name";
    private static final String ATTR_GAME_MODE = "gameMode";
    private static final String ATTR_SCALING = "scaling";
    private static final String ATTR_FPS = "fps";
    private static final String ATTR_USE_ANGLE = "useAngle";
    private static final String ATTR_LOADING_BOOST_DURATION = "loadingBoost";

    private final File mSystemDir;
    @VisibleForTesting
    final AtomicFile mSettingsFile;

    // PackageName -> GameMode
    private final ArrayMap<String, Integer> mGameModes = new ArrayMap<>();
    // PackageName -> GamePackageConfiguration
    private final ArrayMap<String, GamePackageConfiguration> mConfigOverrides = new ArrayMap<>();

    GameManagerSettings(File dataDir) {
        mSystemDir = new File(dataDir, "system");
        mSystemDir.mkdirs();
        FileUtils.setPermissions(mSystemDir.toString(),
                FileUtils.S_IRWXU | FileUtils.S_IRWXG
                        | FileUtils.S_IROTH | FileUtils.S_IXOTH,
                -1, -1);
        mSettingsFile = new AtomicFile(new File(mSystemDir, GAME_SERVICE_FILE_NAME));
    }

    /**
     * Returns the game mode of a given package.
     * This operation must be synced with an external lock.
     */
    int getGameModeLocked(String packageName) {
        if (mGameModes.containsKey(packageName)) {
            final int gameMode = mGameModes.get(packageName);
            if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
                // force replace cached UNSUPPORTED mode with STANDARD starting in U
                return GameManager.GAME_MODE_STANDARD;
            }
            return gameMode;
        }
        return GameManager.GAME_MODE_STANDARD;
    }

    /**
     * Sets the game mode of a given package.
     * This operation must be synced with an external lock.
     */
    void setGameModeLocked(String packageName, int gameMode) {
        mGameModes.put(packageName, gameMode);
    }

    /**
     * Removes all game settings of a given package.
     * This operation must be synced with an external lock.
     */
    void removeGameLocked(String packageName) {
        mGameModes.remove(packageName);
        mConfigOverrides.remove(packageName);
    }

    /**
     * Returns the game config override of a given package or null if absent.
     * This operation must be synced with an external lock.
     */
    GamePackageConfiguration getConfigOverrideLocked(String packageName) {
        return mConfigOverrides.get(packageName);
    }

    /**
     * Sets the game config override of a given package.
     * This operation must be synced with an external lock.
     */
    void setConfigOverrideLocked(String packageName, GamePackageConfiguration configOverride) {
        mConfigOverrides.put(packageName, configOverride);
    }

    /**
     * Removes the game mode config override of a given package.
     * This operation must be synced with an external lock.
     */
    void removeConfigOverrideLocked(String packageName) {
        mConfigOverrides.remove(packageName);
    }

    /**
     * Writes all current game service settings into disk.
     * This operation must be synced with an external lock.
     */
    void writePersistentDataLocked() {
        FileOutputStream fstr = null;
        try {
            fstr = mSettingsFile.startWrite();

            final TypedXmlSerializer serializer = Xml.resolveSerializer(fstr);
            serializer.startDocument(null, true);
            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);

            serializer.startTag(null, TAG_PACKAGES);
            final ArraySet<String> packageNames = new ArraySet<>(mGameModes.keySet());
            packageNames.addAll(mConfigOverrides.keySet());
            for (String packageName : packageNames) {
                serializer.startTag(null, TAG_PACKAGE);
                serializer.attribute(null, ATTR_NAME, packageName);
                if (mGameModes.containsKey(packageName)) {
                    serializer.attributeInt(null, ATTR_GAME_MODE, mGameModes.get(packageName));
                }
                writeGameModeConfigTags(serializer, mConfigOverrides.get(packageName));
                serializer.endTag(null, TAG_PACKAGE);
            }
            serializer.endTag(null, TAG_PACKAGES);

            serializer.endDocument();

            mSettingsFile.finishWrite(fstr);

            FileUtils.setPermissions(mSettingsFile.toString(),
                    FileUtils.S_IRUSR | FileUtils.S_IWUSR
                            | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
                    -1, -1);
            return;
        } catch (java.io.IOException e) {
            mSettingsFile.failWrite(fstr);
            Slog.wtf(TAG, "Unable to write game manager service settings, "
                    + "current changes will be lost at reboot", e);
        }
    }

    private void writeGameModeConfigTags(TypedXmlSerializer serializer,
            GamePackageConfiguration config) throws IOException {
        if (config == null) {
            return;
        }
        final int[] gameModes = config.getAvailableGameModes();
        for (final int mode : gameModes) {
            final GameModeConfiguration modeConfig = config.getGameModeConfiguration(mode);
            if (modeConfig != null) {
                serializer.startTag(null, TAG_GAME_MODE_CONFIG);
                serializer.attributeInt(null, ATTR_GAME_MODE, mode);
                serializer.attributeBoolean(null, ATTR_USE_ANGLE, modeConfig.getUseAngle());
                serializer.attribute(null, ATTR_FPS, modeConfig.getFpsStr());
                serializer.attributeFloat(null, ATTR_SCALING, modeConfig.getScaling());
                serializer.attributeInt(null, ATTR_LOADING_BOOST_DURATION,
                        modeConfig.getLoadingBoostDuration());
                serializer.endTag(null, TAG_GAME_MODE_CONFIG);
            }
        }
    }

    /**
     * Reads game service settings from the disk.
     * This operation must be synced with an external lock.
     */
    boolean readPersistentDataLocked() {
        mGameModes.clear();

        if (!mSettingsFile.exists()) {
            Slog.v(TAG, "Settings file doesn't exist, skip reading");
            return false;
        }

        try (FileInputStream str = mSettingsFile.openRead()) {
            final TypedXmlPullParser parser = Xml.resolvePullParser(str);
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Do nothing
            }
            if (type != XmlPullParser.START_TAG) {
                Slog.wtf(TAG, "No start tag found in game manager settings");
                return false;
            }

            int outerDepth = parser.getDepth();
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }

                String tagName = parser.getName();
                if (type == XmlPullParser.START_TAG && TAG_PACKAGE.equals(tagName)) {
                    readPackage(parser);
                } else {
                    XmlUtils.skipCurrentTag(parser);
                    Slog.w(TAG, "Unknown element under packages tag: " + tagName + " with type: "
                            + type);
                }
            }
        } catch (XmlPullParserException | java.io.IOException e) {
            Slog.wtf(TAG, "Error reading game manager settings", e);
            return false;
        }
        return true;
    }

    // this must be called on tag of type START_TAG.
    private void readPackage(TypedXmlPullParser parser) throws XmlPullParserException,
            IOException {
        final String name = parser.getAttributeValue(null, ATTR_NAME);
        if (name == null) {
            Slog.wtf(TAG, "No package name found in package tag");
            XmlUtils.skipCurrentTag(parser);
            return;
        }
        try {
            final int gameMode = parser.getAttributeInt(null, ATTR_GAME_MODE);
            mGameModes.put(name, gameMode);
        } catch (XmlPullParserException e) {
            Slog.v(TAG, "No game mode selected by user for package" + name);
        }
        final int packageTagDepth = parser.getDepth();
        int type;
        final GamePackageConfiguration config = new GamePackageConfiguration(name);
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG
                || parser.getDepth() > packageTagDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }
            final String tagName = parser.getName();
            if (type == XmlPullParser.START_TAG && TAG_GAME_MODE_CONFIG.equals(tagName)) {
                readGameModeConfig(parser, config);
            } else {
                XmlUtils.skipCurrentTag(parser);
                Slog.w(TAG, "Unknown element under package tag: " + tagName + " with type: "
                        + type);
            }
        }
        if (config.hasActiveGameModeConfig()) {
            mConfigOverrides.put(name, config);
        }
    }

    // this must be called on tag of type START_TAG.
    private void readGameModeConfig(TypedXmlPullParser parser, GamePackageConfiguration config) {
        final int gameMode;
        try {
            gameMode = parser.getAttributeInt(null, ATTR_GAME_MODE);
        } catch (XmlPullParserException e) {
            Slog.wtf(TAG, "Invalid game mode value in config tag: " + parser.getAttributeValue(null,
                    ATTR_GAME_MODE), e);
            return;
        }

        final GameModeConfiguration modeConfig = config.getOrAddDefaultGameModeConfiguration(
                gameMode);
        try {
            final float scaling = parser.getAttributeFloat(null, ATTR_SCALING);
            modeConfig.setScaling(scaling);
        } catch (XmlPullParserException e) {
            final String rawScaling = parser.getAttributeValue(null, ATTR_SCALING);
            if (rawScaling != null) {
                Slog.wtf(TAG, "Invalid scaling value in config tag: " + rawScaling, e);
            }
        }

        final String fps = parser.getAttributeValue(null, ATTR_FPS);
        modeConfig.setFpsStr(fps != null ? fps : GameModeConfiguration.DEFAULT_FPS);

        try {
            final boolean useAngle = parser.getAttributeBoolean(null, ATTR_USE_ANGLE);
            modeConfig.setUseAngle(useAngle);
        } catch (XmlPullParserException e) {
            final String rawUseAngle = parser.getAttributeValue(null, ATTR_USE_ANGLE);
            if (rawUseAngle != null) {
                Slog.wtf(TAG, "Invalid useAngle value in config tag: " + rawUseAngle, e);
            }
        }
        try {
            final int loadingBoostDuration = parser.getAttributeInt(null,
                    ATTR_LOADING_BOOST_DURATION);
            modeConfig.setLoadingBoostDuration(loadingBoostDuration);
        } catch (XmlPullParserException e) {
            final String rawLoadingBoost = parser.getAttributeValue(null,
                    ATTR_LOADING_BOOST_DURATION);
            if (rawLoadingBoost != null) {
                Slog.wtf(TAG, "Invalid loading boost in config tag: " + rawLoadingBoost, e);
            }
        }
    }
}
