/***** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/epl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2006, 2007 Ola Bini <ola@ologix.com>
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby.ext.openssl;

import java.math.BigInteger;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.io.IOException;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERBoolean;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTCTime;
import org.bouncycastle.asn1.DERUTF8String;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBignum;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
import org.jruby.RubyTime;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;

/**
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class ASN1 {
    private static Map<Ruby, Map<String, ASN1ObjectIdentifier>> SYM_TO_OID = new IdentityHashMap<Ruby, Map<String, ASN1ObjectIdentifier>>();
    private static Map<Ruby, Map<ASN1ObjectIdentifier, String>> OID_TO_SYM = new IdentityHashMap<Ruby, Map<ASN1ObjectIdentifier, String>>();
    private static Map<Ruby, Map<ASN1ObjectIdentifier, Integer>> OID_TO_NID = new IdentityHashMap<Ruby, Map<ASN1ObjectIdentifier, Integer>>();
    private static Map<Ruby, Map<Integer, ASN1ObjectIdentifier>> NID_TO_OID = new IdentityHashMap<Ruby, Map<Integer, ASN1ObjectIdentifier>>();
    private static Map<Ruby, Map<Integer, String>> NID_TO_SN = new IdentityHashMap<Ruby, Map<Integer, String>>();
    private static Map<Ruby, Map<Integer, String>> NID_TO_LN = new IdentityHashMap<Ruby, Map<Integer, String>>();


    static void addObject(Ruby runtime, int nid, String sn, String ln, String oid) {
        Map<String, ASN1ObjectIdentifier> s2o = SYM_TO_OID.get(runtime);
        Map<ASN1ObjectIdentifier, String> o2s = OID_TO_SYM.get(runtime);
        Map<ASN1ObjectIdentifier, Integer> o2n = OID_TO_NID.get(runtime);
        Map<Integer, ASN1ObjectIdentifier> n2o = NID_TO_OID.get(runtime);
        Map<Integer, String> n2s = NID_TO_SN.get(runtime);
        Map<Integer, String> n2l = NID_TO_LN.get(runtime);
        if(null != oid && (null != sn || null != ln)) {
            ASN1ObjectIdentifier ident = new ASN1ObjectIdentifier(oid);
            if(sn != null) {
                s2o.put(sn.toLowerCase(),ident);
            }
            if(ln != null) {
                s2o.put(ln.toLowerCase(),ident);
            }
            o2s.put(ident,sn == null ? ln : sn);
            o2n.put(ident,nid);
            n2o.put(nid,ident);
            n2s.put(nid,sn);
            n2l.put(nid,ln);
        }        
    }

    @SuppressWarnings("unchecked")
    private synchronized static void initMaps(Ruby runtime) {
        Map<String, ASN1ObjectIdentifier> val = new HashMap<String, ASN1ObjectIdentifier>(org.bouncycastle.asn1.x509.X509Name.DefaultLookUp);
        Map<ASN1ObjectIdentifier, String> val2 = new HashMap<ASN1ObjectIdentifier, String>(org.bouncycastle.asn1.x509.X509Name.DefaultSymbols);
        SYM_TO_OID.put(runtime,val);
        OID_TO_SYM.put(runtime,val2);
        OID_TO_NID.put(runtime,new HashMap<ASN1ObjectIdentifier, Integer>());
        NID_TO_OID.put(runtime,new HashMap<Integer, ASN1ObjectIdentifier>());
        NID_TO_SN.put(runtime,new HashMap<Integer, String>());
        NID_TO_LN.put(runtime,new HashMap<Integer, String>());
        OpenSSLImpl.defaultObjects(runtime);
    }

    synchronized static Integer obj2nid(Ruby runtime, String oid) {
        return obj2nid(runtime, new ASN1ObjectIdentifier(oid));
    }

    synchronized static String ln2oid(Ruby runtime, String ln) {
        Map<String, ASN1ObjectIdentifier> val = SYM_TO_OID.get(runtime);
        if(null == val) {
            initMaps(runtime);
            val = SYM_TO_OID.get(runtime);
        }
        return val.get(ln).getId();
    }

    synchronized static Integer obj2nid(Ruby runtime, ASN1ObjectIdentifier oid) {
        Map<ASN1ObjectIdentifier, Integer> o2n = OID_TO_NID.get(runtime);
        if(null == o2n) {
            initMaps(runtime);
            o2n = OID_TO_NID.get(runtime);
        }
        return o2n.get(oid);
    }

    synchronized static String o2a(Ruby runtime, ASN1ObjectIdentifier obj) {
        Integer nid = obj2nid(runtime,obj);
        Map<Integer, String> n2l = NID_TO_LN.get(runtime);
        Map<Integer, String> n2s = NID_TO_SN.get(runtime);
        String one = n2l.get(nid);
        if(one == null) {
            one = n2s.get(nid);
        }
        return one;
    }

    synchronized static String nid2ln(Ruby runtime, int nid) {
        return nid2ln(runtime, new Integer(nid));
    }

    synchronized static String nid2ln(Ruby runtime, Integer nid) {
        Map<Integer, String> n2l = NID_TO_LN.get(runtime);
        if(null == n2l) {
            initMaps(runtime);
            n2l = NID_TO_LN.get(runtime);
        }
        return n2l.get(nid);
    }
    
    synchronized static Map<String, ASN1ObjectIdentifier> getOIDLookup(Ruby runtime) {
        Map<String, ASN1ObjectIdentifier> val = SYM_TO_OID.get(runtime);
        if(null == val) {
            initMaps(runtime);
            val = SYM_TO_OID.get(runtime);
        }
        return val;
    }

    synchronized static Map<ASN1ObjectIdentifier, String> getSymLookup(Ruby runtime) {
        Map<ASN1ObjectIdentifier, String> val = OID_TO_SYM.get(runtime);
        if(null == val) {
            initMaps(runtime);
            val = OID_TO_SYM.get(runtime);
        }
        return val;
    }

    private final static Object[][] ASN1_INFO = {
        {"EOC", null, null },
        {"BOOLEAN", org.bouncycastle.asn1.DERBoolean.class, "Boolean" },
        {"INTEGER", org.bouncycastle.asn1.ASN1Integer.class, "Integer" }, 
        {"BIT_STRING",  org.bouncycastle.asn1.DERBitString.class, "BitString" },
        {"OCTET_STRING",  org.bouncycastle.asn1.DEROctetString.class, "OctetString" },
        {"NULL",  org.bouncycastle.asn1.DERNull.class, "Null" },
        {"OBJECT",  org.bouncycastle.asn1.ASN1ObjectIdentifier.class, "ObjectId" },
        {"OBJECT_DESCRIPTOR",  null, null },
        {"EXTERNAL",  null, null },
        {"REAL",  null, null },
        {"ENUMERATED",  org.bouncycastle.asn1.DEREnumerated.class, "Enumerated" },
        {"EMBEDDED_PDV",  null, null },
        {"UTF8STRING",  org.bouncycastle.asn1.DERUTF8String.class, "UTF8String" },
        {"RELATIVE_OID",  null, null },
        {"[UNIVERSAL 14]",  null, null },
        {"[UNIVERSAL 15]",  null, null },
        {"SEQUENCE",  org.bouncycastle.asn1.DLSequence.class, "Sequence" },
        {"SET",  org.bouncycastle.asn1.DLSet.class, "Set" },
        {"NUMERICSTRING",  org.bouncycastle.asn1.DERNumericString.class, "NumericString" },
        {"PRINTABLESTRING",  org.bouncycastle.asn1.DERPrintableString.class, "PrintableString" },
        {"T61STRING",  org.bouncycastle.asn1.DERT61String.class, "T61String" },
        {"VIDEOTEXSTRING", null, null },
        {"IA5STRING",  org.bouncycastle.asn1.DERIA5String.class, "IA5String" },
        {"UTCTIME",  org.bouncycastle.asn1.DERUTCTime.class, "UTCTime" },
        {"GENERALIZEDTIME",  org.bouncycastle.asn1.DERGeneralizedTime.class, "GeneralizedTime" },
        {"GRAPHICSTRING",  null, null },
        {"ISO64STRING",  null, null },
        {"GENERALSTRING",  org.bouncycastle.asn1.DERGeneralString.class, "GeneralString" },
        {"UNIVERSALSTRING",  org.bouncycastle.asn1.DERUniversalString.class, "UniversalString" },
        {"CHARACTER_STRING",  null, null },
        {"BMPSTRING", org.bouncycastle.asn1.DERBMPString.class, "BMPString" }};

    private final static Map<Class, Integer> CLASS_TO_ID = new HashMap<Class, Integer>();
    private final static Map<String, Integer> RUBYNAME_TO_ID = new HashMap<String, Integer>();
    
    static {
        for(int i=0;i<ASN1_INFO.length;i++) {
            if(ASN1_INFO[i][1] != null) {
                CLASS_TO_ID.put((Class)ASN1_INFO[i][1],new Integer(i));
            }
            if(ASN1_INFO[i][2] != null) {
                RUBYNAME_TO_ID.put((String)ASN1_INFO[i][2],new Integer(i));
            }
        }
    }

    public static int idForClass(Class type) {
        Integer v = null;
        while(type != Object.class && v == null) {
            v = CLASS_TO_ID.get(type);
            if(v == null) {
                type = type.getSuperclass();
            }
        }
        return null == v ? -1 : v.intValue();
    }

    public static int idForRubyName(String name) {
        Integer v = RUBYNAME_TO_ID.get(name);
        return null == v ? -1 : v.intValue();
    }

    public static Class<? extends ASN1Encodable> classForId(int id) {
        @SuppressWarnings("unchecked")
        Class<? extends ASN1Encodable> result = (Class<? extends ASN1Encodable>)(ASN1_INFO[id][1]);
        return result;
    }
    
    public static void createASN1(Ruby runtime, RubyModule ossl) {
        RubyModule mASN1 = ossl.defineModuleUnder("ASN1");
        RubyClass openSSLError = ossl.getClass("OpenSSLError");
        mASN1.defineClassUnder("ASN1Error",openSSLError, openSSLError.getAllocator());

        mASN1.defineAnnotatedMethods(ASN1.class);

        List<IRubyObject> ary = new ArrayList<IRubyObject>();
        mASN1.setConstant("UNIVERSAL_TAG_NAME",runtime.newArray(ary));
        for(int i=0;i<ASN1_INFO.length;i++) {
            if(((String)(ASN1_INFO[i][0])).charAt(0) != '[') {
                ary.add(runtime.newString(((String)(ASN1_INFO[i][0]))));
                mASN1.setConstant(((String)(ASN1_INFO[i][0])),runtime.newFixnum(i));
            } else {
                ary.add(runtime.getNil());
            }
        }

        RubyClass cASN1Data = mASN1.defineClassUnder("ASN1Data",runtime.getObject(), ASN1Data.ALLOCATOR);
        cASN1Data.addReadWriteAttribute(runtime.getCurrentContext(), "value");
        cASN1Data.addReadWriteAttribute(runtime.getCurrentContext(), "tag");
        cASN1Data.addReadWriteAttribute(runtime.getCurrentContext(), "tag_class");
        cASN1Data.defineAnnotatedMethods(ASN1Data.class);

        RubyClass cASN1Primitive = mASN1.defineClassUnder("Primitive",cASN1Data, ASN1Primitive.ALLOCATOR);
        cASN1Primitive.addReadWriteAttribute(runtime.getCurrentContext(), "tagging");
        cASN1Primitive.defineAnnotatedMethods(ASN1Primitive.class);

        RubyClass cASN1Constructive = mASN1.defineClassUnder("Constructive",cASN1Data,ASN1Constructive.ALLOCATOR);
        cASN1Constructive.includeModule(runtime.getModule("Enumerable"));
        cASN1Constructive.addReadWriteAttribute(runtime.getCurrentContext(), "tagging");
        cASN1Constructive.defineAnnotatedMethods(ASN1Constructive.class);

        mASN1.defineClassUnder("Boolean",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("Integer",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("Enumerated",cASN1Primitive,cASN1Primitive.getAllocator());

        RubyClass cASN1BitString = mASN1.defineClassUnder("BitString",cASN1Primitive,cASN1Primitive.getAllocator());

        mASN1.defineClassUnder("OctetString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("UTF8String",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("NumericString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("PrintableString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("T61String",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("VideotexString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("IA5String",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("GraphicString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("ISO64String",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("GeneralString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("UniversalString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("BMPString",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("Null",cASN1Primitive,cASN1Primitive.getAllocator());

        RubyClass cASN1ObjectId = mASN1.defineClassUnder("ObjectId",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("UTCTime",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("GeneralizedTime",cASN1Primitive,cASN1Primitive.getAllocator());
        mASN1.defineClassUnder("Sequence",cASN1Constructive,cASN1Constructive.getAllocator());
        mASN1.defineClassUnder("Set",cASN1Constructive,cASN1Constructive.getAllocator());

        cASN1ObjectId.defineAnnotatedMethods(ObjectId.class);

        cASN1BitString.addReadWriteAttribute(runtime.getCurrentContext(), "unused_bits");
    }


    private static String getShortNameFor(Ruby runtime, String nameOrOid) {
        ASN1ObjectIdentifier oid = getObjectIdentifier(runtime,nameOrOid);
        Map<String, ASN1ObjectIdentifier> em = getOIDLookup(runtime);
        String name = null;
        for(Iterator<String> iter = em.keySet().iterator();iter.hasNext();) {
            String key = iter.next();
            if(oid.equals(em.get(key))) {
                if(name == null || key.length() < name.length()) {
                    name = key;
                }
            }
        }
        return name;
    }

    private static String getLongNameFor(Ruby runtime, String nameOrOid) {
        ASN1ObjectIdentifier oid = getObjectIdentifier(runtime,nameOrOid);
        Map<String, ASN1ObjectIdentifier> em = getOIDLookup(runtime);
        String name = null;
        for(Iterator<String> iter = em.keySet().iterator();iter.hasNext();) {
            String key = iter.next();
            if(oid.equals(em.get(key))) {
                if(name == null || key.length() > name.length()) {
                    name = key;
                }
            }
        }
        return name;
    }

    private static ASN1ObjectIdentifier getObjectIdentifier(Ruby runtime, String nameOrOid) {
        Object val1 = ASN1.getOIDLookup(runtime).get(nameOrOid.toLowerCase());
        if(null != val1) {
            return (ASN1ObjectIdentifier)val1;
        }
        ASN1ObjectIdentifier val2 = new ASN1ObjectIdentifier(nameOrOid);
        return val2;
    }
    
    @JRubyMethod(name="Boolean", module=true, rest=true)
    public static IRubyObject fact_Boolean(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("Boolean").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="Integer", module=true, rest=true)
    public static IRubyObject fact_Integer(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("Integer").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="Enumerated", module=true, rest=true)
    public static IRubyObject fact_Enumerated(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("Enumerated").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="BitString", module=true, rest=true)
    public static IRubyObject fact_BitString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("BitString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="OctetString", module=true, rest=true)
    public static IRubyObject fact_OctetString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("OctetString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="UTF8String", module=true, rest=true)
    public static IRubyObject fact_UTF8String(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("UTF8String").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="NumericString", module=true, rest=true)
    public static IRubyObject fact_NumericString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("NumericString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="PrintableString", module=true, rest=true)
    public static IRubyObject fact_PrintableString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("PrintableString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="T61String", module=true, rest=true)
    public static IRubyObject fact_T61String(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("T61String").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="VideotexString", module=true, rest=true)
    public static IRubyObject fact_VideotexString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("VideotexString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="IA5String", module=true, rest=true)
    public static IRubyObject fact_IA5String(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("IA5String").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="GraphicString", module=true, rest=true)
    public static IRubyObject fact_GraphicString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("GraphicString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="ISO64String", module=true, rest=true)
    public static IRubyObject fact_ISO64String(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("ISO64String").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="GeneralString", module=true, rest=true)
    public static IRubyObject fact_GeneralString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("GeneralString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="UniversalString", module=true, rest=true)
    public static IRubyObject fact_UniversalString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("UniversalString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="BMPString", module=true, rest=true)
    public static IRubyObject fact_BMPString(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("BMPString").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="Nul", module=true, rest=true)
    public static IRubyObject fact_Null(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("Null").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="ObjectId", module=true, rest=true)
    public static IRubyObject fact_ObjectId(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("ObjectId").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="UTCTime", module=true, rest=true)
    public static IRubyObject fact_UTCTime(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("UTCTime").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="GeneralizedTime", module=true, rest=true)
    public static IRubyObject fact_GeneralizedTime(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("GeneralizedTime").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="Sequence", module=true, rest=true)
    public static IRubyObject fact_Sequence(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("Sequence").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(name="Set", module=true, rest=true)
    public static IRubyObject fact_Set(IRubyObject recv, IRubyObject[] args) {
        return ((RubyModule)recv).getClass("Set").callMethod(recv.getRuntime().getCurrentContext(),"new",args);
    }

    @JRubyMethod(meta=true, required=1)
    public static IRubyObject traverse(IRubyObject recv, IRubyObject a) {
        System.err.println("WARNING: unimplemented method called: traverse");
        return null;
    }

    public static class ObjectId {
        @JRubyMethod(meta=true, rest=true)
        public static IRubyObject register(IRubyObject recv, IRubyObject[] args) {
            ASN1ObjectIdentifier deroi = new ASN1ObjectIdentifier(args[0].toString());
            getOIDLookup(recv.getRuntime()).put(args[1].toString().toLowerCase(),deroi);
            getOIDLookup(recv.getRuntime()).put(args[2].toString().toLowerCase(),deroi);
            getSymLookup(recv.getRuntime()).put(deroi,args[1].toString());
            return recv.getRuntime().getTrue();
        }

        @JRubyMethod(name={"sn","short_name"})
        public static IRubyObject sn(IRubyObject self) {
            return self.getRuntime().newString(getShortNameFor(self.getRuntime(),self.callMethod(self.getRuntime().getCurrentContext(),"value").toString()));
        }

        @JRubyMethod(name={"ln","long_name"})
        public static IRubyObject ln(IRubyObject self) {
            return self.getRuntime().newString(getLongNameFor(self.getRuntime(),self.callMethod(self.getRuntime().getCurrentContext(),"value").toString()));
        }

        @JRubyMethod
        public static IRubyObject oid(IRubyObject self) {
            return self.getRuntime().newString(getObjectIdentifier(self.getRuntime(),self.callMethod(self.getRuntime().getCurrentContext(),"value").toString()).getId());
        }
    }

    private final static DateFormat dateF = new SimpleDateFormat("yyyyMMddHHmmssz");
    private static IRubyObject decodeObj(RubyModule asnM,Object v) throws IOException, java.text.ParseException {
        int ix = idForClass(v.getClass());
        String v_name = ix == -1 ? null : (String)(ASN1_INFO[ix][2]);
        ThreadContext tc = asnM.getRuntime().getCurrentContext();
        if(null != v_name) {
            RubyClass c = asnM.getClass(v_name);
            if(v instanceof DERBitString) {
                ByteList bl = new ByteList(((DERBitString)v).getBytes(), false);
                IRubyObject bString = c.callMethod(tc,"new",asnM.getRuntime().newString(bl));
                bString.callMethod(tc,"unused_bits=",asnM.getRuntime().newFixnum(((DERBitString)v).getPadBits()));
                return bString;
            } else if(v instanceof ASN1String) {
                ByteList val; 
                if (v instanceof DERUTF8String) {
                    val = new ByteList(((DERUTF8String) v).getString().getBytes("UTF-8"));
                } else {
                    val = ByteList.create(((ASN1String)v).getString());
                }
                return c.callMethod(tc,"new",asnM.getRuntime().newString(val));
            } else if(v instanceof ASN1Sequence) {
                List<IRubyObject> l = new ArrayList<IRubyObject>();
                for(Enumeration enm = ((ASN1Sequence)v).getObjects(); enm.hasMoreElements(); ) {
                    l.add(decodeObj(asnM,enm.nextElement()));
                }
                return c.callMethod(tc,"new",asnM.getRuntime().newArray(l));
            } else if(v instanceof ASN1Set) {
                List<IRubyObject> l = new ArrayList<IRubyObject>();
                for(Enumeration enm = ((ASN1Set)v).getObjects(); enm.hasMoreElements(); ) {
                    l.add(decodeObj(asnM,enm.nextElement()));
                }
                return c.callMethod(tc,"new",asnM.getRuntime().newArray(l));
            } else if(v instanceof DERNull) {
                return c.callMethod(tc,"new",asnM.getRuntime().getNil());
            } else if(v instanceof ASN1Integer) {
                return c.callMethod(tc, "new", BN.newBN(asnM.getRuntime(), ((ASN1Integer) v).getValue()));
            } else if(v instanceof DERUTCTime) {
                Date d = dateF.parse(((DERUTCTime)v).getAdjustedTime());
                Calendar cal = Calendar.getInstance();
                cal.setTime(d);
                IRubyObject[] argv = new IRubyObject[6];
                argv[0] = asnM.getRuntime().newFixnum(cal.get(Calendar.YEAR));
                argv[1] = asnM.getRuntime().newFixnum(cal.get(Calendar.MONTH)+1);
                argv[2] = asnM.getRuntime().newFixnum(cal.get(Calendar.DAY_OF_MONTH));
                argv[3] = asnM.getRuntime().newFixnum(cal.get(Calendar.HOUR_OF_DAY));
                argv[4] = asnM.getRuntime().newFixnum(cal.get(Calendar.MINUTE));
                argv[5] = asnM.getRuntime().newFixnum(cal.get(Calendar.SECOND));
                return c.callMethod(tc,"new",asnM.getRuntime().getClass("Time").callMethod(tc,"local",argv));
            } else if(v instanceof ASN1ObjectIdentifier) {
                String av = ((ASN1ObjectIdentifier)v).getId();
                return c.callMethod(tc,"new",asnM.getRuntime().newString(av));
            } else if(v instanceof DEROctetString) {
                ByteList bl = new ByteList(((DEROctetString)v).getOctets(), false);
                return c.callMethod(tc,"new",asnM.getRuntime().newString(bl));
            } else if(v instanceof DERBoolean) {
                return c.callMethod(tc,"new",((DERBoolean)v).isTrue() ? asnM.getRuntime().getTrue() : asnM.getRuntime().getFalse());
            } else {
                System.out.println("Should handle: " + v.getClass().getName());
            }
        } else if(v instanceof ASN1TaggedObject) {
            RubyClass c = asnM.getClass("ASN1Data");
            IRubyObject val = decodeObj(asnM, ((ASN1TaggedObject)v).getObject());
            IRubyObject tag = asnM.getRuntime().newFixnum(((ASN1TaggedObject)v).getTagNo());
            IRubyObject tag_class = asnM.getRuntime().newSymbol("CONTEXT_SPECIFIC");
            return c.callMethod(tc,"new",new IRubyObject[]{asnM.getRuntime().newArray(val),tag,tag_class});
        } else if(v instanceof ASN1Sequence) {
            //Likely a DERSequence returned by bouncycastle libs. Convert to DLSequence.
            RubyClass c = asnM.getClass("Sequence");
            List<IRubyObject> l = new ArrayList<IRubyObject>();
            for(Enumeration enm = ((ASN1Sequence)v).getObjects(); enm.hasMoreElements(); ) {
                l.add(decodeObj(asnM,enm.nextElement()));
            }
            return c.callMethod(tc,"new",asnM.getRuntime().newArray(l));
        } else if(v instanceof ASN1Set) {
            //Likely a DERSet returned by bouncycastle libs. Convert to DLSet.
            RubyClass c = asnM.getClass("Set");
            List<IRubyObject> l = new ArrayList<IRubyObject>();
            for(Enumeration enm = ((ASN1Set)v).getObjects(); enm.hasMoreElements(); ) {
                l.add(decodeObj(asnM,enm.nextElement()));
            }
            return c.callMethod(tc,"new",asnM.getRuntime().newArray(l));
        }

        //Used to return null. Led to confusing exceptions later.
        throw new IllegalArgumentException("jruby-openssl unable to decode object: " + v + "[" + v.getClass().getName() + "]");
    }

    @JRubyMethod(meta = true)
    public static IRubyObject decode(IRubyObject recv, IRubyObject obj) {
        try {
            IRubyObject obj2 = OpenSSLImpl.to_der_if_possible(obj);
            RubyModule asnM = (RubyModule)recv;
            ASN1InputStream asis = new ASN1InputStream(obj2.convertToString().getBytes());
            IRubyObject ret = decodeObj(asnM, asis.readObject());
            return ret;
        } catch(IOException e) {
            throw recv.getRuntime().newIOErrorFromException(e);
        } catch(Exception e) {
            throw recv.getRuntime().newArgumentError(e.getMessage());
        }
    }

    @JRubyMethod(meta=true, required=1)
    public static IRubyObject decode_all(IRubyObject recv, IRubyObject a) {
        System.err.println("WARNING: unimplemented method called: decode_all");
        return null;
    }

    public static class ASN1Data extends RubyObject {
        private static final long serialVersionUID = 6117598347932209839L;

        public static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
                public IRubyObject allocate(Ruby runtime, RubyClass klass) {
                    return new ASN1Data(runtime, klass);
                }
            };
        public ASN1Data(Ruby runtime, RubyClass type) {
            super(runtime,type);
        }

        protected void asn1Error() {
            asn1Error(null);
        }

        protected void asn1Error(String msg) {
            throw Utils.newError(getRuntime(), "OpenSSL::ASN1::ASN1Error", msg);
        }

        @JRubyMethod
        public IRubyObject initialize(IRubyObject value, IRubyObject tag, IRubyObject tag_class) {
            if(!(tag_class instanceof RubySymbol)) {
                asn1Error("invalid tag class");
            }
            if(tag_class.toString().equals(":UNIVERSAL") && RubyNumeric.fix2int(tag) > 31) {
                asn1Error("tag number for Universal too large");
            }
            ThreadContext tc = getRuntime().getCurrentContext();
            this.callMethod(tc,"tag=", tag);
            this.callMethod(tc,"value=", value);
            this.callMethod(tc,"tag_class=", tag_class);

            return this;
        }

        ASN1Encodable toASN1() {
            ThreadContext tc = getRuntime().getCurrentContext();
            int tag = RubyNumeric.fix2int(callMethod(tc,"tag"));
            IRubyObject val = callMethod(tc,"value");
            if(val instanceof RubyArray) {
                RubyArray arr = (RubyArray)callMethod(tc,"value");
                if(arr.size() > 1) {
                    ASN1EncodableVector vec = new ASN1EncodableVector();
                    for (IRubyObject obj : arr.toJavaArray()) {
                        vec.add(((ASN1Data)obj).toASN1());
                    }
                    return new DERTaggedObject(tag, new DLSequence(vec));
                } else {
                    return new DERTaggedObject(tag,((ASN1Data)(arr.getList().get(0))).toASN1());
                }
            } else {
                return new DERTaggedObject(tag, ((ASN1Data)val).toASN1());
            }
        }

        @JRubyMethod
        public IRubyObject to_der() {
            try {
                return getRuntime().newString(new ByteList(toASN1().toASN1Primitive().getEncoded(ASN1Encoding.DER),false));
            } catch (IOException ex) {
                throw Utils.newError(getRuntime(), "OpenSSL::ASN1::ASN1Error", ex.getMessage());
            }
        }

        protected IRubyObject defaultTag() {
            int i = idForRubyName(getMetaClass().getRealClass().getBaseName());
            if(i != -1) {
                return getRuntime().newFixnum(i);
            } else {
                return getRuntime().getNil();
            }
        }

        protected void print() {
            print(0);
        }

        protected void printIndent(int indent) {
            for(int i=0;i<indent;i++) {
                System.out.print(" ");
            }
        }

        protected void print(int indent) {
            printIndent(indent);
            System.out.println("ASN1Data: ");
            IRubyObject val = callMethod(getRuntime().getCurrentContext(),"value");
            if(val instanceof RubyArray) {
                RubyArray arr = (RubyArray)val;
                for (IRubyObject obj : arr.toJavaArray()) {
                    ((ASN1Data)obj).print(indent+1);
                }
            } else {
                ((ASN1Data)val).print(indent+1);
            }
        }
    }

    public static class ASN1Primitive extends ASN1Data {
        private static final long serialVersionUID = 8489625559339190259L;

        public static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
                public IRubyObject allocate(Ruby runtime, RubyClass klass) {
                    return new ASN1Primitive(runtime, klass);
                }
            };
        public ASN1Primitive(Ruby runtime, RubyClass type) {
            super(runtime,type);
        }

        public String toString() {
            return this.callMethod(getRuntime().getCurrentContext(),"value").toString();
        }

        @JRubyMethod
        public IRubyObject to_der() {
            return super.to_der();
        }

        @JRubyMethod(required=1, optional=4)
        public IRubyObject initialize(IRubyObject[] args) {
            IRubyObject value = args[0];
            IRubyObject tag = getRuntime().getNil();
            IRubyObject tagging = getRuntime().getNil();
            IRubyObject tag_class = getRuntime().getNil();
            if(args.length>1) {
                tag = args[1];
                if(args.length>2) {
                    tagging = args[2];
                    if(args.length>3) {
                        tag_class = args[3];
                    }
                }
                if(tag.isNil()) {
                    asn1Error("must specify tag number");
                }
                if(tagging.isNil()) {
                    tagging = getRuntime().newSymbol("EXPLICIT");
                }
                if(!(tagging instanceof RubySymbol)) {
                    asn1Error("invalid tag default");
                }
                if(tag_class.isNil()) {
                    tag_class = getRuntime().newSymbol("CONTEXT_SPECIFIC");
                }
                if(!(tag_class instanceof RubySymbol)) {
                    asn1Error("invalid tag class");
                }
                if(tagging.toString().equals(":IMPLICIT") && RubyNumeric.fix2int(tag) > 31) {
                    asn1Error("tag number for Universal too large");
                }
            } else {
                tag = defaultTag();
                tagging = getRuntime().getNil();
                tag_class = getRuntime().newSymbol("UNIVERSAL");
            }
            if("ObjectId".equals(getMetaClass().getRealClass().getBaseName())) {
                String v = getSymLookup(getRuntime()).get(getObjectIdentifier(value.toString()));
                if(v != null) {
                    value = getRuntime().newString(v);
                }
            }
            ThreadContext tc = getRuntime().getCurrentContext();
            this.callMethod(tc,"tag=",tag);
            this.callMethod(tc,"value=",value);
            this.callMethod(tc,"tagging=",tagging);
            this.callMethod(tc,"tag_class=",tag_class);

            return this;
        }

        private ASN1ObjectIdentifier getObjectIdentifier(String nameOrOid) {
            Object val1 = ASN1.getOIDLookup(getRuntime()).get(nameOrOid.toLowerCase());
            if(null != val1) {
                return (ASN1ObjectIdentifier)val1;
            }
            ASN1ObjectIdentifier val2 = new ASN1ObjectIdentifier(nameOrOid);
            return val2;
        }

        ASN1Encodable toASN1() {
            //            System.err.println(getMetaClass().getRealClass().getBaseName()+"#toASN1");
            int tag = idForRubyName(getMetaClass().getRealClass().getBaseName());
            @SuppressWarnings("unchecked") Class<? extends ASN1Encodable> imp = (Class<? extends ASN1Encodable>)ASN1_INFO[tag][1];
            IRubyObject val = callMethod(getRuntime().getCurrentContext(),"value");
            if(imp == ASN1ObjectIdentifier.class) {
                return getObjectIdentifier(val.toString());
            } else if(imp == DERNull.class) {
                return new DERNull();
            } else if(imp == DERBoolean.class) {
                return new DERBoolean(val.isTrue());
            } else if(imp == DERUTCTime.class) {
                return new DERUTCTime(((RubyTime)val).getJavaDate());
            } else if(imp == ASN1Integer.class && val instanceof RubyBignum) {
                return new ASN1Integer(((RubyBignum)val).getValue());
            } else if(imp == ASN1Integer.class) {
                return new ASN1Integer(new BigInteger(val.toString()));
            } else if(imp == DEROctetString.class) {
                return new DEROctetString(val.convertToString().getBytes());
            } else if(imp == DERBitString.class) {
                byte[] bs = val.convertToString().getBytes();
                int unused = 0;
                for(int i = (bs.length-1); i>-1; i--) {
                    if(bs[i] == 0) {
                        unused += 8;
                    } else {
                        byte v2 = bs[i];
                        int x = 8;
                        while(v2 != 0) {
                            v2 <<= 1;
                            x--;
                        }
                        unused += x;
                        break;
                    }
                }
                return new DERBitString(bs,unused);
            } else if(val instanceof RubyString) {
                try {
                    return imp.getConstructor(String.class).newInstance(val.toString());
                } catch (Exception ex) {
                    throw RaiseException.createNativeRaiseException(getRuntime(), ex);
                }
            }
            
            //TODO throw an exception here too?
            System.err.println("object with tag: " + tag + " and value: " + val + " and val.class: " + val.getClass().getName() + " and impl: " + imp.getName());
            System.err.println("WARNING: unimplemented method called: asn1data#toASN1");
            return null;    
        }

        protected void print(int indent) {
            printIndent(indent);
            System.out.println(getMetaClass().getRealClass().getBaseName() + ": " + callMethod(getRuntime().getCurrentContext(),"value").callMethod(getRuntime().getCurrentContext(),"inspect").toString());
        }
    }

    public static class ASN1Constructive extends ASN1Data {
        private static final long serialVersionUID = -7166662655104776828L;
        
        public static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
                public IRubyObject allocate(Ruby runtime, RubyClass klass) {
                    return new ASN1Constructive(runtime, klass);
                }
            };
        public ASN1Constructive(Ruby runtime, RubyClass type) {
            super(runtime,type);
        }

        @JRubyMethod
        public IRubyObject to_der() {
            return super.to_der();
        }

        @JRubyMethod(required=1, optional=3)
        public IRubyObject initialize(IRubyObject[] args) {
            IRubyObject value = args[0];
            IRubyObject tag = getRuntime().getNil();
            IRubyObject tagging = getRuntime().getNil();
            IRubyObject tag_class = getRuntime().getNil();
            if(args.length>1) {
                tag = args[1];
                if(args.length>2) {
                    tagging = args[2];
                    if(args.length>3) {
                        tag_class = args[3];
                    }
                }
                if(tag.isNil()) {
                    asn1Error("must specify tag number");
                }
                if(tagging.isNil()) {
                    tagging = getRuntime().newSymbol("EXPLICIT");
                }
                if(!(tagging instanceof RubySymbol)) {
                    asn1Error("invalid tag default");
                }
                if(tag_class.isNil()) {
                    tag_class = getRuntime().newSymbol("CONTEXT_SPECIFIC");
                }
                if(!(tag_class instanceof RubySymbol)) {
                    asn1Error("invalid tag class");
                }
                if(tagging.toString().equals(":IMPLICIT") && RubyNumeric.fix2int(tag) > 31) {
                    asn1Error("tag number for Universal too large");
                }
            } else {
                tag = defaultTag();
                tagging = getRuntime().getNil();
                tag_class = getRuntime().newSymbol("UNIVERSAL");
            }
            ThreadContext tc = getRuntime().getCurrentContext();
            this.callMethod(tc,"tag=",tag);
            this.callMethod(tc,"value=",value);
            this.callMethod(tc,"tagging=",tagging);
            this.callMethod(tc,"tag_class=",tag_class);

            return this;
        }

        ASN1Encodable toASN1() {
            //            System.err.println(getMetaClass().getRealClass().getBaseName()+"#toASN1");
            int id = idForRubyName(getMetaClass().getRealClass().getBaseName());
            if(id != -1) {
                ASN1EncodableVector vec = new ASN1EncodableVector();
                RubyArray arr = (RubyArray)callMethod(getRuntime().getCurrentContext(),"value");
                for (IRubyObject obj : arr.toJavaArray()) {
                    if(obj instanceof ASN1Data) {
                        vec.add(((ASN1Data)obj).toASN1());
                    } else {
                        vec.add(((ASN1Data) ASN1.decode(getRuntime().getClassFromPath("OpenSSL::ASN1"), OpenSSLImpl.to_der_if_possible(obj))).toASN1());
                    }
                }
                try {
                    @SuppressWarnings("unchecked")
                    ASN1Encodable result = ((Class<? extends ASN1Encodable>) (ASN1_INFO[id][1])).getConstructor(new Class[] { ASN1EncodableVector.class })
                            .newInstance(new Object[] { vec });
                    return result;
                } catch (Exception e) {
                    // TODO: deprecated
                    throw RaiseException.createNativeRaiseException(getRuntime(), e);
                }
            }
            return null;
        }

        @JRubyMethod
        public IRubyObject each(Block block) {
            RubyArray arr = (RubyArray) callMethod(getRuntime().getCurrentContext(), "value");
            for (IRubyObject obj : arr.toJavaArray()) {
                block.yield(getRuntime().getCurrentContext(), obj);
            }
            return getRuntime().getNil();
        }

        protected void print(int indent) {
            printIndent(indent);
            System.out.println(getMetaClass().getRealClass().getBaseName() + ": ");
            RubyArray arr = (RubyArray)callMethod(getRuntime().getCurrentContext(),"value");
            for (IRubyObject obj : arr.toJavaArray()) {
                ((ASN1Data)obj).print(indent+1);
            }
        }
    }
}// ASN1
