package com.cyksj.common.util;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.security.*;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * @author valor.
 * @date 2019/9/20 16:47
 */
public class Codec {

    private Codec() { }

    // Base64
    // Cipher
    // Hmac
    // Signature
    // MessageDigest

    public static class Hexs {

        private Hexs() { }

        /**
         * Used to build output as Hex
         */
        private static final char[] DIGITS =
                { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

        public static String encodeToString(final byte[] data) {
            return new String(encode(data));
        }

        private static char[] encode(final byte[] data) {
            final int l = data.length;
            final char[] out = new char[l << 1];
            // two characters form the hex value.
            for (int i = 0, j = 0; i < l; i++) {
                out[j++] = DIGITS[(0xF0 & data[i]) >>> 4];
                out[j++] = DIGITS[0x0F & data[i]];
            }
            return out;
        }

        public static byte[] decode(final char[] data) {
            final int len = data.length;
            if ((len & 0x01) != 0) {
                throw new RuntimeException("Odd number of characters.");
            }

            final byte[] out = new byte[len >> 1];
            // two characters form the hex value.
            for (int i = 0, j = 0; j < len; i++) {
                int f = toDigit(data[j], j) << 4;
                j++;
                f = f | toDigit(data[j], j);
                j++;
                out[i] = (byte) (f & 0xFF);
            }
            return out;
        }

        private static int toDigit(final char ch, final int index) {
            final int digit = Character.digit(ch, 16);
            if (digit == -1) {
                throw new RuntimeException("Illegal hexadecimal character " + ch + " at index " + index);
            }
            return digit;
        }
    }

    /**
     * package {@link Base64}
     */
    private static class Base64s {

        private Base64s() { }

        /* ----- encode ----- */
        private static byte[] encode(byte[] var) {
            return Base64.getEncoder().encode(var);
        }

        /* ----- decode ----- */
        private static byte[] decode(byte[] var) {
            return Base64.getDecoder().decode(var);
        }
    }

    /**
     * package {@link Cipher}
     */
    private static class Ciphers {

        private Ciphers() { }

        /**
         * Cipher算法
         *
         * @param mode    模式 加密/解密
         * @param trans   算法填充
         * @param content 待加密/解密的byte[]
         * @param keyByte 加密/解密密钥
         * @param ivByte  算法向量 (AES CBC类型 需要填充iv偏移量)
         * @return 输出的byte[]
         * @throws Exception 抛出异常
         */
        private static byte[] doCipher(int mode, DoCipher.Trans trans, byte[] content, byte[] keyByte, byte[] ivByte) throws Exception {
            String algorithm = trans.getTransformation();

            Cipher cipher = Cipher.getInstance(algorithm);
            Key sKeySpec = new SecretKeySpec(keyByte, algorithm);

            if (null != ivByte && 0 != ivByte.length) {
                // AES CBC类型 需要填充iv偏移量
                AlgorithmParameterSpec iv = new IvParameterSpec(ivByte);
                cipher.init(mode, sKeySpec, iv);
            } else {
                cipher.init(mode, sKeySpec);
            }
            return cipher.doFinal(content);
        }
    }

    /**
     * package {@link Mac}
     */
    private static class Hmacs {

        private Hmacs() { }

        private static byte[] doHmac(DoHmac.Algorithm algorithm, byte[] content, byte[] keyByte) throws Exception {
            String hmacAlgorithm = algorithm.getValue();

            Mac mac = Mac.getInstance(hmacAlgorithm);
            Key sKeySpec = new SecretKeySpec(keyByte, hmacAlgorithm);
            mac.init(sKeySpec);

            return mac.doFinal(content);
        }
    }

    /**
     * package {@link Signature}
     */
    private static class Signatures {

        private Signatures() { }

        private static byte[] sign(DoSign.SignAlgorithm algorithm, byte[] content, PrivateKey privateKey) throws Exception {
            Signature signature = Signature.getInstance(algorithm.getValue());
            signature.initSign(privateKey);
            signature.update(content);
            return signature.sign();
        }

        private static boolean check(DoSign.SignAlgorithm algorithm, byte[] content, byte[] sign, PublicKey publicKey) throws Exception {
            Signature signature = Signature.getInstance(algorithm.getValue());
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(sign);
        }

        private static PublicKey getPublicKeyFromX509(DoSign.KeyAlgorithm algorithm, byte[] publicKeyBytes) throws Exception {
            KeyFactory keyFactory = KeyFactory.getInstance(algorithm.getValue());
            byte[] key = Base64s.decode(publicKeyBytes);
            return keyFactory.generatePublic(new X509EncodedKeySpec(key));
        }

