from __future__ import print_function

import sys
import warnings
from types import ModuleType
from contextlib import contextmanager
from multiprocessing import cpu_count
from distutils.version import StrictVersion

from .result import Result
from .._util import Capture, DummyBar
from ..error import Error, Missing, MultipleFragments, DuplicatedDescriptorName
from .context import Context
from .._version import __version__
from .descriptor import Descriptor, MissingValueException, is_descriptor_class

try:
    from tqdm import tqdm
    from .._util import NotebookWrapper
except ImportError:
    tqdm = NotebookWrapper = DummyBar

class Calculator(object):
    r"""descriptor calculator.

    Parameters:
        descs: see Calculator.register() method
        ignore_3D: see Calculator.register() method

    """

    __slots__ = (
        "_descriptors", "_name_dict", "_explicit_hydrogens", "_kekulizes", "_require_3D",
        "_cache", "_debug", "_progress_bar",
    )

    def __setstate__(self, dict):
        ds = self._descriptors = dict.get("_descriptors", [])
        self._name_dict = {str(d): d for d in ds}
        self._explicit_hydrogens = dict.get("_explicit_hydrogens", {True, False})
        self._kekulizes = dict.get("_kekulizes", {True, False})
        self._require_3D = dict.get("_require_3D", False)

    @classmethod
    def from_json(cls, obj):
        """Create Calculator from json descriptor objects.

        Parameters:
            obj(list or dict): descriptors to register

        Returns:
            Calculator: calculator

        """
        calc = cls()
        calc.register_json(obj)
        return calc

    def register_json(self, obj):
        """Register Descriptors from json descriptor objects.

        Parameters:
            obj(list or dict): descriptors to register

        """
        if not isinstance(obj, list):
            obj = [obj]

        self.register(Descriptor.from_json(j) for j in obj)

    def to_json(self):
        """Convert descriptors to json serializable data.

        Returns:
            list: descriptors

        """
        return [d.to_json() for d in self.descriptors]

    def __reduce_ex__(self, version):
        return self.__class__, (), {
            "_descriptors": self._descriptors,
            "_explicit_hydrogens": self._explicit_hydrogens,
            "_kekulizes": self._kekulizes,
            "_require_3D": self._require_3D,
        }

    def __getitem__(self, key):
        return self._name_dict[key]

    def __init__(self, descs=None, version=None, ignore_3D=False):
        if descs is None:
            descs = []

        self._descriptors = []
        self._name_dict = {}

        self._explicit_hydrogens = set()
        self._kekulizes = set()
        self._require_3D = False
        self._debug = False

        self.register(descs, version=version, ignore_3D=ignore_3D)

    @property
    def descriptors(self):
        r"""All descriptors.

        you can get/set/delete descriptor.

        Returns:
            tuple[Descriptor]: registered descriptors

        """
        return tuple(self._descriptors)

    @descriptors.setter
    def descriptors(self, descs):
        del self.descriptors
        self.register(descs)

    @descriptors.deleter
    def descriptors(self):
        self._descriptors = []
        self._name_dict = {}
        self._explicit_hydrogens.clear()
        self._kekulizes.clear()
        self._require_3D = False

    def __len__(self):
        return len(self._descriptors)

    def _register_one(self, desc, check_only=False, ignore_3D=False):
        if not isinstance(desc, Descriptor):
            raise ValueError("{!r} is not descriptor".format(desc))

        if ignore_3D and desc.require_3D:
            return

        self._explicit_hydrogens.add(bool(desc.explicit_hydrogens))
        self._kekulizes.add(bool(desc.kekulize))
        self._require_3D |= desc.require_3D

        for dep in (desc.dependencies() or {}).values():
            if isinstance(dep, Descriptor):
                self._register_one(dep, check_only=True)

        if not check_only:
            sdesc = str(desc)
            old = self._name_dict.get(sdesc)
            if old is not None:
                raise DuplicatedDescriptorName(desc, old)

            self._name_dict[sdesc] = desc
            self._descriptors.append(desc)

    def register(self, desc, version=None, ignore_3D=False):
        r"""Register descriptors.

        Descriptor-like:
            * Descriptor instance: self
            * Descriptor class: use Descriptor.preset() method
            * module: use Descriptor-likes in module
            * Iterable: use Descriptor-likes in Iterable

        Parameters:
            desc(Descriptor-like): descriptors to register
            version(str): version
            ignore_3D(bool): ignore 3D descriptors

        """
        if version is None:
            version = __version__

        version = StrictVersion(version)
        return self._register(desc, version, ignore_3D)

    def _register(self, desc, version, ignore_3D):
        if not hasattr(desc, "__iter__"):
            if is_descriptor_class(desc):
                if desc.since > version:
                    return

                for d in desc.preset(version=version):
                    self._register_one(d, ignore_3D=ignore_3D)

            elif isinstance(desc, ModuleType):
                self._register(
                    get_descriptors_in_module(desc),
                    version=version,
                    ignore_3D=ignore_3D,
                )

            else:
                self._register_one(desc, ignore_3D=ignore_3D)

        else:
            for d in desc:
                self._register(d, version=version, ignore_3D=ignore_3D)

    def _calculate_one(self, cxt, desc, reset):
        if desc in self._cache:
            return self._cache[desc]

        if reset:
            cxt.reset()
        desc._context = cxt
        cxt.add_stack(desc)

        if desc.require_connected and desc._context.n_frags != 1:
            return False, Missing(MultipleFragments(), desc._context.get_stack())

        args = {}
        for name, dep in (desc.dependencies() or {}).items():
            if dep is None:
                args[name] = None
            else:
                ok, r = self._calculate_one(cxt, dep, False)
                if ok:
                    args[name] = r
                else:
                    return False, r

        ok = False
        try:
            r = desc.calculate(**args)
            if self._debug:
                self._check_rtype(desc, r)
            ok = True
        except MissingValueException as e:
            r = Missing(e.error, desc._context.get_stack())
        except Exception as e:
            r = Error(e, desc._context.get_stack())

        self._cache[desc] = ok, r

        return ok, r

    def _check_rtype(self, desc, result):
        if desc.rtype is None:
            return

        if isinstance(result, Error):
            return

        if not isinstance(result, desc.rtype):
            raise TypeError("{} not match {}".format(result, desc.rtype))

    def _calculate(self, cxt):
        self._cache = {}
        for desc in self.descriptors:
            _, r = self._calculate_one(cxt, desc, True)
            yield r

    def __call__(self, mol, id=-1):
        r"""Calculate descriptors.

        :type mol: rdkit.Chem.Mol
        :param mol: molecular

        :type id: int
        :param id: conformer id

        :rtype: Result[scalar or Error]
        :returns: iterator of descriptor and value
        """
        return self._wrap_result(
            mol,
            self._calculate(Context.from_calculator(self, mol, id)),
        )

    def _wrap_result(self, mol, r):
        return Result(mol, r, self._descriptors)

    def _serial(self, mols, nmols, quiet, ipynb, id):
        with self._progress(quiet, nmols, ipynb) as bar:
            for m in mols:
                with Capture() as capture:
                    r = self._wrap_result(m, self._calculate(Context.from_calculator(self, m, id)))

                for e in capture.result:
                    e = e.rstrip()
                    if not e:
                        continue

                    bar.write(e, file=capture.orig)

                yield r
                bar.update()

    @contextmanager
    def _progress(self, quiet, total, ipynb):
        args = {
            "dynamic_ncols": True,
            "leave": True,
            "total": total,
        }

        if quiet:
            Bar = DummyBar
        elif ipynb:
            Bar = NotebookWrapper
        else:
            Bar = tqdm

        try:
            with Bar(**args) as self._progress_bar:
                yield self._progress_bar
        finally:
            if hasattr(self, "_progress_bar"):
                del self._progress_bar

    def echo(self, s, file=sys.stdout, end="\n"):
        """Output message.

        Parameters:
            s(str): message to output
            file(file-like): output to
            end(str): end mark of message

        Return:
            None

        """
        p = getattr(self, "_progress_bar", None)
        if p is not None:
            p.write(s, file=file, end="\n")
            return

        print(s, file=file, end="\n")  # noqa: T003

    def map(self, mols, nproc=None, nmols=None, quiet=False, ipynb=False, id=-1):
        r"""Calculate descriptors over mols.

        Parameters:
            mols(Iterable[rdkit.Mol]): moleculars

            nproc(int): number of process to use. default: multiprocessing.cpu_count()

            nmols(int): number of all mols to use in progress-bar. default: mols.__len__()

            quiet(bool): don't show progress bar. default: False

            ipynb(bool): use ipython notebook progress bar. default: False

            id(int): conformer id to use. default: -1.

        Returns:
            Iterator[Result[scalar]]

        """
        if nproc is None:
            nproc = cpu_count()

        if hasattr(mols, "__len__"):
            nmols = len(mols)

        if nproc == 1:
            return self._serial(mols, nmols=nmols, quiet=quiet, ipynb=ipynb, id=id)
        else:
            return self._parallel(mols, nproc, nmols=nmols, quiet=quiet, ipynb=ipynb, id=id)

    def pandas(self, mols, nproc=None, nmols=None, quiet=False, ipynb=False, id=-1):
        r"""Calculate descriptors over mols.

        Returns:
            pandas.DataFrame

        """
        from .pandas_module import MordredDataFrame, Series

        if isinstance(mols, Series):
            index = mols.index
        else:
            index = None

        return MordredDataFrame(
            (list(r) for r in self.map(mols, nproc, nmols, quiet, ipynb, id)),
            columns=[str(d) for d in self.descriptors],
            index=index,
        )

