/*
 * Copyright (C) 2016 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.printservice.recommendation.plugin.mdnsFilter;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.util.ArrayMap;
import com.android.internal.annotations.Immutable;
import com.android.internal.util.Preconditions;
import com.android.printservice.recommendation.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Vendor configuration as read from {@link R.xml#vendorconfigs vendorconfigs.xml}. Configuration
 * can be read via {@link #getConfig(Context, String)}.
 */
@Immutable
public class VendorConfig {
    /** Lock for {@link #sConfigs} */
    private static final Object sLock = new Object();

    /** Strings used as XML tags */
    private static final String VENDORS_TAG = "vendors";
    private static final String VENDOR_TAG = "vendor";
    private static final String NAME_TAG = "name";
    private static final String PACKAGE_TAG = "package";
    private static final String MDNSNAMES_TAG = "mdns-names";
    private static final String MDNSNAME_TAG = "mdns-name";

    /** Map from vendor name to config. Initialized on first {@link #getConfig use}. */
    private static @Nullable ArrayMap<String, VendorConfig> sConfigs;

    /** Localized vendor name */
    public final @NonNull String name;

    /** Package name containing the print service for this vendor */
    public final @NonNull String packageName;

    /** mDNS names used by this vendor */
    public final @NonNull List<String> mDNSNames;

    /**
     * Create an immutable configuration.
     */
    private VendorConfig(@NonNull String name, @NonNull String packageName,
            @NonNull List<String> mDNSNames) {
        this.name = Preconditions.checkStringNotEmpty(name);
        this.packageName = Preconditions.checkStringNotEmpty(packageName);
        this.mDNSNames = Preconditions.checkCollectionElementsNotNull(mDNSNames, "mDNSName");
    }

    /**
     * Get the configuration for a vendor.
     *
     * @param context Calling context
     * @param name    The name of the config to read
     *
     * @return the config for the vendor or null if not found
     *
     * @throws IOException
     * @throws XmlPullParserException
     */
    public static @Nullable VendorConfig getConfig(@NonNull Context context, @NonNull String name)
            throws IOException, XmlPullParserException {
        synchronized (sLock) {
            if (sConfigs == null) {
                sConfigs = readVendorConfigs(context);
            }

            return sConfigs.get(name);
        }
    }

    /**
     * Get all known vendor configurations.
     *
     * @param context Calling context
     *
     * @return The known configurations
     *
     * @throws IOException
     * @throws XmlPullParserException
     */
    public static @NonNull Collection<VendorConfig> getAllConfigs(@NonNull Context context)
            throws IOException, XmlPullParserException {
        synchronized (sLock) {
            if (sConfigs == null) {
                sConfigs = readVendorConfigs(context);
            }

            return sConfigs.values();
        }
    }

    /**
     * Read the text from a XML tag.
     *
     * @param parser XML parser to read from
     *
     * @return The text or "" if no text was found
     *
     * @throws IOException
     * @throws XmlPullParserException
     */
    private static @NonNull String readText(XmlPullParser parser)
            throws IOException, XmlPullParserException {
        String result = "";

        if (parser.next() == XmlPullParser.TEXT) {
            result = parser.getText();
            parser.nextTag();
        }

        return result;
    }

    /**
     * Read a tag with a text content from the parser.
     *
     * @param parser  XML parser to read from
     * @param tagName The name of the tag to read
     *
     * @return The text content of the tag
     *
     * @throws IOException
     * @throws XmlPullParserException
     */
    private static @NonNull String readSimpleTag(@NonNull Context context,
            @NonNull XmlPullParser parser, @NonNull String tagName, boolean resolveReferences)
            throws IOException, XmlPullParserException {
        parser.require(XmlPullParser.START_TAG, null, tagName);
        String text = readText(parser);
        parser.require(XmlPullParser.END_TAG, null, tagName);

        if (resolveReferences && text.startsWith("@")) {
            return context.getResources().getString(
                    context.getResources().getIdentifier(text, null, context.getPackageName()));
        } else {
            return text;
        }
    }

