/* Einfache Klasse, um direkt Eingaben von der Tastatur/
 * Konsole zu lesen. Die Methoden fangen Fehler ab und geben bei
 * falschen Eingaben "Standardwerte", genauer den leeren
 * String, -1 und false, zur&uuml;ck.
 * Benutzung auf eigene Gefahr.
 * @author kleuker
 * Version 1.01 22.11.19
 * Version 1.02 16.07.20
 * Version 1.03 23.06.21
 */

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

/**
 * Diese Klasse ermoeglicht eine recht einfache Ein- und Ausgabe 
 * in Java, dabei wird ein Objekt der Klasse erstellt und deren 
 * Methoden genutzt. Um am Anfang nur wenig Klassen nutzen zu 
 * muessen wurde Funktionalität zur Rueckgabe eines Zufallswertes, 
 * zum Lesen- und Speichern eines Objekts in einer Datei mit fest 
 * vorgegebenen Namen und zur Berechnung eines Strings, der wichtige 
 * Informationen zu einem Objekt zusammenfasst, in dieser Klasse 
 * ergaenzt.
 * @author Kleuker
 *
 */
public class EinUndAusgabe {
  
  private final String DATEINAME = "einObjekt.xml";

  /**
   * Konstruktor zur Erzeugung eines Objekts zur Ein- und Ausgabe.
   */
  public EinUndAusgabe() {
  }

  /**
   * Methode zum Lesen eines Textes von der Konsole, der &uuml;ber die
   * Tastatur eingegeben wird. Die Eingabe endet mit der Return-Taste
   * und darf Leerzeichen enthalten.
   *
   * @return eingegebener Text
   */
  public String leseString() {
      String ergebnis;

      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      try {
          ergebnis = in.readLine();
      } catch (IOException e) {
          ergebnis = "";
      }
      return ergebnis;
  }
  
  /**
   * Methode zum Lesen einer ganzen int-Zahl von der Konsole, die
   * &uuml;ber die Tastatur eingegeben wird. Die Eingabe endet mit der
   * Return-Taste. Sollte es sich bei der Eingabe um keinen
   * g&uuml;ltigen Wert handeln, wird -1 zur&uuml;ckgegeben.
   *
   * @return eingegebene Zahl
   */
  public int leseInteger() {
    int ergebnis;
    try {
      ergebnis = Integer.decode(this.leseString());
    } catch (NumberFormatException e) {
      ergebnis = -1;
    }

    return ergebnis;
  }

  /**
   * Methode zum Lesen einer ganzen byte-Zahl von der Konsole, die
   * &uuml;ber die Tastatur eingegeben wird. Die Eingabe endet mit der
   * Return-Taste. Sollte es sich bei der Eingabe um keinen
   * g&uuml;ltigen Wert handeln, wird -1 zur&uuml;ckgegeben.
   *
   * @return eingegebene Zahl
   */
  public byte leseByte() {
    byte ergebnis;
    try {
      ergebnis = Byte.decode(this.leseString());
    } catch (NumberFormatException e) {
      ergebnis = -1;
    }

    return ergebnis;
  }  
  
    /**
   * Methode zum Lesen einer ganzen long-Zahl von der Konsole, die
   * &uuml;ber die Tastatur eingegeben wird. Die Eingabe endet mit der
   * Return-Taste. Sollte es sich bei der Eingabe um keinen
   * g&uuml;ltigen Wert handeln, wird -1 zur&uuml;ckgegeben.
   *
   * @return eingegebene Zahl
   */
  public long leseLong() {
    long ergebnis;
    try {
      ergebnis = Integer.decode(this.leseString());
    } catch (NumberFormatException e) {
      ergebnis = -1;
    }

    return ergebnis;
  }
  

  /**
   * Methode zum Lesen einer Float-Zahl von der Konsole, die &uuml;ber
   * die Tastatur eingegeben wird. Die Eingabe endet mit der
   * Return-Taste. Sollte es sich bei der Eingabe um keinen
   * g&uuml;ltigen Wert handeln, wird -1 zur&uuml;ckgegeben.
   *
   * @return eingegebene Zahl
   */
  public float leseFloat() {
    float ergebnis;
    try {
      ergebnis = Float.parseFloat(this.leseString());
    } catch (NumberFormatException e) {
      ergebnis = -1f;
    }

    return ergebnis;
  }

