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

package org.xbill.DNS;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * A representation of a $GENERATE statement in a master file.
 *
 * @author Brian Wellington
 */
public class Generator {

  /** The start of the range. */
  public long start;

  /** The end of the range. */
  public long end;

  /** The step value of the range. */
  public long step;

  /** The pattern to use for generating record names. */
  public final String namePattern;

  /** The type of the generated records. */
  public final int type;

  /** The class of the generated records. */
  public final int dclass;

  /** The ttl of the generated records. */
  public final long ttl;

  /** The pattern to use for generating record data. */
  public final String rdataPattern;

  /** The origin to append to relative names. */
  public final Name origin;

  private long current;

  /**
   * Indicates whether generation is supported for this type.
   *
   * @throws InvalidTypeException The type is out of range.
   */
  public static boolean supportedType(int type) {
    Type.check(type);
    return type == Type.PTR
        || type == Type.CNAME
        || type == Type.DNAME
        || type == Type.A
        || type == Type.AAAA
        || type == Type.NS;
  }

  /**
   * Creates a specification for generating records, as a $GENERATE statement in a master file.
   *
   * @param start The start of the range.
   * @param end The end of the range.
   * @param step The step value of the range.
   * @param namePattern The pattern to use for generating record names.
   * @param type The type of the generated records. The supported types are PTR, CNAME, DNAME, A,
   *     AAAA, and NS.
   * @param dclass The class of the generated records.
   * @param ttl The ttl of the generated records.
   * @param rdataPattern The pattern to use for generating record data.
   * @param origin The origin to append to relative names.
   * @throws IllegalArgumentException The range is invalid.
   * @throws IllegalArgumentException The type does not support generation.
   * @throws IllegalArgumentException The dclass is not a valid class.
   */
  public Generator(
      long start,
      long end,
      long step,
      String namePattern,
      int type,
      int dclass,
      long ttl,
      String rdataPattern,
      Name origin) {
    if (start < 0 || end < 0 || start > end || step <= 0) {
      throw new IllegalArgumentException("invalid range specification");
    }
    if (!supportedType(type)) {
      throw new IllegalArgumentException("unsupported type");
    }
    DClass.check(dclass);

    this.start = start;
    this.end = end;
    this.step = step;
    this.namePattern = namePattern;
    this.type = type;
    this.dclass = dclass;
    this.ttl = ttl;
    this.rdataPattern = rdataPattern;
    this.origin = origin;
    this.current = start;
  }

  private String substitute(String spec, long n) throws IOException {
    boolean escaped = false;
    byte[] str = spec.getBytes();
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < str.length; i++) {
      char c = (char) (str[i] & 0xFF);
      if (escaped) {
        sb.append(c);
        escaped = false;
      } else if (c == '\\') {
        if (i + 1 == str.length) {
          throw new TextParseException("invalid escape character");
        }
        escaped = true;
      } else if (c == '$') {
        boolean negative = false;
        long offset = 0;
        long width = 0;
        long base = 10;
        boolean wantUpperCase = false;
        if (i + 1 < str.length && str[i + 1] == '$') {
          // '$$' == literal '$' for backwards
          // compatibility with old versions of BIND.
          c = (char) (str[++i] & 0xFF);
          sb.append(c);
          continue;
        } else if (i + 1 < str.length && str[i + 1] == '{') {
          // It's a substitution with modifiers.
          i++;
          if (i + 1 < str.length && str[i + 1] == '-') {
            negative = true;
            i++;
          }
          while (i + 1 < str.length) {
            c = (char) (str[++i] & 0xFF);
            if (c == ',' || c == '}') {
              break;
            }
            if (c < '0' || c > '9') {
              throw new TextParseException("invalid offset");
            }
            c -= '0';
            offset *= 10;
            offset += c;
          }
          if (negative) {
            offset = -offset;
          }

          if (c == ',') {
            while (i + 1 < str.length) {
              c = (char) (str[++i] & 0xFF);
              if (c == ',' || c == '}') {
                break;
              }
              if (c < '0' || c > '9') {
                throw new TextParseException("invalid width");
              }
              c -= '0';
              width *= 10;
              width += c;
            }
          }

          if (c == ',') {
            if (i + 1 == str.length) {
              throw new TextParseException("invalid base");
            }
            c = (char) (str[++i] & 0xFF);
            if (c == 'o') {
              base = 8;
            } else if (c == 'x') {
              base = 16;
            } else if (c == 'X') {
              base = 16;
              wantUpperCase = true;
            } else if (c != 'd') {
              throw new TextParseException("invalid base");
            }
          }

          if (i + 1 == str.length || str[i + 1] != '}') {
            throw new TextParseException("invalid modifiers");
          }
          i++;
        }
        long v = n + offset;
        if (v < 0) {
          throw new TextParseException("invalid offset expansion");
        }
        String number;
        if (base == 8) {
          number = Long.toOctalString(v);
        } else if (base == 16) {
          number = Long.toHexString(v);
        } else {
          number = Long.toString(v);
        }
        if (wantUpperCase) {
          number = number.toUpperCase();
        }
        if (width != 0 && width > number.length()) {
          int zeros = (int) width - number.length();
          while (zeros-- > 0) {
            sb.append('0');
          }
        }
        sb.append(number);
      } else {
        sb.append(c);
      }
    }
    return sb.toString();
  }

  /**
   * Constructs and returns the next record in the expansion.
   *
   * @throws IOException The name or rdata was invalid after substitutions were performed.
   */
  public Record nextRecord() throws IOException {
    if (current > end) {
      return null;
    }
    String namestr = substitute(namePattern, current);
    Name name = Name.fromString(namestr, origin);
    String rdata = substitute(rdataPattern, current);
    current += step;
    return Record.fromString(name, type, dclass, ttl, rdata, origin);
  }

  /**
   * Constructs and returns all records in the expansion.
   *
   * @throws IOException The name or rdata of a record was invalid after substitutions were
   *     performed.
   */
  public Record[] expand() throws IOException {
    List<Record> list = new ArrayList<>();
    for (long i = start; i < end; i += step) {
      String namestr = substitute(namePattern, current);
      Name name = Name.fromString(namestr, origin);
      String rdata = substitute(rdataPattern, current);
      list.add(Record.fromString(name, type, dclass, ttl, rdata, origin));
    }
    return list.toArray(new Record[0]);
  }

  /**
   * Converts the generate specification to a string containing the corresponding $GENERATE
   * statement.
   */
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("$GENERATE ");
    sb.append(start).append("-").append(end);
    if (step > 1) {
      sb.append("/").append(step);
    }
    sb.append(" ");
    sb.append(namePattern).append(" ");
    sb.append(ttl).append(" ");
    if (dclass != DClass.IN || !Options.check("noPrintIN")) {
      sb.append(DClass.string(dclass)).append(" ");
    }
    sb.append(Type.string(type)).append(" ");
    sb.append(rdataPattern).append(" ");
    return sb.toString();
  }
}