    /**
     * Read content of a list of tags.
     *
     * @param parser     XML parser to read from
     * @param tagName    The name of the list tag
     * @param subTagName The name of the list-element tags
     * @param tagReader  The {@link TagReader reader} to use to read the tag content
     * @param <T>        The type of the parsed tag content
     *
     * @return A list of {@link T}
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static @NonNull <T> ArrayList<T> readTagList(@NonNull XmlPullParser parser,
            @NonNull String tagName, @NonNull String subTagName, @NonNull TagReader<T> tagReader)
            throws XmlPullParserException, IOException {
        ArrayList<T> entries = new ArrayList<>();

        parser.require(XmlPullParser.START_TAG, null, tagName);
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(subTagName)) {
                entries.add(tagReader.readTag(parser, subTagName));
            } else {
                throw new XmlPullParserException(
                        "Unexpected subtag of " + tagName + ": " + parser.getName());
            }
        }

        return entries;
    }

    /**
     * Read the vendor configuration file.
     *
     * @param context The content issuing the read
     *
     * @return An map pointing from vendor name to config
     *
     * @throws IOException
     * @throws XmlPullParserException
     */
    private static @NonNull ArrayMap<String, VendorConfig> readVendorConfigs(
            @NonNull final Context context) throws IOException, XmlPullParserException {
        try (XmlResourceParser parser = context.getResources().getXml(R.xml.vendorconfigs)) {
            // Skip header
            int parsingEvent;
            do {
                parsingEvent = parser.next();
            } while (parsingEvent != XmlResourceParser.START_TAG);

            ArrayList<VendorConfig> configs = readTagList(parser, VENDORS_TAG, VENDOR_TAG,
                    new TagReader<VendorConfig>() {
                        public VendorConfig readTag(XmlPullParser parser, String tagName)
                                throws XmlPullParserException, IOException {
                            return readVendorConfig(context, parser, tagName);
                        }
                    });

            ArrayMap<String, VendorConfig> configMap = new ArrayMap<>(configs.size());
            final int numConfigs = configs.size();
            for (int i = 0; i < numConfigs; i++) {
                VendorConfig config = configs.get(i);

                configMap.put(config.name, config);
            }

            return configMap;
        }
    }

    /**
     * Read a single vendor configuration.
     *
     * @param parser  XML parser to read from
     * @param tagName The vendor tag
     * @param context Calling context
     *
     * @return A config
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static VendorConfig readVendorConfig(@NonNull final Context context,
            @NonNull XmlPullParser parser, @NonNull String tagName) throws XmlPullParserException,
            IOException {
        parser.require(XmlPullParser.START_TAG, null, tagName);

        String name = null;
        String packageName = null;
        List<String> mDNSNames = null;

        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }

            String subTagName = parser.getName();

            switch (subTagName) {
                case NAME_TAG:
                    name = readSimpleTag(context, parser, NAME_TAG, false);
                    break;
                case PACKAGE_TAG:
                    packageName = readSimpleTag(context, parser, PACKAGE_TAG, true);
                    break;
                case MDNSNAMES_TAG:
                    mDNSNames = readTagList(parser, MDNSNAMES_TAG, MDNSNAME_TAG,
                            new TagReader<String>() {
                                public String readTag(XmlPullParser parser, String tagName)
                                        throws XmlPullParserException, IOException {
                                    return readSimpleTag(context, parser, tagName, true);
                                }
                            }
                    );
                    break;
                default:
                    throw new XmlPullParserException("Unexpected subtag of " + tagName + ": "
                            + subTagName);

            }
        }

        if (name == null) {
            throw new XmlPullParserException("name is required");
        }

        if (packageName == null) {
            throw new XmlPullParserException("package is required");
        }

        if (mDNSNames == null) {
            mDNSNames = Collections.emptyList();
        }

        // A vendor config should be immutable
        mDNSNames = Collections.unmodifiableList(mDNSNames);

        return new VendorConfig(name, packageName, mDNSNames);
    }

    @Override
    public String toString() {
        return name + " -> " + packageName + ", " + mDNSNames;
    }

    /**
     * Used a a "function pointer" when reading a tag in {@link #readTagList(XmlPullParser, String,
     * String, TagReader)}.
     *
     * @param <T> The type of content to read
     */
    private interface TagReader<T> {
        T readTag(XmlPullParser parser, String tagName) throws XmlPullParserException, IOException;
    }
}
