import java.io.*;
import java.util.*;
import java.text.*;
import java.nio.charset.StandardCharsets;
import java.math.BigInteger;
import java.util.function.Consumer;

public class Asn1Object implements Iterable<Asn1Object> {

  public final static int UNIVERSAL =         0x00; // 0000 0000
  public final static int APPLICATION =       0x40; // 0100 0000
  public final static int CONTEXT =           0x80; // 1000 0000
  public final static int PRIVATE =           0xC0; // 1100 0000

  public final static int CONSTRUCTED =       0x20; // 0010 0000

  public final static int EOC =               0x00;
  public final static int BOOLEAN =           0x01;
  public final static int INTEGER =           0x02;
  public final static int BIT_STRING =        0x03;
  public final static int OCTET_STRING =      0x04;
  public final static int NULL =              0x05;
  public final static int OBJECT_IDENTIFIER = 0x06;
  public final static int OBJECT_DESCRIPTOR = 0x07;
  public final static int EXTERNAL =          0x08;
  public final static int REAL =              0x09;
  public final static int ENUMERATED =        0x0A;
  public final static int EMBEDDED_PDV =      0x0B;
  public final static int UTF8_STRING =       0x0C;
  public final static int SEQUENCE =          0x10;
  public final static int SET =               0x11;
  public final static int NUMERIC_STRING =    0x12;
  public final static int PRINTABLE_STRING =  0x13;
  public final static int T61_STRING =        0x14;
  public final static int VIDEOTEX_STRING =   0x15;
  public final static int IA5_STRING =        0x16;
  public final static int UTC_TIME =          0x17;
  public final static int GENERALIZED_TIME =  0x18;
  public final static int GRAPHIC_STRING =    0x19;
  public final static int VISIBLE_STRING =    0x1A;
  public final static int GENERAL_STRING =    0x1B;
  public final static int UNIVERSAL_STRING =  0x1C;
  public final static int CHARACTER_STRING =  0x1D;
  public final static int BMP_STRING =        0x1E; // 0001 1110

  private boolean indefinite = false, encapsulated = false;
  private int flags, tag, length = 0;
  private byte[] bytes = null;
  private List<Asn1Object> subElements = null;
  private Asn1Object parent = null;

  public Asn1Object(InputStream in) {
    try { 
      read(in);
    } catch (Exception e) { throw new RuntimeException(e); }
  }

  public Asn1Object(byte[] bytes) {
    this(new ByteArrayInputStream(bytes));
    if (headLength() + length != bytes.length) throw new IllegalArgumentException("fragment found");
  }

  public Asn1Object(int flags, int tag) {
    if ((flags & 0xE0) != flags) throw new IllegalArgumentException("flags"); // 1110 0000
    this.flags = flags;
    this.tag = tag;
  }

  public Asn1Object(int flags, int tag, Object obj) {
    this(flags, tag, toBytes(tag, obj));
  }

  public Asn1Object(int flags, int tag, byte[] bytes) {
    this(flags, tag);
    this.bytes = (isConstructed() ? null : bytes);
    this.length = (isConstructed() || bytes == null ? 0 : bytes.length);
    checkEncapsulated();
  }

  public Asn1Object() {
    this(UNIVERSAL, EOC);
  }

  public boolean isConstructed() { return (flags & CONSTRUCTED) != 0; }
  public boolean isEncapsulated() { return encapsulated; }
  public boolean isIndefinite() { return indefinite; }
  public Asn1Object setIndefinite(boolean indefinite) {
    this.indefinite = isConstructed() && indefinite;
    return this;
  }

  public int getFlags() { return flags; }
  public int getTag() { return tag; }
  public int getLength() { return length; }
  public byte[] getValue() { return bytes; }
  public Asn1Object getParent() { return parent; }
  public int headLength() { return tagLength() + lengthLength(); }
  public int tagLength() { return tag < 31 ? 1 : getNum(tag, 7) + 1; }
  public int lengthLength() { return length < 128 || indefinite ? 1 : getNum(length, 8) + 1; }
  public Asn1Object getSubElement(int i) { return subElements.get(i); }
  public int getSubElementsCount() { return subElements == null ? 0 : subElements.size(); }
  public Iterator<Asn1Object> getSubElementsIterator() {
    return subElements == null ? Collections.<Asn1Object>emptyIterator() : subElements.iterator();
  }

