package com.android.server.wifi.anqp;

import java.net.ProtocolException;
import java.nio.ByteBuffer;

/**
 * Holds an AP Geospatial Location ANQP Element, as specified in IEEE802.11-2012 section
 * 8.4.4.12.
 * <p/>
 * <p>
 * Section 8.4.2.24.10 of the IEEE802.11-2012 specification refers to RFC-3825 for the format of the
 * Geospatial location information. RFC-3825 has subsequently been obsoleted by RFC-6225 which
 * defines the same basic binary format for the DHCPv4 payload except that a few unused bits of the
 * Datum field have been reserved for other uses.
 * </p>
 * <p/>
 * <p>
 * RFC-3825 defines a resolution field for each of latitude, longitude and altitude as "the number
 * of significant bits" of precision in the respective values and implies through examples and
 * otherwise that the non-significant bits should be simply disregarded and the range of values are
 * calculated as the numeric interval obtained by varying the range of "insignificant bits" between
 * its extremes. As a simple example, consider the value 33 as a simple 8-bit number with three
 * significant bits: 33 is 00100001 binary and the leading 001 are the significant bits. With the
 * above definition, the range of numbers are [32,63] with 33 asymmetrically located at the low end
 * of the interval. In a more realistic setting an instrument, such as a GPS, would most likely
 * deliver measurements with a gaussian distribution around the exact value, meaning it is more
 * reasonable to assume the value as a "center" value with a symmetric uncertainty interval.
 * RFC-6225 redefines the "resolution" from RFC-3825 with an "uncertainty" value with these
 * properties, which is also the definition suggested here.
 * </p>
 * <p/>
 * <p>
 * The res fields provides the resolution as the exponent to a power of two,
 * e.g. 8 means 2^8 = +/- 256, 0 means 2^0 = +/- 1 and -7 means 2^-7 +/- 0.00781250.
 * Unknown resolution is indicated by not setting the respective resolution field in the RealValue.
 * </p>
 */
public class GEOLocationElement extends ANQPElement {
    public enum AltitudeType {Unknown, Meters, Floors}

    public enum Datum {Unknown, WGS84, NAD83Land, NAD83Water}

    private static final int ELEMENT_ID = 123;       // ???
    private static final int GEO_LOCATION_LENGTH = 16;

    private static final int LL_FRACTION_SIZE = 25;
    private static final int LL_WIDTH = 34;
    private static final int ALT_FRACTION_SIZE = 8;
    private static final int ALT_WIDTH = 30;
    private static final int RES_WIDTH = 6;
    private static final int ALT_TYPE_WIDTH = 4;
    private static final int DATUM_WIDTH = 8;

    private final RealValue mLatitude;
    private final RealValue mLongitude;
    private final RealValue mAltitude;
    private final AltitudeType mAltitudeType;
    private final Datum mDatum;

    public static class RealValue {
        private final double mValue;
        private final boolean mResolutionSet;
        private final int mResolution;

        public RealValue(double value) {
            mValue = value;
            mResolution = Integer.MIN_VALUE;
            mResolutionSet = false;
        }

        public RealValue(double value, int resolution) {
            mValue = value;
            mResolution = resolution;
            mResolutionSet = true;
        }

        public double getValue() {
            return mValue;
        }

        public boolean isResolutionSet() {
            return mResolutionSet;
        }

        public int getResolution() {
            return mResolution;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(String.format("%f", mValue));
            if (mResolutionSet) {
                sb.append("+/-2^").append(mResolution);
            }
            return sb.toString();
        }
    }

    public GEOLocationElement(Constants.ANQPElementType infoID, ByteBuffer payload)
            throws ProtocolException {
        super(infoID);

        payload.get();
        int locLength = payload.get() & Constants.BYTE_MASK;

        if (locLength != GEO_LOCATION_LENGTH) {
            throw new ProtocolException("GeoLocation length field value " + locLength +
                    " incorrect, expected 16");
        }
        if (payload.remaining() != GEO_LOCATION_LENGTH) {
            throw new ProtocolException("Bad buffer length " + payload.remaining() +
                    ", expected 16");
        }

        ReverseBitStream reverseBitStream = new ReverseBitStream(payload);

        int rawLatRes = (int) reverseBitStream.sliceOff(RES_WIDTH);
        double latitude =
                fixToFloat(reverseBitStream.sliceOff(LL_WIDTH), LL_FRACTION_SIZE, LL_WIDTH);

        mLatitude = rawLatRes != 0 ?
                new RealValue(latitude, bitsToAbsResolution(rawLatRes, LL_WIDTH,
                        LL_FRACTION_SIZE)) :
                new RealValue(latitude);

        int rawLonRes = (int) reverseBitStream.sliceOff(RES_WIDTH);
        double longitude =
                fixToFloat(reverseBitStream.sliceOff(LL_WIDTH), LL_FRACTION_SIZE, LL_WIDTH);

        mLongitude = rawLonRes != 0 ?
                new RealValue(longitude, bitsToAbsResolution(rawLonRes, LL_WIDTH,
                        LL_FRACTION_SIZE)) :
                new RealValue(longitude);

        int altType = (int) reverseBitStream.sliceOff(ALT_TYPE_WIDTH);
        mAltitudeType = altType < AltitudeType.values().length ?
                AltitudeType.values()[altType] :
                AltitudeType.Unknown;

        int rawAltRes = (int) reverseBitStream.sliceOff(RES_WIDTH);
        double altitude = fixToFloat(reverseBitStream.sliceOff(ALT_WIDTH), ALT_FRACTION_SIZE,
                ALT_WIDTH);

        mAltitude = rawAltRes != 0 ?
                new RealValue(altitude, bitsToAbsResolution(rawAltRes, ALT_WIDTH,
                        ALT_FRACTION_SIZE)) :
                new RealValue(altitude);

        int datumValue = (int) reverseBitStream.sliceOff(DATUM_WIDTH);
        mDatum = datumValue < Datum.values().length ? Datum.values()[datumValue] : Datum.Unknown;
    }

