/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.resource.adapter.google.dataprotocol;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.resource.adapter.google.GDataAPI;
import org.teiid.resource.adapter.google.auth.AuthHeaderFactory;
import org.teiid.translator.goole.api.SpreadsheetAuthException;
import org.teiid.translator.goole.api.SpreadsheetOperationException;
import org.teiid.translator.goole.api.metadata.Column;
import org.teiid.translator.goole.api.metadata.SpreadsheetColumnType;
import org.teiid.translator.goole.api.result.PartialResultExecutor;
import org.teiid.translator.goole.api.result.RowsResult;
import org.teiid.translator.goole.api.result.SheetRow;

/**
 * 
 * This class is used to make requests to Google Visualization Data Protocol. The most important
 * method is executeQuery.
 * 
 * @author fnguyen
 *
 */
public class GoogleDataProtocolAPI {
	private GDataAPI spreadsheetBrowser = null;
	private AuthHeaderFactory headerFactory = null;
	public static String ENCODING = "UTF-8"; //$NON-NLS-1$
	private GoogleJSONParser parser = new GoogleJSONParser();
	
	public AuthHeaderFactory getHeaderFactory() {
		return headerFactory;
	}

	public void setHeaderFactory(AuthHeaderFactory headerFactory) {
		this.headerFactory = headerFactory;
	}

	public void setSpreadSheetBrowser( GDataAPI spreadsheetBrowser ){
		this.spreadsheetBrowser=spreadsheetBrowser;
	}
		
	/**
	 * Most important method that will issue query [1] to specific worksheet. The columns in the query
	 * should be identified by their real alphabetic name (A, B, C...). 
	 * 
	 * There is one important restriction to query. It should not contain offset and limit clauses.
	 * To achieve functionality of offset and limit please use corresponding parameters in this method.
	 * 
	 * 
	 * [1] https://developers.google.com/chart/interactive/docs/querylanguage
	 * 
	 * @param query The query defined in [1]
	 * @param batchSize How big portions of data should be returned by one roundtrip to Google.
	 * @return Iterable RowsResult that will actually perform the roundtrips to Google for data 
	 */
	public RowsResult executeQuery(String spreadsheetTitle, String worksheetTitle,
			String query, int batchSize, Integer offset, Integer limit) {
	
		String key = spreadsheetBrowser.getSpreadsheetKeyByTitle(spreadsheetTitle);
		
		RowsResult result = new RowsResult(new DataProtocolQueryStrategy(key,worksheetTitle,query), batchSize);
		if (offset!= null)
			result.setOffset(offset);
		if (limit != null)
			result.setLimit(limit);
		
		return result;
	}
	
	public List<Column> getMetadata(String spreadsheetTitle, String worksheetTitle) {
		String key = spreadsheetBrowser.getSpreadsheetKeyByTitle(spreadsheetTitle);
		DataProtocolQueryStrategy dpqs = new DataProtocolQueryStrategy(key,worksheetTitle,"SELECT *"); //$NON-NLS-1$
		dpqs.getResultsBatch(0, 1);
		return dpqs.getMetadata();
	}
	
	/**
	 * Logic to query portion of data from Google Visualization Data Protocol. We do not use any special library just simple
	 * Http request. Google sends response back in CSV that we parse afterwards.
	 * 
	 * @author fnguyen
	 *
	 */
	public class DataProtocolQueryStrategy implements PartialResultExecutor {
		private String spreadsheetKey;
		private String worksheetName;
		private String urlEncodedQuery;
		private List<Column> metadata;
		
		public DataProtocolQueryStrategy(String key, String worksheetKey,
				String query) {
			super();
			this.spreadsheetKey = key;
			this.worksheetName = worksheetKey;
			try {
				this.urlEncodedQuery = URLEncoder.encode(query, ENCODING); 
			} catch (UnsupportedEncodingException e) {
				throw new SpreadsheetOperationException(e);
			}
		}

		public List<Column> getMetadata() {
			return metadata;
		}

