
__version__ = '1.0.0'

from cgitb import text
import itertools
import json
import logging
import os
import statistics
import sys
import tkinter as tk
from tkinter import ttk
import time
from inspect import getsourcefile
from tokenize import Name
from turtle import width
from unicodedata import name

import gui
import settings
from GRANTA_MIScriptingToolkit import granta
from py_omdb import PyOMDB
from py_os_design import PyOSDesign, PyOSDesignPoint, PyOSDesignContainer
from py_os_parameter import PyParameterManager
from stdcpp_python_export import Path
import sys
import password_setting


def setup_logging(filename, cleanup_previous_setup=False):
    if cleanup_previous_setup:
        cleanup_previous_logging_setup()
    format_ = '%(asctime)s \t %(module)s \t %(lineno)s \t %(levelname)s \t %(message)s'
    logging.basicConfig(filename=filename,
                        filemode='a',
                        level=logging.DEBUG,
                        format=format_,
                        datefmt='%d-%m-%Y %H:%M:%S')


def cleanup_previous_logging_setup():
    if logging.root:
        del logging.root.handlers[:]


ALLOWED_TYPES = ["POIN", "INPT", "RNGE"]


class Names:
    # Double mapping has something to do with Settings and default values
    address = 'Address'
    database = 'Database'
    table = "Table"
    user = 'Username'
    password = 'Password'
    record = "Record"
    Connect = "Connect"

    server_address = 'Server.Address'
    server_database = 'Server.Database'
    server_table = 'Server.Table'
    login_user = 'Login.Username'
    login_password = 'Login.Password'
    record_guids = "Record.GUIDs"
    parameters = "Parameters"
    responses = "Responses"
    exporters = "Exporters"


class Environment:

    def __init__(self, env_dp):
        self._env = self._dp_to_dict(env_dp)

    @staticmethod
    def _dp_to_dict(dp):
        return {n: v.get() for n, v in dp}

    def get_design_dir(self):
        return self._env.get('OSL_DESIGN_DIR', None)

    def get_node_name(self):
        return self._env.get('OSL_NODE_NAME', None)

    def _get_log_file(self):
        log_dir = self.get_design_dir()
        if log_dir is None:
            return None
        file_name = self.get_node_name()
        if file_name is None:
            return None
        return os.path.join(log_dir, file_name+'.log')

    def get_action(self):
        return self._env['ACTION']

    def is_action_send(self):
        return self.get_action() == 'Send'

    def is_action_receive(self):
        return self.get_action() == 'Receive'

    def setup_logging(self):
        # comment by sm:
        # using the node on osl we have OSL_DESIGN_DIR and OSL_NODE_NAME
        # but not as centrally registered file
        log_file = self._get_log_file()
        if log_file is not None:
            setup_logging(log_file, True)

    def user_wants_to_upload_file(self):
        return self.is_action_send()

    def user_wants_to_download_file(self):
        return self.is_action_receive()

    def show_files(self):
        return self.user_wants_to_download_file()


class MIConnectionError(Exception):
    pass


