package at.gv.egovernment.moa.spss.server.invoke;

import iaik.ixsil.algorithms.CanonicalizationAlgorithm;
import iaik.ixsil.algorithms.CanonicalizationAlgorithmImplExclusiveCanonicalXMLWithComments;
import iaik.server.modules.xml.BinaryDataObject;
import iaik.server.modules.xml.DataObject;
import iaik.server.modules.xml.XMLDataObject;
import iaik.server.modules.xml.XMLNodeListDataObject;
import iaik.server.modules.xmlverify.CertificateValidationResult;
import iaik.server.modules.xmlverify.DsigManifest;
import iaik.server.modules.xmlverify.HashUnavailableException;
import iaik.server.modules.xmlverify.ReferenceData;
import iaik.server.modules.xmlverify.ReferenceInfo;
import iaik.server.modules.xmlverify.SecurityLayerManifest;
import iaik.server.modules.xmlverify.XMLSignatureVerificationProfile;
import iaik.server.modules.xmlverify.XMLSignatureVerificationResult;
import iaik.x509.X509Certificate;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.w3c.dom.DocumentFragment;
import org.w3c.dom.NodeList;

import at.gv.egovernment.moa.spss.MOAApplicationException;
import at.gv.egovernment.moa.spss.api.SPSSFactory;
import at.gv.egovernment.moa.spss.api.common.CheckResult;
import at.gv.egovernment.moa.spss.api.common.Content;
import at.gv.egovernment.moa.spss.api.common.InputData;
import at.gv.egovernment.moa.spss.api.common.SignerInfo;
import at.gv.egovernment.moa.spss.api.impl.InputDataBinaryImpl;
import at.gv.egovernment.moa.spss.api.impl.InputDataXMLImpl;
import at.gv.egovernment.moa.spss.api.xmlverify.ManifestRefsCheckResultInfo;
import at.gv.egovernment.moa.spss.api.xmlverify.ReferencesCheckResult;
import at.gv.egovernment.moa.spss.api.xmlverify.ReferencesCheckResultInfo;
import at.gv.egovernment.moa.spss.api.xmlverify.VerifyXMLSignatureResponse;
import at.gv.egovernment.moa.util.CollectionUtils;
import at.gv.egovernment.moa.util.DOMUtils;
import at.gv.egovernment.moa.util.NodeListAdapter;

/**
 * A class to build a <code>VerifyXMLSignatureResponse</code> object.
 * 
 * <p>Via a call to <code>addResult()</code> the only result of the
 * signature verification must be added.</p>
 * 
 * <p>The <code>getResponseElement()</code> method then returns the
 * <code>VerifyXMLSignatureResponse</code> built so far.</p>
 * 
 * @author Patrick Peck
 * @version $Id$
 */
public class VerifyXMLSignatureResponseBuilder {

  /** The <code>SPSSFactory</code> for creating API objects. */
  private SPSSFactory factory = SPSSFactory.getInstance();

  /** Information about the signer certificate. */
  private SignerInfo signerInfo;
  /** The hash input data. */
  private List hashInputDatas;
  /** The reference input data. */
  private List referenceInputDatas;
  /** The result of the signature check. */
  private ReferencesCheckResult signatureCheck;
  /** The result of the signature manifest check. */
  private ReferencesCheckResult signatureManifestCheck;
  /** The result of the XMLDsig manifest check. */
  private List xmlDsigManifestChecks;
  /** The result of the certificate check. */
  private CheckResult certificateCheck;

  /**
   * Get the <code>VerifyMLSignatureResponse</code> built so far.
   * 
   * @return The <code>VerifyXMLSignatureResponse</code> built so far.
   */
  public VerifyXMLSignatureResponse getResponse() {
    return factory.createVerifyXMLSignatureResponse(
      signerInfo,
      hashInputDatas,
      referenceInputDatas,
      signatureCheck,
      signatureManifestCheck,
      xmlDsigManifestChecks,
      certificateCheck);
  }

