package com.softwarepearls.apps.hardware.tinkerforge.waterpump;

import static com.softwarepearls.lego.constants.time.TimeConstants.MILLIS_IN_ONE_MINUTE;
import static com.softwarepearls.lego.constants.time.TimeConstants.MILLIS_IN_ONE_SECOND;
import static com.softwarepearls.lego.java.system.SystemProperties.*;
import static com.softwarepearls.lego.time.Frequency.EVERY_500_MS;
import static com.softwarepearls.lego.time.TimeAndDateKit.*;

import com.softwarepearls.apps.hardware.tinkerforge.waterpump.external.Pump;
import com.softwarepearls.apps.hardware.tinkerforge.waterpump.external.PumpState;
import com.softwarepearls.lego.hardware.tinkerforge.TinkerFactory;
import com.softwarepearls.lego.hardware.tinkerforge.TinkerFactory.ComponentType;
import com.softwarepearls.lego.hardware.tinkerforge.impl.emulator.Emulator;
import com.softwarepearls.lego.hardware.tinkerforge.impl.emulator.io.EmulatedLCD20x4;
import com.softwarepearls.lego.hardware.tinkerforge.impl.emulator.io.ui.resources.LCDBigDigits;
import com.softwarepearls.lego.hardware.tinkerforge.impl.emulator.io.ui.resources.LCDCustomChars;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.Device;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.input.DistanceIR;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.input.LinearPoti;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.input.Temperature;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.io.LCD20x4;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.output.DualRelay;
import com.softwarepearls.lego.hardware.tinkerforge.interfaces.relays.DualRelayOutput;
import com.softwarepearls.lego.image.ImageKit;
import com.softwarepearls.lego.time.Frequency;
import com.softwarepearls.lego.time.TimeAndDateKit;
import com.tinkerforge.BrickletLCD20x4.ButtonReleasedListener;
import com.tinkerforge.IPConnection;
import com.tinkerforge.TinkerforgeException;

import java.awt.Image;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;

/****************************************************************************
 * Cellar Water Pump Controller logic interacting with a TinkerForge stack
 * consisting of:
 * <UL>
 * <LI>1x Master Brick 2.0
 * <LI>1x Distance IR Sensor
 * <LI>1x Dual Relay
 * <LI>1x LCD 20x4 display
 * </UL>
 ****************************************************************************/
public final class CellarWaterPumpController {

	private static final Frequency POLLING_FREQUENCY = EVERY_500_MS;
	//
	private static final double PUMP_OFF_WATER_DISTANCE = 14.0; // in cm
	private static final double PUMP_ON_WATER_DISTANCE = 9.3;

	// private static final String TINKERFORGE_BRICKD_HOST = "localhost";
	private static final String TINKERFORGE_BRICKD_HOST = "192.168.1.5";
	private static final int TINKERFORGE_BRICKD_PORT = 4223;

	private static final TinkerFactory.ComponentType HARD_OR_SOFT = ComponentType.HARDWARE;

	private static final String UID_DUAL_RELAY = "c2J";
	private static final String UID_DISTANCE_IR = "9gh";
	private static final String UID_LCD_20x4 = "cZ1";
	private static final String UID_THERMOMETER = "bT3";
	private static final String UID_LINEAR_POTI = "li1";
	//
	private static final int BACKLIGHT_TOGGLE_SPEED = 12;

	private static final String WEB_PAGE_LCD_IMAGE_PATH = "/Volumes/ramdisk/images/LCD.png";
	private static final String PROGRAM_ICON_FILENAME = "resources/water_pump_icon.png";

	private static final long SAFETY_PUMP_OFF_DELAY = 45 * MILLIS_IN_ONE_SECOND;
	private static final long MINIMUM_TIME_BETWEEN_SWITCHES = 5 * MILLIS_IN_ONE_MINUTE;

	// the main actor
	private final Pump pump;

	private List<Device> tinkerForgeStack;

	// system input devices
	private DistanceIR distanceIR;
	private Temperature thermometer;
	private LinearPoti linearPoti;

	// system output devices
	private DualRelay dualRelay;
	private LCD20x4 lcd;

	// sensor values
	private double waterDistance;
	private double temperature;
	//
	private boolean runningNormally;
	private IPConnection ipConnection;
	//
	private int backlightToggle;
	protected short lastButtonPressed;
	private boolean showTime;
	//
	private final Emulator emulator;
	//
	private Timer safetyPumpOffTimer;
	private long switchedPumpOnAt;
	private long lastSafetyOffTime;

