/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.security.x509;

import com.caucho.util.*;
import com.caucho.vfs.*;
import java.io.*;
import java.util.*;

/**
 * pkcs#10 is in PEM format, DER with base64  and --- BEGIN ---
 */
public class X509Parser {
  private static final L10N L = new L10N(X509Parser.class);
  
  /**
   * Parses the certificate in pkcs#10
   */
  public String parseCertificate(Path path)
    throws IOException
  {
    ReadStream is = path.openRead();

    try {
      String line;

      while ((line = is.readLine()) != null) {

        if (line.startsWith("-----BEGIN CERTIFICATE---"))
          return parseCertificateContent(is);
      }

      throw new IOException(L.l("Can't find certificate in '{0}'",
                                path));
    } finally {
      is.close();
    }
  }

  private String parseCertificateContent(ReadStream is)
    throws IOException
  {
    TempOutputStream os = new TempOutputStream();

    String line;
    LineReader reader = new LineReader();

    while ((line = is.readLine()) != null) {
      if (line.startsWith("-----END CERTIFICATE-----")) {
        System.out.println("TOTAL-LEN: " + os.getLength());

        return parseCertificateDer(os.openRead());
      }

      reader.init(line);
      Base64.decode(reader, os);
    }


    throw new IOException(L.l("Can't find end certificate"));
  }

  /**
   * Certificate ::= SEQUENCE {
   *   tbsCertificate
   *   signatureAlgorithm
   *   signature
   * }
   */
  private String parseCertificateDer(ReadStream is)
    throws IOException
  {
    int len = parseSequenceHeader(is);

    parseTbsCertificate(is);
    parseSignatureAlgorithm(is);
    parseSignature(is);
    
    return "ok";
  }

  /**
   * tbsCertificate ::= SEQUENCE {
   * version          [ 0 ]  Version DEFAULT v1(0),
   * serialNumber            CertificateSerialNumber,
   * signature               AlgorithmIdentifier,
   * issuer                  Name,
   * validity                Validity,
   * subject                 Name,
   * subjectPublicKeyInfo    SubjectPublicKeyInfo,
   * issuerUniqueID    [ 1 ] IMPLICIT UniqueIdentifier OPTIONAL,
   * subjectUniqueID   [ 2 ] IMPLICIT UniqueIdentifier OPTIONAL,
   * extensions        [ 3 ] Extensions OPTIONAL
   * }
   */
  private void parseTbsCertificate(ReadStream is)
    throws IOException
  {
    int len = parseSequenceHeader(is);

    int field = parseFieldHeader(is);
    int version = 0;
    if (field >= 0) {
      version = (int) parseInteger(is);
    }

    System.out.println("Version: " + version);
    // actually an integer
    byte []serial = parseBlob(is, 0x02);

    Oid algorithmOid = parseAlgorithmIdentifier(is);
    System.out.println("ALG: " + algorithmOid);

    System.out.println("ISSUER:");
    String issuer = parseName(is);

    parseValidity(is);

    System.out.println("SUBJECT:");
    String subject = parseName(is);

    parseSubjectPublicKeyInfo(is);
  }

  /**
   * signatureAlgorithm ::=  {
   * }
   */
  private void parseSignatureAlgorithm(ReadStream is)
    throws IOException
  {
  }

  /**
   * signature ::=  {
   * }
   */
  private void parseSignature(ReadStream is)
    throws IOException
  {
  }

  private Oid parseAlgorithmIdentifier(ReadStream is)
    throws IOException
  {
    int len = parseSequenceHeader(is);

    Oid oid = parseOid(is);

    while (parseOid(is) != null) {
    }

    return oid;
  }

  /**
   *
   */
  private void parseSubjectPublicKeyInfo(ReadStream is)
    throws IOException
  {
    int len = parseSequenceHeader(is);
    long end = is.getPosition() + len;

    while (is.getPosition() < end) {
      Object value = parseAny(is);

      System.out.println("PK: " + value);
    }
  }

  /**
   * Name :: SEQUENCE OF RelativeDistinguishedName
   *
   * RelativeDistinguidhedName ::= SET OF AttributeValueAssertion
   *
   * AttributeValueAssertion ::= SEQUENCE {
   *   attributeType OID,
   *   attributeValue ANY
   * }
   */
  private String parseName(ReadStream is)
    throws IOException
  {
    int length = parseSequenceHeader(is);
    System.out.println("LEN: " + length);

    while (length > 0) {
      int code = is.read();
      if (code != 0x31)
        throw new IOException(L.l("expected 0x31 at {0}",
                                  Integer.toHexString(code)));

      int len = parseLength(is);
      length -= 2 + len;

      int sublen = parseSequenceHeader(is);
      Oid oid = parseOid(is);
      Object value = parseAny(is);

      System.out.println("NAME: " + oid + " " + value);
    }

    return "name";
  }

