/***** 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.x509store;

import static org.jruby.ext.openssl.x509store.X509Utils.X509_FILETYPE_DEFAULT;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_FILETYPE_PEM;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_R_CERT_ALREADY_IN_HASH_TABLE;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.net.ssl.X509TrustManager;

import org.jruby.Ruby;

/**
 * c: X509_STORE
 *
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class Store implements X509TrustManager {

    public static interface VerifyFunction extends Function1<StoreContext> {
        public static final VerifyFunction EMPTY = new VerifyFunction(){
            public int call(StoreContext context) {
                return -1;
            }
        };
    }
    public static interface VerifyCallbackFunction extends Function2<StoreContext, Integer> {
        public static final VerifyCallbackFunction EMPTY = new VerifyCallbackFunction(){
            public int call(StoreContext context, Integer outcome) {
                return -1;
            }
        };
    }
    static interface GetIssuerFunction extends Function3<StoreContext, X509AuxCertificate[], X509AuxCertificate> {
        public static final GetIssuerFunction EMPTY = new GetIssuerFunction(){
            public int call(StoreContext context, X509AuxCertificate[] issuer, X509AuxCertificate cert) {
                return -1;
            }
        };
    }
    static interface CheckIssuedFunction extends Function3<StoreContext, X509AuxCertificate, X509AuxCertificate> {
        public static final CheckIssuedFunction EMPTY = new CheckIssuedFunction(){
            public int call(StoreContext context, X509AuxCertificate cert, X509AuxCertificate issuer) throws Exception {
                return -1;
            }
        };
    }
    static interface CheckRevocationFunction extends Function1<StoreContext> {
        public static final CheckRevocationFunction EMPTY = new CheckRevocationFunction(){
            public int call(StoreContext context) {
                return -1;
            }
        };
    }
    static interface GetCRLFunction extends Function3<StoreContext, java.security.cert.X509CRL[], X509AuxCertificate> {
        public static final GetCRLFunction EMPTY = new GetCRLFunction(){
            public int call(StoreContext context, java.security.cert.X509CRL[] crls, X509AuxCertificate cert) {
                return -1;
            }
        };
    }
    static interface CheckCRLFunction extends Function2<StoreContext, java.security.cert.X509CRL> {
        public static final CheckCRLFunction EMPTY = new CheckCRLFunction(){
            public int call(StoreContext context, java.security.cert.X509CRL crl) {
                return -1;
            }
        };
    }
    static interface CertificateCRLFunction extends Function3<StoreContext, java.security.cert.X509CRL, X509AuxCertificate> {
        public static final CertificateCRLFunction EMPTY = new CertificateCRLFunction(){
            public int call(StoreContext context, java.security.cert.X509CRL crl, X509AuxCertificate cert) {
                return -1;
            }
        };
    }
    static interface CleanupFunction extends Function1<StoreContext> {
        public static final CleanupFunction EMPTY = new CleanupFunction(){
            public int call(StoreContext context) {
                return -1;
            }
        };
    }

    // @Deprecated int cache = 1; // not-used

    private volatile X509Object[] objects = new X509Object[0];
    private volatile Lookup[] certLookups = new Lookup[0];

    public final VerifyParameter verifyParameter;

    VerifyFunction verify = VerifyFunction.EMPTY;
    VerifyCallbackFunction verifyCallback = VerifyCallbackFunction.EMPTY;

    GetIssuerFunction getIssuer = GetIssuerFunction.EMPTY;
    CheckIssuedFunction checkIssued = CheckIssuedFunction.EMPTY;
    CheckRevocationFunction checkRevocation = CheckRevocationFunction.EMPTY;
    GetCRLFunction getCRL = GetCRLFunction.EMPTY;
    CheckCRLFunction checkCRL = CheckCRLFunction.EMPTY;
    CertificateCRLFunction certificateCRL = CertificateCRLFunction.EMPTY;
    CleanupFunction cleanup = CleanupFunction.EMPTY;

    private final List<Object> extraData;

    /**
     * c: X509_STORE_new
     */
    public Store() {
        verifyParameter = new VerifyParameter();

        extraData = new ArrayList<Object>(10);
        this.extraData.add(null); this.extraData.add(null); this.extraData.add(null);
        this.extraData.add(null); this.extraData.add(null); this.extraData.add(null);
        this.extraData.add(null); this.extraData.add(null); this.extraData.add(null);
    }

    public List<X509Object> getObjects() {
        return Arrays.asList(objects);
    }

    public List<Lookup> getCertificateMethods() {
        return Arrays.asList(certLookups);
    }

    public VerifyParameter getVerifyParameter() {
        return verifyParameter;
    }

    public VerifyFunction getVerifyFunction() {
        return verify;
    }

    /**
     * c: X509_STORE_set_verify_func
     */
    public void setVerifyFunction(VerifyFunction func) {
        verify = func;
    }

    public VerifyCallbackFunction getVerifyCallback() {
        return verifyCallback;
    }

    /**
     * c: X509_STORE_set_verify_cb_func
     */
    public void setVerifyCallbackFunction(VerifyCallbackFunction func) {
        verifyCallback = func;
    }

    /**
     * c: X509_STORE_free
     */
    public void free() throws Exception {
       for (Lookup lu : certLookups) {
           lu.shutdown();
           lu.free();
        }
        if (verifyParameter != null) {
            verifyParameter.free();
        }
    }

    /**
     * c: X509_set_ex_data
     */
    public int setExtraData(int idx, Object data) {
        synchronized (extraData) {
            extraData.set(idx, data);
            return 1;
        }
    }

    /**
     * c: X509_get_ex_data
     */
    public Object getExtraData(int idx) {
        synchronized (extraData) {
            return extraData.get(idx);
        }
    }

    /**
     * c: X509_STORE_set_depth
     */
    public int setDepth(int depth) {
        verifyParameter.setDepth(depth);
        return 1;
    }

    /**
     * c: X509_STORE_set_flags
     */
    public int setFlags(long flags) {
        return verifyParameter.setFlags(flags);
    }

    /**
     * c: X509_STORE_set_purpose
     */
    public int setPurpose(int purpose) {
        return verifyParameter.setPurpose(purpose);
    }

    /**
     * c: X509_STORE_set_trust
     */
    public int setTrust(int trust) {
        return verifyParameter.setTrust(trust);
    }

    /**
     * c: X509_STORE_set1_param
     */
    public int setParam(VerifyParameter pm) {
        return verifyParameter.set(verifyParameter);
    }

    /**
     * c: X509_STORE_add_lookup
     */
    public Lookup addLookup(final Ruby runtime, final LookupMethod method) {
        final Lookup[] certLookups = this.certLookups;
        Lookup foundLookup = findLookupMethod(certLookups, method);
        if ( foundLookup != null ) return foundLookup;

        final Lookup newLookup = new Lookup(runtime, method); newLookup.store = this;
        synchronized (this) {
            final int length = this.certLookups.length;
            if ( certLookups.length != length ) {
                foundLookup = findLookupMethod(this.certLookups, method);
                if ( foundLookup != null ) return foundLookup;
            }
            Lookup[] newCertLookups = Arrays.copyOf(this.certLookups, length + 1);
            newCertLookups[length] = newLookup;
            this.certLookups = newCertLookups;
        }
        return newLookup;
    }

    private static Lookup findLookupMethod(final Lookup[] lookups, final LookupMethod method) {
        for ( final Lookup lookup : lookups ) {
            if ( lookup.method.equals(method) ) return lookup;
        }
        return null;
    }

    /**
     * c: X509_STORE_add_cert
     */
    public int addCertificate(final X509Certificate cert) {
        if ( cert == null ) return 0;

        final Certificate certObj = new Certificate(StoreContext.ensureAux(cert));

        final X509Object[] objects = this.objects;
        if ( matchedObject(objects, certObj) ) {
            X509Error.addError(X509_R_CERT_ALREADY_IN_HASH_TABLE);
            return 0;
        }

        return addObject(certObj, objects.length);
    }

    /**
     * c: X509_STORE_add_crl
     */
    public int addCRL(final java.security.cert.CRL crl) {
        if ( crl == null ) return 0;

        final CRL crlObj = new CRL(); crlObj.crl = crl;

        final X509Object[] objects = this.objects;
        if ( matchedObject(objects, crlObj) ) {
            X509Error.addError(X509_R_CERT_ALREADY_IN_HASH_TABLE);
            return 0;
        }

        return addObject(crlObj, objects.length);
    }

    private static boolean matchedObject(final X509Object[] objects, final X509Object xObject) {
        for ( int i = 0; i< objects.length; i++ ) {
            if ( objects[i].matches(xObject) ) return true;
        }
        return false;
    }

    private synchronized int addObject(final X509Object xObject, final int prevLength) {
        final int length = objects.length;
        if ( length != prevLength ) { // something added concurrently
            if ( matchedObject(objects, xObject) ) {
                X509Error.addError(X509_R_CERT_ALREADY_IN_HASH_TABLE);
                return 0;
            }
        }
        X509Object[] newObjects = Arrays.copyOf(objects, length + 1);
        newObjects[ length ] = xObject;
        objects = newObjects;
        return 1;
    }

    /**
     * c: X509_STORE_load_locations
     */
    public int loadLocations(Ruby runtime, final String file, final String path) throws Exception {
        if ( file != null ) {
            final Lookup lookup = addLookup( runtime, Lookup.fileLookup() );
            if ( lookup == null ) {
                return 0;
            }
            if ( lookup.loadFile(new CertificateFile.Path(file, X509_FILETYPE_PEM)) != 1 ) {
                return 0;
            }
        }

        if ( path != null ) {
            final Lookup lookup = addLookup( runtime, Lookup.hashDirLookup() );
            if ( lookup == null ) {
                return 0;
            }
            if ( lookup.addDir(new CertificateHashDir.Dir(path, X509_FILETYPE_PEM)) != 1 ) {
                return 0;
            }
        }

        if ( path == null && file == null ) return 0;
        return 1;
    }

    /**
     * c: X509_STORE_set_default_paths
     */
    public int setDefaultPaths(Ruby runtime) throws Exception {

        Lookup lookup = addLookup(runtime, Lookup.fileLookup());
        //if ( lookup == null ) return 0;

        try {
            lookup.loadFile(new CertificateFile.Path(null, X509_FILETYPE_DEFAULT));
        }
        catch (FileNotFoundException e) {
            // set_default_paths ignores FileNotFound
        }
        catch (IOException e) {
            // this is for older jrubies as they do not have a
            // org.jruby.util.ResourceException.NotFound
            if (!e.getClass().getSimpleName().equals("NotFound")) {
                throw e;
            }
            // set_default_paths ignores FileNotFound
        }

        lookup = addLookup(runtime, Lookup.hashDirLookup());
        //if ( lookup == null ) return 0;

        try {
            lookup.addDir(new CertificateHashDir.Dir(null, X509_FILETYPE_DEFAULT));
        }
        catch (FileNotFoundException e) {
            // set_default_paths ignores FileNotFound
        }
        catch (IOException e) {
            if (!e.getClass().getSimpleName().equals("NotFound")) {
                throw e;
            }
            // set_default_paths ignores FileNotFound
        }

        X509Error.clearErrors();
        return 1;
    }


    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) {
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) {
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        final X509Object[] objects = this.objects;
        final ArrayList<X509Certificate> issuers = new ArrayList<X509Certificate>(objects.length);
        for ( int i = 0; i< objects.length; i++ ) {
            final X509Object object = objects[i];
            if ( object instanceof Certificate ) {
                issuers.add( ( (Certificate) object ).x509 );
            }
        }
        return issuers.toArray( new X509Certificate[ issuers.size() ] );
    }

}// X509_STORE