	/**
	 * Constructor.
	 * 
	 * @throws UnknownHostException
	 * @throws IOException
	 * @throws AlreadyConnectedException
	 */
	private CellarWaterPumpController() throws IOException, TinkerforgeException {

		System.out.println("Starting Cellar Water Pump Control System @ " + new Date());
		System.out.println("Running on Java " + getJavaVersion() + " hosted by " + getOperatingSystemName() + " " + getOperatingSystemVersion());

		ensureTinkerForgeStackResetsOnJavaShutdown();

		emulator = Emulator.getSingleton();

		// final Image programIcon =
		// ImageKit.loadImageRelativeToClass(CellarWaterPumpController.class,
		// PROGRAM_ICON_FILENAME);
		// emulator.setWindowIcon(programIcon);

		initialiseTinkerForgeStack();

		pump = new Pump();

		pump.setConnectingRelay(dualRelay, DualRelayOutput.ONE);
	}

	private void initialiseTinkerForgeStack() throws TinkerforgeException, IOException {

		System.out.print("Initializing TinkerForge stack..");

		final TinkerFactory tinkerFactory = TinkerFactory.getSingleton();

		tinkerFactory.make(HARD_OR_SOFT);

		if (HARD_OR_SOFT == ComponentType.HARDWARE) {
			ipConnection = new IPConnection();
			ipConnection.connect(TINKERFORGE_BRICKD_HOST, TINKERFORGE_BRICKD_PORT);
			tinkerFactory.use(ipConnection);
		}

		dualRelay = tinkerFactory.createDualRelay(UID_DUAL_RELAY);
		thermometer = tinkerFactory.createTemperature(UID_THERMOMETER);
		distanceIR = tinkerFactory.createDistanceIR(UID_DISTANCE_IR);
		// linearPoti = tinkerFactory.createLinearPoti(UID_LINEAR_POTI);
		lcd = tinkerFactory.createLCD20x4(UID_LCD_20x4, TinkerFactory.ComponentType.SOFTWARE);

		emulator.layoutComponentsOnDesktop();

		lastButtonPressed = -1;
		lcd.addButtonReleasedListener(new ButtonReleasedListener() {

			@Override
			public void buttonReleased(final short button) {

				lastButtonPressed = button;
			}
		});

		initLcdCustomChars();

		tinkerForgeStack = new ArrayList<Device>();
		tinkerForgeStack.add(distanceIR);
		tinkerForgeStack.add(dualRelay);
		tinkerForgeStack.add(lcd);

		System.out.println(" Done.");
	}

	private void start() {

		runningNormally = true;

		while (runningNormally) {
			try {
				controlLogic();
			}
			catch (final TinkerforgeException e) {
				log("Ooops: " + e.getMessage());
			}
		}
	}

	private void controlLogic() throws TinkerforgeException {

		lcd.backlightOn();

		runningNormally = true;

		while (runningNormally) {

			dealWithWaterLevelAndPump();

			updateLCD();

			wakeup(POLLING_FREQUENCY);
		}
	}

	private void dealWithWaterLevelAndPump() throws TinkerforgeException {

		log("Polling sensors... ");

		waterDistance = ((double) distanceIR.getDistance()) / 10; // convert mm
																	// to cm
		temperature = ((double) thermometer.getTemperature()) / 100;
		log("Water Distance = " + waterDistance + " cm\n");

		if ((waterDistance < PUMP_ON_WATER_DISTANCE) && (System.currentTimeMillis() - lastSafetyOffTime > MINIMUM_TIME_BETWEEN_SWITCHES)) {

			log("Water level is high.\n");
			if (pump.getPumpState() == PumpState.OFF) {
				switchPump(PumpState.ON);
			}
		}

		if (waterDistance > PUMP_OFF_WATER_DISTANCE) {
			log("Water level is low.\n");
			if (pump.getPumpState() == PumpState.ON) {
				switchPump(PumpState.OFF);
			}
		}
	}

	private void updateLCD() throws TinkerforgeException {

		// if (showTime) {
		// showTime();
		// } else {
		showStatus();
		// }
		// if (lastButtonPressed != -1) {
		// lcd.print('*', 19, lastButtonPressed);
		// }
		//
		// toggleDisplayBetweenTimeAndStatus();

		updateLCDImageForWebPage();
	}

	private void updateLCDImageForWebPage() {

		final Image lcdImage = ((EmulatedLCD20x4) lcd).getLCDImage();
		final File imageFile = new File(WEB_PAGE_LCD_IMAGE_PATH);
		try {
			ImageKit.saveImage((RenderedImage) lcdImage, imageFile);
		}
		catch (final IOException e) {
			e.printStackTrace();
		}
	}

