/**********************************************************************
 *
 * This file is part of HBCI4Java.
 * Copyright (c) Olaf Willuhn
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 **********************************************************************/

package org.kapott.hbci.manager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Implementierung des Flicker-Codes fuer optisches ChipTAN.
 * Basiert auf der Javascript-Implementierung von
 * http://6xq.net/media/00/20/flickercode.html
 * 
 * Die Javascript-Implementierung war jedoch nicht mehr aktuell (basiert auf HHD 1.3).
 */
public class FlickerCode
{
  /**
   * Versionskennung.
   */
  public static enum HHDVersion
  {
    /**
     * HHD-Version 1.4
     */
    HHD14(org.kapott.hbci.manager.HHDVersion.QR_1_4,
          org.kapott.hbci.manager.HHDVersion.HHD_1_4,
          org.kapott.hbci.manager.HHDVersion.MS_1),
    
    /**
     * HHD-Version 1.3
     */
    HHD13(org.kapott.hbci.manager.HHDVersion.QR_1_3,
          org.kapott.hbci.manager.HHDVersion.HHD_1_3,
          org.kapott.hbci.manager.HHDVersion.HHD_1_2),
    
    ;
      
    private List<org.kapott.hbci.manager.HHDVersion> assigned = new ArrayList<>();
      
    /**
     * ct.
     * @param assigned Liste der zugeordneten HHD-Versionen.
     */
    private HHDVersion(org.kapott.hbci.manager.HHDVersion... assigned)
    {
      this.assigned = Arrays.asList(assigned);
    }
    
    /**
     * Versucht die HHD-Version zu ermitteln.
     * @param h die HHD-Version.
     * @return das korrespondierende interne Enum.
     */
    private static HHDVersion find(org.kapott.hbci.manager.HHDVersion h)
    {
      for (HHDVersion v:values())
      {
        if (v.assigned.contains(h))
          return v;
      }
      return null;
    }
  }
  
  /**
   * Das Encoding der Nutzdaten.
   */
  public static enum Encoding
  {
    /**
     * ASC-Encoding.
     */
    ASC,
    
    /**
     * BCD-Encoding.
     */
    BCD,
  }
  

  /**
   * Die Anzahl der Bytes, in der die Laenge des Challenge bei HHD 1.4 steht.
   * Bei HHD 1.3 war das noch 2 Zeichen lang.
   * Wenn der Flicker-Code nicht in "Challenge HHDuc" uebertragen wurde
   * sondern direkt im Freitext-Challenge, koennen wir das Problem umgehen,
   * indem wir in clean() einfach eine "0" vorn anhaengen.
   * Wenn er aber tatsaechlich im "Challenge HHDuc" steht, kann man dem
   * Code nicht ansehen, ob es ein HHD 1.3-Code ist. In dem Fall hilft nur
   * Try&Error. Also mit HHD 1.4 parsen. Und wenn das fehlschlaegt, dann
   * HHD 1.3 versuchen.
   */
  private final static int LC_LENGTH_HHD14 = 3;
  
  /**
   * Die Anzahl der Bytes, in der die Laenge des Challenge bei HHD 1.3 steht.
   */
  private final static int LC_LENGTH_HHD13 = 2;
  
  /**
   * Default-Laenge der LDE-Laengen-Angabe.
   */
  private final static int LDE_LENGTH_DEFAULT = 2; 

  /**
   * Fallback-Laenge der LDE-Laengen-Angabe bei der Sparda.
   */
  private final static int LDE_LENGTH_SPARDA = 3;

  /**
   * Die Position des Bits, welches das Encoding enthaelt.
   */
  private final static int BIT_ENCODING = 6;
  
  /**
   * Die Position des Bits, welches festlegt, ob ein Controlbyte folgt.
   */
  private final static int BIT_CONTROLBYTE = 7;
  
  /**
   * Die HHD-Version.
   */
  public HHDVersion version = null;
  
  /**
   * Laenge des gesamten Codes.
   */
  public int lc           = 0;
  
