/*
 * 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.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
// https://github.com/teiid/teiid/commit/98fe64e3662916716a3d900556ab67df2932f0da#diff-e055303bc20a7b36c2296027c5edaff9
// https://issues.jboss.org/browse/TEIID-2148
// https://github.com/teiid/teiid/commit/2f0398217dbc7d038a26243d7419878bde975edb#diff-17344d7e04c5f1d5acb6d6e728e878c1
// https://github.com/teiid/teiid/commit/fceb638afb1a3e164bd479feb1839864b8c6fe29#diff-17344d7e04c5f1d5acb6d6e728e878c1
// https://issues.jboss.org/browse/TEIID-1757
// https://github.com/teiid/teiid/commit/70e4ee0f81df05d4878bebe4e5c0247501b0a31e#diff-17344d7e04c5f1d5acb6d6e728e878c1
// https://issues.jboss.org/browse/TEIID-2268
// https://issues.jboss.org/browse/TEIID-2537
// https://issues.jboss.org/browse/TEIID-3016
// https://issues.jboss.org/browse/TEIID-2630 (partially, only changes for BinaryWSProcedureExecution class)
// https://issues.jboss.org/browse/TEIID-3520
// https://issues.jboss.org/browse/TEIID-2601
package org.teiid.translator.ws;

import org.teiid.core.types.*;
import org.teiid.core.util.StringUtil;
import org.teiid.json.simple.JSONParser;
import org.teiid.json.simple.ParseException;
import org.teiid.json.simple.SimpleContentHandler;
import org.teiid.language.Argument;
import org.teiid.language.Call;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.metadata.RuntimeMetadata;
import org.teiid.translator.*;

import javax.activation.DataSource;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service.Mode;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPBinding;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.util.*;

/**
 * http handler 
 */
public class BinaryWSProcedureExecution implements ProcedureExecution {

	public static final class StreamingBlob extends BlobImpl {
 	    InputStream is;

		public StreamingBlob(InputStream is) {
			this.is = is;
		}

		@Override
 	    public InputStream getBinaryStream() throws SQLException {
			if (is == null) {
				throw new SQLException(WSExecutionFactory.UTIL.gs(WSExecutionFactory.Event.DVCORE94028));
			}
			InputStream result = is;
			is = null;
			return result;
		}
 	}
	
	RuntimeMetadata metadata;
    ExecutionContext context;
    private Call procedure;
    private List<Argument> arguments;
    private DataSource returnValue;
    private WSConnection conn;
    WSExecutionFactory executionFactory;
	Map<String, List<String>> customHeaders;
	Map<String, Object> responseContext = Collections.emptyMap();
	int responseCode = 200;
	private boolean useResponseContext;
	private boolean alwaysAllowPayloads;
	private String headersXML;
	private Object[] headersArray;
	private Boolean failed;
    
    /** 
     * @param env
     */
    public BinaryWSProcedureExecution(Call procedure, RuntimeMetadata metadata, ExecutionContext context, WSExecutionFactory executionFactory, WSConnection conn) {
        this.metadata = metadata;
        this.context = context;
        this.procedure = procedure;
        this.conn = conn;
        this.executionFactory = executionFactory;
	    this.arguments = WSExecutionFactory.getInArguments(this.procedure);
    }

    public void setUseResponseContext(boolean useResponseContext) {
		this.useResponseContext = useResponseContext;
	}

	public void setAlwaysAllowPayloads(boolean alwaysAllowPayloads) {
		this.alwaysAllowPayloads = alwaysAllowPayloads;
    }

