package com.siliconlabs.bledemo.beaconutils.eddystone;
// Copyright 2015 Google Inc. All rights reserved.
//
// 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.
// see https://github.com/google/eddystone

import android.os.ParcelUuid;
import android.util.Log;
import android.util.SparseArray;
import android.webkit.URLUtil;

import java.net.URISyntaxException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Locale;
import java.util.UUID;


/**
 * Copied from https://github.com/google/uribeacon/android-uribeacon/uribeacon-library/
 * src/main/java/org/uribeacon/beacon/UriBeacon.java
 */
public class UriBeacon {

    /**
     * The Service Data UUID is the reserved 16-bit Service Data Code in the form of a globally unique
     * 128-bit UUID.
     */
    public static final ParcelUuid URI_SERVICE_UUID =
            ParcelUuid.fromString("0000FED8-0000-1000-8000-00805F9B34FB");
    public static final byte NO_TX_POWER_LEVEL = -101;
    public static final byte NO_FLAGS = 0;
    public static final String NO_URI = "";
    private static final String TAG = "UriBeacon";
    private static final int DATA_TYPE_SERVICE_DATA = 0x16;
    private static final byte[] URI_SERVICE_16_BIT_UUID_BYTES = {(byte) 0xd8, (byte) 0xfe};

    //TODO: Add comments
    public static final ParcelUuid TEST_SERVICE_UUID =
            ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
    private static final byte[] TEST_SERVICE_16_BIT_UUID_BYTES = {(byte) 0xaa, (byte) 0xfe};
    private static final byte TEST_URL_FRAME_TYPE = 0x10;

    /**
     * URI Scheme maps a byte code into the scheme and an optional scheme specific prefix.
     */
    private static final SparseArray<String> URI_SCHEMES = new SparseArray<String>() {{
        put((byte) 0, "http://www.");
        put((byte) 1, "https://www.");
        put((byte) 2, "http://");
        put((byte) 3, "https://");
        put((byte) 4, "urn:uuid:");    // RFC 2141 and RFC 4122};
    }};
    /**
     * Expansion strings for "http" and "https" schemes. These contain strings appearing anywhere in a
     * URL. Restricted to Generic TLDs. <p/> Note: this is a scheme specific encoding.
     */
    private static final SparseArray<String> URL_CODES = new SparseArray<String>() {{
        put((byte) 0, ".com/");
        put((byte) 1, ".org/");
        put((byte) 2, ".edu/");
        put((byte) 3, ".net/");
        put((byte) 4, ".info/");
        put((byte) 5, ".biz/");
        put((byte) 6, ".gov/");
        put((byte) 7, ".com");
        put((byte) 8, ".org");
        put((byte) 9, ".edu");
        put((byte) 10, ".net");
        put((byte) 11, ".info");
        put((byte) 12, ".biz");
        put((byte) 13, ".gov");
    }};
    private static final int FLAGS_FIELD_SIZE = 3;
    private static final int URI_SERVICE_FLAGS_TXPOWER_SIZE = 2;
    private static final byte[] URI_SERVICE_UUID_FIELD = {(byte) 0x03, (byte) 0x03, (byte) 0xD8,
            (byte) 0xFE};
    private static final byte[] URI_SERVICE_DATA_FIELD_HEADER = {0x16, (byte) 0xD8, (byte) 0xFE};
    private static final int MAX_ADVERTISING_DATA_BYTES = 31;
    private static final int MAX_URI_LENGTH = 18;
    private final byte mFlags;
    private final byte mTxPowerLevel;
    private final String mUriString;

    // Copy constructor
    UriBeacon(UriBeacon uriBeacon) {
        mUriString = uriBeacon.getUriString();
        mFlags = uriBeacon.getFlags();
        mTxPowerLevel = uriBeacon.getTxPowerLevel();
    }

    /**
     * Creates the Uri string with embedded expansion codes.
     *
     * @param uri to be encoded
     * @return the Uri string with expansion codes.
     */
    public static byte[] encodeUri(String uri) {
        if (uri.length() == 0) {
            return new byte[0];
        }
        ByteBuffer bb = ByteBuffer.allocate(uri.length());
        // UUIDs are ordered as byte array, which means most significant first
        bb.order(ByteOrder.BIG_ENDIAN);
        int position = 0;

        // Add the byte code for the scheme or return null if none
        Byte schemeCode = encodeUriScheme(uri);
        if (schemeCode == null) {
            return null;
        }
        String scheme = URI_SCHEMES.get(schemeCode);
        bb.put(schemeCode);
        position += scheme.length();

        if (URLUtil.isNetworkUrl(scheme)) {
            return encodeUrl(uri, position, bb);
        } else if ("urn:uuid:".equals(scheme)) {
            return encodeUrnUuid(uri, position, bb);
        }
        return null;
    }

