/*
 * Copyright (C) 2015 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.net.wifi.ScanResult;
import android.net.wifi.WifiScanner.ScanData;
import android.net.wifi.WifiSsid;

import com.android.server.wifi.hotspot2.NetworkDetail;
import com.android.server.wifi.util.NativeUtil;

import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

/**
 * Utility for creating scan results from a scan
 */
public class ScanResults {
    private final ArrayList<ScanDetail> mScanDetails = new ArrayList<>();
    private final ScanData mScanData;
    private final ScanData mRawScanData;
    private final ScanResult[] mScanResults;

    private ScanResults(ArrayList<ScanDetail> scanDetails, ScanData scanData,
            ScanResult[] scanResults) {
        mScanDetails.addAll(scanDetails);
        mScanData = scanData;
        mRawScanData = scanData;
        mScanResults = scanResults;
    }

    /**
     * Merge the results contained in a number of ScanResults into a single ScanResults
     */
    public static ScanResults merge(int bandScanned, ScanResults... others) {
        ArrayList<ScanDetail> scanDetails = new ArrayList<>();
        ArrayList<ScanResult> scanDataResults = new ArrayList<>();
        ArrayList<ScanResult> rawScanResults = new ArrayList<>();
        for (ScanResults other : others) {
            scanDetails.addAll(other.getScanDetailArrayList());
            scanDataResults.addAll(Arrays.asList(other.getScanData().getResults()));
            rawScanResults.addAll(Arrays.asList(other.getRawScanResults()));
        }
        Collections.sort(scanDataResults, SCAN_RESULT_RSSI_COMPARATOR);
        int id = others[0].getScanData().getId();
        return new ScanResults(scanDetails, new ScanData(id, 0, 0, bandScanned, scanDataResults
                        .toArray(new ScanResult[scanDataResults.size()])),
                rawScanResults.toArray(new ScanResult[rawScanResults.size()]));
    }

    private static String generateBssid(Random r) {
        return String.format("%02X:%02X:%02X:%02X:%02X:%02X",
                r.nextInt(256), r.nextInt(256), r.nextInt(256),
                r.nextInt(256), r.nextInt(256), r.nextInt(256));
    }

    public static final Comparator<ScanResult> SCAN_RESULT_RSSI_COMPARATOR =
            new Comparator<ScanResult>() {
        public int compare(ScanResult r1, ScanResult r2) {
            return r2.level - r1.level;
        }
    };

    public static ScanResult.InformationElement generateSsidIe(String ssid) {
        ScanResult.InformationElement ie = new ScanResult.InformationElement();
        ie.id = ScanResult.InformationElement.EID_SSID;
        ie.bytes = ssid.getBytes(Charset.forName("UTF-8"));
        return ie;
    }

    public static byte[] generateIERawDatafromScanResultIE(ScanResult.InformationElement[] ies) {
        ArrayList<Byte> ieRawData = new ArrayList<>();
        for (int i = 0; i < ies.length; i++) {
            if (ies[i].id > 255 || ies[i].bytes.length > 255) {
                break;
            }
            ieRawData.add(BigInteger.valueOf(ies[i].id).toByteArray()[0]);
            ieRawData.add(BigInteger.valueOf(ies[i].bytes.length).toByteArray()[0]);
            for (int j = 0; j < ies[i].bytes.length; j++) {
                ieRawData.add(ies[i].bytes[j]);
            }
        }
        return NativeUtil.byteArrayFromArrayList(ieRawData);
    }

    /**
     * Generates an array of random ScanDetails with the given frequencies, seeded by the provided
     * seed value and test method name and class (annotated with @Test). This method will be
     * consistent between calls in the same test across runs.
     *
     * @param seed combined with a hash of the test method this seeds the random number generator
     * @param freqs list of frequencies for the generated scan results, these will map 1 to 1 to
     *              to the returned scan details. Duplicates can be specified to create multiple
     *              ScanDetails with the same frequency.
     */
    private static ScanDetail[] generateNativeResults(boolean needIE, int seed, int... freqs) {
        ScanDetail[] results = new ScanDetail[freqs.length];
        // Seed the results based on the provided seed as well as the test method name
        // This provides more varied scan results between individual tests that are very similar.
        Random r = new Random(seed + WifiTestUtil.getTestMethod().hashCode());
        for (int i = 0; i < freqs.length; ++i) {
            int freq = freqs[i];
            String ssid = new BigInteger(128, r).toString(36);
            String bssid = generateBssid(r);
            int rssi = r.nextInt(40) - 99; // -99 to -60
            ScanResult.InformationElement[] ie;
            if (needIE) {
                ie = new ScanResult.InformationElement[1];
                ie[0] = generateSsidIe(ssid);
            } else {
                ie = new ScanResult.InformationElement[0];
            }
            List<String> anqpLines = new ArrayList<>();
            NetworkDetail nd = new NetworkDetail(bssid, ie, anqpLines, freq);
            ScanDetail detail = new ScanDetail(nd, WifiSsid.fromUtf8Text(ssid),
                    bssid, "", rssi, freq,
                    // needed so that scan results aren't rejected because they are older than scan
                    // start.
                    Long.MAX_VALUE,
                    ie, anqpLines, generateIERawDatafromScanResultIE(ie));
            results[i] = detail;
        }
        return results;
    }

    /**
     * Create scan results with no IE information.
     */
    public static ScanDetail[] generateNativeResults(int seed, int... freqs) {
        return generateNativeResults(true, seed, freqs);
    }

    /**
     * Create a ScanResults with randomly generated results seeded by the id.
     * @see #generateNativeResults for more details on how results are generated
     */
    public static ScanResults create(int id, int bandScanned, int... freqs) {
        return create(id, bandScanned, generateNativeResults(id, freqs));
    }

    public static ScanResults create(int id, int bandScanned,
            ScanDetail... nativeResults) {
        return new ScanResults(id, bandScanned, -1, nativeResults);
    }

    /**
     * Create scan results that contain all results for the native results and
     * full scan results, but limits the number of onResults results after sorting
     * by RSSI
     */
    public static ScanResults createOverflowing(int id, int bandScanned, int maxResults,
            ScanDetail... nativeResults) {
        return new ScanResults(id, bandScanned, maxResults, nativeResults);
    }

    private ScanResults(int id, int bandScanned, int maxResults, ScanDetail... nativeResults) {
        mScanResults = new ScanResult[nativeResults.length];
        for (int i = 0; i < nativeResults.length; ++i) {
            mScanDetails.add(nativeResults[i]);
            mScanResults[i] = nativeResults[i].getScanResult();
        }
        ScanResult[] sortedScanResults = Arrays.copyOf(mScanResults, mScanResults.length);
        Arrays.sort(sortedScanResults, SCAN_RESULT_RSSI_COMPARATOR);
        mRawScanData = new ScanData(id, 0, 0, bandScanned, sortedScanResults);
        if (maxResults == -1) {
            mScanData = mRawScanData;
        } else {
            ScanResult[] reducedScanResults = Arrays.copyOf(sortedScanResults,
                    Math.min(sortedScanResults.length, maxResults));
            mScanData = new ScanData(id, 0, 0, bandScanned, reducedScanResults);
        }
    }

    public ArrayList<ScanDetail> getScanDetailArrayList() {
        return mScanDetails;
    }

    public ScanData getScanData() {
        return mScanData;
    }

    public ScanResult[] getRawScanResults() {
        return mScanResults;
    }

    public ScanData getRawScanData() {
        return mRawScanData;
    }
}