    public void execute() throws TranslatorException {
        String method = (String)arguments.get(0).getArgumentValue().getValue();
        Object payload = arguments.get(1).getArgumentValue().getValue();
        String endpoint = (String)arguments.get(2).getArgumentValue().getValue();
    	try {
    		// EXTENSION
	    	// ISSUE: DVCORE-2218
	    	// FIXED BY: Salvatore Raunich
	    	// DATE: 2014.07.21
//	        Dispatch<DataSource> dispatch = conn.createDispatch(HTTPBinding.HTTP_BINDING, endpoint, DataSource.class, Mode.MESSAGE);
		    Boolean failOnHTTPError = null;
		    if (arguments.size() > 5)
				failOnHTTPError = (Boolean)arguments.get(5).getArgumentValue().getValue();
			Dispatch<DataSource> dispatch = null;
			if (failOnHTTPError != null)
		        dispatch = conn.createDispatch(HTTPBinding.HTTP_BINDING, endpoint, DataSource.class, Mode.MESSAGE, failOnHTTPError); 
			else
		        dispatch = conn.createDispatch(HTTPBinding.HTTP_BINDING, endpoint, DataSource.class, Mode.MESSAGE, true); 
			// END EXTENSION
	
			if (method == null) {
				method = "POST"; //$NON-NLS-1$
			}
			
			dispatch.getRequestContext().put(MessageContext.HTTP_REQUEST_METHOD, method);

			// EXTENSION
			// ISSUE: DVCORE-382
			// FIXED BY: Roman Pedchenko
			// DATE: 2013.04.23
		    String requestContentType = null;
		    if (arguments.size() > 3) {
			    requestContentType = (String) arguments.get(3).getArgumentValue().getValue();
		    }
			if (requestContentType != null)
				((Map<String, List<String>>) dispatch.getRequestContext().get(MessageContext.HTTP_REQUEST_HEADERS)).put("Content-Type", Collections.singletonList(requestContentType));
			// END EXTENSION

			Map<String, List<String>> httpHeaders = (Map<String, List<String>>)dispatch.getRequestContext().get(MessageContext.HTTP_REQUEST_HEADERS);
		    if (customHeaders != null) {
			    httpHeaders.putAll(customHeaders);
		    }
		    if (arguments.size() > 4) {
			    Clob headers = (Clob)arguments.get(4).getArgumentValue().getValue();
			    if (headers != null) {
				    ClobType requestHeadersClob = new ClobType(headers);
				    String requestHeaders = ClobType.getString(requestHeadersClob);
				    if (requestHeaders.trim().startsWith("{")) {
					    parseHeader(httpHeaders, headers);
				    } else {
					    String lines[] = requestHeaders.split("\\r?\\n");
					    for (String line : lines) {
						    if (!line.contains(":"))
							    throw new TranslatorException(WSExecutionFactory.UTIL.gs(WSExecutionFactory.Event.DVCORE94128));
						    String key = line.substring(0, line.indexOf(":")).trim();
						    String value = line.substring(line.indexOf(":") + 1).trim();
						    httpHeaders.put(key, Collections.singletonList(value));
					    }
				    }
			    }
		    }
			if(payload == null && "POST".equals(method) && httpHeaders.get("Content-Length") == null) {
				httpHeaders.put("Content-Length", Collections.singletonList("0"));
			}
		    dispatch.getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders);

			LogManager.logDetail(LogConstants.CTX_WS,
					"Request Context: " + WsDataRetentionManager.cloneMapSanitizingSensibleValues(dispatch.getRequestContext()));

			DataSource ds = null;
			if (payload instanceof String) {
				ds = new InputStreamFactory.ClobInputStreamFactory(new ClobImpl((String)payload));
			} else if (payload instanceof SQLXML) {
				ds = new InputStreamFactory.SQLXMLInputStreamFactory((SQLXML)payload);
			} else if (payload instanceof Clob) {
				ds = new InputStreamFactory.ClobInputStreamFactory((Clob)payload);
			} else if (payload instanceof Blob) {
				ds = new InputStreamFactory.BlobInputStreamFactory((Blob)payload);
			}

			LogManager.logDetail(LogConstants.CTX_WS, "Payload: ", payload);

			Integer dataRetentionId = WsDataRetentionManager.generateDatRetentionId();

			ds = WsDataRetentionManager.getInstance().logRequestAndReturnDataSource(dataRetentionId, endpoint, ds, dispatch);
		    this.returnValue = dispatch.invoke(ds);

			Map<String, Object> rc = dispatch.getResponseContext();
		    this.responseCode = (Integer)rc.get(WSConnection.RESPONSE_CODE);
			this.returnValue = WsDataRetentionManager.getInstance().logResponseAndReturnDataSource(dataRetentionId, rc, returnValue, responseCode);
		    if (useResponseContext) {
			    //it's presumed that the caller will handle the response codes
			    this.responseContext = rc;
		    } else {
			    // actually we already handle error messages in WSConnectionImpl class
			    //TODO: may need to add logic around some 200/300 codes - cxf should at least be logging this
			    //if (this.responseCode >= 400) {
			    //    String message = conn.getStatusMessage(this.responseCode);
			    //    throw new TranslatorException(WSExecutionFactory.Event.TEIID15005, WSExecutionFactory.UTIL.gs(WSExecutionFactory.Event.TEIID15005, this.responseCode, message));
			    //}
		    }

		    // gets headers in XML format
		    Map<String, List<String>> headers = (Map<String, List<String>>)dispatch.getResponseContext().get(MessageContext.HTTP_RESPONSE_HEADERS);
		    try {
			    headersXML = getHeadersXML(headers);
		    } catch (Exception e) {
			    throw new TranslatorException(e);
		    }
		    headersArray = getHeadersArray(headers);
		    failed = (Boolean)rc.get(WSConnection.INVOCATION_FAILED);
	    } catch (WebServiceException e) {
			throw new TranslatorException(e);
	    } catch (ParseException e) {
		    throw new TranslatorException(e);
    	} catch (IOException e) {
		    throw new TranslatorException(e);
    	} catch (SQLException e) {
		    throw new TranslatorException(e);
		}
    }

	static void parseHeader(Map<String, List<String>> httpHeaders,
 	                Clob headers) throws ParseException, TranslatorException, IOException, SQLException {
		SimpleContentHandler sch = new SimpleContentHandler();
		JSONParser parser = new JSONParser();
		Reader characterStream = headers.getCharacterStream();
		try {
			parser.parse(characterStream, sch);
			Object result = sch.getResult();
			if (!(result instanceof Map)) {
				throw new TranslatorException(WSExecutionFactory.Event.TEIID15006, WSExecutionFactory.UTIL.gs(WSExecutionFactory.Event.TEIID15006));
			}
			Map<String, Object> values = (Map)result;
			for (Map.Entry<String, Object> entry : values.entrySet()) {
				if ((entry.getValue() instanceof Map) || entry.getValue() == null) {
					throw new TranslatorException(WSExecutionFactory.Event.TEIID15006, WSExecutionFactory.UTIL.gs(WSExecutionFactory.Event.TEIID15006));
				}
				if (!(entry.getValue() instanceof List)) {
					entry.setValue(Arrays.asList(entry.getValue().toString()));
					continue;
				}
				List<Object> list = (List)entry.getValue();
				for (int i = 0; i < list.size(); i++) {
					Object value = list.get(i);
					if (value instanceof Map || value instanceof List) {
						throw new TranslatorException(WSExecutionFactory.Event.TEIID15006, WSExecutionFactory.UTIL.gs(WSExecutionFactory.Event.TEIID15006));
					}
					if (!(value instanceof String)) {
						list.set(i, value.toString());
					}
				}
			}
			httpHeaders.putAll((Map) values);
		} finally {
			characterStream.close();
		}

    }

	private void buildResposeCode(String object) {
		StringTokenizer st = new StringTokenizer(object, " ");//$NON-NLS-1$
		st.nextToken(); // http
		this.responseCode = Integer.parseInt(st.nextToken());
		StringBuilder sb = new StringBuilder();
		while (st.hasMoreTokens()) {
			sb.append(st.nextToken());
			sb.append(" "); //$NON-NLS-1$
		}
    }

    @Override
    public List<?> next() throws TranslatorException, DataNotAvailableException {
    	return null;
    }  
    
    @Override
    public List<?> getOutputParameterValues() throws TranslatorException {
	    Object result = returnValue;
	    if (returnValue != null && arguments.size() > 6 && Boolean.TRUE.equals(arguments.get(6).getArgumentValue().getValue())) {
		    try {
			    result = new BlobType(new StreamingBlob(returnValue.getInputStream()));
		    } catch (IOException e) {
			    throw new TranslatorException(e);
		    }
	    }
	    String contentType = returnValue != null ? returnValue.getContentType() : null;
		return Arrays.asList(result, contentType, headersArray, headersXML, failed, responseCode);
    }
    
    public void close() {
    	
    }

    public void cancel() throws TranslatorException {
        // no-op
    }

	public String getHeadersXML(Map<String, List<String>> headers) throws Exception {
		StringWriter stringWriter = new StringWriter();
		XMLStreamWriter writer = createHeaderStreamWriter(stringWriter);
		if (headers != null) {
			for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
				String headerName = entry.getKey() != null ? entry.getKey() : "null";
				String headerValue = StringUtil.join(entry.getValue(), ",");
				writer = addHeader(writer, headerName, headerValue);
			}
		}
		return finishDocument(writer, stringWriter);
	}

	private XMLStreamWriter createHeaderStreamWriter(StringWriter stringWriter) throws Exception {
		XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
		XMLStreamWriter writer = outputFactory.createXMLStreamWriter(stringWriter);
		writer.writeStartDocument("UTF-8", "1.0");
		writer.writeStartElement("headers");
		return writer;
	}

	private XMLStreamWriter addHeader(XMLStreamWriter writer, String headerName, String headerValue) throws Exception {
		writer.writeStartElement("header");
		writer.writeAttribute("name", headerName);
		writer.writeCharacters(headerValue);
		writer.writeEndElement();
		return writer;
	}

	private String finishDocument(XMLStreamWriter writer, StringWriter stringWriter) throws Exception {
		writer.writeEndElement();
		writer.writeEndDocument();
		return stringWriter.toString();
	}

	private Object[] getHeadersArray(Map<String, List<String>> headers) {
		if (headers == null)
			return null;

		Object[] result = new Object[headers.size()];
		int i = 0;
		for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
			String headerValue = StringUtil.join(entry.getValue(), ",");
			String[] header = new String[2];
			header[0] = entry.getKey();
			header[1] = headerValue;
			result[i++] = header;
		}
		return result;
	}

	public void setCustomHeaders(Map<String, List<String>> customHeaders) {
		this.customHeaders = customHeaders;
    }

	public Object getResponseHeader(String name){
		return this.responseContext.get(name);
    }

	public Map<String, Object> getResponseHeaders(){
		return new HashMap<String, Object>(this.responseContext) ;
    }

	public int getResponseCode() {
		return this.responseCode;
    }
}
