/*
 * Copyright (C) 2017 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.hotspot2.anqp;

import android.net.Uri;
import android.text.TextUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.ByteBufferReader;

import java.net.ProtocolException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

/**
 * The OSU Provider subfield in the OSU Providers List ANQP Element,
 * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
 * section 4.8.1
 *
 * Format:
 *
 * | Length | Friendly Name Length | Friendly Name #1 | ... | Friendly Name #n |
 *     2               2                variable                  variable
 * | Server URI length | Server URI | Method List Length | Method List |
 *          1             variable             1             variable
 * | Icon Available Length | Icon Available | NAI Length | NAI | Description Length |
 *            2                variable            1     variable      2
 * | Description #1 | ... | Description #n |
 *      variable               variable
 *
 * | Operator Name Duple #N (optional) |
 *             variable
 */
public class OsuProviderInfo {
    /**
     * The raw payload should minimum include the following fields:
     * - Friendly Name Length (2)
     * - Server URI Length (1)
     * - Method List Length (1)
     * - Icon Available Length (2)
     * - NAI Length (1)
     * - Description Length (2)
     */
    @VisibleForTesting
    public static final int MINIMUM_LENGTH = 9;

    /**
     * Maximum octets for a I18N string.
     */
    private static final int MAXIMUM_I18N_STRING_LENGTH = 252;

    private final Map<String, String> mFriendlyNames;
    private final Uri mServerUri;
    private final List<Integer> mMethodList;
    private final List<IconInfo> mIconInfoList;
    private final String mNetworkAccessIdentifier;
    private final List<I18Name> mServiceDescriptions;

    @VisibleForTesting
    public OsuProviderInfo(List<I18Name> friendlyNames, Uri serverUri, List<Integer> methodList,
            List<IconInfo> iconInfoList, String nai, List<I18Name> serviceDescriptions) {
        mFriendlyNames = new HashMap<>();
        if (friendlyNames != null) {
            friendlyNames.forEach(
                    e -> mFriendlyNames.put(e.getLocale().getLanguage(), e.getText()));
        }
        mServerUri = serverUri;
        mMethodList = methodList;
        mIconInfoList = iconInfoList;
        mNetworkAccessIdentifier = nai;
        mServiceDescriptions = serviceDescriptions;
    }

