"""Refactoring methods for elpy.

This interfaces directly with rope, regardless of the backend used,
because the other backends don't really offer refactoring choices.
Once Jedi is similarly featureful as Rope we can try and offer both.


# Too complex:

- Restructure: Interesting, but too complex, and needs deep Rope
  knowledge to do well.

- ChangeSignature: Slightly less complex interface, but still to
  complex, requiring a large effort for the benefit.


# Too useless:

I could not get these to work in any useful fashion. I might be doing
something wrong.

- ExtractVariable does not replace the code extracted with the
  variable, making it a glorified copy&paste function. Emacs can do
  better than this interface by itself.

- EncapsulateField: Getter/setter methods are outdated, this should be
  using properties.

- IntroduceFactory: Inserts a trivial method to the current class.
  Cute.

- IntroduceParameter: Introduces a parameter correctly, but does not
  replace the old code with the parameter. So it just edits the
  argument list and adds a shiny default.

- LocalToField: Seems to just add "self." in front of all occurrences
  of a variable in the local scope.

- MethodObject: This turns the current method into a callable
  class/object. Not sure what that would be good for.


# Can't even get to work:

- ImportOrganizer expand_star_imports, handle_long_imports,
  relatives_to_absolutes: Seem not to do anything.

- create_move: I was not able to figure out what it would like to see
  as its attrib argument.

"""

import os

from elpy.rpc import Fault

try:
    from rope.base.exceptions import RefactoringError
    from rope.base.project import Project
    from rope.base.libutils import path_to_resource
    from rope.base import change as rope_change
    from rope.base import worder
    from rope.refactor.importutils import ImportOrganizer
    from rope.refactor.topackage import ModuleToPackage
    from rope.refactor.rename import Rename
    from rope.refactor.move import create_move
    from rope.refactor.inline import create_inline
    from rope.refactor.extract import ExtractMethod
    from rope.refactor.usefunction import UseFunction
    ROPE_AVAILABLE = True
except ImportError:
    ROPE_AVAILABLE = False


def options(description, **kwargs):
    """Decorator to set some options on a method."""
    def set_notes(function):
        function.refactor_notes = {'name': function.__name__,
                                   'category': "Miscellaneous",
                                   'description': description,
                                   'doc': getattr(function, '__doc__',
                                                  ''),
                                   'args': []}
        function.refactor_notes.update(kwargs)
        return function
    return set_notes


