/**********************************************************************
 *
 * This file is part of HBCI4Java.
 * Copyright (c) 2001-2008 Stefan Palme
 *
 * 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.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.kapott.hbci.GV.HBCIJob;
import org.kapott.hbci.GV.HBCIJobImpl;
import org.kapott.hbci.datatypes.SyntaxDE;
import org.kapott.hbci.datatypes.factory.SyntaxDEFactory;
import org.kapott.hbci.exceptions.HBCI_Exception;
import org.kapott.hbci.exceptions.InvalidUserDataException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Diese Klasse ermittelt die noetigen HKTAN-Challenge-Parameter fuer einen
 * Geschaeftsvorfall
 */
public class ChallengeInfo
{
  /**
   * Das Singleton.
   */
  private static ChallengeInfo singleton = null;
    private Map<String,Job> data = null; // Die Parameter-Daten aus der XML-Datei.

  /**
   * Erzeugt ein neues Challenge-Info-Objekt.
   * @return das Challenge-Info-Objekt.
   */
  public static synchronized ChallengeInfo getInstance()
  {
    if (singleton == null)
      singleton = new ChallengeInfo();
    return singleton;
  }
  
  /**
   * ct.
   */
  private ChallengeInfo()
  {
    HBCIUtils.log("initializing challenge info engine",HBCIUtils.LOG_DEBUG);

    
    ////////////////////////////////////////////////////////////////////////////
    // XML-Datei lesen
    String xmlpath = HBCIUtils.getParam("kernel.kernel.challengedatapath","");
    InputStream dataStream = null;

    String filename = xmlpath+"challengedata.xml";
    dataStream = ChallengeInfo.class.getClassLoader().getResourceAsStream(filename);
    if (dataStream == null)
      throw new InvalidUserDataException("*** can not load challenge information from "+filename);

    // mit den so gefundenen xml-daten ein xml-dokument bauen
    Document doc = null;
    try
    {
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      dbf.setIgnoringComments(true);
      dbf.setValidating(true);

      DocumentBuilder db = dbf.newDocumentBuilder();
      doc = db.parse(dataStream);
      dataStream.close();
    }
    catch (Exception e)
    {
      throw new HBCI_Exception("*** can not load challengedata from file "+filename,e);
    }
    //
    ////////////////////////////////////////////////////////////////////////////

    data = new HashMap<String,Job>();

    ////////////////////////////////////////////////////////////////////////////
    // Parsen
    NodeList jobs = doc.getElementsByTagName("job");
    int size      = jobs.getLength();
    
    for (int i=0;i<size;++i)
    {
      Element job = (Element) jobs.item(i);
      String code = job.getAttribute("code");
      data.put(code,new Job(job));
    }
    //
    ////////////////////////////////////////////////////////////////////////////

    HBCIUtils.log("challenge information loaded",HBCIUtils.LOG_DEBUG);
  }
  
  /**
   * Liefert die Challenge-Daten fuer einen Geschaeftsvorfall.
   * @param code die Segmentkennung des Geschaeftsvorfalls.
   * @return die Challenge-Daten.
   */
  public Job getData(String code)
  {
    return data.get(code);
  }

