/*
 * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the Classpath exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.openjdk.btrace.runtime;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.IdentityHashMap;
import java.util.Map;

/**
 * This class serializes an object to XML. This class handles circular references by generating ID
 * and IDREF attributes.
 *
 * @author A. Sundararajan
 */
final class XMLSerializer {
  private static final String ID = "id";
  private static final String IDREF = "idref";
  private static final String CLASS = "class";
  // do not create me!
  private XMLSerializer() {}

  /** Write an object as an XML document to the out. */
  public static void write(Object obj, Writer out) throws IOException {
    if (obj == null || out == null) {
      throw new NullPointerException();
    }
    Serializer s = new Serializer(out);
    s.write(obj);
    out.flush();
  }

  /** Return XML document string for the given object. */
  public static String toXML(Object obj) throws IOException {
    if (obj == null) {
      throw new NullPointerException();
    }
    StringWriter sw = new StringWriter();
    write(obj, sw);
    return sw.toString();
  }

  public static void main(String[] args) throws IOException {
    if (args.length > 0) {
      System.out.println(toXML(args));
    } else {
      System.out.println(toXML(XMLSerializer.class));
    }
  }

  // class that handles XML serialization
  private static class Serializer {
    private static final CharsetEncoder encoder = Charset.forName("8859_1").newEncoder();

    // out on which we will write XML
    private final PrintWriter out;
    // map to maintain objects serialized already
    private final Map<Object, String> objToId;
    // next object id (unique id for objects)
    private long nextId;

    Serializer(Writer writer) {
      if (writer instanceof PrintWriter) {
        out = (PrintWriter) writer;
      } else {
        out = new PrintWriter(writer);
      }
      objToId = new IdentityHashMap<>();
      writeln("<?xml version='1.0' encoding='ISO-8859-1'?>");
    }

    private static Field[] getAllFields(Class clazz) {
      return AccessController.doPrivileged(
          (PrivilegedAction<Field[]>)
              () -> {
                try {
                  Field[] fields = clazz.getDeclaredFields();
                  for (Field f : fields) {
                    f.setAccessible(true);
                  }
                  return fields;
                } catch (RuntimeException re) {
                  throw re;
                } catch (Exception exp) {
                  throw new RuntimeException(exp);
                }
              });
    }

    private static String encodeTagName(String str) {
      return str.replace("$", "d-");
    }

    private static String quote(int code) {
      switch (code) {
        case '&':
          return "&amp;";
        case '<':
          return "&lt;";
        case '>':
          return "&gt;";
        case '"':
          return "&quot;";
        case '\'':
          return "&apos;";
          // case '\r': return "&#13;";
        default:
          return null;
      }
    }

    private static boolean isValidCharCode(int code) {
      return (0x0020 <= code && code <= 0xD7FF)
          || (0x000A == code)
          || (0x0009 == code)
          || (0x000D == code)
          || (0xE000 <= code && code <= 0xFFFD)
          || (0x10000 <= code && code <= 0x10ffff);
    }

    private static String encodeCode(int code) {
      return "<char cp=\"#" + Integer.toString(code, 16) + "\"/>";
    }

    private static String encodeText(char ch) {
      return encodeText(new char[] {ch});
    }

    private static String encodeText(char[] array) {
      return encodeText(new String(array));
    }

    private static String encodeText(String string) {
      StringBuilder sb = new StringBuilder();
      int index = 0;
      int len = string.length();
      while (index < len) {
        int point = string.codePointAt(index);
        int count = Character.charCount(point);
        if (isValidCharCode(point)) {
          if (encoder.canEncode(string.substring(index, index + count))) {
            String value = quote(point);
            if (value != null) {
              sb.append(value);
            } else {
              sb.appendCodePoint(point);
            }
          } else {
            sb.append("&#x");
            sb.append(Integer.toString(point, 16));
            sb.append(';');
          }
          index += count;
        } else {
          sb.append(encodeCode(string.charAt(index)));
          index++;
        }
      }
      return sb.toString();
    }

    void write(Object obj) {
      if (obj instanceof Class) {
        write("class", obj);
      } else {
        write("object", obj);
      }
    }

    void write(String name, Object obj) {
      name = encodeTagName(name);
      // check null
      if (obj == null) {
        out.print('<');
        out.print(name);
        out.print(">null</");
        out.print(name);
        writeln('>');
        return;
      }

      // check for circularity
      if (hasSeenAlready(name, obj)) {
        return;
      }

      Class clazz = obj.getClass();
      if (clazz.isArray()) {
        writeArray(name, obj);
      } else {
        writeObject(name, obj);
      }
    }