  /**
   * Der Startcode.
   */
  public Startcode startCode = new Startcode();

  /**
   * Datenelement 1.
   */
  public DE de1           = new DE();
  
  /**
   * Datenelement 2.
   */
  public DE de2           = new DE();

  /**
   * Datenelement 3.
   */
  public DE de3           = new DE();
  
  /**
   * Der Rest des Codes. Mit dem koennen wir nichts anfangen
   */
  public String rest      = null;

  /**
   * Versucht, aus Challenge und Challenge HHDuc den Flicker-Code zu extrahieren
   * und ihn in einen flickerfaehigen Code umzuwandeln.
   * Nur wenn tatsaechlich ein gueltiger Code enthalten ist, der als
   * HHDuc-Code geparst und in einen Flicker-Code umgewandelt werden konnte,
   * liefert die Funktion den Code. Sonst immer NULL.
   * @param challenge der Challenge-Text. Das DE "Challenge HHDuc" gibt es
   * erst seit HITAN4. Einige Banken haben aber schon vorher optisches chipTAN
   * gemacht. Die haben das HHDuc dann direkt im Freitext des Challenge
   * mitgeschickt (mit String-Tokens zum Extrahieren markiert). Die werden vom
   * FlickerCode-Parser auch unterstuetzt.
   * @param hhduc das echte Challenge HHDuc.
   * @return der geparste Flickercode oder NULL.
   */
  public static FlickerCode tryParse(String challenge, String hhduc)
  {
    return tryParse(null,challenge,hhduc);
  }

  /**
   * Versucht, aus Challenge und Challenge HHDuc den Flicker-Code zu extrahieren
   * und ihn in einen flickerfaehigen Code umzuwandeln.
   * Nur wenn tatsaechlich ein gueltiger Code enthalten ist, der als
   * HHDuc-Code geparst und in einen Flicker-Code umgewandelt werden konnte,
   * liefert die Funktion den Code. Sonst immer NULL.
   * @param hhd die HHD-Version. Kann NULL sein.
   * @param challenge der Challenge-Text. Das DE "Challenge HHDuc" gibt es
   * erst seit HITAN4. Einige Banken haben aber schon vorher optisches chipTAN
   * gemacht. Die haben das HHDuc dann direkt im Freitext des Challenge
   * mitgeschickt (mit String-Tokens zum Extrahieren markiert). Die werden vom
   * FlickerCode-Parser auch unterstuetzt.
   * @param hhduc das echte Challenge HHDuc.
   * @return der geparste Flickercode oder NULL.
   */
  public static FlickerCode tryParse(org.kapott.hbci.manager.HHDVersion hhd, String challenge, String hhduc)
  {
      // 1. Prioritaet hat hhduc. Gibts aber erst seit HITAN4
      if (hhduc != null && hhduc.trim().length() > 0)
      {
        try
        {
          FlickerCode code = new FlickerCode(hhd,hhduc);
          code.render(); // testweise rendern
          return code;
        }
        catch (Exception e)
        {
          HBCIUtils.log("unable to parse Challenge HHDuc " + hhduc + ":" + HBCIUtils.exception2String(e),HBCIUtils.LOG_DEBUG);
        }
      }
      
      // 2. Checken, ob im Freitext-Challenge was parse-faehiges steht.
      // Kann seit HITAN1 auftreten
      if (challenge != null && challenge.trim().length() > 0)
      {
        try
        {
          FlickerCode code = new FlickerCode(hhd,challenge);
          code.render(); // testweise rendern
          return code;
        }
        catch (Exception e)
        {
          // Das darf durchaus vorkommen, weil das Challenge auch bei manuellem
          // chipTAN- und smsTAN Verfahren verwendet wird, wo gar kein Flicker-Code enthalten ist.
          // Wir loggen es aber trotzdem - fuer den Fall, dass tatsaechlich ein Flicker-Code
          // enthalten ist. Sonst koennen wir das nicht debuggen.
          HBCIUtils.log("challenge contains no HHDuc (no problem in most cases):" + HBCIUtils.exception2String(e),HBCIUtils.LOG_DEBUG2);
        }
      }
      // Ne, definitiv kein Flicker-Code.
      return null;
  }
  
