using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections;
using System.Threading;

namespace Danga {

    // get a file name from the client
    // open the file and send the
    // contents from the server to the client
    public class MonoBal
    {  
	int inChunks = 0;   // chunks in queues from clients to proxies (POSTs)
	int outChunks = 0;  // chunks in queues from proxies to clients (responses)

	struct Chunk {
	    public int    Size;
	    public byte[] Buf;

	    public Chunk (int size, byte[] buf) {
		Size = size;
		Buf = buf;
	    }
	}

	class ClientHandler
	{
	    const int       BufferSize = 4096;
	    byte[]          buffer;

	    Socket          cSock;
	    UsefulStream    cUS;    // for reading headers/POST data
	    NetworkStream   cNS;    // stream for writing back responses
	    
	    Encoding        encISO;  // 8bit preserving, HTTP standard

	    Queue           inQ;
	    Queue           outQ;

	    // Client State
	    private enum ClientState {
		ReadingHeaders,
		ReadingRest,
		Writing,
	    };
	    private const int MaxHeaders = 4096;
	    private bool headersRead = false;
	    string clientMethod, clientURI, clientHTTPVer;
	    string clientRequest = "";

	    private int  clientRead = 0;  // bytes read from client, zeroed at each reading stage
	    private AsyncCallback   clientReadCallback;
	    private byte[]  requestBytes;

	    // Proxy State
	    NetworkStream pNS;
	    Socket pSock;

	    // constructor
	    public ClientHandler(Socket socketForClient)
	    {
		cSock = socketForClient;
		cUS = new UsefulStream(socketForClient); // for reading
		cNS = new NetworkStream(cSock);
		
		encISO = Encoding.GetEncoding("ISO-8859-1");
		inQ = Queue.Synchronized(new Queue());
		outQ = Queue.Synchronized(new Queue());
	    }

	    public void StartRead ()
	    {
		cUS.BeginGetLine(new AsyncCallback(this.OnGetRequestLine), 0, null);
	    }

	    // gets the first line of an HTTP request.
	    // doesn't allow proxy requests.  just /abs_path or *.  HTTP version is optional and
	    // otherwise assumes old 0.9-style
	    private void OnGetRequestLine (IAsyncResult ar) {
		string s = cUS.EndGetLine();
		Regex reVerb = new Regex(@"^(?<method>\w+) (?<uri>(?:\*|(?:/\S*?)))(?: HTTP/(?<ver>\d+\.\d+))?\r?\n?$");
		Match m = reVerb.Match(s);
		if (! m.Success) {
		    this.Close();
		    return;
		}
		clientRequest = s;

		clientMethod = m.Groups["method"].ToString();
		clientURI = m.Groups["uri"].ToString();
		clientHTTPVer = m.Groups["ver"].ToString();

		cUS.BeginGetLine(new AsyncCallback(this.OnGetHeader), 0, null);
	    }

	    private void OnGetHeader (IAsyncResult ar) {
		String s = cUS.EndGetLine();
		if (s == null) { Close(); return; }

		// temporary
		clientRequest = clientRequest + s;
		if (clientRequest.Length > 4096) { Close(); return; }

		String ds = s.Replace("\r\n", "").Replace("\n", "");
		//Console.WriteLine("Header({0} bytes): [{1}]", s.Length, ds);
		if (s == "\n" || s == "\r\n") {
		    headersRead = true;
		    ProxyConnect();
		    return;
		}
		cUS.BeginGetLine(new AsyncCallback(this.OnGetHeader), 0, null);
	    }


