/**
 * The MIT License
 * Copyright (c) 2010 <NAME>
 *
 * Contributors: Ovea.com, Mycila.<EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.glines.socketio.server;

import java.util.ArrayList;
import java.util.List;

public class SocketIOFrame {
	public static final char SEPERATOR_CHAR = '~';
	public enum FrameType {
		UNKNOWN(-1),
		CLOSE(0),
		SESSION_ID(1),
		HEARTBEAT_INTERVAL(2),
		PING(3),
		PONG(4),
		DATA(0xE),
		FRAGMENT(0xF);

		private int value;
		
		FrameType(int value) {
			this.value = value;
		}
		
		public int value() {
			return value;
		}
		
		public static FrameType fromInt(int val) {
			switch (val) {
			case 0:
				return CLOSE;
			case 1:
				return SESSION_ID;
			case 2:
				return HEARTBEAT_INTERVAL;
			case 3:
				return PING;
			case 4:
				return PONG;
			case 0xE:
				return DATA;
			case 0xF:
				return FRAGMENT;
			default:
				return UNKNOWN;
			}
		}
	}

	public static final int TEXT_MESSAGE_TYPE = 0;
	public static final int JSON_MESSAGE_TYPE = 1;
	
	private static boolean isHexDigit(String str, int start, int end) {
		for (int i = start; i < end; i++) {
			char c = str.charAt(i);
			if (!Character.isDigit(c) &&
					c < 'A' && c > 'F' && c < 'a' && c > 'f') {
				return false;
			}
		}
		return true;
	}
	
	public static List<SocketIOFrame> parse(String data) {
		List<SocketIOFrame> messages = new ArrayList<SocketIOFrame>();
		int idx = 0;

		// Parse the data and silently ignore any part that fails to parse properly.
		while (data.length() > idx && data.charAt(idx) == SEPERATOR_CHAR) {
			int start = idx + 1;
			int end = data.indexOf(SEPERATOR_CHAR, start);

			if (-1 == end || start == end || !isHexDigit(data, start, end)) {
				break;
			}

			int mtype = 0;
			int ftype = Integer.parseInt(data.substring(start, start + 1), 16);

			FrameType frameType = FrameType.fromInt(ftype);
			if (frameType == FrameType.UNKNOWN) {
				break;
			}

			if (end - start > 1) {
				mtype = Integer.parseInt(data.substring(start + 1, end), 16);
			}
			
			start = end + 1;
			end = data.indexOf(SEPERATOR_CHAR, start);

			if (-1 == end || start == end || !isHexDigit(data, start, end)) {
				break;
			}
			
			int size = Integer.parseInt(data.substring(start, end), 16);

			start = end + 1;
			end = start + size;

			if (data.length() < end) {
				break;
			}
			
			messages.add(new SocketIOFrame(frameType, mtype, data.substring(start, end)));
			idx = end;
		}
		
		return messages;
	}
	
	public static String encode(FrameType type, int messageType, String data) {
		StringBuilder str = new StringBuilder(data.length() + 16);
		str.append(SEPERATOR_CHAR);
		str.append(Integer.toHexString(type.value()));
		if (messageType != TEXT_MESSAGE_TYPE) {
			str.append(Integer.toHexString(messageType));
		}
		str.append(SEPERATOR_CHAR);
		str.append(Integer.toHexString(data.length()));
		str.append(SEPERATOR_CHAR);
		str.append(data);
		return str.toString();
	}
	
	private final FrameType frameType;
	private final int messageType;
	private final String data;
	
	public SocketIOFrame(FrameType frameType, int messageType, String data) {
		this.frameType = frameType;
		this.messageType = messageType;
		this.data = data;
	}
	
	public FrameType getFrameType() {
		return frameType;
	}
	
	public int getMessageType() {
		return messageType;
	}
	
	public String getData() {
		return data;
	}
	
	public String encode() {
		return encode(frameType, messageType, data);
	}
}