  /**
   * ct.
   * Parameterloser Konstruktor zum manuellen Zusammenstecken eines Codes.
   */
  public FlickerCode()
  {
  }
  
  /**
   * ct.
   * Parst den HHDuc-Code aus dem uebergebenen Code.
   * @param code der zu parsende Code.
   */
  public FlickerCode(String code)
  {
    this(null,code);
  }

  /**
   * ct.
   * Parst den HHDuc-Code aus dem uebergebenen Code.
   * @param hhd die HHD-Version. Kann NULL sein.
   * @param code der zu parsende Code.
   */
  public FlickerCode(org.kapott.hbci.manager.HHDVersion hhd, String code)
  {
    // Wenn eine Version angegeben ist, versuchen wir es mit der
    if (hhd != null)
    {
        try
        {
            HHDVersion v = HHDVersion.find(hhd);
            if (v != null)
                parse(code,v);
            
            // Wenn keine Exception geflogen ist, koennte es die richtige Version gewesen sein.
            return;
        }
        catch (Exception e)
        {
          // Die HHD-Version war explizit angegeben, liess sich damit aber nicht parsen? Sehr verdaechtig
          HBCIUtils.log("unable to parse code " + code + " as " + hhd + ": " + HBCIUtils.exception2String(e),HBCIUtils.LOG_DEBUG);
        }
    }
    
    // Wir versuchen es erstmal als HHD 1.4
    try
    {
      try
      {
        parse(code,HHDVersion.HHD14);
      }
      catch (Exception e)
      {
        // Wir versuchen den Sparda-Workaround mit 3 Zeichen langem LDE
        parse(code,HHDVersion.HHD14,LDE_LENGTH_SPARDA);
      }
    }
    catch (Exception e)
    {
      // OK, dann HHD 1.3
      parse(code,HHDVersion.HHD13);
    }
  }

  /**
   * Parst den Code mit der angegebenen HHD-Version.
   * @param code der zu parsende Code.
   * @param version die HHD-Version.
   */
  private void parse(String code, HHDVersion version)
  {
    this.parse(code,version,LDE_LENGTH_DEFAULT);
  }

  /**
   * Parst den Code mit der angegebenen HHD-Version.
   * @param code der zu parsende Code.
   * @param version die HHD-Version.
   * @param ldeLen explizite Angabe der Laenge des LDE.
   */
  private void parse(String code, HHDVersion version, int ldeLen)
  {
    reset();
    code = clean(code);
    
    // 1. LC ermitteln. Banales ASCII
    {
      int len = version == HHDVersion.HHD14 ? LC_LENGTH_HHD14 : LC_LENGTH_HHD13;
      this.lc = Integer.parseInt(code.substring(0,len));
      code = code.substring(len); // und abschneiden
    }

    // 2. Startcode/Control-Bytes
    code = this.startCode.parse(code);
    
    // 3. LDE/DE 1-3
    code = this.de1.parse(code,ldeLen);
    code = this.de2.parse(code,ldeLen);
    code = this.de3.parse(code,ldeLen);

    // 4. Den Rest speichern wir hier.
    this.rest = code.length() > 0 ? code : null;
  }
  
