# 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
import multiprinter_gpio_config


class BaseGPIOInterface:

    ALL_PRINTERS = "*"

    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 = []
        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', "falling")
        self.in_pins = {}
        self.out_pins = {}
        multiconfig = multiprinter_gpio_config.MultiPrinterGPIOConf().load_conf()
        if self.setup_gpio(self.pull, self.trigger, self.bouncetime):
            if multiconfig:
                for args_dict in multiconfig:
                    pin_number = args_dict['pin']
                    printer = args_dict['printer_id']
                    command = args_dict['command']
                    pull_or_invert = args_dict.get('pull')
                    if command.startswith(multiprinter_gpio_config.MultiPrinterGPIOConf.OUT_PREFIX):
                        event = command.strip(multiprinter_gpio_config.MultiPrinterGPIOConf.OUT_PREFIX)
                        handle = self.setup_output(pin_number)
                        self.out_pins[pin_number] = {'event': event, 'handle': handle, 'invert': bool(pull_or_invert), 'printer': printer}
                        self.logger.info(f"GPIO output registered: pin{pin_number}={event} for printer {printer}. Invert: {bool(pull_or_invert)}")
                    else:
                        if not pull_or_invert:
                            pull_or_invert = self.pull
                        handle = self.setup_input(pin_number, pull_or_invert)
                        self.in_pins[pin_number] = {'action': command, 'handle': handle, 'printer': printer}
                        self.logger.info(f"GPIO input registered: pin{pin_number}={command} for printer {printer}. Pull: {pull_or_invert}")
            else:
                buttons = config.get_settings()['gpio'].get('buttons', {})
                for command, pin_number in buttons.items():
                    handle = self.setup_input(pin_number, self.pull)
                    if handle:
                        self.in_pins[pin_number] = {'action': command, 'handle': handle, 'printer': self.ALL_PRINTERS}
                        self.logger.info(f"GPIO input registered: pin{pin_number}={command} for printer {self.ALL_PRINTERS}. Pull: {self.pull}")
                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)
                    try:
                        pin_number = int(pin_number)
                    except:
                        self.logger.error('Invalid GPIO event pin number: ' + str(pin_number))
                        continue
                    handle = self.setup_output(pin_number)
                    if handle:
                        self.out_pins[pin_number] = {'event': event, 'handle': handle, 'invert': invert_leds, 'printer': self.ALL_PRINTERS}
                        self.logger.info(f"GPIO output registered: pin{pin_number}={event} for printer {self.ALL_PRINTERS}. Invert: {invert_leds}")
                    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']
                printer_id = self.in_pins[pin_num]['printer']
                if self.event_detected.get(pin_num):
                    self.event_detected[pin_num] = False
                    for pi in printer_interfaces:
                        if not printer_id or printer_id == self.ALL_PRINTERS or printer_id == pi.id_string:
                            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():
                if pin_settings['event'] == event:
                    printer_id = pin_settings.get('printer')
                    if not printer_id or printer_id == self.ALL_PRINTERS or printer_id == caller_id:
                        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)