    private String nextObjectId(Object obj) {
      return Long.toString(nextId++);
    }

    private void writeln(String str) {
      out.print(str);
      out.print("\r\n");
    }

    private void writeln(char ch) {
      out.print(ch);
      out.print("\r\n");
    }

    private void writeln() {
      out.print("\r\n");
    }

    private void writeAttribute(String name, String value) {
      out.print(name);
      out.print("=\"");
      out.print(value);
      out.print("\"");
    }

    private void writeIdProperty(Object obj) {
      String id = nextObjectId(obj);
      writeAttribute(ID, id);
      objToId.put(obj, id);
    }

    private boolean hasSeenAlready(String name, Object obj) {
      String id = objToId.get(obj);
      if (id != null) {
        out.print('<');
        out.print(name);
        out.print(' ');
        writeAttribute(IDREF, id);
        writeln("/>");
        return true;
      } else {
        return false;
      }
    }

    private void arrayStart(String name, Object obj) {
      objectStart(name, obj);
    }

    private void arrayEnd(String name, Object obj) {
      objectEnd(name, obj);
    }

    private void writeArray(String name, Object array) {
      arrayStart(name, array);
      int len = Array.getLength(array);
      if (len == 0) {
        arrayEnd(name, array);
        return;
      }
      Class clazz = array.getClass().getComponentType();
      writeln("<elements>");
      if (clazz.isPrimitive()) {
        if (clazz == Character.TYPE) {
          out.print(encodeText((char[]) array));
        } else {
          for (int index = 0; index < len; index++) {
            out.print(Array.get(array, index));
            if (index != len - 1) {
              out.print(", ");
            }
          }
        }
        writeln();
      } else {
        for (int index = 0; index < len; index++) {
          write("li", Array.get(array, index));
        }
      }
      writeln("</elements>");
      arrayEnd(name, array);
    }

    private void objectStart(String name, Object obj) {
      out.print('<');
      out.print(name);
      out.print(' ');
      writeIdProperty(obj);
      writeln('>');
      write(CLASS, obj.getClass());
    }

    private void objectEnd(String name, Object obj) {
      out.print("</");
      out.print(name);
      writeln('>');
    }

    private void writeObject(String name, Object obj) {
      objectStart(name, obj);
      if (obj instanceof Class) {
        Class clazz = (Class) obj;
        writeStaticFields(clazz);
        write("name", clazz.getName());
        Object loader = clazz.getClassLoader();
        if (loader != null) {
          write("loader", loader);
        }
        Object protDomain = clazz.getProtectionDomain();
        if (protDomain != null) {
          write("protectionDomain", protDomain);
        }
        Object[] signers = clazz.getSigners();
        if (signers != null && signers.length > 0) {
          write("signers", signers);
        }
        Class sc = clazz.getSuperclass();
        if (sc != null) {
          writeln("<extends>");
          write("class", sc);
          writeln("</extends>");
        }
        Class[] interfaces = clazz.getInterfaces();
        if (interfaces != null && interfaces.length > 0) {
          writeln("<implements>");
          for (Class cl : interfaces) {
            write("class", cl);
          }
          writeln("</implements>");
        }
      } else {
        Class clazz = obj.getClass();
        while (clazz != null) {
          writeFields(obj, clazz);
          clazz = clazz.getSuperclass();
        }
      }
      objectEnd(name, obj);
    }

    private void writeStaticFields(Class clazz) {
      Field[] fields = getAllFields(clazz);
      if (fields.length == 0) {
        return;
      }
      writeln("<fields>");
      for (Field f : fields) {
        int modifiers = f.getModifiers();
        if (!Modifier.isStatic(modifiers)) {
          continue;
        }
        writeField(f, null);
      }
      writeln("</fields>");
    }

    private void writeFields(Object obj, Class clazz) {
      Field[] fields = getAllFields(clazz);
      for (Field f : fields) {
        int modifiers = f.getModifiers();
        if (Modifier.isStatic(modifiers)) {
          continue;
        }
        writeField(f, obj);
      }
    }

    private void writeField(Field f, Object obj) {
      Class type = f.getType();
      try {
        if (type.isPrimitive()) {
          String name = encodeTagName(f.getName());
          String value;
          if (type == Character.TYPE) {
            value = encodeText(f.getChar(obj));
          } else {
            value = f.get(obj).toString();
          }
          out.print("<");
          out.print(name);
          out.print(">");
          out.print(value);
          out.print("</");
          out.print(name);
          writeln('>');
        } else {
          write(f.getName(), f.get(obj));
        }
      } catch (RuntimeException re) {
        throw re;
      } catch (Exception exp) {
        throw new RuntimeException(exp);
      }
    }
  }
}