  /**
   * Entfernt das CHLGUC0026....CHLGTEXT aus dem Code, falls vorhanden.
   * Das sind HHD 1.3-Codes, die nicht im "Challenge HHDuc" uebertragen
   * wurden sondern direkt im Challenge-Freitext,
   * @param code
   * @return
   */
  private String clean(String code)
  {
    code = code.replaceAll(" ",""); // Alle Leerzeichen entfernen
    code = code.trim();             // Whitespaces entfernen
    
    // Jetzt checken, ob die beiden Tokens enthalten sind
    int t1Start = code.indexOf("CHLGUC");
    int t2Start = code.indexOf("CHLGTEXT");
    if (t1Start == -1 || t2Start == -1 || t2Start <= t1Start)
      return code; // Ne, nicht enthalten
    
    // Erstmal den 2. Token abschneiden
    code = code.substring(0,t2Start);
    
    // Dann alles abschneiden bis zum Beginn von "CHLGUC"
    code = code.substring(t1Start);

    // Wir haben eigentlich nicht nur "CHLGUC" sondern "CHLGUC0026"
    // Wobei die 4 Zahlen sicher variieren koennen. Wir schneiden einfach alles ab.
    code = code.substring(10);
    
    // Jetzt vorn noch ne "0" dran haengen, damit LC wieder 3-stellig ist - wie bei HHD 1.4
    return "0" + code;
  }

  /**
   * Rendert den flickerfaehigen Code aus dem Challenge im HHD-Format.
   * @return der neu generierte Flicker-Code.
   */
  public String render()
  {
    // 1. Payload ermitteln
    String s = createPayload();

    // 2. Luhn-Checksumme neu berechnen
    String luhn = createLuhnChecksum();
    
    // 3. XOR-Checksumme neu berechnen
    String xor = createXORChecksum(s);
    
    // 4. Alles zusammenbauen und zurueckliefern
    return s + luhn + xor;
  }
  
  /**
   * Generiert den Payload neu.
   * Das ist der komplette Code, jedoch ohne Pruefziffern am Ende.
   * @return der neu generierte Payload.
   */
  private String createPayload()
  {
    StringBuffer sb  = new StringBuffer();
    
    // 1. Laenge Startcode
    sb.append(this.startCode.renderLength());
    
    // 2. Die Control-Bytes
    for (Integer i:this.startCode.controlBytes)
    {
      sb.append(toHex(i,2));
    }

    // 3. Der Startcode
    sb.append(this.startCode.renderData());

    // 4. DEs anhaengen.
    DE[] deList = new DE[]{this.de1,this.de2,this.de3};
    for (int i=0;i<deList.length;++i)
    {
      DE de = deList[i];
      sb.append(de.renderLength());
      sb.append(de.renderData());
    }
    
    String s = sb.toString();
    
    // 5. Laenge neu berechnen und vorn dran haengen
    int len = s.length();
    len += 2; // die zwei Zeichen am Ende mit den Pruefsummen muessen wir noch mit reinrechnen. 
    len = len / 2; // Anzahl der Bytes. Jedes Byte sind 2 Zeichen.
    String lc = toHex(len,2);
    
    return (lc + s);
  }
  
  /**
   * Berechnet die XOR-Checksumme fuer den Code neu.
   * @param der Payload.
   * @return die XOR-Checksumme im Hex-Format.
   */
  private String createXORChecksum(String payload)
  {
    int xorsum = 0;
    for (int i=0; i<payload.length(); ++i)
    {
      xorsum ^= Integer.parseInt(Character.toString(payload.charAt(i)),16);
    }
    return toHex(xorsum,1);
  }