def Restore(args):
    """ Function is called by optiSLang to receive file.

        Parameters
        ----------
        args : list
            see optislang help: ANSYS optiSLang > Interfaces and Customization > File Access

        Returns
        -------
        bool
            The publish success.

    """
    try:
        logging.info("Export operation started")
        local_path = os.path.normpath(args[0])
        browse_data = json.loads(args[4])
        variables = args[8]

        if not browse_data:
            return False

        env = Environment(variables)
        env.setup_logging()

        user = browse_data[Names.login_user]
        enc_password = browse_data[Names.login_password]
        database_key = browse_data[Names.server_database]
        table = browse_data[Names.server_table]
        server = browse_data[Names.server_address]
        record_guids = browse_data[Names.record_guids]
        attribute_parameters = browse_data[Names.parameters]
        attribute_responses = browse_data[Names.responses]
        exporters = browse_data[Names.exporters]
        password = gui.Entry._decrypt_string('PasswordSetting', enc_password)
      
        if user and password:
            session = granta.Session(server, user_name=user, password=password)
        else:
            session = granta.Session(server, autologon=True)

        database = session.get_db(db_key=database_key)
        table = database.get_table(table)
        records = [database.get_record_by_id(vguid=guid) for guid in record_guids]
        records_names = [record.short_name for record in records]
        print("Records name: ", records_names)
        #Extracting exporters from record and selecting the transfered one
        exporter_to_use = None
        for exporter in records[0].get_available_exporters(applicability_tag = "MIMaterialsGateway"):
            if exporter.name == exporters[0]:
                exporter_to_use = exporter
        if not exporter_to_use:
            print("No exporter found!")
            return False
        #Default file extension for selected exporter
        file_extension = exporter_to_use.default_file_extension

        # ##############################################################################
        # Export file from material card
        # ##############################################################################  

        if str(local_path).endswith(file_extension):
            #Spliting the full file path to directory parh and file name
            path_to_save, file_name = os.path.split(str(local_path))
            #Extracting the file name without extension        
            file_name = file_name.rsplit('.', 1)[0]
            path_to_save = path_to_save.replace('\\', '/')+'/'
            print("Package name: ", exporter_to_use.package)
            print("Exporter model: ", exporter_to_use.model)            
            #Collecting required parameters for selected exporters
            parameters_required = exporter_to_use.get_parameters_required_for_export(records)            
            parameter_values = {}
            for parameter_name in parameters_required.keys():
                parameters_required[parameter_name].value_for_exporters = parameter_values[parameter_name]

            material_card = exporter_to_use.run_exporter(records, parameter_defs = parameters_required)
            exporter_to_use.save(path_to_save, file_name=file_name)            
            print("Exporter output saved to \"{}{}.{}\"".format(path_to_save, file_name, file_extension))
        # ##############################################################################
        # If the file is .omdb kind then the old usecase will applied
        # ############################################################################## 
            
        elif str(local_path).endswith(".omdb") and attribute_parameters:  
            records = records_and_children(records)
            logging.info("Exporting {} records as designs".format(len(records)))
        
            table.bulk_fetch(records, attributes=[*attribute_parameters, *attribute_responses])

            output_path = Path(local_path)
        
       
            # Empty container to be filled
            designs = PyOSDesignContainer()
            # Dictionary parameter_name : lowerbound, upperbound
            param_dict = {}

            for num_design, record in enumerate(records, 1):
                try:
                    design = record_to_design(record, attribute_parameters, attribute_responses, param_dict)
                except InvalidDesign:
                    continue
                else:
                    # Set designnumber
                    design.set_number(num_design)
                    # Append the design to the list of designs
                    designs.push_back(design)

            # ##############################################################################
            # Create an empty parameter manager
            # ##############################################################################

            params = PyParameterManager()
            # Add all input parameters to the parameter manager
            for name in param_dict:
                # Parameterbounds equal sampling bounds
                bounds = param_dict[name]
                # Parameter is deterministic, reference is center of imported sampling
                params.add_deterministic_continuous_parameter(name, bounds[0], bounds[1], 0.5 * (bounds[0] + bounds[1]), 0)
            params.add_parameter("record_guid")
            params.add_parameter("db_key")
            # ##############################################################################
            # Write designs and parameter to newly created omdb file
            # ##############################################################################

            if str(output_path).endswith(".omdb"):
                omdb_file = PyOMDB()
                omdb_file.design_container = designs
                omdb_file.parameter_manager = params
                # Mandatory to call after changes to designs or parameters
                omdb_file.check_consistency()
                omdb_file.save_to_file(output_path)
        else:  
            sys.stderr.write("Wrong file extension, you should use ."+file_extension+" file extension! \n")            
            print("No monitoring file written.")

        return True
    except Exception as e:
        print(str(e))


def record_to_design(record, attribute_parameters, attribute_responses, param_dict):
    # Create empty design - to be filled
    design = PyOSDesign()
    # Create empty designpoint (inputs)
    parameters = PyOSDesignPoint()

    # TODO handle empty attributes or partially empty attributes (low/high for ranges)
    for att_name in attribute_parameters:
        attribute_value = record.attributes[att_name]
        try:
            pvalue = attribute_to_value(attribute_value)
        except (InvalidDataError, UnsupportedType):
            raise InvalidDesign()
        # TODO what if there is no para left when we remove wrong types?
        para_name = att_name
        # Check/adapt sampling bounds
        para_bounds = param_dict.setdefault(att_name, [pvalue, pvalue])
        if pvalue < para_bounds[0]:
            para_bounds[0] = pvalue
        elif pvalue > para_bounds[1]:
            para_bounds[1] = pvalue

        # Add input to design point
        parameters.add(para_name, pvalue)

    # Attempt to keep record reference available
    parameters.add("record_guid", record.record_guid)
    parameters.add("db_key", record.db_key)

    # Set collected values at design
    design.set_parameters(parameters)

    # Create empty vector (for responses)
    responses = PyOSDesignPoint()

    for att_name in attribute_responses:
        try:
            pvalue = attribute_to_value(record.attributes[att_name])
        except (InvalidDataError, UnsupportedType):
            pass
        else:
            responses.add(att_name, pvalue)

    # Set collected values at design
    design.set_responses(responses)
    return design


def attribute_to_value(attribute: granta.AttributeValue):
    if attribute.is_empty():
        raise InvalidDataError()
    if isinstance(attribute, granta.AttributePoint):
        return attribute.points[0]
    elif isinstance(attribute, granta.AttributeRange):
        if "low" in attribute.value and "high" in attribute.value:
            return statistics.mean([attribute.value["low"], attribute.value["high"]])
        else:
            raise InvalidDataError()
    elif isinstance(attribute, granta.AttributeInteger):
        return attribute.value
    else:
        raise UnsupportedType(f"Unsupported attribute type '{attribute.type}' ({attribute.name})")


class InvalidDataError(Exception):
    pass


class UnsupportedType(Exception):
    pass


