import logging
import os
import idaapi
from idaapi import Form

import idautils
import idc
from fcatalog_client.ida_client import FCatalogClient,clean_idb,MAX_SIM_GRADE

# Set up logging:
LOG_FILE_NAME = 'fcatalog_plugin.log'
cur_dir = os.path.dirname(os.path.abspath(__file__))
log_file_path = os.path.join(cur_dir,LOG_FILE_NAME)
logging.basicConfig(filename=log_file_path,level=logging.INFO)


# Client configuration:
class ClientConfig(object):
    def __init__(self):
        self.db_name = None
        self.remote_host = None
        self.remote_port = None
        self.exclude_pattern = None


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

# Configuration stashing:

def save_sstring(s):
    """
    Save a short string inside the idb.
    """
    min_segment_addr = min(list(idautils.Segments()))
    # Keep the string as a regular comment on the first instruction:
    idc.MakeComm(min_segment_addr,s)


def load_sstring():
    """
    Load a short string from the idb.
    """
    min_segment_addr = min(list(idautils.Segments()))
    return idc.GetCommentEx(min_segment_addr,0)

def save_config(client_config):
    """
    Save configuration (client_config instance) to IDB.
    """
    config_str = "%%%"
    config_str += client_config.remote_host
    config_str += ":"
    config_str += str(client_config.remote_port)
    config_str += ":"
    config_str += client_config.db_name
    config_str += ":"
    if client_config.exclude_pattern is not None:
        config_str += client_config.exclude_pattern

    save_sstring(config_str)

def load_config():
    """
    Load configuration (client_config instance) to IDB.
    """
    config_str = load_sstring()
    if (config_str is None) or (not config_str.startswith('%%%')):
        # Return empty configuration:
        return None

    # Skip the percents prefix:
    config_str = config_str[3:]

    try:
        remote_host,remote_port_str,db_name,exclude_pattern = config_str.split(':')
    except ValueError:
        # Abort if could not unpack 4 values
        return None

    remote_port = int(remote_port_str)

    # Create a client config instance and fill it with the loaded
    # configuration:
    client_config = ClientConfig()
    client_config.remote_host = remote_host
    client_config.remote_port = remote_port
    client_config.db_name = db_name
    if len(exclude_pattern) == 0:
        client_config.exclude_pattern = None

    return client_config



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


class ConfForm(Form):
    def __init__(self):
        self.invert = False
        Form.__init__(self, r"""STARTITEM {id:host}
FCatalog Client Configuration

<#Host:{host}>
<#Port:{port}>
<#Database Name:{db_name}>
<#Exclude Pattern:{exclude_pattern}>
""", {
        'host': Form.StringInput(tp=Form.FT_TYPE),
        'port': Form.StringInput(tp=Form.FT_TYPE),
        'db_name': Form.StringInput(tp=Form.FT_TYPE),
        'exclude_pattern': Form.StringInput(tp=Form.FT_TYPE),
    })


