// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.text.*;

/**
 * Location - describes the physical location of hosts, networks, subnets.
 *
 * @author Brian Wellington
 */

public class LOCRecord extends Record {

    private static final long serialVersionUID = 9058224788126750409L;

    private static NumberFormat w2, w3;

    private long size, hPrecision, vPrecision;
    private long latitude, longitude, altitude;

    static {
        w2 = new DecimalFormat();
        w2.setMinimumIntegerDigits(2);

        w3 = new DecimalFormat();
        w3.setMinimumIntegerDigits(3);
    }

    LOCRecord() {
    }

    Record
    getObject() {
        return new LOCRecord();
    }

    /**
     * Creates an LOC Record from the given data
     *
     * @param latitude   The latitude of the center of the sphere
     * @param longitude  The longitude of the center of the sphere
     * @param altitude   The altitude of the center of the sphere, in m
     * @param size       The diameter of a sphere enclosing the described entity, in m.
     * @param hPrecision The horizontal precision of the data, in m.
     * @param vPrecision The vertical precision of the data, in m.
     */
    public LOCRecord(Name name, int dclass, long ttl, double latitude, double longitude,
                     double altitude, double size, double hPrecision, double vPrecision) {
        super(name, Type.LOC, dclass, ttl);
        this.latitude = (long) (latitude * 3600 * 1000 + (1L << 31));
        this.longitude = (long) (longitude * 3600 * 1000 + (1L << 31));
        this.altitude = (long) ((altitude + 100000) * 100);
        this.size = (long) (size * 100);
        this.hPrecision = (long) (hPrecision * 100);
        this.vPrecision = (long) (vPrecision * 100);
    }

    void
    rrFromWire(DNSInput in) throws IOException {
        int version;

        version = in.readU8();
        if (version != 0)
            throw new WireParseException("Invalid LOC version");

        size = parseLOCformat(in.readU8());
        hPrecision = parseLOCformat(in.readU8());
        vPrecision = parseLOCformat(in.readU8());
        latitude = in.readU32();
        longitude = in.readU32();
        altitude = in.readU32();
    }

    private double
    parseFixedPoint(String s) {
        if (s.matches("^-?\\d+$"))
            return Integer.parseInt(s);
        else if (s.matches("^-?\\d+\\.\\d*$")) {
            String[] parts = s.split("\\.");
            double value = Integer.parseInt(parts[0]);
            double fraction = Integer.parseInt(parts[1]);
            if (value < 0)
                fraction *= -1;
            int digits = parts[1].length();
            return value + (fraction / Math.pow(10, digits));
        } else
            throw new NumberFormatException();
    }

    private long
    parsePosition(Tokenizer st, String type) throws IOException {
        boolean isLatitude = type.equals("latitude");
        int deg = 0, min = 0;
        double sec = 0;
        long value;
        String s;

        deg = st.getUInt16();
        if (deg > 180 || (deg > 90 && isLatitude))
            throw st.exception("Invalid LOC " + type + " degrees");

        s = st.getString();
        try {
            min = Integer.parseInt(s);
            if (min < 0 || min > 59)
                throw st.exception("Invalid LOC " + type + " minutes");
            s = st.getString();
            sec = parseFixedPoint(s);
            if (sec < 0 || sec >= 60)
                throw st.exception("Invalid LOC " + type + " seconds");
            s = st.getString();
        } catch (NumberFormatException e) {
        }

        if (s.length() != 1)
            throw st.exception("Invalid LOC " + type);

        value = (long) (1000 * (sec + 60L * (min + 60L * deg)));

        char c = Character.toUpperCase(s.charAt(0));
        if ((isLatitude && c == 'S') || (!isLatitude && c == 'W'))
            value = -value;
        else if ((isLatitude && c != 'N') || (!isLatitude && c != 'E'))
            throw st.exception("Invalid LOC " + type);

        value += (1L << 31);

        return value;
    }

    private long
    parseDouble(Tokenizer st, String type, boolean required, long min, long max,
                long defaultValue)
            throws IOException {
        Tokenizer.Token token = st.get();
        if (token.isEOL()) {
            if (required)
                throw st.exception("Invalid LOC " + type);
            st.unget();
            return defaultValue;
        }
        String s = token.value;
        if (s.length() > 1 && s.charAt(s.length() - 1) == 'm')
            s = s.substring(0, s.length() - 1);
        try {
            long value = (long) (100 * parseFixedPoint(s));
            if (value < min || value > max)
                throw st.exception("Invalid LOC " + type);
            return value;
        } catch (NumberFormatException e) {
            throw st.exception("Invalid LOC " + type);
        }
    }

