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

import android.annotation.Nullable;
import android.net.MacAddress;
import android.util.Log;

import com.android.server.wifi.WifiNetworkFactory.AccessPoint;
import com.android.server.wifi.util.WifiConfigStoreEncryptionUtil;
import com.android.server.wifi.util.XmlUtil;
import com.android.server.wifi.util.XmlUtil.WifiConfigurationXmlUtil;

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

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * This class performs serialization and parsing of XML data block that contain the list of WiFi
 * network request approvals.
 */
public class NetworkRequestStoreData implements WifiConfigStore.StoreData {
    private static final String TAG = "NetworkRequestStoreData";

    private static final String XML_TAG_SECTION_HEADER_NETWORK_REQUEST_MAP =
            "NetworkRequestMap";
    private static final String XML_TAG_SECTION_HEADER_APPROVED_ACCESS_POINTS_PER_APP =
            "ApprovedAccessPointsPerApp";
    private static final String XML_TAG_REQUESTOR_PACKAGE_NAME = "RequestorPackageName";
    private static final String XML_TAG_SECTION_HEADER_ACCESS_POINT = "AccessPoint";
    private static final String XML_TAG_ACCESS_POINT_SSID = WifiConfigurationXmlUtil.XML_TAG_SSID;
    private static final String XML_TAG_ACCESS_POINT_BSSID = WifiConfigurationXmlUtil.XML_TAG_BSSID;
    private static final String XML_TAG_ACCESS_POINT_NETWORK_TYPE = "NetworkType";

    /**
     * Interface define the data source for the network requests store data.
     */
    public interface DataSource {
        /**
         * Retrieve the approved access points from the data source to serialize them to disk.
         *
         * @return Map of package name to a set of {@link AccessPoint}
         */
        Map<String, Set<AccessPoint>> toSerialize();

        /**
         * Set the approved access points in the data source after serializing them from disk.
         *
         * @param approvedAccessPoints Map of package name to {@link AccessPoint}
         */
        void fromDeserialized(Map<String, Set<AccessPoint>> approvedAccessPoints);

        /**
         * Clear internal data structure in preparation for user switch or initial store read.
         */
        void reset();

        /**
         * Indicates whether there is new data to serialize.
         */
        boolean hasNewDataToSerialize();
    }

    private final DataSource mDataSource;

    public NetworkRequestStoreData(DataSource dataSource) {
        mDataSource = dataSource;
    }

    @Override
    public void serializeData(XmlSerializer out,
            @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)
            throws XmlPullParserException, IOException {
        serializeApprovedAccessPointsMap(out, mDataSource.toSerialize());
    }

    @Override
    public void deserializeData(XmlPullParser in, int outerTagDepth,
            @WifiConfigStore.Version int version,
            @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)
            throws XmlPullParserException, IOException {
        // Ignore empty reads.
        if (in == null) {
            return;
        }
        mDataSource.fromDeserialized(parseApprovedAccessPointsMap(in, outerTagDepth));
    }

    @Override
    public void resetData() {
        mDataSource.reset();
    }

    @Override
    public boolean hasNewDataToSerialize() {
        return mDataSource.hasNewDataToSerialize();
    }

    @Override
    public String getName() {
        return XML_TAG_SECTION_HEADER_NETWORK_REQUEST_MAP;
    }

    @Override
    public @WifiConfigStore.StoreFileId int getStoreFileId() {
        return WifiConfigStore.STORE_FILE_USER_GENERAL;
    }