  public Asn1Object addAll(Asn1Object... elements) {
    for (Asn1Object o : elements) add(o);
    return this;
  }

  public void add(Asn1Object o) {
    if (o == null) return;
    if (!isConstructed() && !isEncapsulated()) throw new RuntimeException("addition in non constructed or encapsulated");
    //if (!isIndefinite() && o.getFlags() == 0 && o.getTag() == EOC) throw new RuntimeException("addition EOC in non indefinite");
    if (subElements == null) subElements = new ArrayList<>();
    subElements.add(o);
    o.parent = this;
    if (!isEncapsulated()) {
      int l = o.headLength() + o.getLength();
      length += l;
      Asn1Object parent = getParent();
      while (parent != null && !parent.isEncapsulated()) {
        parent.length += l;
        parent = parent.getParent();
      }
    }
  }

  private static int getNum(long value, int bits) {
    int size = 1;
    while ((value >>>= bits) != 0) size++;
    return size;
  }

  private void readTag(InputStream in) throws IOException {
    int data = in.read();
    if (data == -1) throw new EOFException("EOF found");
    flags = data & 0xE0;  // 1110 0000
    tag = data & 0x1F;    // 0001 1111
    if (tag == 0x1F) {
      tag = 0;
      int b = in.read();
      if (b == -1) throw new EOFException("EOF found");
      if ((b & 0x7f) == 0) throw new IOException("invalid tag number");
      while ((b & 0x80) != 0) {
        tag |= (b & 0x7f);  // 0111 1111
        tag <<= 7;
        b = in.read();
        if (b == -1) throw new EOFException("EOF found");
      }
      tag |= (b & 0x7f);
    }
  }

  private void writeTag(OutputStream out) throws IOException {
    if (tag < 31) { // 0x1f  0001 1111
      out.write((byte)(flags | tag));
    } else {
      out.write((byte)(flags | 0x1f));
      if (tag < 128) {  // 0x80  1000 0000
        out.write((byte)tag);
      } else {
        int num = getNum(tag, 7);
        for (int i = (num - 1) * 7; i >= 0; i -= 7) {
          out.write((byte)(i > 0 ? (tag >>> i) | 0x80 : (tag >>> i) & 0x7F));
        }
      }
    }
  }

  private void readLength(InputStream in) throws IOException {
    length = in.read();
    if (length == -1) throw new EOFException("EOF found");
    if (length == 128) { // 0x80  1000 0000 indefinite length encoding
      if (!isConstructed()) throw new IOException("tag can't be indefinite");
      length = 0;
      indefinite = true;
    } else if (length > 127) {  //0x7F 0111 1111
      int num = length & 0x7F;
      if (num > 4) throw new IOException("length more than 4 bytes");
      length = 0;
      for (int i = 0; i < num; i++) {
        int b = in.read();
        if (b == -1) throw new EOFException("EOF found");
        if (i == 0 && b == 0) throw new IOException("invalid tag length");
        length = (length << 8) | (b & 0xFF);
      }
    }
  }

  private void writeLength(OutputStream out) throws IOException {
    if (isConstructed() && indefinite) {
      out.write((byte)0x80);  // 1000 0000
    } else {
      if (length > 127) {  // 0x7F  0111 1111
        int num = getNum(length, 8);
        out.write((byte)(num | 0x80));  // 1000 0000
        for (int i = (num - 1) * 8; i >= 0; i -= 8) {
          out.write((byte)(length >>> i));
        }
      } else {
        out.write((byte)length);
      }
    }
  }
 
  public int effHeadLength() {
    int len = headLength();
    if (!isConstructed()) {
      if (isEncapsulated()) len += prefEncapsulated().length;
      else len += getLength();
    }
    return len;
  }

  public Iterator<Asn1Object> iterator() {
    return new Asn1Iterator(this);
  }

  public void read(InputStream in) throws IOException {
    readTag(in);
    readLength(in);
    if (isConstructed() && (length > 0 || indefinite)) {
      int l = length;
      length = 0;
      for (;;) {
        Asn1Object so = new Asn1Object(in);
        add(so);
        if (!indefinite) {
          if (length == l) break;
          if (length > l) throw new IOException("invalid summary length");
        } else if (so.getFlags() == 0 && so.getTag() == EOC && so.length == 0) break;
      }
    } else if (length > 0) {
      bytes = new byte[length];
      int n = in.read(bytes);
      if (n != length) throw new EOFException("length " + length + " found " + n);
      checkEncapsulated();
    }
  }

