import sublime
import sublime_plugin

import codecs
import traceback
import os.path
import sys
import re
import threading
import webbrowser

sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), 'ksp_compiler3'))

import ksp_compiler
import ksp_ast

import urllib, tarfile, json, shutil
from subprocess import call
try:
    import winsound
except Exception:
    pass

last_compiler = None

class KspRecompile(sublime_plugin.ApplicationCommand):
    def is_enabled(self):
        # only show the command when a file with KSP syntax highlighting is visible
        view = sublime.active_window().active_view()
        if view:
            return 'KSP.sublime-syntax' in view.settings().get('syntax', '')

    def run(self, *args):
        sublime.active_window().run_command('compile_ksp', {'recompile': True})

class CompileKspCommand(sublime_plugin.ApplicationCommand):
    def __init__(self):
        sublime_plugin.ApplicationCommand.__init__(self)
        self.thread = None
        self.last_filename = None

    def is_enabled(self):
        # only show the command when a file with KSP syntax highlighting is visible
        view = sublime.active_window().active_view()
        if view:
            return 'KSP.sublime-syntax' in view.settings().get('syntax', '')

    def run(self, *args, **kwargs):
        # wait until any previous thread is finished
        if self.thread and self.thread.is_alive():
            sublime.status_message('Waiting for earlier compilation to finish...')
            self.thread.stop()
            self.thread.join()

        # find the view containing the code to compile
        view = None
        if kwargs.get('recompile', None) and self.last_filename:
            view = CompileKspThread.find_view_by_filename(self.last_filename)
        if view is None:
            view = sublime.active_window().active_view()

        self.thread = CompileKspThread(view)
        self.thread.start()
        self.last_filename = view.file_name()

class CompilerSounds:
    dir = None

    def __init__(self):
        self.dir = os.path.join(os.path.dirname(__file__), 'sounds')

    def play(self, **kwargs):
        sound_path = os.path.join(self.dir, '{}.wav'.format(kwargs['command']))

        if sublime.platform() == "osx":
            if os.path.isfile(sound_path):
                call(["afplay", "-v", str(1), sound_path])

        if sublime.platform() == "windows":
            if os.path.isfile(sound_path):
                winsound.PlaySound(sound_path, winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_NODEFAULT)

        if sublime.platform() == "linux":
            if os.path.isfile(sound_path):
                call(["aplay", sound_path])