class InvalidDesign(Exception):
    pass


def Browse(args):
    """ Function is called by optiSLang to open the browse gui.

        Parameters
        ----------
        args : list
            see optislang help: ANSYS optiSLang > Interfaces and Customization > File Access

        Returns
        -------
        list of str
            [
                str : updated browse data
                str : empty
            ]

    """
    local_path = os.path.normpath(args[0])
    browse_data = args[4]
    script_path = args[7]
    variables = args[8]
    #
    env = Environment(variables)
    show_files = env.show_files()
    #
    # raise Exception(browse_data)
    if not browse_data:
        browse_data = get_default_settings().to_dict()
    else:
        browse_data = json.loads(browse_data)
        password = browse_data[Names.login_password]
        browse_data[Names.login_password] = gui.Entry._decrypt_string('PasswordSetting', password)

    script_dir = os.path.dirname(script_path)
    password = browse_data[Names.login_password]
    if env.is_action_receive():
        root = tk.Tk()
        icon_photo = tk.PhotoImage(file=os.path.join(os.path.dirname(os.path.abspath(getsourcefile(lambda:0))), "resources", "Materials.png"))
        root.title("Granta MI for optiSLang")
        root.iconphoto(False, icon_photo)
        browser = MainApplication(root)
 
        browse_data = browser.wait_for_user()
 
    else:
        raise NotImplementedError("Unknown action: {}".format(env.get_action()))

    password = browse_data[Names.login_password]
    if password:
        browse_data[Names.login_password] = gui.Entry._encrypt_string('PasswordSetting', password)

    return [json.dumps(browse_data, indent=4), '']


def get_default_settings():
    server = settings.StringSetting(Names.address, 'http://localhost/mi_servicelayer', 'Server')
    database = settings.StringSetting(Names.database, '', 'Server')
    user = settings.StringSetting(Names.user, '', 'Login')
    password = password_setting.PasswordSetting(Names.password, '', 'Login')

    gui_settings = settings.Settings('ANSYS Granta_plm')
    gui_settings.add_setting(server)
    gui_settings.add_setting(database)
    gui_settings.add_setting(user)
    gui_settings.add_setting(password)
    gui_settings.load()

    return gui_settings

# -----------------------------------------------------------------------------
# ExtractSaveLocationString callback
# -----------------------------------------------------------------------------
def ExtractSaveLocationString(args):
    """ Function is called by optiSLang after closing browse gui.
        It provides a str which is used to show in "Path".

        Parameters
        ----------
        args : list
            see optislang help: ANSYS optiSLang > Interfaces and Customization > File Access

        Returns
        -------
        str
            String to show in as Path.

    """
    browse_data = args[4]
    variables = args[8]

    if not browse_data:
        return 'Push "Browse" button for setup'

    env = Environment(variables)
    browse_data = json.loads(browse_data)

    if env.user_wants_to_download_file():
        database_key = browse_data[Names.server_database]
        record_guids = browse_data[Names.record_guids]
        parameters = browse_data[Names.parameters]
        responses = browse_data[Names.responses]
        exporters = browse_data[Names.exporters]
        return 'Download: [{}]: {}/{}/{}/{}'.format(database_key, record_guids, parameters, responses, exporters)
    elif env.user_wants_to_upload_file():
        return 'Upload to: [{}]: {}'.format("database_key", "record_guid")
    return 'Unknown action!'



def records_and_children(records):
    ret = []
    for r in records:
        if r.type != "Folder":
            ret.append(r)
        ret.extend(all_children(r))
    return ret

def all_children(record):
    ret = []
    ret.extend([r for r in record.children if r.type != "Folder"])
    for r in record.children:
        ret.extend(all_children(r))
    return ret
    
    

 