  /**
   * Methode zum Lesen einer Double-Zahl von der Konsole, die
   * &uuml;ber die Tastatur eingegeben wird. Die Eingabe endet mit der
   * Return-Taste. Sollte es sich bei der Eingabe um keinen
   * g&uuml;ltigen Wert handeln, wird -1 zur&uuml;ckgegeben.
   *
   * @return eingegebene Zahl
   */
  public double leseDouble() {
    double ergebnis;
    try {
      ergebnis = Double.parseDouble(this.leseString());
    } catch (NumberFormatException e) {
      ergebnis = -1d;
    }

    return ergebnis;
  }

  /**
   * Methode zum Lesen eines Wahrheitswertes von der Konsole, der
   * &uuml;ber die Tastatur eingegeben wird. Die Eingabe endet mit der
   * Return-Taste. Sollte es sich bei der Eingabe nicht um "true"
   * handeln, wird false zur&uuml;ckgegeben.
   *
   * @return eingegebener Wahrheitswert
   */
  public boolean leseBoolean() {
    boolean ergebnis;
    try {
      ergebnis = Boolean.parseBoolean(this.leseString());
    } catch (Exception e) {
      ergebnis = false;
    }

    return ergebnis;
  }

  /**
   * Methode zur Ausgabe eines &uuml;bergebenen Textes. Es wird kein
   * Zeilenumbruch angeh&auml;ngt.
   *
   * @param obj auszugebendes Objekt (nutzt jeweiliges toString)
   */
  public void ausgeben(Object obj) {
    System.out.print(obj.toString());
  }

  /**
   * Methode zur C-formatierten Ausgabe verschiedener Variablen und
   * Textelemente
   *
   * @param text auszugebender Text, der Spezialzeichen wie %d als
   * Platzhalter f&uuml;r auszugebene Variablen enthalten kann
   * @param objekte Objekte, die an Stelle der Platzhalter im Text
   * ausgegeben werden sollen
   */
  public void formatiertAusgeben(String text,
          Object... objekte) {
    System.out.printf(text, objekte);
  }

  /**
   * Methode zur Erzeugung einer ganzahligen Zufallszahl zwischen
   * (einschlie&szlig;lich) den &uuml;bergebenen Grenzen. Es wird
   * erwartet und nicht gepr&uuml;ft, dass der Endwert nicht kleiner
   * als der Startwert ist.
   *
   * @param start minimal m&ouml;glicher Zufallswert
   * @param ende maximal m&ouml;glicher Zufallswert
   * @return zuf&auml;lliger Wert zwischen start und ende (auch diese
   * beiden Werte sind m&ouml;glich
   */
  public int zufall(Integer start, Integer ende) {
    return start
            + (int) (Math.random() * (ende - start + 1));
  }
  
  /** Methode dient zum Speichern eines beliebigen Objekts, dessen
  * Klasse public sein muss, die einen 
  * parameterlosen Konstruktor und fuer jede Objektvariable eine
  * get- sowie set-Methode hat. Bei Problemen wird eine 
  * Fehlermeldung ausgegeben. Bei fehlenden get- und set-Methoden
  * verschwinden die Werte.
  * @param <T> beliebiger Typ des abzuspeichernden Objekts
  * @param objekt zu speicherndes Objekt
  */
  public <T> void speichern(T objekt) {
    try (
        FileOutputStream fos = new FileOutputStream(DATEINAME); 
        XMLEncoder encoder = new XMLEncoder(fos)
    ) {
      Class<?> cl = objekt.getClass();
      if (!cl.getName().startsWith("java") && !cl.getName().startsWith("[")) {
        try {
          cl.getDeclaredConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException 
            | IllegalArgumentException
            | InvocationTargetException | NoSuchMethodException 
            | SecurityException e) {
          System.err.println("Objekte der Klasse " + cl.getName()
              + " so nicht speicherbar, da " + "parameterloser Konstruktor fehlt.");
        }
      }
      encoder.writeObject(objekt);
    } catch (FileNotFoundException e) {
      System.err.println(DATEINAME + " anscheinend nicht nutzbar: " + e);
    } catch (IOException e) {
      System.err.println(DATEINAME + " nicht schreibbar: " + e);
    }
  }

