/****************************************************************************/
// Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.org/sumo
// Copyright (C) 2017-2018 German Aerospace Center (DLR) and others.
// TraaS module
// Copyright (C) 2016-2017 Dresden University of Technology
// This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v2.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v20.html
// SPDX-License-Identifier: EPL-2.0
/****************************************************************************/
/// @file    Query.java
/// @author  Mario Krumnow
/// @author  Evamarie Wiessner
/// @date    2016
/// @version $Id$
///
//
/****************************************************************************/
package de.tudresden.sumo.util;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.List;
import java.util.Observable;

import de.tudresden.sumo.config.Constants;
import de.tudresden.sumo.subscription.ResponseType;
import de.tudresden.sumo.subscription.SubscriptionObject;
import de.tudresden.ws.container.SumoObject;
import de.uniluebeck.itm.tcpip.Storage;
import it.polito.appeal.traci.TraCIException;
import it.polito.appeal.traci.TraCIException.UnexpectedData;
import it.polito.appeal.traci.protocol.Command;
import it.polito.appeal.traci.protocol.RequestMessage;
import it.polito.appeal.traci.protocol.ResponseContainer;
import it.polito.appeal.traci.protocol.ResponseMessage;
import it.polito.appeal.traci.protocol.StatusResponse;

public abstract class Query extends Observable {
	
	private final DataOutputStream outStream; 
	private final DataInputStream inStream; 
	
	public Query(Socket sock) throws IOException {
		outStream = new DataOutputStream(sock.getOutputStream());
		inStream = new DataInputStream(sock.getInputStream());
	}

	/**
	 * Sends a request message to SUMO and returns a response message. 
	 * @param msg
	 * @throws IOException
	 */
	protected ResponseMessage doQuery(RequestMessage msg) throws IOException {
		msg.writeTo(getOutStream());
		return new ResponseMessage(inStream);
	}

	/**
	 * Like {@link #doQuery(RequestMessage)}; in addition, verifies that all
	 * responses are successful and and the statuses match the requests.
	 * @param reqMsg
	 * @return the verified response message
	 * @throws IOException
	 * @see #doQuery(RequestMessage)
	 */
	protected ResponseMessage queryAndVerify(RequestMessage reqMsg) throws IOException {
		
		reqMsg.writeTo(getOutStream());
		
		ResponseMessage respMsg = new ResponseMessage(inStream);

		List<Command> commands = reqMsg.commands();
		List<ResponseContainer> responses = respMsg.responses();
		if (commands.size() > responses.size())
			throw new TraCIException("not enough responses received");
		
		for (int i=0; i<commands.size(); i++) {
			Command cmd = commands.get(i);
			ResponseContainer responsePair = responses.get(i);
			StatusResponse statusResp = responsePair.getStatus();
			verify("command and status IDs match", cmd.id(), statusResp.id());
			if (statusResp.result() != Constants.RTYPE_OK)
				throw new TraCIException("SUMO error for command "
						+ statusResp.id() + ": " + statusResp.description());
		}
		
		return respMsg;
	}

	/**
	 * Like {@link #doQuery(RequestMessage)}, but good for one-command/
	 * one-response queries.
	 * 
	 * @param request
	 * @throws IOException
	 */
	protected ResponseContainer doQuerySingle(Command request) throws IOException {
		RequestMessage msg = new RequestMessage();
		msg.append(request);
		ResponseMessage resp = doQuery(msg);
		return resp.responses().iterator().next();
	}

