package nuts.core.net;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.zip.GZIPInputStream;

import javax.servlet.http.HttpServletRequest;

import nuts.core.intl.chardet.nsDetector;
import nuts.core.io.IOUtils;
import nuts.core.lang.ArrayUtils;
import nuts.core.lang.CharsetUtils;
import nuts.core.lang.StringUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SchemeRegistryFactory;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

public class HttpClientAgent {
	private static Log log = LogFactory.getLog(HttpClientAgent.class);

	public final static int GET = 1;
	public final static int POST = 2;

	public static final int LANG_ALL = nsDetector.ALL;
	public static final int JAPANESE = nsDetector.JAPANESE;
	public static final int CHINESE = nsDetector.CHINESE;
	public static final int SIMPLIFIED_CHINESE = nsDetector.SIMPLIFIED_CHINESE;
	public static final int TRADITIONAL_CHINESE = nsDetector.TRADITIONAL_CHINESE;
	public static final int KOREAN = nsDetector.KOREAN;

	
	private static ClientConnectionManager defaultClientConnectionManager;
	
	private ClientConnectionManager clientConnectionManager;

	private Integer timeout;
	
	private List<Header> requestHeaders;
	
	// Create a local instance of cookie store
	private CookieStore cookieStore = new BasicCookieStore();

	private HttpResponse response;
	private InputStream responseStream;
	private byte[] responseContent;
	private String responseText;
	

	/**
	 * @return the defaultClientConnectionManager
	 */
	public static ClientConnectionManager getDefaultClientConnectionManager() {
		if (defaultClientConnectionManager == null) {
			SchemeRegistry schemeRegistry = SchemeRegistryFactory.createDefault();
			defaultClientConnectionManager = new ThreadSafeClientConnManager(schemeRegistry);
//			defaultClientConnectionManager = new SingleClientConnManager(schemeRegistry);
		}
		return defaultClientConnectionManager;
	}

	/**
	 * @param defaultClientConnectionManager the defaultClientConnectionManager to set
	 */
	public static void setDefaultClientConnectionManager(
			ClientConnectionManager defaultClientConnectionManager) {
		HttpClientAgent.defaultClientConnectionManager = defaultClientConnectionManager;
	}

	public HttpClientAgent() {
		this(getDefaultClientConnectionManager());
	}
	
	public HttpClientAgent(ClientConnectionManager clientConnectionManager) {
		this.clientConnectionManager = clientConnectionManager;
		requestHeaders = new ArrayList<Header>();
		resetRequestHeaders();
	}