  /**
   * Uebernimmt die Challenge-Parameter in den HKTAN-Geschaeftsvorfall.
   * @param task der Job, zu dem die Challenge-Parameter ermittelt werden sollen.
   * @param hktan der HKTAN-Geschaeftsvorfall, in dem die Parameter gesetzt werden sollen.
   * @param secmech die BPD-Informationen zum TAN-Verfahren.
   */
  public void applyParams(HBCIJobImpl task, HBCIJob hktan, Properties secmech)
  {
    String code = task.getHBCICode(); // Code des Geschaeftsvorfalls

    // Job-Parameter holen
    Job job = this.getData(code);
    
    // Den Geschaeftsvorfall kennen wir nicht. Dann brauchen wir
    // auch keine Challenge-Parameter setzen
    if (job == null)
    {
      HBCIUtils.log("have no challenge data for " + code + ", will not apply challenge params", HBCIUtils.LOG_DEBUG);
      return;
    }
    
    HHDVersion version = HHDVersion.find(secmech);
    HBCIUtils.log("using hhd version " + version, HBCIUtils.LOG_DEBUG2);

    // Parameter fuer die passende HHD-Version holen
    HhdVersion hhd = job.getVersion(version.getChallengeVersion());
    
    // Wir haben keine Parameter fuer diese HHD-Version
    if (hhd == null)
    {
      HBCIUtils.log("have no challenge data for " + code + " in " + version + ", will not apply challenge params", HBCIUtils.LOG_DEBUG);
      return;
    }


    // Schritt 1: Challenge-Klasse uebernehmen
    String klass = hhd.getKlass();
    HBCIUtils.log("using challenge klass " + klass, HBCIUtils.LOG_DEBUG2);
    hktan.setParam("challengeklass", klass);

    
    // Schritt 2: Challenge-Parameter uebernehmen
    List<Param> params = hhd.getParams();
    for (int i=0;i<params.size();++i)
    {
      int num = i+1; // Die Job-Parameter beginnen bei 1
      Param param = params.get(i);
      
      // Checken, ob der Parameter angewendet werden soll.
      if (!param.isComplied(secmech))
      {
        HBCIUtils.log("skipping challenge parameter " + num + " (" + param.path + "), condition " + param.conditionName + "=" + param.conditionValue + " not complied",HBCIUtils.LOG_DEBUG2);
        continue;
      }
      
      // Parameter uebernehmen. Aber nur wenn er auch einen Wert hat.
      // Seit HHD 1.4 duerfen Parameter mittendrin optional sein, sie
      // werden dann freigelassen
      String value = param.getValue(task);
      if (value == null || value.length() == 0)
      {
        HBCIUtils.log("challenge parameter " + num + " (" + param.path + ") is empty",HBCIUtils.LOG_DEBUG2);
        continue;
      }
      
      HBCIUtils.log("adding challenge parameter " + num + " " + param.path + "=" + value, HBCIUtils.LOG_DEBUG2);
      hktan.setParam("ChallengeKlassParam" + num, value);
    }
  }
  
  /**
   * Eine Bean fuer die Parameter-Saetze eines Geschaeftsvorfalles fuer die HHD-Versionen.
   */
  public static class Job
  {
    /**
     * Die Parameter fuer die jeweilige HHD-Version.
     */
    private Map<String,HhdVersion> versions = new HashMap<String,HhdVersion>();
    
    /**
     * ct.
     * @param job der XML-Knoten, in dem die Daten stehen.
     */
    private Job(Element job)
    {
      NodeList specs = job.getElementsByTagName("challengeinfo");
      int size       = specs.getLength();
      
      for (int i=0;i<size;++i)
      {
        Element spec    = (Element) specs.item(i);
        String  version = spec.getAttribute("spec");
        
        this.versions.put(version,new HhdVersion(spec));
      }
    }
    
    /**
     * Liefert die Challenge-Parameter fuer die angegeben HHD-Version.
     * @param version die HHD-Version.
     * @return die Challenge-Parameter fuer die HHD-Version.
     */
    public HhdVersion getVersion(String version)
    {
      return this.versions.get(version);
    }
  }
  
  /**
   * Eine Bean fuer den Parameter-Satz eines Geschaeftvorfalles innerhalb einer HHD-Version.
   */
  public static class HhdVersion
  {
    /**
     * Die Challenge-Klasse.
     */
    private String klass = null;
    
    /**
     * Liste der Challenge-Parameter.
     */
    private List<Param> params = new ArrayList<Param>();
    
    /**
     * ct.
     * @param spec der XML-Knoten mit den Daten.
     */
    private HhdVersion(Element spec)
    {
      this.klass = ((Element)spec.getElementsByTagName("klass").item(0)).getFirstChild().getNodeValue();

      NodeList list = spec.getElementsByTagName("param");
      int size      = list.getLength();
      for (int i=0;i<size;++i)
      {
        Element param = (Element) list.item(i);
        this.params.add(new Param(param));
      }
    }
    
    /**
     * Liefert die Challenge-Klasse.
     * @return die Challenge-Klasse.
     */
    public String getKlass()
    {
      return this.klass;
    }
    
    /**
     * Liefert die Challenge-Parameter fuer den Geschaeftsvorfall in dieser HHD-Version.
     * @return die Challenge-Parameter fuer den Geschaeftsvorfall in dieser HHD-Version.
     */
    public List<Param> getParams()
    {
      return this.params;
    }
  }
  
