# 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 os
import sys
import json
import uuid
import logging
import zipfile

import config

# Plugins are just folders with python modules put in correct folder.
# The modules must be in folder with same as plugin name and written in plugin_settings.json file inside the folder
# Example of plugin_settings.json
# {"name": "dummy_plugin", "enabled": true}
# Plugins could be installed using .zip packages containing plugin_settings.json and all other needed files


class PluginController(object):

    PLUGIN_FOLDER = os.path.join(os.path.dirname(__file__), "plugins")
    PLUGIN_SETTINGS_FILE_NAME = "plugin_settings.json"
    PLUGIN_PACKAGE_EXTENSION = ".zip"

    def __init__(self, parent):
        self.parent = parent
        self.logger = logging.getLogger(__name__)
        self.plugins = {}
        self.plugins_to_uninstall = []
        if config.get_settings()['plugins']['enabled']:
            if not os.path.isdir(self.PLUGIN_FOLDER):
                try:
                    os.mkdir(self.PLUGIN_FOLDER)
                except IOError:
                    self.logger.warning("!Can't create plugins folder")
            else:
                folder_contents = self.get_folder_contents()
                if config.get_settings()['plugins']['auto_install']:
                    for package in self.get_plugin_packages_to_install(folder_contents):
                        self.install(os.path.join(self.PLUGIN_FOLDER, package))
                plugins = list(self.get_installed_plugins(folder_contents))
                for plugin_settings in plugins:
                    self.logger.info("Enabling plug-in: " + str(plugin_settings['display_name']))
                    self.load_plugin(plugin_settings['name'])

    def get_folder_contents(self):
        try:
            return os.listdir(self.PLUGIN_FOLDER)
        except IOError:
            self.logger.warning("!Can't access plugin folder: %s" % self.PLUGIN_FOLDER)
            return []

    def install(self, package_path):
        self.logger.info("Installing plugin %s" % package_path)
        backup_folder_name = None
        try:
            z = zipfile.ZipFile(package_path)
            with z.open(self.PLUGIN_SETTINGS_FILE_NAME) as f:
                json_settings = f.read()
            try:
                settings = json.loads(json_settings)
            except (ValueError, TypeError):
                settings = None
            if settings and type(settings) == dict:
                name = settings.get('name')
                if name:
                    folder_name = os.path.join(self.PLUGIN_FOLDER, name)
                    os.mkdir(folder_name)
                    z.extractall(path=folder_name)
                    return name
        except Exception:
            self.logger.exception("Error installing plugin %s" % package_path)
        else:
            self.logger.info("Installing successful")
        finally:
            try:
                if os.path.dirname(package_path).endswith("plugins"):
                    os.remove(package_path)
            except IOError:
                pass

    def uninstall_plugin(self, plugin_name): #this function will command to uninstall the plugin on application exit
        self.logger.info("Preparing to uninstalling plugin %s on quit" % plugin_name)
        if not plugin_name in self.plugins:
            self.logger.warning("! Can't uninstall plugin. No plugin with name: %s" % plugin_name)
            return False
        if self.plugins[plugin_name]['enabled']:
            self.disable_plugin(plugin_name)
        del self.plugins[plugin_name]
        if not plugin_name in self.plugins_to_uninstall:
            self.plugins_to_uninstall.append(plugin_name)
        return True

    def uninstall_plugin_now(self, plugin_name):
        self.logger.info("Uninstalling plugin %s" % plugin_name )
        try:
            folder = os.path.join(self.PLUGIN_FOLDER, plugin_name)
            self.delete_folder_recursively(folder)
            return True
        except Exception:
            self.logger.warning("!Failed to uninstall plugin. No plugin with name: %s?" % plugin_name)

    def delete_folder_recursively(self, folder):
        # Warning! Use this function with caution - it WILL delete everything you pointed to
        self.logger.info("Deleting folder %s" % folder)
        try:
            for root, dirs, files in os.walk(folder, topdown=False):
                for name in files:
                    os.remove(os.path.join(root, name))
                for name in dirs:
                    os.rmdir(os.path.join(root, name))
        except IOError:
            self.logger.warning("!Error while deleting %s" % folder)

    def load_plugin(self, plugin_name):
        self.logger.info("Loading plugin: %s" % plugin_name)
        try:
            plugin_settings = self.load_plugin_settings(plugin_name)
            plugin_folder = os.path.join(self.PLUGIN_FOLDER, plugin_name)
            sys.path.append(plugin_folder)
            self.plugins[plugin_name] = plugin_settings
            if config.get_settings()['plugins']['allow_active_plugins']:
                module_to_run = plugin_settings.get('run')
                if module_to_run:
                    module_object = __import__(module_to_run)
                    module_object.run(self.parent)
        except Exception:
            self.logger.exception("!Error loading plugin %s:\n" % plugin_name)

    def get_plugin_packages_to_install(self, folder_contents):
        for name in folder_contents:
            if name.endswith(self.PLUGIN_PACKAGE_EXTENSION):
                yield os.path.join(self.PLUGIN_FOLDER, name)

    def get_installed_plugins(self, folder_contents):
        for name in folder_contents:
            if os.path.isdir(os.path.join(self.PLUGIN_FOLDER, name)):
                settings = self.load_plugin_settings(name)
                if settings:
                    yield settings

    def load_plugin_settings(self, plugin_name):
        try:
            settings_file_path = os.path.join(self.PLUGIN_FOLDER, plugin_name, self.PLUGIN_SETTINGS_FILE_NAME)
            if not os.path.exists(settings_file_path):
                self.logger.warning("!Corrupted plugin named %s - no plugin_settings.json inside its folder." % plugin_name)
                return
            with open(settings_file_path) as f:
                json_settings = f.read()
            return json.loads(json_settings)
        except Exception:
            self.logger.exception("!Corrupted plugin %s:\n" % plugin_name)

    def save_plugin_settings(self, plugin_name):
        settings_file_path = os.path.join(self.PLUGIN_FOLDER, plugin_name, self.PLUGIN_SETTINGS_FILE_NAME)
        try:
            with open(settings_file_path, "w") as f:
                f.write(json.dumps(self.plugins[plugin_name], sort_keys=True, indent=4, separators=(',', ': ')))
        except:
            self.logger.warning('!Failed to write settings for plugin %s.' % plugin_name)

    def edit_plugin_settings(self, plugin_name, edit_dict):
        settings = self.plugins[plugin_name]
        if isinstance(settings, dict):
            for replaceable, replacement in list(edit_dict.items()):
                settings[replaceable] = replacement
        self.plugins[plugin_name] = settings

    def enable_plugin(self, plugin_name):
        self.logger.info("Enabling plugin: %s" % plugin_name)
        self.load_plugin(plugin_name)
        self.plugins[plugin_name]['enabled'] = True
        self.save_plugin_settings(plugin_name)

    def disable_plugin(self, plugin_name):
        self.logger.info("Disabling plugin: %s" % plugin_name)
        try:
            self.plugins[plugin_name]['enabled'] = False
            plugin_folder = os.path.join(self.PLUGIN_FOLDER, plugin_name)
            sys.path.remove(plugin_folder)
        except Exception:
            self.logger.exception("! Error unloading plugin %s:\n" % plugin_name)
        self.save_plugin_settings(plugin_name)

    # def switch_plugin(self, plugin_name):
    #     for plugin in self.installed_plugins:
    #         if plugin['name'] == plugin_name:
    #             if plugin['enabled']:
    #                 self.disable_plugin(plugin_name)
    #             else:
    #                 self.enable_plugin(plugin_name)
    #             return
    #     self.logger.warning("Not such plugin to switch: %s" % plugin_name)

    def close(self):
        try:
            for plugin_name in self.plugins_to_uninstall:
                self.logger.info("Deleting plugin %s" % plugin_name)
                self.uninstall_plugin_now(plugin_name)
        except Exception:
            self.logger.exception("Error on plugin controller close.")
