/*
* The contents of this file are subject to the Common 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/cpl-v10.html
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR APARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*  Copyright (C) 2017 Donovan Lampa <donovan.lampa@gmail.com>
*  Copyright (C) 2009-2017 The JRuby Team
*
* 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.
*
*
* JRuby-OpenSSL includes software by The Legion of the Bouncy Castle Inc.
* Please, visit (http://bouncycastle.org/license.html) for licensing details.
*/
package org.jruby.ext.openssl;

import static org.jruby.ext.openssl.OCSP._OCSP;
import static org.jruby.ext.openssl.OCSP.newOCSPError;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1GeneralizedTime;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.ocsp.CertID;
import org.bouncycastle.asn1.ocsp.RevokedInfo;
import org.bouncycastle.asn1.ocsp.SingleResponse;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
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.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

/*
 * An OpenSSL::OCSP::SingleResponse represents an OCSP SingleResponse structure, 
 * which contains the basic information of the status of the certificate.
 * 
 * @author lampad
 */
public class OCSPSingleResponse extends RubyObject {
    private static final long serialVersionUID = 7947277768033100227L;

    private static ObjectAllocator SINGLERESPONSE_ALLOCATOR = new ObjectAllocator() {
        public IRubyObject allocate(Ruby runtime, RubyClass klass) {
            return new OCSPSingleResponse(runtime, klass);
        }
    };
        
    public static void createSingleResponse(final Ruby runtime, final RubyModule _OCSP) {
        RubyClass _request = _OCSP.defineClassUnder("SingleResponse", runtime.getObject(), SINGLERESPONSE_ALLOCATOR);
        _request.defineAnnotatedMethods(OCSPSingleResponse.class);
    }

    public OCSPSingleResponse(Ruby runtime, RubyClass metaClass) {
        super(runtime, metaClass);
    }
    
    public OCSPSingleResponse(Ruby runtime) {
        this(runtime, (RubyClass) _OCSP(runtime).getConstantAt("SingleResponse"));
    }
    
    private SingleResponse bcSingleResponse;
    
    @JRubyMethod(name = "initialize", visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, IRubyObject derStr) {
        Ruby runtime = context.getRuntime();
        RubyString rubyDerStr = (RubyString) derStr;
        try {
            bcSingleResponse = SingleResponse.getInstance(DERTaggedObject.fromByteArray(rubyDerStr.getBytes()));
        }
        catch (IOException e) {
            throw newOCSPError(runtime, e);
        }
        
        return this;
    }
    
    @JRubyMethod(name = "cert_status")
    public IRubyObject cert_status() {
        return RubyFixnum.newFixnum(getRuntime(), bcSingleResponse.getCertStatus().getTagNo());
    }
    
    @JRubyMethod(name = "certid")
    public IRubyObject certid(ThreadContext context) {
        Ruby runtime = context.runtime;
        CertID bcCertId = bcSingleResponse.getCertID();
        OCSPCertificateId rubyCertId = new OCSPCertificateId(runtime); 
        try {
            rubyCertId.initialize(context, RubyString.newString(runtime, bcCertId.getEncoded()));
        }
        catch (IOException e) {
            throw newOCSPError(runtime, e);
        }
        
        return rubyCertId;
    }
    
    @JRubyMethod(name = "check_validity", rest = true)
    public IRubyObject check_validity(IRubyObject[] args) {
        Ruby runtime = getRuntime();
        int nsec, maxsec;
        Date thisUpdate, nextUpdate;
        
        if ( Arity.checkArgumentCount(runtime, args, 0, 2) == 0 ) {
            nsec = 0;
            maxsec = -1;
        }
        else if ( Arity.checkArgumentCount(runtime, args, 0, 2) == 1 ) {
            RubyFixnum rNsec = (RubyFixnum) args[0];
            nsec = (int)rNsec.getLongValue();
            maxsec = -1;
        }
        else {
            RubyFixnum rNsec = (RubyFixnum) args[0];
            RubyFixnum rMaxsec = (RubyFixnum) args[1];
            nsec = (int)rNsec.getLongValue();
            maxsec = (int)rMaxsec.getLongValue();
        }

        try {
            ASN1GeneralizedTime bcThisUpdate = bcSingleResponse.getThisUpdate();
            if (bcThisUpdate == null) {
                thisUpdate = null;
            }
            else {
                thisUpdate = bcThisUpdate.getDate();
            }
            ASN1GeneralizedTime bcNextUpdate = bcSingleResponse.getNextUpdate();
            if (bcNextUpdate == null) {
                nextUpdate = null;
            }
            else {
                nextUpdate = bcNextUpdate.getDate();
            }
        }
        catch (ParseException e) {
            throw newOCSPError(runtime, e);
        }
        
        return RubyBoolean.newBoolean(runtime, checkValidityImpl(thisUpdate, nextUpdate, nsec, maxsec));
    }
    
