#########################################################################
""" _gen_def_lex_info.py -- Generates Python file containing default
settings for builtin scintilla lexers from a SciTE properties file.

Copyright (c) 1999-2000, Archaeopteryx Software, Inc.  All rights reserved.

Written by Stephan R.A. Deibel (sdeibel@archaeopteryx.com) and
John Ehresman (jpe@archaeopteryx.com)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""

import sys
import string
import types
import os

kConstantsModule = 'sci_names'

def ReadProperties(filename):
  """ Reads contents of properties file into a list of lines, eliminating blank
  lines & comments and joining continuation lines. """

  f = open(filename)
  line_list = []
  last_line = ' '
  for line in f.readlines():

    # Trim newline
    if len(line) != 0 and line[-1] == '\n':
        line = line[:-1]

    # Handle import
    if len(line) > len('import ') and line[0:len('import ')] == 'import ':
      importfile = string.strip(line[len('import'):])
      importdir, ignore = os.path.split(filename)
      importfp = os.path.join(importdir, importfile + '.properties')
      imported_lines = ReadProperties(importfp)
      for line in imported_lines:
        line_list.append(line)

    # Add regular non-comment line
    if len(line) != 0 and line[0] != '#':
      if last_line[-1] == '\\':
        line_list[-1] = last_line[:-1] + line
      else:
        line_list.append(line)
      last_line = line_list[-1]
      
  # Done
  f.close()
  return line_list

def LinesToDict(line_list):
  """ Convert line list into a dict of key / value, one entry per line """

  dict = {}
  for line in line_list:
    if len(string.strip(line)) > 0 and string.find(line, '=') > 0:
      eqpos = string.find(line, '=')
      key = string.strip(line[:eqpos])
      value = string.strip(line[eqpos+1:])
      if len(key) != 0 and len(value) != 0:
        dict[key] = value

  return dict

def ResolveKeyMatches(str, dict):
  """ Resolve the $() delimiter in given string by looking up values
  in the given dict.  Returns a list of all matches, since the $() may
  refer to a list of things """

  wildsplit = string.split(str, '$(')
  if len(wildsplit) == 1:
    return (str,)
  assert(len(wildsplit) == 2)

  closepos = string.find(wildsplit[1], ')')
  if closepos == -1:
    return (str,)

  start = wildsplit[0]
  wildcard = wildsplit[1][:closepos]
  end = wildsplit[1][closepos+1:]

  if dict.has_key(wildcard):
    valuelist = dict[wildcard]
    values = string.split(valuelist, ';')
    retval = []
    for value in values:
      match = start + value + end
      retval.append(match)
    return retval

  else:
    return ("",)

def ResolveKeyReferences(dict):
  """ Convert all $() delimited references in keys in the given dict into
  plain ascii values; returns a dict with all-expanded keys """

  new_dict = {}
  for key in dict.keys():
    matches = ResolveKeyMatches(key, dict)
    for match in matches:
      new_dict[match] = dict[key]
  return new_dict

def FilterLines(line_list, prefix, file_dict):
  """ Returns dictionary composed from lines beginning with given prefix.  Keys
  will be everything between the end of the prefix and the 1st = sign. """
  
  dict = {}
  for line in line_list:
    if string.find(line, prefix) == 0:
      split_list = string.split(line, '=')
      key = split_list[0]
      value = string.strip(string.join(split_list[1:], '='))
      key = string.strip(key[len(prefix):])

      if len(key) != 0 and len(value) != 0:
        dict[ResolveReferences(key, file_dict)] = ResolveReferences(value, file_dict)
  return dict
  
def ResolveReferences(str, dict):
  """ Convert all $() delimited references in the string into the real
  value.  Any references not found will be treated as blank values. """

  wildsplit = string.split(str, '$(')
  if len(wildsplit) == 1:
    return str

  retval = wildsplit[0]
  for item in wildsplit[1:]:
    closepos = string.find(item, ')')
    assert(closepos) > 0

    wildcard = item[:closepos]
    end = item[closepos+1:]

    if dict.has_key(wildcard):
      retval = retval + dict[wildcard] + end
    else:
      retval = retval + end

  return ResolveReferences(retval, dict)

def FindKeywordClasses(line_list, dict):
  return FilterLines(line_list, 'keywordclass.', dict)

def GetStyleDict(str_value):
  """ Return dictionary of info from style property definition. """

  dict = {}
  part_list = map(string.strip, string.split(str_value, ','))
  for part in part_list:
    if string.find(part, ':') == -1:
      name = part
      value = 1
    else:
      name, value = map(string.strip, string.split(part, ':'))

    if name == 'size':
      value = string.atoi(value)
    dict[name] = value

  return dict

def MergeDicts(d1, d2, overwrite):
  """ Merges values fron d2 into d1.  Previous values will be overwritten
  iff overwrite is true. """

  for key, value in d2.items():
    if overwrite or not d1.has_key(key):
      d1[key] = value
      
def FindStyles(line_list, dict):
  """ Find style definitions in given line list.  Returns 2 dictionaries,
  the first for default styles, the second with entries for specific lexer. """

  ret_dict = {}
  line_dict = FilterLines(line_list, 'style.', dict)
  for key, value in line_dict.items():
    lexer, state = string.split(key, '.')
    if state != '*':
      state = string.atoi(state)

    lex_dict = ret_dict.get(lexer)
    if lex_dict == None:
      ret_dict[lexer] = lex_dict = {}

    style_dict = GetStyleDict(value)
    state_dict = lex_dict.get(state)
    if state_dict == None:
      lex_dict[state] = style_dict
    else:
      lex_dict[state].update(style_dict)
    
  # Set default values where not overridden
  def_dict = ret_dict.get('*')
  if def_dict != None:
    del ret_dict['*']
    for lex_id, lex_map in ret_dict.items():
      for def_state, def_value in def_dict.items():
        lex_state = lex_map.get(def_state)
        if lex_state == None:
          lex_state = {}
          lex_map[def_state] = lex_state
        MergeDicts(lex_state, def_value, 0)
        
  return ret_dict

class Name:
  def __init__(self, name):
    self.name = name

  def __repr__(self):
    return self.name
  
def FindDocumentTypes(name_dict):
  """ Transform language names to lexer ids. """

  id_for_name = {
    'cpp': Name(kConstantsModule + ".kCppDocument"),
    'java': Name(kConstantsModule + ".kJavaDocument"),
    'python': Name(kConstantsModule + ".kPythonDocument"),
    'makefile': Name(kConstantsModule + ".kMakefileDocument"),
    'batch': Name(kConstantsModule + ".kDosBatchDocument"),
    'vb': Name(kConstantsModule + ".kVBDocument"),
    'html': Name(kConstantsModule + ".kHTMLDocument"),
    'hypertext': Name(kConstantsModule + ".kHTMLDocument"),
    'props': Name(kConstantsModule + ".kPropsDocument" ),
    'errorlist': Name(kConstantsModule + ".kErrListDocument" ),
    'msidl': Name(kConstantsModule + ".kMSIDLDocument"),
    'sql': Name(kConstantsModule + ".kSQLDocument"),
    'xml': Name(kConstantsModule + ".kXMLDocument"),
    'latex': Name(kConstantsModule + ".kLatexDocument"),
    'lua': Name(kConstantsModule + ".kLuaDocument"),
    'idl': Name(kConstantsModule + ".kXPIDLDocument"),
    'xpidl': Name(kConstantsModule + ".kXPIDLDocument"),
    'javascript': Name(kConstantsModule + ".kJavascriptDocument"),
    'rc': Name(kConstantsModule + ".kRCDocument"),
    'plsql': Name(kConstantsModule + ".kPLSQLDocument"),
    'php': Name(kConstantsModule + ".kPHPDocument"),
    'perl': Name(kConstantsModule + ".kPerlDocument"),
    'diff': Name(kConstantsModule + ".kDiffDocument"),
  }

  id_dict = {}
  for name, value in name_dict.items():
    id = id_for_name.get(name)
    if id != None:
      id_dict[id] = value
    else:
      print 'Unknown language type:', name

  return id_dict

def EscapeQuote(s, q):
  """ Returns given string with all q characters and '\' escaped. """

  parts = string.split(s, '\\')
  if len(parts) != 1:
    s = string.join(parts, '\\\\')
  parts = string.split(s, q)
  if len(parts) != 1:
    s = string.join(parts, '\\' + q)
  return s