	private void toggleDisplayBetweenTimeAndStatus() {

		backlightToggle++;
		if (backlightToggle % BACKLIGHT_TOGGLE_SPEED == 0) {
			if (((backlightToggle / BACKLIGHT_TOGGLE_SPEED) & 1) == 0) {
				// lcd.backlightOn();
				showTime = false;
			} else {
				// lcd.backlightOff();
				showTime = true;
			}
		}
	}

	private void showStatus() throws TinkerforgeException {

		lcd.clearDisplay();
		lcd.home();

		lcd.println("Time: " + String.format("%tT", new Date()) + " " + currentDay() + currentMonth().substring(0, 3));

		lcd.setCursor(0, 1);
		lcd.println("H2O : " + waterDistance);
		lcd.println("Pump: " + pump.getPumpState());
		lcd.println("Temp: " + temperature);

		if (temperature < 0) {
			lcd.println("COLD", 15, 3);
		}
	}

	private void switchPump(final PumpState state) throws TinkerforgeException {

		if (state == PumpState.ON) {

			switchedPumpOnAt = System.currentTimeMillis();
			System.out.print("Switching pump " + state + " @ " + new Date() + ". ");

			startSafetyOffTimer();
		}

		if (state == PumpState.OFF) {

			if (safetyPumpOffTimer != null) {
				safetyPumpOffTimer.cancel();
			}

			final long elapsed = System.currentTimeMillis() - switchedPumpOnAt;
			System.out.println("Duration: " + TimeAndDateKit.toHoursMinutesSecondsString(elapsed));
		}

		pump.setPumpState(state);
	}

	private void startSafetyOffTimer() {

		final TimerTask safetyOffTimerTask = new TimerTask() {

			@Override
			public void run() {

				while (pump.getPumpState() == PumpState.ON) {
					try {
						pump.setPumpState(PumpState.OFF);
						lastSafetyOffTime = System.currentTimeMillis();
						break;
					}
					catch (final TinkerforgeException e) {
						e.printStackTrace();
					}
					wakeup(POLLING_FREQUENCY);
				}

				System.out.print("Safety OFF override! ");
				final long elapsed = System.currentTimeMillis() - switchedPumpOnAt;
				System.out.println("Duration: " + TimeAndDateKit.toHoursMinutesSecondsString(elapsed));
			}
		};

		safetyPumpOffTimer = new Timer();
		safetyPumpOffTimer.schedule(safetyOffTimerTask, SAFETY_PUMP_OFF_DELAY);
	}

	private void showTime() throws TinkerforgeException {

		lcd.clearDisplay();

		final int[][] currentTimeCharCodes = LCDBigDigits.renderCodesForCurrentTime();

		for (int lineNr = 0; lineNr < 4; lineNr++) {
			final StringBuilder sb = new StringBuilder(20);
			for (int x = 0; x < 16; x++) {
				char ch = (char) (currentTimeCharCodes[lineNr][x]);
				if (ch == 0) {
					ch = ' ';
				} else {
					ch += 7;
				}
				sb.append(ch);

				// add blinking : every second.
				if (x == 8) {
					if ((lineNr == 1) || (lineNr == 2)) {
						if (((System.currentTimeMillis() / 1000) & 1) == 1) {
							sb.append(" * ");
						} else {
							sb.append("   ");
						}
					} else {
						sb.append("   ");
					}
				}
			}
			lcd.println(sb.toString(), 0, lineNr);
		}
	}

	private void initLcdCustomChars() throws TinkerforgeException {

		for (int i = 0; i < LCDCustomChars.NUM_SPECIAL_CHARS; i++) {

			lcd.setCustomCharacter((short) i, LCDCustomChars.getCustomCharBitmapForCode(i));
		}
	}

	private void ensureTinkerForgeStackResetsOnJavaShutdown() {

		Runtime.getRuntime().addShutdownHook(new Thread() {

			@Override
			public void run() {

				reset();
			}
		});
	}

	private void reset() {

		if (tinkerForgeStack != null) {
			for (final Device device : tinkerForgeStack) {
				try {
					device.reset();
				}
				catch (final TinkerforgeException e) {
					e.printStackTrace();
				}
			}
		}
	}

	private void log(final String msg) {

		// System.out.print(new Date() + ": " + msg);
	}

	public static void main(final String[] args) throws IOException, TinkerforgeException {

		final CellarWaterPumpController waterPumpController = new CellarWaterPumpController();

		waterPumpController.start();
	}
}