        private static PrivateKey getPrivateKeyFromPKCS8(DoSign.KeyAlgorithm algorithm, byte[] privateKeyBytes) throws Exception {
            KeyFactory keyFactory = KeyFactory.getInstance(algorithm.getValue());
            byte[] key = Base64s.decode(privateKeyBytes);
            return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(key));
        }
    }

    private static class Digests {

        private Digests() { }

        private static byte[] doDigest(DoDigest.Algorithm algorithm, byte[] content) throws Exception {
            MessageDigest digest = MessageDigest.getInstance(algorithm.getValue());
            return digest.digest(content);
        }

        private static byte[] doDigest(DoDigest.Algorithm algorithm, InputStream in) throws Exception {
            MessageDigest digest = MessageDigest.getInstance(algorithm.getValue());

            byte[] buffer = new byte[1024];
            int offset;
            while (true) {
                offset = in.read(buffer);
                if (offset < 0) {
                    break;
                }
                digest.update(buffer, 0, offset);
            }

            // close
            IoKit.close(in);
            return digest.digest();
        }
    }

    public static class DoDigest {

        public enum Algorithm {
            MD2("MD2"),
            MD5("MD5"),
            SHA1("SHA-1"),
            SHA224("SHA-224"),
            SHA256("SHA-256"),
            SHA384("SHA-384"),
            SHA512("SHA-512"),
            SHA3224("SHA3-224"),
            SHA3256("SHA3-256"),
            SHA3384("SHA3-384"),
            SHA3512("SHA3-512");

            private String value;

            Algorithm(String algorithm) {
                this.value = algorithm;
            }

            public String getValue() {
                return this.value;
            }
        }

        private Algorithm algorithm;
        private byte[] data;

        private DoDigest() { }

        public static DoDigest custom() {
            return new DoDigest();
        }

        public DoDigest setAlgorithm(Algorithm algorithm) {
            this.algorithm = algorithm;
            return this;
        }

        public DoDigest setData(byte[] data) {
            this.data = data;
            return this;
        }

        public DoDigest setBase64Data(String data, IoKit.Charsets charset) {
            this.data = DoBase64.custom().setData(data, charset).decode();
            return this;
        }

        public DoDigest setBase64Data(String data) {
            return this.setBase64Data(data, IoKit.Charsets.UTF_8);
        }

        public DoDigest setStringData(String data, IoKit.Charsets charset) {
            this.data = data.getBytes(charset.getCharset());
            return this;
        }

        public DoDigest setStringData(String data) {
            return this.setStringData(data, IoKit.Charsets.UTF_8);
        }

        public byte[] toBytes() throws Exception {
            return Digests.doDigest(this.algorithm, this.data);
        }

        public String toHexString() throws Exception {
            return Hexs.encodeToString(this.toBytes());
        }

        public String toBase64String(IoKit.Charsets charset) throws Exception {
            return DoBase64.custom().setData(this.toBytes()).encodeAsText(charset);
        }

        public String toBase64String() throws Exception {
            return this.toBase64String(IoKit.Charsets.UTF_8);
        }

        public String toText(IoKit.Charsets charset) throws Exception {
            return new String(this.toBytes(), charset.getCharset());
        }

        public String toText() throws Exception {
            return this.toText(IoKit.Charsets.UTF_8);
        }
    }

    public static class DoSign {

        public enum KeyAlgorithm {
            RSA("RSA"),
            DSA("DSA"),
            EC("EC");

            private String value;

            KeyAlgorithm(String algorithm) {
                this.value = algorithm;
            }

            public String getValue() {
                return this.value;
            }
        }

        public enum SignAlgorithm {
            NONEwithRSA("NONEwithRSA"),
            MD2withRSA("MD2withRSA"),
            MD5withRSA("MD5withRSA"),
            SHA1withRSA("SHA1withRSA"),
            SHA224withRSA("SHA224withRSA"),
            SHA256withRSA("SHA256withRSA"),
            SHA384withRSA("SHA384withRSA"),
            SHA512withRSA("SHA512withRSA"),

            NONEwithDSA("NONEwithDSA"),
            SHA1withDSA("SHA1withDSA"),
            SHA224withDSA("SHA224withDSA"),
            SHA256withDSA("SHA256withDSA"),

