/**
 * IOT Service
 */
package iot.basen;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import javax.persistence.EntityManager;
import iot.model.dto.OnOffCounter;
import iot.model.dto.SolarPanelTempTrend;
import iot.model.dto.SolarPanelTempTrendDTO;
import iot.model.entity.MeasuredValue;
import iot.model.entity.Relay;
import iot.model.entity.RelayState;
import iot.model.entity.Sensor;
import iot.model.service.MeasuredValueService;
import iot.model.service.RelayService;
import iot.model.service.RelayStateService;
import iot.model.service.SensorService;
import iot.model.service.ServiceFactory;
import util.DateUtil;
import util.common.MailUtil;

/**
 * Controls Relay that switch ON/OFF Basen pump to obtain maximum efficiency of Solar panels 
 * 
 * @author lvanek
 *
 */
public class BasenPumpControllerJob implements Runnable 
{
	private final DateTimeFormatter dateFormat =  DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
	private final SimpleDateFormat dateFormatOut = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss z", DateUtil.czechLocale);
	
	/**
	 * Value formatter
	 */
	private final DecimalFormat valueFormatter = new DecimalFormat("0.0");
		
	/**
	 * Code of relay controlling basen pump
	 */
	private final String codeRelayPump = "LV_RELAY_02";
	
	/**
	 * Code of thermometer - Basen water
	 */
	private final String codeTempWater = "LV_TEMP_08";
	
	/**
	 * Code of thermometer - Solar panel
	 */
	private final String codeTempSolarPanel = "LV_TEMP_11"; 
	
	/**
	 * Threshold for switching pump ON
	 */
	private final float temperatureThreshold = 6.0f;
	
	/**
	 * Maximum required temperature
	 */
	private final float temperatureRequired = 28.0f;
	
	/**
	 * Default constructor
	 */
	public BasenPumpControllerJob()
	{
	}
	
	/**
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() 
	{
		ZoneId zoneIdCET = ZoneId.of("CET");
		final LocalDateTime now = LocalDateTime.now(zoneIdCET);
		
		System.out.println("=========================================================");
		System.out.println("BasenPumpControllerJob trigged by scheduler " + dateFormat.format(now));
		
		try (ServiceFactory serviceFactory = ServiceFactory.createServiceFactory())
		{
			RelayService relayService = serviceFactory.getRelayService();
			RelayStateService relayStateService = serviceFactory.getRelayStateService();
			SensorService sensorService = serviceFactory.getSensorService();
			MeasuredValueService measuredValueService = serviceFactory.getMeasuredValueService();
			
			final Relay relay = relayService.getRelayByCode(codeRelayPump);
			
			if (relay != null)
			{
				if (relay.getDateTerminated() != null)
				{
					return; // Relay is not active, do nothing
				}
				
				RelayState relayState = relayStateService.getLastRelayState(relay);
					
				final LocalDateTime lastChange = LocalDateTime.ofInstant(relayState.getDateChange().toInstant(), ZoneId.of("CET"));
				
				Duration duration = Duration.between(lastChange, now);
				System.out.println("Relay " + codeRelayPump + " has last state: " + (relayState.getState() ? "ON" : "OFF") + ", last change before: " + String.valueOf(duration.getSeconds()) + " seconds");
				
				/*
				 * Basen pump control logic
				 */
				final EntityManager em = serviceFactory.getEntityManager();
				
				final Sensor sensorWaterTemp = sensorService.getSensorByCode(codeTempWater);
				final Sensor sensorSolarPanelTemp = sensorService.getSensorByCode(codeTempSolarPanel);
				
				final MeasuredValue waterTemp = measuredValueService.getLastMeasuredValue(sensorWaterTemp);
				final MeasuredValue solarPanelTemp = measuredValueService.getLastMeasuredValue(sensorSolarPanelTemp);
				
				System.out.println("Basen water: " + valueFormatter.format(waterTemp.getValue()) + ", Solar panel: " + valueFormatter.format(solarPanelTemp.getValue()));
				