    /**
     * Parse a OsuProviderInfo from the given buffer.
     *
     * @param payload The buffer to read from
     * @return {@link OsuProviderInfo}
     * @throws BufferUnderflowException
     * @throws ProtocolException
     */
    public static OsuProviderInfo parse(ByteBuffer payload)
            throws ProtocolException {
        // Parse length field.
        int length = (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2)
                & 0xFFFF;
        if (length < MINIMUM_LENGTH) {
            throw new ProtocolException("Invalid length value: " + length);
        }

        // Parse friendly names.
        int friendlyNameLength =
                (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF;
        ByteBuffer friendlyNameBuffer = getSubBuffer(payload, friendlyNameLength);
        List<I18Name> friendlyNameList = parseI18Names(friendlyNameBuffer);

        // Parse server URI.
        Uri serverUri = Uri.parse(
                ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8));

        // Parse method list.
        int methodListLength = payload.get() & 0xFF;
        List<Integer> methodList = new ArrayList<>();
        while (methodListLength > 0) {
            methodList.add(payload.get() & 0xFF);
            methodListLength--;
        }

        // Parse list of icon info.
        int availableIconLength =
                (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF;
        ByteBuffer iconBuffer = getSubBuffer(payload, availableIconLength);
        List<IconInfo> iconInfoList = new ArrayList<>();
        while (iconBuffer.hasRemaining()) {
            iconInfoList.add(IconInfo.parse(iconBuffer));
        }

        // Parse Network Access Identifier.
        String nai = ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8);

        // Parse service descriptions.
        int serviceDescriptionLength =
                (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF;
        ByteBuffer descriptionsBuffer = getSubBuffer(payload, serviceDescriptionLength);
        List<I18Name> serviceDescriptionList = parseI18Names(descriptionsBuffer);

        return new OsuProviderInfo(friendlyNameList, serverUri, methodList, iconInfoList, nai,
                serviceDescriptionList);
    }

    /**
     * Returns friendly names for the OSU Provider.
     *
     * @return {@link Map} that consists of language code and friendly name expressed in the locale.
     */
    public Map<String, String> getFriendlyNames() {
        return mFriendlyNames;
    }

    public Uri getServerUri() {
        return mServerUri;
    }

    public List<Integer> getMethodList() {
        return Collections.unmodifiableList(mMethodList);
    }

    public List<IconInfo> getIconInfoList() {
        return Collections.unmodifiableList(mIconInfoList);
    }

    public String getNetworkAccessIdentifier() {
        return mNetworkAccessIdentifier;
    }

    public List<I18Name> getServiceDescriptions() {
        return Collections.unmodifiableList(mServiceDescriptions);
    }

    /**
     * Return the friendly Name for current language from the list of friendly names of OSU
     * provider.
     *
     * The string matching the default locale will be returned if it is found, otherwise the string
     * in english or the first string in the list will be returned if english is not found.
     * A null will be returned if the list is empty.
     *
     * @return String matching the default locale, null otherwise
     */
    public String getFriendlyName() {
        if (mFriendlyNames == null || mFriendlyNames.isEmpty()) return null;
        String lang = Locale.getDefault().getLanguage();
        String friendlyName = mFriendlyNames.get(lang);
        if (friendlyName != null) {
            return friendlyName;
        }
        friendlyName = mFriendlyNames.get("en");
        if (friendlyName != null) {
            return friendlyName;
        }
        return mFriendlyNames.get(mFriendlyNames.keySet().stream().findFirst().get());
    }

    /**
     * Return the service description string from the service description list.  The string
     * matching the default locale will be returned if it is found, otherwise the first element in
     * the list will be returned.  A null will be returned if the list is empty.
     *
     * @return service description string
     */
    public String getServiceDescription() {
        return getI18String(mServiceDescriptions);
    }

    @Override
    public boolean equals(Object thatObject) {
        if (this == thatObject) {
            return true;
        }
        if (!(thatObject instanceof OsuProviderInfo)) {
            return false;
        }
        OsuProviderInfo that = (OsuProviderInfo) thatObject;
        return (mFriendlyNames == null ? that.mFriendlyNames == null
                        : mFriendlyNames.equals(that.mFriendlyNames))
                && (mServerUri == null ? that.mServerUri == null
                        : mServerUri.equals(that.mServerUri))
                && (mMethodList == null ? that.mMethodList == null
                        : mMethodList.equals(that.mMethodList))
                && (mIconInfoList == null ? that.mIconInfoList == null
                        : mIconInfoList.equals(that.mIconInfoList))
                && TextUtils.equals(mNetworkAccessIdentifier, that.mNetworkAccessIdentifier)
                && (mServiceDescriptions == null ? that.mServiceDescriptions == null
                        : mServiceDescriptions.equals(that.mServiceDescriptions));
    }

    @Override
    public int hashCode() {
        return Objects.hash(mFriendlyNames, mServerUri, mMethodList, mIconInfoList,
                mNetworkAccessIdentifier, mServiceDescriptions);
    }

    @Override
    public String toString() {
        return "OsuProviderInfo{"
                + "mFriendlyNames=" + mFriendlyNames
                + ", mServerUri=" + mServerUri
                + ", mMethodList=" + mMethodList
                + ", mIconInfoList=" + mIconInfoList
                + ", mNetworkAccessIdentifier=" + mNetworkAccessIdentifier
                + ", mServiceDescriptions=" + mServiceDescriptions
                + "}";
    }

    /**
     * Parse list of I18N string from the given payload.
     *
     * @param payload The payload to parse from
     * @return List of {@link I18Name}
     * @throws ProtocolException
     */
    private static List<I18Name> parseI18Names(ByteBuffer payload) throws ProtocolException {
        List<I18Name> results = new ArrayList<>();
        while (payload.hasRemaining()) {
            I18Name name = I18Name.parse(payload);
            // Verify that the number of bytes for the operator name doesn't exceed the max
            // allowed.
            int textBytes = name.getText().getBytes(StandardCharsets.UTF_8).length;
            if (textBytes > MAXIMUM_I18N_STRING_LENGTH) {
                throw new ProtocolException("I18Name string exceeds the maximum allowed "
                        + textBytes);
            }
            results.add(name);
        }
        return results;
    }

    /**
     * Creates a new byte buffer whose content is a shared subsequence of
     * the given buffer's content.
     *
     * The sub buffer will starts from |payload|'s current position
     * and ends at |payload|'s current position plus |length|.  The |payload|'s current
     * position will advance pass |length| bytes.
     *
     * @param payload The original buffer
     * @param length The length of the new buffer
     * @return {@link ByteBuffer}
     * @throws BufferUnderflowException
     */
    private static ByteBuffer getSubBuffer(ByteBuffer payload, int length) {
        if (payload.remaining() < length) {
            throw new BufferUnderflowException();
        }
        // Set the subBuffer's starting and ending position.
        ByteBuffer subBuffer = payload.slice();
        subBuffer.limit(length);
        // Advance the original buffer's current position.
        payload.position(payload.position() + length);
        return subBuffer;
    }

    /**
     * Return the appropriate I18 string value from the list of I18 string values.
     * The string matching the default locale will be returned if it is found, otherwise the
     * first string in the list will be returned.  A null will be returned if the list is empty.
     *
     * @param i18Strings List of I18 string values
     * @return String matching the default locale, null otherwise
     */
    private static String getI18String(List<I18Name> i18Strings) {
        for (I18Name name : i18Strings) {
            if (name.getLanguage().equals(Locale.getDefault().getLanguage())) {
                return name.getText();
            }
        }
        if (i18Strings.size() > 0) {
            return i18Strings.get(0).getText();
        }
        return null;
    }
}