	/**
	 * Do next time step and update subscription results
	 * 
	 * @param targetTime
	 * @throws IOException
	 */
	protected void doSimulationStep(double targetTime) throws IOException {
		
			Command cmd = new Command(Constants.CMD_SIMSTEP);
		 	cmd.content().writeDouble(targetTime);
			
			RequestMessage msg = new RequestMessage();
			msg.append(cmd);
			msg.writeTo(getOutStream());
			
			int totalLen = this.inStream.readInt() - Integer.SIZE/8;
			byte[] buffer = new byte[totalLen];
			this.inStream.readFully(buffer);
			
			
			Storage s = new Storage(buffer);
			for(int i=0; i<7; i++){s.readUnsignedByte();} //offset

			int count = s.readInt();
			
			//subscription results
			for(int i=0; i<count; i++){
				
				for(int i1=0; i1<5; i1++){s.readUnsignedByte();} //offset

				int response = s.readUnsignedByte();
				ResponseType rt = ResponseType.getType(response);
				
				if(rt != ResponseType.UNKNOWN){
				
					String id = s.readStringASCII();
					
					//context
					if(rt.isContext()){
						
						int domain =  s.readUnsignedByte();
						int Variable_Count = s.readUnsignedByte();
						int Object_Count = s.readInt();
						
							for(int oc=0; oc<Object_Count; oc++){
							
								String name = s.readStringASCII();
								for(int vc=0; vc<Variable_Count; vc++){
									
									int variable =  s.readUnsignedByte(); 
									int status =  s.readUnsignedByte(); //RTYPE_OK
									int return_type =  s.readUnsignedByte(); 
									SumoObject o = CommandProcessor.read(return_type, s);
									
									SubscriptionObject so = new SubscriptionObject(id, rt, domain, name, variable, status, return_type, o);
									notifyObservers(so);
									setChanged(); 
								
								}
							}
					}else{
						
						//variable
						int Variable_Count = s.readUnsignedByte();
						for(int vc=0; vc<Variable_Count; vc++){
									
									int variable =  s.readUnsignedByte(); 
									int status =  s.readUnsignedByte(); //RTYPE_OK
									int return_type =  s.readUnsignedByte(); 
									SumoObject o = CommandProcessor.read(return_type, s);
									
									SubscriptionObject so = new SubscriptionObject(id, rt, variable, status, return_type, o);
									notifyObservers(so);
									setChanged(); 
								
						}
						
					}
					
				}
	
			}
	}
	
	
	/**
	 * Like {@link #queryAndVerify(RequestMessage)}, but good for one-command/
	 * one-response queries.
	 * 
	 * @param request
	 * @return the response container for the specified request
	 * @throws IOException
	 */
	protected ResponseContainer queryAndVerifySingle(Command request) throws IOException {
		
		RequestMessage msg = new RequestMessage();
		msg.append(request);
		ResponseMessage resp = queryAndVerify(msg);
		return resp.responses().iterator().next();
	
	}
	
	/**
	 * fireAndForget function
	 * 
	 * @param request
	 * @throws IOException
	 */
	protected void fireAndForget(Command request) {
		
		try{
		
			RequestMessage msg = new RequestMessage();
			msg.append(request);
			msg.writeTo(this.outStream);
			
			int totalLen = this.inStream.readInt() - Integer.SIZE/8;
			
			byte[] buffer = new byte[totalLen];
			this.inStream.readFully(buffer);
			
		}catch(Exception ex){ex.printStackTrace();}
	
	}
	
	protected static String verifyGetVarResponse(Command resp, int commandID, int variable, String objectID) throws UnexpectedData {
		verify("response code", commandID, resp.id());
		verify("variable ID", variable, (int)resp.content().readUnsignedByte());
		String respObjectID = resp.content().readStringASCII();
		if (objectID != null) {
			verify("object ID", objectID, respObjectID);
		}
		return respObjectID;
	}

	protected static void verify(String description, Object expected, Object actual) throws UnexpectedData {
		if(!actual.equals(expected))
			throw new UnexpectedData(description, expected, actual);
	}

	protected static void verify(String description, int expected, short actual) throws UnexpectedData {
		verify(description, expected, (int)actual);
	}

	protected static void verify(String description, int expected, byte actual) throws UnexpectedData {
		verify(description, expected, (int)actual);
	}

	public DataOutputStream getOutStream() {
		return outStream;
	}

}