  /**
   * Sets the verification result to the response.
   * 
   * This method must be called exactly once to ensure a valid
   * <code>VerifyXMLSignatureResponse</code>.
   * 
   * @param result The result to set for the response.
   * @param profile The profile used for verifying the signature.
   * @param transformsSignatureManifestCheck The overall result for the signature 
   *        manifest check.
   * @param certificateCheck The overall result for the certificate check. 
   * @throws MOAApplicationException An error occurred adding the result.
   */
  public void setResult(
    XMLSignatureVerificationResult result,
    XMLSignatureVerificationProfile profile,
    ReferencesCheckResult transformsSignatureManifestCheck,
    CheckResult certificateCheck)
    throws MOAApplicationException {

    CertificateValidationResult certResult =
      result.getCertificateValidationResult();
    List referenceDataList;
    ReferenceData referenceData;
    List dsigManifestList;
    ReferencesCheckResultInfo checkResultInfo;
    int[] failedReferences;
    Iterator iter;

    // create the SignerInfo;
    signerInfo =
      factory.createSignerInfo(
        (X509Certificate) certResult.getCertificateChain().get(0),
        certResult.isQualifiedCertificate(),
        certResult.isPublicAuthorityCertificate(),
        certResult.getPublicAuthorityID());

    // Create HashInputData Content objects
    referenceDataList = result.getReferenceDataList();
    if (profile.includeHashInputData()) {
      hashInputDatas = new ArrayList();
      
      // Include SignedInfo references
      addHashInputDatas(
        hashInputDatas, 
        referenceDataList, 
        InputData.CONTAINER_SIGNEDINFO_, 
        InputData.REFERER_NONE_);
      
      // Include XMLDSIGManifest references
      List xMLDSIGManifests = result.getDsigManifestList();
      for (iter = xMLDSIGManifests.iterator(); iter.hasNext();)
      {
        DsigManifest currentMF = (DsigManifest) iter.next();
        List xMLDSIGMFReferenceDataList = currentMF.getReferenceDataList();
        addHashInputDatas(
          hashInputDatas, 
          xMLDSIGMFReferenceDataList, 
          InputData.CONTAINER_XMLDSIGMANIFEST_, 
          currentMF.getReferringReferenceInfo().getReferenceIndex());
      }
    }

    // Create the ReferenceInputData Content objects
    if (profile.includeReferenceInputData()) {
      referenceInputDatas = new ArrayList();
      
      // Include SignedInfo references
      addReferenceInputDatas(
        referenceInputDatas, 
        referenceDataList, 
        InputData.CONTAINER_SIGNEDINFO_, 
        InputData.REFERER_NONE_);

      // Include XMLDSIGManifest references
      List xMLDSIGManifests = result.getDsigManifestList();
      for (iter = xMLDSIGManifests.iterator(); iter.hasNext();)
      {
        DsigManifest currentMF = (DsigManifest) iter.next();
        List xMLDSIGMFReferenceDataList = currentMF.getReferenceDataList();
        addReferenceInputDatas(
          referenceInputDatas, 
          xMLDSIGMFReferenceDataList, 
          InputData.CONTAINER_XMLDSIGMANIFEST_, 
          currentMF.getReferringReferenceInfo().getReferenceIndex());
      }
    }

    // create the signature check
    failedReferences = buildFailedReferences(result.getReferenceDataList());
    checkResultInfo =
      failedReferences != null
        ? factory.createReferencesCheckResultInfo(null, failedReferences)
        : null;
    signatureCheck =
      factory.createReferencesCheckResult(
        result.getSignatureValueVerificationCode().intValue(),
        checkResultInfo);

    // create the signature manifest check
    if (profile.checkSecurityLayerManifest())
    {
      if (transformsSignatureManifestCheck.getCode() == 1)
      {
        // checking the transforms failed
        signatureManifestCheck = transformsSignatureManifestCheck;
      }
      else if (result.isSecurityLayerManifestRequired())
      {
        if (!result.containsSecurityLayerManifest())
        {
          // required security layer manifest is missing in signature
          signatureManifestCheck = factory.createReferencesCheckResult(2, null);
        } 
        else
        {
          // security layer manifest exists, so we have to check its validity
          SecurityLayerManifest slManifest = result.getSecurityLayerManifest();
          int verificationResult = slManifest.getManifestVerificationResult().intValue();

          if (SecurityLayerManifest.CODE_MANIFEST_VALID.intValue() == verificationResult)
          {
            // security layer manifest exists and is free of errors
            signatureManifestCheck = factory.createReferencesCheckResult(0, null);
          }
          else
          {
            // security layer manifest exists, but has errors
            failedReferences = buildFailedReferences(slManifest.getReferenceDataList());
            checkResultInfo = (failedReferences != null)
              ? factory.createReferencesCheckResultInfo(null, failedReferences)
              : null;
            if (SecurityLayerManifest.CODE_MANIFEST_INCOMPLETE.intValue() == verificationResult)
            {
              signatureManifestCheck =  factory.createReferencesCheckResult(3, checkResultInfo);
            }
            else if (SecurityLayerManifest.CODE_REFERENCE_HASH_INVALID.intValue() == verificationResult)
            {
              signatureManifestCheck =  factory.createReferencesCheckResult(4, checkResultInfo);
            }
            else
            {
              // Should not happen
              throw new RuntimeException("Unexpected result from security layer manifest verification.");
            }
          }
        }
      }
      else
      {
        // no security layer manifest is required, so the signature manifest check is ok
        signatureManifestCheck = factory.createReferencesCheckResult(0, null);
      }
    }

    // create the xmlDsigManifestCheck
    if (profile.checkXMLDsigManifests()) {
      xmlDsigManifestChecks = new ArrayList();
      dsigManifestList = result.getDsigManifestList();
      for (iter = dsigManifestList.iterator(); iter.hasNext();) {
        DsigManifest dsigManifest = (DsigManifest) iter.next();
        int refIndex =
          dsigManifest.getReferringReferenceInfo().getReferenceIndex();
        ManifestRefsCheckResultInfo manifestCheckResultInfo;

        failedReferences =
          buildFailedReferences(dsigManifest.getReferenceDataList());
        manifestCheckResultInfo =
          factory.createManifestRefsCheckResultInfo(
            null,
            failedReferences,
            refIndex);
        xmlDsigManifestChecks.add(
          factory.createManifestRefsCheckResult(
            dsigManifest.getManifestVerificationResult().intValue(),
            manifestCheckResultInfo));
      }
    }

    // create the certificate check 
    this.certificateCheck = certificateCheck;
  }