class MainApplication(tk.Frame):
    """The main application, which controls widgets created as sibling classes."""
    def __init__(self, parent, *args, **kwargs):
        self.parent = parent

        # Create class attributes for storing MI STK objects:
        self.sl_url = ""
        self.mi_session = None
        self.currently_selected_database = None
        self.currently_selected_table = None
        self.record_guids_for_analysis = []
        self.selected_parameter_names = []
        self.selected_response_names = []
        self.selected_exporter = []

        self.stvar_auth_type = tk.StringVar()

        # Define labels and frames in the main application windows
        self.label_title = ttk.Label(self.parent, text="Select records to use:")
        self.label_title.config(font=("", 12, "bold"))
        self.top_frame = PositioningFrame(self.parent, self)
        self.middle_frame = PositioningFrame(self.parent, self)
        self.bottom_frame = PositioningFrame(self.parent, self)
        self.label_status_message = StatusLabel(self.parent, self)

        # Handle geometry
        self.label_title.pack(anchor="w", padx=10, pady=10)
        self.top_frame.pack(padx=10)
        self.middle_frame.pack(padx=20)
        self.bottom_frame.pack(padx=10)
        self.label_status_message.label.pack(anchor="se", side="bottom")

        # Call first view
        self.logon_view()

    def logon_view(self):
        """
        Configures the first view of the GUI, to allow the user to log on to MI.
        :return: None, but if connection succeeds, it calls the method record_picking_view() to populate the next
                 screen.
        """
        self.label_title.config(text="Logon to MI:")

        # Define labels:
        self.label_servicelayer = ttk.Label(self.top_frame, text="Enter the service url here:")
        self.label_username = ttk.Label(self.middle_frame, text="Username:")
        self.label_domain = ttk.Label(self.middle_frame, text="Domain:")
        self.label_password = ttk.Label(self.middle_frame, text="Password:")

        # Define entry fields:
        self.sl_entry = GenericEntry(self.parent, self.top_frame, width=60,
                                     insert_text="http://localhost/mi_servicelayer")
        self.username_entry = GenericEntry(self.parent, self.middle_frame, width=30)
        self.domain_entry = GenericEntry(self.parent, self.middle_frame, width=30)
        self.password_entry = GenericEntry(self.parent, self.middle_frame, width=30, show="*")

        # Define radio buttons:
        self.windows_radio_button = WindowsRadioButton(self.parent, self.top_frame)
        self.basic_radio_button = BasicRadioButton(self.parent, self.top_frame)
        self.stvar_auth_type.set("windows")
        self._disable_basic_entries()

        # Define buttons:
        self.logon_button = LogonButton(self.parent, self.bottom_frame)
        self.cancel_button = CancelButton(self.parent, self.bottom_frame)

        # Handle geometry:
        self.label_servicelayer.grid(row=0, column=0, sticky="w", pady=5)
        self.sl_entry.entry.grid(row=10, column=0, pady=5)
        self.windows_radio_button.radio_button.grid(row=20, column=0, sticky="w", pady=(15, 2))
        self.basic_radio_button.radio_button.grid(row=30, column=0, sticky="w", pady=(2, 15))

        self.label_username.grid(row=0, column=0, sticky="e", pady=5, padx=10)
        self.username_entry.entry.grid(row=0, column=1, pady=5)
        self.label_domain.grid(row=1, column=0, sticky="e", pady=5, padx=10)
        self.domain_entry.entry.grid(row=1, column=1, pady=5)
        self.label_password.grid(row=2, column=0, sticky="e", pady=5, padx=10)
        self.password_entry.entry.grid(row=2, column=1, pady=5)

        self.logon_button.button.grid(row=0, column=0, pady=20, padx=10)
        self.cancel_button.button.grid(row=0, column=2, pady=20, padx=10)
        
    def record_picking_view(self):
        """
        Configures the second GUI view, to allow selection of records.
        :return: None, but if successful, it calls the method attribute_picking_view() to populate the next
        screen.
        """
        self.label_title.config(text="Select records to use:")
        # Remove previous stuff.
        for frame in [self.top_frame, self.middle_frame, self.bottom_frame]:
            for widget in frame.winfo_children():
                widget.destroy()

        # Define labels:
        self.search_label = ttk.Label(self.top_frame, text="Search:")
        self.database_label = ttk.Label(self.top_frame, text="Select database:")
        self.table_label = ttk.Label(self.top_frame, text="Select table:")        
        self.record_select_label = ttk.Label(self.middle_frame, text="Select records:")

        self.search_box = GenericEntry(self.parent, self.top_frame, width=40)
        self.search_box.entry.bind("<KeyRelease>", self.search)
        # Define comboboxes
        self.cmbbox_database = DatabaseCombobox(self.parent, self.top_frame, self.mi_session)
        self.cmbbox_tables = TableCombobox(self.parent, self.top_frame)         

        # Define Record picker
        self.record_treeview = RecordTree(self.parent, self.middle_frame)
        self.export_treeview = RecordTree(self.parent, self.middle_frame, "Model", "Package", "Ext")
        self.record_treeview.treeview.bind("<<TreeviewSelect>>", self.selected_item)

        # Define buttons:
        self.cancel_button = CancelButton(self.parent, self.bottom_frame)
        self.select_button = SelectRecordsButton(self.parent, self.bottom_frame)

        # Handle geometry       
        self.database_label.grid(row=0, column=0, columnspan=10, sticky="e", padx=10)
        self.cmbbox_database.combobox.grid(row=0, column=10, columnspan=20, pady=(10, 5))
        self.table_label.grid(row=1, column=0, columnspan=10, sticky="e", padx=10)
        self.cmbbox_tables.combobox.grid(row=1, column=10, columnspan=20, pady=(5, 10))
        self.search_label.grid(row=2, column=0, columnspan=10, sticky="e", padx=10)
        self.search_box.entry.grid(row=2, column=10, columnspan=10, pady=(5, 10))
        self.select_button.button.grid(row=0, column=0, columnspan=10, ipadx=5, padx=10, pady=10)
        self.cancel_button.button.grid(row=0, column=10, columnspan=10, ipadx=5, padx=10, pady=10)
        self.record_treeview.treeview.grid(row=0, pady=10)       
        self.export_treeview.treeview.grid(row=1, pady=10)

    def attribute_picking_view(self):
        """
        Configures the third GUI view, to allow selection of attributes.
        :return: None, but if successful, it bulk exports the selected records with the selected attributes.
        """
        self.label_title.config(text="Select parameters and responses:")

        # Remove previous stuff.
        for frame in [self.top_frame, self.middle_frame, self.bottom_frame]:
            for widget in frame.winfo_children():
                widget.destroy()

        self.label_attribute_tree = ttk.Label(self.top_frame, text="Attributes:")
        self.att_tree = AttributeTree(self.parent, self.top_frame, self.currently_selected_table.attributes)
        self.param_tree = EmptyTree(self.parent, self.middle_frame)
        self.label_response_tree = ttk.Label(self.middle_frame, text="Selected responses:")
        self.label_parameter_tree = ttk.Label(self.middle_frame, text="Selected parameters:")
        self.response_tree = EmptyTree(self.parent, self.middle_frame)
        self.add_parameter_button = AddParameterButton(self.parent, self.middle_frame)
        self.remove_parameter_button = RemoveParameterButton(self.parent, self.middle_frame)
        self.add_response_button = AddResponseButton(self.parent, self.middle_frame)
        self.remove_response_button = RemoveResponseButton(self.parent, self.middle_frame)
        self.make_model_button = MakeModelButton(self.parent, self.bottom_frame)
        self.cancel_button = CancelButton(self.parent, self.bottom_frame)

        # Handle geometry:

        self.label_attribute_tree.grid(row=0, column=0, stick="w")
        self.att_tree.treeview.grid(row=1, column=0, pady=(0, 20))
        self.att_tree.scrollbar.grid(row=1, column=1, stick="ns", pady=(0, 20))

        self.add_parameter_button.button.grid(row=0, column=0, columnspan=10, pady=(10, 5), padx=5, ipadx=5, stick="e")
        self.remove_parameter_button.button.grid(row=0, column=10, columnspan=10, pady=(10, 5), padx=5, ipadx=5, stick="w")
        self.add_response_button.button.grid(row=0, column=20, columnspan=10, pady=(10, 5), padx=5, ipadx=5, stick="e")
        self.remove_response_button.button.grid(row=0, column=30, columnspan=10, pady=(10, 5), padx=5, ipadx=5, stick="w")
        self.label_parameter_tree.grid(row=5, column=0, columnspan=20, padx=10, stick="w")
        self.param_tree.treeview.grid(row=10, column=0, columnspan=20, padx=(5, 5))
        self.label_response_tree.grid(row=5, column=20, columnspan=20, padx=5, stick="w")
        self.response_tree.treeview.grid(row=10, column=20, columnspan=20, padx=(5, 5))

        self.make_model_button.button.grid(row=0, column=0, pady=(20, 10), padx=50, ipadx=5)
        self.cancel_button.button.grid(row=0, column=1, pady=(20, 10), padx=50, ipadx=5)

    def _disable_basic_entries(self):
        """Calls the _on_select() method of the windows authentification radio button to disable basic entries."""
        self.windows_radio_button._on_select()

    def _populate_table_combobox(self, x):
        """Populates the table combobox."""
        db_name = self.cmbbox_database.combobox.get()
        db = self.mi_session.get_db(name=db_name)        
        self.currently_selected_database = db
        table_name_list = [tb.name for tb in db.tables]
        self.cmbbox_tables.combobox.config(state="readonly", values=table_name_list)
        self.cmbbox_tables.combobox.set(table_name_list[0])
        self._on_table_selected(1)

    def _on_table_selected(self, x):
        """The command executed when a table is selected. It auto-populates the first level of records."""
        tb_name = self.cmbbox_tables.combobox.get()
        self.currently_selected_table = self.currently_selected_database.get_table(name=tb_name)

        items_to_delete = self.record_treeview.treeview.get_children("")
        for item in items_to_delete:
            self.record_treeview.treeview.delete(item)

        self.record_treeview.populate_top_level(self.currently_selected_table.children) 
    
    def selected_item(self, e):
        selected = self.record_treeview.treeview.focus()
       
        if (self.record_treeview.treeview.item(selected)["values"][0]=="Record"):            
            searched = self.currently_selected_table.search_for_records_by_name(self.record_treeview.treeview.item(selected)["text"])[0]
            exporter_to_use = searched.get_available_exporters(applicability_tag = "MIMaterialsGateway")
      
            items_to_delete = self.export_treeview.treeview.get_children("")            
            for item in items_to_delete:
                self.export_treeview.treeview.delete(item)
            self.export_treeview.populate_tree(exporter_to_use)
           
        else:
            items_to_delete = self.export_treeview.treeview.get_children("")            
            for item in items_to_delete:
                self.export_treeview.treeview.delete(item)

    def search(self, e):
        typed = self.search_box.entry.get()
        if(typed==""):
            self._on_table_selected(1)
        if (len(typed)>3):      
            searched = self.currently_selected_table.search_for_records_by_name(typed)     
            if(searched):
                items_to_delete = self.record_treeview.treeview.get_children("")            
                for item in items_to_delete:
                    self.record_treeview.treeview.delete(item)
                self.record_treeview.populate_top_level(searched)

    
    def _connect_to_MI(self):
        """
        Connects to MI
        :return: True/False. True = connection successful, False = connection unsuccessful.
        """

        auth_type = self.stvar_auth_type.get()

        if self.sl_entry.entry.get() == "":
            self.label_status_message.error_message("No service layer url was provided.")
            return False

        if auth_type == "windows":
            try:
                self.label_status_message.status_message("Attempting to connect to MI...")
                self.mi_session = granta.connect(self.sl_entry.entry.get(), autologon=True)
                self.label_status_message.status_message("Connected to MI.")
                self.sl_url = self.sl_entry.entry.get()
                return True
            except:
                self.label_status_message.error_message("There was a problem connecting to MI.")
                return False

        elif auth_type == "basic":
            if self.sl_entry.entry.get() != "" or self.username_entry.entry.get() != "" or self.domain_entry.entry.get() != "" or self.password_entry.entry.get() != "":
                try:
                    self.label_status_message.status_message("Attempting to connect to MI...")
                    self.mi_session = granta.connect(self.sl_entry.entry.get(), user_name=self.username_entry.entry.get(),
                                             domain=self.domain_entry.entry.get(), password=self.password_entry.entry.get(),
                                             autologon=False)
                    self.label_status_message.status_message("Connected to MI.")
                    self.sl_url = self.sl_entry.entry.get()
                    return True
                except:
                    self.label_status_message.error_message("There was a problem connecting to MI. Check credentials are correct.")
                    return False
        else:
            self.label_status_message.error_message("Authorisation status could not be ascertained.")
            return False

    def wait_for_user(self):
        """A method copied from Dynardo's BasicBrowseDialog class"""
        self.parent.wait_window()
        return self.get_save_location()

    def get_save_location(self):
        """What the browser returns"""

        save_location = {}
        save_location[Names.server_address] = self.sl_url
        save_location[Names.server_database] = self.currently_selected_database.db_key
        save_location[Names.server_table] = self.currently_selected_table.name
        # TODO: modify to work with basic authentification.
        save_location[Names.login_user] = ""
        save_location[Names.login_password] = ""

        save_location[Names.record_guids] = self.record_guids_for_analysis
        save_location[Names.parameters] = self.selected_parameter_names
        save_location[Names.responses] = self.selected_response_names
        save_location[Names.exporters] = self.selected_exporter
        return save_location


