/*
 * Copyright 2019 IBM All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package org.hyperledger.fabric.gateway;

import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.File;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.io.Reader;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.util.io.pem.PemGenerationException;
import org.bouncycastle.util.io.pem.PemObject;
import org.hyperledger.fabric.gateway.impl.identity.X509IdentityImpl;
import org.hyperledger.fabric.sdk.Enrollment;
//如果jdk为1.8.0_301版本以下，需引用sun.security.ec.CurveDB
import sun.security.util.CurveDB;
import sun.security.util.ObjectIdentifier;
import sun.security.x509.AlgorithmId;

/**
 * This class consists exclusively of static methods used to create and operate on identity information.
 */
public final class Identities {

    static {
        Security.addProvider(new BouncyCastleProvider());

        try {
            enableGM();
        } catch (IOException | NoSuchFieldException | ClassNotFoundException | InvocationTargetException
                | IllegalAccessException | NoSuchMethodException e) {
            throw new RuntimeException("Fail to enable GM, please check exception detail", e);
        }
    }

    //register GM curve
    private static void enableGM() throws IllegalAccessException, InvocationTargetException, NoSuchFieldException, NoSuchMethodException,
            IOException, ClassNotFoundException {
        Method[] methods = CurveDB.class.getDeclaredMethods();
        Method method = null;

        Pattern var0 = Pattern.compile(",|\\[|\\]");
        for (Method m : methods) {
            if ("add".equals(m.getName())) {
                method = m;
            }
        }
        method.setAccessible(true);
        method.invoke(CurveDB.class, "sm2p256v1", "1.2.156.10197.1.301", 1, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF",
                "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93",
                "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0",
                "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 1, var0);

        final Field specCollection = CurveDB.class.getDeclaredField("specCollection");
        final Field oidMap = CurveDB.class.getDeclaredField("oidMap");
        oidMap.setAccessible(true);
        specCollection.setAccessible(true);
        specCollection.set(CurveDB.class, Collections.unmodifiableCollection(((Map) oidMap.get(CurveDB.class)).values()));

        Field nameTable = AlgorithmId.class.getDeclaredField("nameTable");
        nameTable.setAccessible(true);
        Map<ObjectIdentifier, String> map = (HashMap) nameTable.get(AlgorithmId.class);
        ObjectIdentifier objectIdentifier = ObjectIdentifier.newInternal(new int[]{1, 2, 156, 10197, 1, 501});
        map.put(objectIdentifier, "SM3withSM2");

        Class clazz = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.ExtendedOpenSslSession");
        Field algorithmsField = clazz.getDeclaredField("LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS");
        algorithmsField.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(algorithmsField, algorithmsField.getModifiers() & ~Modifier.FINAL);
        String[] algorithms = (String[]) algorithmsField.get(null);
        String[] newAlgorithms = new String[algorithms.length + 1];
        System.arraycopy(algorithms, 0, newAlgorithms, 0, algorithms.length);
        newAlgorithms[algorithms.length] = "SM3withSM2";
        algorithmsField.set(null, newAlgorithms);
        String os = System.getProperty("os.name");
        if (os.toLowerCase().startsWith("win")) {
            loadLib("libcrypto-1_1-x64");
            loadLib("libssl-1_1-x64");
        }
    }

    private static void loadLib(final String libName) throws IOException {
        String libExtension = ".dll";

        String libFullName = libName + libExtension;

        String nativeTempDir = System.getProperty("java.io.tmpdir");

        InputStream in = null;
        BufferedInputStream reader = null;
        FileOutputStream writer = null;

        File extractedLibFile = new File(nativeTempDir + File.separator + libFullName);
        if (!extractedLibFile.exists()) {
            try {
                in = Identities.class.getResourceAsStream("/win32-x86-64/" + libFullName);
                if (in == null) {
                    in = Identities.class.getResourceAsStream(libFullName);
                }
                Identities.class.getResource(libFullName);
                reader = new BufferedInputStream(in);
                writer = new FileOutputStream(extractedLibFile);

                byte[] buffer = new byte[1024];

                while (reader.read(buffer) > 0) {
                    writer.write(buffer);
                    buffer = new byte[1024];
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (in != null) {
                    in.close();
                }
                if (writer != null) {
                    writer.close();
                }
            }
        }
        System.load(extractedLibFile.toString());
    }

    /**
     * Create a new identity using X.509 credentials.
     *
     * @param mspId       Member Services Provider identifier for the organization to which this identity belongs.
     * @param certificate An X.509 certificate.
     * @param privateKey  Private key.
     * @return An identity.
     * @throws NullPointerException if any of the arguments are null.
     */
    public static X509Identity newX509Identity(final String mspId, final X509Certificate certificate, final PrivateKey privateKey) {
        return new X509IdentityImpl(mspId, certificate, privateKey);
    }

    /**
     * Create a new X.509 identity from an enrollment returned from a Certificate Authority.
     *
     * @param mspId      Member Services Provider identifier.
     * @param enrollment Identity credentials.
     * @return An identity.
     * @throws CertificateException if the certificate is invalid.
     * @throws NullPointerException if any of the arguments are null.
     */
    public static X509Identity newX509Identity(final String mspId, final Enrollment enrollment) throws CertificateException {
        return newX509Identity(mspId, readX509Certificate(enrollment.getCert()), enrollment.getKey());
    }

    /**
     * Read a PEM format X.509 certificate.
     *
     * @param pem PEM data.
     * @return An X.509 certificate.
     * @throws CertificateException if the data is not valid X.509 certificate PEM.
     */
    public static X509Certificate readX509Certificate(final String pem) throws CertificateException {
        try {
            return readX509Certificate(new StringReader(pem));
        } catch (IOException e) {
            // Should not happen with StringReader
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Read a PEM format X.509 certificate.
     *
     * @param pemReader Reader of PEM data.
     * @return An X.509 certificate.
     * @throws IOException          if an error occurs reading data.
     * @throws CertificateException if the data is not valid X.509 certificate PEM.
     */
    public static X509Certificate readX509Certificate(final Reader pemReader) throws IOException, CertificateException {
        try {
            Object pemObject = readPemObject(pemReader);
            X509CertificateHolder certificateHolder = asX509CertificateHolder(pemObject);
            return new JcaX509CertificateConverter().getCertificate(certificateHolder);
        } catch (PEMException e) {
            throw new CertificateException(e);
        }
    }

    private static Object readPemObject(final Reader reader) throws IOException {
        try (PEMParser parser = new PEMParser(reader)) {
            final Object result = parser.readObject(); // throws PEMException on parse error
            if (result == null) {
                throw new PEMException("Invalid PEM content");
            }
            return result;
        }
    }

    private static X509CertificateHolder asX509CertificateHolder(final Object pemObject) throws CertificateException {
        if (pemObject instanceof X509CertificateHolder) {
            return (X509CertificateHolder) pemObject;
        } else {
            throw new CertificateException("Unexpected PEM content type: " + pemObject.getClass().getSimpleName());
        }
    }

    /**
     * Read a PEM format private key.
     *
     * @param pem PEM data.
     * @return An X.509 certificate.
     * @throws InvalidKeyException if the data is not valid private key PEM.
     */
    public static PrivateKey readPrivateKey(final String pem) throws InvalidKeyException {
        try {
            return readPrivateKey(new StringReader(pem));
        } catch (IOException e) {
            // Should not happen with StringReader
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Read a PEM format private key.
     *
     * @param pemReader Reader of PEM data.
     * @return A private key.
     * @throws IOException         if an error occurs reading data.
     * @throws InvalidKeyException if the data is not valid private key PEM.
     */
    public static PrivateKey readPrivateKey(final Reader pemReader) throws IOException, InvalidKeyException {
        try {
            Object pemObject = readPemObject(pemReader);
            PrivateKeyInfo privateKeyInfo = asPrivateKeyInfo(pemObject);
            return new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
        } catch (PEMException e) {
            throw new InvalidKeyException(e);
        }
    }

    private static PrivateKeyInfo asPrivateKeyInfo(final Object pemObject) throws InvalidKeyException {
        PrivateKeyInfo privateKeyInfo;
        if (pemObject instanceof PEMKeyPair) {
            privateKeyInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
        } else if (pemObject instanceof PrivateKeyInfo) {
            privateKeyInfo = (PrivateKeyInfo) pemObject;
        } else {
            throw new InvalidKeyException("Unexpected PEM content type: " + pemObject.getClass().getSimpleName());
        }
        return privateKeyInfo;
    }

    /**
     * Converts the argument to a PEM format string.
     *
     * @param certificate A certificate.
     * @return A PEM format string.
     */
    public static String toPemString(final Certificate certificate) {
        return asPemString(certificate);
    }

    private static String asPemString(final Object obj) {
        StringWriter stringWriter = new StringWriter();
        try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
            pemWriter.writeObject(obj);
            pemWriter.flush();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return stringWriter.toString();
    }

    /**
     * Converts the argument to a PKCS #8 PEM format string.
     *
     * @param privateKey A private key.
     * @return A PEM format string.
     * @throws IllegalArgumentException if the argument can not be represented in PKCS #8 PEM format.
     */
    public static String toPemString(final PrivateKey privateKey) {
        try {
            PemObject pkcs8PrivateKey = new JcaPKCS8Generator(privateKey, null).generate();
            return asPemString(pkcs8PrivateKey);
        } catch (PemGenerationException e) {
            throw new IllegalArgumentException(e);
        }
    }

    // Private constructor to prevent instantiation
    private Identities() {
    }
}