  /**
   * Adds {@link InputData} entries to the specified <code>inputDatas</code> list. The content of the entry will
   * be created from {@link ReferenceData#getHashInputData()}.
   * 
   * @param inputDatas The list to be amended.
   * 
   * @param referenceDataList The list of {@link ReferenceData} objects to be investigated.
   * 
   * @param containerType The type of container of the {@link InputData} objects to be created.
   * 
   * @param refererNumber The number of the referring reference for the {@link InputData} objects to be created.
   * 
   * @throws MOAApplicationException if creating an {@link InputData} fails. 
   */
  private void addHashInputDatas(List inputDatas, List referenceDataList, String containerType, int refererNumber)
  throws MOAApplicationException
  {
    for (Iterator iter = referenceDataList.iterator(); iter.hasNext();)
    {
      ReferenceData referenceData = (ReferenceData) iter.next();
      inputDatas.add(buildInputData(
        referenceData.getHashInputData(),
        containerType,
        refererNumber));
    }
  }
  
  /**
   * Adds {@link InputData} entries to the specified <code>inputDatas</code> list. The content of the entry will
   * be created from {@link ReferenceData#getReferenceInputData()}.
   * 
   * @param inputDatas The list to be amended.
   * 
   * @param referenceDataList The list of {@link ReferenceData} objects to be investigated.
   * 
   * @param containerType The type of container of the {@link InputData} objects to be created.
   * 
   * @param refererNumber The number of the referring reference for the {@link InputData} objects to be created.
   * 
   * @throws MOAApplicationException if creating an {@link InputData} fails. 
   */
  private void addReferenceInputDatas(List inputDatas, List referenceDataList, String containerType, int refererNumber)
    throws MOAApplicationException
  {
    for (Iterator iter = referenceDataList.iterator(); iter.hasNext();)
    {
      ReferenceData referenceData = (ReferenceData) iter.next();
      inputDatas.add(buildInputData(
        referenceData.getReferenceInputData(),
        containerType,
        refererNumber));
    }
  }