def get_descriptors_from_module(mdl, submodule=False):
    r"""[DEPRECATED] Get descriptors from module.

    Parameters:
        mdl(module): module to search

    Returns:
        [Descriptor]

    """
    warnings.warn("use get_descriptors_in_module", DeprecationWarning)
    __all__ = getattr(mdl, "__all__", None)
    if __all__ is None:
        __all__ = dir(mdl)

    all_functions = (getattr(mdl, name) for name in __all__ if name[:1] != "_")

    if submodule:
        descs = [
            d
            for fn in all_functions
            if is_descriptor_class(fn) or isinstance(fn, ModuleType)
            for d in (
                [fn] if is_descriptor_class(fn)
                else get_descriptors_from_module(fn, submodule=True)
            )
        ]
    else:
        descs = [
            fn
            for fn in all_functions
            if is_descriptor_class(fn)
        ]

    return descs

def get_descriptors_in_module(mdl, submodule=True):
    r"""Get descriptors in module.

    Parameters:
        mdl(module): module to search
        submodule(bool): search recursively

    Returns:
        Iterator[Descriptor]

    """
    __all__ = getattr(mdl, "__all__", None)
    if __all__ is None:
        __all__ = dir(mdl)

    all_values = (getattr(mdl, name) for name in __all__ if name[:1] != "_")

    if submodule:
        for v in all_values:
            if is_descriptor_class(v):
                yield v
            if isinstance(v, ModuleType):
                for v in get_descriptors_in_module(v, submodule=True):
                    yield v

    else:
        for v in all_values:
            if is_descriptor_class(v):
                yield v


