// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;

/**
 * Location - describes the physical location of hosts, networks, subnets.
 *
 * @author Brian Wellington
 * @see <a href="https://datatracker.ietf.org/doc/html/rfc1876">RFC 1876: A Means for Expressing
 *     Location Information in the Domain Name System</a>
 */
public class LOCRecord extends Record {
  private static final NumberFormat w2;
  private static final NumberFormat 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() {}

  /**
   * 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);
  }

  @Override
  protected 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, 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);
    }
  }

  @Override
  protected 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(
      StringBuilder 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) {
    StringBuilder sb = new StringBuilder();
    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 */
  @Override
  protected String rrToString() {
    StringBuilder sb = new StringBuilder();

    /* 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;
  }

  @Override
  protected 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 & 0xFF));
  }
}