class CompileKspThread(threading.Thread):
    def __init__(self, view):
        threading.Thread.__init__(self)
        self.base_path = None
        self.compiler = None
        self.view = view

    def stop(self):
        if self.compiler:
            self.compiler.abort_compilation()

    def compile_on_progress(self, text, percent_complete):
        sublime.status_message('Compiling (%d%%) - %s...' %
                              (percent_complete, text))

    @classmethod
    def find_view_by_filename(cls, filename, base_path=None):
        if filename is None:
            return sublime.active_window().active_view()
        if not os.path.isabs(filename) and base_path:
            filename = os.path.join(base_path, filename)

        for window in sublime.windows():
            for view in window.views():
                if view.file_name() and view.file_name() == filename:
                    return view
        return None

    def compile_handle_error(self, error_msg, error_lineno, error_filename):
        view = CompileKspThread.find_view_by_filename(error_filename, self.base_path)
        if view:
            sublime.active_window().focus_view(view)
            if error_lineno is not None:
                pos = view.text_point(error_lineno, 0)
                line_region = view.line(sublime.Region(pos, pos))
                selection = view.sel()
                view.show(line_region)
                selection.clear()
                selection.add(line_region)
        sublime.status_message('Error - compilation aborted!')
        sublime.error_message(error_msg)
        sublime.status_message('')

    def read_file_function(self, filepath):
        if filepath.startswith('http://'):
            from urllib.request import urlopen
            s = urlopen(filepath, timeout=5).read().decode('utf-8')
            return re.sub('\r+\n*', '\n', s)

        if self.base_path:
            filepath = os.path.join(self.base_path, filepath)
        filepath = os.path.abspath(filepath)
        view = CompileKspThread.find_view_by_filename(filepath, self.base_path)
        if view is None:
            s = codecs.open(filepath, 'r', 'utf-8').read()
            return re.sub('\r+\n*', '\n', s)
        else:
            return view.substr(sublime.Region(0, view.size()))

    def run(self, *args):
        global last_compiler

        view = self.view
        code = view.substr(sublime.Region(0, view.size()))
        filepath = view.file_name()
        if filepath:
            self.base_path = os.path.dirname(filepath)
        else:
            self.base_path = None

        settings = sublime.load_settings("KSP.sublime-settings")

        compact = settings.get('ksp_compact_output', False)
        compactVars = settings.get('ksp_compact_variables', False)
        check = settings.get('ksp_extra_checks', True)
        optimize = settings.get('ksp_optimize_code', False)
        comments_on_expansion = settings.get('ksp_comment_inline_functions', False)
        check_empty_compound_statements = settings.get('ksp_signal_empty_ifcase', True)
        add_compiled_date_comment = settings.get('ksp_add_compiled_date', True)
        should_play_sound = settings.get('ksp_play_sound', False)

        error_msg = None
        error_lineno = None
        error_filename = filepath # path to main script

        sound_utility = CompilerSounds()

        try:
            sublime.status_message('Compiling...')

            self.compiler = ksp_compiler.KSPCompiler(code, self.base_path, compact, compactVars, comments_on_expansion,
                                                     read_file_func=self.read_file_function,
                                                     extra_syntax_checks=check,
                                                     optimize=optimize and check,
                                                     check_empty_compound_statements=check_empty_compound_statements,
                                                     add_compiled_date_comment=add_compiled_date_comment)
            if self.compiler.compile(callback=self.compile_on_progress):
                last_compiler = self.compiler
                code = self.compiler.compiled_code
                code = code.replace('\r', '')
                if self.compiler.output_file:
                    if not os.path.isabs(self.compiler.output_file):
                        self.compiler.output_file = os.path.join(self.base_path, self.compiler.output_file)
                    codecs.open(self.compiler.output_file, 'w', 'latin-1').write(code)
                    sublime.status_message("Successfully compiled (compiled code saved to %s)." % self.compiler.output_file)
                else:
                    sublime.status_message("Successfully compiled (the code is now on the clipboard ready to be pasted into Kontakt).")
                    sublime.set_clipboard(code)
            else:
                sublime.status_message('Compilation aborted.')
        except ksp_ast.ParseException as e:
            error_msg = unicode(e)
            line_object = self.compiler.lines[e.lineno]
            if line_object:
                error_lineno = line_object.lineno-1
                error_filename = line_object.filename
            if line_object:
                error_msg = re.sub(r'line (\d+)', 'line %s' % line_object.lineno, error_msg)
        except ksp_compiler.ParseException as e:
            error_lineno = e.line.lineno-1
            error_filename = e.line.filename
            error_msg = e.message
        except Exception as e:
            error_msg = str(e)
            error_msg = ''.join(traceback.format_exception(*sys.exc_info()))

        if error_msg:
            self.compile_handle_error(error_msg, error_lineno, error_filename)
            if should_play_sound:
                sound_utility.play(command="error")
        else:
            if should_play_sound:
                sound_utility.play(command="finished")

    def description(self, *args):
        return 'Compiled KSP'

# **********************************************************************************************

from ksp_compiler3.ksp_builtins import keywords, variables, functions, function_signatures
all_builtins = set(functions.keys()) | set([v[1:] for v in variables]) | variables | keywords
functions, variables = set(functions), set(variables)

builtin_compl_vars = []
builtin_compl_vars.extend(('%s\tvariable' % v[1:], v[1:]) for v in variables)
builtin_compl_vars.sort()

builtin_compl_funcs = []
for f in functions:
    args = [a.replace('number variable or text','').replace('-', '_') for a in function_signatures[f][0]]
    args = ['${%d:%s}' % (i+1, a) for i, a in enumerate(args)]
    args_str = '(%s)' % ', '.join(args) if args else ''
    builtin_compl_funcs.append(("%s\tfunction" % (f), "%s%s" % (f,args_str)))
builtin_compl_funcs.sort()

# control par references that can be used as control->x, or control->value
magic_control_pars = []
remap_control_pars = {'POS_X': 'x', 'POS_Y': 'y', 'MAX_VALUE': 'MAX', 'MIN_VALUE': 'MIN', 'DEFAULT_VALUE': 'DEFAULT'}
for v in variables:
    if v.startswith('$CONTROL_PAR_'):
        v = v.replace('$CONTROL_PAR_', '')
        v = remap_control_pars.get(v, v).lower()
        magic_control_pars.append(('%s\tui param' % v, v))
magic_control_pars.sort()