    /**
     * @return The Uri that will be broadcasted in a byte[]
     */
    public byte[] getUriBytes() {
        return encodeUri(mUriString);
    }

    /**
     * @return the Uri flags indicating the discoverable mode and capability of the device.
     */
    public byte getFlags() {
        return mFlags;
    }

    /**
     * @return the transmission power level of the packet in dBm. This value can be used to calculate
     * the path loss of a received packet using the following equation: <p/> <code>path loss =
     * txPowerLevel - RSSI</code>
     */
    public byte getTxPowerLevel() {
        return mTxPowerLevel;
    }

    /**
     * @return the Uri text of the packet.
     */
    public String getUriString() {
        return mUriString;
    }

    /**
     * Parse scan record bytes to {@link UriBeacon}. <p/> The format is defined in Uri Beacon
     * Definition.
     *
     * @param scanRecordBytes The scan record of Bluetooth LE advertisement and/or scan response.
     */
    public static UriBeacon parseFromBytes(byte[] scanRecordBytes) {
        byte[] serviceData = parseServiceDataFromBytes(scanRecordBytes);
        // Minimum UriBeacon consists of flags, TxPower
        if (serviceData != null && serviceData.length >= 2) {
            int currentPos = 0;
            byte flags = serviceData[currentPos++];
            byte txPowerLevel = serviceData[currentPos++];
            String uri = decodeUri(serviceData, currentPos);
            return new UriBeacon(flags, txPowerLevel, uri);
        }
        //TODO: refactor
        serviceData = parseTestServiceDataFromBytes(scanRecordBytes);
        if (serviceData != null && serviceData.length >= 2) {
            int currentPos = 0;
            byte txPowerLevel = serviceData[currentPos++];
            byte flags = (byte) (serviceData[currentPos] >> 4);
            serviceData[currentPos] = (byte) (serviceData[currentPos] & 0xFF);
            String uri = decodeUri(serviceData, currentPos);
            return new UriBeacon(flags, txPowerLevel, uri);
        }
        return null;
    }

    @Override
    public String toString() {
        return String.format(Locale.ENGLISH,
                "%[email protected](uri:'%s' txPowerLevel:%d flags:%d)",
                getClass().getSimpleName(), mUriString, mTxPowerLevel, mFlags);
    }

    /**
     * The advertisement data for the UriBeacon as a byte array.
     *
     * @return the UriBeacon bytes
     */
    public byte[] toByteArray() {
        int totalUriBytes = totalBytes(mUriString);
        if (totalUriBytes == 0) {
            return null;
        }
        ByteBuffer buffer = ByteBuffer.allocateDirect(totalUriBytes);
        buffer.put(URI_SERVICE_UUID_FIELD);
        byte[] uriBytes;
        uriBytes = encodeUri(mUriString);
        byte length = (byte) (URI_SERVICE_DATA_FIELD_HEADER.length +
                URI_SERVICE_FLAGS_TXPOWER_SIZE + uriBytes.length);
        buffer.put(length);
        buffer.put(URI_SERVICE_DATA_FIELD_HEADER);
        buffer.put(mFlags);
        buffer.put(mTxPowerLevel);
        buffer.put(uriBytes);
        return byteBufferToArray(buffer);
    }

    /**
     * @param uriString
     * @return
     */
    public static int uriLength(String uriString) {
        byte[] encodedUri = encodeUri(uriString);
        if (encodedUri == null) {
            return -1;
        } else {
            return encodedUri.length;
        }
    }

    public static class Builder {

        private String mUriString;
        private byte[] mUriBytes;
        private byte mFlags = NO_FLAGS;
        private byte mTxPowerLevel = NO_TX_POWER_LEVEL;

        /**
         * Add a Uri to the UriBeacon advertised data.
         *
         * @param uriString The Uri to be advertised.
         * @return The UriBeacon Builder.
         */
        public Builder uriString(String uriString) {
            mUriString = uriString;
            return this;
        }

        public Builder uriString(byte[] uriBytes) {
            mUriBytes = uriBytes;
            return this;
        }

        /**
         * Add flags to the UriBeacon advertised data.
         *
         * @param flags The flags to be advertised.
         * @return The UriBeacon Builder.
         */
        public Builder flags(byte flags) {
            mFlags = flags;
            return this;
        }

        /**
         * Add a Tx Power Level to the UriBeacon advertised data.
         *
         * @param txPowerLevel The TX Power Level to be advertised.
         * @return The UriBeacon Builder.
         */
        public Builder txPowerLevel(byte txPowerLevel) {
            mTxPowerLevel = txPowerLevel;
            return this;
        }