  /**
   * Build a <code>InputDataBinaryImpl</code> or an <code>InputDataXMLImpl</code>
   * object from the given <code>DataObject</code> and the given attributes.
   * 
   * @param dataObject The <code>DataObject</code> from which to build the result.
   * Based on the type of this parameter, the type of the result will either be
   * <code>InputDataBinaryImpl</code> or <code>InputDataXMLImpl</code>. 
   * 
   * @param partof see {@link InputData}
   * 
   * @param referringReferenceNumber see {@link InputData}
   * 
   * @return The corresponinding input data implementation.
   *  
   * @throws MOAApplicationException An error occurred creating the result.
   */
  private Content buildInputData(DataObject dataObject, String partOf, int referringReferenceNumber)
    throws MOAApplicationException {

    if (dataObject instanceof BinaryDataObject) {
      BinaryDataObject binaryData = (BinaryDataObject) dataObject;
      return new InputDataBinaryImpl(
        factory.createContent(binaryData.getInputStream(), null),
        partOf,
        referringReferenceNumber);
    } else if (dataObject instanceof XMLDataObject) {
      XMLDataObject xmlData = (XMLDataObject) dataObject;
      List nodes = new ArrayList();

      nodes.add(xmlData.getElement());
      return new InputDataXMLImpl(
        factory.createContent(new NodeListAdapter(nodes), null),
        partOf,
        referringReferenceNumber);
    } else { // dataObject instanceof XMLNodeListDataObject
      // if the data in the NodeList can be converted back to valid XML,
      // write it as XMLContent; otherwise, write it as Base64Content 
      XMLNodeListDataObject nodeData = (XMLNodeListDataObject) dataObject;
      NodeList nodes = nodeData.getNodeList();

      if (DOMUtils.checkAttributeParentsInNodeList(nodes)) {
        // insert as XMLContent
        try {
          DocumentFragment fragment = DOMUtils.nodeList2DocumentFragment(nodes);

          return new InputDataXMLImpl(
            factory.createContent(fragment.getChildNodes(), null),
            partOf, 
            referringReferenceNumber);
        } catch (Exception e) {
          // not successful -> fall through to the Base64Content
        }
      }

      // insert canonicalized NodeList as binary content
      try {
        CanonicalizationAlgorithm c14n =
          new CanonicalizationAlgorithmImplExclusiveCanonicalXMLWithComments();
        InputStream is;

        c14n.setInput(nodes);
        is = c14n.canonicalize();
        return new InputDataBinaryImpl(
          factory.createContent(is, null),
          partOf,
          referringReferenceNumber);
      } catch (Exception e) {
        throw new MOAApplicationException("2200", null);
      }
    }
  }

  /**
   * Build the failed references.
   * 
   * Failed references are references for which the <code>isHashValid()</code>
   * method returns <code>false</code>.
   * 
   * @param refInfos A <code>List</code> containing the
   * <code>ReferenceInfo</code> objects to be checked.
   * @return The indexes of the failed references. 
   */
  private int[] buildFailedReferences(List refInfos) {
    List failedReferencesList = new ArrayList();
    int i;

    // find out the failed references
    for (i = 0; i < refInfos.size(); i++) {
      ReferenceInfo refInfo = (ReferenceInfo) refInfos.get(i);

      try {
        if (refInfo.isHashCalculated() && !refInfo.isHashValid()) {
          failedReferencesList.add(new Integer(i + 1));
        }
      } catch (HashUnavailableException e) {
        // nothing to do here because we called refInfo.isHashCalculated first
      }
    }

    // convert to an int array
    if (failedReferencesList.isEmpty()) {
      return null;
    } else {
      int[] failedReferences = CollectionUtils.toIntArray(failedReferencesList);

      return failedReferences;
    }
  }

}