def PrettyPrintString(s, max_line_len, first_col, out):
  """ Prints a string with escape chars if necessary. """

  avail = max_line_len - first_col
  if avail < 15:
    avail = 15
  if len(s) < avail:
    out.write('"%s"' % EscapeQuote(s, '"'))
  else:
    i = 0
    out.write('(\n')
    first_col = first_col + 2
    out.write(first_col * ' ')
    while i < len(s):
      count = avail
      if count < len(s) - i:
        while count > 0 and s[i + count] != ' ':
          count = count - 1
        if count == 0:
          count = avail

      out.write('"%s"' % EscapeQuote(s[i:i + count], '"'))
      i = i + count
      if i < len(s):
        out.write('\n')
        out.write(first_col * ' ')
    out.write('\n' + (first_col - 2) * ' ' + ')')

def PrettyPrintDictValue(val_dict, start_col, out):

  if len(val_dict) == 0:
    out.write('{}')
  else:
    out.write('{\n')
    start_col = start_col + 2
    out.write(start_col * ' ')
    indent = start_col * ' '
    first = 1
    for id, value in val_dict.items():
      if not first:
        out.write(indent)
      first = 0
      if type(id) == types.StringType:
        out.write('"%s": ' % id)
      else:
        out.write('%s: ' % str(id))
      if type(value) == types.StringType:
        PrettyPrintString(value, 75, len(indent) + 2, out)
      elif type(value) == types.DictType:
        PrettyPrintDictValue(value, len(indent) + 2, out)
      elif type(value) == types.IntType:
        out.write(str(value))
      else:
        out.write(repr(value))
      out.write(',\n')
  
    out.write('\n' + (start_col - 2) * ' ' + '}')
  