  /** Dient zum Laden des zuletzt gespeicherten Objekts, das
   * zurueckgegeben wird. Bei Problemen ist das Ergebnis null
   * und es wird eine Fehlermeldung ausgegeben.
   * @param <T> Erwarteter Typ der Rueckgabe, der zum Objekttyp passen muss
   * @return eingelesenes Objekt oder null und Ausgabe einer 
   *     Fehlermeldung auf dem Bildschirm
   */
  @SuppressWarnings("unchecked")
  public <T> T laden() {
    try (
        InputStream input = new FileInputStream(DATEINAME); 
        XMLDecoder decoder = new XMLDecoder(input)
    ) {
      Object o = decoder.readObject();
      return (T) o;
    } catch (IOException e) {
      System.err.println("Problem bei Zugriff auf " + DATEINAME 
          + ":" + e);
    } catch (ClassCastException e) {
      System.err.println("Gespeichertes Objekt passt nicht zur"
          + " Zielvariablen: " + e);
    } 
    return null;
  }
  
  /** Methode zur Berechnung eines Strings, der das uebergebene
   * Objekt repraesentiert, dies ist insbesondere dann interessant,
   * wenn die Klasse die Methode toString() nicht ueberschreibt.
   * Generell werden dazu die Werte aller Objekt- und Klassen
   * -variablen ausgegeben. Falls toString() existiert, wird 
   * toString() nicht genutzt, so dass die Methode als Alternative
   * auch bei einer vorhandenen toString()-Methode sein kann.
   * @param p Objekt, fuer das eine Ausgabe als String berechnet
   * werden soll.
   * @return Ausgabe des Objektes als String
   */
  public String alsString(Object p) {
    List<Object> list = new ArrayList<Object>(); // besuchte Objekte
    Map<Object, String> berechnet = new IdentityHashMap<>();
    String erg = alsString(p, list, berechnet);
    return erg.substring(0, erg.length() - 1);
  }

  private String alsString(Object p, List<Object> objects
      , Map<Object, String> berechnet) {
    if (berechnet.containsKey(p)) {
      return berechnet.get(p);
    }
    if (p!= null && objects.contains(p)) {
      String erg =  "...,";
      berechnet.put(p, erg);
      return erg;
    }

    if (p == null) {
      return "null,";
    } else {
      if (p.getClass() == String.class) {
          String erg =  ("\"" + p + "\",");
          berechnet.put(p, erg);
          return erg;
        } else {
          if (p.getClass().isArray()) {
            String erg =  (this.arrayAlsText(p, objects, berechnet) + ",");
            berechnet.put(p, erg);
            return erg;
          } else {
              if (p instanceof Iterable) {
                String erg =  (this.collectionAlsText(p, objects, berechnet) + ",");
                berechnet.put(p, erg);
                return erg;
              } else {
                if (p instanceof Map) {
                  String erg =  (this.mapAlsText(p,  objects, berechnet) + ",");
                  berechnet.put(p, erg);
                  return erg;
                }
              }
            }
        }
    }
    String erg =  this.objektAlsText(p, objects, berechnet) + ",";
    berechnet.put(p, erg);
    return erg;
  }