  /**
   * Eine Bean fuer einen einzelnen Challenge-Parameter.
   */
  public static class Param
  {
    /**
     * Der Typ des Parameters.
     */
    private String type = null;
    
    /**
     * Der Pfad in den Geschaeftsvorfall-Parametern, unter dem der Wert steht.
     */
    private String path = null;
    
    /**
     * Optional: Der Name einer Bedingung, die erfuellt sein muss, damit
     * der Parameter verwendet wird. Konkret ist hier der Name eines Property
     * aus secmechInfo gemeint. Also ein BPD-Parameter.
     */
    private String conditionName = null;
    
    /**
     * Optional: Der Wert, den der BPD-Parameter haben muss, damit der Challenge-Parameter
     * verwendet wird.
     */
    private String conditionValue = null;
    
    /**
     * ct.
     * @param param der XML-Knoten mit den Daten.
     */
    private Param(Element param)
    {
      Node content = param.getFirstChild();
      this.path           = content != null ? content.getNodeValue() : null;
      this.type           = param.getAttribute("type");
      this.conditionName  = param.getAttribute("condition-name");
      this.conditionValue = param.getAttribute("condition-value");
    }
    
    /**
     * Liefert true, wenn entweder keine Bedingung angegeben ist oder
     * die Bedingung erfuellt ist und der Parameter verwendet werden kann.
     * @param secmech die BPD-Informationen zum TAN-Verfahren.
     * @return true, wenn der Parameter verwendet werden kann.
     */
    public boolean isComplied(Properties secmech)
    {
      if (this.conditionName == null || this.conditionName.length() == 0)
        return true;
      
      // Wir haben eine Bedingung. Mal schauen, ob sie erfuellt ist.
      String value = secmech.getProperty(this.conditionName,"");
      return value.equals(this.conditionValue);
    }
    
    /**
     * Liefert den Typ des Parameters.
     * @return der Typ des Parameters.
     */
    public String getType()
    {
      return this.type;
    }
    
    /**
     * Liefert den Pfad zum Wert.
     * @return der Pfad zum Wert.
     */
    public String getPath()
    {
      return this.path;
    }
    
    /**
     * Liefert den Wert des Parameters.
     * @param job der Geschaeftsvorfall.
     * @return der Wert des Parameters.
     */
    private String getValue(HBCIJobImpl job)
    {
      // Leerer Parameter
      if (this.path == null || this.path.length() == 0)
        return null;
      
      String value = job.getChallengeParam(this.path);
      
      // Wert im passenden Format zurueckliefern
      return format(value);
    }
    
    /**
     * Formatiert den Text abhaengig vom Typ.
     * Wenn kein Typ angegeben ist, wird der Wert unformatiert zurueckgegeben.
     * @param value der zu formatierende Wert.
     * @return der formatierte Wert.
     */
    public String format(String value)
    {
      // Bei leeren Werten lieferen wir generell NULL.
      // Die Parameter werden dann uebersprungen.
      if (value == null || value.trim().length() == 0)
        return null;

      // Wenn kein Typ angegeben ist, gibts auch nichts zu formatieren.
      // Nein, wir duerfen NICHT SyntaxAN verwenden. Denn die Parameter
      // in ChallengeKlassParams#param[1-9] sind ja bereits als Type AN
      // deklariert. Wuerden wir hier SyntaxAN verwenden, wuerden die
      // Werte dann doppelt codiert werden (das zweite Codieren macht ja
      // anschliessend HBCI4Java intern beim Zusammenbauen des Segments).
      // Was zum Beispiel dazu fuehren wuerde, dass ein Sonderzeichen wie
      // "+" oder "?" doppelt escaped werden wuerde.
      if (this.type == null || this.type.trim().length() == 0)
        return value;

      SyntaxDEFactory factory = SyntaxDEFactory.getInstance();
      SyntaxDE syntax = null;
      try
      {
        syntax = factory.createSyntaxDE(this.type,this.path,value,0,0);
        return syntax.toString(0);
      }
      finally
      {
        // Objekt wieder freigeben
        if (syntax != null)
          factory.unuseObject(syntax,this.type);
      }
    }
  }
}