  /**
   * Berechnet die Luhn-Pruefziffer neu.
   * @return die Pruefziffer im Hex-Format.
   */
  private String createLuhnChecksum()
  {
    ////////////////////////////////////////////////////////////////////////////
    // Schritt 1: Payload ermitteln
    StringBuffer sb = new StringBuffer();
    
    // a) Controlbytes
    for (Integer i:this.startCode.controlBytes)
      sb.append(toHex(i,2));
    
    // b) Startcode
    sb.append(this.startCode.renderData());
    
    // c) DEs
    if (this.de1.data != null) sb.append(this.de1.renderData());
    if (this.de2.data != null) sb.append(this.de2.renderData());
    if (this.de3.data != null) sb.append(this.de3.renderData());
    
    String payload = sb.toString();
    
    //
    ////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////////////////////
    // Schritt 2: Pruefziffer berechnen
    int luhnsum = 0;
    int i = 0;
    for (i=0; i<payload.length(); i+=2)
    {
      luhnsum += (1*Integer.parseInt(Character.toString(payload.charAt(i)),16)) + 
                 quersumme(2*Integer.parseInt(Character.toString(payload.charAt(i+1)),16));
    }
    
    // Ermittelt, wieviel zu "luhnsum" addiert werden muss, um auf die
    // naechste Zahl zu kommen, die durch 10 teilbar ist
    // Beispiel:
    // luhnsum = 129 modulo 10 -> 9
    // 10 - 9 = 1
    // also 129 + 1 = 130
    int mod = luhnsum % 10;
    if (mod == 0)
      return "0"; // Siehe "Schritt 3" in tan_hhd_uc_v14.pdf, Seite 17
    
    int rest = 10 - mod;
    int sum = luhnsum + rest;
    
    // Von dieser Summe ziehen wir die berechnete Summe ab
    // Beispiel:
    // 130 - 129 = 1
    // 1 -> ist die Luhn-Checksumme.
    int luhn = sum - luhnsum;
    return toHex(luhn,1);
    //
    ////////////////////////////////////////////////////////////////////////////
  }

  /**
   * @see java.lang.Object#toString()
   */
  public String toString()
  {
    StringBuffer sb = new StringBuffer();
    sb.append("VERSION:\n" + this.version + "\n");
    sb.append("LC: " + this.lc + "\n");
    sb.append("Startcode:\n" + this.startCode + "\n");
    sb.append("DE1:\n" + this.de1 + "\n");
    sb.append("DE2:\n" + this.de2 + "\n");
    sb.append("DE3:\n" + this.de3 + "\n");
    sb.append("CB : " + this.rest + "\n");
    return sb.toString();
  }
  
  /**
   * Resettet den Code.
   */
  private void reset()
  {
    this.version   = null;
    this.lc        = 0;
    this.startCode = new Startcode();
    this.de1       = new DE();
    this.de2       = new DE();
    this.de3       = new DE();
    this.rest      = null;
  }
  

  /**
   * @see java.lang.Object#equals(java.lang.Object)
   */
  public boolean equals(Object obj)
  {
    if (!(obj instanceof FlickerCode))
      return false;
    FlickerCode other = (FlickerCode) obj;
    
    if (this.lc != other.lc)                     return false;
    if (!this.startCode.equals(other.startCode)) return false;
    if (!this.de1.equals(other.de1))             return false;
    if (!this.de2.equals(other.de2))             return false;
    if (!this.de3.equals(other.de3))             return false;
    
    if (this.rest == null)
      return (other.rest == null);
    return this.rest.equals(other.rest);
  }

  
  
  /**
   * Bean fuer die Eigenschaften eines einzelnen DE.
   */
  public class DE
  {
    /**
     * Die tatsaechliche Laenge des DE.
     * Bereinigt um ggf. vorhandene Control-Bits.
     */
    public int length = 0;
    
    /**
     * Die Laengen-Angabe des DE im Roh-Format.
     * Sie kann noch Control-Bits enthalten, sollte daher
     * also NICHT fuer Laengenberechnungen verwendet werden.
     * In dem Fall stattdessen <code>length</code> verwenden.
     */
    public int lde    = 0;
    
    /**
     * Die Laenge des LDE.
     */
    public int ldeLen = 0;
    
    /**
     * Das Encoding der Nutzdaten.
     * Per Definition ist im Challenge HHDuc dieses Bit noch NICHT gesetzt.
     * Das Encoding passiert erst beim Rendering.
     */
    public Encoding encoding = null;
    
    /**
     * Die eigentlichen Nutzdaten des DE.
     */
    public String data  = null;

    /**
     * Parst das DE am Beginn des uebergebenen Strings.
     * @param s der String, dessen Anfang das DE enthaelt.
     * @return der Reststring.
     */
    String parse(String s)
    {
      return this.parse(s,LDE_LENGTH_DEFAULT);
    }