  public boolean allowEncapsulated() { return (flags & 0xC0) == UNIVERSAL && (tag == BIT_STRING || tag == OCTET_STRING); }

  private void checkEncapsulated() {
    encapsulated = false;
    if(!isConstructed() && subElements != null) subElements.clear();
    if (!allowEncapsulated() || bytes == null || bytes.length < 2) return;
    byte[] bt;
    if (tag == BIT_STRING) {
      if (bytes[0] != 0) return;
      bt = new byte[bytes.length - prefEncapsulated().length];
      System.arraycopy(bytes, 1, bt, 0, bt.length);
    } else bt = bytes;
    try {
      Asn1Object e = new Asn1Object(bt);
      if (!(e.isConstructed() || e.allowEncapsulated()) || e.isIndefinite()) return;
      encapsulated = true;
      add(e);
    } catch (Exception e) {}
  }

  public byte[] prefEncapsulated() {
    if (allowEncapsulated() && tag == BIT_STRING) return new byte[] { 0 };
    return new byte[0];
  }

  public void write(OutputStream out) throws IOException {
    for (Asn1Object o : this) o.writeObj(out);
  }

  public void writeObj(OutputStream out) throws IOException {
    if (isIndefinite() && isConstructed() &&
       (getSubElementsCount() == 0 || 
       (getSubElementsCount() > 0 && getSubElement(getSubElementsCount() - 1).getTag() != EOC)))
         throw new IOException("EOC missing");
    writeTag(out);
    writeLength(out);
    if (!isConstructed()) {
      if (isEncapsulated()) out.write(prefEncapsulated());
      else if (bytes != null) {
        if (bytes.length != length) throw new IOException("byte array length incorrect");
        out.write(bytes);
      }
    }
  }