  /**
   * Validity ::= SEQUENCE {
   *   notBefore               UTCTIME,
   *   notAfter                UTCTIME
   *  }
   */
  private void parseValidity(ReadStream is)
    throws IOException
  {
    int len = parseSequenceHeader(is);

    long notBefore = parseUtcTime(is);
    long notAfter = parseUtcTime(is);
  }

  private Object parseAny(ReadStream is)
    throws IOException
  {
    int code = is.read();

    switch (code) {
    case 0x02: // integer
      {
        is.unread();
        return parseInteger(is);
      }
      
    case 0x03: // bit string
      {
        is.unread();
        return parseBitString(is);
      }
      
    case 0x05: // null
      {
        int len = parseLength(is);
        is.skip(len);
        return null;
      }
      
    case 0x06: // oid
      {
        is.unread();

        return parseOid(is);
      }

    case 0x13: // printablestring
      {
        is.unread();
        return parsePrintableString(is);
      }

    case 0x16: // ia5string
      {
        is.unread();
        return parseIa5String(is);
      }

    case 0x30: // sequence
      {
        is.unread();
        return parseSequence(is);
      }

    default:
      {
        System.out.println(String.format("UNKNOWN: %x", code));

        int len = parseLength(is);
        is.skip(len);
        return null;
      }
    }
  }

  private ArrayList parseSequence(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x30)
      throw new IOException(L.l("expected 0x17 at '0x{0}'",
                                Integer.toHexString(ch)));

    int len = parseLength(is);
    long end = is.getPosition() + len;

    ArrayList value = new ArrayList();
    
    while (is.getPosition() < end) {
      value.add(parseAny(is));
    }

    return value;
  }

  private long parseUtcTime(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x17)
      throw new IOException(L.l("expected 0x17 at '{0}'", ch));

    int len = parseLength(is);

    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < len; i++) {
      sb.append((char) is.read());
    }

    return 0;
  }

  private BitString parseBitString(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x03)
      throw new IOException(L.l("expected 0x03 at '{0}'", ch));

    int len = parseLength(is);

    int unused = is.read();
    
    byte []data = new byte[len - 1];

    is.readAll(data, 0, len - 1);

    return new BitString(data, unused);
  }

  private String parsePrintableString(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x13)
      throw new IOException(L.l("expected 0x13 at '{0}'", ch));

    int len = parseLength(is);

    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < len; i++) {
      sb.append((char) is.read());
    }

    return sb.toString();
  }

  private String parseIa5String(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x16)
      throw new IOException(L.l("expected 0x13 at '{0}'", ch));

    int len = parseLength(is);

    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < len; i++) {
      sb.append((char) is.read());
    }

    return sb.toString();
  }
  
  private Oid parseOid(ReadStream is)
    throws IOException
  {
    int ch = is.read();
    is.unread();

    if (ch == 0x05) {
      parseNull(is);
      return null;
    }
    else
      return new Oid(parseBlob(is, 0x06));
  }

  private int parseSequenceHeader(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x30)
      throw new IOException(L.l("expected sequence 0x30 at {0}",
                                Integer.toHexString(ch)));

    return parseLength(is);
  }

  private int parseFieldHeader(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if ((ch & 0xf0) != 0xa0) {
      is.unread();
      return -1;
    }

    int len = parseLength(is);

    return ch & 0xf;
  }

  private long parseInteger(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x02)
      throw new IOException(L.l("expected sequence 0x02 at {0}",
                                Integer.toHexString(ch)));

    int len = parseLength(is);

    long v = 0;
    for (int i = 0; i < len; i++) {
      v = 256 * v + is.read();
    }
    
    return v;
  }

  private void parseNull(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if (ch != 0x05)
      throw new IOException(L.l("expected sequence 0x05 at {0}",
                                Integer.toHexString(ch)));

    int len = parseLength(is);

    is.skip(len);
  }

  private byte []parseBlob(ReadStream is, int code)
    throws IOException
  {
    int ch = is.read();

    if (code != 0 && ch != code)
      throw new IOException(L.l("expected value {0} at {1}",
                                Integer.toHexString(code),
                                Integer.toHexString(ch)));

    int len = parseLength(is);

    byte []data = new byte[len];

    is.readAll(data, 0, data.length);
    
    return data;
  }

  private int parseLength(ReadStream is)
    throws IOException
  {
    int ch = is.read();

    if ((ch & 0x80) == 0)
      return ch & 0x7f;

    int count = ch & 0x7f;

    int len = 0;
    for (int i = 0; i < count; i++) {
      len = 256 * len + is.read();
    }

    return len;
  }

  static class LineReader extends Reader {
    String _string;
    int _offset;
    int _length;

    void init(String string)
    {
      _string = string;
      _offset = 0;
      _length = string.length();
    }

    public int read()
    {
      if (_offset < _length)
        return _string.charAt(_offset++);

      return -1;
    }

    public int read(char []buffer, int offset, int length)
    {
      if (_offset < _length) {
        buffer[offset] = _string.charAt(_offset++);
        return 1;
      }

      return -1;
    }

    public void close()
    {
    }
  }
}