    /**
     * Parst das DE am Beginn des uebergebenen Strings.
     * @param s der String, dessen Anfang das DE enthaelt.
     * @param ldeLen explizite Angabe der Laenge des LDE.
     * @return der Reststring.
     */
    String parse(String s, int ldeLen)
    {
      // Nichts mehr zum Parsen da
      if (s == null || s.length() == 0)
        return s;

      // LDE ermitteln (dezimal)
      this.lde = Integer.parseInt(s.substring(0,ldeLen));
      s = s.substring(ldeLen); // und abschneiden
      
      this.ldeLen = ldeLen;

      // Control-Bits abschneiden. Die Laengen-Angabe steht nur in den Bits 0-5.
      // In den Bits 6 und 7 stehen Steuer-Informationen
      this.length  = getBitSum(this.lde,5); // Bit 0-5
      
      // Encoding gibts hier noch nicht. Das passiert erst beim Rendern
      
      // Nutzdaten ermitteln
      this.data = s.substring(0,this.length);
      s = s.substring(this.length); // und abschneiden
    
      return s;
    }

    /**
     * Rendert die Laengenangabe fuer die Uebertragung via Flickercode.
     * @return die codierten Nutzdaten.
     * Wenn das DE keine Nutzdaten enthaelt, wird "" zurueck gegeben.
     */
    String renderLength()
    {
      // Keine Daten enthalten. Dann muessen wir auch nichts weiter
      // beruecksichtigen.
      // Laut Belegungsrichtlinien TANve1.4  mit Erratum 1-3 final version vom 2010-11-12.pdf
      // duerfen im "ChallengeHHDuc" eigentlich keine leeren DEs enthalten
      // sein. Daher geben wir in dem Fall "" zurueck und nicht "00" wie in
      // tan_hhd_uc_v14.pdf angegeben. Denn mit "00" wollte es mein TAN-Generator nicht
      // lesen. Kann aber auch sein, dass der einfach nicht HHD 1.4 tauglich ist
      if (this.data == null)
        return "";

      Encoding enc = this.getEncoding();

      // Die wollen die Anzahl der Bytes, nicht die Laenge der Zeichen
      int len = renderData().length() / 2;
      
      // A) BCD -> Muss nichts weiter codiert werden.
      if (enc == Encoding.BCD)
        return toHex(len,2);

      // B) ASC -> Encoding-Bit reincodieren
      // HHD 1.4 -> in das Bit-Feld codieren
      if (FlickerCode.this.version == HHDVersion.HHD14)
      {
        len = len + (1 << BIT_ENCODING);
        return toHex(len,2);
      }

      // HHD 1.3 -> nur ne 1 im linken Halbbyte schicken
      return "1" + toHex(len,1);
    }
    
    /**
     * Liefert das zu verwendende Encoding fuer die Uebertragung via Flickercode.
     * Im Normalfall (also nach dem Parsen eines HHDuc) ist kein Encoding angegeben
     * (im Challenge HHDuc ist das per Definition nie gesetzt) machen wir ASC.
     * Es sei denn, das Encoding wurde explizit auf BCD gesetzt.
     * @return das fuer das Rendering zu verwendende Encoding.
     */
    Encoding getEncoding()
    {
      if (this.data == null)
        return Encoding.BCD;
      
      // Explizit angegeben
      if (this.encoding != null)
        return this.encoding;
      
      // Siehe tan_hhd_uc_v14.pdf, letzter Absatz in B.2.3
      // Bei SEPA-Auftraegen koennen auch Buchstaben in BIC/IBAN vorkommen.
      // In dem Fall muss auch ASC-codiert werden. Also machen wir BCD nur
      // noch dann, wenn ausschliesslich Zahlen drin stehen.
      // Das macht subsembly auch so
      // http://www.onlinebanking-forum.de/phpBB2/viewtopic.php?p=75602#75602
      if (this.data.matches("[0-9]{1,}"))
        return Encoding.BCD;
      
      return Encoding.ASC;
    }
    
