# Arguments are:
# 1. Working directory.
# 2. Rope folder

import difflib
import io
import json
import os
import sys
import traceback

try:
    import rope
    import rope.base.project
    import rope.base.taskhandle
    from rope.base import libutils
    from rope.refactor.rename import Rename
    from rope.refactor.extract import ExtractMethod, ExtractVariable
    from rope.refactor.importutils import FromImport, NormalImport
    from rope.refactor.importutils.module_imports import ModuleImports
except ImportError:
    jsonMessage = {
        "error": True,
        "message": "Rope not installed",
        "traceback": "",
        "type": "ModuleNotFoundError",
    }
    sys.stderr.write(json.dumps(jsonMessage))
    sys.stderr.flush()

WORKSPACE_ROOT = sys.argv[1]
ROPE_PROJECT_FOLDER = '.vim/.ropeproject'


class RefactorProgress:
    """
    Refactor progress information
    """

    def __init__(self, name="Task Name", message=None, percent=0):
        self.name = name
        self.message = message
        self.percent = percent


class ChangeType:
    """
    Change Type Enum
    """

    EDIT = 0
    NEW = 1
    DELETE = 2


class Change:
    """"""

    EDIT = 0
    NEW = 1
    DELETE = 2

    def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""):
        self.filePath = filePath
        self.diff = diff
        self.fileMode = fileMode


def x_diff(x):
    new = x["new_contents"]
    old = x["old_contents"]
    old_lines = old.splitlines(True)
    if not old_lines[-1].endswith("\n"):
        old_lines[-1] = old_lines[-1] + os.linesep
        new = new + os.linesep

    result = difflib.unified_diff(
        old_lines,
        new.splitlines(True),
        "a/" + x["path"],
        "b/" + x["path"],
    )
    return "".join(list(result))


def get_diff(changeset):
    """This is a copy of the code form the ChangeSet.get_description method found in Rope."""
    new = changeset.new_contents
    old = changeset.old_contents
    if old is None:
        if changeset.resource.exists():
            old = changeset.resource.read()
        else:
            old = ""

    # Ensure code has a trailing empty lines, before generating a diff.
    # https://github.com/Microsoft/vscode-python/issues/695.
    old_lines = old.splitlines(True)
    if not old_lines[-1].endswith("\n"):
        old_lines[-1] = old_lines[-1] + os.linesep
        new = new + os.linesep

    result = difflib.unified_diff(
        old_lines,
        new.splitlines(True),
        "a/" + changeset.resource.path,
        "b/" + changeset.resource.path,
    )
    return "".join(list(result))


class BaseRefactoring(object):
    """
    Base class for refactorings
    """

    def __init__(self, project, resource, name="Refactor", progressCallback=None):
        self._progressCallback = progressCallback
        self._handle = rope.base.taskhandle.TaskHandle(name)
        self._handle.add_observer(self._update_progress)
        self.project = project
        self.resource = resource
        self.changes = []

    def _update_progress(self):
        jobset = self._handle.current_jobset()
        if jobset and not self._progressCallback is None:
            progress = RefactorProgress()
            # getting current job set name
            if jobset.get_name() is not None:
                progress.name = jobset.get_name()
            # getting active job name
            if jobset.get_active_job_name() is not None:
                progress.message = jobset.get_active_job_name()
            # adding done percent
            percent = jobset.get_percent_done()
            if percent is not None:
                progress.percent = percent
            if not self._progressCallback is None:
                self._progressCallback(progress)

    def stop(self):
        self._handle.stop()

    def refactor(self):
        try:
            self.onRefactor()
        except rope.base.exceptions.InterruptedTaskError:
            # we can ignore this exception, as user has cancelled refactoring
            pass

    def onRefactor(self):
        """
        To be implemented by each base class
        """
        pass


