/*
 * PS3 Media Server, for streaming any medias to your PS3.
 * Copyright (C) 2008  A.Brochard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 2
 * of the License only.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package net.pms.network;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.pms.PMS;
import net.pms.configuration.RendererConfiguration;
import net.pms.external.StartStopListenerDelegate;
import net.pms.dlna.rz_SessionCtl;	//regzamod
import net.pms.dlna.rz_SessionInfo;	//regzamod
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ChildChannelStateEvent; //regzamod
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.handler.codec.frame.TooLongFrameException;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RequestHandlerV2 extends SimpleChannelUpstreamHandler {
	private static final Logger logger = LoggerFactory.getLogger(RequestHandlerV2.class);
	private static final Pattern TIMERANGE_PATTERN =
 Pattern.compile("timeseekrange\\.dlna\\.org\\W*npt\\W*=\\W*([\\d\\.:]+)?\\-?([\\d\\.:]+)?",
			Pattern.CASE_INSENSITIVE);
	private volatile HttpRequest nettyRequest;
	private final ChannelGroup group;
	private static int already_dumped; //regzam

	// Used to filter out known headers when the renderer is not recognized
	private final static String[] KNOWN_HEADERS = { "Accept", "Accept-Language", "Accept-Encoding", "Connection",
		"Content-Length", "Content-Type", "Date", "Host", "User-Agent" };
	
	public RequestHandlerV2(ChannelGroup group) {
		this.group = group;
	}

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
		throws Exception {
		RequestV2 request = null;
		RendererConfiguration renderer = null;
		String userAgentString = null;
		StringBuilder unknownHeaders = new StringBuilder();
		String separator = "";
		String iadr="";
		
		HttpRequest nettyRequest = this.nettyRequest = (HttpRequest) e.getMessage();

		InetSocketAddress remoteAddress = (InetSocketAddress) e.getChannel().getRemoteAddress();
		InetAddress ia = remoteAddress.getAddress();

		// Apply the IP filter
		if (filterIp(ia)) {
			e.getChannel().close();
			logger.trace("Access denied for address " + ia + " based on IP filter");
			return;
		}
		if(ia!=null) {
			iadr=ia.getHostAddress();
		}

		logger.trace("Opened request handler on socket " + remoteAddress);
		PMS.get().getRegistry().disableGoToSleep();
		String argument=null;

		if (HttpMethod.GET.equals(nettyRequest.getMethod())) {
			argument=nettyRequest.getUri().substring(1);
			request = new RequestV2("GET", argument );
			
		} else if (HttpMethod.POST.equals(nettyRequest.getMethod())) {
			request = new RequestV2("POST", nettyRequest.getUri().substring(1));
		} else if (HttpMethod.HEAD.equals(nettyRequest.getMethod())) {
			request = new RequestV2("HEAD", nettyRequest.getUri().substring(1));
		} else {
			request = new RequestV2(nettyRequest.getMethod().getName(), nettyRequest.getUri().substring(1));
		}
		
		/*--- moved down to after renderer fixed
		//--- now request created ----
		rz_SessionInfo sess=PMS.rz_SessCtl.getSess(remoteAddress);	//regzamod
		request.setSessionInfo(sess);	// inform to request class
		sess.ctx=ctx;
		*/

		if(PMS.rz_debug>1) {	//regzamod
			PMS.dbg("===> MessageReceived: remoteAddress="+remoteAddress
				+", Ver=" + nettyRequest.getProtocolVersion().getText() 
				+ " : Method=" + request.getMethod() 
				+ " : Args=" + request.getArgument());
		}

		if (nettyRequest.getProtocolVersion().getMinorVersion() == 0) {
			request.setHttp10(true);
		}

		// The handler makes a couple of attempts to recognize a renderer from its requests.
		// IP address matches from previous requests are preferred, when that fails request
		// header matches are attempted and if those fail as well we're stuck with the
		// default renderer.

		int attempt_no=0;

		// Attempt 1: try to recognize the renderer by its socket address from previous requests
		renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(ia); 
		if (renderer != null) {
			attempt_no=1;
			//PMS.get().setRendererfound(renderer,ia);  //moved to later
			//request.setMediaRenderer(renderer); 		//moved to later
			if(PMS.rz_debug>2) {
				PMS.dbg("Attempt 1: getRendererConfigurationBySocketAddress: succeeded ia="+ia);
			}
		}
		else {
			if(PMS.rz_debug>2) {
				PMS.dbg("Attempt 1: getRendererConfigurationBySocketAddress: failed, ia="+ia+", try Attempt 2 by UserAgentHeader");
			}
		}
		
		//---- header dump ,regzam
		boolean hout=false;
		//if(renderer == null && PMS.rz_dump_ua_header>0) {
		if(PMS.rz_dump_ua_header>0) {
			if(PMS.rz_dump_ua_header>1 || renderer == null) { 
				hout=true;
				if(iadr.endsWith(".1") && PMS.rz_dump_ua_header<=1) {  //avoid verbose dump
					if(already_dumped>0) {
						hout=false;
					}
					already_dumped++;
				}
				if(hout) {	
					logger.info("--------- Reaquest HeaderLines Start (ia="+remoteAddress+") ---------------"); 
					if(renderer!=null) {
						logger.info("Renderer =" + renderer.getRendererName()); 
					}
					logger.info("Method(ia="+remoteAddress+") =" + request.getMethod()+", Argument="+argument); 
				}
			}
		}

		int hcnt=0;
		for (String name : nettyRequest.getHeaderNames()) {
			String headerLine = name + ": " + nettyRequest.getHeader(name);
			logger.trace("Received on socket: " + headerLine);
			//PMS.dbg("RequestHandlerV2.messageReceived: Received on socket: " + headerLine);

			//---- header dump ,regzam
			if(hout) {	//regzamod
				hcnt++;
				logger.info("HeaderLine (ia="+remoteAddress+") (No."+hcnt+")=" + headerLine); 
			}
			
			if (renderer == null && headerLine != null
					&& headerLine.toUpperCase().startsWith("USER-AGENT")
					&& request != null) {
				userAgentString = headerLine.substring(headerLine.indexOf(":") + 1).trim();
				
				logger.trace("Received UserAgentAdditionalHeader(whole headeline)=" + headerLine); //regzamod
				logger.trace("Received UserAgentString(a part of headerline)=" + userAgentString); //regzamod


				// Attempt 2: try to recognize the renderer by matching the "User-Agent" header
				renderer = RendererConfiguration.getRendererConfigurationByUA(userAgentString,ia);

				if (renderer != null) {	//regzamod, judge later!!
					attempt_no=2;
					//request.setMediaRenderer(renderer);
					//renderer.associateIP(ia);
					//PMS.get().setRendererfound(renderer,ia);
					if(PMS.rz_debug>2) {
						logger.info("Attempt 2: Matched media renderer \"" + renderer.getRendererName() 
							+ "\" based on User-Agent header \"" + headerLine + "\"");
					}
				}
			}

			if (renderer == null && headerLine != null && request != null) {
				// Attempt 3: try to recognize the renderer by matching an additional header
				renderer = RendererConfiguration.getRendererConfigurationByUAAHH(headerLine,ia);

				if (renderer != null) {	//regzamod
					attempt_no=3;
					//request.setMediaRenderer(renderer);
					//renderer.associateIP(ia);	// Associate IP address for later requests
					//PMS.get().setRendererfound(renderer,ia);
					if(PMS.rz_debug>2) {
						logger.trace("Attempt 3: Matched media renderer \"" + renderer.getRendererName() 
							+ "\" based on additional header \"" + headerLine + "\"");
					}
				}
			}
			
			try {
				StringTokenizer s = new StringTokenizer(headerLine);
				String temp = s.nextToken();
				if (request != null && temp.toUpperCase().equals("SOAPACTION:")) {
					request.setSoapaction(s.nextToken());
				} else if (headerLine.toUpperCase().indexOf("RANGE: BYTES=") > -1) {
					request.setSeekMode(1);	//regzamod
					//sess.CurrentSeekMode=1;	//regzamod
					String nums = headerLine.substring(
						headerLine.toUpperCase().indexOf(
						"RANGE: BYTES=") + 13).trim();
					StringTokenizer st = new StringTokenizer(nums, "-");
					if (!nums.startsWith("-")) {
						request.setLowRange(Long.parseLong(st.nextToken()));
					}
					if (!nums.startsWith("-") && !nums.endsWith("-")) {
						request.setHighRange(Long.parseLong(st.nextToken()));
					} else {
						request.setHighRange(-1);
					}
					if(PMS.rz_debug>2) {	//regzamod
						PMS.dbg("RequestHandlerV2: RangeByte found, headerLine="+headerLine);	
					}
						
				} else if (headerLine.toLowerCase().indexOf("transfermode.dlna.org:") > -1) {
					request.setTransferMode(headerLine.substring(headerLine.toLowerCase().indexOf("transfermode.dlna.org:") + 22).trim());
				} else if (headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") > -1) {
					request.setContentFeatures(headerLine.substring(headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") + 28).trim());
					//PMS.dbg("RequestHandlerV2: getcontentfeatures.dlna.org="+request.getContentFeatures());
				} else {
					Matcher matcher = TIMERANGE_PATTERN.matcher(headerLine);
					if (matcher.find()) {
						request.setSeekMode(2);		//regzamod
						//sess.CurrentSeekMode=2;	//regzamod
						String first = matcher.group(1);
						if (first != null) {
							request.setTimeRangeStartString(first);
						}
						String end = matcher.group(2);
						if (end != null) {
							request.setTimeRangeEndString(end);
						}
						if(PMS.rz_debug>2) {	//regzamod
							PMS.dbg("RequestHandlerV2: RangeTime found, headerLine="+headerLine);	
						}

					}  else {
						 // If we made it to here, none of the previous header checks matched.
						 // Unknown headers make interesting logging info when we cannot recognize
						 // the media renderer, so keep track of the truly unknown ones.
						boolean isKnown = false;

						// Try to match possible known headers.
						for (String knownHeaderString : KNOWN_HEADERS) {
							if (headerLine.toLowerCase().startsWith(knownHeaderString.toLowerCase())) {
								isKnown = true;
								break;
							}
						}

						if (!isKnown) {
							// Truly unknown header, therefore interesting. Save for later use.
							unknownHeaders.append(separator + headerLine);
							separator = ", ";
						}
					}
				}
			} catch (Exception ee) {
				logger.error("Error parsing HTTP headers", ee);
			}

		}
		
		//---- header dump ,regzam
		if(hout) {
			logger.info("--------- Reaquest HeaderLines End (ia="+remoteAddress+") -------------------"); 
		}

		if (request != null) {
			if (renderer != null) {
				if(PMS.rz_debug>2 || hout)  logger.info("RendererJudging Finally Succeeded: inetAddr="+remoteAddress+", Attempt["+attempt_no+"]: Matching renderer="+renderer);
			}
			else {
				attempt_no=4; // Still no media renderer recognized?
				// Attempt 4: Not really an attempt; all other attempts to recognize
				// the renderer have failed. The only option left is to assume the default renderer.
				if(PMS.rz_debug>2 || hout)  logger.info("RendererJudging Finally Failed: inetAddr="+remoteAddress+", Attempt["+attempt_no+"]: Matching Renderer Not Found, use getDefaultConf");
				if (userAgentString != null) {
					if(!userAgentString.equals("FDSSDP")) {
						// We have found an unknown renderer
						if(PMS.rz_debug>2 || hout) {
							logger.info("Media renderer was not recognized. Possible identifying HTTP headers: User-Agent: " + userAgentString
								+ ("".equals(unknownHeaders.toString()) ? "" : ", " + unknownHeaders.toString()));
						}
					}
				}
			}
		}
		
		//---- here anyway, renderer is fixed
		
		if (HttpHeaders.getContentLength(nettyRequest) > 0) {
			byte data[] = new byte[(int) HttpHeaders.getContentLength(nettyRequest)];
			ChannelBuffer content = nettyRequest.getContent();
			content.readBytes(data);
			String s=new String(data, "UTF-8");
			request.setTextContent(s);
			if(PMS.rz_debug>2) {
				PMS.dbg("===> MessageReceived: Content=");
				PMS.dbg(""+s);
			}
		}
		else {
			if(PMS.rz_debug>2) PMS.dbg("===> MessageReceived: Content is NULL");
		}

		if (request != null) {
			if(PMS.rz_debug>2) {
				logger.trace("HTTP: " + request.getArgument() + " / "
					+ request.getLowRange() + "-" + request.getHighRange());
			}
		}
		
		rz_SessionInfo sess=null;
		boolean s_initScript=true;
		boolean s_dispIconEnable=true;
		boolean s_isMyHost=false;
		if(request != null) {	//regzamod
			if(renderer==null) {  //Finally not found
				renderer=RendererConfiguration.getDefaultConf();  //set default
				s_initScript=false;  //dummyRenderer: dont setup script executor (that consume big resourecs)
			}
			//--- specific for myself(myServer) send this request
			if(userAgentString!=null && ia.equals(PMS.myHostAddr)) {
				if(PMS.rz_debug>2) PMS.dbg("userAgentString!=null: check if dev is mySelf: may cost Heavy!, ia="+ia);
				String myUah=PMS.get().getMyServerUAH();
				if(myUah!=null && userAgentString.equals(myUah)) {  // dev is myself(myServer)
					if(PMS.rz_debug>2 || hout) logger.info("RendererJudging: inetAddr="+remoteAddress+", Device seems to be myself(myServer): don't disp as a renderer, uah="+myUah); 
					s_isMyHost=true;
					s_initScript=false;
					s_dispIconEnable=false;
				}
			}
			//--- get exisiting or new session by judging ipAddr+renderer
			sess=PMS.rz_SessCtl.getSess(remoteAddress,renderer,s_isMyHost,s_initScript,s_dispIconEnable);
			sess.ctx=ctx;
			sess.CurrentSeekMode=request.getSeekMode();
			sess.setRenderer(renderer);		//re-make renderer instance (may be changed for multi-session on same renderer_type)
			renderer=sess.getRenderer();  	//re-get  renderer instance (may be changed for multi-session on same renderer_type)
			
			renderer.associateIP(remoteAddress);  //regzam
			request.setMediaRenderer(renderer);
			request.setSessionInfo(sess);
			
			if(sess.getDispIconEnable()) {
				PMS.get().setRendererfound_port(renderer,ia,remoteAddress,sess);
			}
			PMS.get().setCurrentRenderer(renderer);	// regzamod
			if(PMS.rz_debug>2) PMS.dbg("RequestHandlerV2: messageReceived, renderer="+sess.getRenderer().toString());
		}
		writeResponse(e, request, ia, sess);
	}

	/**
	 * Applies the IP filter to the specified internet address. Returns true
	 * if the address is not allowed and therefore should be filtered out,
	 * false otherwise.
	 * @param inetAddress The internet address to verify.
	 * @return True when not allowed, false otherwise.
	 */
	private boolean filterIp(InetAddress inetAddress) {
		return !PMS.getConfiguration().getIpFiltering().allowed(inetAddress);
	}

	//private void writeResponse(MessageEvent e, RequestV2 request, InetAddress ia) {
	private void writeResponse(MessageEvent e, RequestV2 request, InetAddress ia, rz_SessionInfo sess) {
		// Decide whether to close the connection or not.
		boolean close = HttpHeaders.Values.CLOSE.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.CONNECTION))
			|| nettyRequest.getProtocolVersion().equals(
			HttpVersion.HTTP_1_0)
			&& !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.CONNECTION));

		// Build the response object.
		HttpResponse response = null;
		if (request.getLowRange() != 0 || request.getHighRange() != 0) {
			response = new DefaultHttpResponse(
				/*request.isHttp10() ? HttpVersion.HTTP_1_0
				: */HttpVersion.HTTP_1_1,
				HttpResponseStatus.PARTIAL_CONTENT);
		} else {
			response = new DefaultHttpResponse(
				/*request.isHttp10() ? HttpVersion.HTTP_1_0
				: */HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		}

		//StartStopListenerDelegate startStopListenerDelegate = new StartStopListenerDelegate(ia.getHostAddress());
		StartStopListenerDelegate startStopListenerDelegate = new StartStopListenerDelegate(ia.getHostAddress(),sess);

		try {
			//request.setMediaRenderer(sess.getRenderer());
			//request.setSessionInfo(sess);
			request.answer(response, e, close, startStopListenerDelegate);
		} catch (IOException e1) {
			logger.trace("HTTP request V2 IO error: " + e1.getMessage());
			// note: we don't call stop() here in a finally block as
			// answer() is non-blocking. we only (may) need to call it
			// here in the case of an exception. it's a no-op if it's
			// already been called
			startStopListenerDelegate.stop(1);
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
		throws Exception {
		Channel ch = e.getChannel();
		Throwable cause = e.getCause();
		
		if(PMS.rz_debug>2) PMS.dbg("RequestHandlerV2.exceptionCaught: called");
		
		if (cause instanceof TooLongFrameException) {
			sendError(ctx, HttpResponseStatus.BAD_REQUEST);
			return;
		}
		if (cause != null && !cause.getClass().equals(ClosedChannelException.class) && !cause.getClass().equals(IOException.class)) {
			cause.printStackTrace();
		}
		if (ch.isConnected()) {
			sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
		}
		e.getChannel().close();
	}

	private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
		HttpResponse response = new DefaultHttpResponse(
			HttpVersion.HTTP_1_1, status);
		response.setHeader(
			HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
		response.setContent(ChannelBuffers.copiedBuffer(
			"Failure: " + status.toString() + "\r\n", Charset.forName("UTF-8")));

		// Close the connection as soon as the error message is sent.
		ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
		//ctx.getChannel().write(response);  //no effect
	}

	@Override
	public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e)
		throws Exception {
		
		//PMS.dbg("RequestHandlerV2.channelOpen: called");
		//InetSocketAddress remoteAddress = (InetSocketAddress) e.getChannel().getRemoteAddress();
		//rz_SessionInfo sess=PMS.rz_SessCtl.getSess(remoteAddress);
		//sess.channel_stat=1;	//regzamod, test
		
		// as seen in http://www.jboss.org/netty/community.html#nabble-td2423020
		super.channelOpen(ctx, e);
		if (group != null) {
			group.add(ctx.getChannel());
		}
	}
	/* Uncomment to see channel events in the trace logs
	@Override
	public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
	// Log all channel events.
	logger.trace("Channel upstream event: " + e);
	super.handleUpstream(ctx, e);
	}
	*/
	
	//--- negzamod, add for test
	@Override
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		
		//PMS.dbg("RequestHandlerV2.channelClosed: called");
		//followings havw no effects
		//InetSocketAddress remoteAddress = (InetSocketAddress) e.getChannel().getRemoteAddress();
		//rz_SessionInfo sess=PMS.rz_SessCtl.getSess(remoteAddress);
		//sess.channel_stat=-1;	//regzamod, test
		
		super.channelClosed(ctx, e);
	}

	@Override
	public void channelDisconnected(ChannelHandlerContext ctx,ChannelStateEvent e) throws Exception {
		//PMS.dbg("RequestHandlerV2.channelDisconnected: called");
		super.channelDisconnected(ctx, e);
	}
	
	@Override
	public void childChannelClosed(ChannelHandlerContext ctx,ChildChannelStateEvent e) throws Exception {
		
		//PMS.dbg("RequestHandlerV2.childChannelClosed: called");
		//InetSocketAddress remoteAddress = (InetSocketAddress) e.getChannel().getRemoteAddress();
		//rz_SessionInfo sess=PMS.rz_SessCtl.getSess(remoteAddress);
		//sess.channel_stat=-2;	//regzamod, test
		
		super.childChannelClosed(ctx, e);
	}
}