class LogonButton(tk.Frame):
    """Create the button to log on to MI."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Logon to MI", command=self._on_click)

    def _on_click(self):
        connection_successful = self.controller.controller._connect_to_MI()
        if connection_successful:
            self.controller.controller.record_picking_view()


class BasicRadioButton(tk.Frame):
    """Creates the radio button for basic authentification."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.radio_button = ttk.Radiobutton(self.controller,
                                            text="Use basic authentification",
                                            variable=self.controller.controller.stvar_auth_type,
                                            value="basic",
                                            state="disable",
                                            command=self._on_select)

    def _on_select(self):
        """Enable basic entries"""
        for widget in self.controller.controller.middle_frame.winfo_children():
            widget.state(["!disabled"])


class WindowsRadioButton(tk.Frame):
    """Creates the radio button for windows authentification."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.radio_button = ttk.Radiobutton(self.controller,
                                            text="Use Windows authentification",
                                            variable=self.controller.controller.stvar_auth_type,
                                            value="windows",                                            
                                            command=self._on_select)

    def _on_select(self):
        """Disable basic entries"""
        for widget in self.controller.controller.middle_frame.winfo_children():
            widget.state(["disabled"])


class GenericEntry(tk.Frame):
    """Creates an entry widget. Can be configured to insert text, change width and to hide characters."""
    def __init__(self, parent, controller, **kwargs):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.entry = ttk.Entry(self.controller)
        if "width" in kwargs: self.entry.config(width=kwargs["width"])
        if "show" in kwargs: self.entry.config(show=kwargs["show"])
        if "insert_text" in kwargs: self.entry.insert(0, kwargs["insert_text"])


class SelectRecordsButton(tk.Frame):
    """Creates a button which, when clicked, collects all the children of the selected record."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Select records", command=self.on_click)

    def on_click(self):
        # Update status:
        self.controller.controller.label_status_message.status_message("Gathering selected records...")
        # Get selected record
        selected_record_guid = list(self.controller.controller.record_treeview.treeview.selection()) #focus()
        selected_exporter = self.controller.controller.export_treeview.treeview.focus()
        material_name = [self.controller.controller.record_treeview.treeview.item(record)["text"] for record in selected_record_guid]
        exporter_values = self.controller.controller.export_treeview.treeview.item(selected_exporter)["values"]        
        if selected_record_guid == "":
            self.controller.controller.label_status_message.error_message(
                "No records found. Please select at least one record.")
            return
        if selected_exporter == "":
            self.controller.controller.label_status_message.error_message(
                "No exporter selected! Please select at least one exporter.")
            return
             
        print(selected_record_guid)        
        print("Extention to use: ", exporter_values[1])
        print("Selected package: ", exporter_values[0])
        print("Selected material: ", material_name)
        self.controller.controller.record_guids_for_analysis = selected_record_guid
        self.controller.controller.selected_exporter = [selected_exporter]
        self.controller.controller.label_status_message.status_message("Ready.")      
        time.sleep(1)
        self.controller.controller.parent.destroy()            

    def get_all_children(self, record, record_list):
        for child in record.children:
            record_list.append(child)
            self.get_all_children(child, record_list)
        return record_list