        /**
         * Build the beacon.
         *
         * @return The UriBeacon
         * @throws URISyntaxException if the uri provided is not valid
         */
        public UriBeacon build() throws URISyntaxException {
            if (mUriBytes != null) {
                mUriString = decodeUri(mUriBytes, 0);
                if (mUriString == null) {
                    throw new IllegalArgumentException("Could not decode URI");
                }
            }

            if (mUriString == null) {
                throw new IllegalArgumentException(
                        "UriBeacon advertisements must include a URI");
            }
            // The firmware adds ADV Flags taking up 3 bytes so we can only send 31 - 3 = 28 bytes.
            // The Service UUID is 4 bytes. The Service Data contains a header (4 bytes),
            // Flags (1 byte) and  TX Power Level (1 byte).
            // So this works out to 28 - 4 - 4 - 1 - 1 = 18 bytes for Service Data Uri.
            int length = uriLength(mUriString);
            if (length > MAX_URI_LENGTH) {
                throw new URISyntaxException(mUriString, "Uri size is larger than "
                        + MAX_URI_LENGTH + " bytes");
            } else if (length == -1) {
                throw new URISyntaxException(mUriString, "Not a valid URI");
            }
            return new UriBeacon(mFlags, mTxPowerLevel, mUriString);
        }
    }

    private UriBeacon(byte flags, byte txPowerLevel, String uriString) {
        mFlags = flags;
        mTxPowerLevel = txPowerLevel;
        mUriString = uriString;
    }

    private static String decodeUri(byte[] serviceData, int offset) {
        if (serviceData.length == offset) {
            return NO_URI;
        }
        StringBuilder uriBuilder = new StringBuilder();
        if (offset < serviceData.length) {
            byte b = serviceData[offset++];
            String scheme = URI_SCHEMES.get(b);
            if (scheme != null) {
                uriBuilder.append(scheme);
                if (URLUtil.isNetworkUrl(scheme)) {
                    return decodeUrl(serviceData, offset, uriBuilder);
                } else if ("urn:uuid:".equals(scheme)) {
                    return decodeUrnUuid(serviceData, offset, uriBuilder);
                }
            }
            Log.w(TAG, "decodeUri unknown Uri scheme code=" + b);
        }
        return null;
    }

    private static String decodeUrl(byte[] serviceData, int offset, StringBuilder urlBuilder) {
        while (offset < serviceData.length) {
            byte b = serviceData[offset++];
            String code = URL_CODES.get(b);
            if (code != null) {
                urlBuilder.append(code);
            } else {
                urlBuilder.append((char) b);
            }
        }
        return urlBuilder.toString();
    }

    private static String decodeUrnUuid(byte[] serviceData, int offset, StringBuilder urnBuilder) {
        ByteBuffer bb = ByteBuffer.wrap(serviceData);
        // UUIDs are ordered as byte array, which means most significant first
        bb.order(ByteOrder.BIG_ENDIAN);
        long mostSignificantBytes, leastSignificantBytes;
        try {
            bb.position(offset);
            mostSignificantBytes = bb.getLong();
            leastSignificantBytes = bb.getLong();
        } catch (BufferUnderflowException e) {
            Log.w(TAG, "decodeUrnUuid BufferUnderflowException!");
            return null;
        }
        UUID uuid = new UUID(mostSignificantBytes, leastSignificantBytes);
        urnBuilder.append(uuid.toString());
        return urnBuilder.toString();
    }

    /**
     * Finds the longest expansion from the uri at the current position.
     *
     * @param uriString the Uri
     * @param pos       start position
     * @return an index in URI_MAP or 0 if none.
     */
    private static byte findLongestExpansion(String uriString, int pos) {
        byte expansion = -1;
        int expansionLength = 0;
        for (int i = 0; i < URL_CODES.size(); i++) {
            // get the key and value.
            int key = URL_CODES.keyAt(i);
            String value = URL_CODES.valueAt(i);
            if (value.length() > expansionLength && uriString.startsWith(value, pos)) {
                expansion = (byte) key;
                expansionLength = value.length();
            }
        }
        return expansion;
    }

    private static Byte encodeUriScheme(String uri) {
        String lowerCaseUri = uri.toLowerCase(Locale.ENGLISH);
        for (int i = 0; i < URI_SCHEMES.size(); i++) {
            // get the key and value.
            int key = URI_SCHEMES.keyAt(i);
            String value = URI_SCHEMES.valueAt(i);
            if (lowerCaseUri.startsWith(value)) {
                return (byte) key;
            }
        }
        return null;
    }

