#!/usr/bin/python -u
# coding=utf-8

import logging
import os
import time
import states as State
import target_type as TargetType

from random import randint
from python_libs.ie_dbus.dbus_service import DBusService
from python_libs.ie_utils.main_loop import run_on_main_loop

# noinspection PyUnreachableCode
if False:
	from typing import NoReturn, Optional, Any, Iterable, List

logging.basicConfig(level=logging.INFO)
_log = logging.getLogger(__name__)

VERSION = '1.0.0'
PRODUCT = 'Controller'

GRID_SERVICE_PREFIX = 'com.victronenergy.grid.'
BATTERY_SERVICE_PREFIX = 'com.victronenergy.battery.'
INVERTER_SERVICE_PREFIX = 'com.victronenergy.vebus.'
SYSTEM_SERVICE_PREFIX = 'com.victronenergy.system'
HUB4_SERVICE_PREFIX = 'com.victronenergy.hub4'
SETTINGS_SERVICE_PREFIX = 'com.victronenergy.settings'

UPDATE_PERIOD_MS = 2000
MAX_POWER_PER_BATTERY = 2500

MAX_DAYS_WITHOUT_EOC = 7
SECONDS_PER_DAY = 24 * 60 * 60

GRID_SET_POINT_SETTING = PRODUCT + '/GridSetPoint'
LAST_EOC_SETTING = PRODUCT + '/LastEOC'
CALIBRATION_CHARGE_START_TIME_OF_DAY_SETTING = PRODUCT + '/CalibrationChargeStartTime'

HEAT_LOSS = 150  # W
P_CONST = 0.5   # W/W

Epoch = int
Seconds = int


def time_now():
	return int(time.time())


class Controller(object):

	def __init__(self, measurement, target, target_type, state):
		# type: (float, float, int, int) -> NoReturn
		self.target_type = target_type
		self.target = target
		self.measurement = measurement
		self.state = state

		d_p = target - measurement
		self.delta = d_p * P_CONST

	@staticmethod
	def min(controllers):
		# type: (Iterable[Controller]) -> Controller
		return min(controllers, key=lambda c: c.delta)

	@staticmethod
	def max(controllers):
		# type: (Iterable[Controller]) -> Controller
		return max(controllers, key=lambda c: c.delta)

	def clamp(self, lower_limit_controllers, upper_limit_controllers):
		# type: (List[Controller],List[Controller]) -> Controller
		c_min = Controller.min(upper_limit_controllers + [self])
		return Controller.max(lower_limit_controllers + [c_min])


