import sublime
import sublime_plugin
import re
import os

class ProjectSpecificKeyBinding(sublime_plugin.EventListener):
  def on_query_context(self, view, key, operator, operand, match_all):
    current_project_name = view.window().project_file_name()

    if not current_project_name:
      return None

    if key != "project_name" and os.path.isfile(current_project_name):
      return None

    if operator == sublime.OP_EQUAL:
      return current_project_name == operand
    elif operator == sublime.OP_NOT_EQUAL:
      return current_project_name != operand
    elif operator == sublime.QueryOperator.REGEX_CONTAINS:
      return re.search(operand, current_project_name)
    elif operator == sublime.QueryOperator.NOT_REGEX_CONTAINS:
      return not re.search(operand, current_project_name)

class FileSpecificKeyBinding(sublime_plugin.EventListener):
  def on_query_context(self, view, key, operator, operand, match_all):
    current_file_name = view.file_name()

    if not current_file_name:
      return None

    if key != "file_name" and os.path.isfile(current_file_name):
      return None

    if operator == sublime.OP_EQUAL:
      return current_file_name == operand
    elif operator == sublime.OP_NOT_EQUAL:
      return current_file_name != operand
    elif operator == sublime.QueryOperator.REGEX_CONTAINS:
      return re.search(operand, current_file_name)
    elif operator == sublime.QueryOperator.NOT_REGEX_CONTAINS:
      return not re.search(operand, current_file_name)

class _CursorAdaptiveCommand(sublime_plugin.TextCommand):
  handle_region_fallback = ('keep',)

  def __align_point_after_insertions(self, orig_point, direction):
    new_point = orig_point
    for (insert_point, insert_offset) in self.__inserted_offsets:
      if direction == 'right' and insert_point <= orig_point:
        new_point += insert_offset
      elif direction == 'left' and insert_point < orig_point:
        new_point += insert_offset
    return new_point

  def __cleanup(self):
    self.__inserted_offsets = list()

  def run(self, edit, **kwargs):
    self.__cleanup()

    actions = list()
    for region in self.view.sel():
      action = self.handle_region(region)
      if action:
        actions.append((region, *(action or self.handle_region_fallback)))

    if not actions:
      return self.no_action_fallback(edit)

    new_regions = list()
    for (region, action, *args) in actions:
      handler = getattr(self, f'perform_{action}')
      new_region = handler(edit, region, *args)
      if new_region:
        new_regions.append(new_region)
      else:
        new_regions.append(self.perform_keep(region, edit))

    real_regions = list()
    for (region, direction, delta, backoff) in new_regions:
      if direction in ['collapse-left', 'collapse-right']:
        # Adding a `delta` hasn't been particularly tested with multiple cursors at once.
        point = _normalize_region(region).a
        align_direction = ('right' if direction == 'collapse-left' else 'left')
        aligned_point = self.__align_point_after_insertions(point, align_direction)
        if delta:
          if direction == 'collapse-right':
            aligned_point += delta
          else:
            aligned_point -= delta
        real_regions.append(aligned_point)
      elif direction in ['extend-left', 'extend-right']:
        start = region.a
        end = region.b
        align_direction = ('left' if direction == 'extend-left' else 'right')
        aligned_start = self.__align_point_after_insertions(start, None)
        aligned_end = self.__align_point_after_insertions(end, align_direction)
        if region.a == region.b and delta:
          if direction == 'extend-right':
            aligned_start += delta
          else:
            aligned_start -= delta
        if backoff:
          if direction == 'extend-right':
            aligned_end -= backoff
          else:
            aligned_end += backoff
        real_regions.append(sublime.Region(aligned_start, aligned_end))
      elif direction in ['left', 'right']:
        start = region.a
        end = region.b + delta
        aligned_end = self.__align_point_after_insertions(end, direction)
        if region.a == region.b:
          if backoff:
            if direction == 'right':
              aligned_end -= backoff
            else:
              aligned_end += backoff
          real_regions.append(sublime.Region(aligned_end, aligned_end))
        else:
          aligned_start = self.__align_point_after_insertions(start, direction)
          real_regions.append(sublime.Region(aligned_start, aligned_end))

    self.view.sel().clear()
    self.view.sel().add_all(real_regions)
    self.__cleanup()

  def handle_region(self, region):
    pass

  def no_action_fallback(self, edit):
    pass

  def perform_keep(self, edit, region):
    return (region, 'right', 0, 0)

  def perform_move(self, edit, region, delta):
    return (region, 'right', delta, 0)

  def perform_collapse_move(self, edit, region, delta):
    return (region, 'collapse-right', delta, 0)

  def perform_insert(self, edit, region, direction, insert, backoff, extend_delta=None):
    insert_point = region.b
    insert_direction = ('left' if direction in ['left', 'extend-left'] else 'right')
    aligned_insert_point = self.__align_point_after_insertions(insert_point, insert_direction)
    insert_offset = self.view.insert(edit, aligned_insert_point, insert)
    self.__inserted_offsets.append((insert_point, insert_offset))
    delta = (extend_delta if direction in ['extend-left', 'extend-right'] else 0)
    return (region, direction, delta, backoff)

  def perform_replace_left(self, edit, region, erase, replace, backoff, move_side):
    replace_point = _normalize_region(region).a
    replace_point -= erase
    aligned_replace_point = self.__align_point_after_insertions(replace_point, 'right')
    aligned_replace_region = sublime.Region(aligned_replace_point, aligned_replace_point + erase)
    self.view.replace(edit, aligned_replace_region, replace)
    self.__inserted_offsets.append((replace_point, len(replace) - erase))
    return (region, move_side, 0, backoff)

  def perform_wrap(self, edit, region, insert_left, insert_right, move_side = None):
    if region.a < region.b:
      left = region.a
      right = region.b
    else:
      left = region.b
      right = region.a

    aligned_insert_point = self.__align_point_after_insertions(left, 'left')
    insert_offset = self.view.insert(edit, aligned_insert_point, insert_left)
    self.__inserted_offsets.append((left, insert_offset))

    aligned_insert_point = self.__align_point_after_insertions(right, 'right')
    insert_offset = self.view.insert(edit, aligned_insert_point, insert_right)
    self.__inserted_offsets.append((right, insert_offset))

    if move_side == 'left':
      return (region, 'collapse-left', 0, 0)
    else:
      return (region, 'collapse-right', 0, 0)