class RecordTree(tk.Frame):
    """Create the record tree for record selection."""
    def __init__(self, parent, controller, col1name="Name", col2name="Type", col3name=None):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.treeview = ttk.Treeview(self.controller)
        self.treeview.config(columns=("Type"))
        width_col = 200
        if col3name:
            self.col3name = col3name
            self.treeview.config(columns=("Type", "ext"), selectmode="browse")
            width_col = 150
        self.treeview.column("#0", width=300)
        self.treeview.column("Type", width=width_col, anchor="center")
        self.treeview.heading("#0", text=col1name)
        self.treeview.heading("Type", text=col2name)
        if col3name:
            self.treeview.column("ext", width=50, anchor="center")
            self.treeview.heading("ext", text=col3name)
     
        if self.controller.controller.currently_selected_table is not None:
            self.populate_top_level(self.controller.controller.currently_selected_table)

        self.treeview.bind("<<TreeviewOpen>>", self.folder_expanded)    

    def populate_tree(self, tl_recs):
        if self.col3name:
            for r in tl_recs:
                if not self.treeview.exists(r.name):                    
                    self.treeview.insert("", "end", r.name, text=r.model, values=(r.package, r.default_file_extension))
        
    def populate_top_level(self, tl_recs):
        for r in tl_recs:
            self.treeview.insert("", "end", r.record_guid, text=r.short_name)
            self.treeview.set(r.record_guid, "Type", r.type)
            if len(r.children) > 0:
                self.treeview.insert(r.record_guid, "end", r.record_guid + "_dummy", text="")

    def folder_expanded(self, a):
        """Method called when the treeview opens a folder."""
        item_selected = self.treeview.focus()
        self.populate_next_level(item_selected)

    def populate_next_level(self, item_guid):
        """
        Dynamically populates the tree as it is expanded.
        :param item_guid: The record guid of the item which was expanded. This is the item ID.
        :return: None
        """
        src_rec = self.controller.controller.currently_selected_table.get_record_by_id(vguid=item_guid)
        next_level_recs = src_rec.children
        if self.treeview.exists(item_guid + "_dummy"): self.treeview.delete(item_guid + "_dummy")
        for r in next_level_recs:
            if not self.treeview.exists(r.record_guid):
                self.treeview.insert(item_guid, "end", r.record_guid, text=r.short_name)
                self.treeview.set(r.record_guid, "Type", r.type)
            if len(r.children) > 0:
                if not self.treeview.exists(r.record_guid + "_dummy"): self.treeview.insert(r.record_guid, "end", r.record_guid + "_dummy", text="")