	public void resetRequestHeaders() {
		requestHeaders.clear();
		addRequestHeader(
				HttpHeaderDefine.USER_AGENT,
				"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1");
		addRequestHeader(HttpHeaderDefine.ACCEPT,
				"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
		addRequestHeader(HttpHeaderDefine.ACCEPT_LANGUAGE, "en,ja,zh;q=0.7,en;q=0.3");
		addRequestHeader(HttpHeaderDefine.ACCEPT_ENCODING, "gzip,deflate");
		addRequestHeader(HttpHeaderDefine.ACCEPT_CHARSET, "UTF-8,Shift_JIS,GB2312;q=0.7,*;q=0.7");
	}
	
	/**
	 * @return the clientConnectionManager
	 */
	public ClientConnectionManager getClientConnectionManager() {
		return clientConnectionManager;
	}

	/**
	 * @param clientConnectionManager the clientConnectionManager to set
	 */
	public void setClientConnectionManager(ClientConnectionManager clientConnectionManager) {
		this.clientConnectionManager = clientConnectionManager;
	}

	/**
	 * @return the timeout
	 */
	public Integer getTimeout() {
		return timeout;
	}

	/**
	 * @param timeout the timeout to set
	 */
	public void setTimeout(Integer timeout) {
		this.timeout = timeout;
	}

	/**
	 * clear the headers
	 */
	public void clearRequestHeaders() {
		requestHeaders.clear();
	}

	/**
	 * @param name the header name
	 * @param value the header value
	 */
	public void addRequestHeader(String name, String value) {
		this.requestHeaders.add(new BasicHeader(name, value));
	}
	
	/**
	 * remove the specified header
	 * @param name the header name
	 */
	public void removeRequestHeader(String name) {
		for (int i = requestHeaders.size() - 1; i >= 0; i--) {
			if (requestHeaders.get(i).getName().equals(name)) {
				requestHeaders.remove(i);
			}
		}
	}
	
	/**
	 * add or replace the header
	 * @param name the header name
	 * @param value the header value
	 */
	public void setRequestHeader(String name, String value) {
		removeRequestHeader(name);
		addRequestHeader(name, value);
	}

	/**
	 * add servlet request headers
	 * @param request request
	 * @param excepts except header keys
	 */
	public void addRequestHeaders(HttpServletRequest request, List<String> excepts) {
		Enumeration en = request.getHeaderNames();
		while (en.hasMoreElements()) {
			String name = en.nextElement().toString();
			if (excepts != null && excepts.contains(name)) {
				continue;
			}

			Enumeration ev = request.getHeaders(name);
			while (ev.hasMoreElements()) {
				addRequestHeader(name, ev.nextElement().toString());
			}
		}
	}
	
	/**
	 * add servlet request headers
	 * @param request request
	 */
	public void addRequestHeaders(HttpServletRequest request) {
		addRequestHeaders(request, null);
	}
	
	/**
	 * @return the cookieStore
	 */
	public CookieStore getCookieStore() {
		return cookieStore;
	}

	/**
	 * @param cookieStore the cookieStore to set
	 */
	public void setCookieStore(CookieStore cookieStore) {
		this.cookieStore = cookieStore;
	}

	/**
	 * @return the responseEntity
	 */
	public Header[] getResponseHeaders() {
		return response.getAllHeaders();
	}

	/**
	 * @return the responseEntity
	 */
	public HttpEntity getResponseEntity() {
		return response.getEntity();
	}

	/**
	 * @return the responseStatus
	 */
	public StatusLine getResponseStatus() {
		return response.getStatusLine();
	}

	/**
	 * @return the responseStatusCode
	 */
	public int getResonseStatusCode() {
		return getResponseStatus().getStatusCode();
	}
	
	/**
	 * @return the responseStream
	 * @throws IOException if an io error occurs
	 */
	public InputStream getResponseRawStream() throws IOException {
		if (responseStream == null) {
			responseStream = getResponseEntity().getContent();
		}
		return responseStream;
	}
	
	/**
	 * @return the responseStream
	 * @throws IOException if an io error occurs
	 */
	public InputStream getResponseStream() throws IOException {
		if (responseContent != null) {
			responseStream = new ByteArrayInputStream(responseContent);
		}
		else if (responseStream == null) {
			responseStream = getResponseEntity().getContent();
			if (responseStream != null) {
				Header ce = getResponseEntity().getContentEncoding();
				if (ce != null && "gzip".equalsIgnoreCase(ce.getValue())) {
					responseStream = new GZIPInputStream(responseStream);
				}
			}
		}
		return responseStream;
	}

	/**
	 * @return the responseContent
	 * @throws IOException if an io error occurs
	 */
	public byte[] getResponseContent() throws IOException {
		if (responseContent == null) {
			InputStream is = getResponseStream();
			if (is == null) {
				responseContent = new byte[0];
			}
			else {
				try {
					ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
					IOUtils.copy(is, baos);
					responseContent = baos.toByteArray();
				}
				finally {
					IOUtils.closeQuietly(is);
				}
			}
		}		
		return responseContent;
	}

	/**
	 * @return the responseContentLength
	 */
	public long getResponseContentLength() {
		return response.getEntity().getContentLength();
	}
	
	/**
	 * @return the responseCharset
	 */
	public String getResponseCharset() {
		return EntityUtils.getContentCharSet(getResponseEntity());
	}
	
	/**
	 * @return the responseText
	 * @throws IOException if an io error occurs
	 */
	public String getResponseText() throws IOException {
		return getResponseText(null, LANG_ALL);
	}
	
	/**
	 * @param defaultCharset default charset
	 * @return the responseText
	 * @throws IOException if an io error occurs
	 */
	public String getResponseText(String defaultCharset) throws IOException {
		return getResponseText(defaultCharset, LANG_ALL);
	}

	/**
	 * @param lang lang
	 * @return the responseText
	 * @throws IOException if an io error occurs
	 */
	public String getResponseText(int lang) throws IOException {
		return getResponseText(null, lang);
	}
	
	/**
	 * @param defaultCharset default charset
	 * @param lang lang
	 * @return the responseText
	 * @throws IOException if an io error occurs
	 */
	public String getResponseText(String defaultCharset, int lang) throws IOException {
		if (responseText == null) {
			byte[] rc = getResponseContent();
			
			String charset = getResponseCharset();
			if (!CharsetUtils.isSupportedCharset(charset)) {
				if (CharsetUtils.isSupportedCharset(defaultCharset)) {
					charset = defaultCharset;
				}
				else {
					String[] cs = CharsetUtils.detectCharset(rc, lang);
					if (ArrayUtils.isNotEmpty(cs)) {
						charset = cs[0];
					}
				}
			}
			
			responseText = new String(rc, charset);
		}
		return responseText;
	}
	
	public void doGet(String uri) throws IOException {
		doRequest(uri, GET, null);
	}
	
	public void doGet(String uri, Map<String, Object> params) throws IOException {
		doRequest(uri, GET, params);
	}
	
	public void doPost(String uri) throws IOException {
		doRequest(uri, POST, null);
	}
	
	public void doPost(String uri, Map<String, Object> forms) throws IOException {
		doRequest(uri, POST, forms);
	}
	
	public void doRequest(String uri, String method, Map<String, Object> forms) throws IOException {
		if ("get".equalsIgnoreCase(method)) {
			doRequest(uri, GET, forms);
		}
		else if ("post".equalsIgnoreCase(method)) {
			doRequest(uri, POST, forms);
		}
	}
	
	private String getMethod(int method) {
		switch (method) {
		case GET:
			return "GET";
		case POST:
			return "POST";
		default:
			return "";
		}
	}

	public void doRequest(String uri, int method, Map<String, Object> forms) throws IOException {
		response = null;
		responseStream = null;
		responseContent = null;
		responseText = null;
		
		HttpClient httpclient = new DefaultHttpClient(clientConnectionManager, null);
		if (timeout != null) {
			httpclient.getParams().setParameter("http.socket.timeout", timeout);
		}

		// Create local HTTP context
		HttpContext localContext = new BasicHttpContext();

		// Bind custom cookie store to the local context
		localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);

		List<NameValuePair> params = null;
		if (forms != null) {
			params = new ArrayList<NameValuePair>();
			for (Entry<String, Object> e : forms.entrySet()) {
				String k = e.getKey();
				Object v = e.getValue();
				if (v == null) {
					continue;
				}
				if (v.getClass().isArray()) {
					for (Object o : (Object[])v) {
						params.add(new BasicNameValuePair(k, o.toString()));
					}
				}
				else if (v instanceof Collection) {
					for (Object o : (Collection)v) {
						params.add(new BasicNameValuePair(k, o.toString()));
					}
				}
				else {
					params.add(new BasicNameValuePair(k, v.toString()));
				}
			}
		}

		HttpRequestBase httpreq = null;
		
		switch (method) {
		case GET:
			if (params != null) {
				httpreq = new HttpGet(uri + "?" + URLEncodedUtils.format(params, CharsetUtils.UTF_8));
			}
			else {
				httpreq = new HttpGet(uri);
			}
			break;
		case POST:
			httpreq = new HttpPost(uri); 
			((HttpPost)httpreq).setEntity(new UrlEncodedFormEntity(params, CharsetUtils.UTF_8));
			break;
		}

		if (requestHeaders != null) {
			for (Header header : requestHeaders) {
				httpreq.addHeader(header);
			}
		}

		final String CLRF = StringUtils.LINE_SEPARATOR;
		if (log.isDebugEnabled()) {
			StringBuilder msg = new StringBuilder();

			msg.append(getMethod(method)).append(": ").append(uri);
			if (params != null && method == GET) {
				msg.append("?").append(URLEncodedUtils.format(params, CharsetUtils.UTF_8));
			}
			msg.append(CLRF);

			if (requestHeaders != null) {
				for (Header h : requestHeaders) {
					msg.append(h.getName()).append(": ").append(h.getValue()).append(CLRF);
				}
			}
			msg.append(CLRF);
			
			if (params != null) {
				for (NameValuePair p : params) {
					msg.append(p.getName()).append("=").append(p.getValue()).append(CLRF);
				}
			}
			log.debug(msg.toString());
		}
		
		long st = System.currentTimeMillis();
		response = httpclient.execute(httpreq, localContext);
		long et = System.currentTimeMillis();

		
		if (log.isDebugEnabled()) {
			StringBuilder msg = new StringBuilder();
			msg.append(uri)
				.append(" - ").append(response.getStatusLine())
				.append(" ").append(getResponseEntity().getContentLength()).append("B")
				.append(" ").append(et - st).append("ms");
			
			if (log.isTraceEnabled()) {
				msg.append(CLRF); 

				Header[] ah = response.getAllHeaders();
				if (ah.length > 0) {
					msg.append(StringUtils.center(" Response Header ", 78, '=')).append(CLRF);
					for (Header h : ah) {
						msg.append(h).append(CLRF);
					}
				}
				
				msg.append(StringUtils.center(" Response Content (" + getResponseContent().length + ")", 78, '='))
					.append(CLRF)
					.append(getResponseText());
			}
			
			log.debug(msg);
		}
	}
	
	public static void main(String[] args) {
		try {
			HttpClientAgent hca = new HttpClientAgent();
			
			System.out.print(">");
			
			Scanner stdin = new Scanner(System.in);

			String line;
			while ((line = stdin.nextLine()) != null) {
				line = StringUtils.strip(line);
				if ("exit".equals(line)) {
					break;
				}
				else {
					String[] ss = StringUtils.split(line);
					if (ss.length == 1) {
						hca.doGet(ss[0]);
					}
					else if (ss.length == 2) {
						String method = ss[0];
						String url = ss[1];
						hca.doRequest(url, method, null);
					}
				}
				System.out.print(">");
			}
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}
}