				OnOffCounter counter = getOnOffCount(relay, relayStateService);
				//System.out.println(counter.toString());
				System.out.println("Today statistics - OFF: " + counter.getSecondsOFF() / 60 + " min, ON: " + counter.getSecondsON() / 60 + " min.");
				
				
				if (relayState.getState() && (duration.getSeconds() > 180)) // maximum pump running time: 3 min.
				{
					if ((now.getHour() >= 16) && (now.getHour() < 20) && counter.getSecondsON() < requiredCleaningTime(waterTemp.getValue()))
					{
						System.out.println("Leaving Pump ON to clean water");
						System.out.println("---------------------------------------------------------");
						return; // but if today's pump ON time is less than required, let it ON to clean water
					}

					if (solarPanelTemp.getValue() > (waterTemp.getValue() + temperatureThreshold))
					{
						System.out.println("Leaving Pump ON to cool Solar panel");
						System.out.println("---------------------------------------------------------");
						return; // Panel still so hot, continue with pumping
					}
		
					System.out.println("Switching relay " + codeRelayPump + " OFF ");
					
					em.getTransaction().begin();
					relayService.setState(relay, false); // Switch pump OFF
					em.getTransaction().commit();
				}
				else if ((relayState.getState() == false) && (duration.getSeconds() > 600)) // minimum pump delay time: 10 min.
				{
					SolarPanelTempTrendDTO trendDTO = analyzeSolarPanelTempTrend(sensorSolarPanelTemp, waterTemp, measuredValueService);
					System.out.println(trendDTO.getDescription());
					
					if ((trendDTO.getSolarPanelTempTrend() == SolarPanelTempTrend.TEMPERATURE_STAGNATE_BUT_HOT) && (waterTemp.getValue() < temperatureRequired))
					{
						String subject = "Switching pump ON for water heating";
						
						em.getTransaction().begin();
						relayService.setState(relay, true); // Switch pump ON
						em.getTransaction().commit();
						
						System.out.println(subject);
						MailUtil.sendMail(sensorSolarPanelTemp.getOwner().getMailRecipient(), subject, "Generated from IOT service.\r\n" + trendDTO.getDescription()); 
					}
					else if ((now.getHour() >= 16) && (now.getHour() < 20) && counter.getSecondsON() < requiredCleaningTime(waterTemp.getValue()))
					{
						String subject = "Switching pump ON for water cleaning";
						
						em.getTransaction().begin();
						relayService.setState(relay, true); // Switch pump ON
						em.getTransaction().commit();
						
						System.out.println(subject);
						MailUtil.sendMail(sensorSolarPanelTemp.getOwner().getMailRecipient(), subject, "Generated from IOT service.\r\n" + trendDTO.getDescription()); 
					}
				}
			}
		}
		catch (Exception e) 
		{
			System.err.println(e.getLocalizedMessage());
		}
		
		System.out.println("=========================================================");
	}
	
	/**
	 * Estimate daily water cleaning time
	 * 
	 * @param waterTemperature Temperature of water
	 * @return Cleaning time in seconds
	 */
	private long requiredCleaningTime(float waterTemperature)
	{
		if (waterTemperature < 20.0f)
		{
			return 60 * 15; // 15 min.
		}
		else if (waterTemperature < 25.0f)
		{
				return 60 * 30; // 30 min.
		}
		else if (waterTemperature < 28.0f)
		{
				return 60 * 60; // 1h
		}
		
		return 2 * 60 * 60; // 2 h
	}
	
	/**
	 * Analyze trends of temperatures to help to make decision if switch Pump ON
	 * 
	 * @param sensorSolarPanelTemp Solar panel temperature Sensor
	 * @param waterTemp Basen water temperature
	 * @param measuredValueService Service for class MeasuredValue
	 * @return Analyzed trend
	 */
	private SolarPanelTempTrendDTO analyzeSolarPanelTempTrend(Sensor sensorSolarPanelTemp, MeasuredValue waterTemp , MeasuredValueService measuredValueService)
	{
		ZoneId zoneIdCET = ZoneId.of("CET");
		final LocalDateTime now = LocalDateTime.now(zoneIdCET);
		final LocalDateTime before20mins = now.minusMinutes(20);
		
		final ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneIdCET);
		final Date dateFrom = Date.from(before20mins.toInstant(zonedDateTime.getOffset()));
		final Date dateTo = Date.from(now.toInstant(zonedDateTime.getOffset()));
					
		System.out.println(DateUtil.getDateAsFullUTCStringWithTZ(dateFrom, DateUtil.czechLocale) + " - " +  DateUtil.getDateAsFullUTCStringWithTZ(dateTo, DateUtil.czechLocale));
					
		List<MeasuredValue> measuredValues = measuredValueService.getMeasuredValues(sensorSolarPanelTemp, dateFrom, dateTo);
		
		StringBuilder description = new StringBuilder();
		description.append("-----------------------------------------------------------------\n");
		List<Float> changes = new ArrayList<>();
		Float previousTemp = null;
		for (MeasuredValue solarPanelTemp : measuredValues)
		{
			description.append("Solar panel: " + dateFormatOut.format(solarPanelTemp.getDateMeasured()) + " - " + valueFormatter.format(solarPanelTemp.getValue()) + " °C");
			
			if (previousTemp != null)
			{
				Float change = solarPanelTemp.getValue() - previousTemp;
				changes.add(change);
				description.append(" change: " + valueFormatter.format(change) + "\n");
			}
			else
			{
				description.append("\n");
			}
			
			previousTemp = solarPanelTemp.getValue();
		}
		
		// Last change become first, etc ... 
		Collections.reverse(changes);
		
		/*
		 *  Analyze changes to decide if switch pump ON
		 */
		description.append("-----------------------------------------------------------------\n");
		description.append("Water temperature " + valueFormatter.format(waterTemp.getValue()) + " °C\n");
		SolarPanelTempTrend trend = SolarPanelTempTrend.NO_DATA;
		
		if ((changes.size() >= 2) && 
				(changes.get(0) <= (changes.get(1) + 0.5)) &&
				((measuredValues.get(measuredValues.size() - 1).getValue()) > (waterTemp.getValue() + temperatureThreshold))
			   )
			{
				trend = SolarPanelTempTrend.TEMPERATURE_STAGNATE_BUT_HOT;
			}
			else if((changes.size() >= 2) && 
					(changes.get(0) < 0.0 && (changes.get(1) < 0.5))
					)
			{
				trend = SolarPanelTempTrend.TEMPERATURE_FALLS;
			}
			else if((changes.size() >= 2) && 
					(changes.get(0) > 0.0 && (changes.get(1) > 0.0)) &&
					(changes.get(0) >= (changes.get(1)))
					)
			{
				trend = SolarPanelTempTrend.TEMPERATURE_RISES;
			}
			else if(changes.size() >= 2)
			{
				trend = SolarPanelTempTrend.OTHER;
			}
		
		description.append("Trend is: " + trend.name() + "\n");
		
		
		return new SolarPanelTempTrendDTO(trend, description.toString());
	}
	
	/**
	 * Get today ON / OFF info
	 * 
	 * @param relay Relay
	 * @param relayStateService Service for class RelayState
	 * @return ON / OFF seconds info
	 */
	private OnOffCounter getOnOffCount(Relay relay, RelayStateService relayStateService)
	{
		OnOffCounter counter = new OnOffCounter();
		
		try 
		{
			ZoneId zoneIdCET = ZoneId.of("CET");
			final LocalDateTime now = LocalDateTime.now(zoneIdCET);
			final LocalDateTime dayStart = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
			
			final ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneIdCET);
			final Date dateFrom = Date.from(dayStart.toInstant(zonedDateTime.getOffset()));
			final Date dateTo = Date.from(now.toInstant(zonedDateTime.getOffset()));
						
			//System.out.println(DateUtil.getDateAsFullUTCStringWithTZ(dateFrom, DateUtil.czechLocale) + " - " +  DateUtil.getDateAsFullUTCStringWithTZ(dateTo, DateUtil.czechLocale));
		
			List<RelayState> relayStates = relayStateService.getRelayStates(relay, dateFrom, dateTo);
			
			
			LocalDateTime start = dayStart;
			RelayState relayStateLast = null;
			for (RelayState relayState : relayStates)
			{
				//System.out.println(dateFormatOut.format(relayState.getDateChange()) + " " + relayState.getState());
				
				LocalDateTime lastChange = LocalDateTime.ofInstant(relayState.getDateChange().toInstant(), zoneIdCET);
				
				Duration duration = Duration.between(start, lastChange);
				start = lastChange;
				relayStateLast = relayState;
				
				if (relayState.getState())
				{
					counter.addSecondsOFF(duration.getSeconds());
				}
				else
				{
					counter.addSecondsON(duration.getSeconds());
				}
			}
			
			if (relayStateLast != null)
			{
				LocalDateTime lastChange = LocalDateTime.ofInstant(relayStateLast.getDateChange().toInstant(), zoneIdCET);
				
				Duration duration = Duration.between(lastChange, now);
				
				if (relayStateLast.getState())
				{
					counter.addSecondsON(duration.getSeconds());
				}
				else
				{
					counter.addSecondsOFF(duration.getSeconds());
				}
			}
		} 
		catch (Exception e) 
		{
			System.err.println(e.getMessage());
		}
		
		return counter;
	}
}