  private String objektAlsText(Object obj, List<Object> objects
      , Map<Object, String> berechnet) {
    if (berechnet.containsKey(obj)) {
      return berechnet.get(obj);
    }
    if (obj!= null && objects.contains(obj)) {
      String erg =  "...";
      berechnet.put(obj, erg);
      return erg;
    }
    if (obj instanceof Number 
        || obj instanceof String 
        || obj instanceof Character
        || obj instanceof Boolean
        || obj.getClass().isEnum()
        || obj.getClass().isAnnotation()
        || obj.getClass().isSynthetic()
        || obj.getClass().isSealed() 
        ) {
      String erg =  obj + "";
      berechnet.put(obj, erg);
      return erg;
    }
    
    if (klasseNichtZuZeigen(obj.getClass())) {
      if (classHasToString(obj.getClass())) {
        String erg =  "<"+ obj.toString() + ">";
        berechnet.put(obj, erg);
        return erg;
      }
      String erg =  "%" + obj.getClass().getSimpleName() + "%";
      berechnet.put(obj, erg);
      return erg;
    }
    
    objects.add(obj);
    StringBuilder sb = new StringBuilder();
    List<Field> fields = new ArrayList<>();
    Class<?> cl = obj.getClass(); 
    fields.addAll(Arrays.asList(cl.getDeclaredFields()));

    while (cl.getSuperclass() != null) {
      fields.addAll(Arrays.asList(cl.getSuperclass().getDeclaredFields()));
      cl = cl.getSuperclass();
    }

    sb.append("<" + obj.getClass().getSimpleName() + " ");
    for (Field feld : fields) {
      try {
        if (true 
          //  && feld.getType().getModule().isNamed() 
          //  && klasseNichtZuZeigen(feld.getType())
            && feld.trySetAccessible() 
            &&  !feld.isSynthetic()
            ) {
          if (!Arrays.asList(feld.getAnnotations()).toString()
              .contains("@tostringer.annotation.NoShow")) {
            Object wert;
            try {
              wert = feld.get(obj);
            } catch (IllegalArgumentException | IllegalAccessException e) {
              wert = "@unreadable";
            }
            String feldname = feld.getName();
            if (Modifier.isStatic(feld.getModifiers())) {
              feldname = "static " + feldname; // Leerzeichen, damit static am Anfang der Ausgabe
            }
    
            sb.append(feldname + "=" + this.alsString(wert, objects, berechnet));
          }
        }     
      } catch(Exception ex) {
        //System.out.println("EXCEPTION: " + ex);
        sb.append(feld.getName() + "= @noAccess,");
      }
    }
    String erg =  sb.substring(0, sb.length() - 1) + ">";
    berechnet.put(obj, erg);
    return erg;
  }
  
  private boolean classHasToString(Class<?> cl) {
    try {
      cl.getDeclaredMethod("toString");
    } catch (Exception e) {
      return false;
    }
    return true;
  }
  
  private boolean klasseNichtZuZeigen(Class<?> cl) {
    String className = cl.getName();
    if (className.startsWith("gsdet")
            || className.startsWith("java")
            || className.contains("javafx")
            || className.startsWith("bluej")
            || className.contains("__") // bluej intern
            || className.startsWith("nu")
            || className.startsWith("io")
            || className.startsWith("difflib")
            || className.startsWith("nonapi")
            || className.startsWith("junit")
            || className.startsWith("sun")
            || className.startsWith("jdk")
            || className.startsWith("org")
            || className.startsWith("EDU") // Glassfish
            || className.startsWith("jersey") // Glassfish
            || className.startsWith("com") //            
            || className.startsWith("gsdet") // eigene Klassen zur Ueberachung, sonst Endlosrekursion moeglich
            || className.startsWith("ignore")// TODO: alles in kryptisches Paket      
        ){
      //// System.out.println("   verhindert kl: " + cl.getName());
      return true;
    }
    return !modulZuZeigen(cl.getModule());
  }
  
  private boolean modulZuZeigen(Module module) {
    return !(module != null && module.getName() != null && (
          module.getName().startsWith("java.base")
        || module.getName().startsWith("java.instrument")
        || module.getName().startsWith("java.desktop")
        || module.getName().startsWith("java.se")
        || module.getName().startsWith("java.datatransfer")
        || module.getName().startsWith("java.logging")
        ));
  }
  
  private String mapAlsText(Object arr, List<Object> objects
      , Map<Object, String> berechnet) {
    if (berechnet.containsKey(arr)) {
      return berechnet.get(arr);
    }
    if (arr!= null && objects.contains(arr)) {
      String erg =  "...";
      berechnet.put(arr, erg);
      return erg;
    }
    objects.add(arr);
    StringBuilder sb = new StringBuilder("[");
    ((Map<?, ?>) arr).forEach((k, v) -> {
      String key = this.alsString(k, objects, berechnet);
      String val = this.alsString(v, objects, berechnet);
      sb.append("(" + key.substring(0, key.length() - 1) + " -> " 
          + val.substring(0, val.length() - 1) + "),");
    });
    if (sb.substring(sb.length() - 1, sb.length()).equals("[")) { // schraege Pruefung auf leeren Array
      sb.append("]");
      String erg =  sb.toString();
      berechnet.put(arr, erg);
      return erg;
    }
    String erg =  sb.substring(0, sb.length() - 1) + "]";
    berechnet.put(arr, erg);
    return erg;
  }