# noinspection PyMethodMayBeStatic
class InnovEnergyController(DBusService):

	def __init__(self):

		super(InnovEnergyController, self).__init__(PRODUCT.lower())

		self.settings.add_setting(path=LAST_EOC_SETTING, default_value=0)        # unix epoch timestamp
		self.settings.add_setting(path=GRID_SET_POINT_SETTING, default_value=0)  # grid setpoint, Watts

		self.settings.add_setting(path=CALIBRATION_CHARGE_START_TIME_OF_DAY_SETTING, default_value=32400)  # 09:00

		self.own_properties.set('/ProductName',         	PRODUCT)
		self.own_properties.set('/Mgmt/ProcessName',    	__file__)
		self.own_properties.set('/Mgmt/ProcessVersion', 	VERSION)
		self.own_properties.set('/Mgmt/Connection',     	'dbus')
		self.own_properties.set('/ProductId',           	PRODUCT)
		self.own_properties.set('/FirmwareVersion',     	VERSION)
		self.own_properties.set('/HardwareVersion',     	VERSION)
		self.own_properties.set('/Connected',           	1)
		self.own_properties.set('/TimeToCalibrationCharge', 'unknown')
		self.own_properties.set('/State',                   0)

		self.phases = [
			p for p in ['/Hub4/L1/AcPowerSetpoint', '/Hub4/L2/AcPowerSetpoint', '/Hub4/L3/AcPowerSetpoint']
			if self.remote_properties.exists(self.inverter_service + p)
		]

		self.n_phases = len(self.phases)
		print ('The system has ' + str(self.n_phases) + ' phase' + ('s' if self.n_phases != 1 else ''))

		self.max_inverter_power = 32700
		# ^ defined in https://github.com/victronenergy/dbus_modbustcp/blob/master/CCGX-Modbus-TCP-register-list.xlsx

	def clamp_power_command(self, value):
		# type: (float) -> int

		value = max(value, -self.max_inverter_power)
		value = min(value, self.max_inverter_power)

		return int(value)

	def get_service(self, prefix):
		# type: (str) -> Optional[unicode]
		service = next((s for s in self.available_services if s.startswith(prefix)), None)

		if service is None:
			raise Exception('no service matching ' + prefix + '* available')

		return service

	def is_service_available(self, prefix):
		# type: (str) -> bool
		return next((True for s in self.available_services if s.startswith(prefix)), False)

	@property
	def battery_service(self):
		# type: () -> Optional[unicode]
		return self.get_service(BATTERY_SERVICE_PREFIX)

	@property
	def battery_available(self):
		# type: () -> bool
		return self.is_service_available(BATTERY_SERVICE_PREFIX)

	@property
	def grid_service(self):
		# type: () -> Optional[unicode]
		return self.get_service(GRID_SERVICE_PREFIX)

	@property
	def grid_meter_available(self):
		# type: () -> bool
		return self.is_service_available(GRID_SERVICE_PREFIX)

	@property
	def inverter_service(self):
		# type: () -> Optional[unicode]
		return self.get_service(INVERTER_SERVICE_PREFIX)

	@property
	def inverter_available(self):
		# type: () -> bool
		return self.is_service_available(INVERTER_SERVICE_PREFIX)
		
	@property
	def system_service(self):
		# type: () -> Optional[unicode]
		return self.get_service(SYSTEM_SERVICE_PREFIX)

	@property
	def system_service_available(self):
		# type: () -> bool
		return self.is_service_available(SYSTEM_SERVICE_PREFIX)

	@property
	def hub4_service(self):
		# type: () -> Optional[unicode]
		return self.get_service(HUB4_SERVICE_PREFIX)

	@property
	def hub4_service_available(self):
		# type: () -> bool
		return self.is_service_available(HUB4_SERVICE_PREFIX)

	@property
	def inverter_power_setpoint(self):
		# type: () -> float
		return sum((self.get_inverter_prop(p) for p in self.phases))

	def get_battery_prop(self, dbus_path):
		# type: (str) -> Any
		battery_service = self.battery_service
		return self.remote_properties.get(battery_service + dbus_path).value

	def get_grid_prop(self, dbus_path):
		# type: (str) -> Any
		return self.remote_properties.get(self.grid_service + dbus_path).value

	def get_inverter_prop(self, dbus_path):
		# type: (str) -> Any
		return self.remote_properties.get(self.inverter_service + dbus_path).value
		
	def get_system_prop(self, dbus_path):
		# type: (str) -> Any
		system_service = self.system_service
		return self.remote_properties.get(system_service + dbus_path).value

	def get_hub4_prop(self, dbus_path):
		# type: (str) -> Any
		hub4_service = self.hub4_service
		return self.remote_properties.get(hub4_service + dbus_path).value

	def set_settings_prop(self, dbus_path, value):
		# type: (str, Any) -> bool
		return self.remote_properties.set(SETTINGS_SERVICE_PREFIX + dbus_path, value)

	def set_inverter_prop(self, dbus_path, value):
		# type: (str, Any) -> bool
		inverter_service = self.inverter_service
		return self.remote_properties.set(inverter_service + dbus_path, value)

	@property
	def max_battery_charge_power(self):
		# type: () -> int
		return self.get_battery_prop('/Info/MaxChargePower')

	@property
	def max_battery_discharge_power(self):
		# type: () -> int
		return self.get_battery_prop('/Info/MaxDischargePower')

	@property
	def max_configured_charge_power(self):
		# type: () -> Optional[int]
		max_power = self.settings.get('/Settings/CGwacs/MaxChargePower')
		return max_power if max_power >= 0 else None

	@property
	def max_configured_discharge_power(self):  # unsigned
		# type: () -> Optional[int]
		max_power = self.settings.get('/Settings/CGwacs/MaxDischargePower')
		return max_power if max_power >= 0 else None  

	@property
	def max_charge_power(self):
		# type: () -> int
		if self.max_configured_charge_power is None:
			return self.max_battery_charge_power
		else:
			return min(self.max_battery_charge_power, self.max_configured_charge_power)

	@property
	def max_discharge_power(self):   # unsigned
		# type: () -> int
		if self.max_configured_discharge_power is None:
			return self.max_battery_discharge_power
		else:
			return min(self.max_battery_discharge_power, self.max_configured_discharge_power)

	def set_inverter_power_setpoint(self, power):
		# type: (float) -> NoReturn

		if self.settings.get('/Settings/CGwacs/BatteryLife/State') == 9:
			self.settings.set('/Settings/CGwacs/BatteryLife/State', 0)  # enables scheduled charge
		self.settings.set('/Settings/CGwacs/Hub4Mode', 3)  # disable hub4
		self.set_inverter_prop('/Hub4/DisableCharge', 0)
		self.set_inverter_prop('/Hub4/DisableFeedIn', 0)

		power = self.clamp_power_command(power / self.n_phases)
		for p in self.phases:
			self.set_inverter_prop(p, power + randint(-1, 1))  # use randint to force dbus re-send

	def set_controller_state(self, state):
		# type: (int) -> NoReturn
		self.own_properties.set('/State', state)

	@property
	def grid_power(self):
		# type: () -> Optional[float]
		try:
			return self.get_grid_prop('/Ac/Power')
		except:
			return None

	@property
	def battery_cold(self):
		# type: () -> bool
		return self.get_battery_prop('/IoStatus/BatteryCold') == 1

	@property
	def eoc_reached(self):
		# type: () -> bool
		if not self.battery_available:
			return False

		return min(self.get_battery_prop('/EOCReached')) == 1

	@property
	def battery_power(self):
		# type: () -> float
		return self.get_battery_prop('/Dc/0/Power')

	@property
	def inverter_ac_in_power(self):
		# type: () -> float
		return self.get_inverter_prop('/Ac/ActiveIn/P')

	@property
	def inverter_ac_out_power(self):
		# type: () -> float
		return self.get_inverter_prop('/Ac/Out/P')

	@property
	def soc(self):
		# type: () -> float
		return self.get_battery_prop('/Soc')

	@property
	def n_batteries(self):
		# type: () -> int
		return self.get_battery_prop('/NbOfBatteries')

	@property
	def min_soc(self):
		# type: () -> float
		return self.settings.get('/Settings/CGwacs/BatteryLife/MinimumSocLimit')

	@property
	def should_hold_min_soc(self):
		# type: () -> bool
		return self.min_soc <= self.soc <= self.min_soc + 5

	@property
	def utc_offset(self):
		# type: () -> int

		# stackoverflow.com/a/1301528
		# stackoverflow.com/a/3168394

		os.environ['TZ'] = self.settings.get('/Settings/System/TimeZone')
		time.tzset()
		is_dst = time.daylight and time.localtime().tm_isdst > 0
		return -(time.altzone if is_dst else time.timezone)

	@property
	def grid_set_point(self):
		# type: () -> float
		return self.settings.get('/Settings/CGwacs/AcPowerSetPoint')

	@property
	def time_to_calibration_charge_str(self):
		# type: () -> str
		return self.own_properties.get('/TimeToCalibrationCharge').text
	
	@property
	def calibration_charge_deadline(self):
		# type: () -> Epoch

		utc_offset = self.utc_offset
		ultimate_deadline = self.settings.get(LAST_EOC_SETTING) + MAX_DAYS_WITHOUT_EOC * SECONDS_PER_DAY
		midnight_before_udl = int((ultimate_deadline + utc_offset) / SECONDS_PER_DAY) * SECONDS_PER_DAY - utc_offset  # round off to last midnight

		dead_line = midnight_before_udl + self.calibration_charge_start_time_of_day

		while dead_line > ultimate_deadline:   # should fire at most once, but let's be defensive...
			dead_line -= SECONDS_PER_DAY   # too late, advance one day

		return dead_line

	@property
	def time_to_calibration_charge(self):
		# type: () -> Seconds
		return self.calibration_charge_deadline - time_now()

	@property
	def grid_blackout(self):
		# type: () -> bool
		return self.get_inverter_prop('/Leds/Mains') < 1

	@property
	def scheduled_charge(self):
		# type: () -> bool
		return self.get_hub4_prop('/Overrides/ForceCharge') != 0

	@property
	def calibration_charge_start_time_of_day(self):
		# type: () -> Seconds
		return self.settings.get(CALIBRATION_CHARGE_START_TIME_OF_DAY_SETTING)   # seconds since midnight

	@property
	def must_do_calibration_charge(self):
		# type: () -> bool
		return self.time_to_calibration_charge <= 0

	def controller_charge_to_min_soc(self):
		# type: () -> Controller

		return Controller(
			measurement=self.battery_power,
			target=self.max_charge_power,
			target_type=TargetType.BATTERY_DC,
			state=State.CHARGE_TO_MIN_SOC
		)

	def controller_hold_min_soc(self):
		# type: () -> Controller

		# TODO: explain

		a = -4 * HEAT_LOSS * self.n_batteries
		b = -a * (self.min_soc + .5)

		target_dc_power = a * self.soc + b

		return Controller(
			measurement = self.battery_power,
			target      = target_dc_power,
			target_type = TargetType.BATTERY_DC,
			state       = State.HOLD_MIN_SOC
		)

	def controller_calibration_charge(self):
		# type: () -> Controller

		return Controller(
			measurement = self.battery_power,
			target      = self.max_charge_power,
			target_type = TargetType.BATTERY_DC,
			state       = State.CALIBRATION_CHARGE
		)

	def controller_limit_discharge_power(self):  # signed
		# type: () -> Controller

		return Controller(
			measurement = self.battery_power,
			target      = -self.max_discharge_power,  # add sign!
			target_type = TargetType.BATTERY_DC,
			state       = State.LIMIT_DISCHARGE_POWER
		)

	def controller_limit_charge_power(self):
		# type: () -> Controller
		return Controller(
			measurement = self.battery_power,
			target      = self.max_charge_power,
			target_type = TargetType.BATTERY_DC,
			state       = State.LIMIT_CHARGE_POWER
		)

	def controller_optimize_self_consumption(self):
		# type: () -> Controller

		return Controller(
			measurement = self.grid_power,
			target      = self.grid_set_point,
			target_type = TargetType.GRID_AC,
			state       = State.OPTIMIZE_SELF_CONSUMPTION
		)

	def controller_heating(self):
		# type: () -> Controller

		return Controller(
			measurement = self.battery_power,
			target      = self.max_charge_power,
			target_type = TargetType.BATTERY_DC,
			state       = State.HEATING
		)

	def controller_scheduled_charge(self):
		# type: () -> Controller

		return Controller(
			measurement = self.battery_power,
			target      = self.max_charge_power,
			target_type = TargetType.BATTERY_DC,
			state       = State.SCHEDULED_CHARGE
		)

	def controller_no_grid_meter(self):
		# type: () -> Controller

		return Controller(
			measurement = self.battery_power,
			target      = self.max_charge_power,
			target_type = TargetType.BATTERY_DC,
			state       = State.NO_GRID_METER_AVAILABLE
		)

	def controller_no_battery(self):
		# type: () -> Controller

		return Controller(
			measurement = self.inverter_ac_in_power,
			target      = 0,
			target_type = TargetType.INVERTER_AC_IN,
			state       = State.NO_BATTERY_AVAILABLE
		)

	def controller_bridge_grid_blackout(self):
		# type: () -> Controller

		return Controller(
			measurement = 0,
			target      = 0,
			target_type = TargetType.GRID_AC,
			state       = State.BRIDGE_GRID_BLACKOUT
		)

	def update_eoc(self):

		if self.eoc_reached:
			print('battery has reached EOC')
			self.settings.set(LAST_EOC_SETTING, time_now())

		self.publish_time_to_calibration_charge()

	def publish_time_to_calibration_charge(self):

		total_seconds = self.time_to_calibration_charge

		if total_seconds <= 0:
			time_to_eoc_str = 'now'
		else:
			total_minutes, seconds = divmod(total_seconds, 60)
			total_hours, minutes   = divmod(total_minutes, 60)
			total_days, hours      = divmod(total_hours, 24)

			days_str    = (str(total_days) + 'd') if total_days  > 0 else ''
			hours_str   = (str(hours)      + 'h') if total_hours > 0 else ''
			minutes_str = (str(minutes)    + 'm') if total_days == 0 else ''

			time_to_eoc_str = "{0} {1} {2}".format(days_str, hours_str, minutes_str).strip()

		self.own_properties.set('/TimeToCalibrationCharge', time_to_eoc_str)

	def print_system_stats(self, controller):
		# type: (Controller) -> NoReturn

		def soc_setpoint():
			if controller.state == State.CALIBRATION_CHARGE or controller.state == State.NO_GRID_METER_AVAILABLE:
				return ' => 100%'
			if controller.state == State.CHARGE_TO_MIN_SOC:
				return ' => ' + str(int(self.min_soc)) + '%'
			return ''

		def setpoint(target_type):
			if target_type != controller.target_type:
				return ''
			return ' => ' + str(int(controller.target)) + 'W'

		def p(power):
			# type: (Optional[float]) -> str
			if power is None:
				return '   --- W'
			else:
				return str(int(power)) + 'W'

		ac_loads = None if self.grid_power is None else self.grid_power - self.inverter_ac_in_power
		delta = p(controller.delta) if controller.delta < 0 else '+' + p(controller.delta)
		battery_power = self.battery_power if self.battery_available else None
		soc_ = str(self.soc) + '%' if self.battery_available else '---'

		print (State.name_of[controller.state])
		print ('')
		print ('time to CC: ' + self.time_to_calibration_charge_str)
		print ('       SOC: ' + soc_ + soc_setpoint())
		print ('      grid: ' + p(self.grid_power) + setpoint(TargetType.GRID_AC))
		print ('   battery: ' + p(battery_power) + setpoint(TargetType.BATTERY_DC))
		print ('     AC in: ' + p(self.inverter_ac_in_power) + '  ' + delta)
		print ('    AC out: ' + p(self.inverter_ac_out_power))
		print ('  AC loads: ' + p(ac_loads))

	def choose_controller(self):
		# type: () -> Controller

		if self.grid_blackout:
			return self.controller_bridge_grid_blackout()

		if not self.battery_available:
			return self.controller_no_battery()

		if self.battery_cold:
			return self.controller_heating()

		if self.scheduled_charge:
			return self.controller_scheduled_charge()

		if self.must_do_calibration_charge:
			return self.controller_calibration_charge()

		if self.soc < self.min_soc:
			return self.controller_charge_to_min_soc()

		if not self.grid_meter_available:
			return self.controller_no_grid_meter()

		hold_min_soc = self.controller_hold_min_soc()
		limit_discharge_power = self.controller_limit_discharge_power()  # signed

		lower_limit = [limit_discharge_power, hold_min_soc]

		# No upper limit. We no longer actively limit charge power. DC/DC Charger inside the BMS will do that for us.
		upper_limit = []

		optimize_self_consumption = self.controller_optimize_self_consumption()

		return optimize_self_consumption.clamp(lower_limit, upper_limit)

	def update(self):

		print('iteration started\n')

		self.update_eoc()

		if self.inverter_available:

			controller = self.choose_controller()
			power = self.inverter_ac_in_power + controller.delta

			self.set_inverter_power_setpoint(power)
			self.set_controller_state(controller.state)
			self.print_system_stats(controller)  # for debug

		else:
			self.set_controller_state(State.NO_INVERTER_AVAILABLE)
			print('inverter not available!')

		print('\niteration finished\n')


def main():

	print('starting ' + __file__)

	with InnovEnergyController() as service:
		run_on_main_loop(service.update, UPDATE_PERIOD_MS)

	print(__file__ + ' has shut down')


if __name__ == '__main__':
	main()