import inspect
import operator
from abc import ABCMeta, abstractmethod
from contextlib import contextmanager
from distutils.version import StrictVersion

import six
import numpy as np
from rdkit import Chem

if hasattr(inspect, "getfullargspec"):
    def getargs(func):
        return tuple(inspect.getfullargspec(func).args[1:])

else:
    def getargs(func):
        try:
            return tuple(inspect.getargspec(func).args[1:])
        except TypeError:
            return ()

class MissingValueException(Exception):
    """Internally used exception."""

    __slots__ = ("error",)

    def __init__(self, error):
        self.error = error

class DescriptorMeta(ABCMeta):
    def __new__(cls, classname, bases, dict):
        __init__ = dict.get("__init__")
        if __init__ is None:
            for base in bases:
                __init__ = getattr(base, "__init__", None)
                if __init__ is not None:
                    break

        dict["parameter_names"] = getargs(__init__)
        if "since" in dict:
            dict["since"] = StrictVersion(dict["since"])

        return ABCMeta.__new__(cls, classname, bases, dict)

class Descriptor(six.with_metaclass(DescriptorMeta, object)):
    r"""Abstract base class of descriptors.

    Attributes:
        mol(rdkit.Mol): target molecule

    """

    __slots__ = ("_context",)

    explicit_hydrogens = True
    kekulize = False
    require_connected = False
    require_3D = False

    def __reduce_ex__(self, version):
        return self.__class__, self.parameters()

    def description(self):
        pass

    @classmethod
    def preset(cls, version):
        r"""Generate preset descriptor instances.

        Returns:
            Iterable[Descriptor]: preset descriptors

        """
        return ()

    @abstractmethod
    def parameters(self):
        """[abstractmethod] get __init__ arguments of this descriptor instance.

        this method used in pickling and identifying descriptor instance.

        Returns:
            tuple: tuple of __init__ arguments

        """
        raise NotImplementedError("not implemented Descriptor.parameters method")

    def get_parameter_dict(self):
        return dict(zip(self.parameter_names, self.parameters()))

    def to_json(self):
        """Convert to json serializable dictionary.

        Returns:
            dict: dictionary of descriptor

        """
        d, ps = self._to_json()
        if len(ps) == 0:
            return {"name": d}
        else:
            return {"name": d, "args": ps}

    def _to_json(self):
        d = self.__class__.__name__
        ps = self.get_parameter_dict()

        return d, {k: getattr(v, "as_argument", v) for k, v in ps.items()}

    @abstractmethod
    def calculate(self):
        r"""[abstractmethod] calculate descriptor value.

        Returns:
            rtype

        """
        raise NotImplementedError("not implemented Descriptor.calculate method")

    def dependencies(self):
        r"""Descriptor dependencies.

        Returns:
            dict[str, Descriptor or None] or None

        """
        pass

    @property
    def as_argument(self):
        """Argument representation of descriptor.

        Returns:
            any

        """
        return self

    @staticmethod
    def _pretty(v):
        v = getattr(v, "as_argument", v)
        return repr(v)

    def __repr__(self):
        return "{}.{}({})".format(
            self.__class__.__module__,
            self.__class__.__name__,
            ", ".join(self._pretty(a) for a in self.parameters()),
        )

    def __hash__(self):
        return hash((self.__class__, self.parameters()))

    def __compare_by_reduce(meth):
        def compare(self, other):
            L = self.__class__, self.parameters()
            r = other.__class__, other.parameters()
            return getattr(L, meth)(r)

        return compare

    __eq__ = __compare_by_reduce("__eq__")
    __ne__ = __compare_by_reduce("__ne__")

    __lt__ = __compare_by_reduce("__lt__")
    __gt__ = __compare_by_reduce("__gt__")
    __le__ = __compare_by_reduce("__le__")
    __ge__ = __compare_by_reduce("__ge__")

    rtype = None

    @property
    def mol(self):
        """Get molecule.

        Returns:
            rdkit.Mol

        """
        return self._context.get_mol(self)

    @property
    def coord(self):
        """Get 3D coordinate.

        Returns:
            numpy.array[3, N]: coordinate matrix

        """
        if not self.require_3D:
            self.fail(AttributeError("use 3D coordinate in 2D descriptor"))

        return self._context.get_coord(self)

    def get_3D_mol(self):
        mol = Chem.Mol(self.mol)
        conf = Chem.Conformer(mol.GetNumAtoms())
        for i, xyz in enumerate(self.coord):
            conf.SetAtomPosition(i, xyz)

        mol.AddConformer(conf)
        return mol

    def fail(self, exception):
        """Raise known exception and return missing value.

        Raises:
            MissingValueException

        """
        raise MissingValueException(exception)

    @contextmanager
    def rethrow_zerodiv(self):
        """[contextmanager] treat zero div as known exception."""
        with np.errstate(divide="raise", invalid="raise"):
            try:
                yield
            except (FloatingPointError, ZeroDivisionError) as e:
                self.fail(ZeroDivisionError(*e.args))

    @contextmanager
    def rethrow_na(self, exception):
        """[contextmanager] treat any exceptions as known exception."""
        try:
            yield
        except exception as e:
            self.fail(e)

    def _unary_common(name, operator):
        def unary(self):
            return UnaryOperatingDescriptor(name.format(self), operator, self)

        return unary

    def _binary_common(name, operator):
        def binary(self, other):
            if not isinstance(other, Descriptor):
                other = ConstDescriptor(other)

            return BinaryOperatingDescriptor(name.format(self, other), operator, self, other)

        return binary

    __add__ = _binary_common("({}+{})", "+")
    __sub__ = _binary_common("({}-{})", "-")
    __mul__ = _binary_common("({}*{})", "*")
    __truediv__ = _binary_common("({}/{})", "/")
    __floordiv__ = _binary_common("({}//{})", "//")
    __mod__ = _binary_common("({}%{})", "%")
    __pow__ = _binary_common("({}**{})", "**")

    __neg__ = _unary_common("-{}", "-")
    __pos__ = _unary_common("+{}", "+")
    __abs__ = _unary_common("|{}|", "abs")
    __trunc__ = _unary_common("trunc({})", "trunc")

    if six.PY3:
        __ceil__ = _unary_common("ceil({})", "ceil")
        __floor__ = _unary_common("floor({})", "floor")