class Refactor(object):
    """The main refactoring interface.

    Once initialized, the first call should be to get_refactor_options
    to get a list of refactoring options at a given position. The
    returned value will also list any additional options required.

    Once you picked one, you can call get_changes to get the actual
    refactoring changes.

    """
    def __init__(self, project_root, filename):
        self.project_root = project_root
        if not ROPE_AVAILABLE:
            raise Fault('rope not installed, cannot refactor code.',
                        code=400)
        if not os.path.exists(project_root):
            raise Fault(
                "cannot do refactoring without a local project root",
                code=400
            )
        self.project = Project(project_root, ropefolder=None)
        self.resource = path_to_resource(self.project, filename)

    def get_refactor_options(self, start, end=None):
        """Return a list of options for refactoring at the given position.

        If `end` is also given, refactoring on a region is assumed.

        Each option is a dictionary of key/value pairs. The value of
        the key 'name' is the one to be used for get_changes.

        The key 'args' contains a list of additional arguments
        required for get_changes.

        """
        result = []
        for symbol in dir(self):
            if not symbol.startswith("refactor_"):
                continue
            method = getattr(self, symbol)
            if not method.refactor_notes.get('available', True):
                continue
            category = method.refactor_notes['category']
            if end is not None and category != 'Region':
                continue
            if end is None and category == 'Region':
                continue
            is_on_symbol = self._is_on_symbol(start)
            if not is_on_symbol and category in ('Symbol', 'Method'):
                continue
            requires_import = method.refactor_notes.get('only_on_imports',
                                                        False)
            if requires_import and not self._is_on_import_statement(start):
                continue
            result.append(method.refactor_notes)
        return result

    def _is_on_import_statement(self, offset):
        "Does this offset point to an import statement?"
        data = self.resource.read()
        bol = data.rfind("\n", 0, offset) + 1
        eol = data.find("\n", 0, bol)
        if eol == -1:
            eol = len(data)
        line = data[bol:eol]
        line = line.strip()
        if line.startswith("import ") or line.startswith("from "):
            return True
        else:
            return False

    def _is_on_symbol(self, offset):
        "Is this offset on a symbol?"
        if not ROPE_AVAILABLE:
            return False
        data = self.resource.read()
        if offset >= len(data):
            return False
        if data[offset] != '_' and not data[offset].isalnum():
            return False
        word = worder.get_name_at(self.resource, offset)
        if word:
            return True
        else:
            return False

    def get_changes(self, name, *args):
        """Return a list of changes for the named refactoring action.

        Changes are dictionaries describing a single action to be
        taken for the refactoring to be successful.

        A change has an action and possibly a type. In the description
        below, the action is before the slash and the type after it.

        change: Change file contents
        - file: The path to the file to change
        - contents: The new contents for the file
        - Diff: A unified diff showing the changes introduced

        create/file: Create a new file
        - file: The file to create

        create/directory: Create a new directory
        - path: The directory to create

        move/file: Rename a file
        - source: The path to the source file
        - destination: The path to the destination file name

        move/directory: Rename a directory
        - source: The path to the source directory
        - destination: The path to the destination directory name

        delete/file: Delete a file
        - file: The file to delete

        delete/directory: Delete a directory
        - path: The directory to delete

        """
        if not name.startswith("refactor_"):
            raise ValueError("Bad refactoring name {0}".format(name))
        method = getattr(self, name)
        if not method.refactor_notes.get('available', True):
            raise RuntimeError("Method not available")
        return method(*args)

    @options("Convert from x import y to import x.y as y", category="Imports",
             args=[("offset", "offset", None)],
             only_on_imports=True,
             available=ROPE_AVAILABLE)
    def refactor_froms_to_imports(self, offset):
        """Converting imports of the form "from ..." to "import ..."."""
        refactor = ImportOrganizer(self.project)
        changes = refactor.froms_to_imports(self.resource, offset)
        return translate_changes(changes)

    @options("Reorganize and clean up", category="Imports",
             available=ROPE_AVAILABLE)
    def refactor_organize_imports(self):
        """Clean up and organize imports."""
        refactor = ImportOrganizer(self.project)
        changes = refactor.organize_imports(self.resource)
        return translate_changes(changes)

    @options("Convert the current module into a package", category="Module",
             available=ROPE_AVAILABLE)
    def refactor_module_to_package(self):
        """Convert the current module into a package."""
        refactor = ModuleToPackage(self.project, self.resource)
        return self._get_changes(refactor)

    @options("Rename symbol at point", category="Symbol",
             args=[("offset", "offset", None),
                   ("new_name", "string", "Rename to: "),
                   ("in_hierarchy", "boolean",
                    "Rename in super-/subclasses as well? "),
                   ("docs", "boolean",
                    "Replace occurences in docs and strings? ")
                   ],
             available=ROPE_AVAILABLE)
    def refactor_rename_at_point(self, offset, new_name, in_hierarchy, docs):
        """Rename the symbol at point."""
        try:
            refactor = Rename(self.project, self.resource, offset)
        except RefactoringError as e:
            raise Fault(str(e), code=400)
        return self._get_changes(refactor, new_name,
                                 in_hierarchy=in_hierarchy, docs=docs)

    @options("Rename current module", category="Module",
             args=[("new_name", "string", "Rename to: ")],
             available=ROPE_AVAILABLE)
    def refactor_rename_current_module(self, new_name):
        """Rename the current module."""
        refactor = Rename(self.project, self.resource, None)
        return self._get_changes(refactor, new_name)

    @options("Move the current module to a different package",
             category="Module",
             args=[("new_name", "directory", "Destination package: ")],
             available=ROPE_AVAILABLE)
    def refactor_move_module(self, new_name):
        """Move the current module."""
        refactor = create_move(self.project, self.resource)
        resource = path_to_resource(self.project, new_name)
        return self._get_changes(refactor, resource)

    @options("Inline function call at point", category="Symbol",
             args=[("offset", "offset", None),
                   ("only_this", "boolean", "Only this occurrence? ")],
             available=ROPE_AVAILABLE)
    def refactor_create_inline(self, offset, only_this):
        """Inline the function call at point."""
        refactor = create_inline(self.project, self.resource, offset)
        if only_this:
            return self._get_changes(refactor, remove=False, only_current=True)
        else:
            return self._get_changes(refactor, remove=True, only_current=False)

    @options("Extract current region as a method", category="Region",
             args=[("start", "start_offset", None),
                   ("end", "end_offset", None),
                   ("name", "string", "Method name: "),
                   ("make_global", "boolean", "Create global method? ")],
             available=ROPE_AVAILABLE)
    def refactor_extract_method(self, start, end, name,
                                make_global):
        """Extract region as a method."""
        refactor = ExtractMethod(self.project, self.resource, start, end)
        return self._get_changes(
            refactor, name, similar=True, global_=make_global
        )

    @options("Use the function at point wherever possible", category="Method",
             args=[("offset", "offset", None)],
             available=ROPE_AVAILABLE)
    def refactor_use_function(self, offset):
        """Use the function at point wherever possible."""
        try:
            refactor = UseFunction(self.project, self.resource, offset)
        except RefactoringError as e:
            raise Fault(
                'Refactoring error: {}'.format(e),
                code=400
            )
        return self._get_changes(refactor)

    def _get_changes(self, refactor, *args, **kwargs):
        try:
            changes = refactor.get_changes(*args, **kwargs)
        except Exception as e:
            raise Fault("Error during refactoring: {}".format(e),
                        code=400)
        return translate_changes(changes)