            NONEwithECDSA("NONEwithECDSA"),
            SHA1withECDSA("SHA1withECDSA"),
            SHA224withECDSA("SHA224withECDSA"),
            SHA256withECDSA("SHA256withECDSA"),
            SHA384withECDSA("SHA384withECDSA"),
            SHA512withECDSA("SHA512withECDSA");

            private String value;

            SignAlgorithm(String algorithm) {
                this.value = algorithm;
            }

            public String getValue() {
                return this.value;
            }
        }

        private KeyAlgorithm keyAlgorithm;
        private SignAlgorithm signAlgorithm;
        private byte[] data;
        private byte[] key;
        private byte[] signature;

        private DoSign() { }

        public static DoSign custom() {
            return new DoSign();
        }

        public DoSign setKeyAlgorithm(KeyAlgorithm keyAlgorithm) {
            this.keyAlgorithm = keyAlgorithm;
            return this;
        }

        public DoSign setSignAlgorithm(SignAlgorithm signAlgorithm) {
            this.signAlgorithm = signAlgorithm;
            return this;
        }

        public DoSign setData(byte[] data) {
            this.data = data;
            return this;
        }

        public DoSign setBase64Data(String data, IoKit.Charsets charset) {
            this.data = DoBase64.custom().setData(data, charset).decode();
            return this;
        }

        public DoSign setBase64Data(String data) {
            return this.setBase64Data(data, IoKit.Charsets.UTF_8);
        }

        public DoSign setStringData(String data, IoKit.Charsets charset) {
            this.data = data.getBytes(charset.getCharset());
            return this;
        }

        public DoSign setStringData(String data) {
            return this.setStringData(data, IoKit.Charsets.UTF_8);
        }

        public DoSign setKey(byte[] key) {
            this.key = key;
            return this;
        }

        public DoSign setBase64Key(String key, IoKit.Charsets charset) {
            this.key = DoBase64.custom().setData(key, charset).decode();
            return this;
        }

        public DoSign setBase64Key(String key) {
            return this.setBase64Key(key, IoKit.Charsets.UTF_8);
        }

        public DoSign setStringKey(String key, IoKit.Charsets charset) {
            this.key = key.getBytes(charset.getCharset());
            return this;
        }

        public DoSign setStringKey(String key) {
            return this.setStringKey(key, IoKit.Charsets.UTF_8);
        }

        public DoSign setSignature(byte[] signature) {
            this.signature = signature;
            return this;
        }

        public DoSign setBase64Signature(String signature, IoKit.Charsets charset) {
            this.signature = DoBase64.custom().setData(signature, charset).decode();
            return this;
        }

        public DoSign setBase64Signature(String signature) {
            return this.setBase64Signature(signature, IoKit.Charsets.UTF_8);
        }

        public DoSign setStringSignature(String signature, IoKit.Charsets charset) {
            this.signature = signature.getBytes(charset.getCharset());
            return this;
        }

        public DoSign setStringSignature(String signature) {
            return this.setStringSignature(signature, IoKit.Charsets.UTF_8);
        }

        public byte[] sign() throws Exception {
            PrivateKey privateKey = Signatures.getPrivateKeyFromPKCS8(this.keyAlgorithm, this.key);
            return Signatures.sign(this.signAlgorithm, this.data, privateKey);
        }

        public String toHexString() throws Exception {
            return Hexs.encodeToString(this.sign());
        }

        public String toBase64String(IoKit.Charsets charset) throws Exception {
            return DoBase64.custom().setData(this.sign()).encodeAsText(charset);
        }

        public String toBase64String() throws Exception {
            return this.toBase64String(IoKit.Charsets.UTF_8);
        }

        public String toText(IoKit.Charsets charset) throws Exception {
            return new String(this.sign(), charset.getCharset());
        }

        public String toText() throws Exception {
            return this.toText(IoKit.Charsets.UTF_8);
        }

        public boolean check() throws Exception {
            PublicKey publicKey = Signatures.getPublicKeyFromX509(this.keyAlgorithm, this.key);
            return Signatures.check(this.signAlgorithm, this.data, this.signature, publicKey);
        }
    }

    public static class DoHmac {

        public enum Algorithm {
            HmacMD5("HmacMD5"),
            HmacSHA1("HmacSHA1"),
            HmacSHA224("HmacSHA224"),
            HmacSHA256("HmacSHA256"),
            HmacSHA384("HmacSHA384"),
            HmacSHA512("HmacSHA512");

