package dev.peerat.framework;

import java.security.MessageDigest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dev.peerat.framework.utils.json.JsonMap;

public class RouteMapper<U extends User>{
	
	private Router<U> router;
	
	private Response[] responses;
	private Route[] routes;
	private Pattern[] patterns;
	private int[] calls;
	private int dif;
	
	public RouteMapper(Router<U> router){
		this.router = router;
		this.responses = new Response[0];
		this.routes = new Route[0];
		this.patterns = new Pattern[0];
	}

	public void register(Response response, Route route, Pattern pattern){
		int length = responses.length+1;
		this.responses = addElement(responses, new Response[length], response);
		this.routes = addElement(routes, new Route[length], route);
		this.patterns = addElement(patterns, new Pattern[length], pattern);
		if(this.calls != null) this.calls = new int[length];
	}
	
	public void activeReOrdering(){
		this.calls = new int[responses.length];
		this.dif = 2;
	}
	
	public boolean exec(Context context, HttpReader reader, HttpWriter writer) throws Exception{
		Response result = null;
		Matcher matcher = null;
		
		String path = context.getPath();
		
		synchronized(responses){
			for(int i = 0; i < responses.length; i++){
				Response response = responses[i];
				Route route = routes[i];
				Pattern pattern = patterns[i];
				matcher = pattern.matcher(path);
				if(matcher.matches()){
					if(route.websocket()){
						switchToWebSocket(reader, context);
						reader = new WebSocketReader(reader);
						writer = new WebSocketWriter(writer);
						if(route.needLogin()){
							String token = reader.<JsonMap>readJson().get("token");
							context.setUser(this.router.getUser(token));
						}
					}
					if((!context.isLogged()) && route.needLogin()){
						writer.response(401, router.getDefaultHeaders(context.getType()));
						return true;
					}
					order(i);
					result = response;
					break;
				}
			}
		}
		if(result != null){
			result.exec(matcher, context, reader, writer);
			return true;
		}
		return false;
	}
	
	public Response execTomcat(Context context, HttpReader reader, HttpWriter writer) throws Exception{
		Response result = null;
		Matcher matcher = null;
		
		String path = context.getPath();
		
		synchronized(responses){
			for(int i = 0; i < responses.length; i++){
				Response response = responses[i];
				Route route = routes[i];
				Pattern pattern = patterns[i];
				matcher = pattern.matcher(path);
				if(matcher.matches()){
					if(route.websocket()){
						switchToWebSocket(reader, context);
						result = response;
						break;
					}
					if((!context.isLogged()) && route.needLogin()){
						writer.response(401, router.getDefaultHeaders(context.getType()));
						return null;
					}
					order(i);
					result = response;
					break;
				}
			}
		}
		return result;
	}
	
	Matcher matchTomcat(Context context){
		Matcher result = null;
		String path = context.getPath();
		synchronized(responses){
			for(int i = 0; i < responses.length; i++){
				Pattern pattern = patterns[i];
				Matcher match = pattern.matcher(path);
				if(match.matches()){
					result = match;
					break;
				}
			}
		}
		
		return result;
	}
	
	private void order(int index){
		if(this.calls == null) return;
		int call = this.calls[index]++;
		if(call >= (Integer.MAX_VALUE-(dif+1))){
			for(int i = 0; i < this.calls.length; i++) this.calls[i]/=dif;
			dif++;
			call = this.calls[index];
		}
		if(index < 1) return;
		if(call > this.calls[index-1]){
			switchElement(responses, index, index-1);
			switchElement(routes, index, index-1);
			switchElement(patterns, index, index-1);
			
			this.calls[index] = this.calls[index-1];
			this.calls[index-1] = call;
		}
	}
	
	private <E> E[] addElement(E[] current, E[] growed, E element){
		System.arraycopy(current, 0, growed, 0, current.length);
		growed[current.length] = element;
		return growed;
	}
	
	private <E> void switchElement(E[] array, int before, int after){
		E tmp = array[before];
		array[before] = array[after];
		array[after] = tmp;
	}
	
	private void switchToWebSocket(HttpReader reader, Context context) throws Exception{
		String key = reader.getHeader("Sec-WebSocket-Key");
		if (key == null) throw new IllegalArgumentException();
		
		context.response(101,
				"Connection: Upgrade",
				"Upgrade: websocket",
				"Sec-WebSocket-Accept: "+printBase64Binary(MessageDigest.getInstance("SHA-1")
						.digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes("UTF-8"))));
	}


	// From javax.xml.bind.DatatypeConverter
	private String printBase64Binary(byte[] array){
		char[] arrayOfChar = new char[(array.length + 2) / 3 * 4];
		int i = _printBase64Binary(array, 0, array.length, arrayOfChar, 0);
		assert i == arrayOfChar.length;
		return new String(arrayOfChar);
	}

	private int _printBase64Binary(byte[] paramArrayOfbyte, int paramInt1, int paramInt2,
			char[] paramArrayOfchar, int paramInt3){
		int i = paramInt2;
		int j;
		for (j = paramInt1; i >= 3; j += 3){
			paramArrayOfchar[paramInt3++] = encode(paramArrayOfbyte[j] >> 2);
			paramArrayOfchar[paramInt3++] = encode(
					(paramArrayOfbyte[j] & 0x3) << 4 | paramArrayOfbyte[j + 1] >> 4 & 0xF);
			paramArrayOfchar[paramInt3++] = encode(
					(paramArrayOfbyte[j + 1] & 0xF) << 2 | paramArrayOfbyte[j + 2] >> 6 & 0x3);
			paramArrayOfchar[paramInt3++] = encode(paramArrayOfbyte[j + 2] & 0x3F);
			i -= 3;
		}
		if (i == 1){
			paramArrayOfchar[paramInt3++] = encode(paramArrayOfbyte[j] >> 2);
			paramArrayOfchar[paramInt3++] = encode((paramArrayOfbyte[j] & 0x3) << 4);
			paramArrayOfchar[paramInt3++] = '=';
			paramArrayOfchar[paramInt3++] = '=';
		}
		if (i == 2){
			paramArrayOfchar[paramInt3++] = encode(paramArrayOfbyte[j] >> 2);
			paramArrayOfchar[paramInt3++] = encode(
					(paramArrayOfbyte[j] & 0x3) << 4 | paramArrayOfbyte[j + 1] >> 4 & 0xF);
			paramArrayOfchar[paramInt3++] = encode((paramArrayOfbyte[j + 1] & 0xF) << 2);
			paramArrayOfchar[paramInt3++] = '=';
		}
		return paramInt3;
	}

	private char encode(int paramInt){
		return encodeMap[paramInt & 0x3F];
	}

	private static final char[] encodeMap = initEncodeMap();

	private static char[] initEncodeMap(){
		char[] arrayOfChar = new char[64];
		byte b;
		for (b = 0; b < 26; b++)
			arrayOfChar[b] = (char) (65 + b);
		for (b = 26; b < 52; b++)
			arrayOfChar[b] = (char) (97 + b - 26);
		for (b = 52; b < 62; b++)
			arrayOfChar[b] = (char) (48 + b - 52);
		arrayOfChar[62] = '+';
		arrayOfChar[63] = '/';
		return arrayOfChar;
	}

}