class KSPCompletions(sublime_plugin.EventListener):
    def _extract_completions(self, view, prefix, point):
        # the sublime view.extract_completions implementation doesn't seem to allow for
        # the . character to be included in the prefix irrespectively of the "word_separators" setting
        if '.' in prefix:
            # potentially slow work around for the case where there is a period in the prefix
            code = view.substr(sublime.Region(0, view.size()))
            return sorted(re.findall(re.escape(prefix) + r'[a-zA-Z0-9_.]+', code))
        else:
            return view.extract_completions(prefix, point) # default implementation if no '.' in the prefix

    def unique(self, seq):
        seen = set()
        for item in seq:
            if item not in seen:
                seen.add(item)
                yield item

    def on_query_completions(self, view, prefix, locations):
        # parts of the code inspired by: https://github.com/agibsonsw/AndyPython/blob/master/PythonCompletions.py
        global builtin_compl_vars, builtin_compl_funcs, magic_control_pars
        if not view.match_selector(locations[0], 'source.ksp -string -comment -constant'):
            return []
        pt = locations[0] # - len(prefix) - 1
        line_start_pos = view.line(sublime.Region(pt, pt)).begin()
        line = view.substr(sublime.Region(line_start_pos, pt))    # the character before the trigger

        if re.match(r' *declare .*', line) and ':=' not in line:
            compl = []
        elif re.match(r'.*-> ?[a-zA-Z_]*$', line): # if the line ends with something like '->' or '->valu'
            compl = magic_control_pars
        else:
            compl = self._extract_completions(view, prefix, pt)
            compl = [(item + "\tdefault", item.replace('$', '\\$', 1)) for item in compl
                     if len(item) > 3 and item not in all_builtins]
            if '.' not in prefix:
                bc = []
                bc.extend(builtin_compl_vars)
                bc.extend(builtin_compl_funcs)
                compl.extend(bc)
        compl = self.unique(compl)

        return (compl, sublime.INHIBIT_WORD_COMPLETIONS |
                sublime.INHIBIT_EXPLICIT_COMPLETIONS)


class NumericSequenceCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        if len(self.view.sel()) < 2:
            return
        start = self.view.substr(self.view.sel()[0])
        if start and not re.match(r'\d+', start):
            return
        start = int(start) if start else 1

        for i, selection in enumerate(self.view.sel()):
            self.view.replace(edit, selection, str(start + i))


class ReplaceTextWithCommand(sublime_plugin.TextCommand):
    def run(self, edit, new_text=''):
        self.view.replace(edit, sublime.Region(0,self.view.size()),new_text)


class KspGlobalSettingToggleCommand(sublime_plugin.ApplicationCommand):
    def run(self, setting, default):
        sksp_options_dict = {
            "ksp_compact_output" : "Remove Indents and Empty Lines",
            "ksp_compact_variables" : "Compact Variables",
            "ksp_extra_checks" : "Extra Syntax Checks",
            "ksp_optimize_code" : "Optimize Compiled Code",
            "ksp_signal_empty_ifcase" : "Raise Error on Empty 'if' or 'case' Statements",
            "ksp_add_compiled_date" : "Add Compilation Date/Time Comment",
            "ksp_comment_inline_functions" : "Insert Comments When Expanding Functions",
            "ksp_play_sound" : "Play Sound When Compilation Finishes"
        }

        s = sublime.load_settings("KSP.sublime-settings")
        s.set(setting, not s.get(setting, False))
        sublime.save_settings("KSP.sublime-settings")

        if s.get(setting, False):
            option_toggle = "enabled!"
        else:
            option_toggle = "disabled!"

        sublime.status_message('SublimeKSP option %s is %s' % (sksp_options_dict[setting], option_toggle))

    def is_checked(self, setting, default):
        return bool(sublime.load_settings("KSP.sublime-settings").get(setting, default))

    def is_enabled(self, setting, default):
        extra_checks = bool(sublime.load_settings("KSP.sublime-settings").get("ksp_extra_checks", default))
        optim_code = bool(sublime.load_settings("KSP.sublime-settings").get("ksp_optimize_code", default))
        signal_empty = bool(sublime.load_settings("KSP.sublime-settings").get("ksp_signal_empty_ifcase", default))

        if setting == "ksp_optimize_code":
            return extra_checks
        elif setting == "ksp_signal_empty_ifcase":
            return signal_empty and not (extra_checks and optim_code)
        else:
            return True

class KspIndentListener(sublime_plugin.EventListener):
    def on_text_command(self, view, command_name, args):
        if command_name == 'reindent' and view.sel()[0].size() > 0:
            return ('ksp_reindent', args)
        else:
            return None


class KspReindent(sublime_plugin.TextCommand):
    def get_indent(self, line):
        return line[:len(line) - len(line.lstrip())]

    def reindent(self, lines, indent):
        increase_indent = re.compile(r'\s*(on|if|else|select|while|function|taskfunc|macro|for|family|property|case)\b')
        decrease_indent = re.compile(r'(?m)^\s*(end\s+(\w+)|case\b|else\b)')
        result = []

        last_line = None
        for line in lines:
            if last_line is not None:
                m = increase_indent.match(last_line)
                ind = self.get_indent(last_line)
                if m:
                    if m.group(1) == 'select':
                        ind = ind + indent * 2
                    else:
                        ind = ind + indent
                m = decrease_indent.match(line)
                if m:
                    if line.lstrip().startswith('end select'):
                        ind = ind.replace(indent, '', 2)
                    else:
                        ind = ind.replace(indent, '', 1)
                line = ind + line.lstrip()
            if line.strip():
                last_line = line
            result.append(line)
        return result

    def run(self, edit, **kwargs):
        tab_size = int(self.view.settings().get('tab_size', 8))
        use_spaces = self.view.settings().get('translate_tabs_to_spaces', True)
        indent = ' ' * tab_size if use_spaces else '\t'

        for sel in self.view.sel():
            code = self.view.substr(sel)
            code = '\n'.join(self.reindent(code.split('\n'), indent))
            self.view.replace(edit, sel, code)


