/*
 * 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.server.inputmethod;

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.os.Environment;
import android.os.FileUtils;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;

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;
import java.util.ArrayList;
import java.util.List;

/**
 * Utility class to read/write subtype.xml.
 */
final class AdditionalSubtypeUtils {
    private static final String TAG = "AdditionalSubtypeUtils";

    private static final String SYSTEM_PATH = "system";
    private static final String INPUT_METHOD_PATH = "inputmethod";
    private static final String ADDITIONAL_SUBTYPES_FILE_NAME = "subtypes.xml";
    private static final String NODE_SUBTYPES = "subtypes";
    private static final String NODE_SUBTYPE = "subtype";
    private static final String NODE_IMI = "imi";
    private static final String ATTR_ID = "id";
    private static final String ATTR_LABEL = "label";
    private static final String ATTR_ICON = "icon";
    private static final String ATTR_IME_SUBTYPE_ID = "subtypeId";
    private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale";
    private static final String ATTR_IME_SUBTYPE_LANGUAGE_TAG = "languageTag";
    private static final String ATTR_IME_SUBTYPE_MODE = "imeSubtypeMode";
    private static final String ATTR_IME_SUBTYPE_EXTRA_VALUE = "imeSubtypeExtraValue";
    private static final String ATTR_IS_AUXILIARY = "isAuxiliary";
    private static final String ATTR_IS_ASCII_CAPABLE = "isAsciiCapable";

    private AdditionalSubtypeUtils() {
    }

    /**
     * Returns a {@link File} that represents the directory at which subtype.xml will be placed.
     *
     * @param userId User ID with with subtype.xml path should be determined.
     * @return {@link File} that represents the directory.
     */
    @NonNull
    private static File getInputMethodDir(@UserIdInt int userId) {
        final File systemDir = userId == UserHandle.USER_SYSTEM
                ? new File(Environment.getDataDirectory(), SYSTEM_PATH)
                : Environment.getUserSystemDirectory(userId);
        return new File(systemDir, INPUT_METHOD_PATH);
    }

    /**
     * Returns an {@link AtomicFile} to read/write additional subtype for the given user id.
     *
     * @param inputMethodDir Directory at which subtype.xml will be placed
     * @return {@link AtomicFile} to be used to read/write additional subtype
     */
    @NonNull
    private static AtomicFile getAdditionalSubtypeFile(File inputMethodDir) {
        final File subtypeFile = new File(inputMethodDir, ADDITIONAL_SUBTYPES_FILE_NAME);
        return new AtomicFile(subtypeFile, "input-subtypes");
    }

    /**
     * Write additional subtypes into "subtype.xml".
     *
     * <p>This method does not confer any data/file locking semantics. Caller must make sure that
     * multiple threads are not calling this method at the same time for the same {@code userId}.
     * </p>
     *
     * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty
     *                    map deletes the file.
     * @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}.
     * @param userId The user ID to be associated with.
     */
    static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
             ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
        final File inputMethodDir = getInputMethodDir(userId);

        if (allSubtypes.isEmpty()) {
            if (!inputMethodDir.exists()) {
                // Even the parent directory doesn't exist.  There is nothing to clean up.
                return;
            }
            final AtomicFile subtypesFile = getAdditionalSubtypeFile(inputMethodDir);
            if (subtypesFile.exists()) {
                subtypesFile.delete();
            }
            if (FileUtils.listFilesOrEmpty(inputMethodDir).length == 0) {
                if (!inputMethodDir.delete()) {
                    Slog.e(TAG, "Failed to delete the empty parent directory " + inputMethodDir);
                }
            }
            return;
        }

        if (!inputMethodDir.exists() && !inputMethodDir.mkdirs()) {
            Slog.e(TAG, "Failed to create a parent directory " + inputMethodDir);
            return;
        }