    /**
     * Rendert die Nutzdaten fuer die Uebertragung via Flickercode.
     * @return die codierten Nutzdaten.
     * Wenn das DE keine Nutzdaten enthaelt, wird "" zurueck gegeben.
     */
    String renderData()
    {
      if (this.data == null)
        return "";

      Encoding enc = this.getEncoding();
      if (enc == Encoding.ASC)
        return toHex(this.data);

      // Bei BCD-Encoding noch mit "F" auf Byte-Grenze ergenzen
      String s = this.data;
      if (s.length() % 2 == 1)
        s += "F";
      
      return s;
    }
    
    /**
     * @see java.lang.Object#toString()
     */
    public String toString()
    {
      StringBuffer sb = new StringBuffer();
      sb.append("  Length  : " + this.length + "\n");
      sb.append("  LDE     : " + this.lde + "\n");
      if (this.length > 0)
          sb.append("  LDE len : " + this.ldeLen + "\n");
      sb.append("  Data    : " + this.data + "\n");
      sb.append("  Encoding: " + this.encoding + "\n");
      return sb.toString();
    }
    
    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public boolean equals(Object obj)
    {
      if (!(obj instanceof DE))
        return false;
      return this.toString().equals(obj.toString());
    }
  }
  
  
  /**
   * Bean fuer die Eigenschaften des Startcodes.
   * Selbstverstaendlich sind hier so einige Sachen anders codiert als im DE.
   * Waer ja auch zu einfach sonst.
   * Die Laengen-Angabe ist anders codiert (hex statt dec). Und nach der
   * Laenge kommen nicht sofort die Nutzdaten sondern erst noch die Control-Bytes.
   */
  public class Startcode extends DE
  {
    /**
     * Die Control-Bytes.
     * In der Regel sollte das nur eines sein.
     */
    public List<Integer> controlBytes = new ArrayList<Integer>();
    
    /**
     * Parst das DE am Beginn des uebergebenen Strings.
     * @param s der String, dessen Anfang das DE enthaelt.
     * @return der Reststring.
     * @see org.kapott.hbci.manager.FlickerCode.DE#parse(java.lang.String)
     */
    @Override
    String parse(String s)
    {
      // 1. LDE ermitteln (hex)
      this.lde = Integer.parseInt(s.substring(0,2),16);
      s = s.substring(2); // und abschneiden
      
      // 2. tatsaechliche Laenge ermitteln
      this.length = getBitSum(this.lde,5); // Bit 0-5
      
      // Encoding gibts hier noch nicht.
      // Das passiert erst beim Rendern

      // Wenn kein Control-Byte vorhanden ist, muss es HHD 1.3 sein
      FlickerCode.this.version = HHDVersion.HHD13;

      // 3. Control-Byte ermitteln, falls vorhanden
      if (isBitSet(this.lde,BIT_CONTROLBYTE))
      {
        FlickerCode.this.version = HHDVersion.HHD14;
        
        // Es darf maximal 9 Controlbytes geben
        for (int i=0;i<10;++i)
        {
          // 2 Zeichen, Hex
          int controlByte = Integer.parseInt(s.substring(0,2),16);
          this.controlBytes.add(controlByte);
          s = s.substring(2); // und abschneiden
          
          // Solange beim Controlbyte das groesste Bit gesetzt ist,
          // folgen weitere
          if (!isBitSet(controlByte,BIT_CONTROLBYTE))
            break;
        }
      }
      
      // 4. Startcode ermitteln
      this.data = s.substring(0,this.length);
      s = s.substring(this.length); // und abschneiden
      
      return s;
    }
    
    
    /**
     * @see org.kapott.hbci.manager.FlickerCode.DE#renderLength()
     * Ueberschrieben, weil wir hier noch reincodieren muessen, ob ein Controlbyte folgt.
     */
    String renderLength()
    {
      String s = super.renderLength();
      
      // HHD 1.3 -> gibt keine Controlbytes
      if (FlickerCode.this.version == HHDVersion.HHD13)
        return s;

      // HHD 1.4 -> aber keine Controlbytes vorhanden
      if (this.controlBytes.size() == 0)
        return s;

      // Controlbytes reincodieren
      int len = Integer.parseInt(s,16);

      if (this.controlBytes.size() > 0)
        len += (1 << BIT_CONTROLBYTE);

      return toHex(len,2);
    }