def translate_changes(initial_change):
    """Translate rope.base.change.Change instances to dictionaries.

    See Refactor.get_changes for an explanation of the resulting
    dictionary.

    """
    agenda = [initial_change]
    result = []
    while agenda:
        change = agenda.pop(0)
        if isinstance(change, rope_change.ChangeSet):
            agenda.extend(change.changes)
        elif isinstance(change, rope_change.ChangeContents):
            result.append({'action': 'change',
                           'file': change.resource.real_path,
                           'contents': change.new_contents,
                           'diff': change.get_description()})
        elif isinstance(change, rope_change.CreateFile):
            result.append({'action': 'create',
                           'type': 'file',
                           'file': change.resource.real_path})
        elif isinstance(change, rope_change.CreateFolder):
            result.append({'action': 'create',
                           'type': 'directory',
                           'path': change.resource.real_path})
        elif isinstance(change, rope_change.MoveResource):
            result.append({'action': 'move',
                           'type': ('directory'
                                    if change.new_resource.is_folder()
                                    else 'file'),
                           'source': change.resource.real_path,
                           'destination': change.new_resource.real_path})
        elif isinstance(change, rope_change.RemoveResource):
            if change.resource.is_folder():
                result.append({'action': 'delete',
                               'type': 'directory',
                               'path': change.resource.real_path})
            else:
                result.append({'action': 'delete',
                               'type': 'file',
                               'file': change.resource.real_path})
    return result


class FakeResource(object):
    """A fake resource in case Rope is absence."""

    def __init__(self, filename):
        self.real_path = filename

    def read(self):
        with open(self.real_path) as f:
            return f.read()