def is_descriptor_class(desc, include_abstract=False):
    r"""Check calculatable descriptor class or not.

    Returns:
        bool

    """
    return (
        isinstance(desc, type)
        and issubclass(desc, Descriptor)
        and (True if include_abstract else not inspect.isabstract(desc))
    )

class UnaryOperatingDescriptor(Descriptor):
    @classmethod
    def preset(cls, version):
        return cls()

    operators = {
        "+": operator.pos,
        "-": operator.neg,
        "abs": operator.abs,
        "trunc": np.trunc,
        "ceil": np.ceil,  # noqa: S001
        "floor": np.floor,
    }

    def parameters(self):
        return self._name, self._operator, self._value

    def __init__(self, name, operator, value):
        self._name = name
        self._operator = operator
        self._fn = self.operators[operator]
        self._value = value

    def _to_json(self):
        return self.__class__.__name__, {
            "name": self._name,
            "operator": self._operator,
            "value": self._value.to_json(),
        }

    def __str__(self):
        return self._name

    def dependencies(self):
        return {
            "value": self._value,
        }

    def calculate(self, value):
        return self._fn(value)

class ConstDescriptor(Descriptor):
    @classmethod
    def preset(cls, version):
        return cls()

    def parameters(self):
        return (self._value,)

    def __init__(self, value):
        self._value = value

    def __str__(self):
        return str(self._value)

    def calculate(self):
        return self._value

