import tempfile
import os
import sublime
import sublime_plugin
import subprocess
import json
import socket
import re
import webbrowser

_socket = None
_logfile = open(os.path.join(tempfile.gettempdir(), 'ElixirSublime.log'), 'w')
_sessions = {}
_elixir_source_dir = ""

def plugin_loaded(): 
    global _elixir_source_dir
    settings = sublime.load_settings("ElixirSublime.sublime-settings")
    _elixir_source_dir = settings.get('elixir_source_dir') or ""
    
    global _socket
    _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    _socket.bind(('', 0))   
    _socket.listen(1)
    _socket.settimeout(5)

    run_mix_task('deps.get')

def plugin_unloaded():
    if _logfile:
        _logfile.close() 
    if _socket:
        _socket.close()   
    for session in _sessions.values():
        session.close()


def run_mix_task(cmd):
    settings = sublime.load_settings('Preferences.sublime-settings')
    cwd = os.path.join(os.path.dirname(__file__), 'sublime_completion')
    env = os.environ.copy()
    try:
        env['PATH'] = os.pathsep.join(
            [settings.get('env')['PATH'], env['PATH']])
    except (TypeError, ValueError, KeyError):
        pass
    if _socket:
        env['ELIXIR_SUBLIME_PORT'] = str(_socket.getsockname()[1])

    if sublime.platform() == "windows":
        # on Windows, mix is a .bat file, which `subprocess` can't just launch like that. Use cmd.exe to launch the .bat file
        launcher = ['cmd', '/c', 'mix']

        # don't show the console window
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        
    else:
        launcher = ['mix']
        startupinfo = None

    return subprocess.Popen( 
        launcher + cmd.split(), 
        cwd=cwd, 
        stderr=_logfile.fileno(),
        stdout=_logfile.fileno(),
        env=env,
        startupinfo=startupinfo)


def find_mix_project(cwd=None):
    cwd = cwd or os.getcwd()   
    if cwd == os.path.realpath('/'):
        return None
    elif os.path.exists(os.path.join(cwd, 'mix.exs')):
        return cwd
    else: 
        return find_mix_project(os.path.dirname(cwd))


def find_ebin_folders(mix_project):
    paths = []
    if mix_project is not None:
        lib_path = os.path.join(mix_project, '_build/dev/lib')
        for lib in os.listdir(lib_path):
            paths.append(os.path.join(lib_path, lib, 'ebin'))
    return paths


def is_elixir_file(filename):
    return filename and filename.endswith(('.ex', '.exs'))


def is_erlang_file(filename):
    return filename and filename.endswith('erl')


def expand_selection(view, point_or_region, aliases={}):
    region = view.expand_by_class(point_or_region, 
        sublime.CLASS_WORD_START | 
        sublime.CLASS_WORD_END, ' (){},[]%&')
    selection = view.substr(region).strip()
    if aliases:
        parts = selection.split('.')
        for alias, canonical in aliases.items():
            if alias == parts[0]:
                parts[0] = canonical
                return '.'.join(parts)
    return selection


def do_focus(fn, pattern):
    window = sublime.active_window()
    view = window.open_file(fn)
    if view.is_loading():
        focus(fn, pattern)
    else:
        window.focus_view(view)
        if pattern:
            r = view.find(pattern, 0)
            if r:
                row, col = view.rowcol(r.begin())
                pt = view.text_point(row, col)
                r = sublime.Region(pt, pt)
                view.sel().clear()
                view.sel().add(r)
                view.show(pt)


def focus(fn, pattern, timeout=25):
    sublime.set_timeout(lambda: do_focus(fn, pattern), timeout)


def focus_function(fn, function):
    focus(fn, 'def(p|macrop?)?\s%s\(?' % function)


def find_aliases(view):
    aliases = {}
    for region in view.find_all('^[\s\t]*?alias\s.+?$'):
        alias_line = view.substr(region).strip()
        for (pattern, replacer) in [
            (r'^alias (.+?)\.(.+?)$', lambda prefix, alias: '%s.%s' % (prefix, alias)),
            (r'^alias (.+?), as: (.+)$', lambda prefix, _: prefix),
        ]:
            matches = re.findall(pattern, alias_line)
            if matches:
                [(prefix, alias)] = matches
                aliases[alias] = replacer(prefix, alias)
                break
    return aliases


