#!/usr/bin/env python3.3
# -*- coding: utf-8 -*-

import sublime
import sublime_plugin

import socket
import socketserver
import struct
import json
import os
import threading
from weakref import WeakValueDictionary

class IPCError(Exception):
  pass

class InvalidSerialization(IPCError):
  pass

class ConnectionClosed(IPCError):
  pass

class UnsupportedAttribute(IPCError):
  pass

RequestTypeAttributeElement = '0'
RequestTypeAttributeAttribute = '1'

RequestTypeParameterizedAttributeElement = '5'
RequestTypeParameterizedAttributeAttribute = '6'
RequestTypeParameterizedAttributeParameterType = '7'
RequestTypeParameterizedAttributeParameter = '8'

ValueTypeNone = 0
ValueTypeInteger = ValueTypeNone + 1
ValueTypeFloatingPoint = ValueTypeInteger + 1
ValueTypeBool = ValueTypeFloatingPoint + 1
ValueTypeString = ValueTypeBool + 1
ValueTypeElement = ValueTypeString + 1
ValueTypeStringArray = ValueTypeElement + 1
ValueTypeElementArray = ValueTypeStringArray + 1
ValueTypeIntegerArray = ValueTypeElementArray + 1
ValueTypeIntegerArrayArray = ValueTypeIntegerArray + 1

AttributeRole = 0
AttributeSubrole = AttributeRole + 1
AttributeValue = AttributeSubrole + 1
AttributeTitle = AttributeValue + 1
AttributeParent = AttributeTitle + 1
AttributeChildren = AttributeParent + 1
AttributeWindows = AttributeChildren + 1
AttributeFocusedWindow = AttributeWindows + 1
AttributeURL = AttributeFocusedWindow + 1
AttributeSelectedTextRange = AttributeURL + 1
AttributeSyntax = AttributeSelectedTextRange + 1
AttributeTabSize = AttributeSyntax + 1
AttributeIndentation = AttributeTabSize + 1
AttributeFocused = AttributeIndentation + 1

ParameterizedAttributeStringForRange = 0
ParameterizedAttributeRangeForPosition = 1

ElementTypeApp = 0
ElementTypeWindow = ElementTypeApp + 1
ElementTypeView = ElementTypeWindow + 1

NotificationSelectedTextChanged = 0
NotificationUIElementDestroyed = 0

def read(socket):
  header = socket.recv(4)
  if len(header) == 0:
    raise ConnectionClosed()
  size = struct.unpack('!i', header)[0]
  if size == 0:
    return None
  data = socket.recv(size)
  if len(data) == 0:
    raise ConnectionClosed()
  string = data.decode("utf-8")
  parsed = json.loads(string)
  return parsed

def write(socket, data):
  datawithcount = struct.pack(bytes('!i', 'UTF-8'), len(data) + 4) + data
  socket.sendall(datawithcount)

windowidcache = WeakValueDictionary()
def windowlookup(identifier):
  if identifier in windowidcache:
    return windowidcache[identifier]
  for window in sublime.windows():
    if window.id() == identifier:
      windowidcache[identifier] = window
      return window
  return None

viewidcache = WeakValueDictionary()
def viewlookup(identifier):
  if identifier in viewidcache:
    return viewidcache[identifier]
  for window in sublime.windows():
    for view in window.views():
      if view.id() == identifier:
        viewidcache[identifier] = view
        return view
  return None

appkey = 'a'
windowkey = 'w'
viewkey = 'v'

def targetlookup(info):
  if appkey in info:
    return element(ElementTypeApp, None)
  elif windowkey in info:
    identifier = info[windowkey]
    if identifier != None:
      window = windowlookup(identifier)
      return element(ElementTypeWindow, window.id())
    return None
  elif viewkey in info:
    identifier = info[viewkey]
    if identifier != None:
      view = viewlookup(identifier)
      if view == None:
        return None
      return element(ElementTypeView, view.id())
    return None
  return None