class BinaryOperatingDescriptor(Descriptor):
    @classmethod
    def preset(cls, version):
        return cls()

    operators = {
        "+": operator.add,
        "-": operator.sub,
        "*": operator.mul,  # noqa: S001
        "/": operator.truediv,
        "//": operator.floordiv,
        "%": operator.mod,  # noqa: S001
        "**": operator.pow,
    }

    def _to_json(self):
        return self.__class__.__name__, {
            "name": self._name,
            "operator": self._operator,
            "left": self._left.to_json(),  # noqa: S001
            "right": self._right.to_json(),
        }

    def parameters(self):
        return self._name, self._operator, self._left, self._right

    def __init__(self, name, operator, left, right):
        self._name = name
        self._operator = operator
        self._fn = self.operators[operator]
        self._left = left
        self._right = right

    def __str__(self):
        return self._name

    def dependencies(self):
        return {
            "left": self._left,
            "right": self._right,
        }

    def calculate(self, left, right):
        return self._fn(left, right)


from __future__ import division

from collections import defaultdict

import numpy as np

from ._mesh import SphereMesh
from .._util import atoms_to_numpy
from .._atomic_property import GetAtomicNumber, vdw_radii

class SurfaceArea(object):
    r"""calculate solvent accessible surface area.

    :type radiuses: np.ndarray(dtype=float, shape=(N,))
    :param radiuses: atomic radius + solvent radius vector

    :type xyzs: np.ndarray(dtype=float, shape=(N, 3))
    :param xyzs: atomic position matrix

    :type level: int
    :param level: mesh level. subdivide icosahedron n-1 times.

        .. math::

            N_{\rm points} = 5 \times 4^{level} - 8
    """

    def __init__(self, radiuses, xyzs, level=4):
        self.rads = radiuses
        self.rads2 = radiuses ** 2
        self.xyzs = xyzs
        self._gen_neighbor_list()
        self.sphere = SphereMesh(level).vertices.T

    def _gen_neighbor_list(self):
        r = self.rads[:, np.newaxis] + self.rads

        d = np.sqrt(
            np.sum(
                (self.xyzs[:, np.newaxis] - self.xyzs) ** 2,
                axis=2,
            ),
        )

        ns = defaultdict(list)
        for i, j in np.transpose(np.nonzero(d <= r)):
            if i == j:
                continue

            ns[i].append((j, d[i, j]))

        for _, l in ns.items():
            l.sort(key=lambda i: i[1])

        self.neighbors = ns

    def atomic_sa(self, i):
        r"""Calculate atomic surface area.

        :type i: int
        :param i: atom index

        :rtype: float
        """
        sa = 4.0 * np.pi * self.rads2[i]

        neighbors = self.neighbors.get(i)

        if neighbors is None:
            return sa

        XYZi = self.xyzs[i, np.newaxis].T

        sphere = self.sphere * self.rads[i] + XYZi
        N = sphere.shape[1]

        for j, _ in neighbors:
            XYZj = self.xyzs[j, np.newaxis].T

            d2 = (sphere - XYZj) ** 2
            mask = (d2[0] + d2[1] + d2[2]) > self.rads2[j]
            sphere = np.compress(mask, sphere, axis=1)

        return sa * sphere.shape[1] / N

    def surface_area(self):
        r"""Calculate all atomic surface area.

        :rtype: [float]
        """
        return [self.atomic_sa(i) for i in range(len(self.rads))]

    @classmethod
    def from_mol(cls, mol, conformer=-1, solvent_radius=1.4, level=4):
        r"""Construct SurfaceArea from rdkit Mol type.

        :type mol: rdkit.Chem.Mol
        :param mol: input molecule

        :type conformer: int
        :param conformer: conformer id

        :type solvent_radius: float
        :param solvent_radius: solvent radius

        :type level: int
        :param level: mesh level

        :rtype: SurfaceArea
        """
        rs = atoms_to_numpy(lambda a: vdw_radii[a.GetAtomicNum()] + solvent_radius, mol)

        conf = mol.GetConformer(conformer)

        ps = np.array([list(conf.GetAtomPosition(i)) for i in range(mol.GetNumAtoms())])

        return cls(rs, ps, level)

    @classmethod
    def from_pdb(cls, pdb, solvent_radius=1.4, level=3):
        try:
            from Bio.PDB import PDBParser
        except ImportError:
            raise ImportError("There isn't biopython package.")

        rs = []
        coords = []

        for atom in PDBParser().get_structure("", pdb).get_atoms():
            rs.append(vdw_radii[GetAtomicNumber(atom.element)] + solvent_radius)
            coords.append(atom.coord)

        return cls(np.array(rs), np.array(coords), level)