class TableCombobox(tk.Frame):
    """Creates the combobox for selecting tables."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.table_selected = tk.StringVar()
        self.combobox = ttk.Combobox(self.controller, textvariable=self.table_selected, state="disabled")
        self.combobox.config(width=40)

        self.combobox.bind("<<ComboboxSelected>>", func=self.on_select)

    def on_select(self, a):
        self.controller.controller._on_table_selected(self.combobox.get())


class DatabaseCombobox(tk.Frame):
    """Creates the combobox for selecting databases."""
    def __init__(self, parent, controller, mi_session):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.database_selected = tk.StringVar()
        self.combobox = ttk.Combobox(self.controller, textvariable=self.database_selected, state="readonly")
        self.combobox.config(width=40, values=[db.name for db in mi_session.dbs])

        self.combobox.bind("<<ComboboxSelected>>", func=self.on_select)

    def on_select(self, a):
        self.controller.controller._populate_table_combobox(self.combobox.get())


class PositioningFrame(tk.Frame):
    """Creates generic tk frames for placing and organising other widgets."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.frame = ttk.Frame(parent)


class CancelButton(tk.Frame):
    """Creates a button which closes the app if clicked."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Cancel", command=self.on_click)

    def on_click(self):
        # Close application.
        self.parent.destroy()


class MakeModelButton(tk.Frame):
    """Creates a button which bulk exports the selected attributes on the selected records."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Pass data to oSL", command=self.on_click)

    def on_click(self):
        # Make a list of the attributes and responses.
        selected_parameter_names = self.controller.controller.param_tree.treeview.get_children("")
        self.controller.controller.selected_parameter_names = selected_parameter_names
        selected_response_names = self.controller.controller.response_tree.treeview.get_children("")
        self.controller.controller.selected_response_names = selected_response_names

        if len(selected_parameter_names) > 0 and len(selected_response_names) > 0:
            self.controller.controller.label_status_message.status_message(
                "Processing data for optiSLang...")
            print("Selected parameters:\n", selected_parameter_names)
            print("Selected responses:\n", selected_response_names)
            print(self.controller.controller.record_guids_for_analysis)
            self.controller.controller.label_status_message.status_message(
                "Done.")
            time.sleep(1)
            self.controller.controller.parent.destroy()

        else:
            self.controller.controller.label_status_message.error_message(
                "Error. Please select at least one parameter and response.")