  private String collectionAlsText(Object arr, List<Object> objects
      , Map<Object, String> berechnet) {
    if (berechnet.containsKey(arr)) {
      return berechnet.get(arr);
    }
    if (arr!= null && objects.contains(arr)) {
      return "...";
    }
    objects.add(arr);
    StringBuilder sb = new StringBuilder("[");
    for (Object p : (Iterable<?>) arr) {
      sb.append(this.alsString(p, objects, berechnet));
    }
    if (sb.substring(sb.length() - 1, sb.length()).equals("[")) { // schraege Pruefung auf leeren Array
      sb.append("]");
      String erg =  sb.toString();
      berechnet.put(arr, erg);
      return erg;
    }
    String erg =  sb.substring(0, sb.length() - 1) + "]";
    berechnet.put(arr, erg);
    return erg;
  }

  private String arrayAlsText(Object arr, List<Object> objects
      , Map<Object, String> berechnet) {
    if (berechnet.containsKey(arr)) {
      return berechnet.get(arr);
    }
    if (arr!= null && objects.contains(arr)) {
      return "...";
    }
    objects.add(arr);
    StringBuilder sb = new StringBuilder("{");
    if (arr instanceof int[]) {
      int[] oarr = (int[]) arr;
      for (int p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof byte[]) {
      byte[] oarr = (byte[]) arr;
      for (byte p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof short[]) {
      short[] oarr = (short[]) arr;
      for (short p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof long[]) {
      long[] oarr = (long[]) arr;
      for (long p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof float[]) {
      float[] oarr = (float[]) arr;
      for (float p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof double[]) {
      double[] oarr = (double[]) arr;
      for (double p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof boolean[]) {
      boolean[] oarr = (boolean[]) arr;
      for (boolean p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    if (arr instanceof char[]) {
      char[] oarr = (char[]) arr;
      for (char p : oarr) {
        sb.append(p + ",");
      }
      if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
        sb.append("}");
        String erg =  sb.toString();
        berechnet.put(arr, erg);
        return erg;
      }
      String erg =  sb.substring(0, sb.length() - 1) + "}";
      berechnet.put(arr, erg);
      return erg;
    }
    Object[] oarr = (Object[]) arr;
    for (Object p : oarr) {
      sb.append(this.alsString(p, objects, berechnet));
    }
    if (sb.substring(sb.length() - 1, sb.length()).equals("{")) { // schraege Pruefung auf leeren Array
      sb.append("}");
      String erg =  sb.toString();
      berechnet.put(arr, erg);
      return erg;
    }
    String erg =  sb.substring(0, sb.length() - 1) + "}";
    berechnet.put(arr, erg);
    return erg;
  }


// rein zu Testzwecken hier stehen gelassen, kann geloescht werden
//  public static void main(String[] s) {
//    EinUndAusgabe eingabe = new EinUndAusgabe();
//    int ein = 0;
//    while (ein != -1) {
//      System.out.print("Text eingeben: ");
//      System.out.println("Eingegeben wurde:" + eingabe.leseString());
//      System.out.print("Float eingeben: ");
//      System.out.println("Eingegeben wurde:" + eingabe.leseFloat());
//      System.out.print("Double eingeben: ");
//      System.out.println("Eingegeben wurde:" + eingabe.leseDouble());
//      System.out.print("Boolean eingeben: ");
//      System.out.println("Eingegeben wurde:" + eingabe.leseBoolean());
//      System.out.print("Byte eingeben: ");
//      System.out.println("Eingegeben wurde:" + eingabe.leseByte());
//      System.out.print("Long eingeben: ");
//      System.out.println("Eingegeben wurde:" + eingabe.leseLong());
//      System.out.print("Ganze Zahl eingeben (Abbruch mit -1): ");
//      ein = eingabe.leseInteger();
//      System.out.println("Eingegeben wurde: " + ein);
//    }
//  }
}