    private static byte[] encodeUrl(String url, int position, ByteBuffer bb) {
        while (position < url.length()) {
            byte expansion = findLongestExpansion(url, position);
            if (expansion >= 0) {
                bb.put(expansion);
                position += URL_CODES.get(expansion).length();
            } else {
                bb.put((byte) url.charAt(position++));
            }
        }
        return byteBufferToArray(bb);
    }

    private static byte[] encodeUrnUuid(String urn, int position, ByteBuffer bb) {
        String uuidString = urn.substring(position);
        UUID uuid;
        try {
            uuid = UUID.fromString(uuidString);
        } catch (IllegalArgumentException e) {
            Log.w(TAG, "encodeUrnUuid invalid urn:uuid format - " + urn);
            return null;
        }
        // UUIDs are ordered as byte array, which means most significant first
        bb.order(ByteOrder.BIG_ENDIAN);
        bb.putLong(uuid.getMostSignificantBits());
        bb.putLong(uuid.getLeastSignificantBits());
        return byteBufferToArray(bb);
    }

    private static byte[] byteBufferToArray(ByteBuffer bb) {
        byte[] bytes = new byte[bb.position()];
        bb.rewind();
        bb.get(bytes, 0, bytes.length);
        return bytes;
    }

    private static byte[] UuidToByteArray(UUID uuid) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
        bb.putLong(uuid.getMostSignificantBits());
        bb.putLong(uuid.getLeastSignificantBits());
        return byteBufferToArray(bb);
    }

    // Compute the size of the advertisement data in the Service UUID and Service Data fields.
    // This does not include the ADV Flag Fields (3 bytes).
    private static int totalBytes(String uriString) {
        byte[] encodedUri = encodeUri(uriString);
        if (encodedUri == null) {
            return 0;
        }
        int size = URI_SERVICE_UUID_FIELD.length;
        size += 1; // length is one byte
        size += URI_SERVICE_DATA_FIELD_HEADER.length;
        size += 1; // flags is one byte.
        size += 1; // tx power level value is one byte.
        size += encodedUri.length;
        return size;
    }

    /**
     * Return the Service Data for Uri Service.
     *
     * @param scanRecord The scanRecord containing the UriBeacon advertisement.
     * @return data from the Uri Service field
     */
    private static byte[] parseServiceDataFromBytes(byte[] scanRecord) {
        int currentPos = 0;
        try {
            while (currentPos < scanRecord.length) {
                int fieldLength = scanRecord[currentPos++] & 0xff;
                if (fieldLength == 0) {
                    break;
                }
                int fieldType = scanRecord[currentPos] & 0xff;
                if (fieldType == DATA_TYPE_SERVICE_DATA) {
                    // The first two bytes of the service data are service data UUID.
                    if (scanRecord[currentPos + 1] == URI_SERVICE_16_BIT_UUID_BYTES[0]
                            && scanRecord[currentPos + 2] == URI_SERVICE_16_BIT_UUID_BYTES[1]) {
                        // jump to data
                        currentPos += 3;
                        // length includes the length of the field type and ID
                        byte[] bytes = new byte[fieldLength - 3];
                        System.arraycopy(scanRecord, currentPos, bytes, 0, fieldLength - 3);
                        return bytes;
                    }
                }
                // length includes the length of the field type
                currentPos += fieldLength;
            }
        } catch (Exception e) {
            Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord), e);
        }
        return null;
    }

    private static byte[] parseTestServiceDataFromBytes(byte[] scanRecord) {
        //TODO: Add comments
        int currentPos = 0;
        try {
            while (currentPos < scanRecord.length) {
                int fieldLength = scanRecord[currentPos++] & 0xff;
                if (fieldLength == 0) {
                    break;
                }
                int fieldType = scanRecord[currentPos] & 0xff;
                if (fieldType == DATA_TYPE_SERVICE_DATA) {
                    if (scanRecord[currentPos + 1] == TEST_SERVICE_16_BIT_UUID_BYTES[0]
                            && scanRecord[currentPos + 2] == TEST_SERVICE_16_BIT_UUID_BYTES[1]
                            && scanRecord[currentPos + 3] == TEST_URL_FRAME_TYPE) {
                        // Jump to beginning of frame.
                        currentPos += 4;
                        // TODO: Add tests
                        // field length - field type - ID - frame type
                        byte[] bytes = new byte[fieldLength - 4];
                        System.arraycopy(scanRecord, currentPos, bytes, 0, fieldLength - 4);
                        return bytes;
                    }
                }
                // length includes the length of the field type.
                currentPos += fieldLength;
            }
        } catch (Exception e) {
            Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord), e);
        }
        return null;
    }

}