        // Safety net for the case that this function is called before methodMap is set.
        final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
        FileOutputStream fos = null;
        final AtomicFile subtypesFile = getAdditionalSubtypeFile(inputMethodDir);
        try {
            fos = subtypesFile.startWrite();
            final TypedXmlSerializer out = Xml.resolveSerializer(fos);
            out.startDocument(null, true);
            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            out.startTag(null, NODE_SUBTYPES);
            for (String imiId : allSubtypes.keySet()) {
                if (isSetMethodMap && !methodMap.containsKey(imiId)) {
                    Slog.w(TAG, "IME uninstalled or not valid.: " + imiId);
                    continue;
                }
                out.startTag(null, NODE_IMI);
                out.attribute(null, ATTR_ID, imiId);
                final List<InputMethodSubtype> subtypesList = allSubtypes.get(imiId);
                final int numSubtypes = subtypesList.size();
                for (int i = 0; i < numSubtypes; ++i) {
                    final InputMethodSubtype subtype = subtypesList.get(i);
                    out.startTag(null, NODE_SUBTYPE);
                    if (subtype.hasSubtypeId()) {
                        out.attributeInt(null, ATTR_IME_SUBTYPE_ID, subtype.getSubtypeId());
                    }
                    out.attributeInt(null, ATTR_ICON, subtype.getIconResId());
                    out.attributeInt(null, ATTR_LABEL, subtype.getNameResId());
                    out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale());
                    out.attribute(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG,
                            subtype.getLanguageTag());
                    out.attribute(null, ATTR_IME_SUBTYPE_MODE, subtype.getMode());
                    out.attribute(null, ATTR_IME_SUBTYPE_EXTRA_VALUE, subtype.getExtraValue());
                    out.attributeInt(null, ATTR_IS_AUXILIARY, subtype.isAuxiliary() ? 1 : 0);
                    out.attributeInt(null, ATTR_IS_ASCII_CAPABLE, subtype.isAsciiCapable() ? 1 : 0);
                    out.endTag(null, NODE_SUBTYPE);
                }
                out.endTag(null, NODE_IMI);
            }
            out.endTag(null, NODE_SUBTYPES);
            out.endDocument();
            subtypesFile.finishWrite(fos);
        } catch (java.io.IOException e) {
            Slog.w(TAG, "Error writing subtypes", e);
            if (fos != null) {
                subtypesFile.failWrite(fos);
            }
        }
    }

    /**
     * Read additional subtypes from "subtype.xml".
     *
     * <p>This method does not confer any data/file locking semantics. Caller must make sure that
     * multiple threads are not calling this method at the same time for the same {@code userId}.
     * </p>
     *
     * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter
     *                    will be used to return the result.
     * @param userId The user ID to be associated with.
     */
    static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
            @UserIdInt int userId) {
        allSubtypes.clear();

        final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId));
        if (!subtypesFile.exists()) {
            // Not having the file means there is no additional subtype.
            return;
        }
        try (FileInputStream fis = subtypesFile.openRead()) {
            final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
            int type = parser.getEventType();
            // Skip parsing until START_TAG
            while (true) {
                type = parser.next();
                if (type == XmlPullParser.START_TAG || type == XmlPullParser.END_DOCUMENT) {
                    break;
                }
            }
            String firstNodeName = parser.getName();
            if (!NODE_SUBTYPES.equals(firstNodeName)) {
                throw new XmlPullParserException("Xml doesn't start with subtypes");
            }
            final int depth = parser.getDepth();
            String currentImiId = null;
            ArrayList<InputMethodSubtype> tempSubtypesArray = null;
            while (((type = parser.next()) != XmlPullParser.END_TAG
                    || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                if (type != XmlPullParser.START_TAG) {
                    continue;
                }
                final String nodeName = parser.getName();
                if (NODE_IMI.equals(nodeName)) {
                    currentImiId = parser.getAttributeValue(null, ATTR_ID);
                    if (TextUtils.isEmpty(currentImiId)) {
                        Slog.w(TAG, "Invalid imi id found in subtypes.xml");
                        continue;
                    }
                    tempSubtypesArray = new ArrayList<>();
                    allSubtypes.put(currentImiId, tempSubtypesArray);
                } else if (NODE_SUBTYPE.equals(nodeName)) {
                    if (TextUtils.isEmpty(currentImiId) || tempSubtypesArray == null) {
                        Slog.w(TAG, "IME uninstalled or not valid.: " + currentImiId);
                        continue;
                    }
                    final int icon = parser.getAttributeInt(null, ATTR_ICON);
                    final int label = parser.getAttributeInt(null, ATTR_LABEL);
                    final String imeSubtypeLocale =
                            parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE);
                    final String languageTag =
                            parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG);
                    final String imeSubtypeMode =
                            parser.getAttributeValue(null, ATTR_IME_SUBTYPE_MODE);
                    final String imeSubtypeExtraValue =
                            parser.getAttributeValue(null, ATTR_IME_SUBTYPE_EXTRA_VALUE);
                    final boolean isAuxiliary = "1".equals(String.valueOf(
                            parser.getAttributeValue(null, ATTR_IS_AUXILIARY)));
                    final boolean isAsciiCapable = "1".equals(String.valueOf(
                            parser.getAttributeValue(null, ATTR_IS_ASCII_CAPABLE)));
                    final InputMethodSubtype.InputMethodSubtypeBuilder
                            builder = new InputMethodSubtype.InputMethodSubtypeBuilder()
                            .setSubtypeNameResId(label)
                            .setSubtypeIconResId(icon)
                            .setSubtypeLocale(imeSubtypeLocale)
                            .setLanguageTag(languageTag)
                            .setSubtypeMode(imeSubtypeMode)
                            .setSubtypeExtraValue(imeSubtypeExtraValue)
                            .setIsAuxiliary(isAuxiliary)
                            .setIsAsciiCapable(isAsciiCapable);
                    final int subtypeId = parser.getAttributeInt(null, ATTR_IME_SUBTYPE_ID,
                            InputMethodSubtype.SUBTYPE_ID_NONE);
                    if (subtypeId != InputMethodSubtype.SUBTYPE_ID_NONE) {
                        builder.setSubtypeId(subtypeId);
                    }
                    tempSubtypesArray.add(builder.build());
                }
            }
        } catch (XmlPullParserException | IOException | NumberFormatException e) {
            Slog.w(TAG, "Error reading subtypes", e);
        }
    }
}
