/*
 * 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.InputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import net.pms.PMS;
import net.pms.configuration.RendererConfiguration;
import net.pms.external.StartStopListenerDelegate;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.DLNAMediaAudio;	//regzamod
import net.pms.dlna.DLNAResource;
import net.pms.dlna.Range;
import net.pms.dlna.rz_SessionCtl;	//regzamod
import net.pms.dlna.rz_SessionInfo;	//regzamod
import net.pms.dlna.rz_VirtualVideoActionF;	//regzamod

import org.apache.commons.lang.StringUtils;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;	//regzamod
import org.jboss.netty.handler.codec.http.HttpVersion;			//regzamod
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;	//regzamod
import org.jboss.netty.handler.stream.ChunkedStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class handles all forms of incoming HTTP requests by constructing a proper HTTP response. 
 */
public class RequestV2 extends HTTPResource {
	private static final Logger logger = LoggerFactory.getLogger(RequestV2.class);
	private final static String CRLF = "\r\n";
	private static SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US);
	private static int BUFFER_SIZE = 8 * 1024;
	private static final int[] MULTIPLIER = new int[] { 1, 60, 3600, 24*3600}; 
	private final String method;
	
	//---- regzamod start
	private static String prev_msg;
	private static long prev_time;
	public static	int	ans_cnt;
	public static	int playstartf;
	public static	DLNAResource CurrentDLNA;	//global 
	public static	String CurrentFolderID;
	//public static	DLNAResource CurrentFolder;	//global 
	public int seekmode=0;	//=0: ByteSeek, =1:TimeSeek
	private rz_SessionInfo sess;

	public void setSessionInfo(rz_SessionInfo s) {
		//PMS.dbg("RequestV2.java.setSessionInfo: s="+s);
		sess=s;
	}
	public rz_SessionInfo getSessionInfo() {
		return sess;
	}
	//---- regzamod end

	/**
	 * A {@link String} that contains the argument with which this {@link RequestV2} was
	 * created. It contains a command, a unique resource id and a resource name, all
	 * separated by slashes. For example: "get/0$0$2$17/big_buck_bunny_1080p_h264.mov" or
	 * "get/0$0$2$13/thumbnail0000Sintel.2010.1080p.mkv"
	 */
	private final String argument;
	private String soapaction;
	private String content;
	private String objectID;
	private int startingIndex;
	private int requestCount;
	private String browseFlag;

	/**
	 * When sending an input stream, the lowRange indicates which byte to start from.  
	 */
	// private long lowRange
	private long lowRange = 0;	//regzamod
	private InputStream inputStream;
	private RendererConfiguration mediaRenderer;
	private String transferMode;
	private String contentFeatures;
	private final Range.Time range = new Range.Time();

	
	/**
	 * When sending an input stream, the highRange indicates which byte to stop at.  
	 */
	//private long highRange;
	private long highRange = 0;	//regzamod, -2:indicates highRange not setted
	private boolean http10;

	public RendererConfiguration getMediaRenderer() {
		return mediaRenderer;
	}

	public void setMediaRenderer(RendererConfiguration mediaRenderer) {
		this.mediaRenderer = mediaRenderer;
	}

	public InputStream getInputStream() {
		return inputStream;
	}

	/**
	 * When sending an input stream, the lowRange indicates which byte to start from.  
	 * @return The byte to start from
	 */
	public long getLowRange() {
		return lowRange;
	}

	/**
	 * Set the byte from which to start when sending an input stream. This value will
	 * be used to send a CONTENT_RANGE header with the response.
	 * @param lowRange The byte to start from.
	 */
	public void setLowRange(long lowRange) {
		//PMS.dbg("RequestV2: setLowRange, set seekmode=0");
		this.lowRange = lowRange;
		//this func may be used even if timeSeek mode?
	}

	public String getTransferMode() {
		return transferMode;
	}

	public void setTransferMode(String transferMode) {
		this.transferMode = transferMode;
	}

	public String getContentFeatures() {
		return contentFeatures;
	}

	public void setContentFeatures(String contentFeatures) {
		this.contentFeatures = contentFeatures;
	}

	public void setSeekMode(int mode) {
		//PMS.dbg("RequestV2: setSeekMode, set seekmode="+mode);
		seekmode=mode;
	}
	public int getSeekMode() {
		//PMS.dbg("RequestV2: setSeekMode, set seekmode="+mode);
		return seekmode;
	}
	public void setTimeRangeStart(Double timeseek) {
		//PMS.dbg("RequestV2: setTimeRangeStart, set seekmode=1");
		this.range.setStart(timeseek);
	}

	public void setTimeRangeStartString(String str) {
		setTimeRangeStart(convertTime(str));
	}

	public void setTimeRangeEnd(Double rangeEnd) {
		//PMS.dbg("RequestV2: setTimeRangeEnd, set seekmode=1");
		this.range.setEnd(rangeEnd);
	}

	public void setTimeRangeEndString(String str) {
		setTimeRangeEnd(convertTime(str));
	}

	/**
	 * When sending an input stream, the highRange indicates which byte to stop at.
	 * @return The byte to stop at.  
	 */
	public long getHighRange() {
		return highRange;
	}

	/**
	 * Set the byte at which to stop when sending an input stream. This value will
	 * be used to send a CONTENT_RANGE header with the response.
	 * @param highRange The byte to stop at.
	 */
	public void setHighRange(long highRange) {
		this.highRange = highRange;
		//this func may be used even if timeSeek mode?
	}

	public boolean isHttp10() {
		return http10;
	}

	public void setHttp10(boolean http10) {
		this.http10 = http10;
	}

	/**
	 * This class will construct and transmit a proper HTTP response to a given HTTP request.
	 * Rewritten version of the {@link Request} class.  
	 * @param method The {@link String} that defines the HTTP method to be used.
	 * @param argument The {@link String} containing instructions for PMS. It contains a command,
	 * 		a unique resource id and a resource name, all separated by slashes.
	 */
	public RequestV2(String method, String argument) {
		this.method = method;
		this.argument = argument;
	}

	public String getSoapaction() {
		return soapaction;
	}

	public void setSoapaction(String soapaction) {
		this.soapaction = soapaction;
	}

	public String getTextContent() {
		return content;
	}

	public void setTextContent(String content) {
		this.content = content;
		//PMS.dbg("received content: "+content);
	}

	/**
	 * Retrieves the HTTP method with which this {@link RequestV2} was created.
	 * @return The (@link String} containing the HTTP method.
	 */
	public String getMethod() {
		return method;
	}

	/**
	 * Retrieves the argument with which this {@link RequestV2} was created. It contains
	 * a command, a unique resource id and a resource name, all separated by slashes. For
	 * example: "get/0$0$2$17/big_buck_bunny_1080p_h264.mov" or "get/0$0$2$13/thumbnail0000Sintel.2010.1080p.mkv"
	 * @return The {@link String} containing the argument.
	 */
	public String getArgument() {
		return argument;
	}

	/**
	 * Construct a proper HTTP response to a received request. After the response has been
	 * created, it is sent and the resulting {@link ChannelFuture} object is returned.
	 * See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">RFC-2616</a>
	 * for HTTP header field definitions. 
	 * @param output The {@link HttpResponse} object that will be used to construct the response.
	 * @param e The {@link MessageEvent} object used to communicate with the client that sent
	 * 			the request.
	 * @param close Set to true to close the channel after sending the response. By default the
	 * 			channel is not closed after sending.
	 * @param startStopListenerDelegate The {@link StartStopListenerDelegate} object that is used
	 * 			to notify plugins that the {@link DLNAResource} is about to start playing.
	 * @return The {@link ChannelFuture} object via which the response was sent.
	 * @throws IOException
	 */
	public ChannelFuture answer(
		HttpResponse output,
		MessageEvent e,
		final boolean close,
		final StartStopListenerDelegate startStopListenerDelegate) throws IOException {
			
		ChannelFuture future = null;
		long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit
		StringBuilder response = new StringBuilder();
		DLNAResource dlna = null;
		boolean xbox = mediaRenderer.isXBOX();
		boolean wmp = mediaRenderer.isWMP();
		
		ans_cnt++;	//regzamod
		playstartf=0;	//regzamod
		long	lowRange_sv=lowRange;	//regzamod
		long	highRange_sv=highRange;	//regzamod
		boolean range_recover=false;	// need or not recover by low/highRange_sv
		int	trickf=0;	// regzamod
		//Range.Time range_sv=range;
		
		if(PMS.rz_debug>1) {	//regzamod
			PMS.dbg("================ ans_cnt="+ans_cnt+" RequestV2.answer Start ================");	//regzamod
			PMS.dbg("RequestV2.answer: method="+method+", argument="+argument+", close="+close);	//regzamod
			PMS.dbg("RequestV2.answer: lowRange="+lowRange+", highRange="+highRange+", Range="+range);	//regzamod
		}
		
		if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) {
			// Request to output a page to the HTLM console.
			output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html");
			response.append(HTMLConsole.servePage(argument.substring(8)));
		} else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) {
			// Request to retrieve a file

			// Extract the resource id from the argument string.
			//String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/"));
			int pos1=argument.indexOf("get/")+4;
			int pos2=argument.lastIndexOf("/");
			String id =null;
			if(pos1<pos2) {
				// get/0$1$2$/File
				id = argument.substring(pos1,pos2);
			}
			else {
				// get/0$1$2$
				id = argument.substring(pos1);
			}

			// Some clients escape the separators in their request, unescape them.
			id = id.replace("%24", "$");

			// Retrieve the DLNAresource itself.
			List<DLNAResource> files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer,sess);

			if (transferMode != null) {
				output.setHeader("TransferMode.DLNA.ORG", transferMode);
			}

			if (files.size() == 1) {
				// DNLAresource was found.
				dlna = files.get(0);
				
				if(sess!=null) {
					PMS.setCurrentSession(sess);
					if(sess.CurrentDLNA!=null) {
						if(sess.CurrentDLNA!=dlna) {
							//PMS.dbg("RequestV2.answer: ipa="+sess.ipaddr+", currentDLNA changed ->reset pauseTime to 0");
							//test for ByteSeekPause
							sess.CurrentDLNA.resStartTime=0;
							sess.CurrentDLNA.resStartTimeBias=0;
							sess.CurrentDLNA.resStartTimeBias2=0;
							sess.CurrentDLNA.pauseTime=0;
							sess.CurrentDLNA.resumeTime=0;
							sess.CurrentDLNA.resStopRealTime=0;
							sess.CurrentDLNA.resStat=0;  //reset: not started/paused yet
							sess.CurrentDLNA.bitrateForTimeSeek=0;
							sess.CurrentDLNA.bpsCalcCnt=0;
							sess.CurrentSeekMode=sess.CurrentDLNA.seek_mode;
							sess.cur_stopped=0;
							dlna.pauseTime=0;
						}
					}
					else {
						//test for ByteSeekPause
						dlna.pauseTime=0;
					}
					if(dlna!=null && dlna.getSrcPath()!=null) {
						sess.CurrentDLNA=dlna;
					}
					//PMS.dbg("RequestV2: method=get sess="+sess+", dlna="+sess.CurrentDLNA);
					
					//sess.CurrentFolderID=dlna.getParent().getResourceId(); //don't set Here!!
					CurrentFolderID=sess.CurrentFolderID;
					CurrentDLNA=sess.CurrentDLNA;	// set global last one
					
					if(PMS.rz_debug>2) {
						if(sess.CurrentDLNA!=null) {
							DLNAResource d=sess.CurrentDLNA;
							PMS.dbg("===== Current DLNA profile ===================================");
							PMS.dbg("-- SockAddr="+sess.sa);
							PMS.dbg("-- DLNA spec    ="+d.toString());
							
							RendererConfiguration r=PMS.get().getCurrentRenderer();
							if(r!=null) {
								PMS.dbg("-- DLNA_prof    = "+d.toString(r));
							}

							//PMS.dbg("-- DLNA_prof    = "+d.dlna_prof);
							//PMS.dbg("-- DLNA_prof    = "+d.getDlnaContentFeatures()
							//	+", DLNA_prof_org= "+d.dlnaspec_org);
							//PMS.dbg("-- MimeType     = "+d.currentMimeType
							//  +", MimeType_org = "+d.currentMimeType_org);
							PMS.dbg("-- Mediainfo    = "+d.getMedia());
							PMS.dbg("-- dlna    = "+d);
							PMS.dbg("=============================================");
						  //logger.info("-- MimeType     = "+d.mimeType());
						}
						else {
						  	PMS.dbg("Current DLNA is null");
						}
					}
				}
				
				
				String fileName = argument.substring(argument.lastIndexOf("/") + 1);

				if (fileName.startsWith("thumbnail0000")) {
					// This is a request for a thumbnail file.
					output.setHeader(HttpHeaders.Names.CONTENT_TYPE, files.get(0).getThumbnailContentType());
					output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
					output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT");
					output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");

					//if (mediaRenderer.isMediaParserV2()) { //origin 
					if (true || mediaRenderer.isMediaParserV2()) { //regzam
						// Need if the renderer is not isMediaParserV2(), 
						// b/c DB is common with renderer that isMediaParserV2()
						//PMS.dbg("RequestV2: call dlna.checkThumbnail(), name="+dlna.getName());
						dlna.checkThumbnail();
					}
					if(PMS.rz_debug>2) PMS.dbg("RequestV2: call dlna.getThumbnailInputStream(), name="+dlna.getName());
					inputStream = dlna.getThumbnailInputStream();
				} else {
					// This is a request for a regular file.

					// If range has not been initialized yet and the DLNAResource has its
					// own start and end defined, initialize range with those values before
					// requesting the input stream.
					
					final DLNAMediaInfo media = dlna.getMedia();
					Range.Time splitRange = dlna.getSplitRange();
					int zhack=mediaRenderer.getRZ_HackForZS1();
					int hack_mode=0;
					int hacked=0;
					//int	videof=0;	// regzamod, media is video?
					long maxlen=     99000000000L;
					long sseg_len=1443839+1024;	// typical 1st highRange
					
					//---- regzamod start
					if (media!=null) {
						//if(StringUtils.isNotBlank(media.getCodecV())) {	//regzamod
						//	videof=1;
						//}
						if(mediaRenderer.isRZ_TrickyStart()) {
							if(range.getStart()!=null) {
								if(range.getStart()<=2.1f) {
									PMS.dbg("answer: range is TIME, and start="+range.getStart()+" <2.0sec -> forcd 0.0sec ");
									range.setStart(0.0d);
								}
							}
							else if (lowRange>0 && highRange<0 && range.getStart()== null && dlna.getPlayer()!= null) {	
								//---- regzamod, For TEST ONLY!! 
								// hack for restart after skip/ff (for non-timeseekable renderer)
								// Not Good logic and has bad influence ,but effective for avoid abend of 1st play
								if(PMS.rz_debug>=0) {
									//logger.info("requestIdToRefcount="+dlna.requestIdToRefcount);
									PMS.dbg("RequestV2: ans_cnt="+ans_cnt+" Tricky start Type1 to avoid abend");
									PMS.dbg("( lowRange="+lowRange+", highRange="+highRange+", range="+range+" )");
									//logger.info("( lowRange_sv="+lowRange_sv+", highRange_sv="+highRange_sv+", playsart="+playstartf+" )");
								}
								//lowRange=0;		//regzamod
								//highRange=0;		//regzamod
								trickf=1;
							}
						}
						
						//---- regzamod, Regza SZ1 hack for illegal ByteRange(peeping far tail) 
						// typical illegal ranges are ...
						// lowRange=	 99998048256, highRange=99999492095 (length=1443839)
						// ENDFILE_POS = 99999475712L;
						// TRANS_SIZE = 100000000000L;
						
						if(zhack >0 && lowRange>maxlen) {	//real mode
							hack_mode=1;// for real hack (illegal ByteRange)
						}
						else if(zhack<0 && highRange<=sseg_len) {	//emulate mode for illegal byteseek 
							zhack= -zhack;
							if(zhack>100) {
								zhack -=100;
								// emulate illegal byte range
								lowRange =99998048256L;
								highRange=99999492095L;
							}
							hack_mode=2;// for test of hack 
						}
						if(hack_mode!=0) {	
							PMS.dbg("hack for illegal ByteRange request, RZ_HackForZS1="+zhack);
							
							if(zhack==1 || zhack==2) {
								
								if(splitRange.getStart() == null) {	//not splitrange
									//fakeup splitrange
									PMS.dbg("hack type="+zhack+": act as if Metafile/Chapter play");
									// create fake splitrange
									Double end,start;
									if(dlna.getMedia()!=null) {
										end=dlna.getMedia().getDurationInSeconds();
									}
									else {
										end=3600d;	//??
									}
									if(zhack==1) {
										start=0d;	//from top
									}
									else {
										start=end-1.0d;	//last 1 second
									}
									splitRange=new Range.Time(start,end);
									hacked=1;
									PMS.dbg("hacked start="+start+", end="+end+", splitRange="+splitRange);
								}
							}
							else if(zhack==3) {	// force correct range WITH fake reply
								PMS.dbg("hack type="+zhack+": send whole range WITH fake reply");
								lowRange=0;
								highRange=-1;
								range_recover=true;	// reply range as requested
							} 
							else if(zhack==4) {	// force correct range WITHOUT fake reply
								PMS.dbg("hack type="+zhack+": send whole range WITHOUT fake reply");
								lowRange=0;
								highRange=-1;
								range_recover=false;	// reply range corrected
							} 
							else if(zhack==5) {	// reply 416 error (Requested Range Not Satisfiable) 
								PMS.dbg("hack type="+zhack+": reply 416 error (Requested Range Not Satisfiable)");
								HttpResponse rep = null;
								rep = new DefaultHttpResponse(HttpVersion.HTTP_1_1, 
									HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
								
								rep.setHeader("Server", PMS.get().getServerName());
								rep.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
								
								future = e.getChannel().write(rep);
								return future;
							}
						}
					}
					//---- regzamod end
					
					//Range.Time splitRange = dlna.getSplitRange(); //regzamod, moved upper
					boolean need_time_bias=false;	//regzamod, need to add bias to timeseek 

					/* original
					if (range.getStart() == null && splitRange.getStart() != null) {	
							range.setStart(splitRange.getStart());
					}
					*/
				 	if(splitRange.getStart() != null) {		// chapter splitted file
						if(range.getStart() == null) {	// start from top
							range.setStart(splitRange.getStart());
						}
				 		else {  //start from mid
							Double bias=null;
							if(lowRange>0 && lowRange<maxlen ) {	//normal ByteRange
								//bias=dlna.convByteToTime(lowRange,mediaRenderer,0);
								bias=0d;
							}
							else {
								bias=range.getStart();
							}
							if(bias!=null) {
								//PMS.dbg("splitRange: getStart!=null, lowRange="+lowRange+", bias="+bias);
								range.setStart(splitRange.getStart()+bias);	//Good!
								//need_time_bias=true;	//Bad
							}
						}
					}
					
					if (range.getEnd() == null && splitRange.getEnd() != null) {
						range.setEnd(splitRange.getEnd());
					}
					
					if(PMS.rz_debug>1) {	//regzamod
						if(dlna.getMedia()!=null) {
							PMS.dbg("RequestV2.answer: media="+dlna.getMedia());	//regzamod
						}
						PMS.dbg("RequestV2.answer: After Maint. lowRange="+lowRange+", highRange="+highRange);	//regzamod
						PMS.dbg("RequestV2.answer: range="+range);	//regzamod
					}
					
					//serving msg ph1: dlna profile may NOT be accurate
					
					long now_time;
					//if(PMS.rz_debug>2 && dlna.sotype!=DLNAResource.SO_BUTTON_ELEMENT) {
					if(PMS.rz_debug>2 && (dlna.mflags&DLNAResource.MF_BUTTON_ELEMENT)==0) {
						now_time=System.currentTimeMillis();
						prev_msg=serv_msg(0,dlna,mediaRenderer,prev_msg,prev_time,now_time); 
						prev_time=now_time;
					}
					
					//inputStream = dlna.getInputStream(Range.create(lowRange, highRange, range.getStart(), range.getEnd()), 
					//	mediaRenderer,need_time_bias,seekmode);
					inputStream = dlna.getInputStream(lowRange,highRange,Range.create(lowRange, highRange, range.getStart(), range.getEnd()), 
						mediaRenderer,need_time_bias,seekmode,sess);
					//String name = dlna.getDisplayName(mediaRenderer);
					
					//serving msg ph2: dlna profile may be accurate
					//if(dlna.sotype!=DLNAResource.SO_BUTTON_ELEMENT) {
					
					if(trickf>0) {
						//highRange= -1;  // restore regzamod
					}
					if(range_recover) {// restore regzamod
						lowRange= lowRange_sv;  
						highRange= highRange_sv;  
					}

					if (inputStream == null) {
						// No inputStream indicates that transcoding / remuxing probably crashed.
						String msg="There is no inputstream to return for " + dlna.getName();
						if(prev_msg==null || !prev_msg.equals(msg)) {
							logger.error(msg);
							prev_msg=msg;
						}
						//int mtype=PMS.getConfiguration().getRZ_reply_error();
						int mtype=PMS.rz_reply_error;
						if(mtype>0) {//test
							// sould reply some error, otherwise, renderer may wait for play-start infinitly
							if(PMS.rz_debug>2) PMS.dbg("RequestV2.answer: inputStream == null --> Reply HTTP Error Messge (type="+mtype+"), for test");
							HttpResponse rep = null;
							HttpResponseStatus res = null;
							switch(mtype) {
							case 1:
								res=HttpResponseStatus.NOT_FOUND;		//good for mpegts(byteseek) & m2ts(timeseek) 
								break;
							case 2:
								res=HttpResponseStatus.NO_CONTENT;  	//best for mpegts(byteseek), bad for m2ts
								break;
							case 3:
								res=HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE; //effective
								break;
							case 4:
								res=HttpResponseStatus.INTERNAL_SERVER_ERROR; //??
								break;
							//---- not adequate, but for test	
							case 5:
								res=HttpResponseStatus.BAD_REQUEST; //??
								break;
							case 6:
								res=HttpResponseStatus.FORBIDDEN; //??
								break;
							case 7:
								res=HttpResponseStatus.LOCKED; //??
								break;
							case 8:
								res=HttpResponseStatus.NOT_IMPLEMENTED; //??
								break;
							case 9:
								res=HttpResponseStatus.SERVICE_UNAVAILABLE; //??
								break;
							case 10:
								res=HttpResponseStatus.UNAUTHORIZED; //??
								break;
							case 11:
								res=HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE; //??
								break;
							default:
								res=HttpResponseStatus.NOT_ACCEPTABLE; //??
								break;
							}
							rep=new DefaultHttpResponse(HttpVersion.HTTP_1_1,res); 
							rep.setHeader("Server", PMS.get().getServerName());
							rep.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0");
							//rep.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
							rep.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
							future = e.getChannel().write(rep);
							return future;
							
							//---- closing connection has no effects on stopping re-request from fool REGZA
							//future.getChannel().close();
							//future.addListener(ChannelFutureListener.CLOSE);
							//return null;
						}
					} 
					else {
						if((dlna.mflags&DLNAResource.MF_BUTTON_ELEMENT)==0) {  // not button: i.e. normal mediafile
							now_time=System.currentTimeMillis();
							prev_msg=serv_msg(1,dlna,mediaRenderer,prev_msg,prev_time,now_time); 
							prev_time=now_time;
						}
						if(PMS.getConfiguration().getRZ_reply_processing()>0) { 
							//test for quick response when playtarget changed
							//PMS.dbg("RequestV2.answer: reply HttpResponseStatus.PROCESSING, test for quick response");
							HttpResponse rep= new DefaultHttpResponse(HttpVersion.HTTP_1_1, 
								HttpResponseStatus.PROCESSING);
							rep.setHeader("Server", PMS.get().getServerName());
							rep.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
							ChannelFuture futurewk = e.getChannel().write(rep);
							//e.getChannel().flush();
						}

						// Notify plugins that the DLNAresource is about to start playing
						startStopListenerDelegate.start(dlna);

						// Try to determine the content type of the file
						String rendererMimeType = getRendererMimeType(files.get(0).mimeType(), mediaRenderer);
						if(dlna.getForcedMimeType()!=null) {	//regzamod, forced
							rendererMimeType=dlna.getForcedMimeType();
						}
						//dlna.currentMimeType=rendererMimeType; //memory
						
						if (rendererMimeType != null && !"".equals(rendererMimeType)) {
							output.setHeader(HttpHeaders.Names.CONTENT_TYPE, rendererMimeType);
						}

						//serv_msg(dlna,mediaRenderer); //moved upper
						// Response generation:
						// We use -1 for arithmetic convenience but don't send it as a value. 
						// If Content-Length < 0 we omit it, for Content-Range we use '*' to signify unspecified.

						boolean chunked = mediaRenderer.isChunkedTransfer();

						// Determine the total size. Note: when transcoding the length is
						// not known in advance, so DLNAMediaInfo.TRANS_SIZE will be returned instead.

						long totalsize = dlna.length(mediaRenderer);
						if(PMS.rz_direct_seek && dlna.getPlayer()==null && dlna.getMedia()!=null) {
							double bias_time=dlna.rz_PartPlaySttp;
							if(bias_time>0) {
								long bias_bytes=(long)(bias_time*dlna.getMedia().getSize()/dlna.getMedia().getDurationInSeconds());
								totalsize-=bias_bytes;
							}
						}
						if(totalsize<=0 && dlna.is_iav()) {	//add, regzamod
							//web imageFeed item return 0 length?
							totalsize = DLNAMediaInfo.TRANS_SIZE;
						}

						if (chunked && totalsize == DLNAMediaInfo.TRANS_SIZE) {
							// In chunked mode we try to avoid arbitrary values.
							totalsize = -1;
						}

						long remaining = totalsize - lowRange;
						long requested = highRange - lowRange;
						if(PMS.rz_debug>1) {	//regzamod
							PMS.dbg("totalsize="+totalsize+", lowRange="+lowRange
								+", highRange="+highRange+", inputStream.available="+inputStream.available());
						}
						if (requested != 0) {
							// Determine the range (i.e. smaller of known or requested bytes)
							long bytes = remaining > -1 ? remaining : inputStream.available();

							if (requested > 0 && bytes > requested) {
								bytes = requested + 1;
							}
							// Calculate the corresponding highRange (this is usually redundant).
							highRange = lowRange + bytes - (bytes > 0 ? 1 : 0);

							logger.trace((chunked ? "Using chunked response. " : "")  + "Sending " + bytes + " bytes.");
							String str="bytes " + lowRange + "-" + (highRange > -1 ? highRange : "*") + "/" 
								+ (totalsize > -1 ? totalsize : "*");	// regzamod
							
							if(PMS.rz_debug>1) {	//regzamod
								PMS.dbg("RequestV2: send answer= [ "+str+" ]");	//regzamod
							}
							
							output.setHeader(HttpHeaders.Names.CONTENT_RANGE,str);
								
							//output.setHeader(HttpHeaders.Names.CONTENT_RANGE, "bytes " + lowRange + "-" 
							//	+ (highRange > -1 ? highRange : "*") + "/" + (totalsize > -1 ? totalsize : "*"));

							// Content-Length refers to the current chunk size here, though in chunked
							// mode if the request is open-ended and totalsize is unknown we omit it.
							if (chunked && requested < 0 && totalsize < 0) {
								CLoverride = -1;
							} else {
								CLoverride = bytes;
							}
						} else {
							// Content-Length refers to the total remaining size of the stream here.
							CLoverride = remaining;
						}

						// Calculate the corresponding highRange (this is usually redundant).
						highRange = lowRange + CLoverride - (CLoverride > 0 ? 1 : 0);
						
						if(PMS.rz_debug>1) {	//regzamod
							PMS.dbg("RequestV2: new highRange= "+highRange);	//regzamod
						}

						if (contentFeatures != null) {
							if(zhack==6 && dlna.getPlayer()!=null) {
								PMS.dbg("hack type="+zhack+": set DLNA.ORG_CI=1, instead of 0. more fits to DLNA spec?");

								// if transcoding, set DLNA.ORG_CI=1, more compliant to DLNA spec?
								output.setHeader("ContentFeatures.DLNA.ORG", files.get(0).getDlnaContentFeaturesCI1());
							}
							else {
								// allways, set DLNA.ORG_CI=0
								output.setHeader("ContentFeatures.DLNA.ORG", files.get(0).getDlnaContentFeatures());
							}
						}

						output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
						output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
					}
				}
			}
		} else if ((method.equals("GET") || method.equals("HEAD")) && (argument.toLowerCase().endsWith(".png") || argument.toLowerCase().endsWith(".jpg") || argument.toLowerCase().endsWith(".jpeg"))) {
			if (argument.toLowerCase().endsWith(".png")) {
				output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/png");
			} else {
				output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/jpeg");
			}
			output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
			output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
			output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT");
			inputStream = getResourceInputStream(argument);
		} else if ((method.equals("GET") || method.equals("HEAD")) && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) {
			output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
			output.setHeader(HttpHeaders.Names.CACHE_CONTROL, "no-cache");
			output.setHeader(HttpHeaders.Names.EXPIRES, "0");
			output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
			output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
			inputStream = getResourceInputStream((argument.equals("description/fetch") ? "PMS.xml" : argument));
			if (argument.equals("description/fetch")) {
				byte b[] = new byte[inputStream.available()];
				inputStream.read(b);
				String s = new String(b);
				s = s.replace("uuid:1234567890TOTO", PMS.get().usn());//.substring(0, PMS.get().usn().length()-2));
				String profileName = PMS.getConfiguration().getProfileName();
				//if(PMS.rz_machine_name!=null) {
				//	profileName=PMS.rz_machine_name;
				//}
				if (PMS.get().getServer().getHost() != null) {
					s = s.replace("<host>", PMS.get().getServer().getHost());
					s = s.replace("<port>", "" + PMS.get().getServer().getPort());
				}
				if (xbox) {
					logger.debug("DLNA changes for Xbox360");
					//s = s.replace("PS3 Media Server", "PS3 Media Server [" + profileName + "] : Windows Media Connect");
					s = s.replace("PMS for REGZA", PMS.rz_server_name+" [" + profileName + "] : Windows Media Connect");
					s = s.replace("<modelName>PMS</modelName>", "<modelName>Windows Media Connect</modelName>");
					s = s.replace("<serviceList>", "<serviceList>" + CRLF + "<service>" + CRLF
						+ "<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>" + CRLF
						+ "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>" + CRLF
						+ "<SCPDURL>/upnp/mrr/scpd</SCPDURL>" + CRLF
						+ "<controlURL>/upnp/mrr/control</controlURL>" + CRLF
						+ "</service>" + CRLF);
				} else {
					//s = s.replace("PS3 Media Server", "PS3 Media Server [" + profileName + "]");
					s = s.replace("PMS for REGZA", PMS.rz_server_name+" [" + profileName + "]");
				}

				if (!mediaRenderer.isPS3()) {
					// hacky stuff. replace the png icon by a jpeg one. Like mpeg2 remux,
					// really need a proper format compatibility list by renderer
					s = s.replace("<mimetype>image/png</mimetype>", "<mimetype>image/jpeg</mimetype>");
					s = s.replace("/images/thumbnail-256.png", "/images/thumbnail-120.jpg");
					s = s.replace(">256<", ">120<");
				}
				response.append(s);
				inputStream = null;
			}
		} else if (method.equals("POST") && (argument.contains("MS_MediaReceiverRegistrar_control") || argument.contains("mrr/control"))) {
			output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
			response.append(HTTPXMLHelper.XML_HEADER);
			response.append(CRLF);
			response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
			response.append(CRLF);
			if (soapaction != null && soapaction.contains("IsAuthorized")) {
				response.append(HTTPXMLHelper.XBOX_2);
				response.append(CRLF);
			} else if (soapaction != null && soapaction.contains("IsValidated")) {
				response.append(HTTPXMLHelper.XBOX_1);
				response.append(CRLF);
			}
			response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER);
			response.append(CRLF);
			response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
			response.append(CRLF);
		} else if (method.equals("POST") && argument.equals("upnp/control/connection_manager")) {
			output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
			if (soapaction.indexOf("ConnectionManager:1#GetProtocolInfo") > -1) {
				response.append(HTTPXMLHelper.XML_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.PROTOCOLINFO_RESPONSE);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
				response.append(CRLF);
			}
		} else if (method.equals("POST") && argument.equals("upnp/control/content_directory")) {
			output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
			if (soapaction.indexOf("ContentDirectory:1#GetSystemUpdateID") > -1) {
				response.append(HTTPXMLHelper.XML_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_HEADER);
				response.append(CRLF);
				response.append("<Id>" + DLNAResource.getSystemUpdateId() + "</Id>");
				response.append(CRLF);
				response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
				response.append(CRLF);
			} else if (soapaction.indexOf("ContentDirectory:1#GetSortCapabilities") > -1) {
				response.append(HTTPXMLHelper.XML_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SORTCAPS_RESPONSE);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
				response.append(CRLF);
			} else if (soapaction.indexOf("ContentDirectory:1#GetSearchCapabilities") > -1) {
				response.append(HTTPXMLHelper.XML_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SEARCHCAPS_RESPONSE);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
				response.append(CRLF);
			} else if (soapaction.contains("ContentDirectory:1#Browse") 
				|| soapaction.contains("ContentDirectory:1#Search")) {
				
				//---- folder opened
				if(PMS.rz_debug>2) PMS.dbg("==>> RequestV2.answer: ContentDirectory:soapaction="+soapaction+", content="+content);
				objectID = getEnclosingValue(content, "<ObjectID>", "</ObjectID>");
				String containerID = null;
				if ((objectID == null || objectID.length() == 0)) {
					if(xbox) { 
						containerID = getEnclosingValue2(content, "<ContainerID>", "</ContainerID>");
						if (!containerID.contains("$")) {
							objectID = "0";
						} else {
							objectID = containerID;
							containerID = null;
						}
					}
					else if(wmp) {  //regzam, Tag may contain args: <tag arg1="..." arg2="...>
						objectID=getEnclosingValue2(content, "<ContainerID", "</ContainerID>");
						if(objectID==null) {
							objectID=getEnclosingValue2(content, "<ObjectID", "</ObjectID>");
						}
					}
				}
				Object sI = null;
				Object rC = null;
				if(wmp) {  //regzam, Tag may contain args: <tag arg1="..." arg2="...>
					sI = getEnclosingValue2(content, "<StartingIndex", "</StartingIndex>");
					rC = getEnclosingValue2(content, "<RequestedCount", "</RequestedCount>");
					browseFlag = getEnclosingValue2(content, "<BrowseFlag", "</BrowseFlag>");
				}
				else {
					sI = getEnclosingValue(content, "<StartingIndex>", "</StartingIndex>");
					rC = getEnclosingValue(content, "<RequestedCount>", "</RequestedCount>");
					browseFlag = getEnclosingValue(content, "<BrowseFlag>", "</BrowseFlag>");
				}

				if (sI != null) {
					startingIndex = Integer.parseInt(sI.toString());
				}

				if (rC != null) {
					requestCount = Integer.parseInt(rC.toString());
				}

				response.append(HTTPXMLHelper.XML_HEADER);
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
				response.append(CRLF);

				if (soapaction.contains("ContentDirectory:1#Search")) {
					response.append(HTTPXMLHelper.SEARCHRESPONSE_HEADER);
				} else {
					response.append(HTTPXMLHelper.BROWSERESPONSE_HEADER);
				}

				response.append(CRLF);
				response.append(HTTPXMLHelper.RESULT_HEADER);
				response.append(HTTPXMLHelper.DIDL_HEADER);

				if (soapaction.contains("ContentDirectory:1#Search")) {
					browseFlag = "BrowseDirectChildren";
				}

				// XBOX virtual containers ... d'oh!
				String searchCriteria = null;
				if (xbox && PMS.getConfiguration().getUseCache() && PMS.get().getLibrary() != null && containerID != null) {
					if (containerID.equals("7") && PMS.get().getLibrary().getAlbumFolder() != null) {
						objectID = PMS.get().getLibrary().getAlbumFolder().getResourceId();
					} else if (containerID.equals("6") && PMS.get().getLibrary().getArtistFolder() != null) {
						objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
					} else if (containerID.equals("5") && PMS.get().getLibrary().getGenreFolder() != null) {
						objectID = PMS.get().getLibrary().getGenreFolder().getResourceId();
					} else if (containerID.equals("F") && PMS.get().getLibrary().getPlaylistFolder() != null) {
						objectID = PMS.get().getLibrary().getPlaylistFolder().getResourceId();
					} else if (containerID.equals("4") && PMS.get().getLibrary().getAllFolder() != null) {
						objectID = PMS.get().getLibrary().getAllFolder().getResourceId();
					} else if (containerID.equals("1")) {
						String artist = getEnclosingValue(content, "upnp:artist = &quot;", "&quot;)");
						if (artist != null) {
							objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
							searchCriteria = artist;
						}
					}
				}

				//------------------------------------------------------------------------
				// Regzamod, save resume pos
				//------------------------------------------------------------------------
				String filtStr = getEnclosingValue(content, "<Filter>", "</Filter>");
				//PMS.dbg("RequestV2.answer: objectID="+objectID+", filterStr="+filtStr);
				if(sess!=null && requestCount>1) { 
					//tricky judge valid for REGZA-Z2 
					//requestCount>1 : normal folder open (get whole list under the folder)
					//requestCount==1: NOT normal folder open (instead, re-get single entry in the list) 
					//                 --> occures when move cursol over some pages in list on terminal
					if(true) { 
						//---- reset play stat for autoRepeat play
						// b/c folder open means play terminated for almost renderer 
						if(PMS.rz_trace>1) PMS.trace("RequestV2.answer:ContentBrowse, reset playPos");
						sess.playTimeTotal=0;  //msec
						sess.playTime=0;   //msec
						sess.playPos0=-1;
						sess.playPos1=-1;
						sess.playPos1_base=-1;
						sess.seek_bias=0;
					}
					
					//PMS.setCurrentSession(sess);
					//if(filtStr.contains("upnp:class")) {	
					if(true) {	
						//PMS.dbg("RequestV2.answer: [ContentDirectory:1#Browse] contains [upnp:class]");
						//"ContentDirectory:1#Browse" by upnp : occur once in browse actions?
						//if(PMS.rz_resume_mode==2 && sess.CurrentDLNA!=null && sess.CurrentFolderID!=null 
						
						if(PMS.rz_debug>1) {
							PMS.dbg("RequestV2.answer: ContentBrowse sess.CurrentDLNA="+sess.CurrentDLNA
								+",sess.CurrentFolderID="+sess.CurrentFolderID
								+", objectID="+objectID+", sess.CurrentFolderID="+sess.CurrentFolderID);
						}
						
						if(sess.cur_stopped==0 && sess.CurrentDLNA!=null && sess.CurrentFolderID!=null 
							&& objectID!=null
							&& objectID.equals(sess.CurrentFolderID)) {	//regzamod
							if(PMS.rz_debug>1) {
								//PMS.dbg("RequestV2: save ResumeTime, CurrentDLNA="+CurrentDLNA);
								//save resume pos when "ContentDirectory:1#Browse": play stopped and returned to list view
								//PMS.dbg("RequestV2: FolderBrowse="+objectID+", currentFolder="+sess.CurrentFolderID);
								//PMS.dbg("RequestV2.answer: session(renderer) ipaddr="+sess.ipaddr);
								//PMS.dbg("RequestV2.answer: save resume_pos of CurrentDLNA="+sess.CurrentDLNA.getName());
							}
							//PMS.dbg("RequestV2.answer:ContentBrowse, PlayStopped");
							PMS.rz_resume.playStopped(sess.CurrentDLNA,sess,2);	//regzamod
							sess.CurrentDLNA.pauseTime=0;	//clear pauseTime
							sess.cur_stopped=1;
							//sess.CurrentDLNA=null;
						}
						else if(objectID!=null && !objectID.equals(sess.CurrentFolderID)){  //folder changed
							if(sess.CurrentDLNA!=null) {
								DLNAResource pa=sess.CurrentDLNA.getParent();
								if(pa!=null && pa instanceof rz_VirtualVideoActionF) {
									//PMS.dbg("RequestV2.answer: BrowseContentDirectory, folderButton closed --> reset ignore_activate");
									((rz_VirtualVideoActionF)pa).ignore_activate=false;
								}
							}
						}
						//PMS.dbg("===>> RequestV2: received ContentDirectory, reset sess.CurrentDLNA=null");
						sess.CurrentFolderID=objectID; //current folder
						//sess.CurrentDLNA=null; //current dlna
					}
				}
				CurrentFolderID=objectID; //current folder
						
				//------------------------------------------------------------------------

				List<DLNAResource> files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(objectID, browseFlag != null && browseFlag.equals("BrowseDirectChildren"), startingIndex, requestCount, mediaRenderer,sess);
				
				//PMS.dbg("RequestV2.answer: getDLNAResources files cnt="+files.size());
				if (searchCriteria != null && files != null) {
					for (int i = files.size() - 1; i >= 0; i--) {
						if (!files.get(i).getName().equals(searchCriteria)) {
							files.remove(i);
						}
					}
					if (files.size() > 0) {
						files = files.get(0).getChildren();
					}
				}

				int minus = 0;
				int i=0;
				if (files != null) {
					for (DLNAResource uf : files) {
						//PMS.dbg("RequestV2.answer: i="+(i++)+", name="+uf.getName()+", isFolder="+uf.isFolder());
						if (xbox && containerID != null) {
							uf.setFakeParentId(containerID);
						}
						if (uf.isCompatible(mediaRenderer) && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer))) {
							response.append(uf.toString(mediaRenderer));
							if(PMS.rz_debug>3) {	//regzamod
								//PMS.dbg("RequestV2.answer: toString="+uf.toString(mediaRenderer));
								if(!uf.isFolder()) {
									//PMS.dbg("RequestV2: DLNAResource=" +uf.toString());	//regzamod
									if(PMS.rz_debug>1) {
										PMS.dbg("RequestV2: === DLNA Name="+uf.getName()+", mimeType="+uf.mimeType());	//regzamod
										PMS.dbg("RequestV2:     DLNA Spec="+uf.getDlnaContentFeatures());	//regzamod
									}
								}
							}
						} else {
							//PMS.dbg("RequestV2: Name()="+uf.getName()+" not playable --> delete from list");
							minus++;
						}
					}
				}

				response.append(HTTPXMLHelper.DIDL_FOOTER);
				response.append(HTTPXMLHelper.RESULT_FOOTER);
				response.append(CRLF);
				int filessize = 0;
				if (files != null) {
					filessize = files.size();
				}
				int cnt_ret=filessize - minus;
				response.append("<NumberReturned>").append(cnt_ret).append("</NumberReturned>");
				response.append(CRLF);
				DLNAResource parentFolder = null;
				if (files != null && filessize > 0) {
					parentFolder = files.get(0).getParent();
				}
				if (browseFlag != null && browseFlag.equals("BrowseDirectChildren") && mediaRenderer.isMediaParserV2() 
					&& mediaRenderer.isDLNATreeHack()) {
					// with the new parser, files are parsed and analyzed *before* creating the DLNA tree,
					// every 10 items (the ps3 asks 10 by 10),
					// so we do not know exactly the total number of items in the DLNA folder to send
					// (regular files, plus the #transcode folder, maybe the #imdb one, also files can be
					// invalidated and hidden if format is broken or encrypted, etc.).
					// let's send a fake total size to force the renderer to ask following items
					
					int totalCount = startingIndex + requestCount + 1; // returns 11 when 10 asked
					
					//PMS.dbg("RequestV2: startingIndex="+startingIndex
					//	+", requestCount="+requestCount+", totalCount="+totalCount+", minus="+minus
					//	);
						
					if (filessize - minus <= 0) // if no more elements, send the startingIndex
					{
						totalCount = startingIndex;
					}
					//else if(startingIndex==0 && (filessize - minus)<requestCount) {
					//	PMS.dbg("RequestV2: totalCount<requestCount, so make single reply");
					//	totalCount=requestCount;
					//}
					response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>");
				} else if (browseFlag != null && browseFlag.equals("BrowseDirectChildren")) {
					int cnt_ttl=((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus;
					
					if(PMS.rz_debug>2) {
						PMS.dbg("RequestV2: ContentDirectory startingIndex="+startingIndex
							+", requestCount="+requestCount+", count_returned="+cnt_ret+", count_total="+cnt_ttl);
					}
					
					//response.append("<TotalMatches>").append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus).append("</TotalMatches>");
					response.append("<TotalMatches>").append(cnt_ttl).append("</TotalMatches>");
				} else { //from upnp spec: If BrowseMetadata is specified in the BrowseFlags then TotalMatches = 1
					response.append("<TotalMatches>1</TotalMatches>");
				}
				response.append(CRLF);
				response.append("<UpdateID>");
				if (parentFolder != null) {
					response.append(parentFolder.getUpdateId());
				} else {
					response.append("1");
				}
				response.append("</UpdateID>");
				response.append(CRLF);
				if (soapaction.contains("ContentDirectory:1#Search")) {
					response.append(HTTPXMLHelper.SEARCHRESPONSE_FOOTER);
				} else {
					response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER);
				}
				response.append(CRLF);
				response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
				response.append(CRLF);
				// logger.trace(response.toString());
			}
		} else if (method.equals("SUBSCRIBE")) {
			output.setHeader("SID", PMS.get().usn());
			output.setHeader("TIMEOUT", "Second-1800");
		} else if (method.equals("NOTIFY")) {
			output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml");
			output.setHeader("NT", "upnp:event");
			output.setHeader("NTS", "upnp:propchange");
			output.setHeader("SID", PMS.get().usn());
			output.setHeader("SEQ", "0");
			response.append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
			response.append("<e:property>");
			response.append("<TransferIDs></TransferIDs>");
			response.append("</e:property>");
			response.append("<e:property>");
			response.append("<ContainerUpdateIDs></ContainerUpdateIDs>");
			response.append("</e:property>");
			response.append("<e:property>");
			response.append("<SystemUpdateID>").append(DLNAResource.getSystemUpdateId()).append("</SystemUpdateID>");
			response.append("</e:property>");
			response.append("</e:propertyset>");
		}

		output.setHeader("Server", PMS.get().getServerName());

		if (response.length() > 0) {
			// A response message was constructed; convert it to data ready to be sent.
			byte responseData[] = response.toString().getBytes("UTF-8");
			output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + responseData.length);

			// HEAD requests only require headers to be set, no need to set contents.
			if (!method.equals("HEAD")) {
				// Not a HEAD request, so set the contents of the response.
				ChannelBuffer buf = ChannelBuffers.copiedBuffer(responseData);
				output.setContent(buf);
			}

			// Send the response to the client.
			if(PMS.rz_debug>2) logger.debug("RequestV2.answer: Answer Type1 (Headder Only), Head="+output);	// regzamod
			future = e.getChannel().write(output);

			if (close) {
				// Close the channel after the response is sent.
				future.addListener(ChannelFutureListener.CLOSE);
			}
		} else if (inputStream != null) {
			// There is an input stream to send as a response.

			if (CLoverride > -2) {
				// Content-Length override has been set, send or omit as appropriate
				if (CLoverride > -1 && CLoverride != DLNAMediaInfo.TRANS_SIZE) {
					// Since PS3 firmware 2.50, it is wiser not to send an arbitrary Content-Length,
					// as the PS3 will display a network error and request the last seconds of the
					// transcoded video. Better to send no Content-Length at all.
					if(trickf==0) {
						output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + CLoverride);
					}
					else {	// some Regza TV is also same as PS3
						//Good!! 
						if(PMS.rz_debug>1) PMS.dbg("trickf="+trickf+", not send CONTENT_LENGTH");
					}
				}
			} else {
				int cl = inputStream.available();
				logger.trace("Available Content-Length: " + cl);
				output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + cl);
			}

			if (range.isStartOffsetAvailable() && dlna != null) {
				// Add timeseek information headers.
				String timeseekValue = DLNAMediaInfo.getDurationString(range.getStartOrZero());
				String timetotalValue = dlna.getMedia()==null?"":dlna.getMedia().getDurationString();
				String timeEndValue = range.isEndLimitAvailable() ? DLNAMediaInfo.getDurationString(range.getEnd()) : timetotalValue;
				output.setHeader("TimeSeekRange.dlna.org", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue);
				output.setHeader("X-Seek-Range", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue);
			}

			// Send the response headers to the client.
			if(PMS.rz_debug>2) PMS.dbg("RequestV2.answer: Answer Type2 (Headder+Contents), Head="+output);	// regzamod

			future = e.getChannel().write(output);

			if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) {
				// Send the response body to the client in chunks.
				ChannelFuture chunkWriteFuture = e.getChannel().write(new ChunkedStream(inputStream, BUFFER_SIZE));

				// Add a listener to clean up after sending the entire response body.
				chunkWriteFuture.addListener(new ChannelFutureListener() {
					@Override
					public void operationComplete(ChannelFuture future) {
						if(PMS.getConfiguration().isRZ_close_after_send()) {//regzamod for test
							//if not close, then play will fail at end.
							try {
								PMS.get().getRegistry().reenableGoToSleep();
								inputStream.close();
							} catch (IOException e) {
							}

							// Always close the channel after the response is sent because of
							// a freeze at the end of video when the channel is not closed.
							future.getChannel().close();
							if(PMS.rz_debug>2) PMS.dbg("RequestV2.answer: clean up after sending the entire response body, call startStopListenerDelegate.stop");
							startStopListenerDelegate.stop(1);
						}
						else {
							if(PMS.rz_debug>1) PMS.dbg("RequestV2.answer: NOT closing channel/streams! after respons data send.");
						}
					}
				});
			} else {
				// HEAD method is being used, so simply clean up after the response was sent.
				try {
					PMS.get().getRegistry().reenableGoToSleep();
					inputStream.close();
				} catch (IOException ioe) {
				}

				if (close) {
					// Close the channel after the response is sent
					future.addListener(ChannelFutureListener.CLOSE);
				}
				if(PMS.rz_debug>2) PMS.dbg("RequestV2.answer: simply clean up after the response was sent, call startStopListenerDelegate.stop");
				startStopListenerDelegate.stop(1);
			}
		} else {
			// No response data and no input stream. Seems we are merely serving up headers.
			if (lowRange > 0 && highRange > 0) {
				// FIXME: There is no content, so why set a length?
				output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + (highRange - lowRange + 1));
			} else {
				output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0");
			}

			// Send the response headers to the client.
			if(PMS.rz_debug>2) {
				logger.debug("RequestV2.answer: Answer Type3 (NoHead & NoContents), Head="+output);	// regzamod
				logger.debug(" (Request Seems No response data and no input stream, so Let's Send Dummy Head!!??)" );	// regzamod
			}
			
			future = e.getChannel().write(output);

			if (close) {
				// Close the channel after the response is sent.
				future.addListener(ChannelFutureListener.CLOSE);
			}
		}

		// Log trace information
		if(PMS.rz_debug>2) {	// regzamod add if
			PMS.dbg("RequestV2.answer: Reply Head="+output);	// regzamod
			
			Iterator<String> it = output.getHeaderNames().iterator();
			while (it.hasNext()) {
				String headerName = it.next();
				logger.trace("Sent to socket: " + headerName + ": " + output.getHeader(headerName));
			}
		}

		return future;
	}

    /**
	 * Returns a date somewhere in the far future.
	 * @return The {@link String} containing the date
	 */
	private String getFUTUREDATE() {
		sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
		return sdf.format(new Date(10000000000L + System.currentTimeMillis()));
	}

	/**
	 * Returns the string value that is enclosed by the left and right tag in a content string.
	 * Only the first match of each tag is used to determine positions. If either of the tags
	 * cannot be found, null is returned.
	 * @param content The entire {@link String} that needs to be searched for the left and right tag. 
	 * @param leftTag The {@link String} determining the match for the left tag. 
	 * @param rightTag The {@link String} determining the match for the right tag.
	 * @return The {@link String} that was enclosed by the left and right tag.
	 */
	private String getEnclosingValue(String content, String leftTag, String rightTag) {
		String result = null;
		int leftTagPos = content.indexOf(leftTag);
		int rightTagPos = content.indexOf(rightTag, leftTagPos + 1);

		if (leftTagPos > -1 && rightTagPos > leftTagPos) {
			result = content.substring(leftTagPos + leftTag.length(), rightTagPos);
		}
		return result;
	}
				
	//for XBOX,WMP
	private String getEnclosingValue2(String content, String leftTag, String rightTag) {
		String result = null;
		int leftTagPos = content.indexOf(leftTag);
		int rightTagPos = content.indexOf(rightTag, leftTagPos + 1);
		
		if (leftTagPos > -1 && rightTagPos > leftTagPos) {
			result = content.substring(leftTagPos + leftTag.length(), rightTagPos);
			
			//result has leftTag's Tailpart(>)  i.e. result=".....>ObjectID"
			//Let's cutOff tailpart
			int leftTag_endp=result.indexOf(">");
			if(leftTag_endp>-1) {
				result=result.substring(leftTag_endp+1);
			}
		}
		//PMS.dbg("getEnclosingValue2: tag="+leftTag+", got_result="+result+"\n, target_content="+content);
		return result;
	}
	
	/**
	 * Parse as double, or if it's not just one number, handles {hour}:{minute}:{seconds}
	 * @param time
	 * @return
	 */
	private double convertTime(String time) {
		try {
			return Double.parseDouble(time);
		} catch (NumberFormatException e) {
			String[] arrs = time.split(":");
			double value, sum = 0;
			for (int i = 0; i < arrs.length; i++) {
				value = Double.parseDouble(arrs[arrs.length - i - 1]);
				sum += value * MULTIPLIER[i];
			}
			return sum;
		}

	}
	
	String serv_msg(int mode, DLNAResource dlna, RendererConfiguration mediaRenderer,
		String prev_msg,long prev_time,long now_time) {
			
		//String name = dlna.getDisplayName(mediaRenderer);
		String name=dlna.getName();
		DLNAMediaInfo media = dlna.getMedia();
		//private static String prev_msg=null;  //can't use static in method
		//private static long prev_time=0;
		
		int mtype=0;
		if(dlna.getExt()!=null) {
			if(dlna.getExt().isVideo()) mtype=1;
			else if(dlna.getExt().isAudio()) mtype=2;
			else if(dlna.getExt().isImage()) mtype=3;
		}
		
		if(true || mode>0) {
			if(dlna.getPlayer()==null) {
				name += " [Direct]";	//regzamod
			}
			else if(dlna.getEncMode()==PMS.ENC_REMUX) {
				name += " [Remux]";
			}
			else if(dlna.getEncMode()==PMS.ENC_TRANSCODE) {
				name += " [Trans]";
			}
			
			if(media != null) {
				if(StringUtils.isNotBlank(media.getCodecV())) {	//regzamod
					//videof=1;
				}
				if(dlna.wmed_valid) {	//for simple web contens
					if(PMS.rz_debug>2) {
						PMS.dbg("RequestV2: dlna.wmed_valid="+dlna.wmed_valid);
						PMS.dbg("RequestV2: dlna.wmed_w="+dlna.wmed_w);
						PMS.dbg("RequestV2: dlna.wmed_h="+dlna.wmed_h);
						PMS.dbg("RequestV2: dlna.wmed_vc="+dlna.wmed_vc);
					}
					
					if(dlna.wmed_fmt!=null) name += " [fmt: " + dlna.wmed_fmt + "]";
					if(dlna.wmed_vc!=null) name += " [vc: " + dlna.wmed_vc + "]";
					if(dlna.wmed_ac!=null) name += " [ac: " + dlna.wmed_ac + "]";	
					if(dlna.wmed_w!=null && dlna.wmed_h!=null) name += ", ("+ dlna.wmed_w +"x" + dlna.wmed_h+")";
					if(dlna.wmed_asp!=null) name += ", asp="+dlna.wmed_asp;
					if(dlna.wmed_fps!=null) name += ", fps="+dlna.wmed_fps;
					if(false && dlna.bitrateForTimeSeek>0) {
						//value may changed by skipp/FF --> cause multiple message lines : too verbose
						name+=String.format(", Estimated Mbps=(%.3f)",(dlna.bitrateForTimeSeek/1024.0));
					}
				}
				else {	//local contents
					if (StringUtils.isNotBlank(media.getContainer())) {
						//name += " [container: " + media.getContainer() + "]";
						name += " [fmt: " + media.getContainer() + "]";
					}
					//if (StringUtils.isNotBlank(media.getCodecV())) {
					//if(videof>0) {	//regzamod
					if(mtype==1) {	//video
						name += " [vc: " + media.getVideoCodec() + "]";	
						//name += " [vc: " + media.getCodecV() + "]";	  //detail but too long
						if(media.getAudioCodes().size() > 0) {
							DLNAMediaAudio audio=media.getAudioCodes().get(0);
							if(audio!=null ) {
								name += " [ac: " + audio.getAudioCodec() + "]";	
								//name += " [ac: " + audio.getCodecA() + "]";	//too long
								name += "[sf: " + audio.getSampleRate() +", ch :"+audio.getNrAudioChannels()+ "]";
							}
						}
						String trans_mbps="";
						if(dlna.bitrateForTimeSeek>0) {
							trans_mbps=String.format("(%.3f)",(dlna.bitrateForTimeSeek/1024.0));
						}
						name +=String.format(", (%dx%d), asp=%.4f, fps=%s, Mbps=%.3f %s",
							media.getWidth(),media.getHeight(),media.getAspect_f(),
							media.getFrameRate(),(media.getRealVideoBitrate()/1048576.0),trans_mbps);
					}
					else if(mtype==2) {  //audio
					//else if(media.getAudioCodes().size() > 0) {
						DLNAMediaAudio audio=media.getFirstAudioTrack();
						if(audio!=null ) {
							name += " [ac: " + audio.getAudioCodec() + "]";	
							//name += " [ac: " + audio.getCodecA() + "]";	//too long
							name += "[sf: " + audio.getSampleRate() +", ch: "+audio.getNrAudioChannels()+ "]";
							//name += String.format(",ch=%d, kbps=%.3f",audio.getNrAudioChannels(),media.getBitrate()/1000.0);
							name += String.format(", kbps=%.1f",media.getBitrate()/1000.0);
							
							if(PMS.rz_debug>2) {
								try {
									//try convert(recover) shift-jis entry to unicode
									//but seems can't recover: already damaged some whrere!
									String title_ut=audio.getSongname();
									byte[] tb=title_ut.getBytes("UTF16"); 	//encode (from "UTF16" ) to "UTF16": retain entry un converted
									String title_sj=new String(tb,"MS932");	//decode  from "MS932" ( to  UTF16): convert ms932 to unicode
									name += ", Title_utf="+title_ut+", Title_sjis="+title_sj;
								} catch (UnsupportedEncodingException e){
									logger.error("Error",e);
								}
							}
						}
					}
					else if(mtype==3) {  //image
						name += " [vc: " + media.getVideoCodec() + "]";	
						//name += " [vc: " + media.getCodecV() + "]";	//for detail
						name +=String.format(", (%dx%d)",
							media.getWidth(),media.getHeight());
					}
				}
			}
		}
		
		String msg="Serving " + name;
		
		//long nowtime=System.currentTimeMillis();
		if(prev_msg!=null && prev_msg.equals(msg) && now_time-prev_time<PMS.rz_dupmsg_supp_msec) {
			//suppress same messages succeed within the time(rz_dupmsg_supp_msec)
			//prev_time=nowtime;
		}
		else {
			PMS.get().getFrame().setStatusLine(msg);
			logger.info(msg);
			//prev_msg=msg;
			//prev_time=nowtime;
		}
		return msg;
	}

}
