# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************

import inspect
from pathlib import Path
from importlib import import_module
from frappy.core import Module, Parameter, Property
from frappy.datatypes import DataType
from frappy.lib.comparestring import compare
from frappy.tools.configdata import site, ClassChecker, ModuleClass
from frappy.tools.terminalgui import Completion


SKIP_PROPS = {'implementation', 'features',
    'interface_classes', 'slowinterval', 'omit_unchanged_within', 'original_id'}


def recommended_prs(cls):
    """get properties and parameters which are useful in configuration

    returns a dict <pname> of <is mandatory (bool)>
    """
    if isinstance(cls, str):
        try:
            cls = ClassCompletion.validate(cls)
        except Exception:
            return {}
    result = {}
    for pname, pdict in cls.configurables.items():
        pr = getattr(cls, pname)
        if isinstance(pr, Property):
            if pname not in SKIP_PROPS:
                result[pname] = pr.mandatory
        elif isinstance(pr, Parameter):
            if pr.needscfg:
                result[pname] = True
            elif not pr.readonly or hasattr(cls, f'write_{pname}'):
                result[pname] = False
    if result.get('uri') is False and result.get('io') is False:
        result['io'] = True
    return result


class FrappyModule(str):
    """checker for finding subclasses of Module defined in a python module"""
    def check(self, cls):
        return isinstance(cls, type) and issubclass(cls, Module) and self.startswith(cls.__module__)


def get_suggested(guess, allowed_keys):
    """select and sort values from allowed_keys based on similarity to a given value

    :param guess: given value. when empty, the allowed keys are return in the given order
    :param allowed_keys: list of values sorted in a given order
    :return: a list of values
    """
    if len(guess) == 0:
        return allowed_keys
    low = guess.lower()
    if len(guess) == 1:
        # return only items starting with a single letter
        return [k for k in allowed_keys if k.lower().startswith(low)]
    comp = {}
    # if there are items containing guess, return them only
    result = [k for k in allowed_keys if low in k.lower()]
    if result:
        return result
    # calculate similarity
    for key in allowed_keys:
        comp[key] = compare(guess, key)
    comp = sorted(comp.items(), key=lambda t: t[1], reverse=True)
    scorelimit = 2
    result = []
    for i, (key, score) in enumerate(comp):
        if score < scorelimit:
            break
        if i > 2:
            scorelimit = max(2, score - 0.05)
        result.append(key)
    return result or allowed_keys


def class_completion(value):
    """analyze class path and return an array of suggestions for
    the last element not matching"""
    checker = ClassChecker(value)
    if not checker.error:
        return checker.position, []
    if checker.root is None:
        sdict = {p: f'{p}.' for p in site.packages}
    else:
        sdict = {}
        if not checker.clsobj:
            file = checker.pyfile
            if file.name == '__init__.py':
                sdict = {p.stem: f'{p.stem}.'
                         for p in sorted(file.parent.glob('*.py'))
                         if p.stem != '__init__'}
        sdict.update((k, k) for k, v in sorted(inspect.getmembers(
            checker.root, FrappyModule(checker.modname).check)))
    found = sdict.get(checker.name, None)
    if found:
        selection = [found]
        # selection = [found] + list(sdict.values())
    else:
        selection = list(get_suggested(checker.name, sdict.values()))
    return checker.position, [checker.name] + selection


class NameCompletion(Completion, DataType):
    # TODO: make obsolete
    def __init__(self, callback, get_name_info):
        self.callback = callback
        self.get_name_info = get_name_info
        super().__init__()

    def get_selection(self, param):
        cls, used_names = self.get_name_info()
        if param:
            names = dict(self.cls.configurables)
            selection = []
            for name in recommended_prs(self.cls):
                names.pop(name, None)
                if name not in used_names:
                    selection.append(name)
            selection.extend(k for k in names if k not in used_names)
        else:
            names = dict(cls.configurables.get(param, {}))
            selection = [k for k in names if f'{param}.{k}' not in used_names]
        return selection

    def propose(self, value):
        """analyze value to propositions of class path

        returns the length of the valid part and a list of guesses
        """
        param, dot, prop = value.partition('.')
        if (param and not param.isidentifier()) or (prop and not prop.isidentifier()):
            return 0, None
        cls, used_names = self.get_name_info()
        if dot:
            print(param, repr(dot), prop, list(cls.configurables.get(param, {})))
            position = len(param) + 1
            selection = [k for k in cls.configurables.get(param, {}) if f'{param}.{k}' not in used_names]
            guess = prop
        else:
            position = 0
            selection = {}
            for name in recommended_prs(cls):
                if name not in used_names:
                    selection[name] = 1
            selection.update({k: 1 for k in cls.configurables if k not in used_names})
            for k, v in cls.configurables.items():
                print(k, getattr(v, 'needscfg', ''))
            guess = param
        selection = get_suggested(guess, selection)
        return position, [guess] + selection