class element(object):
  def __init__(self, type, identifier):
    self.type = type
    self.identifier = identifier
  def role(self):
    if self.type == ElementTypeApp:
      return "AXApplication"
    if self.type == ElementTypeWindow:
      return "AXWindow"
    if self.type == ElementTypeView:
      return "AXTextArea"
    return "AXUnknown"
  def subrole(self):
    if self.type == ElementTypeApp:
      return None
    if self.type == ElementTypeWindow:
      return None
    if self.type == ElementTypeView:
      return None
    return None
  def value(self):
    if self.type == ElementTypeApp:
      return None
    if self.type == ElementTypeWindow:
      return None
    if self.type == ElementTypeView:
      view = viewlookup(self.identifier)
      return view.substr(sublime.Region(0, view.size()))
    return None
  def title(self):
    if self.type == ElementTypeApp:
      return None
    if self.type == ElementTypeWindow:
      return None
    if self.type == ElementTypeView:
      return viewlookup(self.identifier).name()
    return None
  def parent(self):
    if self.type == ElementTypeApp:
      return None
    if self.type == ElementTypeWindow:
      return {appkey:0}
    if self.type == ElementTypeView:
      return {windowkey:viewlookup(self.identifier).window().id()}
  def children(self):
    if self.type == ElementTypeApp:
      return self.windows()
    if self.type == ElementTypeWindow:
      window = windowlookup(self.identifier)
      if window == None:
        return None
      return [{viewkey:v.id()} for v in window.views()]
    return None
  def windows(self):
    if self.type == ElementTypeApp:
      return [{windowkey:w.id()} for w in sublime.windows()]
    return None
  def focusedWindow(self):
    if self.type == ElementTypeApp:
      return {windowkey:sublime.active_window().id()}
    return None
  def url(self):
    if self.type == ElementTypeApp:
      return None
    if self.type == ElementTypeWindow:
      return None
    if self.type == ElementTypeView:
      return viewlookup(self.identifier).file_name()
    return None
  def selectedTextRange(self):
    if self.type != ElementTypeView:
      raise UnsupportedAttribute()
    selections = []
    for region in viewlookup(self.identifier).sel():
      selections.append([region.a, region.b])
    return selections
  def string(self, range):
    if self.type != ElementTypeView:
      raise UnsupportedAttribute()
    if len(range) != 2:
      raise InvalidSerialization()
    view = viewlookup(self.identifier)
    return view.substr(sublime.Region(range[0], range[1]))
  def linerange(self, index):
    if self.type != ElementTypeView:
      raise UnsupportedAttribute()
    view = viewlookup(self.identifier)
    linerange = view.line(sublime.Region(index, index))
    return [linerange.a, linerange.b]
  def syntax(self):
    if self.type != ElementTypeView:
      raise UnsupportedAttribute()
    view = viewlookup(self.identifier)
    syntax = view.settings().get("syntax")
    components = syntax.split('/')
    count = len(components)
    if count == 0:
      return None
    language = components[count-1]
    language = language.replace('.sublime-syntax', '')
    language = language.replace('.tmLanguage', '')
    return language
  def tabsize(self):
    if self.type != ElementTypeView:
      raise UnsupportedAttribute()
    view = viewlookup(self.identifier)
    tabsize = view.settings().get("tab_size")
    return tabsize
  def indentation(self):
    if self.type != ElementTypeView:
      raise UnsupportedAttribute()
    view = viewlookup(self.identifier)
    if view.settings().get("translate_tabs_to_spaces"):
      return 0
    return 1
  def focused(self):
    if self.type != ElementTypeView:
      return False
    view = viewlookup(self.identifier)
    if view == None:
      return False
    window = view.window()
    if window == None:
      return False
    active_view = window.active_view()
    if active_view == None:
      return False
    return active_view.id() == self.identifier

