/***** 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.security.cert.CRLException;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubyTime;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.Visibility;

import org.jruby.ext.openssl.x509store.X509AuxCertificate;
import org.jruby.ext.openssl.x509store.StoreContext;

import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
import static org.jruby.ext.openssl.OpenSSL.warn;
import static org.jruby.ext.openssl.X509._X509;
import static org.jruby.ext.openssl.X509CRL._CRL;
import static org.jruby.ext.openssl.X509Cert._Certificate;
import static org.jruby.ext.openssl.x509store.X509Utils.verifyCertificateErrorString;

/**
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class X509StoreContext extends RubyObject {
    private static final long serialVersionUID = -4165247923898746888L;

    private static ObjectAllocator X509STORECTX_ALLOCATOR = new ObjectAllocator() {
        public IRubyObject allocate(Ruby runtime, RubyClass klass) {
            return new X509StoreContext(runtime, klass);
        }
    };

    public static void createX509StoreContext(final Ruby runtime, final RubyModule X509) {
        RubyClass StoreContext = X509.defineClassUnder("StoreContext", runtime.getObject(), X509STORECTX_ALLOCATOR);
        StoreContext.defineAnnotatedMethods(X509StoreContext.class);
        StoreContext.undefineMethod("dup");
    }

    private static RubyClass _StoreContext(final Ruby runtime) {
        return _X509(runtime).getClass("StoreContext");
    }

    private StoreContext storeContext;

    public X509StoreContext(Ruby runtime, RubyClass type) {
        super(runtime, type);
    }

    // constructor for creating callback parameter object of verify_cb
    private X509StoreContext(Ruby runtime, RubyClass type, StoreContext storeContext) {
        super(runtime, type);
        this.storeContext = storeContext;
    }

    static X509StoreContext newStoreContext(final Ruby runtime, final StoreContext storeContext) {
        return new X509StoreContext(runtime, _StoreContext(runtime), storeContext);
    }

    static X509StoreContext newStoreContext(final ThreadContext context, final X509Store store,
        final IRubyObject cert, final IRubyObject chain) {
        final Ruby runtime = context.runtime;
        X509StoreContext instance = new X509StoreContext(runtime, _StoreContext(runtime));
        instance.initialize(context, new IRubyObject[] { store, cert, chain } );
        return instance;
    }

    @JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
        X509Store store; IRubyObject cert, chain; cert = chain = context.nil;

        store = (X509Store) args[0];

        if ( Arity.checkArgumentCount(context.runtime, args, 1, 3) > 1 ) {
            cert = args[1];
            if ( args.length > 2) chain = args[2];
        }

        final X509AuxCertificate _cert;
        if (cert.isNil()) {
            _cert = null;
        }
        else {
            if ( ! (cert instanceof X509Cert) ) {
                throw context.runtime.newTypeError(cert, "OpenSSL::X509::Certificate");
            }
            _cert = ((X509Cert) cert).getAuxCert();
        }
        final List<X509AuxCertificate> _chain;
        if ( ! chain.isNil() ) {
            @SuppressWarnings("unchecked")
            final RubyArray certs = (RubyArray) chain;
            _chain = new ArrayList<X509AuxCertificate>( certs.size() );

            for (int i = 0; i < certs.size(); i++) {
                // NOTE: if we use the normal java syntax for iterating over this
                // RubyArray, the `toJava` method of the X509Cert class will be
                // implicitly called, and that will return the BC certificate object
                // rather than the JRuby one.
                X509Cert c = (X509Cert) certs.eltOk(i);
                _chain.add(c.getAuxCert());
            }
        }
        else {
            _chain = new ArrayList<X509AuxCertificate>(4);
        }

        this.storeContext = new StoreContext(store.getStore());
        if ( storeContext.init(_cert, _chain) != 1 ) {
            throw newStoreError(context.runtime, null);
        }

        IRubyObject time = store.getInstanceVariables().getInstanceVariable("@time");
        if ( ! time.isNil() ) set_time(time);
        this.setInstanceVariable("@verify_callback", store.verify_callback());
        this.setInstanceVariable("@cert", cert);
        return this;
    }

    @JRubyMethod
    public IRubyObject verify(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        storeContext.setExtraData(1, getInstanceVariable("@verify_callback"));
        try {
            final int result = storeContext.verifyCertificate();
            return result != 0 ? runtime.getTrue() : runtime.getFalse();
        }
        catch (Exception e) {
            debugStackTrace(runtime, e);
            // TODO: define suitable exception for jopenssl and catch it.
            throw newStoreError(runtime, e.getMessage());
        }
    }

    @JRubyMethod
    public IRubyObject chain(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        final List<X509AuxCertificate> chain = storeContext.getChain();
        if ( chain == null ) return runtime.getNil();

        final RubyArray result = runtime.newArray(chain.size());
        final RubyClass _Certificate = _Certificate(runtime);
        try {
            for (X509AuxCertificate x509 : chain) {
                RubyString encoded = StringHelper.newString(runtime, x509.getEncoded());
                result.append( _Certificate.callMethod( context, "new", encoded ) );
            }
        }
        catch (CertificateEncodingException e) {
            throw newStoreError(runtime, e.getMessage());
        }
        return result;
    }

    @JRubyMethod
    public IRubyObject error(final ThreadContext context) {
        return context.runtime.newFixnum( storeContext.getError() );
    }

    @JRubyMethod(name="error=")
    public IRubyObject set_error(final IRubyObject error) {
        storeContext.setError( RubyNumeric.fix2int(error) );
        return error;
    }

    @JRubyMethod
    public IRubyObject error_string(final ThreadContext context) {
        final int error = storeContext.getError();
        return context.runtime.newString( verifyCertificateErrorString(error) );
    }

    @JRubyMethod
    public IRubyObject error_depth(final ThreadContext context) {
        final int depth = storeContext.getErrorDepth();
        return context.runtime.newFixnum( depth );
    }

    @JRubyMethod
    public IRubyObject current_cert(final ThreadContext context) {
        final X509AuxCertificate x509 = storeContext.getCurrentCertificate();
        try {
            return X509Cert.wrap(context, x509.getEncoded());
        }
        catch (CertificateEncodingException e) {
            throw newStoreError(context.runtime, e.getMessage());
        }
    }

    @JRubyMethod
    public IRubyObject current_crl(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        final RubyClass _CRL = _CRL(runtime);
        try {
            final java.security.cert.X509CRL crl = storeContext.getCurrentCRL();
            return _CRL.callMethod(context, "new", StringHelper.newString(runtime, crl.getEncoded()));
        }
        catch (CRLException e) {
            throw newStoreError(runtime, e.getMessage());
        }
    }

    @JRubyMethod
    public IRubyObject cleanup(final ThreadContext context) {
        try {
            storeContext.cleanup();
        }
        catch (RuntimeException e) {
            throw e;
        }
        catch (Exception e) {
            debugStackTrace(context.runtime, e);
            throw newStoreError(context.runtime, e.getMessage());
        }
        return context.runtime.getNil();
    }

    @JRubyMethod(name = "flags=")
    public IRubyObject set_flags(final ThreadContext context, final IRubyObject arg) {
        storeContext.setFlags(RubyFixnum.fix2long((RubyFixnum)arg));
        return arg;
    }

    @JRubyMethod(name = "purpose=")
    public IRubyObject set_purpose(final ThreadContext context, final IRubyObject arg) {
        storeContext.setPurpose(RubyFixnum.fix2int((RubyFixnum)arg));
        return arg;
    }

    @JRubyMethod(name = "trust=")
    public IRubyObject set_trust(final ThreadContext context, final IRubyObject arg) {
        storeContext.setTrust(RubyFixnum.fix2int((RubyFixnum)arg));
        return arg;
    }

    @JRubyMethod(name = "time=")
    public IRubyObject set_time(IRubyObject arg) {
        storeContext.setTime( 0, ( (RubyTime) arg ).getJavaDate() );
        return arg;
    }

    private static RaiseException newStoreError(Ruby runtime, String message) {
        return Utils.newError(runtime, _X509(runtime).getClass("StoreError"), message);
    }

}// X509StoreContext
