import os
import sublime
import sublime_plugin
import re
import logging
import errno

SETTINGS = [
    "alias",
    "default_initial",
    "use_cursor_text",
    "show_files",
    "show_path",
    "default_root",
    "default_path",
    "default_folder_index",
    "os_specific_alias",
    "ignore_case",
    "alias_root",
    "alias_path",
    "alias_folder_index",
    "debug",
    "auto_refresh_sidebar"
]
VIEW_NAME = "AdvancedNewFileCreation"
WIN_ROOT_REGEX = r"[a-zA-Z]:(/|\\)"
NIX_ROOT_REGEX = r"^/"
HOME_REGEX = r"^~"

# Set up logger
logging.basicConfig(format='[AdvancedNewFile] %(levelname)s %(message)s')
logger = logging.getLogger()


class AdvancedNewFileCommand(sublime_plugin.WindowCommand):
    def run(self, is_python=False):
        self.PLATFORM = sublime.platform().lower()
        self.root = None
        self.alias_root = None
        self.top_level_split_char = ":"
        self.is_python = is_python
        self.view = self.window.active_view()

        # Settings will be based on the view
        settings = get_settings(self.view)
        self.aliases = self.get_aliases(settings)
        self.show_path = settings.get("show_path")
        self.auto_refresh_sidebar = settings.get("auto_refresh_sidebar")
        self.default_folder_index = settings.get("default_folder_index")
        self.alias_folder_index = settings.get("alias_folder_index")
        default_root = self.get_default_root(settings.get("default_root"))
        if default_root == "path":
            self.root = os.path.expanduser(settings.get("default_path"))
            default_root = ""
        self.root, path = self.split_path(default_root)

        # Set some default values for the auto complete
        PathAutocomplete.set_show_files(settings.get("show_files"))
        PathAutocomplete.set_aliases(self.aliases)
        PathAutocomplete.set_ignore_case(settings.get("ignore_case"))

        # Search for initial string
        path = settings.get("default_initial", "")
        if settings.get("use_cursor_text", False):
            tmp = self.get_cursor_path()
            if tmp != "":
                path = tmp

        alias_root = self.get_default_root(settings.get("alias_root"), True)
        if alias_root == "path":
            self.alias_root = os.path.expanduser(settings.get("alias_path"))
            alias_root = ""
        self.alias_root, tmp = self.split_path(alias_root, True)

        debug = settings.get("debug") or False
        if debug:
            logger.setLevel(logging.DEBUG)
        else:
            logger.setLevel(logging.ERROR)
        # Get user input
        self.show_filename_input(path)

    def get_aliases(self, settings):
        aliases = settings.get("alias")
        all_os_aliases = settings.get("os_specific_alias")
        for key in all_os_aliases:
            if self.PLATFORM in all_os_aliases.get(key):
                aliases[key] = all_os_aliases.get(key).get(self.PLATFORM)

        return aliases

    def get_default_root(self, string, is_alias=False):
        root = ""

        if string == "home":
            root = "~/"
        elif string == "current":
            root = self.top_level_split_char
        elif string == "project_folder":
            if is_alias:
                folder_index = self.alias_folder_index
            else:
                folder_index = self.default_folder_index
            if len(self.window.folders()) <= folder_index:
                if is_alias:
                    self.alias_folder_index = 0
                else:
                    self.default_folder_index = 0
        elif string == "top_folder":
            if is_alias:
                self.alias_folder_index = 0
            else:
                self.default_folder_index = 0
        elif string == "path":
            root = "path"
        else:
            logger.error("Invalid specifier for \"default_root\"")
        return root

    def split_path(self, path="", is_alias=False):
        HOME_REGEX = r"^~[/\\]"
        root = None
        try:
            # Parse windows root
            if self.PLATFORM == "windows":
                if re.match(WIN_ROOT_REGEX, path):
                    root = path[0:3]
                    path = path[3:]

            # Parse if alias
            if self.top_level_split_char in path and root == None:
                parts = path.rsplit(self.top_level_split_char, 1)
                root, path = self.translate_alias(parts[0])
                path_list = []
                if path != "":
                    path_list.append(path)
                if parts[1] != "":
                    path_list.append(parts[1])
                path = self.top_level_split_char.join(path_list)
            # Parse if tilde used
            elif re.match(HOME_REGEX, path) and root == None:
                root = os.path.expanduser("~")
                path = path[2:]

            # Default
            if root == None:
                if is_alias:
                    root = self.alias_root
                    folder_index = self.alias_folder_index
                else:
                    root = self.root
                    folder_index = self.default_folder_index
                root = root or self.window.folders()[folder_index]
        except IndexError:
            root = os.path.expanduser("~")
        return root, path

    def translate_alias(self, path):
        root = None
        split_path = None
        if path == "" and self.view is not None:
            filename = self.view.file_name()
            if filename is not None:
                root = os.path.dirname(filename)
        else:
            split_path = path.split(self.top_level_split_char)
            join_index = len(split_path) - 1
            target = path
            root_found = False
            while join_index >= 0 and not root_found:
                # Folder aliases
                for folder in self.window.folders():
                    basename = os.path.basename(folder)
                    if basename == target:
                        root = folder
                        root_found = True
                        break
                # Aliases from settings.
                for alias in self.aliases.keys():
                    if alias == target:
                        alias_path = self.aliases.get(alias)
                        if re.search(HOME_REGEX, alias_path) is None:
                            if self.PLATFORM == "windows":
                                if re.search(WIN_ROOT_REGEX, alias_path) is None:
                                    root = os.path.join(self.alias_root, alias_path)
                                    break
                            else:
                                if re.search(NIX_ROOT_REGEX, alias_path) is None:
                                    root = os.path.join(self.alias_root, alias_path)
                                    break
                        root = os.path.expanduser(alias_path)
                        root_found = True
                        break
                remove = re.escape(split_path[join_index])
                target = re.sub(r":%s$" % remove, "", target)
                join_index -= 1

        if root is None:
            return None, path
        elif split_path is None:
            return os.path.abspath(root), ""
        else:
            # Add to index so we re
            join_index += 2
            return os.path.abspath(root), self.top_level_split_char.join(split_path[join_index:])

    def show_filename_input(self, initial=''):
        caption = 'Enter a path for a new file'
        if self.is_python:
            caption = '%s (creates __init__.py in new dirs)' % caption
        view = self.window.show_input_panel(
            caption, initial,
            self.entered_filename, self.update_filename_input, self.clear
        )

        view.set_name(VIEW_NAME)
        temp = view.settings().get("word_separators")
        temp = temp.replace(".", "")
        view.settings().set("word_separators", temp)
        view.settings().set("auto_complete_commit_on_tab", True)
        view.settings().set("tab_completion", True)

        PathAutocomplete.set_view_id(view.id())
        PathAutocomplete.set_root(self.root, True)

    def update_filename_input(self, path_in):
        base, path = self.split_path(path_in)
        if self.top_level_split_char in path_in or re.match(r"^~[/\\]", path_in):
            PathAutocomplete.set_root(base, False)
        else:
            PathAutocomplete.set_root(base, True)

        creation_path = self.generate_creation_path(base, path)
        if self.show_path:
            if self.view != None:
                self.view.set_status("AdvancedNewFile", "Creating file at %s " % \
                    creation_path)
            else:
                sublime.status_message("Creating file at %s " % creation_path)
        logger.debug("Creation path is '%s'" % creation_path)
        PathAutocomplete.set_path(path)

    def generate_creation_path(self, base, path):
        if self.PLATFORM == "windows":
            if not re.match(WIN_ROOT_REGEX, base):
                return base + self.top_level_split_char + path
        else:
            if not re.match(NIX_ROOT_REGEX, base):
                return base + self.top_level_split_char + path

        return os.path.abspath(os.path.join(base, path))

    def entered_filename(self, filename):
        # Check if valid root specified for windows.
        if self.PLATFORM == "windows":
            if re.match(WIN_ROOT_REGEX, filename):
                root = filename[0:3]
                if not os.path.isdir(root):
                    sublime.error_message(root + " is not a valid root.")
                    self.clear()
                    return

        base, path = self.split_path(filename)
        file_path = os.path.join(base, path)
        # Check for invalid alias specified.
        if self.top_level_split_char in filename and \
            not (self.PLATFORM == "windows" and re.match(WIN_ROOT_REGEX, base)) and \
            not (self.PLATFORM != "windows" and re.match(NIX_ROOT_REGEX, base)):
            if base == "":
                error_message = "Current file cannot be resolved."
            else:
                error_message = "'" + base + "' is an invalid alias."
            sublime.error_message(error_message)
        else:
            attempt_open = True
            logger.debug("Creating file at %s", file_path)
            if not os.path.exists(file_path):
                try:
                    self.create(file_path)
                except OSError as e:
                    attempt_open = False
                    sublime.error_message("Cannot create '" + file_path + "'. See console for details")
                    logger.error("Exception: %s '%s'" % (e.strerror, e.filename))
            if attempt_open:
                if os.path.isdir(file_path):
                    if not re.search(r"(/|\\)$", file_path):
                        sublime.error_message("Cannot open view for '" + file_path + "'. It is a directory. ")
                else:
                    self.window.open_file(file_path)
        self.clear()
        self.refresh_sidebar()

    def refresh_sidebar(self):
        if self.auto_refresh_sidebar:
            try:
                self.window.run_command("refresh_folder_list")
            except:
                pass


    def clear(self):
        if self.view != None:
            self.view.erase_status("AdvancedNewFile")
        PathAutocomplete.clear()

    def create(self, filename):
        base, filename = os.path.split(filename)
        self.create_folder(base)
        if filename != "":
            open(os.path.join(base, filename), "a").close()

    def create_folder(self, path):
        try:
            os.makedirs(path)
        except OSError as ex:
            if ex.errno != errno.EEXIST:
                raise
        if self.is_python:
            open(os.path.join(base, '__init__.py'), 'a').close()

    def get_cursor_path(self):
        if self.view == None:
            return ""

        view = self.view
        path = ""
        for region in view.sel():
            syntax = view.syntax_name(region.begin())
            if region.begin() != region.end():
                path = view.substr(region)
                break
            if re.match(".*string.quoted.double", syntax) or re.match(".*string.quoted.single", syntax):
                path = view.substr(view.extract_scope(region.begin()))
                path = re.sub('^"|\'', '',  re.sub('"|\'$', '', path.strip()))
                break

        return path