		public List<SheetRow> getResultsBatch(int startIndex, int amount) {
			String boundariedQuery =null;
			String worksheet = null;
			try {
				boundariedQuery = getQueryWithBoundaries(amount, Math.max(0,(startIndex)));
				worksheet = URLEncoder.encode(worksheetName, ENCODING);
			} catch (UnsupportedEncodingException e) {
				throw new SpreadsheetOperationException(e);
			}
			HttpGet get = new HttpGet("https://spreadsheets.google.com/tq?key="+spreadsheetKey+"&sheet="+worksheet+"&tqx=responseHandler:x;out:json&tq="+boundariedQuery);  //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			get.setHeader("GData-Version", "3.0");  //$NON-NLS-1$ //$NON-NLS-2$
			get.setHeader("Authorization", headerFactory.getAuthHeader()); //$NON-NLS-1$
			
			try {
				DefaultHttpClient client = new DefaultHttpClient();
				try {
					return executeAndParse(client, get);
				} catch (SpreadsheetAuthException e) {
					// relogin
					headerFactory.login();
					get.setHeader("Authorization", headerFactory.getAuthHeader()); //$NON-NLS-1$
					return executeAndParse(client, get);
				}
			} catch (IOException e) {
				throw new SpreadsheetOperationException("Error retrieving batch from Gogole Visualization Data protocol", e);
			}
		}

		private List<SheetRow> executeAndParse(HttpClient client, HttpGet get) throws IOException {
			HttpResponse response = client.execute(get);

			if (response.getStatusLine().getStatusCode() == 200)
			{
				Calendar cal = null;
				Reader reader = null;
				try {
					reader = new InputStreamReader(response.getEntity().getContent(), Charset.forName(ENCODING));  
					Map<?, ?> jsonResponse = (Map<?, ?>)parser.parseObject(reader, true);
					String status = (String)jsonResponse.get("status"); //$NON-NLS-1$
					if ("error".equals(status)) { //$NON-NLS-1$
						//TODO: better formatting
						List<Map<?, ?>> errors = (List<Map<?, ?>>) jsonResponse.get("errors"); //$NON-NLS-1$
						List<String> reasons = new ArrayList<String>();
						for (Map<?, ?> map : errors) {
							String reason = (String)map.get("reason"); //$NON-NLS-1$
							if ("user_not_authenticated".equals(reason)) { //$NON-NLS-1$
								throw new SpreadsheetAuthException("User not authenticated");
							}
							reasons.add(reason);
						}
						LogManager.logDetail(LogConstants.CTX_CONNECTOR, "Google request failed", errors); //$NON-NLS-1$
						throw new SpreadsheetOperationException(reasons.toString());							
					}
					//TODO: the warning could be sent to the client via the ExecutionContext
					
					Map<?,?> table = (Map<?,?>)jsonResponse.get("table"); //$NON-NLS-1$
					List<Map<?, ?>> cols = (List<Map<?, ?>>) table.get("cols"); //$NON-NLS-1$
					this.metadata = new ArrayList<Column>(cols.size());
					for (Map<?, ?> col : cols) {
						Column c = new Column();
						c.setAlphaName((String) col.get("id")); //$NON-NLS-1$
						String label = (String)col.get("label"); //$NON-NLS-1$
						if (label != null && !label.isEmpty()) {
							c.setLabel(label);
						}
						String type = (String)col.get("type"); //$NON-NLS-1$
						if (type != null) {
							c.setDataType(SpreadsheetColumnType.valueOf(type.toUpperCase()));
						}
						this.metadata.add(c);
					}

					List<SheetRow> result = new ArrayList<SheetRow>();

					List<Map<?,?>> rows = (List<Map<?,?>>) table.get("rows");  //$NON-NLS-1$
					for (Map<?,?> row : rows) {
						SheetRow returnRow = new SheetRow();
						List<Map<?,?>> vals = (List<Map<?,?>>)row.get("c"); //$NON-NLS-1$
						int i = -1;
						for (Map<?,?> val : vals) {
							i++;
							if (val == null) {
								returnRow.addColumn(null);
								continue;
							}
							Object object = val.get("v"); //$NON-NLS-1$
							
							if (object != null) {
								//special handling for time types
								Column c = this.metadata.get(i);
								object = convertValue(cal, object, c.getDataType());
							}

							//TODO: empty string values could be interpreted as null
							returnRow.addColumn(object);
						}
						result.add(returnRow);
					}
					
					return result;
				} finally {
					if (reader != null) {
						reader.close();
					}
				}
			} else if (response.getStatusLine().getStatusCode() == 500){ 
				//500 from server may not be a actual error. It can mean that offset is higher then actual result size. Can be solved by calling "count" first but performance penalty
				return new ArrayList<SheetRow>();
			}
			else {
				throw new SpreadsheetOperationException("Error when getting batch "+response.getStatusLine().getStatusCode()+":" +response.getStatusLine().getReasonPhrase());
			}
		}