    /**
     * @see org.kapott.hbci.manager.FlickerCode.DE#toString()
     */
    public String toString()
    {
      StringBuffer sb = new StringBuffer(super.toString());
      sb.append("  Controlbytes: " + this.controlBytes + "\n");
      return sb.toString();
    }

    /**
     * @see org.kapott.hbci.manager.FlickerCode.DE#equals(java.lang.Object)
     */
    public boolean equals(Object obj)
    {
      if (!(obj instanceof Startcode))
        return false;
       return this.toString().equals(obj.toString());
    }
  }



  //////////////////////////////////////////////////////////////////////////////
  // Hilfsfunktionen fuer die Berechnungen

  /**
   * Wandelt die Zahl in Hex-Schreibweise um und fuellt links mit Nullen auf, bis die Laenge "len" erreicht ist.
   * @param n die Zahl.
   * @param len die zu erreichende Laenge.
   * @return die links mit Nullen aufgefuellte Zahl in HEX-Schreibweise.
   */
  private static String toHex(int n,int len)
  {
    String s = Integer.toString(n,16).toUpperCase();
    while (s.length() < len)
      s = "0" + s;
    return s;
  }
  
  /**
   * Wandelt alle Zeichen des String gemaess des jeweiligen ASCII-Wertes in HEX-Codierung um.
   * Beispiel: Das Zeichen "0" hat den ASCII-Wert "30" in Hexadezimal-Schreibweise.
   * @param s der umzuwandelnde String.
   * @return der codierte String.
   */
  private static String toHex(String s)
  {
    StringBuffer sb = new StringBuffer();
    char[] chars = s.toCharArray();
    for (char c:chars)
    {
      sb.append(toHex(c,2));
    }
    return sb.toString();
  }

  /**
   * Berechnet die Quersumme.
   * @param n die Zahl, deren Quersumme errechnet werden soll.
   * @return die Quersumme.
   */
  private static int quersumme(int n)
  {
    int q = 0;
    while (n != 0)
    {
      q += n % 10;
      n = (int) Math.floor(n / 10);
    }
    return q;
  }
  
  /**
   * Liefert die Summe der Bit-Wertigkeiten fuer die genannten Bits
   * (beginndend bei 0 und beim kleinsten Bit, angegebens inclusive).
   * 
   * Beispiel:
   * num  = 156 (-> 10011100)
   * bits = 5
   * 
   * Es wird die Summe der Bitwertigkeiten 2^0 bis 2^5 errechnet.
   * Also der Wert von **011100 = 2^4+2^3+s^2 = 28
   * 
   * @param num Zahl, aus der die Summe berechnet werden soll.
   * @param bits Anzahl der Bits (beginnend bei 0 und beim kleinsten Bit, angegebenes inclusive), deren Wertigkeit addiert werden soll.
   * @return der errechnete Wert.
   */
  private static int getBitSum(int num,int bits)
  {
    int sum = 0;
    for (int i=0;i<=bits;++i)
      sum += (num & (1 << i));
    return sum;
  }
  
  
  /**
   * Prueft, ob in der genannten Zahl das angegebene Bit gesetzt ist.
   * @param num die zu pruefende Zahl.
   * @param bit die Nummer des zu pruefenden Bits.
   * Wobei "0" das kleinste (rechts) und "7" das groesste (links) Bit ist.
   * @return true, wenn das Bit gesetzt ist.
   */
  private static boolean isBitSet(int num,int bit)
  {
    return (num & (1 << bit)) != 0;
  }
  //////////////////////////////////////////////////////////////////////////////
}