def _normalize_region(region):
  if region.a < region.b:
    return region
  else:
    return sublime.Region(region.b, region.a)

def _match_tag_in_progress(view, region):
  region = _normalize_region(region)
  to_start = sublime.Region(view.line(region).a, region.a)
  match = re.search(r'(\[\[(?:(?!]]|\|).)*)$', view.substr(to_start))
  if match:
    return match.group(1)

def _match_regex_ahead_same_line(view, region, regex):
  region = _normalize_region(region)
  to_end = sublime.Region(region.a, view.line(region).b)
  return re.search(regex, view.substr(to_end))

def _match_rest_of_tag_in_progress(view, region):
  regex = r'^((?:(?!\[\[).)*]])'
  match = _match_regex_ahead_same_line(view, region, regex)
  if match:
    return match.group(1)

def _match_through_closing_html_tag(view, region, tag):
  regex = rf'^(.*?</{tag}>)'
  match = _match_regex_ahead_same_line(view, region, regex)
  if match:
    return match.group(1)

def _consider_reference_cycle(view, region, cycle):
  tag_in_progress = _match_tag_in_progress(view, region)

  if not tag_in_progress:
    return None

  rest_of_tag = _match_rest_of_tag_in_progress(view, region)

  rest_of_ref = re.search(r'^((?:(?!\|).)*:)', rest_of_tag)
  if rest_of_ref:
    return ('move', len(rest_of_ref.group(1)))

  if tag_in_progress == '[[':
    current_ref = None
    insert_pipe = rest_of_tag != ']]' and '|' not in rest_of_tag
  elif tag_in_progress[-1:] == ':':
    current_ref = tag_in_progress[2:-1]
    insert_pipe = False
  else:
    return None

  if current_ref in cycle:
    next_ref = cycle[(cycle.index(current_ref) + 1) % len(cycle)]
  else:
    next_ref = cycle[0]

  if current_ref:
    return ('replace_left', len(current_ref) + 1, next_ref + ':', 0, 'right')
  elif insert_pipe:
    directory = _guess_directory(rest_of_tag[:-2])
    return ('insert', 'extend-right', next_ref + ':' + directory + '|', 1, len(next_ref) + 1)
  else:
    return ('insert', 'right', next_ref + ':', 0)