	    private void ProxyConnect () {
		IPAddress kenny = IPAddress.Parse("10.1.0.2");
		IPEndPoint pxEP = new IPEndPoint(kenny, 80);
		pSock = new Socket(pxEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
		pSock.BeginConnect(pxEP,
				   new AsyncCallback(ProxyConnected), pSock);
	    }

	    private void ProxyConnected ( IAsyncResult ar ) {
		if (! pSock.Connected) {
		    Console.WriteLine("Proxy connect errro!");
		    Close();
		    return;
		}
		pNS = new NetworkStream(pSock);
	   
		byte[] req = encISO.GetBytes(clientRequest);
		ProxyWrite(req);
	    }

	    byte[] proxyWriteBuf;
	    byte[] proxyReadBuf;
	    private void onProxyWrite (IAsyncResult ar) {
		pNS.EndWrite(ar);
		proxyWriteBuf = null;
		//Console.WriteLine("Done proxy writing!");
		// FIXME: haven't pushed POST/PUT response yet (just headers)
		// but we're just testing GET now, so time to read from proxy...
		ProxyReadMore();
	    }

	    private void ProxyReadMore () {
		proxyReadBuf = new byte[4096];
		pNS.BeginRead(proxyReadBuf, 0, 4096, new AsyncCallback(this.onProxyRead), null);
	    }

	    private void onProxyRead (IAsyncResult ar) {
		int bytesRead = pNS.EndRead(ar);
		//Console.WriteLine("proxy read = {0}", bytesRead);
		if (bytesRead == 0) {
		    pNS.Close();
		    if (pSock.Connected) pSock.Close();
 		    pSock = null;
		    pNS = null;

		    // signal the end of the stream with a 0/null chunk
		    outQ.Enqueue(new Chunk(0, null));
		} else {
		    outQ.Enqueue(new Chunk(bytesRead, proxyReadBuf));
		}
		ClientPush();

		if (bytesRead > 0) {  // TODO: and not too much in the queue
		    ProxyReadMore();
		}
	    }

	    private void ProxyWrite (byte[] buf) {
		if (buf != null) {
		    if (buf != proxyWriteBuf) {
			proxyWriteBuf = buf;
		    }
		    pNS.BeginWrite(proxyWriteBuf,
				   0, proxyWriteBuf.Length,
				   new AsyncCallback(this.onProxyWrite), null);
		}

	    }
	    
	    int clientWriting = 0;
	    private void ClientPush () {
		if (Interlocked.CompareExchange(ref clientWriting, 1, 0) > 0)
		    return;

		//Console.WriteLine("clientWriting!");

		if (outQ.Count == 0) {
		    clientWriting = 0;
		    //Console.WriteLine("outQ empty");
		    return;
		}

		Chunk c = (Chunk) outQ.Dequeue();
		//Console.WriteLine("client push = {0}", c.Size);

		if (c.Size != 0) {
		    try {
			cNS.BeginWrite(c.Buf, 0, c.Size, 
				       new AsyncCallback(this.onClientWrite), null);
		    } catch {
			Console.WriteLine("exception after cNS.BeginWrite");
			Close();
			return;
		    }
		} else {
		    // done!
		    //Console.WriteLine("Done!");
		    Close();
		    return;
		}
	    }

	    private void onClientWrite (IAsyncResult ar) {
		try {
		    cNS.EndWrite(ar);
		} catch {
		    Console.WriteLine("Client died or something");
		    Close();
		    return;
		}
		clientWriting = 0;
		//Console.WriteLine("did client write");
		ClientPush();
	    }

	    private void Close () {
		try {
		    cNS.Close();
		    cNS = null;
		    cUS.Close( );
		    cUS = null;
		} catch {
		    Console.Write("exception closing socket?");
		}

		if (pSock != null && pSock.Connected) pSock.Close();
		pSock = null;
	    }
	}


	private void Run( )
	{
	    IPAddress localAddr = IPAddress.Parse("127.0.0.1");
	    TcpListener tcpListener = new TcpListener(localAddr, 65000);
	    tcpListener.Start( );       

	    for (;;) {
		Socket socketForClient = 
		    tcpListener.AcceptSocket();
		if (socketForClient.Connected)
		    {
			//Console.WriteLine("Client connected");
			ClientHandler handler = 
			    new ClientHandler(socketForClient);
			handler.StartRead( );
		    }
	    }
	}

	public static void Main ()
	{
	    MonoBal app = new MonoBal();
	    app.Run();
	}

    } // public class MonoBal

} // namespace Danga


       