class RenameRefactor(BaseRefactoring):
    def __init__(
        self,
        project,
        resource,
        name="Rename",
        progressCallback=None,
        startOffset=None,
        newName="new_Name",
    ):
        BaseRefactoring.__init__(self, project, resource, name, progressCallback)
        self._newName = newName
        self.startOffset = startOffset

    def onRefactor(self):
        renamed = Rename(self.project, self.resource, self.startOffset)
        changes = renamed.get_changes(self._newName, task_handle=self._handle)
        for item in changes.changes:
            if isinstance(item, rope.base.change.ChangeContents):
                self.changes.append(
                    Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))
                )
            else:
                raise Exception("Unknown Change")


class ExtractVariableRefactor(BaseRefactoring):
    def __init__(
        self,
        project,
        resource,
        name="Extract Variable",
        progressCallback=None,
        startOffset=None,
        endOffset=None,
        newName="new_Name",
        similar=False,
        global_=False,
    ):
        BaseRefactoring.__init__(self, project, resource, name, progressCallback)
        self._newName = newName
        self._startOffset = startOffset
        self._endOffset = endOffset
        self._similar = similar
        self._global = global_

    def onRefactor(self):
        renamed = ExtractVariable(
            self.project, self.resource, self._startOffset, self._endOffset
        )
        changes = renamed.get_changes(self._newName, self._similar, self._global)
        for item in changes.changes:
            if isinstance(item, rope.base.change.ChangeContents):
                self.changes.append(
                    Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))
                )
            else:
                raise Exception("Unknown Change")


class ExtractMethodRefactor(ExtractVariableRefactor):
    def __init__(
        self,
        project,
        resource,
        name="Extract Method",
        progressCallback=None,
        startOffset=None,
        endOffset=None,
        newName="new_Name",
        similar=False,
        global_=False,
    ):
        ExtractVariableRefactor.__init__(
            self,
            project,
            resource,
            name,
            progressCallback,
            startOffset=startOffset,
            endOffset=endOffset,
            newName=newName,
            similar=similar,
            global_=global_,
        )

    def onRefactor(self):
        renamed = ExtractMethod(
            self.project, self.resource, self._startOffset, self._endOffset
        )
        changes = renamed.get_changes(self._newName, self._similar, self._global)
        for item in changes.changes:
            if isinstance(item, rope.base.change.ChangeContents):
                self.changes.append(
                    Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))
                )
            else:
                raise Exception("Unknown Change")


class ImportRefactor(BaseRefactoring):
    def __init__(
        self,
        project,
        resource,
        text = None,
        name = None,
        parent = None,
    ):
        BaseRefactoring.__init__(self, project, resource, name='Add Import', progressCallback=None)
        self._name = name
        self._text = text
        self._parent = parent

    def onRefactor(self):
        if self._parent:
            import_info = FromImport(self._parent, 0, [(self._name, None)])
        else:
            import_info = NormalImport([(self._name, None)])

        pymodule = self.project.get_pymodule(self.resource)
        module_imports = ModuleImports(self.project, pymodule)
        module_imports.add_import(import_info)
        changed_source = module_imports.get_changed_source()
        if changed_source:
            changeset = {
                "old_contents": self._text,
                "new_contents": changed_source,
                "path": self.resource.path
            }
            self.changes.append(Change(self.resource.path, ChangeType.EDIT, x_diff(changeset)))
        else:
            raise Exception('Unknown Change')


