import os
import sys
import time
import logging
import tempfile
import threading

if sys.version_info[0] == 2:
    sys.path.insert(
        0, os.path.join(os.path.dirname(__file__), "..", "lib", "python2"))
    from SimpleXMLRPCServer import SimpleXMLRPCServer
    from xmlrpclib import Binary
else:
    sys.path.insert(
        0, os.path.join(os.path.dirname(__file__), "..", "lib", "python3"))
    from xmlrpc.server import SimpleXMLRPCServer
    from xmlrpc.client import Binary

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from linter import do_linting

from rope.base import libutils
from rope.base.project import Project
from rope.base.exceptions import ModuleSyntaxError
from rope.contrib.codeassist import (
    code_assist, sorted_proposals, get_doc, get_definition_location
)

# global state of the server process
last_heartbeat = None
# constants
HEARTBEAT_TIMEOUT = 19
NO_ROOT_PATH = -1


class RopeProjectMixin(object):
    """
    Creates and manages Rope projects"""

    def __init__(self):
        self.projects = {}
        self.buffer_tmpfile_map = {}
        self.tempfiles = []

    def __del__(self):
        '''Cleanup temporary files when server is deallocated. Although
        Python destructors are not guaranteed to be run it is still ok to
        do cleanup here, as a tempfile surviving the server in TEMPDIR
        is not too big of a problem.'''
        for tfn in self.tempfiles:
            os.unlink(tfn)

    def project_for(self, project_path, file_path, source=""):
        # scratch buffer case: create temp file and proj for buffer and cache it
        if file_path.startswith("BUFFER:"):
            if file_path in self.projects:
                project = self.projects[file_path]
                file_path = self.buffer_tmpfile_map[file_path]
            else:
                original_file_path = file_path
                file_path = self._create_temp_file(source)
                project = self._create_single_file_project(file_path)
                self.projects[original_file_path] = project
                self.buffer_tmpfile_map[original_file_path] = file_path

        # single file case (or scratch buffer with client not sending buffer_id)
        # create temp file and proj, and buffer if file_name given
        elif project_path == NO_ROOT_PATH:
            if file_path in self.projects:
                project = self.projects[file_path]
            else:
                if not file_path:
                    # this path is deprecated and should not be used anymore
                    file_path = self._create_temp_file(source)
                    project = self._create_single_file_project(file_path)
                else:
                    project = self._create_single_file_project(file_path)
                    self.projects[file_path] = project

        # "usual" case: a real file with a project directory is given
        else:
            if project_path in self.projects:
                project = self.projects[project_path]
            else:
                project = self._create_project(project_path)
                self.projects[project_path] = project
        return project, file_path

    def list_projects(self):
        return self.projects.keys()

    def _create_project(self, path):
        project = Project(path, fscommands=None, ropefolder=None)
        return project

    def _create_single_file_project(self, path):
        folder = os.path.dirname(path)
        ignored_res = os.listdir(folder)
        ignored_res.remove(os.path.basename(path))

        project = Project(
            folder, ropefolder=None,
            ignored_resources=ignored_res, fscommands=None)
        return project

    def _create_temp_file(self, content):
        """
        Creates a temporary named file for use by Rope. It expects to
        be able to read files from disk in some places, so there is no
        easy way around creating these files. We try to delete those
        files in the servers destructor (see __del__).
        """
        tmpfile = tempfile.NamedTemporaryFile(delete=False)
        tmpfile.write(content.encode("utf-8"))

        tf_path = tmpfile.name
        self.tempfiles.append(tf_path)

        tmpfile.close()
        return tf_path


class RopeFunctionsMixin(object):
    """Uses Rope to generate completion proposals, depends on RopeProjectMixin
    """

    def profile_completions(self, source, project_path, file_path, loc):
        """
        Only for testing purposes::
            runs Rope's code completion functionality in the python profiler
            and saves statistics, then reruns for actual results
        """

        try:
            import cProfile as profile
        except:
            import profile

        profile.runctx(
            "self.completions(source, project_path, file_path, loc)",
            globals(), locals(), os.path.expanduser("~/SublimePython.stats"))

        return self.completions(source, project_path, file_path, loc)

    def completions(self, source, project_path, file_path, loc):
        """
        Get completions from the underlying Rope library and returns it back
        to the editor interface

        :param source: the document source
        :param project_path: the actual project_path
        :param file_path: the actual file path
        :param loc: the buffer location
        :returns: a list of tuples of strings
        """

        project, resource = self._get_resource(project_path, file_path, source)

        try:
            proposals = code_assist(
                project, source, loc, resource=resource, maxfixes=3)
            proposals = sorted_proposals(proposals)
        except ModuleSyntaxError:
            proposals = []
        except Exception:
            import traceback
            traceback.print_exc()
            proposals = []
        finally:
            proposals = [
                (self._proposal_string(p), self._insert_string(p))
                for p in proposals if p.name != 'self='
            ]

        return proposals

    def documentation(self, source, project_path, file_path, loc):
        """
        Search for documentation about the word in the current location

        :param source: the document source
        :param project_path: the actual project_path
        :param file_path: the actual file path
        :param loc: the buffer location
        :returns: a string containing the documentation
        """

        project, resource = self._get_resource(project_path, file_path, source)

        try:
            doc = get_doc(project, source, loc, resource=resource, maxfixes=3)
        except ModuleSyntaxError:
            doc = None

        return doc

    def definition_location(self, source, project_path, file_path, loc):
        """
        Get a global definition location and returns it back to the editor

        :param source: the document source
        :param project_path: the actual project_path
        :param file_path: the actual file path
        :param loc: the buffer location
        :returns: a tuple containing the path and the line number
        """

        project, resource = self._get_resource(project_path, file_path, source)

        real_path, def_lineno = (None, None)
        try:
            def_resource, def_lineno = get_definition_location(
                project, source, loc, resource=resource, maxfixes=3)
            if def_resource:
                real_path = def_resource.real_path
        except ModuleSyntaxError:
            pass

        return real_path, def_lineno