package monalipse.server.giko;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import monalipse.MonalipsePlugin;
import monalipse.part.CancelableRunner;
import monalipse.server.IResponseEnumeration;
import monalipse.server.IResponseHeaderLine;
import monalipse.server.IThreadContentProvider;
import monalipse.server.IThreadToolTipProvider;
import monalipse.server.RendererResource;
import monalipse.widgets.ColoredText;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.xml.sax.SAXException;

import com.meterware.httpunit.GetMethodWebRequest;
import com.meterware.httpunit.HttpException;
import com.meterware.httpunit.PostMethodWebRequest;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebRequest;
import com.meterware.httpunit.WebResponse;

class ThreadContentProvider implements IAdaptable, IThreadContentProvider
{
	private IWorkbenchWindow workbenchWindow;
	private ThreadListFragment fragment;
	private String baseURL;
	private String urlHint;
	private IFolder logFolder;
	private String id;
	private int index;
	private String name;
	private int responses;

	public ThreadContentProvider(IWorkbenchWindow workbenchWindow, String baseURL, IFolder logFolder, String id, int index, String name, int responses)
	{
		this.workbenchWindow = workbenchWindow;
		this.baseURL = baseURL;
		this.logFolder = logFolder;
		this.id = id;
		this.index = index;
		this.name = name;
		this.responses = responses;

		int ss = baseURL.indexOf("//");
		if(ss != -1 && id.endsWith(".dat"))
		{
			int s = baseURL.indexOf('/', ss + 2);
			urlHint = baseURL.substring(0, s) + "/test/read.cgi" +
						baseURL.substring(s, baseURL.length()) +
						id.substring(0, id.length() - 4) + "/l50";
		}
	}
	
	public void setThreadListFragment(ThreadListFragment fragment)
	{
		this.fragment = fragment;
	}

	public ThreadListFragment getThreadListFragment()
	{
		return fragment;
	}
	
	public String getBaseURL()
	{
		return baseURL;
	}
	
	public String getURLHint()
	{
		return urlHint;
	}

	public IFolder getLogFolder()
	{
		return logFolder;
	}
	
	public IFile getLogFile()
	{
		return logFolder.getFile(id);
	}

	public String getID()
	{
		return id;
	}
	
	public int getIndex()
	{
		return index;
	}

	public String getName()
	{
		return name;
	}
	
	public int getResponseCountHint()
	{
		return responses;
	}
	
	public Object getAdapter(Class adapter)
	{
		return null;
	}
	
	public int hashCode()
	{
		return id.hashCode();
	}
	
	public boolean equals(Object obj)
	{
		if(obj instanceof ThreadContentProvider)
		{
			ThreadContentProvider thread = (ThreadContentProvider) obj;
			return thread.logFolder.equals(logFolder) && thread.id.equals(id);
		}
		return false;
	}
	
	public boolean submitResponse(CancelableRunner cancelable, String name, String mail, String body)
	{
		try
		{
			WebConversation wc = GikoServer.getWebConversation();
			wc.addCookie("NAME", name);
			wc.addCookie("MAIL", mail);
			
			WebRequest req = new PostMethodWebRequest(new URL(new URL(baseURL), "/test/bbs.cgi").toExternalForm());
			req.setHeaderField("Referer", getURLHint());
			
			String bbs = new URL(baseURL).getFile();
			req.setParameter("submit", "\u66f8\u304d\u8fbc\u3080");
			req.setParameter("bbs", bbs.substring(1, bbs.length() - 1));
			req.setParameter("key", id.substring(0, id.lastIndexOf('.')));
			req.setParameter("time", String.valueOf(System.currentTimeMillis() / 1000));
			req.setParameter("FROM", name);
			req.setParameter("mail", mail);
			req.setParameter("MESSAGE", body);
			
			String sid = GikoServer.getOysterSessionID();
			if(sid != null)
				req.setParameter("sid", sid);
			
			boolean retry = false;
			
			WebResponse resp = wc.getResponse(req);
			BufferedReader r = new BufferedReader(new InputStreamReader(resp.getInputStream(), "Windows-31J"));
			try
			{
				while(true)
				{
					String l = r.readLine();
					if(l == null)
						break;
					if(l.indexOf("\u66f8\u304d\u3053\u307f\u307e\u3057\u305f") != -1)
						return true;
					else if(l.indexOf("\u78ba\u8a8d") != -1)
						retry = true;
				}
			}
			finally
			{
				r.close();
			}
				
			if(retry)
			{
				resp = wc.getResponse(req);
				r = new BufferedReader(new InputStreamReader(resp.getInputStream(), "Windows-31J"));
				try
				{
					while(true)
					{
						String l = r.readLine();
						if(l == null)
							break;
						if(l.indexOf("\u66f8\u304d\u3053\u307f\u307e\u3057\u305f") != -1)
							return true;
					}
				}
				finally
				{
					r.close();
				}
			}
		}
		catch (MalformedURLException e)
		{
		}
		catch (UnsupportedEncodingException e)
		{
		}
		catch (IOException e)
		{
		}
		catch (SAXException e)
		{
		}
		
		return false;
	}
	
