# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.

# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import logging
import threading
import time

import config


class BaseGPIOInterface:

    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.info("GPIO interface check...")
        self.enabled = False
        self.loop_thread = None
        if config.get_settings().get('gpio', {}).get('enabled'):
            self.init()

    def init(self):
        self.enabled = False
        self.logger.info("Enabling GPIO interface")
        self.active_buttons = []
        self.cancel_callbacks = []
        # TODO validate
        self.pull = config.get_settings().get('gpio', {}).get('pull')
        self.bouncetime = config.get_settings().get('gpio', {}).get('bouncetime', 200)
        self.trigger = config.get_settings().get('gpio', {}).get('trigger')
        self.in_pins = {}
        self.out_pins = {}
        if self.setup_gpio(self.pull, self.trigger, self.bouncetime):
            buttons = config.get_settings()['gpio'].get('buttons', {})
            for action_name, pin_number in buttons.items():
                handle = self.setup_input(pin_number, self.pull)
                if handle:
                    self.in_pins[pin_number] = {'action': action_name, 'handle': handle}
                    self.logger.info("GPIO event registered: pin%d=%s" % (pin_number, action_name))
            leds = config.get_settings()['gpio'].get('leds', {})
            invert_leds = config.get_settings()['gpio'].get('invert_leds', False)
            for event, pin_number in leds.items():
                event = str(event)
                # validation
                try:
                    pin_number = int(pin_number)
                except:
                    self.logger.error('Invalid GPIO event pin number: ' + str(pin_number))
                    continue
                #self.out_pins[pin_number] = {'event': event, 'handle': self.setup_output(pin_number), 'invert': invert, 'only_for': only_for}
                handle = self.setup_output(pin_number)
                if handle:
                    self.out_pins[pin_number] = {'event': event, 'handle': handle, 'invert': invert_leds}
                    self.logger.info("GPIO LED registered: pin%d=%s" % (pin_number, event))
                else:
                    self.logger.warning(f'Error setting up LED {event} : {pin_number}')
            self.enabled = True
            self.start_loop_thread()
            self.logger.info("GPIO interface init success")

    def setup_gpio(self, pull_str=None, trigger_str=None, _=None):
        return False

    def setup_input(self, pin_number, pull):
        pass

    def setup_output(self, pin_number):
        pass

    def read_level(self, pin_number):
        pass
        
    def write_level(self, pin_number, level):
        pass
    
    def start_loop_thread(self):
        if self.loop_thread and self.loop_thread.is_alive():
            self.enabled = False
            self.loop_thread.join()
            self.enabled = True
        self.loop_thread = threading.Thread(target=self.loop)
        self.loop_thread.start()

    def loop(self):
        self.prev_levels = {}
        self.event_detected = {}
        while self.enabled:
            for pin in self.in_pins:
                try:
                    level = self.read_level(pin)
                except RuntimeError:
                    self.logger.warning("GPIO error on reading input of pin: %d" % pin)
                else:
                    if pin in self.prev_levels:
                        prev_level = self.prev_levels[pin]
                        if self.trigger == 'falling' or self.trigger == 'both':
                            if not level and prev_level:
                                self.event_detected[pin] = True
                        if self.trigger == 'rising' or self.trigger == 'both':
                            if level and not prev_level:
                                self.event_detected[pin] = True
                    else:
                        self.prev_levels[pin] = level
            time.sleep(self.bouncetime/1000)

    def check_events(self, printer_interfaces):
        if self.enabled:
            for pin_num in self.in_pins:
                action_name = self.in_pins[pin_num]['action']
                if self.event_detected.get(pin_num):
                    self.event_detected[pin_num] = False
                    for pi in printer_interfaces:
                        self.run_method(pi, action_name)

    def switch_led(self, event, state, caller_id=None):
        if self.enabled:
            self.logger.debug(f'Attempting switch LED state {event} to {state}')
            for pin_num, pin_settings in self.out_pins.items():
                only_for = pin_settings.get('only_for')
                if pin_settings['event'] == event:
                    if only_for and only_for != caller_id:
                        continue
                    if pin_settings.get('invert'):
                        state = not state
                    self.logger.debug(f'Switching LED {pin_num} to {state}')
                    return self.write_level(pin_num, state)
        return False

    def run_method(self, pi, method_name):
        method = getattr(pi, method_name, None)
        if method:
            try:
                result = method()
            except Exception:
                self.logger.exception(f"GPIO event caused exception {method_name} for {pi}:")
            else:
                self.logger.info(f'GPIO event call {method_name} for {pi}. Result: {result}')
        else:
            self.logger.error("GPIO event error: no method %s" % method_name)

    def close(self):
        self.enabled = False

    def toggle(self):
        if self.enabled:
            self.close()
        else:
            self.init()
        settings = config.get_settings()
        settings['gpio']['enabled'] = self.enabled
        config.Config.instance().save_settings(settings)
