/**
 * Copyright (c) 2014 Etherios, a Division of Digi International, Inc.
 * All rights not expressly granted are reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Etherios 11001 Bren Road East, Minnetonka, MN 55343
 * =======================================================================
 */
package com.etherios.connector.tank;

import com.etherios.connector.core.log.ConnectorLogger;
import com.etherios.connector.core.log.Logger;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Class to simulate a tank behavior.
 * 
 * <p>The tank is represented by its:</p>
 * <ul>
 * <li>Level: Simulated based on a maximum volume ({@value #MAX_VOLUME}) and 
 * input ({@value #INPUT_FLOW} liters per {@value #REFRESH_TIMEOUT} ms) and 
 * output ({@value #OUTPUT_FLOW} liters per {@value #REFRESH_TIMEOUT} ms) 
 * flow values.</li>
 * <li>Temperature: Random variation between -0.5 to +0.5 C every 
 * {@value #REFRESH_TIMEOUT}.</li>
 * <li>Input/Output valves: To manage the level of the tank via the input/output 
 * flow values.</li>
 * </ul>
 * 
 * <p>The status of the tank is updated every {@value #REFRESH_TIMEOUT} 
 * milliseconds.</p>
 */
public class SimulatedTank {

	/* Constants */

	/**
	 * Maximum tank volume: {@value} liters.
	 */
	private static final int MAX_VOLUME = 4000;
	/**
	 * Initial tank volume: {@value} liters.
	 */
	private static final int INIT_VOLUME = (MAX_VOLUME / 2);
	/**
	 * Input flow: {@value} liters per {@value #REFRESH_TIMEOUT} ms.
	 */
	private static final int INPUT_FLOW = 10;
	/**
	 * Output flow: {@value} liters per {@value #REFRESH_TIMEOUT} ms.
	 */
	private static final int OUTPUT_FLOW = 15;
	/**
	 * Initial tank temperature: {@value} Celsius degrees.
	 */
	private static final int INIT_TEMP = 25;
	/**
	 * Minimum tank temperature: {@value} Celsius degrees.
	 */
	private static final int MIN_TEMPERATURE = 20;
	/**
	 * Maximum tank temperature: {@value} Celsius degrees.
	 */
	private static final int MAX_TEMPERATURE = 30;

	/**
	 * Timeout to refresh the tank status: {@value} milliseconds.
	 */
	private static final int REFRESH_TIMEOUT = 2 * 1000; // 2 seconds

	/* Variables */
	
	// Tank valves.
	private boolean valveIn;
	private boolean valveOut;
	
	// Tank volume.
	private int volume;
	
	// Tank temperature.
	private double temperature;

	// Timer to simulate the tank.
	private Timer tankTimer;

	// Cloud Connector logger.
	private final ConnectorLogger logger;

	// Random number generator for calculating the temperature.
	private final Random random = new Random();

	/**
	 * Simulated Tank class constructor. 
	 * <p>Instances a new {@code SimulatedTank} object to simulate a tank 
	 * behavior, where the valves can be managed through Device Cloud.</p>
	 *
	 * @param logger the logger to log messages, or {@code null} if there is no 
	 *		logger.
	 */
	public SimulatedTank(ConnectorLogger logger) {
		this.logger = logger;
		
		// Initialize tank system.
		volume = INIT_VOLUME;
		temperature = INIT_TEMP;
		valveIn = false;
		valveOut = false;
		
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ "New simulated tank, TOTAL VOLUME: " + MAX_VOLUME 
				+ " l, INITIAL VOLUME: " + INIT_VOLUME 
				+ " l, INPUT FLOW: " + (double)(INPUT_FLOW) * 1000 / REFRESH_TIMEOUT 
				+ " l/s, OUTPUT FLOW: " + (double)(OUTPUT_FLOW) * 1000 / REFRESH_TIMEOUT + " l/s");
	}

	/**
	 * Starts the tank simulation.
	 */
	public synchronized void startSimulation() {
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ "Starting tank simulation...");
		// Initialize timer.
		tankTimer = new Timer();
		tankTimer.schedule(new SimulationTask(), 0, REFRESH_TIMEOUT);
	}

	/**
	 * Stops the simulation.
	 */
	public synchronized void stopSimulation() {
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ "Stopping tank simulation...");
		if (tankTimer != null)
			tankTimer.cancel();
		tankTimer = null;
	}

	/**
	 * Retrieves whether the Valve-In is open.
	 * 
	 * @return {@code true} if the input valve is open, {@code false} otherwise.
	 */
	public boolean isValveInOpen() {
		return valveIn;
	}

	/**
	 * Retrieves whether the Valve-Out is open.
	 * 
	 * @return {@code true} if the output valve is open, {@code false} otherwise.
	 */
	public boolean isValveOutOpen() {
		return valveOut;
	}

	/**
	 * Retrieves the tank level (percentage).
	 * 
	 * @return the tank level (percentage).
	 */
	public int getLevel() {
		return calculateLevel(volume);
	}

	/**
	 * Returns the temperature of the tank (in Celsius degrees).
	 * 
	 * @return the temperature of the tank (in Celsius degrees).
	 */
	public double getTemperature() {
		return temperature;
	}

	/**
	 * Opens/closes the tank's Valve-in.
	 * 
	 * @param open {@code true} to open the input valve, {@code false} to close.
	 */
	public void openValveIn(boolean open) {
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ (open ? "Opening" : "Closing") + " valve IN.");
		this.valveIn = open;
	}

	/**
	 * Opens/closes the tank's Valve-Out.
	 * 
	 * @param open {@code true} to open the output valve, {@code false} to close.
	 */
	public void openValveOut(boolean open) {
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ (open ? "Opening" : "Closing") + " valve OUT.");
		this.valveOut = open;
	}

	/**
	 * Calculates the new temperature value (in Celsius degrees).
	 * The value is randomly generated by increasing or decreasing the previous
	 * value in a maximum value of 0.5 C.
	 * 
	 * @return the new temperature value in Celsius degrees.
	 * @see #MIN_TEMPERATURE
	 * @see #MAX_TEMPERATURE
	 */
	private double calculateTemperature() {
		// Modify temperature: -0.5 to +0.5 C.
		double newTemp = temperature + (random.nextInt(101) - 50) / 100.0;
		// Round the temperature to use only one decimal
		newTemp = Math.round(newTemp * 10.0) / 10.0;
		if (newTemp > MAX_TEMPERATURE)
			newTemp = MAX_TEMPERATURE;
		else if (newTemp < MIN_TEMPERATURE)
			newTemp = MIN_TEMPERATURE;
		return newTemp;
	}

	/**
	 * Updates the simulated tank status (volume, level and temperature).
	 */
	private void updateTankStatus() {
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ "Updating tank status...");
		
		// Calculate volume.
		if (valveIn) {
			volume += INPUT_FLOW;
			if (volume >= MAX_VOLUME)
				volume = MAX_VOLUME;
		}
		
		if (valveOut) {
			volume -= OUTPUT_FLOW;
			if (volume <= 0)
				volume = 0;
		}
		
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ "Volume: " + volume + " l");
		
		// Calculate the temperature value.
		temperature = calculateTemperature();
		
		Logger.logSystemDebug(logger, TankMonitor.LOGGER_PREFIX 
				+ "Temperature: " + temperature + " C");
	}

	/**
	 * Calculates the tank level depending on the volume (percentage value).
	 * 
	 * @param volume the tank volume
	 * @return the tank level in percentage.
	 */
	private int calculateLevel(int volume) {
		return (int) (volume * 100 / MAX_VOLUME);
	}

	/**
	 * Simulation task to be performed every {@value #REFRESH_TIMEOUT}.
	 * 
	 * The task will update the tank status depending on the input and output 
	 * valves and the calculated temperature.
	 */
	private class SimulationTask extends TimerTask {
		@Override
		public void run() {
			// Update system status.
			updateTankStatus();
		}
	}
}