class RopeRefactoring(object):
    def __init__(self):
        self.default_sys_path = sys.path
        self._input = io.open(sys.stdin.fileno(), encoding="utf-8")

    def _rename(self, filePath, start, newName, indent_size):
        """
        Renames a variable
        """
        project = rope.base.project.Project(
            WORKSPACE_ROOT,
            ropefolder=ROPE_PROJECT_FOLDER,
            save_history=False,
            indent_size=indent_size,
        )
        resourceToRefactor = libutils.path_to_resource(project, filePath)
        refactor = RenameRefactor(
            project, resourceToRefactor, startOffset=start, newName=newName
        )
        refactor.refactor()
        changes = refactor.changes
        project.close()
        valueToReturn = []
        for change in changes:
            valueToReturn.append({"diff": change.diff})
        return valueToReturn

    def _extractVariable(self, filePath, start, end, newName, indent_size):
        """
        Extracts a variable
        """
        project = rope.base.project.Project(
            WORKSPACE_ROOT,
            ropefolder=ROPE_PROJECT_FOLDER,
            save_history=False,
            indent_size=indent_size,
        )
        resourceToRefactor = libutils.path_to_resource(project, filePath)
        refactor = ExtractVariableRefactor(
            project,
            resourceToRefactor,
            startOffset=start,
            endOffset=end,
            newName=newName,
            similar=True,
        )
        refactor.refactor()
        changes = refactor.changes
        project.close()
        valueToReturn = []
        for change in changes:
            valueToReturn.append({"diff": change.diff})
        return valueToReturn

    def _extractMethod(self, filePath, start, end, newName, indent_size):
        """
        Extracts a method
        """
        project = rope.base.project.Project(
            WORKSPACE_ROOT,
            ropefolder=ROPE_PROJECT_FOLDER,
            save_history=False,
            indent_size=indent_size,
        )
        resourceToRefactor = libutils.path_to_resource(project, filePath)
        refactor = ExtractMethodRefactor(
            project,
            resourceToRefactor,
            startOffset=start,
            endOffset=end,
            newName=newName,
            similar=True,
        )
        refactor.refactor()
        changes = refactor.changes
        project.close()
        valueToReturn = []
        for change in changes:
            valueToReturn.append({"diff": change.diff})
        return valueToReturn

    def _add_import(self, filePath, text, name, parent, indent_size):
        """
        Add import
        """
        project = rope.base.project.Project(
            WORKSPACE_ROOT,
            ropefolder=ROPE_PROJECT_FOLDER,
            save_history=False,
            indent_size=indent_size,
        )
        resourceToRefactor = libutils.path_to_resource(project, filePath)
        refactor = ImportRefactor(
            project,
            resourceToRefactor,
            text,
            name,
            parent
        )
        refactor.refactor()
        changes = refactor.changes
        project.close()
        valueToReturn = []
        for change in changes:
            valueToReturn.append({"diff": change.diff})
        return valueToReturn

    def _serialize(self, identifier, results):
        """
        Serializes the refactor results
        """
        return json.dumps({"id": identifier, "results": results})

    def _deserialize(self, request):
        """Deserialize request from VSCode.

        Args:
            request: String with raw request from VSCode.

        Returns:
            Python dictionary with request data.
        """
        return json.loads(request)

    def _process_request(self, request):
        """Accept serialized request from VSCode and write response."""
        request = self._deserialize(request)
        lookup = request.get("lookup", "")

        if lookup == "":
            pass
        elif lookup == "rename":
            changes = self._rename(
                request["file"],
                int(request["start"]),
                request["name"],
                int(request["indent_size"]),
            )
            return self._write_response(self._serialize(request["id"], changes))
        elif lookup == "add_import":
            changes = self._add_import(
                request["file"],
                request["text"],
                request["name"],
                request.get("parent", None),
                int(request["indent_size"]),
            )
            return self._write_response(self._serialize(request["id"], changes))
        elif lookup == "extract_variable":
            changes = self._extractVariable(
                request["file"],
                int(request["start"]),
                int(request["end"]),
                request["name"],
                int(request["indent_size"]),
            )
            return self._write_response(self._serialize(request["id"], changes))
        elif lookup == "extract_method":
            changes = self._extractMethod(
                request["file"],
                int(request["start"]),
                int(request["end"]),
                request["name"],
                int(request["indent_size"]),
            )
            return self._write_response(self._serialize(request["id"], changes))

    def _write_response(self, response):
        sys.stdout.write(response + "\n")
        sys.stdout.flush()

    def watch(self):
        self._write_response("STARTED")
        while True:
            try:
                self._process_request(self._input.readline())
            except:
                exc_type, exc_value, exc_tb = sys.exc_info()
                tb_info = traceback.extract_tb(exc_tb)
                jsonMessage = {
                    "error": True,
                    "message": str(exc_value),
                    "traceback": str(tb_info),
                    "type": str(exc_type),
                }
                sys.stderr.write(json.dumps(jsonMessage))
                sys.stderr.flush()


if __name__ == "__main__":
    RopeRefactoring().watch()