def _consider_transform_url(view, region):
  region = _normalize_region(region)
  selected_text = view.substr(region)

  start_through_selection = sublime.Region(view.line(region).a, region.b)
  match = re.search(r'https?://\S+$', view.substr(start_through_selection))

  if not match:
    return None

  # We don't have a convenient way to just replace a selection in one move,
  # so just move to the end of the selection and try again next time...
  if selected_text:
    return ('collapse_move', len(selected_text))

  url = match.group(0)
  link = None

  hsm = r'(?:https://(?:preview\.|staging\.)?hsmusic\.wiki/|http://localhost:\d+/)'

  match = re.search(hsm + r'(album|track|artist|group|tag|flash)/(.+?)/', url)
  if match:
    link = f'[[{match.group(1)}:{match.group(2)}]]'

  if not link:
    return None

  return ('replace_left', len(url), link, 0, 'right')

def _guess_directory(name):
  directory = name
  directory =                     "-".join(directory.split(" "))
  directory =                              directory.replace("&", "and")
  directory = re.sub(r"[^a-zA-Z0-9-]", "", directory)
  directory =        re.sub(r"-{2,}", "-", directory)
  directory =       re.sub(r"^-+|-+$", "", directory)
  directory =                              directory.lower()
  return directory

class EnterExitWikiTagCommand(_CursorAdaptiveCommand):
  ref_cycle = ['artist', 'group', 'flash', 'tag']

  def handle_region(self, region):
    cycle_action = _consider_reference_cycle(self.view, region, self.ref_cycle)
    if cycle_action:
      return cycle_action

    rest_of_tag = _match_rest_of_tag_in_progress(self.view, region)
    if rest_of_tag:
      return ('move', len(rest_of_tag))
    elif region.a == region.b:
      return ('insert', 'right', '[[]]', 2)
    else:
      return ('wrap', '[[', ']]', 'left')

class ExitWikiTagCommand(_CursorAdaptiveCommand):
  def handle_region(self, region):
    rest_of_tag = _match_rest_of_tag_in_progress(self.view, region)
    if rest_of_tag:
      return ('collapse_move', len(rest_of_tag))

    transform_url_action = _consider_transform_url(self.view, region)
    if transform_url_action:
      return transform_url_action

    region = _normalize_region(region)
    to_start = sublime.Region(self.view.line(region).a, region.a)
    to_end = sublime.Region(region.a, self.view.line(region).b)
    if to_start.a != to_start.b and to_end.a != to_end.b:
      match = re.search(r'.*?([).,:;!?][).,:;!?"\']*|\\s(?=[(\'"])|[\'"](?=\\s|$)|$)', self.view.substr(to_end))
      if match:
        return ('collapse_move', len(match.group(0)))

  def no_action_fallback(self, edit):
    self.view.run_command('insert', {'characters': '\n'})

class HyphenInWikiTagCommand(_CursorAdaptiveCommand):
  ref_cycle = ['artist', 'group', 'flash', 'tag']

  def handle_region(self, region):
    cycle_action = _consider_reference_cycle(self.view, region, self.ref_cycle)
    if cycle_action:
      return cycle_action

    tag_in_progress = _match_tag_in_progress(self.view, region)

    if tag_in_progress and ':' in tag_in_progress:
      return ('insert', 'right', '-', 0)

class SpaceInWikiTagCommand(_CursorAdaptiveCommand):
  ref_cycle = ['track', 'album']

  handle_region_fallback = ('insert', 'right', ' ', 0)

  def handle_region(self, region):
    cycle_action = _consider_reference_cycle(self.view, region, self.ref_cycle)
    if cycle_action:
      return cycle_action

    tag_in_progress = _match_tag_in_progress(self.view, region)

    if tag_in_progress and ':' in tag_in_progress:
      if self.view.substr(region.b) == '|':
        return ('move', 1)
      else:
        return ('insert', 'right', '|', 0)

  def no_action_fallback(self, edit):
    self.view.run_command('insert', {'characters': ' '})

class BoldWikiTextCommand(_CursorAdaptiveCommand):
  def handle_region(self, region):
    rest_of_tag = _match_through_closing_html_tag(self.view, region, 'b')
    if rest_of_tag:
      return ('move', len(rest_of_tag))
    elif region.a == region.b:
      return ('insert', 'right', '<b></b>', 4)
    else:
      return ('wrap', '<b>', '</b>', 'left')

class InsertLineBreakInWikiTextCommand(_CursorAdaptiveCommand):
  def handle_region(self, region):
    region = _normalize_region(region)
    to_end = sublime.Region(region.a, self.view.line(region).b)
    if region.a == to_end.b:
      return ('insert', 'right', '<br>', 0)
    else:
      return ('move', len(self.view.substr(to_end)))