def PrettyPrintDictInit(name, val_dict, indent, out):
  """ Write python code to bind name to a new dictionary with the given val_dict.
  The values of val_dict must be strings. """

  prefix = indent + name + ' = '
  out.write(prefix)
  PrettyPrintDictValue(val_dict, len(indent) + 2, out)
  out.write('\n')
   
  
def WriteKeywordDict(dict, out):
  """ Write python code to create default keyword dictionary. """

  PrettyPrintDictInit('kDefaultKeywords', dict, '', out)
  
def WriteStyleDicts(dict, out):
  """ Write python code to create default keyword dictionary. """

  kSclexForName = {
    'cpp': Name(kConstantsModule + '.SCLEX_CPP'),
    'java': Name(kConstantsModule + '.SCLEX_CPP'),
    'python': Name(kConstantsModule + '.SCLEX_PYTHON'),
    'makefile': Name(kConstantsModule + '.SCLEX_MAKEFILE'), 
    'batch': Name(kConstantsModule + '.SCLEX_BATCH'),
    'vb': Name(kConstantsModule + '.SCLEX_VB'),
    'html': Name(kConstantsModule + ".SCLEX_HTML"),
    'hypertext': Name(kConstantsModule + ".SCLEX_HTML"),
    'props': Name(kConstantsModule + '.SCLEX_PROPERTIES'),
    'errorlist': Name(kConstantsModule + '.SCLEX_ERRORLIST'),
    'msidl': Name(kConstantsModule + '.SCLEX_CPP'),
    'sql': Name(kConstantsModule + '.SCLEX_SQL'),
    'xml': Name(kConstantsModule + '.SCLEX_XML'),
    'latex': Name(kConstantsModule + '.SCLEX_LATEX'),
    'lua': Name(kConstantsModule + '.SCLEX_LUA'),
    'idl': Name(kConstantsModule + '.SCLEX_CPP'),
    'xpidl': Name(kConstantsModule + '.SCLEX_CPP'),
    'javascript': Name(kConstantsModule + '.SCLEX_CPP'),
    'rc': Name(kConstantsModule + '.SCLEX_CPP'),
    'plsql': Name(kConstantsModule + '.SCLEX_SQL'),
    'php': Name(kConstantsModule + '.SCLEX_CPP'),
    'perl': Name(kConstantsModule + '.SCLEX_PERL'),
    'diff': Name(kConstantsModule + '.SCLEX_DIFF'),
  }

  dict_of_dicts = {}
  for lex_name, value in dict.items():
    name = 'kDefault%sStyles' % lex_name
    PrettyPrintDictInit(name, value, '', out)
    if kSclexForName.has_key(lex_name):
      dict_of_dicts[kSclexForName[lex_name]] = Name(name)
    else:
      print 'Unknown lexer name:', lex_name

  PrettyPrintDictInit('kDefStylesForLexer', dict_of_dicts, '', out)
  
if __name__ == '__main__':
  if len(sys.argv) != 3:
    print 'Usage: %s <scite-properties-file> <ouput-python-file>' % sys.argv[0]
    sys.exit()
  in_filename = sys.argv[1]
  out_filename = sys.argv[2]

  line_list = ReadProperties(in_filename)
  line_dict = LinesToDict(line_list)
  resolved_dict = ResolveKeyReferences(line_dict)
  dict = FindKeywordClasses(line_list, resolved_dict)
  kw_dict = FindDocumentTypes(dict)
  st_dict = FindStyles(line_list, resolved_dict)

  out_file = open(out_filename, 'w')
  out_file.write("# Default setting for scintilla's builtin lexers\n")
  out_file.write("# Generated from %s\n" % in_filename)
  out_file.write("\n")
  out_file.write("import %s\n" % kConstantsModule)
  out_file.write("\n")

  WriteKeywordDict(kw_dict, out_file)
  WriteStyleDicts(st_dict, out_file)