class KspOnEnter(sublime_plugin.TextCommand):
    def get_line(self, lineno):
        if not (0 <= lineno <= self.get_last_lineno()):
            return ''
        return self.view.substr(self.view.line(self.view.text_point(lineno, 0)))

    def get_last_lineno(self):
        row, col = self.view.rowcol(self.view.size())
        return row

    def get_indent(self, line):
        return re.match(r'(\s*)', line).group(1)

    def run(self, edit):
        self.view.run_command('insert', {'characters': '\n'})
        for selection in self.view.sel():
            row, col = self.view.rowcol(selection.begin())
            prev_line = self.get_line(row-1)
            this_line = self.get_line(row)
            next_line = self.get_line(row+1)
            m = re.match(r'\s*(list|const|struct|on|if|select|while|function|taskfunc|macro|for|family|property)\b', prev_line)
            # if the next line is not an 'end ...' line, the next line is not already more indented and the regexp matched
            if (not (next_line and next_line.lstrip().startswith('end ') and
                     len(self.get_indent(next_line)) == len(self.get_indent(prev_line))) and
               len(self.get_indent(next_line)) <= len(self.get_indent(prev_line)) and m):
                # insert end text
                indent = self.get_indent(prev_line)
                end_line = '\n%send %s' % (indent, m.group(1))
                self.view.insert(edit, selection.b, end_line)
                # remove the old selection and add a new
                self.view.sel().subtract(self.view.line(self.view.text_point(row+1, 0)))
                self.view.sel().add(sublime.Region(self.view.text_point(row, len(this_line))))
            self.view.run_command('move_to', {"to": "eol"})


class KspUncompressCode(sublime_plugin.TextCommand):
    def run(self, edit):
        global last_compiler
        if last_compiler:
            uncompress = last_compiler.uncompress_variable_names
            selections = self.view.sel()
            if len(selections) == 1 and selections[0].empty():
                selections = [sublime.Region(0, self.view.size())]
            for selection in selections:
                code = self.view.substr(selection)
                self.view.replace(edit, selection, uncompress(code))

    def is_enabled(self):
        # only show the command when a file with KSP syntax highlighting is visible
        return 'KSP.sublime-syntax' in self.view.settings().get('syntax', '')


class KspAboutCommand(sublime_plugin.ApplicationCommand):
    def run(self):
        webbrowser.open('https://github.com/nojanath/SublimeKSP/wiki')


class KspFixLineEndings(sublime_plugin.EventListener):
    def is_probably_ksp_file(self, view):
        ext = os.path.splitext(view.file_name())[1].lower()
        if ext == '.ksp' or ext == '.b3s' or ext == '.nbsc':
            return True
        elif ext == '.txt':
            code = view.substr(sublime.Region(0, view.size()))
            score = sum(sc for (pat, sc) in [(r'^on init\b', 1), (r'^on note\b', 1), (r'^on release\b', 1), (r'^on controller\b', 1), ('^end function', 1), ('EVENT_ID', 2), ('EVENT_NOTE', 2), ('EVENT_VELOCITY', 2), ('^on ui_control', 3), (r'make_persistent', 2), ('^end on', 1), (r'-> result', 2), (r'declare \w+\[\w+\]', 2)]
                        if re.search('(?m)' + pat, code))
            return score >= 2
        else:
            return False

    def set_ksp_syntax(self, view):
        view.set_syntax_file("KSP.sublime-syntax")

    def on_load(self, view):
        if self.is_probably_ksp_file(view):
            s = codecs.open(view.file_name(), 'r', 'latin-1').read()
            mixed_line_endings = re.search(r'\r(?!\n)', s) and '\r\n' in s
            if mixed_line_endings:
                s, changes = re.subn(r'\r+\n', '\n', s) # normalize line endings
                if changes:
                    s = '\n'.join(x.rstrip() for x in s.split('\n')) # strip trailing white-space too while we're at it
                    view.run_command('replace_text_with', {'new_text': s})
                    sublime.set_timeout(lambda: sublime.status_message('EOL characters automatically fixed. Please save to keep the changes.'), 100)
            self.set_ksp_syntax(view)