class AddResponseButton(tk.Frame):
    """A button to add an attribute from the attribute tree to the response tree."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Add response", command=self.add_response)

    def add_response(self):
        item = self.controller.controller.att_tree.treeview.focus()
        if item != "":
            item_settings = self.controller.controller.att_tree.treeview.item(item)
            self.controller.controller.response_tree.treeview.insert("", "end", iid=item, text=item,
                                                                  values=item_settings["values"])
            self.controller.controller.att_tree.treeview.detach(item)


class RemoveResponseButton(tk.Frame):
    """A button to remove an attribute from the response tree to the attribute tree."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Remove response", command=self.remove_response)

    def remove_response(self):
        item = self.controller.controller.response_tree.treeview.focus()
        if item != "":
            # Find the index at which this item should be re-inserted in the attribute tree:
            existing_attribute_items = self.controller.controller.att_tree.treeview.get_children("")
            index = 0
            for i, name in enumerate(existing_attribute_items):
                if name > item:
                    index = i
                    break
            self.controller.controller.att_tree.treeview.reattach(item, "", index)
            self.controller.controller.response_tree.treeview.delete(item)


class AddParameterButton(tk.Frame):
    """A button to add an attribute from the attribute tree to the parameter tree."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Add parameter", command=self.add_parameter)

    def add_parameter(self):
        item = self.controller.controller.att_tree.treeview.focus()
        if item != "":
            item_settings = self.controller.controller.att_tree.treeview.item(item)
            self.controller.controller.param_tree.treeview.insert("", "end", iid=item, text=item,
                                                                  values=item_settings["values"])
            self.controller.controller.att_tree.treeview.detach(item)


class RemoveParameterButton(tk.Frame):
    """A button to remove an attribute from the parameter tree to the attribute tree."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller
        self.button = ttk.Button(self.controller, text="Remove parameter", command=self.remove_parameter)

    def remove_parameter(self):
        item = self.controller.controller.param_tree.treeview.focus()
        if item != "":
            # Find the index at which this item should be re-inserted in the attribute tree:
            existing_attribute_items = self.controller.controller.att_tree.treeview.get_children("")
            index = 0
            for i, name in enumerate(existing_attribute_items):
                if name > item:
                    index = i
                    break
            self.controller.controller.att_tree.treeview.reattach(item, "", index)
            self.controller.controller.param_tree.treeview.delete(item)


class EmptyTree(tk.Frame):
    """A Class for creating empty treeviews."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.treeview = ttk.Treeview(self.controller)
        self.treeview.config(columns=("Type"))
        self.treeview.column("#0", width=300)
        self.treeview.column("Type", width=80, anchor="center")
        self.treeview.heading("#0", text="Name")
        self.treeview.heading("Type", text="Type")


class AttributeTree(tk.Frame):
    """A tree for selecting attributes."""
    def __init__(self, parent, controller, att_dictionary):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.treeview = ttk.Treeview(self.controller)
        self.treeview.config(columns=("Type"))
        self.treeview.column("#0", width=300)
        self.treeview.column("Type", width=80, anchor="center")
        self.treeview.heading("#0", text="Name")
        self.treeview.heading("Type", text="Type")
        self.populate_att_tree(att_dictionary)

        self.scrollbar = ttk.Scrollbar(self.controller, orient="vertical", command=self.treeview.yview)
        self.treeview.config(yscrollcommand=self.scrollbar.set)

    def populate_att_tree(self, attribute_dict):
        """Populates the attribute tree with the names and types of the attributes."""
        # Filter to just integer, point and range attributes
        accepted_attributes = [a for a in list(attribute_dict.values()) if a.type == "INPT"
                               or a.type == "POIN" or a.type == "RNGE"]
        accepted_attributes.sort(key=lambda a: a.name)
        for item in accepted_attributes:
            self.treeview.insert("", "end", iid=item.name, text=item.name)
            self.treeview.set(item.name, column="Type", value=item.type)


class StatusLabel(tk.Frame):
    """A status label at the bottom of the frame."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.label = ttk.Label(parent, text="Ready.")

    def status_message(self, msg):
        self.label.config(text=msg, foreground="black")
        self.label.update_idletasks()

    def error_message(self, msg):
        self.label.config(text=msg, foreground="red")
        self.label.update_idletasks()

 #### #### ####