    void
    rdataFromString(Tokenizer st, Name origin) throws IOException {
        latitude = parsePosition(st, "latitude");
        longitude = parsePosition(st, "longitude");
        altitude = parseDouble(st, "altitude", true,
                -10000000, 4284967295L, 0) + 10000000;
        size = parseDouble(st, "size", false, 0, 9000000000L, 100);
        hPrecision = parseDouble(st, "horizontal precision", false,
                0, 9000000000L, 1000000);
        vPrecision = parseDouble(st, "vertical precision", false,
                0, 9000000000L, 1000);
    }

    private void
    renderFixedPoint(StringBuffer sb, NumberFormat formatter, long value,
                     long divisor) {
        sb.append(value / divisor);
        value %= divisor;
        if (value != 0) {
            sb.append(".");
            sb.append(formatter.format(value));
        }
    }

    private String
    positionToString(long value, char pos, char neg) {
        StringBuffer sb = new StringBuffer();
        char direction;

        long temp = value - (1L << 31);
        if (temp < 0) {
            temp = -temp;
            direction = neg;
        } else
            direction = pos;

        sb.append(temp / (3600 * 1000)); /* degrees */
        temp = temp % (3600 * 1000);
        sb.append(" ");

        sb.append(temp / (60 * 1000)); /* minutes */
        temp = temp % (60 * 1000);
        sb.append(" ");

        renderFixedPoint(sb, w3, temp, 1000); /* seconds */
        sb.append(" ");

        sb.append(direction);

        return sb.toString();
    }


    /**
     * Convert to a String
     */
    String
    rrToString() {
        StringBuffer sb = new StringBuffer();

	/* Latitude */
        sb.append(positionToString(latitude, 'N', 'S'));
        sb.append(" ");

	/* Latitude */
        sb.append(positionToString(longitude, 'E', 'W'));
        sb.append(" ");

	/* Altitude */
        renderFixedPoint(sb, w2, altitude - 10000000, 100);
        sb.append("m ");

	/* Size */
        renderFixedPoint(sb, w2, size, 100);
        sb.append("m ");

	/* Horizontal precision */
        renderFixedPoint(sb, w2, hPrecision, 100);
        sb.append("m ");

	/* Vertical precision */
        renderFixedPoint(sb, w2, vPrecision, 100);
        sb.append("m");

        return sb.toString();
    }

    /**
     * Returns the latitude
     */
    public double
    getLatitude() {
        return ((double) (latitude - (1L << 31))) / (3600 * 1000);
    }

    /**
     * Returns the longitude
     */
    public double
    getLongitude() {
        return ((double) (longitude - (1L << 31))) / (3600 * 1000);
    }

    /**
     * Returns the altitude
     */
    public double
    getAltitude() {
        return ((double) (altitude - 10000000)) / 100;
    }

    /**
     * Returns the diameter of the enclosing sphere
     */
    public double
    getSize() {
        return ((double) size) / 100;
    }

    /**
     * Returns the horizontal precision
     */
    public double
    getHPrecision() {
        return ((double) hPrecision) / 100;
    }

    /**
     * Returns the horizontal precision
     */
    public double
    getVPrecision() {
        return ((double) vPrecision) / 100;
    }

    void
    rrToWire(DNSOutput out, Compression c, boolean canonical) {
        out.writeU8(0); /* version */
        out.writeU8(toLOCformat(size));
        out.writeU8(toLOCformat(hPrecision));
        out.writeU8(toLOCformat(vPrecision));
        out.writeU32(latitude);
        out.writeU32(longitude);
        out.writeU32(altitude);
    }

    private static long
    parseLOCformat(int b) throws WireParseException {
        long out = b >> 4;
        int exp = b & 0xF;
        if (out > 9 || exp > 9)
            throw new WireParseException("Invalid LOC Encoding");
        while (exp-- > 0)
            out *= 10;
        return (out);
    }

    private int
    toLOCformat(long l) {
        byte exp = 0;
        while (l > 9) {
            exp++;
            l /= 10;
        }
        return (int) ((l << 4) + exp);
    }

}
