/***** 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 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 static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
import static org.jruby.ext.openssl.OpenSSL.warn;
import static org.jruby.ext.openssl.X509._X509;

import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.openssl.x509store.Store;
import org.jruby.ext.openssl.x509store.StoreContext;
import org.jruby.ext.openssl.x509store.X509AuxCertificate;
import org.jruby.ext.openssl.x509store.X509Error;
import org.jruby.ext.openssl.x509store.X509Utils;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

/**
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class X509Store extends RubyObject {

    private static final long serialVersionUID = -2969708892287379665L;

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

    public static void createX509Store(final Ruby runtime, final RubyModule X509) {
        RubyClass Store = X509.defineClassUnder("Store", runtime.getObject(), X509STORE_ALLOCATOR);
        RubyClass OpenSSLError = runtime.getModule("OpenSSL").getClass("OpenSSLError");
        X509.defineClassUnder("StoreError", OpenSSLError, OpenSSLError.getAllocator());

        final ThreadContext context = runtime.getCurrentContext();

        Store.addReadWriteAttribute(context, "error");
        Store.addReadWriteAttribute(context, "error_string");
        Store.addReadWriteAttribute(context, "chain");
        Store.defineAnnotatedMethods(X509Store.class);
        Store.undefineMethod("dup");

        X509StoreContext.createX509StoreContext(runtime, X509);
    }

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

    private X509Store(Ruby runtime) {
        super(runtime, _X509(runtime).getClass("Store"));
    }

    static X509Store newStore(final Ruby runtime) {
        final X509Store store = new X509Store(runtime);
        store.initialize(runtime.getCurrentContext(), NULL_ARRAY);
        return store;
    }

    private final Store store = new Store();

    final Store getStore() { return store; }

    @JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
        final Ruby runtime = context.runtime;

        final IRubyObject nil = runtime.getNil();
        final IRubyObject zero = RubyFixnum.zero(runtime);

        store.setVerifyCallbackFunction(verifyCallback);

        this.set_verify_callback(nil);
        this.setInstanceVariable("@flags", zero);
        this.setInstanceVariable("@purpose", zero);
        this.setInstanceVariable("@trust", zero);

        this.setInstanceVariable("@error", nil);
        this.setInstanceVariable("@error_string", nil);
        this.setInstanceVariable("@chain", nil);
        this.setInstanceVariable("@time", nil);
        return this;
    }

    @JRubyMethod
    public IRubyObject verify_callback() {
        return this.getInstanceVariable("@verify_callback");
    }

    @JRubyMethod(name = "verify_callback=")
    public IRubyObject set_verify_callback(final IRubyObject callback) {
        store.setExtraData(1, callback);
        this.setInstanceVariable("@verify_callback", callback);
        return callback;
    }

    @JRubyMethod(name = "flags=")
    public IRubyObject set_flags(final IRubyObject arg) {
        store.setFlags(RubyNumeric.fix2long(arg));
        return arg;
    }

    @JRubyMethod(name = "purpose=")
    public IRubyObject set_purpose(final IRubyObject arg) {
        store.setPurpose(RubyNumeric.fix2int(arg));
        return arg;
    }

    @JRubyMethod(name = "trust=")
    public IRubyObject set_trust(final IRubyObject arg) {
        store.setTrust(RubyNumeric.fix2int(arg));
        return arg;
    }

    @JRubyMethod(name = "time=")
    public IRubyObject set_time(final IRubyObject arg) {
        setInstanceVariable("@time", arg);
        return arg;
    }

    @JRubyMethod
    public IRubyObject add_path(final ThreadContext context, final IRubyObject arg) {
        warn(context, "WARNING: unimplemented method called: Store#add_path");
        return context.nil;
    }

    @JRubyMethod
    public IRubyObject add_file(final IRubyObject arg) {
        String file = arg.toString();
        final Ruby runtime = getRuntime();
        try {
            store.loadLocations(runtime, file, null);
        }
        catch (Exception e) {
            debugStackTrace(runtime, e);
            throw newStoreError(runtime, "loading file failed: ", e);
        }
        return this;
    }

    @JRubyMethod
    public IRubyObject set_default_paths(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        try {
            store.setDefaultPaths(runtime);
        }
        catch (Exception e) {
            debugStackTrace(runtime, e);
            throw newStoreError(runtime, "setting default path failed: ", e);
        }
        return runtime.getNil();
    }

    @JRubyMethod
    public X509Store add_cert(final IRubyObject cert) {
        X509AuxCertificate auxCert = cert instanceof X509Cert ? ((X509Cert) cert).getAuxCert() : null;
        if ( store.addCertificate(auxCert) != 1 ) {
            throw newStoreError(getRuntime(), X509Error.getLastErrorMessage());
        }
        return this;
    }

    @JRubyMethod
    public X509Store add_crl(final IRubyObject crl) {
        java.security.cert.X509CRL jCRL = (crl instanceof X509CRL) ? ((X509CRL) crl).getCRL() : null;
        if ( store.addCRL(jCRL) != 1 ) {
            throw newStoreError(getRuntime(), X509Error.getLastErrorMessage());
        }
        return this;
    }

    @JRubyMethod(rest = true)
    public IRubyObject verify(final ThreadContext context, final IRubyObject[] args, final Block block) {
        final Ruby runtime = context.runtime;
        final IRubyObject cert = args[0], chain;
        if ( Arity.checkArgumentCount(runtime, args, 1, 2) == 2 ) {
            chain = args[1];
        } else {
            chain = runtime.getNil();
        }

        final IRubyObject verify_callback;
        if (block.isGiven()) {
            verify_callback = runtime.newProc(Block.Type.PROC, block);
        } else {
            verify_callback = getInstanceVariable("@verify_callback");
        }

        final X509StoreContext store_context = X509StoreContext.newStoreContext(context, this, cert, chain);
        store_context.setInstanceVariable("@verify_callback", verify_callback);

        IRubyObject result = store_context.callMethod(context, "verify");
        this.setInstanceVariable("@error", store_context.error(context));
        this.setInstanceVariable("@error_string", store_context.error_string(context));
        this.setInstanceVariable("@chain", store_context.chain(context));
        return result;
    }

    private static Store.VerifyCallbackFunction verifyCallback = new Store.VerifyCallbackFunction() {

        public int call(final StoreContext context, final Integer outcome) {
            int ok = outcome.intValue();
            IRubyObject proc = (IRubyObject) context.getExtraData(1);
            if (proc == null) {
                proc = (IRubyObject) context.getStore().getExtraData(0);
            }

            if ( proc == null ) return ok;

            if ( ! proc.isNil() ) {
                final Ruby runtime = proc.getRuntime();
                X509StoreContext store_context = X509StoreContext.newStoreContext(runtime, context);
                IRubyObject ret = proc.callMethod(runtime.getCurrentContext(), "call",
                    new IRubyObject[] { runtime.newBoolean(ok != 0), store_context }
                );
                if (ret.isTrue()) {
                    context.setError(X509Utils.V_OK);
                    ok = 1;
                }
                else {
                    if (context.getError() == X509Utils.V_OK) {
                        context.setError(X509Utils.V_ERR_CERT_REJECTED);
                    }
                    ok = 0;
                }
            }
            return ok;
        }
    };

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

    private static RaiseException newStoreError(final Ruby runtime, final String message) {
        return Utils.newError(runtime, _StoreError(runtime), message);
    }

    private static RaiseException newStoreError(final Ruby runtime, final String message, final Exception e) {
    	return newStoreError(runtime, message + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
    }
}// X509Store