"""Mordred base package."""

from .util import is_missing
from .result import Result
from ..error import MissingValueBase
from .parallel import parallel
from .calculator import Calculator, get_descriptors_in_module, get_descriptors_from_module
from .descriptor import (
    Descriptor, ConstDescriptor, UnaryOperatingDescriptor, BinaryOperatingDescriptor,
)

__all__ = (
    "Descriptor",
    "Calculator",
    "get_descriptors_from_module",
    "get_descriptors_in_module",
    "is_missing",
    "Result",
)

def _Descriptor__call__(self, mol, id=-1):
    r"""Calculate single descriptor value.

    :type id: int
    :param id: conformer id

    :returns: descriptor result
    :rtype: scalar
    """
    v = Calculator(self)(mol, id)[0]
    if isinstance(v, MissingValueBase):
        raise v.error

    return v

def _from_json(obj, descs):
    name = obj.get("name")
    args = obj.get("args") or {}
    if name is None:
        raise ValueError("invalid json: {}".format(obj))

    if name == UnaryOperatingDescriptor.__name__:
        return UnaryOperatingDescriptor(
            args["name"],
            args["operator"],
            _from_json(args["value"]),
        )

    elif name == BinaryOperatingDescriptor.__name__:
        return BinaryOperatingDescriptor(
            args["name"],
            args["operator"],
            _from_json(args["left"]),
            _from_json(args["right"]),
        )

    cls = descs.get(name)
    if cls is None:
        raise ValueError("unknown class: {}".format(name))

    instance = cls(**(obj.get("args") or {}))
    return instance