class requesthandler(socketserver.BaseRequestHandler):
  def respond(self, type, value):
    data = bytes(json.dumps({0:type,1:value}, sort_keys=True), 'UTF-8')
    write(self.request, data)
  def handle(self):
    while True:
      try:
        results = read(self.request)
      except ConnectionClosed as e:
        return
      target = None
      if RequestTypeAttributeElement in results:
        elementinfo = results[RequestTypeAttributeElement]
        if elementinfo == None:
          raise InvalidSerialization()
        target = targetlookup(elementinfo)
        if target == None:
          print(elementinfo)
          for window in sublime.windows():
            print("w:{}".format(window.id()))
            for view in window.views():
              print("v:{}".format(view.id()))
          raise InvalidSerialization()
        if RequestTypeAttributeAttribute not in results:
          return
        attribute = results[RequestTypeAttributeAttribute]
        if attribute == None:
          raise InvalidSerialization()
        if attribute == AttributeRole:
          self.respond(ValueTypeString, target.role())
          return
        if attribute == AttributeSubrole:
          self.respond(ValueTypeString, target.subrole())
          return
        if attribute == AttributeValue:
          self.respond(ValueTypeString, target.value())
          return
        if attribute == AttributeTitle:
          self.respond(ValueTypeString, target.title())
          return
        if attribute == AttributeParent:
          self.respond(ValueTypeElement, target.parent())
          return
        if attribute == AttributeChildren:
          self.respond(ValueTypeElementArray, target.children())
          return
        if attribute == AttributeWindows:
          self.respond(ValueTypeElementArray, target.windows())
          return
        if attribute == AttributeFocusedWindow:
          self.respond(ValueTypeElement, target.focusedWindow())
          return
        if attribute == AttributeURL:
          self.respond(ValueTypeString, target.url())
          return
        if attribute == AttributeSelectedTextRange:
          self.respond(ValueTypeIntegerArrayArray, target.selectedTextRange())
          return
        if attribute == AttributeSyntax:
          self.respond(ValueTypeString, target.syntax())
          return
        if attribute == AttributeTabSize:
          self.respond(ValueTypeInteger, target.tabsize())
          return
        if attribute == AttributeIndentation:
          self.respond(ValueTypeInteger, target.indentation())
          return
        if attribute == AttributeFocused:
          self.respond(ValueTypeBool, target.focused())
          return
      if RequestTypeParameterizedAttributeElement in results:
        # TODO: Consolidate this and above target lookup and attribute logic
        elementinfo = results[RequestTypeParameterizedAttributeElement]
        if elementinfo == None:
          raise InvalidSerialization()
        target = targetlookup(elementinfo)
        if target == None:
          raise InvalidSerialization()
        if RequestTypeParameterizedAttributeAttribute not in results:
          return
        attribute = results[RequestTypeParameterizedAttributeAttribute]
        if attribute == None:
          raise InvalidSerialization()
        if RequestTypeParameterizedAttributeParameter not in results:
          return
        parameter = results[RequestTypeParameterizedAttributeParameter]
        if parameter == None:
          return
        if attribute == ParameterizedAttributeStringForRange:
          self.respond(ValueTypeString, target.string(parameter))
          return
        if attribute == ParameterizedAttributeRangeForPosition:
          self.respond(ValueTypeIntegerArray, target.linerange(parameter))
          return
      raise UnsupportedAttribute()

class server(socketserver.UnixStreamServer):
  def __init__(self, socketname):
    try:
      os.remove("/tmp/" + socketname)
    except OSError:
      pass
    self.address_family = socket.AF_UNIX
    socketserver.UnixStreamServer.__init__(self, '/tmp/' + socketname, requesthandler, True)
  def shutdown(self):
    print("shutdown")
    socketserver.UnixStreamServer.shutdown(self)

class client():
  def __init__(self, socketname):
    self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    self.socketname = socketname
    self.connected = False
  def connect(self):
    try:
      self.connected = self.socket.connect_ex('/tmp/' + self.socketname) == 0
    except:
      self.connected = False
  def close(self):
    self.socket.close()
    self.connected = False
  def __enter__(self):
    self.connect()
    return self
  def __exit__(self, exc_type, exc_value, traceback):
    self.close()
  def send(self, data):
    if self.connected == True:
      write(self.socket, data)
      return read(self.socket)
    return None

class listener(sublime_plugin.EventListener):
  def selectionnotify(self, identifier, name, value):
    notifier = client("sublimetextnotifications")
    with notifier as c:
      data = bytes(json.dumps({0:ValueTypeIntegerArrayArray,1:value,2:name,3:{viewkey:identifier}}, sort_keys=True), 'UTF-8')
      c.send(data)
  def destroyednotify(self, identifier, name):
    notifier = client("sublimetextnotifications")
    with notifier as c:
      data = bytes(json.dumps({0:ValueTypeNone,1:None,2:name,3:{viewkey:identifier}}, sort_keys=True), 'UTF-8')
      c.send(data)
  def on_selection_modified(self, view):
    selection = []
    for region in view.sel():
      selection.append([region.a, region.b])
    self.selectionnotify(view.id(), NotificationSelectedTextChanged, selection)
  def on_close(self, view):
    self.destroyednotify(view.id(), NotificationUIElementDestroyed)

class connect(object):
  def __init__(self):
    self.server = server('sublimetext')
    self.server_thread = threading.Thread(target=self.server.serve_forever)
    self.server_thread.daemon = True
    self.server_thread.start()

def plugin_loaded():
  print("loaded accessibility server")
  global accessibility
  accessibility = connect()

def plugin_unloaded():
  print("unloaded accessibility server")
  global accessibility
  accessibility.server.shutdown()
  accessibility = None