		/**
		 * Adds limit, offset to the query. This is slightly more complicated becuase limit/offset must appear
		 * before certain clauses (label, format, options)
		 * @param amount
		 * @param offset
		 * @return
		 * @throws UnsupportedEncodingException
		 */
		private String getQueryWithBoundaries(int amount, int offset) throws UnsupportedEncodingException {
			String[] keywordsToJump = new String[] {"label","format","options"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			int indexToPut = urlEncodedQuery.length();
			
			for (String jumpIt : keywordsToJump){
				int index = urlEncodedQuery.indexOf(jumpIt);
				
				if (index != -1) {
					indexToPut = index;
					break;
				}
			}
			
			return urlEncodedQuery.substring(0, indexToPut).toString() +URLEncoder.encode(" limit "+amount+" offset "+offset+" ",ENCODING).toString() //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 
					+ urlEncodedQuery.substring(indexToPut).toString();
		}		
	}
	
	static Object convertValue(Calendar cal, Object object, SpreadsheetColumnType type) {
		switch (type) {
		case DATE:
		case DATETIME:
			if (object instanceof String) {
				String stringVal = (String)object;
				if (stringVal.startsWith("Date(") && stringVal.endsWith(")")) { //$NON-NLS-1$ //$NON-NLS-2$
					String[] parts = stringVal.substring(5, stringVal.length() - 1).split(","); //$NON-NLS-1$
					if (cal == null) {
						cal = Calendar.getInstance();
					}
					cal.clear();
					if (type == SpreadsheetColumnType.DATETIME) {
						cal.set(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]), Integer.valueOf(parts[2]), Integer.valueOf(parts[3]), Integer.valueOf(parts[4]), Integer.valueOf(parts[5]));
						object = new Timestamp(cal.getTimeInMillis());
					} else {
						cal.set(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]), Integer.valueOf(parts[2]));
						object = new Date(cal.getTimeInMillis());
					}
				}
			}
			break;
		case TIMEOFDAY:
			if (object instanceof List<?>) {
				List<Double> doubleVals = (List<Double>)object;
				if (cal == null) {
					cal = Calendar.getInstance();
				}
				cal.clear();
				cal.set(Calendar.YEAR, 1970);
				cal.set(Calendar.MONTH, Calendar.JANUARY);
				cal.set(Calendar.DAY_OF_MONTH, 1);
				cal.set(Calendar.MILLISECOND, 0);
				cal.set(Calendar.HOUR, doubleVals.get(0).intValue());
				cal.set(Calendar.MINUTE, doubleVals.get(1).intValue());
				cal.set(Calendar.SECOND, doubleVals.get(2).intValue());
				//TODO: it's not proper to convey the millis on a time value
				cal.set(Calendar.MILLISECOND, doubleVals.get(3).intValue());
				object = new Time(cal.getTimeInMillis());
			}
			break;
		}
		return object;
	}
	
}





