import java.util.Arrays;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;

import org.apache.axis.message.addressing.Address;
import org.apache.axis.message.addressing.EndpointReferenceType;
import org.gcube.common.core.contexts.GCUBERemotePortTypeContext;
import org.gcube.common.core.contexts.GHNContext;
import org.gcube.common.core.informationsystem.client.ISClient;
import org.gcube.common.core.informationsystem.client.RPDocument;
import org.gcube.common.core.informationsystem.client.queries.WSResourceQuery;
import org.gcube.common.core.scope.GCUBEScope;
import org.gcube.common.core.security.GCUBESecurityManagerImpl;
import org.gcube.opensearch.opensearchdatasource.OpenSearchDataSource;
import org.gcube.opensearch.opensearchdatasource.OpenSearchDataSourceFactory;
import org.gcube.opensearch.opensearchdatasource.stubs.CreateResourceParams;
import org.gcube.opensearch.opensearchdatasource.stubs.CreateResourceResponse;
import org.gcube.opensearch.opensearchdatasource.stubs.RefreshCache;
import org.gcube.opensearch.opensearchdatasource.stubs.OpenSearchDataSourceFactoryPortType;
import org.gcube.opensearch.opensearchdatasource.stubs.OpenSearchDataSourcePortType;
import org.gcube.opensearch.opensearchdatasource.stubs.service.OpenSearchDataSourceServiceAddressingLocator;
import org.gcube.opensearch.opensearchdatasource.stubs.service.OpenSearchDataSourceFactoryServiceAddressingLocator;
import org.gcube.opensearch.opensearchdatasource.stubs.StringArray;
import org.gcube.searchsystem.searchsystemservice.stubs.SearchResponse;
import org.gcube.searchsystem.searchsystemservice.stubs.SearchSystemServicePortType;
import org.gcube.searchsystem.searchsystemservice.stubs.service.SearchMasterServiceAddressingLocator;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.Writer;
import java.net.URLEncoder;
import java.io.OutputStreamWriter;
import java.io.File;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.oasis.wsrf.lifetime.Destroy;
import org.globus.wsrf.encoding.ObjectDeserializer;
import org.globus.wsrf.encoding.ObjectSerializer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

public class OpenSearchTest {
	
	private static class Field {
		public String fieldName;
		public String collectionID;
		public String language;
		public String type;
		
		public void fromXML(Element XML) throws Exception {
			NodeList els = XML.getElementsByTagName("FieldName");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " FieldName elements. Expected 1");
			this.fieldName = els.item(0).getFirstChild().getNodeValue();
			if(this.fieldName.equals("")) throw new Exception("Invalid FieldName element");
			
			els = XML.getElementsByTagName("CollectionID");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " CollectionID elements. Expected 1");
			this.collectionID = els.item(0).getFirstChild().getNodeValue();
			if(this.collectionID.equals("")) throw new Exception("Invalid CollectionID element");
			