            private String value;

            Algorithm(String algorithm) {
                this.value = algorithm;
            }

            public String getValue() {
                return this.value;
            }
        }

        private Algorithm algorithm;
        private byte[] data;
        private byte[] key;

        private DoHmac() { }

        public static DoHmac custom() {
            return new DoHmac();
        }

        public DoHmac setAlgorithm(Algorithm algorithm) {
            this.algorithm = algorithm;
            return this;
        }

        public DoHmac setData(byte[] data) {
            this.data = data;
            return this;
        }

        public DoHmac setBase64Data(String data, IoKit.Charsets charset) {
            this.data = DoBase64.custom().setData(data, charset).decode();
            return this;
        }

        public DoHmac setBase64Data(String data) {
            return this.setBase64Data(data, IoKit.Charsets.UTF_8);
        }

        public DoHmac setStringData(String data, IoKit.Charsets charset) {
            this.data = data.getBytes(charset.getCharset());
            return this;
        }

        public DoHmac setStringData(String data) {
            return this.setStringData(data, IoKit.Charsets.UTF_8);
        }

        public DoHmac setKey(byte[] key) {
            this.key = key;
            return this;
        }

        public DoHmac setBase64Key(String key, IoKit.Charsets charset) {
            this.key = DoBase64.custom().setData(key, charset).decode();
            return this;
        }

        public DoHmac setBase64Key(String key) {
            return this.setBase64Key(key, IoKit.Charsets.UTF_8);
        }

        public DoHmac setStringKey(String key, IoKit.Charsets charset) {
            this.key = key.getBytes(charset.getCharset());
            return this;
        }

        public DoHmac setStringKey(String key) {
            return this.setStringKey(key, IoKit.Charsets.UTF_8);
        }

        public byte[] toBytes() throws Exception {
            return Hmacs.doHmac(this.algorithm, this.data, this.key);
        }

        public String toHexString() throws Exception {
            return Hexs.encodeToString(this.toBytes());
        }

        public String toBase64String(IoKit.Charsets charset) throws Exception {
            return DoBase64.custom().setData(this.toBytes()).encodeAsText(charset);
        }

        public String toBase64String() throws Exception {
            return this.toBase64String(IoKit.Charsets.UTF_8);
        }

        public String toText(IoKit.Charsets charset) throws Exception {
            return new String(this.toBytes(), charset.getCharset());
        }

        public String toText() throws Exception {
            return this.toText(IoKit.Charsets.UTF_8);
        }
    }

    public static class DoCipher {

        static {
            // for Cipher PKCS7Padding
            Security.addProvider(new BouncyCastleProvider());
        }

        public enum Trans {

            AES_CBC_NoPadding("AES/CBC/NoPadding", "128"),
            AES_CBC_PKCS5Padding("AES/CBC/PKCS5Padding", "128"),
            AES_CBC_PKCS7Padding("AES/CBC/PKCS7Padding", "128"),
            AES_ECB_NoPadding("AES/ECB/NoPadding", "128"),
            AES_ECB_PKCS5Padding("AES/ECB/PKCS5Padding", "128"),
            AES_ECB_PKCS7Padding("AES/ECB/PKCS7Padding", "128"),

            DES_CBC_NoPadding("DES/CBC/NoPadding", "56"),
            DES_CBC_PKCS5Padding("DES/CBC/PKCS5Padding", "56"),
            DES_ECB_NoPadding("DES/ECB/NoPadding", "56"),
            DES_ECB_PKCS5Padding("DES/ECB/PKCS5Padding", "56"),

            DESede_CBC_NoPadding("DESede/CBC/NoPadding", "168"),
            DESede_CBC_PKCS5Padding("DESede/CBC/PKCS5Padding", "168"),
            DESede_ECB_NoPadding("DESede/ECB/NoPadding", "168"),
            DESede_ECB_PKCS5Padding("DESede/ECB/PKCS5Padding", "168"),

            RSA_ECB_PKCS1Padding("RSA/ECB/PKCS1Padding", "1024, 2048"),
            RSA_ECB_OAEPWithSHA1AndMGF1Padding("RSA/ECB/OAEPWithSHA-1AndMGF1Padding", "1024, 2048"),
            RSA_ECB_OAEPWithSHA256AndMGF1Padding("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", "1024, 2048");

            private String transformation;

            Trans(String transformation, String bits) {
                this.transformation = transformation;
            }

            public String getTransformation() {
                return this.transformation;
            }
        }