    public RealValue getLatitude() {
        return mLatitude;
    }

    public RealValue getLongitude() {
        return mLongitude;
    }

    public RealValue getAltitude() {
        return mAltitude;
    }

    public AltitudeType getAltitudeType() {
        return mAltitudeType;
    }

    public Datum getDatum() {
        return mDatum;
    }

    @Override
    public String toString() {
        return "GEOLocation{" +
                "mLatitude=" + mLatitude +
                ", mLongitude=" + mLongitude +
                ", mAltitude=" + mAltitude +
                ", mAltitudeType=" + mAltitudeType +
                ", mDatum=" + mDatum +
                '}';
    }

    private static class ReverseBitStream {

        private final byte[] mOctets;
        private int mBitoffset;

        private ReverseBitStream(ByteBuffer octets) {
            mOctets = new byte[octets.remaining()];
            octets.get(mOctets);
        }

        private long sliceOff(int bits) {
            final int bn = mBitoffset + bits;
            int remaining = bits;
            long value = 0;

            while (mBitoffset < bn) {
                int sbit = mBitoffset & 0x7;        // Bit #0 is MSB, inclusive
                int octet = mBitoffset >>> 3;

                // Copy the minimum of what's to the right of sbit
                // and how much more goes to the target
                int width = Math.min(Byte.SIZE - sbit, remaining);

                value = (value << width) | getBits(mOctets[octet], sbit, width);

                mBitoffset += width;
                remaining -= width;
            }

            return value;
        }

        private static int getBits(byte b, int b0, int width) {
            int mask = (1 << width) - 1;
            return (b >> (Byte.SIZE - b0 - width)) & mask;
        }
    }

    private static class BitStream {

        private final byte[] data;
        private int bitOffset;              // bit 0 is MSB of data[0]

        private BitStream(int octets) {
            data = new byte[octets];
        }

        private void append(long value, int width) {
            System.out.printf("Appending %x:%d\n", value, width);
            for (int sbit = width - 1; sbit >= 0; ) {
                int b0 = bitOffset >>> 3;
                int dbit = bitOffset & 0x7;

                int shr = sbit - 7 + dbit;
                int dmask = 0xff >>> dbit;

                if (shr >= 0) {
                    data[b0] = (byte) ((data[b0] & ~dmask) | ((value >>> shr) & dmask));
                    bitOffset += Byte.SIZE - dbit;
                    sbit -= Byte.SIZE - dbit;
                } else {
                    data[b0] = (byte) ((data[b0] & ~dmask) | ((value << -shr) & dmask));
                    bitOffset += sbit + 1;
                    sbit = -1;
                }
            }
        }

        private byte[] getOctets() {
            return data;
        }
    }

    static double fixToFloat(long value, int fractionSize, int width) {
        long sign = 1L << (width - 1);
        if ((value & sign) != 0) {
            value = -value;
            return -(double) (value & (sign - 1)) / (double) (1L << fractionSize);
        } else {
            return (double) (value & (sign - 1)) / (double) (1L << fractionSize);
        }
    }

    private static long floatToFix(double value, int fractionSize, int width) {
        return Math.round(value * (1L << fractionSize)) & ((1L << width) - 1);
    }

    private static final double LOG2_FACTOR = 1.0 / Math.log(2.0);

    /**
     * Convert an absolute variance value into absolute resolution representation,
     * where the variance = 2^resolution.
     *
     * @param variance The absolute variance
     * @return the absolute resolution.
     */
    private static int getResolution(double variance) {
        return (int) Math.ceil(Math.log(variance) * LOG2_FACTOR);
    }

    /**
     * Convert an absolute resolution, into the "number of significant bits" for the given fixed
     * point notation as defined in RFC-3825 and refined in RFC-6225.
     *
     * @param resolution   absolute resolution given as 2^resolution.
     * @param fieldWidth   Full width of the fixed point number used to represent the value.
     * @param fractionBits Number of fraction bits in the fixed point number used to represent the
     *                     value.
     * @return The number of "significant bits".
     */
    private static int absResolutionToBits(int resolution, int fieldWidth, int fractionBits) {
        return fieldWidth - fractionBits - 1 - resolution;
    }

    /**
     * Convert the protocol definition of "number of significant bits" into an absolute resolution.
     *
     * @param bits         The number of "significant bits" from the binary protocol.
     * @param fieldWidth   Full width of the fixed point number used to represent the value.
     * @param fractionBits Number of fraction bits in the fixed point number used to represent the
     *                     value.
     * @return The absolute resolution given as 2^resolution.
     */
    private static int bitsToAbsResolution(long bits, int fieldWidth, int fractionBits) {
        return fieldWidth - fractionBits - 1 - (int) bits;
    }
}
