import sublime
import sublime_plugin
import subprocess
import os
import platform
import sys
import json
from collections import deque


def real_path(go_path):
    if go_path is None:
        return None
    return os.path.expanduser(os.path.expandvars(go_path))


class GodefCommand(sublime_plugin.WindowCommand):
    """
    Godef command class
    use godef to find definition first,
    if not found, use guru to find again.
    """

    def __init__(self, window):
        self.systype = platform.system()
        self.gopath = None
        self.goroot = None
        self.cmdpaths = []
        self.env = None
        self.gosublime = {}
        self.read_gosublime(window)

        default_setting = sublime.load_settings("Preferences.sublime-settings")
        default_setting.set("default_line_ending", "unix")
        gopath = self._gopath()
        goroot = self._goroot()
        self.load(gopath, goroot, self.systype)
        self.gopath = gopath
        self.goroot = goroot
        if sys.version_info[0] == 2:
            super(GodefCommand, self).__init__(window)
        else:
            super().__init__(window)

    def load(self, gopath, goroot, systype):
        print("===============[Godef]Load Begin==============")
        # print("[Godef]DEBUG: system type: %s" % self.systype)
        if not gopath:
            print("[Godef]ERROR: no GOPATH defined")
            print("===============[Godef] Load End===============")
            return False

        if not goroot:
            print("[Godef]WARN: no GOROOT defined")

        cmdpaths = []
        for cmd in ['godef', 'guru']:
            found = False
            if systype == "Windows":
                binary = cmd + ".exe"
            else:
                binary = cmd
            gopaths = gopath.split(os.pathsep)
            for go_path in gopaths:
                cmdpath = os.path.join(go_path, "bin", binary)
                if not os.path.isfile(cmdpath):
                    print('[Godef]WARN: "%s" cmd not found at %s' % (cmd, go_path))
                    continue
                else:
                    found = True
                    break
            if not found:
                print('[Godef]WARN: "%s" cmd is not available.' % cmd)
                continue
            print('[Godef]INFO: found "%s" at %s' % (cmd, cmdpath))
            cmdpaths.append({'mode': cmd, 'path': cmdpath})

        if len(cmdpaths) == 0:
            print('[Godef]ERROR: godef/guru are not available.\n\
Make sure your gopath in settings is right.\n\
Use "go get -u github.com/rogpeppe/godef"\n\
and "go get -u golang.org/x/tools/cmd/guru"\n\
to install them.')
            print("===============[Godef] Load End===============")
            return False

        # a weird bug on windows. sometimes unicode strings end up in the
        # environment and subprocess.call does not like this, encode them
        # to latin1 and continue.
        env = os.environ.copy()
        if systype == "Windows":
            if sys.version_info[0] == 2:
                if gopath and isinstance(gopath, unicode):
                    gopath = gopath.encode('iso-8859-1')
                if goroot and isinstance(goroot, unicode):
                    goroot = goroot.encode('iso-8859-1')
        env["GOPATH"] = gopath
        if goroot:
            env["GOROOT"] = goroot

        self.cmdpaths = cmdpaths
        self.env = env
        print("===============[Godef] Load End===============")
        return True

    def run(self):
        default_setting = sublime.load_settings("Preferences.sublime-settings")
        default_setting.set("default_line_ending", "unix")
        settings = sublime.load_settings("Godef.sublime-settings")
        gopath = real_path(settings.get("gopath", os.getenv('GOPATH')))
        goroot = real_path(settings.get("goroot", os.getenv('GOROOT')))

        if self.gopath != gopath or self.goroot != goroot:
            print('[Godef]INFO: settings change, reload conf')
            self.gopath = gopath
            self.goroot = goroot
            self.cmdpaths = []
            self.env = None
        if len(self.cmdpaths) != 2 and not self.load(gopath, goroot, self.systype):
            return

        print("=================[Godef]Begin=================")
        if len(self.cmdpaths) == 1:
            if self.cmdpaths[0]['mode'] != 'godef':
                print('[Godef]WARN: missing cmd "godef"')
            else:
                print('[Godef]WARN: missing cmd "guru"')
        if not self.goroot:
            print("[Godef]WARN: no GOROOT defined in settings")

        view = self.window.active_view()
        filename = view.file_name()
        select = view.sel()[0]
        select_begin = select.begin()
        select_before = sublime.Region(0, select_begin)
        string_before = view.substr(select_before)
        offset = len(string_before.encode('utf-8'))
        if view.line_endings() == 'Windows':
            offset += string_before.count('\n')

        print("[Godef]INFO: select_begin: %s offset: %s" % (select_begin, offset))

        reset = False
        output = None
        succ = None
        for d in self.cmdpaths:
            if 'godef' == d['mode']:
                args = [d['path'], "-f", filename, "-o", str(offset)]
            else:
                args = [d['path'], "-json", 'definition', filename + ":#" + str(offset)]
            print("[Godef]INFO: spawning: %s" % " ".join(args))

            startupinfo = None
            if os.name == 'nt':
                startupinfo = subprocess.STARTUPINFO()
                startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            stderr = None
            try:
                p = subprocess.Popen(args, stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE, env=self.env,
                                     startupinfo=startupinfo)
                output, stderr = p.communicate()
            except Exception as e:
                print("[Godef]EXPT: %s fail: %s" % (d['mode'], e))
                print('[Godef]WARN: %s binary not existed, need reload conf' % d['mode'])
                reset = True
                continue
            if stderr:
                err = stderr.decode("utf-8").rstrip()
                print("[Godef]ERROR: %s fail: %s" % (d['mode'], err))
                output = None
                continue
            elif len(output) < 3:
                position = output.decode("utf-8").rstrip()
                print("[Godef]ERROR: %s illegal output: %s" % (d['mode'], output))
                continue
            else:
                succ = d
                break

        if reset: self.cmdpaths = []

        if not output:
            print("[Godef]ERROR: all cmds failed")
            print("=================[Godef] End =================")
            return

        position = output.decode("utf-8").rstrip()
        print("[Godef]INFO: %s output:\n%s" % (succ['mode'], position))
        if succ['mode'] == 'guru':
            definition = json.loads(position)
            if 'objpos' not in definition:
                print("[Godef]ERROR: guru result josn unmarshal err")
            else:
                position = definition['objpos']
        print("[Godef]INFO: opening definition at %s" % position)
        self.window.open_file(position, sublime.ENCODED_POSITION)
        # record prev postion
        startrow, startcol = view.rowcol(select_begin)
        GodefPrevCommand.append(filename, startrow, startcol)
        print("=================[Godef] End =================")

    def read_gosublime(self, window):
        """
        reads environment variables from gosublime.
        """
        view = window.active_view()
        if view is not None:
            settings = view.settings()
            gosublime = settings.get("GoSublime", None)
            if gosublime and gosublime.get("env", None):
                env = gosublime.get("env")
                print("[Godef]INFO: found gosublime environment: {env}".format(env=env))
                self.gosublime["gopath"] = env.get("GOPATH", "")
                self.gosublime["goroot"] = env.get("GOROOT", "")

    def _gopath(self):
        settings = sublime.load_settings("Godef.sublime-settings")
        gopath = real_path(settings.get("gopath", os.getenv('GOPATH')))
        if gopath is None:
            return self.gosublime.get("gopath", "")
        return gopath

    def _goroot(self):
        settings = sublime.load_settings("Godef.sublime-settings")
        goroot = real_path(settings.get("goroot", os.getenv('GOROOT')))
        if goroot is None:
            return self.gosublime.get("goroot", "")
        return goroot


# GodefPrev Commands
# code based on https://github.com/SublimeText/CTags/blob/development/ctagsplugin.py
class GodefPrevCommand(sublime_plugin.WindowCommand):
    """
    Provide ``godef_back`` command.

    Command "godef jump back" to the previous code point before a godef was navigated.

    This is functionality supported natively by ST3 but not by ST2. It is
    therefore included for legacy purposes.
    """
    buf = deque(maxlen=100)  # virtually a "ring buffer"

    def run(self):
        if not self.buf:
            print("[GodefPrev]ERROR: GodefPrev buffer empty")
            return

        filename, startrow, startcol = self.buf.pop()
        path = "{0}:{1}:{2}".format(filename, startrow +1, startcol + 1)
        self.window.open_file(path, sublime.ENCODED_POSITION)
        print("[GodefPrev]INFO: jump prev definition at %s" % path)

    @classmethod
    def append(cls, filename, startrow, startcol):
        """Append a code point to the list"""
        if filename:
            cls.buf.append((filename, startrow, startcol))