    /**
     * Serialize the map of package name to approved access points to an output stream in XML
     * format.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private void serializeApprovedAccessPointsMap(
            XmlSerializer out, final Map<String, Set<AccessPoint>> approvedAccessPointsMap)
            throws XmlPullParserException, IOException {
        if (approvedAccessPointsMap == null) {
            return;
        }
        for (Entry<String, Set<AccessPoint>> entry : approvedAccessPointsMap.entrySet()) {
            String packageName = entry.getKey();
            XmlUtil.writeNextSectionStart(out,
                    XML_TAG_SECTION_HEADER_APPROVED_ACCESS_POINTS_PER_APP);
            XmlUtil.writeNextValue(out, XML_TAG_REQUESTOR_PACKAGE_NAME, packageName);
            serializeApprovedAccessPoints(out, entry.getValue());
            XmlUtil.writeNextSectionEnd(out, XML_TAG_SECTION_HEADER_APPROVED_ACCESS_POINTS_PER_APP);
        }
    }

    /**
     * Serialize the set of approved access points to an output stream in XML format.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private void serializeApprovedAccessPoints(
            XmlSerializer out, final Set<AccessPoint> approvedAccessPoints)
            throws XmlPullParserException, IOException {
        for (AccessPoint approvedAccessPoint : approvedAccessPoints) {
            serializeApprovedAccessPoint(out, approvedAccessPoint);
        }
    }

    /**
     * Serialize a {@link AccessPoint} to an output stream in XML format.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private void serializeApprovedAccessPoint(XmlSerializer out,
                                              AccessPoint approvedAccessPoint)
            throws XmlPullParserException, IOException {
        XmlUtil.writeNextSectionStart(out, XML_TAG_SECTION_HEADER_ACCESS_POINT);
        XmlUtil.writeNextValue(out, XML_TAG_ACCESS_POINT_SSID, approvedAccessPoint.ssid);
        XmlUtil.writeNextValue(out, XML_TAG_ACCESS_POINT_BSSID,
                approvedAccessPoint.bssid.toString());
        XmlUtil.writeNextValue(out, XML_TAG_ACCESS_POINT_NETWORK_TYPE,
                approvedAccessPoint.networkType);
        XmlUtil.writeNextSectionEnd(out, XML_TAG_SECTION_HEADER_ACCESS_POINT);
    }

    /**
     * Parse a map of package name to approved access point from an input stream in XML format.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private Map<String, Set<AccessPoint>> parseApprovedAccessPointsMap(XmlPullParser in,
                                                                       int outerTagDepth)
            throws XmlPullParserException, IOException {
        Map<String, Set<AccessPoint>> approvedAccessPointsMap = new HashMap<>();
        while (XmlUtil.gotoNextSectionWithNameOrEnd(
                in, XML_TAG_SECTION_HEADER_APPROVED_ACCESS_POINTS_PER_APP, outerTagDepth)) {
            // Try/catch only runtime exceptions (like illegal args), any XML/IO exceptions are
            // fatal and should abort the entire loading process.
            try {
                String packageName =
                        (String) XmlUtil.readNextValueWithName(in, XML_TAG_REQUESTOR_PACKAGE_NAME);
                Set<AccessPoint> approvedAccessPoints =
                        parseApprovedAccessPoints(in, outerTagDepth + 1);
                approvedAccessPointsMap.put(packageName, approvedAccessPoints);
            } catch (RuntimeException e) {
                // Failed to parse this network, skip it.
                Log.e(TAG, "Failed to parse network suggestion. Skipping...", e);
            }
        }
        return approvedAccessPointsMap;
    }

    /**
     * Parse a set of approved access points from an input stream in XML format.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private Set<AccessPoint> parseApprovedAccessPoints(XmlPullParser in, int outerTagDepth)
            throws XmlPullParserException, IOException {
        Set<AccessPoint> approvedAccessPoints = new LinkedHashSet<>();
        while (XmlUtil.gotoNextSectionWithNameOrEnd(
                in, XML_TAG_SECTION_HEADER_ACCESS_POINT, outerTagDepth)) {
            // Try/catch only runtime exceptions (like illegal args), any XML/IO exceptions are
            // fatal and should abort the entire loading process.
            try {
                AccessPoint approvedAccessPoint =
                        parseApprovedAccessPoint(in, outerTagDepth + 1);
                approvedAccessPoints.add(approvedAccessPoint);
            } catch (RuntimeException e) {
                // Failed to parse this network, skip it.
                Log.e(TAG, "Failed to parse network suggestion. Skipping...", e);
            }
        }
        return approvedAccessPoints;
    }

    /**
     * Parse a {@link AccessPoint} from an input stream in XML format.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private AccessPoint parseApprovedAccessPoint(XmlPullParser in, int outerTagDepth)
            throws XmlPullParserException, IOException {
        String ssid = null;
        MacAddress bssid = null;
        int networkType = -1;

        // Loop through and parse out all the elements from the stream within this section.
        while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
            String[] valueName = new String[1];
            Object value = XmlUtil.readCurrentValue(in, valueName);
            if (valueName[0] == null) {
                throw new XmlPullParserException("Missing value name");
            }
            switch (valueName[0]) {
                case XML_TAG_ACCESS_POINT_SSID:
                    ssid = (String) value;
                    break;
                case XML_TAG_ACCESS_POINT_BSSID:
                    bssid = MacAddress.fromString((String) value);
                    break;
                case XML_TAG_ACCESS_POINT_NETWORK_TYPE:
                    networkType = (int) value;
                    break;
                default:
                    Log.w(TAG, "Ignoring unknown value name found: " + valueName[0]);
                    break;
            }
        }
        if (ssid == null) {
            throw new XmlPullParserException("XML parsing of ssid failed");
        }
        if (bssid == null) {
            throw new XmlPullParserException("XML parsing of bssid failed");
        }
        if (networkType == -1) {
            throw new XmlPullParserException("XML parsing of network type failed");
        }
        return new AccessPoint(ssid, bssid, networkType);
    }
}