    @JRubyMethod(name = "extensions")
    public IRubyObject extensions() {
        Ruby runtime = getRuntime();
        Extensions exts = bcSingleResponse.getSingleExtensions();
        if (exts == null) return RubyArray.newEmptyArray(runtime);
        ASN1ObjectIdentifier[] extOIDs = exts.getExtensionOIDs();
        RubyArray retExts = runtime.newArray(extOIDs.length);
        for (ASN1ObjectIdentifier extOID : extOIDs) {
            Extension ext = exts.getExtension(extOID);
            ASN1Encodable extAsn1 = ext.getParsedValue();
            X509Extension retExt = X509Extension.newExtension(runtime, extOID, extAsn1, ext.isCritical());
            retExts.append(retExt);
        }
        return retExts;
    }
    
    @JRubyMethod(name = "next_update")
    public IRubyObject next_update() {
        Ruby runtime = getRuntime();
        if (bcSingleResponse.getNextUpdate() == null) return runtime.getNil();
        Date nextUpdate;
        try {
            nextUpdate = bcSingleResponse.getNextUpdate().getDate();
        }
        catch (ParseException e) {
            throw newOCSPError(runtime, e);
        }
        
        if (nextUpdate == null) {
            return runtime.getNil();
        }
        
        return RubyTime.newTime(runtime, nextUpdate.getTime());
    }
    
    @JRubyMethod(name = "this_update")
    public IRubyObject this_update() {
        Ruby runtime = getRuntime();
        if (bcSingleResponse.getThisUpdate() == null) return runtime.getNil();
        Date thisUpdate;
        try {
            thisUpdate = bcSingleResponse.getThisUpdate().getDate();
        }
        catch (ParseException e) {
            throw newOCSPError(runtime, e);
        }
        
        return RubyTime.newTime(runtime, thisUpdate.getTime());
    }
    
    @JRubyMethod(name = "revocation_reason")
    public IRubyObject revocation_reason() {
        Ruby runtime = getRuntime();
        RubyFixnum revoked = (RubyFixnum) _OCSP(runtime).getConstant("V_CERTSTATUS_REVOKED");
        if (bcSingleResponse.getCertStatus().getTagNo() == (int) revoked.getLongValue()) {
            try {
                RevokedInfo revokedInfo = RevokedInfo.getInstance(
                    DERTaggedObject.fromByteArray(bcSingleResponse.getCertStatus().getStatus().toASN1Primitive().getEncoded())
                );
                return RubyFixnum.newFixnum(runtime, revokedInfo.getRevocationReason().getValue().intValue());
            }
            catch (IOException e) {
                throw newOCSPError(runtime, e);
            }
        }
        return runtime.getNil();
    }
    
    @JRubyMethod(name = "revocation_time")
    public IRubyObject revocation_time() {
        Ruby runtime = getRuntime();
        RubyFixnum revoked = (RubyFixnum) _OCSP(runtime).getConstant("V_CERTSTATUS_REVOKED");
        if (bcSingleResponse.getCertStatus().getTagNo() == (int)revoked.getLongValue()) {
            try {
                RevokedInfo revokedInfo = RevokedInfo.getInstance(
                        DERTaggedObject.fromByteArray(bcSingleResponse.getCertStatus().getStatus().toASN1Primitive().getEncoded())
                        );
                return RubyTime.newTime(runtime, revokedInfo.getRevocationTime().getDate().getTime());
            }
            catch (Exception e) {
                throw newOCSPError(runtime, e);
            }
        }
        return runtime.getNil();
    }
    
    @JRubyMethod(name = "to_der")
    public IRubyObject to_der() {
        Ruby runtime = getRuntime();
        try {
            return RubyString.newString(runtime, bcSingleResponse.getEncoded());
        }
        catch (IOException e) {
            throw newOCSPError(runtime, e);
        }
    }
    
    public SingleResponse getBCSingleResp() {
        return bcSingleResponse;
    }
    
    // see OCSP_check_validity in ocsp_cl.c
    private boolean checkValidityImpl(Date thisUpdate, Date nextUpdate, int nsec, int maxsec) {
        boolean ret = true;
        Date currentTime = new Date();
        Date tempTime = new Date();
        
        tempTime.setTime(currentTime.getTime() + (nsec*1000));
        if (thisUpdate.compareTo(tempTime) > 0) {
            ret = false;
        }
        
        if (maxsec >= 0) {
            tempTime.setTime(currentTime.getTime() - (maxsec*1000));
            if (thisUpdate.compareTo(tempTime) < 0) {
                ret = false;
            }
        }
        
        if (nextUpdate == null) {
            return ret;
        }
        
        tempTime.setTime(currentTime.getTime() - (nsec*1000));
        if (nextUpdate.compareTo(tempTime) < 0) {
            ret = false;
        }
        
        if (nextUpdate.compareTo(thisUpdate) < 0) {
            ret = false;
        }
        
        return ret;
    }

}