class PathAutocomplete(sublime_plugin.EventListener):
    aliases = {}
    show_files = False
    ignore_case = False

    path = ""
    root = ""
    default_root = True
    view_id = None

    prev_suggestions = []
    prev_base = ""
    prev_directory = ""
    path_empty = True
    prev_root = ""
    prev_prefix = ""
    prev_locations = []

    def on_query_context(self, view, key, operator, operand, match_all):
        if key == "advanced_new_file_completion" and PathAutocomplete.view_id != None and view.id() == PathAutocomplete.view_id:
            return True
        return None

    def continue_previous_autocomplete(self):
        pac = PathAutocomplete
        sep = os.sep
        root_path = pac.root + sep
        prev_base = pac.prev_base
        prev_directory = pac.prev_directory
        prev_root = pac.prev_root

        base = os.path.basename(pac.path)
        directory = os.path.dirname(pac.path)

        # If base is empty, we may be cycling through directory options
        if base == "":
            return True

        # Ensures the correct directory is used if the default root is specified
        # using an alias.
        if base == prev_base and \
        directory == prev_directory and \
        prev_root == root_path and \
        pac.default_root:
            return True
        # Continue completions if file names are completed.
        if os.path.isfile(os.path.join(root_path, pac.path)):
            return True
        return False

    def on_query_completions(self, view, prefix, locations):
        self.suggestion_entries = []
        pac = PathAutocomplete
        if pac.view_id == None or view.id() != pac.view_id:
            return []

        auto_complete_prefix = ""
        if self.continue_previous_autocomplete() and prefix != "":
            logger.debug("(Prev) Suggestions")
            logger.debug(pac.prev_suggestions)
            if len(pac.prev_suggestions) > 1:
                return (pac.prev_suggestions, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
            elif len(pac.prev_suggestions) == 1:
                auto_complete_prefix = pac.prev_suggestions[0][1]

        suggestions = []
        suggestions_w_spaces = []
        root_path = pac.root + os.sep
        directory, base = os.path.split(pac.path)

        if directory == "" and pac.default_root:
            # Project folders
            sugg, sugg_w_spaces = self.generate_project_auto_complete(base)
            suggestions += sugg
            suggestions_w_spaces += sugg_w_spaces
            # Aliases
            sugg, sugg_w_spaces = self.generate_alias_auto_complete(base)
            suggestions += sugg
            suggestions_w_spaces += sugg_w_spaces

        # Directories
        path = os.path.join(root_path, directory)
        if os.path.exists(path):
            sugg, sugg_w_spaces = self.generate_relative_auto_complete(path, base, auto_complete_prefix)
            suggestions += sugg
            suggestions_w_spaces += sugg_w_spaces
        # If suggestions exist, use complete name
        # else remove base prefix
        if len(suggestions) > 0:
            for name in suggestions_w_spaces:
                suggestions.append((" " + name, name))
        else:
            for name in suggestions_w_spaces:
                temp = name
                name = name[len(base) - 1:]
                suggestions.append((" " + temp, name))

        if len(suggestions) == 0 and locations == pac.prev_locations:
            return (pac.prev_suggestions, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
        # Previous used to determine cycling through entries.
        pac.prev_directory = directory
        pac.prev_base = base
        pac.prev_suggestions = suggestions
        pac.prev_root = root_path
        pac.prev_prefix = prefix
        pac.prev_locations = locations
        logger.debug("Suggestions:")
        logger.debug(suggestions)
        return (suggestions, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)

    def generate_project_auto_complete(self, base):
        folders = sublime.active_window().folders()
        if len(folders) > 1:
            folders = map(lambda f: os.path.basename(f), folders)
            return self.generate_auto_complete(base, folders)
        return [], []

    def generate_alias_auto_complete(self, base):
        return self.generate_auto_complete(base, PathAutocomplete.aliases)

    def generate_auto_complete(self, base, iterable_var):
        sugg = []
        sugg_w_spaces = []

        for entry in iterable_var:
            if entry in self.suggestion_entries:
                continue
            self.suggestion_entries.append(entry)
            compare_entry = entry
            compare_base = base
            if PathAutocomplete.ignore_case:
                compare_entry = compare_entry.lower()
                compare_base = compare_base.lower()

            if compare_entry.find(compare_base) == 0:
                if " " in base:
                    sugg_w_spaces.append(entry + ":")
                else:
                    sugg.append((entry + ":", entry + ":"))
        return sugg, sugg_w_spaces

    def generate_relative_auto_complete(self, path, base, auto_complete_prefix):
        sep = os.sep
        sugg = []
        sugg_w_spaces = []

        # Attempt to prevent searching the same path when a path has been specified
        # Problems occur when using tab to complete entry with single completion
        # followed by ctrl + space
        if ":" in auto_complete_prefix:
            compare_prefix = auto_complete_prefix.split(":", 1)[1]
        else:
            compare_prefix = auto_complete_prefix

        if re.search(r"[/\\]$", auto_complete_prefix) and not path.endswith(compare_prefix[0:-1]):
            path = os.path.join(path, compare_prefix)

        for filename in os.listdir(path):
            if PathAutocomplete.show_files or os.path.isdir(os.path.join(path, filename)):
                compare_base = base
                compare_filename = filename
                if PathAutocomplete.ignore_case:
                    compare_base = compare_base.lower()
                    compare_filename = filename.lower()

                if compare_filename.find(compare_base) == 0:
                    # Need to find a better way to do the auto complete.
                    if " " in compare_base:
                        if os.path.isdir(os.path.join(path, filename)):
                            sugg_w_spaces.append(auto_complete_prefix + filename + sep)
                        else:
                            sugg_w_spaces.append(auto_complete_prefix + filename)
                    else:
                        if os.path.isdir(os.path.join(path, filename)):
                            sugg.append((" " + auto_complete_prefix + filename + sep, auto_complete_prefix + filename + sep))
                        else:
                            sugg.append((" " + auto_complete_prefix + filename, auto_complete_prefix + filename))

        return sugg, sugg_w_spaces

    @staticmethod
    def set_path(path_input):
        PathAutocomplete.path = path_input

    @staticmethod
    def set_root(root_input, default_root):
        PathAutocomplete.root = root_input
        PathAutocomplete.default_root = default_root

    @staticmethod
    def clear():
        PathAutocomplete.path = ""
        PathAutocomplete.root = ""
        PathAutocomplete.prev_suggestions = []
        PathAutocomplete.prev_base = ""
        PathAutocomplete.prev_directory = ""
        PathAutocomplete.aliases = {}
        PathAutocomplete.path_empty = True
        PathAutocomplete.prev_root = ""
        PathAutocomplete.default_root = True
        PathAutocomplete.show_files = False
        PathAutocomplete.prev_prefix = ""
        PathAutocomplete.prev_locations = []
        PathAutocomplete.view_id = None

    @staticmethod
    def set_aliases(aliases):
        PathAutocomplete.aliases = aliases

    @staticmethod
    def set_show_files(show_files):
        PathAutocomplete.show_files = show_files

    @staticmethod
    def set_ignore_case(ignore_case):
        PathAutocomplete.ignore_case = ignore_case

    @staticmethod
    def set_view_id(view_id):
        PathAutocomplete.view_id = view_id


def get_settings(view):
    settings = sublime.load_settings("AdvancedNewFile.sublime-settings")
    project_settings = {}
    local_settings = {}
    if view != None:
        project_settings = view.settings().get('AdvancedNewFile', {})

    for setting in SETTINGS:
        local_settings[setting] = settings.get(setting)

    for key in project_settings:
        if key in SETTINGS:
            if key == "alias":
                local_settings[key] = dict(local_settings[key].items() + project_settings.get(key).items())
            else:
                local_settings[key] = project_settings[key]
        else:
            logger.error("AdvancedNewFile[Warning]: Invalid key '%s' in project settings.", key)

    return local_settings