  public byte[] getBytes() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try { 
      write(out);
    } catch (Exception e) { throw new RuntimeException(e); }
    byte[] bytes = out.toByteArray();
    return bytes;
  }

  @Override
  public Asn1Object clone() {
    return new Asn1Object(getBytes());
  }

  @Override
  public boolean equals(Object obj) {
    return (obj instanceof Asn1Object && Arrays.equals(getBytes(), ((Asn1Object)obj).getBytes()));
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(getBytes());
  }

  public final static int STDEFAULT = 0;
  public final static int STPREFIX =  1;
  public final static int STVALUE =   2;

  public String valueAsString() { return valueAsString(STDEFAULT); }

  public String valueAsString(int t) {
    String pref = "", ret = "";
    Object obj = valueAsObject();
    if (obj != null) {
      if (t != STVALUE) {
        if (tag == BIT_STRING && !isEncapsulated() && obj instanceof String) pref = "("+((String)obj).length()+" bit)";
        //else if (tag == OCTET_STRING && !isEncapsulated() && obj instanceof String) pref = "("+((String)obj).length() / 2+" byte)";
      }
      if (t != STPREFIX) {
        if (obj instanceof Date) ret = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(obj);
        else ret = obj.toString();
      }
    }
    return pref + (pref.length() > 0 && ret.length() > 0 ? " " : "") + ret;
  }

  public Object valueAsObject() {
    if (isConstructed() || isEncapsulated() || bytes == null) return null;
    try {
      if ((flags & 0xC0) == UNIVERSAL) {
        if (tag == UTF8_STRING) return new String(bytes, StandardCharsets.UTF_8);
        if (tag == NUMERIC_STRING || tag == PRINTABLE_STRING || tag == T61_STRING ||
            tag == VIDEOTEX_STRING || tag == IA5_STRING || tag == VISIBLE_STRING)
              return new String(bytes, StandardCharsets.ISO_8859_1);
        if (tag == BMP_STRING) return new String(bytes, StandardCharsets.UTF_16BE);
        if (tag == BOOLEAN) return Boolean.valueOf(bytes[0] != 0);
        if ((tag == UTC_TIME || tag == GENERALIZED_TIME) && bytes[bytes.length - 1] == 'Z') {
          String f = (tag == GENERALIZED_TIME ? "yy" : "") + "yyMMddHHmmss";
          String s = new String(bytes, StandardCharsets.UTF_8);
          return new SimpleDateFormat(f).parse(s);
        }
        if (tag == INTEGER) return new BigInteger(bytes);
        if (tag == BIT_STRING && bytes.length > 0) {
          int l = (int)bytes[0] & 0xff;
          StringBuilder sb = new StringBuilder();
          for (int i = 8; i < bytes.length * 8 - l; i++) {
            sb.append(((bytes[i / 8] << (i % 8)) & 0x80) > 0 ? "1" : "0");
          }
          return sb.toString();
        }
        if (tag == OBJECT_IDENTIFIER) {
          int count = 0;
          for (byte b : bytes) if ((b & 0x80) == 0) count++;
          long[] data = new long[count+1];
          for (int d = 1, i = 0; i < bytes.length; i++) {
            data[d] = (data[d] << 7) | (bytes[i] & 0x7f);
            if ((bytes[i] & 0x80) == 0) d++;
          }
          data[0] = data[1] < 80 ? data[1] < 40 ? 0 : 1 : 2;
          data[1] = data[1] - data[0] * 40;
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < data.length; i++) {
            if (data[i] < 0) throw new IllegalArgumentException();
            if (i > 0) sb.append(".");
            sb.append(Long.toString(data[i]));
          }
          return sb.toString();
        }
      }
      StringBuilder sb = new StringBuilder();
      for (byte b : bytes) sb.append(String.format("%02X", b));
      return sb.toString();
    } catch (Exception e) {}
    return null;
  }

  public static byte[] toBytes(int tag, Object obj) {
    if (obj instanceof String) {
      String string = (String)obj;
      if (tag == UTF8_STRING) return string.getBytes(StandardCharsets.UTF_8);
      if (tag == NUMERIC_STRING || tag == PRINTABLE_STRING || tag == T61_STRING ||
          tag == VIDEOTEX_STRING || tag == IA5_STRING || tag == VISIBLE_STRING)
            return string.getBytes(StandardCharsets.ISO_8859_1);
      if (tag == BMP_STRING) return string.getBytes(StandardCharsets.UTF_16BE);
      if (tag == OBJECT_IDENTIFIER) {
        String[] s = string.split("\\.");
        long[] l = new long[s.length];
        for (int i = 0; i < s.length; i++) {
          l[i] = Long.parseLong(s[i]);
          if (l[i] < 0) throw new IllegalArgumentException();
        }
        if ((l.length < 2) || (l[0] > 2) || (l[0] < 2 && l[1] >= 40)) throw new IllegalArgumentException();
        l[1] += l[0] * 40;
        int sum = 0, pos = 0;
        for (int i = 1; i < l.length; i++) sum += getNum(l[i], 7);
        byte[] bytes = new byte[sum];
        for (int i = 1; i < l.length; i++) {
          int num = getNum(l[i], 7);
          for (int j = (num - 1) * 7; j >= 0; j -= 7) {
            bytes[pos++] = ((byte)(j > 0 ? (l[i] >>> j) | 0x80 : (l[i] >>> j) & 0x7F));
          }
        }
        return bytes;
      }
    } else if (obj instanceof Boolean && tag == BOOLEAN) {
        return new byte[] { (byte)(((Boolean)obj).booleanValue() ? 0xFF : 0x00) };
    } else if (obj instanceof Date) {
      String f = null;
      if (tag == UTC_TIME) f = "yyMMddHHmmss'Z'";
      else if (tag == GENERALIZED_TIME) f = "yyyyMMddHHmmss'Z'";
      if (f != null) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(f);
        //dateFormat.setTimeZone(new SimpleTimeZone(0, "Z"));
        return dateFormat.format((Date)obj).getBytes(StandardCharsets.UTF_8);
      }
    } else if (obj instanceof Object[]) {
      Object[] array = ((Object[])obj);
      if (tag == BIT_STRING && array.length == 2 && array[0] instanceof Integer && array[1] instanceof byte[]) {
        //if (((Integer)array[0]).intValue() > 7) throw new IllegalArgumentException();
        byte[] bytes = new byte[((byte[])array[1]).length + 1];
        System.arraycopy((byte[])array[1], 0, bytes, 1, ((byte[])array[1]).length);
        bytes[0] = ((Integer)array[0]).byteValue();
        return bytes;
      }
    } else if (obj instanceof Integer && tag == INTEGER) {
      return new BigInteger(((Integer)obj).toString()).toByteArray();
    } else if (obj instanceof Long && tag == INTEGER) {
      return new BigInteger(((Long)obj).toString()).toByteArray();
    } else if (obj instanceof BigInteger && tag == INTEGER) {
      return ((BigInteger)obj).toByteArray();
    }
    throw new IllegalArgumentException("object not defined");
  }

  public String typeName() {
    switch (flags & 0xC0) {  // 1100 0000
      case UNIVERSAL:
        switch (tag) {
          case EOC: return "EOC";
          case BOOLEAN: return "BOOLEAN";
          case INTEGER: return "INTEGER";
          case BIT_STRING: return "BIT STRING";
          case OCTET_STRING: return "OCTET STRING";
          case NULL: return "NULL";
          case OBJECT_IDENTIFIER: return "OBJECT IDENTIFIER";
          case OBJECT_DESCRIPTOR: return "OBJECT DESCRIPTOR";
          case EXTERNAL: return "EXTERNAL";
          case REAL: return "REAL";
          case ENUMERATED: return "ENUMERATED";
          case EMBEDDED_PDV: return "EMBEDDED PDV";
          case UTF8_STRING: return "UTF8 STRING";
          case SEQUENCE: return "SEQUENCE";
          case SET: return "SET";
          case NUMERIC_STRING: return "NUMERIC STRING";
          case PRINTABLE_STRING: return "PRINTABLE STRING";
          case T61_STRING: return "T61 STRING";
          case VIDEOTEX_STRING: return "VIDEOTEX STRING";
          case IA5_STRING: return "IA5 STRING";
          case UTC_TIME: return "UTC TIME";
          case GENERALIZED_TIME: return "GENERALIZED TIME";
          case GRAPHIC_STRING: return "GRAPHIC STRING";
          case VISIBLE_STRING: return "VISIBLE STRING"; // ISO646_STRING
          case GENERAL_STRING: return "GENERAL STRING";
          case UNIVERSAL_STRING: return "UNIVERSAL STRING";
          case CHARACTER_STRING: return "CHARACTER STRING";
          case BMP_STRING: return "BMP STRING";
        }
        return "Universal_" + tag;
      case APPLICATION: return "Application_" + tag;
      case CONTEXT: return "Context_" + tag;
      case PRIVATE: return "Private_" + tag;
    }
    return "unknown";
  }

  @Override
  public String toString() {
    return "[" + String.format("%02X", tag) + "] " + typeName() +
      (isConstructed() ? " (" + getSubElementsCount() + " elem)" : (isEncapsulated() ? " (encapsulates "+getSubElementsCount()+" elem)":"")) +
      " Length: " + headLength() + (isEncapsulated() && prefEncapsulated().length > 0 ? "+" + prefEncapsulated().length : "") + "+" + length +(indefinite ? " (indefinite)" : "");
  }

  public void print() { print(System.out::println); }

  public void print(Consumer<String> action) {
    int offset = 0;
    for (Asn1Object o : this) {
      StringBuilder sb = new StringBuilder();
      Asn1Object parent = o.getParent();
      while (parent != getParent()) {
        sb.append("  ");
        parent = parent.getParent();
      }
      sb.append(o.toString() + " Offset: " + offset);
      String s = o.valueAsString();
      if (s.length() > 0) {
        if (s.length() > 30) s = s.substring(0, 27) + "...";
        sb.append(" Value: " + s);
      }
      //sb.setLength(80);
      action.accept(sb.toString());
      offset += o.effHeadLength();
    }
  }

  private class Asn1Iterator implements Iterator<Asn1Object> {
    private final Stack<ListIterator<Asn1Object>> stack = new Stack<>();
    private Asn1Object next;

    public Asn1Iterator(Asn1Object root) { next = root; }

    public boolean hasNext() { return (next != null); }

    public Asn1Object next() {
      if (!hasNext()) throw new NoSuchElementException();
      Asn1Object o = next;
      if (next.getSubElementsCount() > 0) {
        stack.push(o.subElements.listIterator());
      } else {
        for (;;) {
          if (stack.empty()) { next = null; return o; }
          if (stack.peek().hasNext()) break;
          stack.pop();
        }
      }
      next = stack.peek().next();
      return o;
    }

  } // End of class Asn1Iterator

} // End of class Asn1Object