package com.palacesun.masking.tools.sm4fpe;

import com.palacesun.masking.adapter.PropertiesLoaderUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.Key;
import java.security.Security;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Random;
import java.util.Scanner;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class SM4FPEBase {
   public static final int KEEP_TYPE = 99999;
   public static final int INVALID_TYPE = -1;
   static int r = 16;
   static ArrayList<SM4FPEEntity> sm4FPEEntities = new ArrayList();
   static char[] enCapitalChars;
   static char[] enSmallChars;
   static char[] digitChars;
   static char[] chineseChars;
   static ArrayList otherChineseChars;
   static HashMap enCapitalCharsMap;
   static HashMap enSmallCharsMap;
   static HashMap digitCharsMap;
   static HashMap chineseCharsMap;
   static HashMap otherChineseCharsMap;

   static void checkDuplicate(ArrayList<SM4FPEEntity> entities) {
      int count = 0;
      HashMap map = new HashMap();
      ArrayList list = new ArrayList();

      for(SM4FPEEntity sm4FPEEntity : entities) {
         count += sm4FPEEntity.getList().size();
         map.putAll(sm4FPEEntity.getCharsMap());
         list.addAll(sm4FPEEntity.getList());
      }

      SM4FPEEntity.printDuplicateData(list);
      if (count != map.size()) {
         throw new RuntimeException("There are duplicate elements in Entitys!");
      }
   }

   public String generateKey() {
      Random random = new Random();
      byte[] keys = new byte[(r - 1) * 16];
      random.nextBytes(keys);
      return Hex.encodeHexString(keys);
   }

   byte[][] upk(String encKey) throws DecoderException {
      byte[] keys = decodeHex(encKey);
      byte[][] key = new byte[r - 1][16];

      for(int i = 0; i < keys.length; ++i) {
         key[i / 16][i % 16] = keys[i];
      }

      return key;
   }

   public static byte[] decodeHex(String data) throws DecoderException {
      return decodeHex(data.toCharArray());
   }

   public static byte[] decodeHex(char[] data) throws DecoderException {
      int len = data.length;
      if ((len & 1) != 0) {
         throw new DecoderException("Odd number of characters.");
      } else {
         byte[] out = new byte[len >> 1];
         int i = 0;

         for(int j = 0; j < len; ++i) {
            int f = toDigit(data[j], j) << 4;
            ++j;
            f |= toDigit(data[j], j);
            ++j;
            out[i] = (byte)(f & 255);
         }

         return out;
      }
   }

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

   public String encryptEcb(String hexKey, String a) throws Exception {
      String out = this.encrypt(hexKey, a, sm4FPEEntities);
      return out;
   }

   String encrypt(String hexKey, String a, ArrayList<SM4FPEEntity> entities) throws Exception {
      int m = a.length();
      SegmentType[] types = this.getTypes(m);
      int dataSegmentLen = this.initTypes(a, m, types, entities);
      int[] P_length = new int[dataSegmentLen];
      int[] Y = new int[dataSegmentLen];
      int[] S = new int[dataSegmentLen];

      for(int i = 0; i < dataSegmentLen; ++i) {
         Y[i] = 0;
         if (types[i].getType() == 99999) {
            P_length[i] = 1;
            S[i] = 1;
         } else {
            P_length[i] = String.valueOf(((SM4FPEEntity)entities.get(types[i].type)).getList().size()).length();
            S[i] = ((SM4FPEEntity)entities.get(types[i].type)).getList().size();
         }
      }

      byte[][] key = this.upk(hexKey);
      String in = "";
      int aIndex = 0;

      for(int i = 0; i < dataSegmentLen; ++i) {
         String index;
         if (types[i].getType() == 99999) {
            index = "0";
            aIndex += types[i].length;
         } else {
            index = Integer.toString(((SM4FPEEntity)entities.get(types[i].type)).mapGet(a, aIndex));
            aIndex += ((SM4FPEEntity)entities.get(types[i].type)).getLength();
         }

         in = in + this.expand(index, P_length[i]);
      }

      String[] P = new String[dataSegmentLen];
      int begin = 0;

      for(int i = 0; i < dataSegmentLen; ++i) {
         P[i] = in.substring(begin, begin + P_length[i]);
         begin += P_length[i];
      }

      String result = this.Feistel(dataSegmentLen, P, key, P_length, S, Y, true);
      begin = 0;
      String out = "";
      int outIndex = 0;

      for(int i = 0; i < dataSegmentLen; ++i) {
         String tmp = result.substring(begin, begin + P_length[i]);
         if (types[i].getType() == 99999) {
            out = out + a.substring(outIndex, outIndex + types[i].length);
            outIndex += types[i].length;
         } else {
            out = out + ((SM4FPEEntity)entities.get(types[i].type)).getList().get(Integer.parseInt(tmp));
            outIndex += ((SM4FPEEntity)entities.get(types[i].type)).getLength();
         }

         begin += P_length[i];
      }

      return out;
   }

   private SegmentType[] getTypes(int m) {
      SegmentType[] types = new SegmentType[m];

      for(int i = 0; i < types.length; ++i) {
         SegmentType type = new SegmentType();
         types[i] = type;
         type.type = -1;
         type.length = 1;
      }

      return types;
   }

   public int initTypes(String a, int m, SegmentType[] types, ArrayList<SM4FPEEntity> entities) throws Exception {
      ArrayList<MatchType> matchTypes = new ArrayList(entities.size());
      ArrayList<Integer> matchFlag = new ArrayList(entities.size());

      for(SM4FPEEntity entity : entities) {
         matchTypes.add(entity.getMatchType());
         matchFlag.add(0);
      }

      int skip = 1;
      int typeIndex = 0;

      for(int i = 0; i < m; i += skip) {
         int j;
         for(j = 0; j < entities.size(); ++j) {
            if (matchTypes.get(j) != MatchType.ONCE || (Integer)matchFlag.get(j) <= 0) {
               matchFlag.set(j, (Integer)matchFlag.get(j) + 1);
               if (((SM4FPEEntity)entities.get(j)).getLength() > 1) {
                  if (a.length() > ((SM4FPEEntity)entities.get(j)).getLength() && ((SM4FPEEntity)entities.get(j)).mapGet(a, i) != null) {
                     types[typeIndex].type = j;
                     types[typeIndex].length = ((SM4FPEEntity)entities.get(j)).getLength();
                     skip = ((SM4FPEEntity)entities.get(j)).getLength();
                     break;
                  }
               } else if (((SM4FPEEntity)entities.get(j)).mapGet(a, i) != null) {
                  types[typeIndex].type = j;
                  skip = ((SM4FPEEntity)entities.get(j)).getLength();
                  break;
               }
            }
         }

         if (j == entities.size()) {
            types[typeIndex].type = 99999;
            skip = 1;
         }

         ++typeIndex;
      }

      return typeIndex;
   }

   String generateInputRandom(String input) {
      Random random = new Random();

      for(int j = 0; j < sm4FPEEntities.size(); ++j) {
         input = input + ((SM4FPEEntity)sm4FPEEntities.get(j)).getList().get(random.nextInt(((SM4FPEEntity)sm4FPEEntities.get(j)).getList().size()));
      }

      return input;
   }

   public String decryptEcb(String hexKey, String input) throws Exception {
      String out = this.decrypt(hexKey, input, sm4FPEEntities);
      return out;
   }

   String decrypt(String hexKey, String input, ArrayList<SM4FPEEntity> entities) throws Exception {
      int m = input.length();
      SegmentType[] types = this.getTypes(m);
      int dataSegmentLen = this.initTypes(input, m, types, entities);
      int[] P_length = new int[dataSegmentLen];
      int[] Y = new int[dataSegmentLen];
      int[] S = new int[dataSegmentLen];

      for(int i = 0; i < dataSegmentLen; ++i) {
         Y[i] = 0;
         if (types[i].type == 99999) {
            P_length[i] = 1;
            S[i] = 1;
         } else {
            P_length[i] = String.valueOf(((SM4FPEEntity)entities.get(types[i].type)).getList().size()).length();
            S[i] = ((SM4FPEEntity)entities.get(types[i].type)).getList().size();
         }
      }

      int begin = 0;
      String in = "";
      int aIndex = 0;

      for(int i = 0; i < dataSegmentLen; ++i) {
         int i1;
         if (types[i].getType() == 99999) {
            i1 = 0;
            aIndex += types[i].length;
         } else {
            i1 = ((SM4FPEEntity)entities.get(types[i].type)).mapGet(input, aIndex);
            aIndex += ((SM4FPEEntity)entities.get(types[i].type)).getLength();
         }

         in = in + this.expand(String.valueOf(i1), P_length[i]);
      }

      byte[][] key = this.upk(hexKey);
      String[] Q = new String[dataSegmentLen];
      byte[][] key2 = new byte[r - 1][16];

      for(int i = 0; i < dataSegmentLen; ++i) {
         Q[i] = in.substring(begin, begin + P_length[i]);
         begin += P_length[i];
      }

      for(int var26 = 0; var26 < r - 1; ++var26) {
         key2[var26] = key[r - 1 - var26 - 1];
      }

      String result = this.Feistel(dataSegmentLen, Q, key2, P_length, S, Y, false);
      String out = "";
      begin = 0;
      int outIndex = 0;

      for(int k = 0; k < dataSegmentLen; ++k) {
         String tmp = result.substring(begin, begin + P_length[k]);
         if (types[k].type == 99999) {
            out = out + input.substring(outIndex, outIndex + types[k].length);
            outIndex += types[k].length;
         } else {
            out = out + ((SM4FPEEntity)entities.get(types[k].type)).getList().get(Integer.parseInt(tmp));
            outIndex += ((SM4FPEEntity)entities.get(types[k].type)).getLength();
         }

         begin += P_length[k];
      }

      return out;
   }

   String PRF(byte[] k, String T, int r) throws Exception {
      Cipher sm4 = Cipher.getInstance("sm4/ecb/pkcs5padding", "BC");
      Key sm4key = new SecretKeySpec(k, "SM4");
      sm4.init(1, sm4key);
      byte[] enc = sm4.doFinal(T.getBytes());
      String ret = this.trunc(enc, r);
      return ret;
   }

   String trunc(byte[] in, int r) {
      String tmp = Hex.encodeHexString(in);
      String ret = tmp.substring(0, r);
      return ret;
   }

   String expand(String a, int b) {
      while(a.length() < b) {
         a = '0' + a;
      }

      return a;
   }

   String Feistel(int m, String[] P, byte[][] key, int[] P_length, int[] S, int[] Y, boolean enc) throws Exception {
      int flag = 1;
      if (!enc) {
         flag = -1;
      }

      int totalLength = 0;
      String[] L = new String[m / 2];
      String[] R = new String[m - m / 2];

      for(int i = 0; i < P.length; ++i) {
         totalLength += P[i].length();
         if (i < m / 2) {
            L[i] = P[i];
         } else {
            R[i - m / 2] = P[i];
         }
      }

      for(int var25 = 1; var25 <= r; ++var25) {
         if (var25 == r && var25 % 2 == 0) {
            String[] t = (String[])((String[])L).clone();
            L = R;
            R = t;
         } else {
            String RString = "";

            for(int RLength = 0; RLength < R.length; ++RLength) {
               RString = RString + R[RLength];
            }

            int var28 = RString.length();
            int LLength = totalLength - var28;
            int loc = (var25 - 1) % 2 * (m / 2);
            String[] LTmp = (String[])((String[])R).clone();
            String[] RTmp = new String[L.length];
            String tmp = this.PRF(key[var25 - 1], RString, LLength);
            int begin = 0;

            for(int j = 0; j < L.length; ++j) {
               int a = Integer.parseInt(L[j]);
               String b = tmp.substring(begin, begin + L[j].length());
               if (b.length() % 2 == 1) {
                  b = '0' + b;
               }

               tmp.substring(begin, begin + L[j].length());
               begin += L[j].length();
               int c = (new BigInteger(1, Hex.decodeHex(b.toCharArray()))).intValue();
               RTmp[j] = Integer.toString(Y[loc + j] + (a - Y[loc + j] + flag * c % S[loc + j] + S[loc + j]) % S[loc + j]);
               RTmp[j] = this.expand(RTmp[j], P_length[loc + j]);
            }

            L = LTmp;
            R = RTmp;
         }
      }

      String ret = "";

      for(int k = 0; k < m; ++k) {
         if (k < m / 2) {
            ret = ret + this.expand(L[k], P_length[k]);
         } else {
            ret = ret + this.expand(R[k - m / 2], P_length[k]);
         }
      }

      return ret;
   }

   static char[] readCharFile(String filename, String charsetName) {
      StringBuilder result = new StringBuilder();
      Scanner sc = readfile(filename, charsetName);

      while(sc.hasNextLine()) {
         result.append(sc.nextLine());
      }

      return result.toString().toCharArray();
   }

   static ArrayList readFileList(String filename, String charsetName) {
      Scanner sc = readfile(filename, charsetName);
      ArrayList arrayList = new ArrayList();

      while(sc.hasNextLine()) {
         arrayList.add(sc.nextLine());
      }

      return arrayList;
   }

   private static Scanner readfile(String filename, String charsetName) {
      Scanner sc = null;

      try {
         File file = null;
         InputStream inputStream = PropertiesLoaderUtils.getResourceAsStream("/charfile/" + filename);
         if (inputStream != null) {
            file = new File(filename);
            copyInputStreamToFile(inputStream, file);
         } else {
            String path = SM4FPEBase.class.getClassLoader().getResource("").getPath();
            file = new File(path + File.separator + "charfile" + File.separator + filename);
         }

         sc = new Scanner(file, charsetName);
      } catch (FileNotFoundException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      return sc;
   }

   public static void copyInputStreamToFile(InputStream inputStream, File file) throws IOException {
      OutputStream outputStream = null;

      try {
         outputStream = new FileOutputStream(file);
         byte[] buffer = new byte[4096];

         int length;
         while((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
         }
      } finally {
         if (inputStream != null) {
            inputStream.close();
         }

         if (outputStream != null) {
            outputStream.close();
         }

      }

   }

   public static boolean check(String input) {
      return true;
   }

   public static ArrayList<SM4FPEEntity> getSm4FPEEntities() {
      return sm4FPEEntities;
   }

   public static ArrayList toArrayList(char[] a) {
      ArrayList arrayList = new ArrayList(a.length);

      for(char c : a) {
         arrayList.add(c);
      }

      return arrayList;
   }

   public static ArrayList toArrayList(String[] a) {
      ArrayList arrayList = new ArrayList(a.length);
      Collections.addAll(arrayList, a);
      return arrayList;
   }

   public static ArrayList excludeChars(ArrayList a, char[] excludeChars) {
      HashMap map = new HashMap();

      for(int i = 0; i < excludeChars.length; ++i) {
         map.put(excludeChars[i], i);
      }

      ArrayList arrayList = new ArrayList();

      for(Object c : a) {
         if (map.get(c) == null) {
            arrayList.add(c);
         }
      }

      return arrayList;
   }

   public static ArrayList excludeChars(ArrayList a, ArrayList excludeArray) {
      HashMap map = new HashMap();

      for(int i = 0; i < excludeArray.size(); ++i) {
         map.put(excludeArray.get(i), i);
      }

      ArrayList arrayList = new ArrayList();

      for(Object c : a) {
         if (map.get(c) == null) {
            arrayList.add(c);
         }
      }

      return arrayList;
   }

   static {
      Security.addProvider(new BouncyCastleProvider());
      enCapitalChars = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
      enSmallChars = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
      digitChars = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
      chineseChars = readCharFile("simplified_chinese.txt", "UTF-8");
      char[] tmp = readCharFile("all_chinese_unicode.txt", "UTF-8");
      otherChineseChars = excludeChars(toArrayList(tmp), chineseChars);
      enCapitalCharsMap = new HashMap();

      for(int i = 0; i < enCapitalChars.length; ++i) {
         enCapitalCharsMap.put(enCapitalChars[i], i);
      }

      enSmallCharsMap = new HashMap();

      for(int i = 0; i < enSmallChars.length; ++i) {
         enSmallCharsMap.put(enSmallChars[i], i);
      }

      digitCharsMap = new HashMap();

      for(int i = 0; i < digitChars.length; ++i) {
         digitCharsMap.put(digitChars[i], i);
      }

      otherChineseCharsMap = new HashMap();

      for(int i = 0; i < otherChineseChars.size(); ++i) {
         otherChineseCharsMap.put(otherChineseChars.get(i), i);
      }

      chineseCharsMap = new HashMap();

      for(int i = 0; i < chineseChars.length; ++i) {
         chineseCharsMap.put(chineseChars[i], i);
      }

      try {
         sm4FPEEntities.add(new SM4FPEEntity(0, toArrayList(enCapitalChars), enCapitalCharsMap));
         sm4FPEEntities.add(new SM4FPEEntity(1, toArrayList(enSmallChars), enSmallCharsMap));
         sm4FPEEntities.add(new SM4FPEEntity(2, toArrayList(digitChars), digitCharsMap));
         sm4FPEEntities.add(new SM4FPEEntity(3, toArrayList(chineseChars), chineseCharsMap));
         sm4FPEEntities.add(new SM4FPEEntity(4, otherChineseChars, otherChineseCharsMap));
         checkDuplicate(sm4FPEEntities);
      } catch (RuntimeException e) {
         throw e;
      }
   }

   public static enum MatchType {
      LOOP("LOOP"),
      ONCE("ONCE");

      public final String name;

      private MatchType(String name) {
         this.name = name;
      }
   }
}