class ElixirSession(object):
    @classmethod
    def ensure(cls, cwd=None):
      mix_project = find_mix_project(cwd)
      session = _sessions.get(mix_project)
      if not session:
            session = cls(mix_project)
            _sessions[mix_project] = session
      if not session.alive:
        session.connect()
      return session

    def __init__(self, mix_project):
        self.mix_project = mix_project
        self.reset()

    @property
    def alive(self):
        return self.process is not None and self.process.returncode is None

    def reset(self):
        self.socket = None
        self.file = None
        self.process = None

    def connect(self):
        self.process = run_mix_task('run')

        self.socket, _ = _socket.accept()
        self.socket.settimeout(5)

        self.file = self.socket.makefile() 

        for lib_path in find_ebin_folders(self.mix_project):
            self.send('PATH', lib_path)

    def send(self, cmd, args):
        try:
            self.socket.send(str.encode(cmd))
            self.socket.send(b' ')
            self.socket.send(str.encode(args))
            self.socket.send(b'\n')
            return True 
        except (OSError, IOError):
            self.reset()
            return False

    def recv(self):
        try:
            return self.file.readline().strip()
        except (OSError, IOError):
            self.reset()
            return None  

    def close(self): 
        if self.socket:
            self.socket.close() 
        if self.process:
            self.process.kill()


class ElixirGotoDefinition(sublime_plugin.TextCommand):
  def run(self, edit):
    aliases = find_aliases(self.view)
    selection = expand_selection(self.view, self.view.sel()[0], aliases=aliases)
    if selection:
        session = ElixirSession.ensure(os.path.dirname(self.view.file_name()))
        if session.send('GOTO', selection):
            goto = json.loads(session.recv())
            if goto:
                source = goto['source']
                function = goto['function']
                if not os.path.exists(source):
                    url = None
                    if is_erlang_file(source):
                        matches = re.findall(r'/lib/(.+?)/src/(.+?)\.erl$', source)
                        if matches:
                            [(_, module)] = matches
                            url = 'http://www.erlang.org/doc/man/%s.html' % module
                            if function:
                                url += '#%s-%s' % (goto['function'], goto['arities'][0])
                    elif is_elixir_file(source):
                        matches = re.findall(r'(/lib/(.+?)/lib/(.+?)\.exs?)$', source)
                        if matches:
                            [(path, lib, _)] = matches
                            global _elixir_source_dir;
                            elixir_source_path = _elixir_source_dir + path;
                            if os.path.exists(elixir_source_path):
                                if function:
                                    focus_function(elixir_source_path, function)
                                else:
                                    focus(elixir_source_path, 'defmodule?\s%(module)s\sdo' % goto)
                                return

                            url = 'http://elixir-lang.org/docs/stable/%s/%s.html' % (lib, goto['module'])
                            if function:
                                url += '#%s/%s' % (goto['function'], goto['arities'][0])
                    if url:
                        webbrowser.open(url)
                    return
                if function:
                    if is_erlang_file(source):
                        focus(source, '^%s\(' % function)
                    else:
                        focus_function(source, function)
                elif is_elixir_file(source):
                    focus(source, 'defmodule?\s%(module)s\sdo' % goto)
            else:
                focus_function(self.view.file_name(), selection)


class ElixirAutocomplete(sublime_plugin.EventListener):
    def on_activated_async(self, view):
        self.on_load_async(view)

    def on_load_async(self, view):
        filename = view.file_name()
        if is_elixir_file(filename):
            ElixirSession.ensure(os.path.dirname(filename))

    def on_query_completions(self, view, prefix, locations):
        if not is_elixir_file(view.file_name()):
            return None

        aliases = find_aliases(view)

        session = ElixirSession.ensure(os.path.dirname(view.file_name()))
        
        if not session.send('COMPLETE', expand_selection(view, locations[0], aliases=aliases)):
            return None

        completions = session.recv()
        if not completions:
            return None 

        seen_completions = set()

        rv = []
        for completion in json.loads(completions):
            seen_completions.add(completion['name'])

            if completion['type'] == 'module':
                rv.append(('%(name)s\t%(name)s' % completion, completion['content']))
            else: 
                rv.append(('%(name)s\t%(name)s/%(arity)s' % completion, completion['content']))

        for completion in view.extract_completions(prefix):
            if completion not in seen_completions:
                rv.append((completion,)) 

        return rv 

try:
  from SublimeLinter.lint import Linter

  class ElixirLinter(Linter):
      syntax = 'elixir'

      executable = 'elixirc' 
      tempfile_suffix = 'ex'

      regex = (
          r"^[^ ].+:(?P<line>\d+):"
          r"(?:(?P<warning>\swarning:\s)|(?P<error>\s))"
          r"(?P<message>.+)"
      )

      def cmd(self):
          command = [
            self.executable_path,
            '--warnings-as-errors',
            '--ignore-module-conflict',
            '-o', os.path.join(tempfile.gettempdir(), 'SublimeLinter3')
          ]

          for path in find_ebin_folders(find_mix_project(self.filename)):
              command.extend(['-pa', path])

          return command
except ImportError:
  pass