#
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# coding=utf-8

from __future__ import absolute_import
import re

import octoprint.plugin
import pigpio
try:
	import RPi.GPIO as GPIO
except (ImportError, RuntimeError):
	# RuntimeError gets thrown when you import RPi.GPIO on a non Raspberry Pi
	GPIO = None

phy_to_bcm = { 0:None, 1:None, 2:None, 3:2, 4:None, 5:3, 6:None, 7:4, 8:14,
			  9:None, 10:15, 11:17, 12:18, 13:27, 14:None, 15:22, 16:23,
			  17:None, 18:24, 19:10, 20:None, 21:9, 22:25, 23:11, 24:8, 25:None,
			  26:7, 27:0, 28:1, 29:5, 30:None, 31:6, 32:12, 33:13, 34:None,
			  35:19, 36:16, 37:26, 38:20, 39:None, 40:21 }

class PiGPIOpin(object):
	def __init__(self, pigpiod, pin, logger):
		self._pigpiod = pigpiod
		self._logger = logger

		# attempt to convert the physical pin to a bcm pin
		# how is this not in a library already?
		if phy_to_bcm.get(pin) is not None:
			self._pin = phy_to_bcm[pin]
		else:
			self._pin = pin
		self._logger.debug(u"PiGPIOpin: coverted pin: %r to %r" % (pin, self._pin))

		self._dutycycle = 0

	def start(self, dutycycle):
		self._dutycycle = dutycycle
		self._logger.debug(u"PiGPIOpin: start() pin: %s" % self._pin)
		if self._pigpiod.connected:
			self._pigpiod.set_PWM_range(self._pin, 100) # emulate RPi.GPIO
			self._pigpiod.set_PWM_dutycycle(self._pin, dutycycle)

	def stop(self):
		self._logger.debug(u"PiGPIOpin: stop() pin: %s" % self._pin)
		if self._pigpiod.connected:
			self._pigpiod.set_PWM_range(self._pin, 100) # emulate RPi.GPIO
			self._pigpiod.set_PWM_dutycycle(self._pin, 0)

	def ChangeDutyCycle(self, dutycycle):
		self._logger.debug(u"PiGPIOpin: ChangeDutyCycle() pin: %s" % self._pin)
		self.start(dutycycle)

class LEDStripControlPlugin(octoprint.plugin.AssetPlugin,
							octoprint.plugin.SettingsPlugin,
							octoprint.plugin.ShutdownPlugin,
							octoprint.plugin.StartupPlugin,
							octoprint.plugin.TemplatePlugin):

	def __init__(self):
		self._leds = dict(r=None, g=None, b=None, w=None)
		self._pigpiod = None

	def _setup_pin(self, pin):
		self._logger.debug(u"_setup_pin(%s)" % (pin,))
		if pin:
			p = None
			startup = 100.0 if self._settings.get_boolean(['on_startup']) else 0.0

			if self._pigpiod is None:
				self._pigpiod = pigpio.pi()

			if self._settings.get_boolean(['pigpiod']):
				if not self._pigpiod.connected:
					self._logger.error(u"Unable to communicate with PiGPIOd")
				else:
					p = PiGPIOpin(self._pigpiod, pin, self._logger)
			else:
				GPIO.setwarnings(False)
				GPIO.setmode(GPIO.BOARD)
				GPIO.setup(pin, GPIO.OUT)
				GPIO.output(pin, GPIO.HIGH)
				p = GPIO.PWM(pin, 100)
			p.start(startup)
			return p

	def _unregister_leds(self):
		self._logger.debug(u"_unregister_leds()")
		for i in ('r', 'g', 'b', 'w'):
			if self._leds[i]:
				self._leds[i].ChangeDutyCycle(0)
				self._leds[i].stop()

		if not self._settings.get_boolean(['pigpiod']) and GPIO:
			GPIO.cleanup()
		self._leds = dict(r=None, g=None, b=None)

	def _register_leds(self):
		self._logger.debug(u"_register_leds()")
		for i in ('r', 'g', 'b', 'w'):
			pin = self._settings.get_int([i])
			self._logger.debug(u"got pin(%s)" % (pin,))
			self._leds[i] = self._setup_pin(pin)

	def on_after_startup(self):
		self._logger.debug(u"LEDStripControl Startup")
		if GPIO:
			self._logger.debug(u"RPi.GPIO version %s" % (GPIO.VERSION,))

	def on_shutdown(self):
		self._logger.debug(u"LEDStripControl Shutdown")
		self._unregister_leds()
		self._pigpiod.stop()

	def HandleM150(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs):
		if gcode and cmd.startswith("M150"):
			self._logger.debug(u"M150 Detected: %s" % (cmd,))
			# Emulating Marlin 1.1.0's syntax
			# https://github.com/MarlinFirmware/Marlin/blob/RC/Marlin/Marlin_main.cpp#L6133
			dutycycles = {'r':0.0, 'g':0.0, 'b':0.0, 'w':0.0}
			for match in re.finditer(r'([RGUBWrgubw]) *(\d*)', cmd):
				k = match.group(1).lower()
				# Marlin uses RUB instead of RGB
				if k == 'u': k = 'g'
				try:
					v = float(match.group(2))
				except ValueError:
					# more than likely match.group(2) was unspecified
					v = 255.0
				v = v/255.0 * 100.0 # convert RGB to RPi dutycycle
				v = max(min(v, 100.0), 0.0) # clamp the value
				dutycycles[k] = v
				self._logger.debug(u"match 1: %s 2: %s" % (k, v))

			for l in dutycycles.keys():
				if self._leds[l]:
					self._leds[l].ChangeDutyCycle(dutycycles[l])

			return None,

	##~~ SettingsPlugin mixin

	def get_settings_version(self):
		return 2

	def get_template_configs(self):
		return [
			dict(type="settings", name="LED Strip Control", custom_bindings=False)
		]

	def get_settings_defaults(self):
		return dict(r=0, g=0, b=0, w=0, pigpiod=False, on_startup=True)

	def on_settings_initialized(self):
		self._logger.debug(u"LEDStripControl on_settings_load()")

		self._register_leds()

	def on_settings_save(self, data):
		self._logger.debug(u"LEDStripControl on_settings_save()")
		self._unregister_leds()
		# cast to proper types before saving
		for k in ('r', 'g', 'b', 'w'):
			if data.get(k): data[k] = max(0, int(data[k]))
		octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
		self._register_leds()

	def on_settings_migrate(self, target, current=None):
		self._logger.debug(u"LEDStripControl on_settings_migrate()")
		if current == 1:
			# add the 2 new values included
			self._settings.set(['w'], self.get_settings_defaults()["w"])
			self._settings.set(['on_startup'], self.get_settings_defaults()["on_startup"])

	##~~ Softwareupdate hook

	def get_update_information(self):
		return dict(
			ledstripcontrol=dict(
				displayName="LED Strip Control Plugin",
				displayVersion=self._plugin_version,

				# version check: github repository
				type="github_release",
				user="google",
				repo="OctoPrint-LEDStripControl",
				current=self._plugin_version,

				# update method: pip
				pip="https://github.com/google/OctoPrint-LEDStripControl/archive/{target_version}.zip"
			)
		)

__plugin_name__ = "LED Strip Control"
__plugin_pythoncompat__ = ">=2.7,<4"

def __plugin_load__():
	global __plugin_implementation__
	__plugin_implementation__ = LEDStripControlPlugin()

	global __plugin_hooks__
	__plugin_hooks__ = {
		"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
		"octoprint.comm.protocol.gcode.queuing": __plugin_implementation__.HandleM150
	}