			els = XML.getElementsByTagName("Language");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " Language elements. Expected 1");
			this.language = els.item(0).getFirstChild().getNodeValue();
			if(this.language.equals("")) throw new Exception("Invalid Language element");
			
			els = XML.getElementsByTagName("Type");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " Type elements. Expected 1");
			this.type = els.item(0).getFirstChild().getNodeValue();
			if(!this.type.equals("s") && !this.type.equals("p")) throw new Exception("Invalid Language element. Expected s or p, found " + this.type);
		}
		
		@Override
		public String toString() {
			return this.collectionID + ":" + this.language + ":" + this.type + ":" + this.fieldName;
		}
	}

	private static class Provider {
		public String collectionID;
		public String genericResourceID;
		public String[] fixedParameters;
		
		public void fromXML(Element XML) throws Exception {
			NodeList els = XML.getElementsByTagName("CollectionID");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " CollectionID elements. Expected 1");
			this.collectionID = els.item(0).getFirstChild().getNodeValue();
			if(this.collectionID.equals("")) throw new Exception("Invalid CollectionID element");
			
			els = XML.getElementsByTagName("GenericResourceID");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " GenericResourceID elements. Expected 1");
			this.genericResourceID = els.item(0).getFirstChild().getNodeValue();
			if(this.genericResourceID.equals("")) throw new Exception("Invalid GenericResourceID element");
			
			els = XML.getElementsByTagName("FixedParameters");
			if(els.getLength() != 1) throw new Exception("Found " + els.getLength() + " FixedParameters elements. Expected 1");
			
			els = ((Element)els.item(0)).getElementsByTagName("FixedParameter");
			int len = 0;
			for(int i = 0; i < els.getLength();i++) {
				if(els.item(i).getFirstChild() != null) len++;
			}
			this.fixedParameters = new String[len];
			for(int i = 0; i < els.getLength(); i++) {
				Node child = els.item(i).getFirstChild();
				if(child!=null) this.fixedParameters[i] = child.getNodeValue();
			}
		}
		
		@Override
		public String toString() {
			StringBuffer str = new StringBuffer();
			str.append("Provider(" + "colId=" + this.collectionID + ", " + " grId=" + this.genericResourceID + ", " + "fixedParams=(");
			for(int i = 0 ; i < this.fixedParameters.length; i++) {
				if(i != 0) str.append(", ");
				str.append("fixedParam=" + this.fixedParameters[i]);
			}
			str.append(")");
			str.append(")");
			return str.toString();
		}
	}
	
	//private static String openSearchFactoryURI = "http://donald.di.uoa.gr:8080/wsrf/services/gcube/opensearch/opensearchdatasource/OpenSearchDataSourceFactory";
	//private static String openSearchFactoryURI = "http://node36.p.d4science.research-infrastructures.eu:8080/wsrf/services/gcube/opensearch/opensearchdatasource/OpenSearchDataSourceFactory";
	//private static String searchMasterURI = "http://donald.di.uoa.gr:8080/wsrf/services/gcube/search/SearchMaster";
	//private static String searchMasterURI = "http://node6.p.d4science.research-infrastructures.eu:8000/wsrf/services/gcube/search/SearchMaster";
	//private static String searchMasterURI = "http://dl17.di.uoa.gr:8080/wsrf/services/gcube/search/SearchMaster";
	/**
	 * The EPR of the search system that will be used
	 */
	private static String searchSystemURI;
	
	/**
	 * Prints client usage and exits
	 */
	public static void printUsageAndExit() {
		System.out.println("USAGE:");
		System.out.println("OpenSearchTest -create stagingFile [resourceKey]");
		System.out.println("OpenSearchTest -destroy scope colID");
		System.out.println("OpenSearchTest -lookup scope colID [queryFilename]");
		System.out.println("OpenSearchTest -lookupEPR scope colID eprFileName [queryFilename]");
		System.out.println("OpenSearchTest -querySearchSystem scope SearchSystemURIFileName [queryFilename]");
		System.out.println("OpenSearchTest -refreshCache scope");
		System.exit(0);
	}
	
	/**
	 * Reads the search system EPR from a file
	 * 
	 * @param filename The name of the file containing the EPR of the search system
	 * @throws Exception In case of error
	 */
	public static void readSearchSystemURI(String filename) throws Exception {
		searchSystemURI = "";
		BufferedReader reader = new BufferedReader(new FileReader(filename));
		String line;
		while((line = reader.readLine()) != null)
			searchSystemURI += line;
	}
	
	public static String readQuery(String filename) throws Exception {
		StringBuffer query = new StringBuffer();
		BufferedReader reader = new BufferedReader(new FileReader(filename));
		String line;
		while((line = reader.readLine()) != null)
			query.append(line);
		return query.toString();
	}
	

	/**
	 * Reads the {@link OpenSearchDataSourceFactory} EPR from the staging document
	 * 
	 * @param stagingXML Document containing staging information
	 * @returns the EPR of the {@link OpenSearchDataSourceFactory}
	 * @throws Exception In case of error
	 */
	public static String readFactoryURI(Document stagingXML) throws Exception {
		String factoryURI=null;
		NodeList elements = stagingXML.getElementsByTagName("FactoryURI");
		if(elements.getLength() != 1) throw new Exception("Found " + elements.getLength() + " FactoryURI elements. Expected 1");
		factoryURI = elements.item(0).getFirstChild().getNodeValue();
		if(factoryURI == null || factoryURI.equals("")) throw new Exception("Invalid FactoryURI value");
		return factoryURI;
	}
	
	/**
	 * Reads the scope from the staging document
	 * 
	 * @param stagingXML Document containing staging information
	 * @returns the scope
	 * @throws Exception In case of error
	 */
	public static GCUBEScope readScope(Document stagingXML) throws Exception {
		String scope=null;
		NodeList elements = stagingXML.getElementsByTagName("Scope");
		if(elements.getLength() != 1) throw new Exception("Found " + elements.getLength() + " Scope elements. Expected 1");
		scope = elements.item(0).getFirstChild().getNodeValue();
		if(scope == null || scope.equals("")) throw new Exception("Invalid Scope value");
		return GCUBEScope.getScope(scope);
	}
	
	/**
	 * Reads the filename to which the WS EPR serialization will be written from the staging document
	 * 
	 * @param stagingXML Document containing staging information
	 * @returns the filename to which the WS EPR serialization will be written
	 * @throws Exception In case of error
	 */
	public static String readOutputWSEPRFilename(Document stagingXML) throws Exception {
		String filename=null;
		NodeList elements = stagingXML.getElementsByTagName("OutputWSEPRFilename");
		if(elements.getLength() != 1) throw new Exception("Found " + elements.getLength() + " OutputWSEPRFilename elements. Expected 1");
		filename = elements.item(0).getFirstChild().getNodeValue();
		if(filename == null || filename.equals("")) throw new Exception("Invalid OutputWSEPRFilename value");
		return filename;
	}
	
	public static List<Provider> readProviders(Document stagingXML) throws Exception {
		List<Provider> providers = new ArrayList<Provider>();
		NodeList elements = stagingXML.getElementsByTagName("Providers");
		if(elements.getLength() != 1) throw new Exception("Found " + elements.getLength() + " Providers elemets. Expected 1");
		elements = ((Element)elements.item(0)).getElementsByTagName("Provider");
		for(int i = 0; i < elements.getLength(); i++) {
			Provider prov = new Provider();
			prov.fromXML((Element)elements.item(i));
			providers.add(prov);
		}
		return providers;
	}
	
	public static List<Field> readFields(Document stagingXML) throws Exception {
		List<Field> fields = new ArrayList<Field>();
		NodeList elements = stagingXML.getElementsByTagName("Fields");
		if(elements.getLength() != 1) throw new Exception("Found " + elements.getLength() + " Fields elemets. Expected 1");
		elements = ((Element)elements.item(0)).getElementsByTagName("Field");
		for(int i = 0; i < elements.getLength(); i++) {
			Field field = new Field();
			field.fromXML((Element)elements.item(i));
			fields.add(field);
		}
		return fields;
	}
	
	public static void printStagingInformation(String factoryURI, GCUBEScope scope, String filename, List<Field> fields, List<Provider> providers) {
		System.out.println("scope: " + scope.toString());
		System.out.println("factoryURI: " + factoryURI);
		System.out.println("output EPR filename: " + filename);
		System.out.println();
		System.out.println("Total " + fields.size() + " fields:");
		for(Field f: fields)
			System.out.println(f);
		System.out.println();
		System.out.println("Total " + providers.size() + " providers:");
		for(Provider p: providers)
			System.out.println(p);
	}
	
	/**
	 * Checks if the correct number of arguments are present depending on the operation
	 * 
	 * @param args The arguments of the client
	 */
	public static void checkArgs(String[] args) {
		List<String> modes = Arrays.asList("-create -destroy -lookup -lookupepr -querysearchsystem -refreshcache".split(" "));
	
		if(args.length < 1)
			printUsageAndExit();
		
		String mode = args[0].toLowerCase();
		
		if(!modes.contains(mode))
			printUsageAndExit();
		if(mode.equalsIgnoreCase("-create") && args.length != 2 && args.length != 3)
			printUsageAndExit();
		if(mode.equalsIgnoreCase("-destroy") && args.length != 3)
			printUsageAndExit();
		if(mode.equalsIgnoreCase("-lookup") && args.length != 3 && args.length != 4)
			printUsageAndExit();
		if(mode.equalsIgnoreCase("-querySearchSystem") && args.length != 3 && args.length != 4)
			printUsageAndExit();
		if(mode.equalsIgnoreCase("-lookupEPR") && args.length != 4 && args.length != 5)
			printUsageAndExit();
		if(mode.equalsIgnoreCase("-refreshCache") && args.length != 2)
			printUsageAndExit();
	}
	/**
	 * @param args The arguments
	 * @see #printUsageAndExit()
	 */
	public static void main(String[] args) throws Exception{
		
		checkArgs(args);
		String mode = args[0];
		String query = URLEncoder.encode("http://a9.com/-/spec/opensearch/1.1/", "UTF-8") + ":searchTerms" + "=\"italy\" config:numOfResults=\"80\"";
		
		GCUBESecurityManagerImpl secManager = new GCUBESecurityManagerImpl() {  public boolean isSecurityEnabled() {return false;}};
		
		if(mode.equalsIgnoreCase("-create")){
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			dbf.setNamespaceAware(true);
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document stagingXML = db.parse(new File(args[1]));
			String resourceKey = null;
			if(args.length == 3)
				resourceKey = args[2];
			
			String factoryURI = readFactoryURI(stagingXML);
			GCUBEScope scope = readScope(stagingXML);
			List<Provider> providers = readProviders(stagingXML);
			List<Field> fields = readFields(stagingXML);
			String filename = readOutputWSEPRFilename(stagingXML);
			List<String> fixedParameters = new ArrayList<String>();
			
			printStagingInformation(factoryURI, scope, filename, fields, providers);
			
			OpenSearchDataSourceFactoryPortType factoryPortType = createFactoryPortType(scope, factoryURI, secManager);
			CreateResourceParams params =  new CreateResourceParams();
			StringArray fieldsArray = new StringArray();
			String[] stringFieldsArray = new String[fields.size()];
			for(int i = 0; i < fields.size(); i++)
				stringFieldsArray[i] = fields.get(i).toString();
			fieldsArray.setArray(stringFieldsArray);
			params.setFields(fieldsArray);
			org.gcube.opensearch.opensearchdatasource.stubs.Provider[] paramProviders = new org.gcube.opensearch.opensearchdatasource.stubs.Provider[providers.size()];
			for(int i = 0; i < providers.size(); i++) {
				paramProviders[i] = new org.gcube.opensearch.opensearchdatasource.stubs.Provider();
				paramProviders[i].setCollectionID(providers.get(i).collectionID);
				paramProviders[i].setOpenSearchResourceID(providers.get(i).genericResourceID);
				StringArray paramFixedParams = new StringArray();
				paramFixedParams.setArray(providers.get(i).fixedParameters);
				paramProviders[i].setFixedParameters(paramFixedParams);
			}
			params.setProviders(paramProviders);
			if(resourceKey != null)
				params.setResourceKey(resourceKey);
			
			long start = Calendar.getInstance().getTimeInMillis();
			CreateResourceResponse resp = factoryPortType.createResource(params);
			long end = Calendar.getInstance().getTimeInMillis();
			System.out.println("factoryPortType.createResource took: " + (end-start));
			
			FileWriter writer = new FileWriter(filename);
	        ObjectSerializer.serialize(writer,resp.getEndpointReference(), new QName("http://gcube-system.org/namespaces/opensearch/OpenSearchDataSource", "ResourceKey"));
	        writer.close();

			System.out.println("Response received: " +  resp.getEndpointReference());
		
		}else if(mode.equalsIgnoreCase("-destroy")) {
			GCUBEScope scope = GCUBEScope.getScope(args[1]);
			String colID = args[2];
			OpenSearchDataSourcePortType servicePortType = getServicePortType(scope, secManager, colID);
			servicePortType.destroy(new Destroy());
			System.out.println("WS resource destroyed");
		
		}else if(mode.equalsIgnoreCase("-lookup")){
			GCUBEScope scope = GCUBEScope.getScope(args[1]);
			String colID =  args[2];
			if(args.length == 4) 
				query = readQuery(args[3]);
			OpenSearchDataSourcePortType servicePortType = getServicePortType(scope, secManager, colID);
			String resp = servicePortType.query(query);
			System.out.println("Query response: " +  resp);
		
		}else if(mode.equalsIgnoreCase("-lookupEPR")){
			GCUBEScope scope = GCUBEScope.getScope(args[1]);
			String colID =  args[2];
			String filename = args[3];
			if(args.length == 5)
				readQuery(args[4]);
			FileReader reader = new FileReader(filename);
	        EndpointReferenceType resourceEPR= 
	               (EndpointReferenceType) ObjectDeserializer.deserialize(new InputSource(reader),EndpointReferenceType.class);
	        reader.close();

	        OpenSearchDataSourceServiceAddressingLocator serviceLocator = new OpenSearchDataSourceServiceAddressingLocator();
			OpenSearchDataSourcePortType servicePorttype = serviceLocator.getOpenSearchDataSourcePortTypePort(resourceEPR);
			servicePorttype =  GCUBERemotePortTypeContext.getProxy(servicePorttype, scope, secManager);
			long start = Calendar.getInstance().getTimeInMillis();
			String resp = servicePorttype.query(query);
			long end = Calendar.getInstance().getTimeInMillis();
			System.out.println("servicePorttype.query took: " + (end-start));
			
			System.out.println("Query response: " +  resp);
		
		}else if(mode.equalsIgnoreCase("-querySearchSystem")){
			GCUBEScope scope = GCUBEScope.getScope(args[1]);
			String searchSystemURIFileName = args[2];
			readSearchSystemURI(searchSystemURIFileName);
			if(args.length == 4)
				query = readQuery(args[3]);
			SearchSystemServicePortType ssPorttype = createSearchSystemPortType(scope, secManager);
			//String resp = ssPorttype.search("parse: opensearch by '" + query + "' on " + colID);
			SearchResponse resp = ssPorttype.search(query);
			System.out.println("Search response: " +  resp.getResultSetEpr());
		}else if(mode.equalsIgnoreCase("-refreshCache")) {
			GCUBEScope scope = GCUBEScope.getScope(args[1]);
			OpenSearchDataSourcePortType servicePortType = getServicePortType(scope, secManager, null);
			System.out.println("Got service portType! " + servicePortType);
			servicePortType.refreshCache(new RefreshCache());
			System.out.println("Sent request to force generic resource cache refresh");
		
		}else{
			System.out.println("Unknown mode: " +  mode);
		}

	}
	
	private static OpenSearchDataSourceFactoryPortType createFactoryPortType(GCUBEScope scope, String openSearchFactoryURI, GCUBESecurityManagerImpl secManager) throws Exception{
		OpenSearchDataSourceFactoryServiceAddressingLocator factoryLocator =  new OpenSearchDataSourceFactoryServiceAddressingLocator();
		EndpointReferenceType factoryEPR = new EndpointReferenceType();
		factoryEPR.setAddress(new Address(openSearchFactoryURI));
		OpenSearchDataSourceFactoryPortType factoryPorttype = factoryLocator.getOpenSearchDataSourceFactoryPortTypePort(factoryEPR);
		factoryPorttype = GCUBERemotePortTypeContext.getProxy(factoryPorttype, scope, secManager);
		return factoryPorttype;
	}
	
	private static OpenSearchDataSourcePortType getServicePortType(GCUBEScope scope, GCUBESecurityManagerImpl secManager, String colID) throws Exception{
		List<String[]> props = new LinkedList<String[]>();
		props.add(new String[] { "ServiceClass", "OpenSearch" });
		props.add(new String[] { "ServiceName", "OpenSearchDataSource" });
		if(colID != null)
			props.add(new String[] { "CollectionID", colID });
		List<EndpointReferenceType> eprs = getWSResourceEPRsFromPropValues(props, scope);
		
		if(eprs.size() == 0){
			System.out.println("Could not find any epr " + (colID != null ? ("for colID: " + colID) : ""));
			System.exit(0);
		}else if(eprs.size() > 1 && colID != null){
			System.out.println("Found more than one eprs for colID: " + colID);
		}
		for(int i = 0; i < eprs.size(); i++) {
			Writer sout = new OutputStreamWriter(System.out);
			ObjectSerializer.serialize(sout, eprs.get(i), new QName("http://gcube-system.org/namespaces/opensearch/OpenSearchDataSource", "ResourceKey"));
			sout.flush();
			System.out.println();
		}
		
		OpenSearchDataSourceServiceAddressingLocator serviceLocator = new OpenSearchDataSourceServiceAddressingLocator();
		OpenSearchDataSourcePortType servicePorttype = null;
		for(int i = 0; i < eprs.size(); i++) {
			try { 
				servicePorttype = serviceLocator.getOpenSearchDataSourcePortTypePort(eprs.get(i)); 
			} 
			catch(Exception e ) { continue; }
		}
		servicePorttype =  GCUBERemotePortTypeContext.getProxy(servicePorttype, scope, secManager);
		return servicePorttype;		
	}
	
	private static List<EndpointReferenceType> getWSResourceEPRsFromPropValues(List<String[]> properties, GCUBEScope scope) throws Exception {
		String filter = "$result/child::*[local-name()='"+properties.get(0)[0].trim()+"']/string() eq '"+properties.get(0)[1].trim()+"'";
		for (int i=1; i<properties.size(); i++)
			filter += " and $result/child::*[local-name()='"+properties.get(i)[0].trim()+"']/string() eq '"+properties.get(i)[1].trim()+"'";
		
		ISClient client = GHNContext.getImplementation(ISClient.class);
		WSResourceQuery gquery = client.getQuery(WSResourceQuery.class);
		gquery.addGenericCondition(filter);
		System.out.println("WSResourceQuery txt is: " + gquery.getExpression().toString());

		List<EndpointReferenceType> ret = new LinkedList<EndpointReferenceType>();
		for(RPDocument d : client.execute(gquery, scope)){
			ret.add(d.getEndpoint());
		}
		return ret;
	}
	
	private static SearchSystemServicePortType createSearchSystemPortType(GCUBEScope scope, GCUBESecurityManagerImpl secManager) throws Exception{
		SearchMasterServiceAddressingLocator systemLocator =  new SearchMasterServiceAddressingLocator();
		EndpointReferenceType systemEPR = new EndpointReferenceType();
		systemEPR.setAddress(new Address(searchSystemURI));
		SearchSystemServicePortType systemPorttype = systemLocator.getSearchSystemServicePortTypePort(systemEPR);
		systemPorttype = GCUBERemotePortTypeContext.getProxy(systemPorttype, scope, secManager);
		return systemPorttype;
	}

}