@classmethod
def _Descriptor_from_json(self, obj):
    """Create Descriptor instance from json dict.

    Parameters:
        obj(dict): descriptor dict

    Returns:
        Descriptor: descriptor

    """
    descs = getattr(self, "_all_descriptors", None)

    if descs is None:
        from mordred import descriptors
        descs = {
            cls.__name__: cls
            for cls in get_descriptors_in_module(descriptors)
        }
        descs[ConstDescriptor.__name__] = ConstDescriptor
        self._all_descriptors = descs

    return _from_json(obj, descs)

Descriptor.__call__ = _Descriptor__call__
Descriptor.from_json = _Descriptor_from_json
Calculator._parallel = parallel


import numpy as np
from six import string_types, integer_types

from .util import is_missing
from .descriptor import Descriptor

class Result(object):
    r"""Result type."""

    __slots__ = ("mol", "_values", "_descriptors", "_name_to_value")

    def __init__(self, mol, r, d):
        self.mol = mol
        self._values = list(r)
        self._descriptors = list(d)
        self._name_to_value = None

    def __str__(self):
        return "{}({{{}}})".format(
            self.__class__.__name__,
            ", ".join("'{}': {}".format(k, v) for k, v in zip(self._descriptors, self._values)),
        )

    def __repr__(self):
        return "{}({!r},{!r},{!r})".format(
            self.__class__.__name__,
            self.mol,
            self._values,
            self._descriptors,
        )

    def fill_missing(self, value=np.nan):
        r"""Replace missing value to "value".

        Parameters:
            value: value that missing value is replaced

        Returns:
            Result

        """
        return self.__class__(
            self.mol,
            [(value if is_missing(v) else v) for v in self.values()],
            self.keys(),
        )

    def drop_missing(self):
        r"""Delete missing value.

        Returns:
            Result

        """
        newvalues = []
        newdescs = []
        for d, v in self.items():
            if not is_missing(v):
                newvalues.append(v)
                newdescs.append(d)

        return self.__class__(self.mol, newvalues, newdescs)

    def items(self):
        r"""Get items.

        Returns:
            Iterable[(Descriptor, value)]

        """
        return ((k, v) for k, v in zip(self.keys(), self.values()))

    def keys(self):
        r"""Get descriptors instances.

        Returns:
            Iterable[Descriptor]

        """
        return iter(self._descriptors)

    def values(self):
        r"""Get descriptor values.

        Returns:
            Iterable[value]

        """
        return iter(self._values)

    __iter__ = values

    def __reversed__(self):
        return reversed(self._values)

    def asdict(self, rawkey=False):
        r"""Convert Result to dict.

        Parameters:
            rawkey(bool):
                * True: dict key is Descriptor instance
                * False: dict key is str

        Returns:
            dict

        """
        if rawkey:
            return dict(self.items())
        else:
            return {
                str(k): v
                for k, v in self.items()
            }

    @property
    def ix(self):
        r"""Access descriptor value by index.

        >>> from mordred import Calculator, Lipinski
        >>> from rdkit import Chem
        >>> result = Calculator(Lipinski.Lipinski)(Chem.MolFromSmiles("C1CCCCC1"))
        >>> result.ix[0]
        True
        """
        return GetValueByIndex(self._values)

    @property
    def name(self):
        r"""Access descriptor value by descriptor name or instance.

        >>> from mordred import Calculator, descriptors
        >>> from rdkit import Chem
        >>> result = Calculator(descriptors)(Chem.MolFromSmiles("C1CCCCC1"))
        >>> result.name["C2SP3"]
        6

        """
        if self._name_to_value is None:
            self._name_to_value = {str(d): v for d, v in zip(self._descriptors, self._values)}

        return GetValueByName(self._name_to_value)

    def __getitem__(self, key):
        if isinstance(key, (integer_types, slice)):
            return self.ix[key]

        elif isinstance(key, (string_types, Descriptor)):
            return self.name[key]

        else:
            raise TypeError(
                "Result indices must be "
                "integers, slices, strings or Descriptor instance, "
                "not {}".format(key.__class__.__name__))

    def __len__(self):
        return len(self._descriptors)

class GetValueByIndex(object):
    __slots__ = ("_values",)

    def __init__(self, values):
        self._values = values

    def __getitem__(self, key):
        return self._values[key]

class GetValueByName(object):
    __slots__ = ("_name_to_value",)

    def __init__(self, name_to_value):
        self._name_to_value = name_to_value

    def __getitem__(self, key):
        return self._name_to_value[str(key)]