	public IResponseEnumeration getResponses(int sequence, int rangeStart, RendererResource rendererResource)
	{
		LogFile log = null;
		try
		{
			IFile file = logFolder.getFile(id);
			MonalipsePlugin.ensureSynchronized(file);
			if(file.exists())
			{
				log = LogFile.of(new DataInputStream(file.getContents()));
				if(log != null)
				{
					boolean partial = log.sequence == sequence;
					if(partial)
					{
						try
						{
							if(log.responseCount < rangeStart)
								throw new IOException();
							for(int i = 0; i < rangeStart; i++)
								log.skipResponse();
						}
						catch(IOException e)
						{
							log.close();
							log = LogFile.of(new DataInputStream(file.getContents()));
							return new ThreadLogReader(log.title, log, false, log.sequence, log.isActive, rendererResource);
						}
					}
					return new ThreadLogReader(log.title, log, partial, log.sequence, log.isActive, rendererResource);
				}
			}
		}
		catch (CoreException e)
		{
			e.printStackTrace();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		
		if(log != null)
			log.close();

		return new NullResponseEnumeration("?", sequence);
	}
	
	public IResponseEnumeration updateResponses(CancelableRunner cancelable, int sequence, int rangeStart, RendererResource rendererResource)
	{
//MonalipsePlugin.getDefault().getPreferenceStore().getString(GikoServer.PREF_OYSTER_ID);
		WebConversation wc = GikoServer.getWebConversation();
		WebRequest req = new GetMethodWebRequest(baseURL + "dat/" + id);
//		req.setHeaderField("User-Agent", "Monazilla/1.00 (monalipse/0.01)");
		IFile file = logFolder.getFile(id);
		String lastModified = null;
		
		boolean notFound = false;
System.err.println("partial");
		
		try
		{
			MonalipsePlugin.ensureSynchronized(file);
			if(file.exists())
			{
				LogFile log = LogFile.of(new DataInputStream(file.getContents()));
				try
				{
					if(log != null)
					{
						lastModified = log.lastModifierd;
						boolean partial = log.sequence == sequence;
						if(partial)
						{
							List logs = new ArrayList();
							List lastResponse = null;

							if(rangeStart <= log.responseCount)
							{
								while(0 < log.available())
								{
									lastResponse = new ArrayList();
									log.readResponse(lastResponse, rendererResource);
									logs.add(lastResponse);
								}
							}

							if(lastResponse != null)
							{
								req.setHeaderField("Range", "bytes=" + log.httpRangeStart + "-");
								req.setHeaderField("If-Modified-Since", lastModified);
								WebResponse wr = wc.getResponse(req);
								if(wr.getResponseCode() == 206)
								{
									RangeAnalyzeInputStream cin = new RangeAnalyzeInputStream(log.httpRangeStart, wr.getInputStream());
									InputStreamReader r = new InputStreamReader(cin, "Windows-31J");
									int contentLength = Integer.parseInt(wr.getHeaderField("Content-Length"));
									List lineReceiver = new ArrayList();
									LogFile.parseResponse(r, lineReceiver, logs.size(), rendererResource);
									if(equalResponse(lastResponse, lineReceiver))
										return new ThreadDownloader(log.title, cancelable, r, cin, true, log.sequence, wr.getHeaderField("Last-Modified"), logs, rangeStart, contentLength, file, true, rendererResource);
									else
										r.close();
								}
								else if(wr.getResponseCode() == 304)
								{
									return new ArrayThreadReader(log.title, true, log.sequence, logs, rangeStart, true);
								}
								else if(wr.getResponseCode() == 404 || wr.getResponseCode() == 302)
								{
									notFound = true;
								}
							}
						}
					}
				}
				finally
				{
					if(log != null)
						log.close();
				}
			}
		}
		catch (MalformedURLException e)
		{
			e.printStackTrace();
		}
		catch (CoreException e)
		{
			e.printStackTrace();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		catch (SAXException e)
		{
			e.printStackTrace();
		}
		catch (HttpException e)
		{
			e.printStackTrace();
		}
		
		sequence++;

System.err.println("full");
		if(!notFound)
		{
			req.removeHeaderField("Range");
			IResponseEnumeration res = getResponse(req, lastModified, cancelable, sequence, file, false, true, rendererResource);
			if(res != null)
				return res;
		}
		
		if(14 <= id.length())
			req = new GetMethodWebRequest(baseURL + "kako/" + id.substring(0, 4) + "/" + id.substring(0, 5) + "/" + id + ".gz");
		else if(id.length() == 13)
			req = new GetMethodWebRequest(baseURL + "kako/" + id.substring(0, 3) + "/" + id + ".gz");
		else
			req = null;
		
System.err.println("log " + req);
		if(req != null)
		{
			IResponseEnumeration res = getResponse(req, lastModified, cancelable, sequence, file, false, false, rendererResource);
			if(res != null)
				return res;
		}
		
System.err.println("oyster ");
		String sid = GikoServer.getOysterSessionID();
		if(sid != null)
		{
			try
			{
				req = new GetMethodWebRequest(new URL(baseURL), "/test/offlaw.cgi");
				String bbs = new URL(baseURL).getFile();
				req.setParameter("bbs", bbs.substring(1, bbs.length() - 1));
				req.setParameter("key", id.substring(0, id.lastIndexOf('.')));
				req.setParameter("sid", sid);
				req.setParameter("raw", "0.0");
				
				IResponseEnumeration res = getResponse(req, lastModified, cancelable, sequence, file, true, false, rendererResource);
				if(res != null)
					return res;
			}
			catch (MalformedURLException e)
			{
			}
		}
		
		return new NullResponseEnumeration("?", sequence);
	}
	
	private boolean equalResponse(List resp1, List resp2)
	{
		if(resp1.size() != resp2.size())
			return false;

		for(int i = 0; i < resp1.size(); i++)
		{
			ColoredText.Line line1 = (ColoredText.Line)resp1.get(i);
			ColoredText.Line line2 = (ColoredText.Line)resp2.get(i);
			if(line1.getLineFragmentCount() != line2.getLineFragmentCount())
				return false;
			
			for(int j = 0; j < line1.getLineFragmentCount(); j++)
			{
				ColoredText.LineFragment frag1 = line1.getLineFragmentAt(j);
				ColoredText.LineFragment frag2 = line2.getLineFragmentAt(j);
				if(!frag1.getText().equals(frag2.getText()))
					return false;
			}
		}
		
		return true;
	}
	
	private IResponseEnumeration getResponse(WebRequest req, String lastModified, CancelableRunner cancelable, int sequence, IFile file, boolean oyster, boolean active, RendererResource rendererResource)
	{
		try
		{
			if(lastModified != null)
				req.setHeaderField("If-Modified-Since", lastModified);

			WebResponse wr = GikoServer.getWebConversation().getResponse(req);
			
			if(wr.getResponseCode() == 200)
			{
				RangeAnalyzeInputStream cin = new RangeAnalyzeInputStream(0, wr.getInputStream());
				InputStreamReader r = new InputStreamReader(cin, "Windows-31J");
				int contentLength = 0;
				String enc = wr.getHeaderField("Content-Encoding");
				String len = wr.getHeaderField("Content-Length");
				if(enc != null && enc.equals("gzip") && oyster)
					contentLength = readContentLength(r);
				else if(len != null)
					contentLength = Integer.parseInt(wr.getHeaderField("Content-Length"));
				return new ThreadDownloader("?", cancelable, r, cin, false, sequence, wr.getHeaderField("Last-Modified"), new ArrayList(), 0, contentLength, file, active, rendererResource);
			}
			else if(wr.getResponseCode() == 304)
			{
				return new NullResponseEnumeration("?", sequence);
			}
		}
		catch (MalformedURLException e)
		{
			e.printStackTrace();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		catch (SAXException e)
		{
			e.printStackTrace();
		}
		catch (HttpException e)
		{
			e.printStackTrace();
		}
		
		return null;
	}

	private int readContentLength(InputStreamReader r) throws IOException
	{
		if(r.read() != '+' || r.read() != 'O' || r.read() != 'K' || r.read() != ' ')
			throw new IOException();
		int ch = r.read();
		StringBuffer buf = new StringBuffer();
		while(ch != '/')
		{
			buf.append((char)ch);
			ch = r.read();
		}
		ch = r.read();
		while(ch != '/')
			ch = r.read();
		r.read();
		return Integer.parseInt(buf.toString());
	}
	
	public int getCachedCount()
	{
		try
		{
			IFile file = logFolder.getFile(getID());
			if(file.exists())
			{
				LogFile log = LogFile.of(new DataInputStream(file.getContents()));
				try
				{
					if(log != null)
						return log.responseCount;
				}
				finally
				{
					if(log != null)
						log.close();
				}
			}
		}
		catch (CoreException e)
		{
		}
		catch (IOException e)
		{
		}
		return 0;
	}
	
	private class ThreadDownloader implements IResponseEnumeration, IRunnableWithProgress
	{
		private String title;
		private Reader r;
		private RangeAnalyzeInputStream cin;
		private boolean partial;
		private int sequence;
		private String lastModified;
		private List log;
		private IFile logFile;
		private int position;
		private int contentRange;
		private int contentLength;
		private boolean closed;
		private boolean active;
		private RendererResource rendererResource;

		public ThreadDownloader(String title, CancelableRunner cancelable, Reader r, RangeAnalyzeInputStream cin, boolean partial, int sequence, String lastModified, List log, int position, int contentLength, IFile logFile, boolean active, RendererResource rendererResource)
		{
			this.title = title;
			this.r = r;
			this.cin = cin;
			this.partial = partial;
			this.sequence = sequence;
			this.lastModified = lastModified;
			this.log = log;
			this.position = position;
			this.contentLength = contentLength;
			this.logFile = logFile;
			this.active = active;
			this.rendererResource = rendererResource;
			contentRange = cin.getRange();
			cancelable.run(null, this);
		}

		public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException
		{
			try
			{
				try
				{
					boolean get = false;
					while(true)
					{
						List lines = new ArrayList();
						String title = LogFile.parseResponse(r, lines, log.size() + 1, rendererResource);
						if(lines.size() == 0)
							break;
						get = true;
						log.add(lines);
						if(log.size() == 1)
							this.title = title;
						synchronized(this)
						{
							notifyAll();
						}
						Thread.yield();
					}
					if(get)
						setLog(cin.getRange());
				}
				catch (IOException e)
				{
				}
			}
			finally
			{
				synchronized(this)
				{
					closed = true;
					notifyAll();
					try
					{
						r.close();
					}
					catch (IOException e)
					{
					}
				}
			}
		}

		private void setLog(int pos) throws IOException
		{
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			DataOutputStream dout = new DataOutputStream(bout);
			LogFile.writeLogFile(dout, sequence, title, lastModified, active, pos, log, rendererResource);
			dout.close();
			final byte[] bytes = bout.toByteArray();
			GikoServer.asyncExec(workbenchWindow, new WorkspaceModifyOperation()
				{
					protected void execute(IProgressMonitor monitor) throws InvocationTargetException
					{
						try
						{
							IFile cache = logFolder.getFile(id);
							MonalipsePlugin.ensureSynchronized(cache);
							if(cache.exists())
								cache.setContents(new ByteArrayInputStream(bytes), false, false, monitor);
							else
								cache.create(new ByteArrayInputStream(bytes), false, monitor);
						}
						catch (CoreException e)
						{
							throw new InvocationTargetException(e);
						}
					}
				});
		}
		
		public int getSequenceNumber()
		{
			return sequence;
		}
		
		public String getTitle()
		{
			return title;
		}
	
		public boolean isPartialContent()
		{
			return partial;
		}

		public boolean isReady()
		{
			return !closed && position < log.size();
		}

		public boolean hasNextResponse()
		{
			return !closed || position < log.size();
		}

		public boolean getNextResponse(List lineReceiver) throws InterruptedException
		{
			synchronized(this)
			{
				while(!closed && position == log.size())
					wait();
			}
			if(position < log.size())
			{
				lineReceiver.addAll((Collection)log.get(position++));
				return true;
			}
			else
			{
				return false;
			}
		}
		
		public int getProgressHint()
		{
			if(contentLength == 0)
				return 100;
			else
				return Math.max((cin.getRange() - contentRange) * 100 / contentLength, 100);
		}
		
		public void close()
		{
		}
		
		public boolean isWritable()
		{
			return active;
		}
	}
	
	private class ArrayThreadReader implements IResponseEnumeration
	{
		private String title;
		private boolean partial;
		private int sequence;
		private List log;
		private int position;
		private boolean active;

		public ArrayThreadReader(String title, boolean partial, int sequence, List log, int position, boolean active)
		{
			this.title = title;
			this.partial = partial;
			this.sequence = sequence;
			this.log = log;
			this.position = position;
			this.active = active;
		}
		
		public int getSequenceNumber()
		{
			return sequence;
		}
		
		public String getTitle()
		{
			return title;
		}
	
		public boolean isPartialContent()
		{
			return partial;
		}

		public boolean isReady()
		{
			return position < log.size();
		}

		public boolean hasNextResponse()
		{
			return position < log.size();
		}

		public boolean getNextResponse(List lineReceiver) throws InterruptedException
		{
			if(position < log.size())
			{
				lineReceiver.addAll((Collection)log.get(position++));
				return true;
			}
			else
			{
				return false;
			}
		}
		
		public int getProgressHint()
		{
			return Math.max(position * 100 / log.size(), 100);
		}
		
		public void close()
		{
		}
		
		public boolean isWritable()
		{
			return active;
		}
	}
	
	private static class RangeAnalyzeInputStream extends InputStream
	{
		private int range;
		private int lf;
		private int count;
		private InputStream in;

		public RangeAnalyzeInputStream(int range, InputStream in)
		{
			this.range = range;
			this.in = in;
			lf = range;
			count = range;
		}
		
		public int getRange()
		{
			return range;
		}
		
		public int read() throws IOException
		{
			int r = in.read();
			if(r == '\n')
			{
				range = lf;
				lf = count + 1;
			}
			count++;
			return r;
		}
		
		public int read(byte[] b) throws IOException
		{
			int r = in.read(b);
			for(int i = 0; i < r; i++)
			{
				if(b[i] == '\n')
				{
					range = lf;
					lf = count + i + 1;
				}
			}
			count += r;
			return r;
		}
		
		public int read(byte[] b, int off, int len) throws IOException
		{
			int r = in.read(b, off, len);
			int end = off + len;
			for(int i = off; i < end; i++)
			{
				if(b[i] == '\n')
				{
					range = lf;
					lf = count + i - off + 1;
				}
			}
			count += r;
			return r;
		}
	}
	
	private static class ThreadLogReader implements IResponseEnumeration
	{
		private String title;
		private LogFile log;
		private int sequence;
		private boolean partial;
		private boolean active;
		private RendererResource rendererResource;
		
		public ThreadLogReader(String title, LogFile log, boolean partial, int sequence, boolean active, RendererResource rendererResource)
		{
			this.title = title;
			this.log = log;
			this.sequence = sequence;
			this.partial = partial;
			this.active = active;
			this.rendererResource = rendererResource;
		}
		
		public int getSequenceNumber()
		{
			return sequence;
		}
		
		public String getTitle()
		{
			return title;
		}
		
		public boolean isPartialContent()
		{
			return partial;
		}

		public boolean isReady()
		{
			return hasNextResponse();
		}

		public boolean hasNextResponse()
		{
			try
			{
				return 0 < log.available();
			}
			catch (IOException e)
			{
				log.close();
				return false;
			}
		}

		public boolean getNextResponse(List lineReceiver)
		{
			try
			{
				log.readResponse(lineReceiver, rendererResource);
				return true;
			}
			catch (IOException e)
			{
				log.close();
				return false;
			}
		}
		
		public int getProgressHint()
		{
			return 100;
		}
		
		public void close()
		{
			log.close();
		}
		
		public boolean isWritable()
		{
			return active;
		}
	}
	
	private static class NullResponseEnumeration implements IResponseEnumeration
	{
		private String title;
		private int sequence;

		public NullResponseEnumeration(String title, int sequence)
		{
			this.title = title;
			this.sequence = sequence;
		}

		public int getSequenceNumber()
		{
			return sequence;
		}
		
		public String getTitle()
		{
			return title;
		}

		public boolean isPartialContent()
		{
			return true;
		}

		public boolean isReady()
		{
			return false;
		}

		public boolean hasNextResponse()
		{
			return false;
		}

		public boolean getNextResponse(List lineReceiver)
		{
			return false;
		}
		
		public int getProgressHint()
		{
			return 100;
		}
		
		public void close()
		{
		}
		
		public boolean isWritable()
		{
			return false;
		}
	}
	
	// int version
	// int sequence (for "a bone")
	// UTF title
	// UTF Last-Modified
	// int active (writable)
	// int HTTP range start
	// int <responseCount>
	// {
	//  int <lineCount>
	//  {
	//   int <fragmentCount>
	//   {
	//    UTF text
	//    UTF link
	//    int color
	//    int font
	//    int style
	//   }*
	//  }*
	// }*
	private static class LogFile
	{
		private static final int LOG_FILE_VERSION = 9;
		private static final int RESPONSE_BODY_INDENT = 32;
		private static final int STYLE_UNDERLINE = 0x01;

		private DataInputStream din;
		public int version;
		public int sequence;
		public String title;
		public String lastModifierd;
		public boolean isActive;
		public int httpRangeStart;
		public int responseCount;
		private int responseRead;
		
		public static LogFile of(DataInputStream din) throws IOException
		{
			LogFile res = null;
			try
			{
				int version = din.readInt();
				if(version == LOG_FILE_VERSION)
					res = new LogFile(din);
			}
			finally
			{
				if(res == null)
					din.close();
			}
			return res;
		}
		
		private LogFile(DataInputStream din) throws IOException
		{
			this.din = din;
			
			version = LOG_FILE_VERSION;
			sequence = din.readInt();
			title = din.readUTF();
			lastModifierd = din.readUTF();
			isActive = din.readInt() != 0;
			httpRangeStart = din.readInt();
			responseCount = din.readInt();
		}
		
		public void close()
		{
			try
			{
				if(din != null)
					din.close();
				din = null;
			}
			catch (IOException e)
			{
			}
		}
		
		public int available() throws IOException
		{
			return din.available();
		}
		
		public void skipResponse() throws IOException
		{
			int lineCount = din.readInt();
			for(int i = 0; i < lineCount; i++)
			{
				int fragmentCount = din.readInt();
				for(int j= 0; j < fragmentCount; j++)
				{
					din.skipBytes(din.readShort());
					din.skipBytes(din.readShort() + 12);
				}
			}
		}
		
		public void readResponse(List lineReceiver, RendererResource rendererResource) throws IOException
		{
			int lineCount = din.readInt();
			responseRead++;
			for(int i = 0; i < lineCount; i++)
			{
				ColoredText.Line line;
				if(i == 0)
					line = new ResponseHeaderLine(0, responseRead);
				else
					line = new ColoredText.Line(RESPONSE_BODY_INDENT);
				int fragmentCount = din.readInt();
				for(int j= 0; j < fragmentCount; j++)
				{
					String text = din.readUTF();
					String link = din.readUTF();
					Color color = rendererResource.getColor(din.readInt());
					Font font = rendererResource.getFont(din.readInt());
					int style = din.readInt();
					if(0 < link.length())
						line.addLineFragment(new LinkedLineFragment(text, color, font, (style & STYLE_UNDERLINE) != 0, link));
					else
						line.addLineFragment(new ColoredText.LineFragment(text, color, font, (style & STYLE_UNDERLINE) != 0));
				}
				lineReceiver.add(line);
			}
		}
		
		private static int skipWhitespace(Reader r) throws IOException
		{
			int ch = r.read();
			while(ch != -1 && Character.isWhitespace((char)ch))
				ch = r.read();
			return ch;
		}
		
		private static void filterMatches(String text, ColoredText.Line line, RendererResource rendererResource, Color color, Font font, PatternAction[] patterns)
		{
			int done = 0;
			
			Matcher[] matchers = new Matcher[patterns.length];
			for(int i = 0; i < matchers.length; i++)
				matchers[i] = patterns[i].matcher(text);
			
			boolean[] matchs = new boolean[matchers.length];
			for(int i = 0; i < matchs.length; i++)
				matchs[i] = patterns[i].next(matchers[i]);
			
			while(true)
			{
				int start = Integer.MAX_VALUE;
				int first = -1;

				for(int i = 0; i < matchs.length; i++)
				{
					if(matchs[i] && matchers[i].start() < start)
					{
						start = matchers[i].start();
						first = i;
					}
				}
				
				if(first == -1)
					break;
				
				int end = matchers[first].end();
				
				if(done <= start)
				{
					String link = patterns[first].getLink(matchers[first]);
					
					line.addLineFragment(new ColoredText.LineFragment(text.substring(done, start), color, font, false));
					line.addLineFragment(new LinkedLineFragment(text.substring(start, end), patterns[first].getColor(color, rendererResource), font, true, link));
	
					done = end;
				}
				
				matchs[first] = patterns[first].next(matchers[first]);
				if(matchs[first] && matchers[first].start() == start)
					matchs[first] = false;
			}

			line.addLineFragment(new ColoredText.LineFragment(text.substring(done, text.length()), color, font, false));
		}
		
		private static ColoredText.Line readToken(Reader r, List lineReceiver, ColoredText.Line line, RendererResource rendererResource, Color color, Font font, PatternAction[] patterns) throws IOException
		{
			StringBuffer buf = new StringBuffer(128);
			int ch = r.read();
			while(true)
			{
				if(ch == -1)
					return null;
	
				if(ch == '<')
				{
					if(0 < buf.length())
					{
						String text = buf.toString();
						if(patterns == null)
							line.addLineFragment(new ColoredText.LineFragment(text, color, font, false));
						else
							filterMatches(text, line, rendererResource, color, font, patterns);
					}
					buf = new StringBuffer(128);

					ch = r.read();
					if(ch == '>')
						return line;
					
					StringBuffer tagBuf = new StringBuffer(32);

					while(ch != '>')
					{
						tagBuf.append((char)ch);
						ch = r.read();
						if(ch == -1)
							return null;
					}
					
					String tag = tagBuf.toString().toLowerCase();
					if(tag.equals("b"))
					{
						font = rendererResource.boldFont;
					}
					else if(tag.equals("/b"))
					{
						font = rendererResource.font;
					}
					else if(tag.equals("br"))
					{
						lineReceiver.add(line);
						line = new ColoredText.Line(RESPONSE_BODY_INDENT);
					}
					else if(tag.startsWith("a "))
					{
					}
					else if(tag.indexOf("red") != -1)
					{
						color = rendererResource.red;
					}
					else if(tag.indexOf("forestgreen") != -1)
					{
						color = rendererResource.forestgreen;
					}

					ch = r.read();
				}
				else if(ch == '&')
				{
					ch = r.read();
					if(ch == '#')
					{
						ch = r.read();

						boolean x16 = false;
						if(ch == 'x')
						{
							x16 = true;
							ch = r.read();
						}
						
						StringBuffer b = new StringBuffer();
						while(Character.isDigit((char)ch) && b.length() < 5)
						{
							b.append((char)ch);
							ch = r.read();
						}
						
						try
						{
							buf.append((char)Integer.parseInt(b.toString(), x16 ? 16 : 10));
						}
						catch (NumberFormatException e)
						{
							buf.append("&#");
							if(x16)
								buf.append('x');
							buf.append(b.toString());
							if(ch == ';')
								buf.append(';');
						}

						if(ch == ';')
							ch = r.read();
					}
					else
					{
						StringBuffer b = new StringBuffer();
						while(Character.isLetter((char)ch) && b.length() < 4)
						{
							b.append((char)ch);
							ch = r.read();
						}

						String ref = b.toString();
						if(ref.equals("lt"))
							buf.append('<');
						else if(ref.equals("gt"))
							buf.append('>');
						else if(ref.equals("amp"))
							buf.append('&');
						else if(ref.equals("apos"))
							buf.append('\'');
						else if(ref.equals("quot"))
							buf.append('"');
						else if(ref.equals("hearts"))
							buf.append('\u2665');
						else
							buf.append('&').append(ref + (ch == ';' ? ";" : ""));

						if(ch == ';')
							ch = r.read();
					}
				}
				else
				{
					buf.append((char)ch);
					ch = r.read();
				}
			}
		}

		private static String parseResponse(Reader r, List lineReceiver, int responseNumber, RendererResource rendererResource) throws IOException
		{
			ColoredText.Line headerLine = new ResponseHeaderLine(0, responseNumber);
			headerLine.addLineFragment(new ColoredText.LineFragment(responseNumber + " : ", rendererResource.black, rendererResource.font, false));
			if(readToken(r, null, headerLine, rendererResource, rendererResource.forestgreen, rendererResource.boldFont, NAME_PATTERN_SET) == null)
				return null;
			headerLine.addLineFragment(new ColoredText.LineFragment(" ", rendererResource.black, rendererResource.font, false));
			if(readToken(r, null, headerLine, rendererResource, rendererResource.blue, rendererResource.boldFont, MAIL_PATTERN_SET) == null)
				return null;
			headerLine.addLineFragment(new ColoredText.LineFragment(" : ", rendererResource.black, rendererResource.font, false));
			if(readToken(r, null, headerLine, rendererResource, rendererResource.red, rendererResource.font, DATE_PATTERN_SET) == null)
				return null;
			lineReceiver.add(headerLine);
			ColoredText.Line line = readToken(r, lineReceiver, new ColoredText.Line(RESPONSE_BODY_INDENT), rendererResource, rendererResource.black, rendererResource.font, BODY_PATTERN_SET);
			if(line == null)
				return null;
			lineReceiver.add(line);
			lineReceiver.add(new ColoredText.Line(RESPONSE_BODY_INDENT));

			String title = null;
			int ch = r.read();
			if(ch != -1 && ch != '\n')
			{
				StringBuffer buf = new StringBuffer();
				while(ch != -1 && ch != '\n')
				{
					buf.append((char)ch);
					ch = r.read();
				}
				title = buf.toString();
			}
			return title;
		}
		
		private static void writeLogFile(DataOutputStream dout, int sequence, String title, String lastModified, boolean isActive, int httpRangeStart, List logs, RendererResource rendererResource) throws IOException
		{
			dout.writeInt(LOG_FILE_VERSION);
			dout.writeInt(sequence);
			dout.writeUTF(title);
			dout.writeUTF(lastModified);
			dout.writeInt(isActive ? 1 : 0);
			dout.writeInt(httpRangeStart);
			dout.writeInt(logs.size());
			for(int i = 0; i < logs.size(); i++)
			{
				List resp = (List)logs.get(i);
				dout.writeInt(resp.size());
				for(int j = 0; j < resp.size(); j++)
				{
					ColoredText.Line line = (ColoredText.Line)resp.get(j);
					dout.writeInt(line.getLineFragmentCount());
					for(int k = 0; k < line.getLineFragmentCount(); k++)
					{
						ColoredText.LineFragment frag = line.getLineFragmentAt(k);
						dout.writeUTF(frag.getText());
						if(frag instanceof LinkedLineFragment)
							dout.writeUTF(((LinkedLineFragment)frag).getReference());
						else
							dout.writeUTF("");
						dout.writeInt(rendererResource.getKey(frag.getColor()));
						dout.writeInt(rendererResource.getKey(frag.getFont()));
						dout.writeInt(frag.getUnderline() ? STYLE_UNDERLINE : 0);
					}
				}
			}
		}
	}
	
	private static String toASCIIDigits(String str)
	{
		StringBuffer buf = new StringBuffer();
		for(int i = 0; i < str.length(); i++)
		{
			char ch = str.charAt(i);
			if('\uff10' <= ch && ch <= '\uff19')
				ch = (char)('0' + (ch - '\uff10'));
			buf.append(ch);
		}
		return buf.toString();
	}

	private static final PatternAction NUM_REF_PATTERN = new PatternAction(Pattern.compile("(>|\uff1e)+(([0-9\uff10-\uff19]+)(->*([0-9\uff10-\uff19]+))?)"))
		{
			public String getLink(Matcher m)
			{
				String start = toASCIIDigits(m.group(3));
				String end = m.group(5);
				if(end == null)
					end = start;
				else
					end = toASCIIDigits(end);
				return "#" + start + "-" + end;
			}
		};
	private static final PatternAction NAME_NUM_REF_PATTERN = new PatternAction(Pattern.compile("[0-9\uff10-\uff19]+"))
		{
			public String getLink(Matcher m)
			{
				String num = toASCIIDigits(m.group());
				return "#" + num + "-" + num;
			}
			
			public boolean next(Matcher m)
			{
				return m.matches();
			}
			
			public Color getColor(Color currentColor, RendererResource rendererResource)
			{
				return currentColor;
			}
		};
	private static final PatternAction TRIP_DECL_PATTERN = new PatternAction(Pattern.compile("\u25C6([\\p{Alnum}\\./]{8,10})"))
		{
			public String getLink(Matcher m)
			{
				return "trip:" + m.group(1);
			}
			
			public boolean next(Matcher m)
			{
				return m.find();
			}
			
			public Color getColor(Color currentColor, RendererResource rendererResource)
			{
				return currentColor;
			}
		};
	private static final PatternAction URL_REF_PATTERN = new PatternAction(Pattern.compile("(((h?t)?t)?p)?(s?://[\\p{Alnum}\\.\\-_:]+(/[\\p{Alnum}!#%&'*+,-./:;=?@\\\\^_`\\|~]*)?)"))
		{
			public String getLink(Matcher m)
			{
				return "http" + m.group(4);
			}
		};
	private static final PatternAction ID_DECL_PATTERN = new PatternAction(Pattern.compile("ID:(.{8})"))
		{
			public String getLink(Matcher m)
			{
				return "id:" + m.group(1).trim();
			}
			
			public Color getColor(Color currentColor, RendererResource rendererResource)
			{
				return currentColor;
			}
		};
	private static final PatternAction[] NAME_PATTERN_SET = new PatternAction[]{NUM_REF_PATTERN, NAME_NUM_REF_PATTERN, TRIP_DECL_PATTERN};
	private static final PatternAction[] MAIL_PATTERN_SET = new PatternAction[]{NUM_REF_PATTERN};
	private static final PatternAction[] DATE_PATTERN_SET = new PatternAction[]{ID_DECL_PATTERN};
	private static final PatternAction[] BODY_PATTERN_SET = new PatternAction[]{NUM_REF_PATTERN, URL_REF_PATTERN};
	
	private static abstract class PatternAction
	{
		private Pattern pattern;
		
		public PatternAction(Pattern pattern)
		{
			this.pattern = pattern;
		}
		
		public Matcher matcher(String text)
		{
			return pattern.matcher(text);
		}
		
		public boolean next(Matcher m)
		{
			return m.find();
		}
		
		public Color getColor(Color currentColor, RendererResource rendererResource)
		{
			return rendererResource.blue;
		}
		
		public abstract String getLink(Matcher m);
	}
	
	private static class LinkedLineFragment extends ColoredText.LineFragment implements IThreadToolTipProvider
	{
		private String href;

		public LinkedLineFragment(String text, Color color, Font font, boolean underline, String href)
		{
			super(text, color, font, underline);
			this.href = href;
		}
		
		public String getReference()
		{
			return href;
		}
		
		public void prefetchToolTip()
		{
		}
		
		public void fillToolTip(Composite parent, ColoredText sourceText)
		{
			if(href.startsWith("#") && href.indexOf('-') != -1)
			{
				int start = Integer.parseInt(href.substring(1, href.indexOf('-')).trim());
				int end = Integer.parseInt(href.substring(href.indexOf('-') + 1, href.length()).trim());
				
				List lines = new ArrayList();
				boolean copy = false;
				
				for(int i = 0; i < sourceText.getLineCount(); i++)
				{
					ColoredText.Line line = sourceText.getLineAt(i);
					if(line instanceof ResponseHeaderLine)
					{
						ResponseHeaderLine rhl = (ResponseHeaderLine)line;
						if(start <= rhl.getReponseNumber())
							copy = true;
						if(end < rhl.getReponseNumber())
							copy = false;
					}
					
					if(copy)
						lines.add(line);
				}
				
				if(0 < lines.size())
				{
					ColoredText text = new ColoredText(parent, SWT.V_SCROLL);
					text.addLines(lines);
					text.setSize(text.computeSize(SWT.DEFAULT, SWT.DEFAULT));
					return;
				}
			}

			Label label = new Label(parent, SWT.NONE);
			label.setText("error: no tooltip for " + href);
		}

	}
	
	private static class ResponseHeaderLine extends ColoredText.Line implements IResponseHeaderLine
	{
		private int responseNumber;

		public ResponseHeaderLine(int indent, int responseNumber)
		{
			super(indent);
			this.responseNumber = responseNumber;
		}
		
		public int getReponseNumber()
		{
			return responseNumber;
		}
	}

}
