package com.planet_ink.coffee_mud.Common;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.collections.*;
import com.planet_ink.coffee_mud.core.exceptions.CMException;
import com.planet_ink.coffee_mud.core.exceptions.CoffeeMudException;
import com.planet_ink.coffee_mud.Abilities.interfaces.*;
import com.planet_ink.coffee_mud.Areas.interfaces.*;
import com.planet_ink.coffee_mud.Behaviors.interfaces.*;
import com.planet_ink.coffee_mud.CharClasses.interfaces.*;
import com.planet_ink.coffee_mud.Commands.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.*;
import com.planet_ink.coffee_mud.Exits.interfaces.*;
import com.planet_ink.coffee_mud.Items.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Locales.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.*;
import com.planet_ink.coffee_mud.Races.interfaces.*;

import java.net.*;
import java.io.*;
import java.util.*;

/*
   Copyright 2013-2020 Bo Zimmerman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

	   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

public class DefaultHttpClient implements HttpClient, Cloneable
{
	@Override
	public String ID()
	{
		return "DefaultHttpClient";
	}

	@Override
	public String name()
	{
		return ID();
	}

	private static enum HState
	{
		PREHEAD,
		INHEAD,
		INBODY,
		PRECHUNK,
		INCHUNK,
		POSTINCHUNK,
		POSTCHUNK
	}

	private volatile int				tickStatus		= Tickable.STATUS_NOT;
	protected Map<String, String>		reqHeaders		= new CaselessTreeMap<String>();
	protected Map<String, List<String>>	respHeaders		= new CaselessTreeMap<List<String>>();
	protected Socket					sock			= null;
	protected OutputStream				out				= null;
	protected InputStream				in				= null;
	protected Method					meth			= Method.GET;
	protected int						connectTimeout	= 10000;
	protected int						readTimeout		= 10000;
	protected int						maxReadBytes	= 0;
	protected byte[]					outBody			= null;
	protected Integer					respStatus		= null;

	@Override
	public CMObject newInstance()
	{
		try
		{
			return getClass().newInstance();
		}
		catch(final Exception e)
		{
			return new DefaultHttpClient();
		}
	}

	@Override
	public void initializeClass()
	{
	}

	@Override
	public HttpClient header(final String key, final String value)
	{
		reqHeaders.put(key, value);
		return this;
	}

	@Override
	public HttpClient method(final Method meth)
	{
		if(meth!=null)
		{
			this.meth=meth;
		}
		return this;
	}

	@Override
	public HttpClient body(final String body)
	{
		if(body!=null)
		{
			this.outBody=body.getBytes();
		}
		return this;
	}

	@Override
	public HttpClient body(final byte[] body)
	{
		if(body!=null)
		{
			this.outBody=body;
		}
		return this;
	}

	@Override
	public HttpClient reset()
	{
		reqHeaders.clear();
		respHeaders.clear();
		respStatus=null;
		return this;
	}

	@Override
	public HttpClient connectTimeout(final int ms)
	{
		this.connectTimeout=ms;
		return this;
	}

	@Override
	public HttpClient readTimeout(final int ms)
	{
		this.readTimeout=ms;
		return this;
	}

	@Override
	public HttpClient maxReadBytes(final int bytes)
	{
		this.maxReadBytes=bytes;
		return this;
	}

	protected void conditionalHeader(final String key, final String value, final List<String> clearSet)
	{
		if(!reqHeaders.containsKey(key))
		{
			reqHeaders.put(key, value);
			clearSet.add(key);
		}
	}

	@Override
	public int getResponseCode()
	{
		if(this.respStatus!=null)
			return this.respStatus.intValue();
		return -1;
	}

	@Override
	public Map<String,List<String>> getResponseHeaders()
	{
		return this.respHeaders;
	}

	@Override
	public HttpClient doRequest(String url) throws IOException
	{
		respHeaders.clear();
		respStatus=null;
		outBody=null;
		if(url == null)
			throw new IOException("Bad url");
		final boolean ssl=url.toLowerCase().startsWith("https");
		if(ssl)
			throw new IOException("Unsupported: ssl");
		int x=url.indexOf("://");
		if(x>=0)
			url=url.substring(x+3);
		String host;
		String rest="/";
		x=url.indexOf('/');
		if(x<0)
			x=url.indexOf('?');
		if(x>=0)
		{
			host=url.substring(0,x);
			rest=url.substring(x);
		}
		else
			host=url;
		int port=ssl?443:80;
		x=host.indexOf(':');
		if(x>=0)
		{
			port=CMath.s_int(host.substring(x+1),80);
			host=host.substring(0,x);
		}
		final List<String> onesToClear=new Vector<String>();
		conditionalHeader("Host",host,onesToClear);
		conditionalHeader("Connection","Keep-Alive",onesToClear);
		conditionalHeader("Accept","*/*",onesToClear);
		final int len=(outBody!=null)?outBody.length:0;
		conditionalHeader("Content-Length",""+len,onesToClear);
		if(sock == null)
		{
			sock=new Socket();
			sock.connect(new InetSocketAddress(host,port), this.connectTimeout);
			in=sock.getInputStream();
			sock.setSoTimeout(10);
			out=sock.getOutputStream();
		}
		final IOException cleanException=new IOException("Connection closed by remote host");
		try
		{
			while (in.read() != -1)
			{
			} /* clear the stream */
			throw cleanException;
		}
		catch(final IOException e)
		{
			if(e==cleanException)
				throw e;
		}
		out.write((meth.toString()+" "+rest+" HTTP/1.1\r\n").getBytes());
		for(final String key : reqHeaders.keySet())
			out.write((key+": "+reqHeaders.get(key)+"\r\n").getBytes());
		for(final String key : onesToClear)
			reqHeaders.remove(key);
		out.write("\r\n".getBytes());
		if(outBody!=null)
			out.write(outBody);
		long nextReadTimeout=(this.readTimeout>0)?(System.currentTimeMillis()+this.readTimeout):Long.MAX_VALUE;
		int lastC=-1;

		HState state=HState.PREHEAD;
		final ByteArrayOutputStream bodyBuilder=new ByteArrayOutputStream();
		final StringBuilder headBuilder=new StringBuilder();
		int c=0;
		int maxBytes=this.maxReadBytes;
		int chunkSize=0;
		while(c!=-1)
		{
			try
			{
				lastC=c;
				c=in.read();
				if(this.readTimeout>0)
					nextReadTimeout=(System.currentTimeMillis()+this.readTimeout);
			}
			catch(final IOException e)
			{
				if(e instanceof java.net.SocketTimeoutException)
				{
					if(System.currentTimeMillis()>nextReadTimeout)
						throw e;
					continue;
				}
				else
					throw e;
			}
			switch(state)
			{
			case PREHEAD: {
				if((c=='\n')&&(lastC=='\r'))
				{
					state=HState.INHEAD;
					final String s=headBuilder.toString();
					headBuilder.setLength(0);
					final String[] parts=s.split(" ", 3);
					if(CMath.isInteger(parts[1]))
						respStatus=Integer.valueOf(CMath.s_int(parts[1]));
					else
						respStatus=Integer.valueOf(-1);
				}
				else
				if((c!='\n')&&(c!='\r'))
					headBuilder.append((char)c);
				break;
				}
			case INHEAD: {
				if((c=='\n')&&(lastC=='\r'))
				{
					if(headBuilder.length()==0)
					{
						if(respHeaders.containsKey("Transfer-Encoding")
						&&(respHeaders.get("Transfer-Encoding").contains("chunked")
							||respHeaders.get("Transfer-Encoding").contains("Chunked")
							||respHeaders.get("Transfer-Encoding").contains("CHUNKED")))
						{
							maxBytes=Integer.MAX_VALUE;
							state=HState.PRECHUNK;
						}
						else
						if(respHeaders.containsKey("Content-Length"))
						{
							final List<String> l=respHeaders.get("Content-Length");
							for(final String s : l)
							{
								if(CMath.isInteger(s))
								{
									final int possMax=CMath.s_int(s);
									if((maxBytes==0)||(possMax<maxBytes))
										maxBytes=possMax;
								}
							}
							state=HState.INBODY;
						}
						else
						{
							c=-1;
							break;
						}
					}
					else
					{
						final String s=headBuilder.toString();
						x=s.indexOf(':');
						if(x>0)
						{
							final String key=s.substring(0,x).trim();
							final String value=s.substring(x+1).trim();
							List<String> list;
							if(respHeaders.containsKey(key))
								list=respHeaders.get(key);
							else
							{
								list=new ArrayList<String>();
								respHeaders.put(key, list);
							}
							list.add(value);
						}
					}
					headBuilder.setLength(0);
				}
				else
				if((c!='\r')&&(c!='\n'))
					headBuilder.append((char)c);
				break;
				}
			case INBODY: {
					bodyBuilder.write(c);
					if((maxBytes==0)||(bodyBuilder.size()>=maxBytes))
						c=-1;
					break;
				}
			case PRECHUNK: {
					if((c=='\n')&&(lastC=='\r'))
					{
						state=HState.INCHUNK;
						String szStr=headBuilder.toString().trim();
						x=szStr.indexOf(';');
						if(x>=0)
							szStr=szStr.substring(0,x).trim();
						x=0;
						while((x<szStr.length())&&(szStr.charAt(x)=='0'))
							x++;
						if(x<szStr.length())
						{
							chunkSize = Integer.parseInt(szStr.substring(x).trim(),16);
							if(chunkSize==0)
								state=HState.POSTCHUNK;
						}
						else
							state=HState.POSTCHUNK;
						headBuilder.setLength(0);
					}
					else
					if((c!='\r')&&(c!='\n'))
					{
						headBuilder.append((char)c);
					}
					break;
				}
			case INCHUNK: {
				bodyBuilder.write(c);
				if((--chunkSize)<=0)
					state=HState.POSTINCHUNK;
				break;
			}
			case POSTINCHUNK: {
				if((c=='\n')&&(lastC=='\r'))
					state=HState.PRECHUNK;
				break;
			}
			case POSTCHUNK: {
				if((c=='\n')&&(lastC=='\r'))
				{
					if(headBuilder.length()==0)
						c=-1;
					else
					{
						final String s=headBuilder.toString();
						x=s.indexOf(':');
						if(x>0)
						{
							final String key=s.substring(0,x).trim();
							final String value=s.substring(x+1).trim();
							List<String> list;
							if(respHeaders.containsKey(key))
								list=respHeaders.get(key);
							else
							{
								list=new ArrayList<String>();
								respHeaders.put(key, list);
							}
							list.add(value);
						}
					}
					headBuilder.setLength(0);
				}
				else
				if((c!='\r')&&(c!='\n'))
					headBuilder.append((char)c);
				break;
			}
			}

		}
		this.outBody=bodyBuilder.toByteArray();
		return this;
	}

	@Override
	public byte[] getRawUrl(final String urlStr, final String cookieStr)
	{
		return getRawUrl(urlStr, cookieStr, 1024*1024*10, 10000);
	}

	@Override
	public byte[] getRawUrl(final String urlStr)
	{
		return getRawUrl(urlStr, null, 1024*1024*10, 10000);
	}

	@Override
	public byte[] getRawUrl(final String urlStr, final int maxLength, final int readTimeout)
	{
		return getRawUrl(urlStr, null, maxLength, readTimeout);
	}

	@Override
	public int getResponseContentLength()
	{
		if(this.outBody!=null)
			return this.outBody.length;
		return 0;
	}

	@Override
	public InputStream getResponseBody()
	{
		if(this.outBody!=null)
			return new ByteArrayInputStream(this.outBody);
		return new ByteArrayInputStream(new byte[0]);

	}

	@Override
	public HttpClient doGet(final String url) throws IOException
	{
		return this.method(Method.GET).doRequest(url);
	}

	@Override
	public HttpClient doHead(final String url) throws IOException
	{
		return this.method(Method.HEAD).doRequest(url);
	}

	@Override
	public byte[] getRawUrl(final String urlStr, final String cookieStr, final int maxLength, final int readTimeout)
	{
		HttpClient h=null;
		try
		{
			h=this.readTimeout(readTimeout).connectTimeout(readTimeout).method(Method.GET);
			if((cookieStr!=null)&&(cookieStr.length()>0))
				h=h.header("Cookie", cookieStr);
			h.doRequest(urlStr);
			if (h.getResponseCode() == 302)
			{
				final InputStream in=h.getResponseBody();
				int len=h.getResponseContentLength();
				if((len > 0)&&((maxLength==0)||(len<=maxLength)))
				{
					final byte[] buffer = new byte[1024];
					final ByteArrayOutputStream bout=new ByteArrayOutputStream();
					while ((len = in.read(buffer)) != -1)
					{
						bout.write(buffer, 0, len);
					}
					return bout.toByteArray();
				}
			}

			if (h.getResponseCode() == 200)
			{
				final InputStream in=h.getResponseBody();
				int len=h.getResponseContentLength();
				if((len > 0)&&((maxLength==0)||(len<=maxLength)))
				{
					final byte[] buffer = new byte[1024];
					final ByteArrayOutputStream bout=new ByteArrayOutputStream();
					while ((len = in.read(buffer)) != -1)
					{
						bout.write(buffer, 0, len);
					}
					return bout.toByteArray();
				}
			}
		}
		catch (final Exception e)
		{
			if(e.getMessage()==null)
				Log.errOut("HttpClient",e);
			else
				Log.errOut("HttpClient: "+e.getMessage()+"("+urlStr+")");
			return null;
		}
		finally
		{
			if(h!=null)
				h.finished();
		}
		return null;
	}

	@Override
	public void finished()
	{
		if(sock!=null)
		{
			try
			{
				sock.shutdownInput();
				sock.shutdownOutput();
				sock.close();
			}
			catch(final Exception e)
			{
			}
			finally
			{
				sock=null;
				in=null;
				out=null;
			}
		}
	}

	@Override
	public Map<String,List<String>> getHeaders(final String urlStr)
	{
		HttpClient h=null;
		try
		{
			h=this.readTimeout(3000).connectTimeout(3000).method(Method.GET);
			h.doRequest(urlStr);
			return h.getResponseHeaders();
		}
		catch (final Exception e)
		{
			if(e.getMessage()==null)
				Log.errOut("HttpClient",e);
			else
				Log.errOut("HttpClient: "+e.getMessage()+"("+urlStr+")");
			return null;
		}
		finally
		{
			if(h!=null)
				h.finished();
		}
	}

	@Override
	public int getTickStatus()
	{
		return tickStatus;
	}

	@Override
	public boolean tick(final Tickable ticking, final int tickID)
	{
		return false;
	}

	@Override
	public CMObject copyOf()
	{
		try
		{
			return (CMObject)this.clone();
		}
		catch (final CloneNotSupportedException e)
		{
			return this;
		}
	}

	@Override
	public int compareTo(final CMObject o)
	{
		final int comp=CMClass.classID(this).compareToIgnoreCase(CMClass.classID(o));
		return (comp==0)?((this==o)?0:1):comp;
	}
}
