package com.airlock.iam.base.domain.service.uri;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class UriComponent {
   private static final char[] HEX_DIGITS = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
   private static final String[] SCHEME = new String[]{"0-9", "A-Z", "a-z", "+", "-", "."};
   private static final String[] UNRESERVED = new String[]{"0-9", "A-Z", "a-z", "-", ".", "_", "~"};
   private static final String[] SUB_DELIMS = new String[]{"!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="};
   private static final boolean[][] ENCODING_TABLES = initEncodingTables();
   private static final int[] HEX_TABLE = initHexTable();

   public static String contextualEncode(String s, UriComponent.Type t) {
      return localEncode(s, t);
   }

   private static String localEncode(String s, UriComponent.Type t) {
      boolean[] table = ENCODING_TABLES[t.ordinal()];
      StringBuilder sb = null;
      int offset = 0;

      while (offset < s.length()) {
         int codePoint = s.codePointAt(offset);
         if (codePoint < 128 && table[codePoint]) {
            if (sb != null) {
               sb.append((char)codePoint);
            }
         } else if (codePoint == 37 && offset + 2 < s.length() && isHexCharacter(s.charAt(offset + 1)) && isHexCharacter(s.charAt(offset + 2))) {
            if (sb != null) {
               sb.append('%').append(s.charAt(offset + 1)).append(s.charAt(offset + 2));
            }

            offset += 2;
         } else {
            if (sb == null) {
               sb = new StringBuilder();
               sb.append(s, 0, offset);
            }

            if (codePoint < 128) {
               if (codePoint == 32 && t == UriComponent.Type.QUERY_PARAM) {
                  sb.append('+');
               } else {
                  appendPercentEncodedOctet(sb, (char)codePoint);
               }
            } else {
               appendUTF8EncodedCharacter(sb, codePoint);
            }
         }

         offset += Character.charCount(codePoint);
      }

      return sb == null ? s : sb.toString();
   }

   private static void appendPercentEncodedOctet(StringBuilder sb, int b) {
      sb.append('%');
      sb.append(HEX_DIGITS[b >> 4]);
      sb.append(HEX_DIGITS[b & 15]);
   }

   private static void appendUTF8EncodedCharacter(StringBuilder sb, int codePoint) {
      CharBuffer chars = CharBuffer.wrap(Character.toChars(codePoint));
      ByteBuffer bytes = StandardCharsets.UTF_8.encode(chars);

      while (bytes.hasRemaining()) {
         appendPercentEncodedOctet(sb, bytes.get() & 255);
      }
   }

   private static boolean[][] initEncodingTables() {
      boolean[][] tables = new boolean[UriComponent.Type.values().length][];
      List<String> l = new ArrayList<>(Arrays.asList(SCHEME));
      tables[UriComponent.Type.SCHEME.ordinal()] = initEncodingTable(l);
      l.clear();
      l.addAll(Arrays.asList(UNRESERVED));
      tables[UriComponent.Type.UNRESERVED.ordinal()] = initEncodingTable(l);
      l.addAll(Arrays.asList(SUB_DELIMS));
      tables[UriComponent.Type.HOST.ordinal()] = initEncodingTable(l);
      tables[UriComponent.Type.PORT.ordinal()] = initEncodingTable(Arrays.asList("0-9"));
      l.add(":");
      tables[UriComponent.Type.USER_INFO.ordinal()] = initEncodingTable(l);
      l.add("@");
      tables[UriComponent.Type.AUTHORITY.ordinal()] = initEncodingTable(l);
      tables[UriComponent.Type.PATH_SEGMENT.ordinal()] = initEncodingTable(l);
      tables[UriComponent.Type.PATH_SEGMENT.ordinal()][59] = false;
      tables[UriComponent.Type.MATRIX_PARAM.ordinal()] = (boolean[])tables[UriComponent.Type.PATH_SEGMENT.ordinal()].clone();
      tables[UriComponent.Type.MATRIX_PARAM.ordinal()][61] = false;
      l.add("/");
      tables[UriComponent.Type.PATH.ordinal()] = initEncodingTable(l);
      tables[UriComponent.Type.QUERY.ordinal()] = initEncodingTable(l);
      tables[UriComponent.Type.QUERY.ordinal()][33] = false;
      tables[UriComponent.Type.QUERY.ordinal()][42] = false;
      tables[UriComponent.Type.QUERY.ordinal()][39] = false;
      tables[UriComponent.Type.QUERY.ordinal()][40] = false;
      tables[UriComponent.Type.QUERY.ordinal()][41] = false;
      tables[UriComponent.Type.QUERY.ordinal()][59] = false;
      tables[UriComponent.Type.QUERY.ordinal()][58] = false;
      tables[UriComponent.Type.QUERY.ordinal()][64] = false;
      tables[UriComponent.Type.QUERY.ordinal()][36] = false;
      tables[UriComponent.Type.QUERY.ordinal()][44] = false;
      tables[UriComponent.Type.QUERY.ordinal()][47] = false;
      tables[UriComponent.Type.QUERY.ordinal()][63] = false;
      tables[UriComponent.Type.QUERY_PARAM.ordinal()] = Arrays.copyOf(
         tables[UriComponent.Type.QUERY.ordinal()], tables[UriComponent.Type.QUERY.ordinal()].length
      );
      tables[UriComponent.Type.QUERY_PARAM.ordinal()][61] = false;
      tables[UriComponent.Type.QUERY_PARAM.ordinal()][43] = false;
      tables[UriComponent.Type.QUERY_PARAM.ordinal()][38] = false;
      tables[UriComponent.Type.QUERY_PARAM_SPACE_ENCODED.ordinal()] = tables[UriComponent.Type.QUERY_PARAM.ordinal()];
      tables[UriComponent.Type.FRAGMENT.ordinal()] = tables[UriComponent.Type.QUERY.ordinal()];
      return tables;
   }

   private static boolean[] initEncodingTable(List<String> allowed) {
      boolean[] table = new boolean[128];

      for (String range : allowed) {
         if (range.length() == 1) {
            table[range.charAt(0)] = true;
         } else if (range.length() == 3 && range.charAt(1) == '-') {
            for (int i = range.charAt(0); i <= range.charAt(2); i++) {
               table[i] = true;
            }
         }
      }

      return table;
   }

   private static int[] initHexTable() {
      int[] table = new int[128];
      Arrays.fill(table, -1);

      for (char c = '0'; c <= '9'; c++) {
         table[c] = c - '0';
      }

      for (char c = 'A'; c <= 'F'; c++) {
         table[c] = c - 'A' + 10;
      }

      for (char c = 'a'; c <= 'f'; c++) {
         table[c] = c - 'a' + 10;
      }

      return table;
   }

   private static boolean isHexCharacter(char c) {
      return c < 128 && HEX_TABLE[c] != -1;
   }

   public enum Type {
      UNRESERVED,
      SCHEME,
      AUTHORITY,
      USER_INFO,
      HOST,
      PORT,
      PATH,
      PATH_SEGMENT,
      MATRIX_PARAM,
      QUERY,
      QUERY_PARAM,
      QUERY_PARAM_SPACE_ENCODED,
      FRAGMENT;
   }
}