def get_similarity_cut():
    """
    Get similarity cut value from the user.
    """
    # The default similarity cut grade is just above half:
    default_sim_cut = (MAX_SIM_GRADE // 2) + 1
    # We have to make sure that default_sim_cut is not more than
    # MAX_SIM_GRADE:
    default_sim_cut = min([default_sim_cut,MAX_SIM_GRADE])

    # Keep going until we get a valid sim_cut from the user, or the user picks
    # cancel.
    while True:
        sim_cut = idaapi.asklong(default_sim_cut,\
                "Please choose a similarity grade cut (1 - {}): ".\
                format(MAX_SIM_GRADE))
        if sim_cut is None:
            # If the user has aborted, we return None:
            return None
        if (1 <= sim_cut <= MAX_SIM_GRADE):
            break

    return sim_cut


class FCatalogPlugin(idaapi.plugin_t):
    flags = 0
    comment = ''
    help = 'The Functions Catalog client'
    wanted_name = 'fcatalog_client'
    wanted_hotkey = ''

    def init(self):
        """
        Initialize plugin:
        """
        self._client_config = load_config()
        self._fcc = None
        if self._client_config is not None:
            self._fcc = FCatalogClient(\
                    (self._client_config.remote_host,\
                    self._client_config.remote_port),\
                    self._client_config.db_name,\
                    self._client_config.exclude_pattern)

        # Make sure that self._client config is built, even if it doesn't have
        # any fields inside:
        if self._client_config is None:
            self._client_config = ClientConfig()

        # Set up menus:
        ui_path = "Edit/"
        self.menu_contexts = []
        self.menu_contexts.append(idaapi.add_menu_item(ui_path,
                                "FCatalog: Configure",
                                "",
                                0,
                                self._show_conf_form,
                                (None,)))

        self.menu_contexts.append(idaapi.add_menu_item(ui_path,
                                "FCatalog: Commit Functions",
                                "",
                                0,
                                self._commit_funcs,
                                (None,)))
        self.menu_contexts.append(idaapi.add_menu_item(ui_path,
                                "FCatalog: Find Similars",
                                "",
                                0,
                                self._find_similars,
                                (None,)))
        self.menu_contexts.append(idaapi.add_menu_item(ui_path,
                                "FCatalog: Clean IDB",
                                "",
                                0,
                                self._clean_idb,
                                (None,)))

        return idaapi.PLUGIN_KEEP

    def run(self,arg):
        pass

    def term(self):
        """
        Terminate plugin
        """
        for context in self.menu_contexts:
            idaapi.del_menu_item(context)
        return None


    def _commit_funcs(self,arg):
        """
        This function handles the event of clicking on "commit funcs" from the
        menu.
        """
        if self._fcc is None:
            print('Please configure FCatalog')
            return
        self._fcc.commit_funcs()

    def _find_similars(self,arg):
        """
        This function handles the event of clicking on "find similars" from the
        menu.
        """
        if self._fcc is None:
            print('Please configure FCatalog')
            return
        # Get the similarity cut from the user:
        similarity_cut = get_similarity_cut()

        # If the user has clicked cancel, we abort:
        if similarity_cut is None:
            print('Aborting find_similars.')
            return

        self._fcc.find_similars(similarity_cut)


    def _clean_idb(self,arg):
        """
        Clean the idb from fcatalog names or comments.
        """
        clean_idb()


    def _show_conf_form(self,arg):
        """
        Show the configuration form and update configuration values according
        to user choices.
        """
        # Create form
        cf = ConfForm()

        # Compile (in order to populate the controls)
        cf.Compile()

        # Populate form fields with current configuration values:
        if self._client_config.remote_host is not None:
            cf.host.value = self._client_config.remote_host
        if self._client_config.remote_port is not None:
            cf.port.value = str(self._client_config.remote_port)
        if self._client_config.db_name is not None:
            cf.db_name.value = self._client_config.db_name
        if self._client_config.exclude_pattern is not None:
            cf.exclude_pattern.value = self._client_config.exclude_pattern

        # Execute the form
        res = cf.Execute()
        if res == 1:
            # User pressed OK:

            is_conf_good = True

            # Extract host:
            host = cf.host.value
            if len(host) == 0:
                host = None
                is_conf_good = False
            self._client_config.remote_host = host

            # Extract port:
            try:
                port = int(cf.port.value)
            except ValueError:
                port = None
                is_conf_good = False
            self._client_config.remote_port = port

            # Extract db name:
            db_name = cf.db_name.value
            if len(db_name) == 0:
                db_name = None
                is_conf_good = False
            self._client_config.db_name = db_name

            # Extract exclude_pattern
            exclude_pattern = cf.exclude_pattern.value
            if len(exclude_pattern) == 0:
                exclude_pattern = None
            self._client_config.exclude_pattern = exclude_pattern

            if is_conf_good:
                save_config(self._client_config)
                self._fcc = FCatalogClient(\
                        (self._client_config.remote_host,\
                        self._client_config.remote_port),\
                        self._client_config.db_name,\
                        self._client_config.exclude_pattern)
                print('Configuration successful.')
            else:
                print('Invalid configuration.')
                self._fcc = None


        # Dispose the form
        cf.Free()


def PLUGIN_ENTRY():
    return FCatalogPlugin()