        private Trans algorithm;
        private byte[] data;
        private byte[] key;
        private byte[] iv;
        private int mode;

        private DoCipher() { }

        public static DoCipher custom() {
            return new DoCipher();
        }

        public DoCipher setAlgorithm(Trans algorithm) {
            this.algorithm = algorithm;
            return this;
        }

        public DoCipher setData(byte[] data) {
            this.data = data;
            return this;
        }

        public DoCipher setBase64Data(String data, IoKit.Charsets charset) {
            this.data = DoBase64.custom().setData(data, charset).decode();
            return this;
        }

        public DoCipher setBase64Data(String data) {
            return this.setBase64Data(data, IoKit.Charsets.UTF_8);
        }

        public DoCipher setStringData(String data, IoKit.Charsets charset) {
            this.data = data.getBytes(charset.getCharset());
            return this;
        }

        public DoCipher setStringData(String data) {
            return this.setStringData(data, IoKit.Charsets.UTF_8);
        }

        public DoCipher setKey(byte[] key) {
            this.key = key;
            return this;
        }

        public DoCipher setBase64Key(String key, IoKit.Charsets charset) {
            this.key = DoBase64.custom().setData(key, charset).decode();
            return this;
        }

        public DoCipher setBase64Key(String key) {
            return this.setBase64Key(key, IoKit.Charsets.UTF_8);
        }

        public DoCipher setStringKey(String key, IoKit.Charsets charset) {
            this.key = key.getBytes(charset.getCharset());
            return this;
        }

        public DoCipher setStringKey(String key) {
            return this.setStringKey(key, IoKit.Charsets.UTF_8);
        }

        public DoCipher setIv(byte[] iv) {
            this.iv = iv;
            return this;
        }

        public DoCipher setBase64Iv(String iv, IoKit.Charsets charset) {
            this.iv = DoBase64.custom().setData(iv, charset).decode();
            return this;
        }

        public DoCipher setBase64Iv(String iv) {
            return this.setBase64Iv(iv, IoKit.Charsets.UTF_8);
        }

        public DoCipher setStringIv(String iv, IoKit.Charsets charset) {
            this.iv = iv.getBytes(charset.getCharset());
            return this;
        }

        public DoCipher setStringIv(String iv) {
            return this.setStringIv(iv, IoKit.Charsets.UTF_8);
        }

        public DoCipher ofDecrypt() {
            this.mode = Cipher.DECRYPT_MODE;
            return this;
        }

        public DoCipher ofEncrypt() {
            this.mode = Cipher.ENCRYPT_MODE;
            return this;
        }

        public byte[] toBytes() throws Exception {
            return Ciphers.doCipher(this.mode, this.algorithm, this.data, this.key, this.iv);
        }

        public String toHexString() throws Exception {
            return Hexs.encodeToString(this.toBytes());
        }

        public String toBase64String(IoKit.Charsets charset) throws Exception {
            return DoBase64.custom().setData(this.toBytes()).encodeAsText(charset);
        }

        public String toBase64String() throws Exception {
            return this.toBase64String(IoKit.Charsets.UTF_8);
        }

        public String toText(IoKit.Charsets charset) throws Exception {
            return new String(this.toBytes(), charset.getCharset());
        }

        public String toText() throws Exception {
            return this.toText(IoKit.Charsets.UTF_8);
        }
    }

    public static class DoBase64 {

        private byte[] var;

        private DoBase64() { }

        public static DoBase64 custom() {
            return new DoBase64();
        }

        public DoBase64 setData(byte[] data) {
            this.var = data;
            return this;
        }

        public DoBase64 setData(String data) {
            return this.setData(data, IoKit.Charsets.UTF_8);
        }

        public DoBase64 setData(String data, IoKit.Charsets charset) {
            this.var = data.getBytes(charset.getCharset());
            return this;
        }

        public byte[] encode() {
            return Base64s.encode(this.var);
        }

        public byte[] decode() {
            return Base64s.decode(this.var);
        }

        public String encodeAsText(IoKit.Charsets charset) {
            return new String(this.encode(), charset.getCharset());
        }

        public String decodeAsText(IoKit.Charsets charset) {
            return new String(this.decode(), charset.getCharset());
        }

        public String encodeAsText() {
            return this.encodeAsText(IoKit.Charsets.UTF_8);
        }

        public String decodeAsText() {
            return this.decodeAsText(IoKit.Charsets.UTF_8);
        }
    }
}
