#########################################################################
# This file is part of Lyntin.
#
# Lyntin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Lyntin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# copyright (c) Free Software Foundation 2001-2007
#
# $Id: utils.py,v 1.16 2007/11/09 18:40:54 willhelm Exp $
#########################################################################
"""
This has a series of utility functions that aren't related to classes 
in the application, but are useful in a variety of places.  They're 
not dependent on application things, so it's easier to test them.
"""
import string, re, time, types, os
from lyntin import ansi, constants, exported

# for finding non-escaped semi-colons in user input
SPLIT = ";"
SPLIT_REGEXP = re.compile(r'(?<!\\);')

TIMESPAN_REGEXP = re.compile(r"^(?P<days>\d+d)?(?P<hours>\d+h)?(?P<minutes>\d+m)?(?P<seconds>\d+s?)?$")
TIME_REGEXP1=re.compile(r"^(?P<hour>[1-9]|1[0-2])(?P<ampm>a|p)$")
TIME_REGEXP2=re.compile(r"^(?P<hour>[1-9]|1[0-2]):(?P<minute>[0-5][0-9])(:(?P<second>[0-5]\d))?(?P<ampm>a|p)?$")
TIME_REGEXP3=re.compile(r"^(?P<hour>0|1[3-9]|2[0-3]):(?P<minute>[0-5][0-9])(:(?P<second>[0-5]\d))?$")

# for finding %... variables
PVAR_REGEXP = re.compile(r'%+(-?(\d+):?-?(\d*)|:-?(\d+))')


import sys

class PriorityQueue:
  """
  This is a pretty basic priority queue.
  """
  def __init__(self):
    # holds the maps of priorities to items
    self._prioritymap = {}

    # the ordered list of items
    self._orderedlist = []

    # whether or not our orderedlist is dirty
    self._dirty = 0

  def __generateList(self):
    """
    Goes through the prioritymap and generates an ordered list. This
    saves cycles since it puts the ordering of the list up front
    rather than when the ordered list is retrieved.
    """
    # Convert dict_keys to a list and sort it
    priorities = sorted(self._prioritymap.keys())

    self._orderedlist = []

    # Iterate over the sorted priorities and add corresponding members to the ordered list
    for priority in priorities:
      for mem in self._prioritymap[priority]:
        self._orderedlist.append(mem)

    self._dirty = 0

  def add(self, func, priority=constants.LAST):
    """
    Adds a function to the prioritymap and marks the PriorityQueue as
    "dirty" which means it needs to regenerate the ordered list before
    it hands it out.

    @param func: the function to call when the hook is spammed
    @type  func: function

    @param priority: the function will get this place in the call
        order.  functions with the same priority specified will get
        arbitrary ordering.  defaults to onstants.LAST.
    @type  priority: int
    """
    if not callable(func):
      exported.write_error("Function %s not callable." % repr(func))
      return

    if priority in self._prioritymap:
      self._prioritymap[priority].append(func)
    else:
      self._prioritymap[priority] = [func]

    self._dirty = 1

  def remove(self, func):
    """
    Removes a func from the priority map.

    @param func: the function to unregister
    @type  func: function
    """
    for priority, funcs in list(self._prioritymap.items()):
      if func in funcs:
        funcs.remove(func)

        if not funcs:
          del self._prioritymap[priority]

        break

    self._dirty = 1
  
  def getList(self):
    """
    Retrieves the list.  It might regenerate it if the prioritymap
    has been adjusted since the last time we regenerated the list.
    """
    if self._dirty == 1:
      self.__generateList()
    return self._orderedlist

  def count(self):
    """
    Returns how many functions are in the list.

    @returns: the number of functions registered
    @rtype: int
    """
    return len(self.getList())

def filter_cm(text):
  """
  Filters out ^M.  Useful for logging.

  @returns: text without ^M stuff
  @rtype: string
  """
  return text.replace("\r", "")


CHOMP_EOL = re.compile("[\r\n]+$")

def chomp(text):
  """
  Removes all CR and LF from the end of the input string.

  @param text: the text to chomp
  @type  text: string

  @returns: the text without CR or LF at the end.
  @rtype: string
  """
  global CHOMP_EOL
  return CHOMP_EOL.sub('', text)

def fixdir(d):
  """
  Takes in a directory (datadir, moduledir, ...) and fixes it (by
  adding an os.sep to the end) as well as verifies that it exists.

  If it does not exist, then it returns a None.  If it does exist,
  then it returns the adjusted directory name.

  @param d: the directory in question
  @type  d: string

  @returns: None or the fixed directory
  @rtype: string
  """
  if not os.path.exists(d):
    return None

  if len(d) > 0 and d[-1] != os.sep:
    d = d + os.sep

  return d


def http_get(url):
  """
  Retrieves the data at a given url and returns it as a big string.

  @param url: the url of the resource to retrieve
  @type  url: string

  @return: the resource at the given url
  @rtype: string

  @raises ValueError: if the url is not valid or if the resource doesn't exist
  """
  import http.client
  from urllib.parse import urlparse

  parsed_url = urlparse(url)

  if parsed_url.scheme != "http":
    raise ValueError("This is not a valid HTTP URL.")

  host = parsed_url.scheme.path
  resource = parsed_url.path
  if not resource:
    resource = "/"

  #Establish connection
  conn = http.client.HTTPConnection(host)

  #send the GET request
  conn.request("GET", resource)

  #get the response
  response = conn.getresponse()
  status = response.status
  reason = response.reason
  data = response.read().decode()

  conn.close()

  #raise an error if the status code is not 200 (OK)
  if status != 200:
    raise ValueError(f"HTTP error: {status} {reason}")

  return data

# handles regular expression syntax and flags
REG_REGEXP = re.compile("^(r\[.*?)(\$?\][Ii]*)$")

# for finding variables in the subject
SUBVAR_REGEXP = re.compile("%_?[0-9]+")

def compile_regexp(text, anchors=0, stars=0):
  """
  Takes in a string and compiles it into a regular expression.  This
  is for commands that take in strings that can be compiled either
  as a full-fledged regular expression (using Perl5 regexp syntax) or
  strings that are not regular expressions and use * as a wildcard
  character.

  @param text: the string to convert
  @type  text: string

  @param anchors: whether (1) or not (0) to deal with anchors
      in the case of a string that's not a regular expression.
      anchors are ^ and $ at the beginning and end of a string.
  @type  anchors: boolean

  @param stars: whether (1) or not (0) to deal with * wildcards
      which can match whatever
  @type  stars: boolean

  @return: the resulting regular expression
  @rtype: Re
  """
  if not text:
    return re.compile("")

  flags_bitmask = 0
  pieces = []

  if REG_REGEXP.match(text) is not None:
    # this is something we should compile as a regular expression
    # without doing any finagling

    end_index = text.rfind("]")
    # handle flags issues
    flags = text[end_index+1:]
    if flags == 'i' or flags == 'I':
      flags_bitmask = re.IGNORECASE

    # handle adjusting the string
    text = text[2:end_index]

    i = 0
    match = SUBVAR_REGEXP.search(text)
    while match:
      b, e = match.span()
      pieces.append(text[i:b])
      if text[b:e].find("_") != -1:
        pieces.append("(\S+?)")
      else:
        pieces.append("(.+?)")
      i = e
      match = SUBVAR_REGEXP.search(text, i)

    pieces.append(text[i:])

  else:
    if anchors == 1:
      anchor_begin = 0
      anchor_end = 0
      if text.startswith("^"):
        anchor_begin = 1
        text = text[1:]

      if text.endswith("$"):
        anchor_end = 1
        text = text[:-1]

    if stars == 1:
      star_begin = 0
      star_end = 0
      if text.startswith("*"):
        star_begin = 1
        text = text[1:]
      if text.endswith("*"):
        star_end = 1
        text = text[:-1]

    i = 0
    match = SUBVAR_REGEXP.search(text)
    while match:
      b, e = match.span()
      pieces.append(re.escape(text[i:b]))
      if text[b:e].find("_") != -1:
        pieces.append("(\S+?)")
      else:
        pieces.append("(.+?)")

      i = e
      match = SUBVAR_REGEXP.search(text, i)

    pieces.append(re.escape(text[i:]))
    if anchors == 1:
      if anchor_begin:
        pieces.insert(0, "^")
      if anchor_end:
        pieces.append("$")

    if stars == 1:
      if star_begin:
        pieces.insert(0, "^.*")
      if star_end:
        pieces.append(".*$")

  return re.compile("".join(pieces), flags_bitmask)


def expand_text(filter, fulllist):
  """
  Returns a subset of the list that matches the given string.

  Takes a list and a string and returns a list of items in the 
  original list that match the given string.  Handles * and 
  anchors too.

  @param filter: the string to match
  @type  filter: string

  @param fulllist: the list of strings to match in
  @type  fulllist: list of strings

  @returns: the matching strings from the full list
  @rtype: list of strings
  """
  ret = []

  # if they didn't have wildcards....
  if not "*" in filter:
    for mem in fulllist:
      if mem == filter:
        ret.append(mem)

  # if they had wildcards....
  else:
    filter = re.escape(filter)

    # escaping the string will replace * with \* so we unreplace
    # it with .*
    # FIXME - this isn't quite right--we need to account for escaped
    # * stuff
    regex = re.compile("^" + filter.replace("\\*", ".*") + "$")

    for mem in fulllist:
      if regex.match(mem):
        ret.append(mem)

  return ret

def __change_command_split(newsplit):
  global SPLIT, SPLIT_REGEXP

  if not newsplit:
    SPLIT_REGEXP = None
  else:
    SPLIT_REGEXP = re.compile(r'(?<!\\)' + re.escape(newsplit))

  SPLIT = newsplit


def split_commands(splitchar, text):
  """
  This method takes in text and parses it into separate commands
  on the SPLIT.  It accounts for \\SPLIT as well as SPLIT in { } which 
  indicate that we shouldn't be splitting there.

  SPLIT is defined in SPLIT_REGEXP.

  If SPLIT_REGEXP is empty string or None, then this doesn't split the
  command.

  @param text: the text to split
  @type  text: string

  @return: the split text
  @rtype: list of strings
  """
  global SPLIT, SPLIT_REGEXP

  if splitchar != SPLIT:
    __change_command_split(splitchar)

  if not SPLIT_REGEXP:
    return [text]

  marker = 0
  ret = []

  matchob = SPLIT_REGEXP.search(text)
  while (matchob):
    (b, e) = matchob.span()
    # we count braces--this is a bit interesting since if the entire 
    # segment we're looking at doesn't have a full set of matched 
    # braces, we ignore this semi-colon as a split point.
    left = 0
    right = 0
    for i in range(marker, b):
      if text[i] == '{' and (i == 0 or text[i-1] != "\\"):
        left += 1
      if text[i] == '}' and (i == 0 or text[i-1] != "\\"):
        right += 1 

    count = left - right

    if count == 0:
      ret.append(text[marker:b])
      marker = e

    matchob = SPLIT_REGEXP.search(text, e)

  ret.append(text[marker:])
  return ret


def strip_braces(text):
  """
  Returns text after stripping the braces around the text.
  If the incoming text has a { at the beginning and a } at the
  end, it removes the braces.

  @param text: the string to remove braces from
  @type  text: string

  @return: the text with braces (if matched) removed
  @rtype: string
  """
  text = text.strip()
  if len(text) < 1:
    return text

  if text.startswith("{") and text.endswith("}"):
    return text[1:-1]
  return text



def parse_args(args):
  """
  Parses command-line arguments into a list of (arg, value) pairs.

  @param args: The list of command-line arguments.
  @type  args: list[str]

  @return: List of tuples (option, value).
  @rtype: list[tuple[str, str or None]]
  """
  i = 0
  optlist = []

  while i < len(args):
    arg = args[i]

    if arg.startswith("-"):  # It's an option
      if i + 1 < len(args) and not args[i + 1].startswith("-"):  # Has a value
        optlist.append((arg, args[i + 1]))
        i += 1  # Skip the next item (used as value)
      else:  # Standalone flag
        optlist.append((arg, None))
    else:  # Positional argument
      optlist.append(("", arg))

    i += 1

  return optlist


def _find_next_break(token, marker, wrapcount, wraplength):
  """
  Figures out where the next break should be while word-wrapping.

  @param token: the token we're working on
  @type  token: string

  @param marker: the point at which to start looking--the break is
      after this marker in the token
  @type  marker: int

  @param wrapcount: 

  @param wraplength: the line length to wrap at or under
  @type  wraplength: int

  @returns: index of where to wrap at or -1 if we don't need
      to wrap on this token
  @rtype: int
  """
  # first we check to see to see if we need to find a break
  if len(token) <= marker - wrapcount + wraplength:
    return -1

  # first we look at carriage returns--they're fun and yummy!
  x = token.rfind("\n", marker, marker - wrapcount + wraplength)
  if x != -1:
    return x

  # ok--no carriage returns.  so we try going out wraplength and working
  # to the left for spaces.
  x = token.rfind(" ", marker, marker - wrapcount + wraplength)
  if x != -1:
    return x

  return marker - wrapcount + wraplength


def wrap_text(textlist, wraplength=50, indent=0, firstline=0):
  """
  It takes a block of text and wraps it nicely.  It accounts for
  indenting lines of text, wraplengths, and wrapping around ANSI 
  colors.

  We break on carriage returns (those are easy) and if no carriage
  returns are available we break on spaces.

  If the actual line is longer than the wraplength, then we'll break
  in the line at the wraplength--this will cut words in two.

  Note: we don't expand tabs or backspaces.  Both count as one
  character.

  @param textlist: either a string of text needing to be formatted and
      wrapped, or a textlist needing to be formatted and wrapped.
  @type  textlist: string or list of strings

  @param wraplength: the maximum length any line can be.  we'll wrap
      at an index equal to or less than this length.
  @type  wraplength: int

  @param indent: how many spaces to indent every line.
  @type  indent: int

  @param firstline: 0 if we shouldn't indent the first line, 1 if we 
      should
  @type  firstline: boolean

  @returns: the wrapped text string
  @rtype: string
  """
  wrapcount = 0           # how much we've got on the line so far
  linecount = 0           # which line we're on

  if wraplength > 2:
    wraplength = wraplength - 2

  # split the formatting from the text
  if isinstance(textlist, str):
    textlist = ansi.split_ansi_from_text(textlist)

  for i in range(0, len(textlist)):
    # if this is a color token, we gloss over it
    if ansi.is_color_token(textlist[i]):
      continue

    # if this is a text token, then we need to factor it into the word
    # wrapping
    marker = 0

    # this handles the offset for not indenting the first line (if that's
    # the sort of thing we're into).
    if firstline:
      x = _find_next_break(textlist[i], marker, wrapcount, wraplength - indent)
    else:
      x = _find_next_break(textlist[i], marker, wrapcount, wraplength)

    # go through finding breaks and sticking in carriage returns and indents
    # and things for this text token
    while x != -1:
      # insert the carriage return, any indent, and lstrip the line as well
      # print "'" + textlist[i] + "'", len(textlist[i]), x
      if textlist[i][x] == "\n":
        if indent:
          textlist[i] = (textlist[i][:x+1] + (indent * ' ') + textlist[i][x+1:].lstrip())
        else:
          textlist[i] = (textlist[i][:x+1] + textlist[i][x+1:])
      else:
        if indent:
          textlist[i] = (textlist[i][:x+1] + '\n' + (indent * ' ') + textlist[i][x+1:].lstrip())
        else:
          textlist[i] = (textlist[i][:x+1] + '\n' + textlist[i][x+1:])

      marker = x + indent + 2
      wrapcount = 0

      x = _find_next_break(textlist[i], marker, wrapcount, wraplength - indent)

    wrapcount = len(textlist[i]) - marker + wrapcount


  # this next line joins the list with no separator
  if firstline:
    return (indent * " ") + ''.join(textlist)
  else:
    return ''.join(textlist)


def build_graph(numbers_dict):
  """
  Takes in a dict of keys to values and prints out a graph accordingly.
  """
  if not numbers_dict:
    return "No data available."

  values = list(numbers_dict.values())
  values.sort()

  min_value = values[0]
  max_value = values[-1]

  if min_value == max_value:
    return "All values are %d." % min_value

  if max_value < 60:
    divisor = 1
  elif max_value < 140:
    divisor = 2
  else:
    divisor = max_value // 60

  keys = sorted(list(numbers_dict.keys()), key=len)
  max_key_length = len(keys[-1])

  graph = []
  graph.append(f"  {' '.ljust(max_key_length + 8)}0{'-' * (max_value // divisor)}{max_value}")

  keys.sort()
  for k in keys:
    v = numbers_dict[k]
    graph.append(f"  {k.ljust(max_key_length + 1)}- {str(v).ljust(5)}|{'=' * (v // divisor)}")

  return "\n".join(graph)

def columnize(textlist, screenwidth=72, indent=0):
    """
    Takes a list of data and converts it into a series of columns and rows
    that are evenly spaced and pretty.

    @param textlist: the list of strings to columnize
    @type  textlist: list of strings

    @param screenwidth: the maximum width to wrap against
    @type  screenwidth: int

    @param indent: the amount of spaces to indent each line
    @type  indent: int

    @return: the final formatted columnized string
    @rtype: string
    """
    if screenwidth > 2 + indent:
      screenwidth = screenwidth - 2 - indent

    SPACING = 4
    maxwidth = max(len(mem) for mem in textlist) if textlist else 0  # Handle empty list

    # Calculate the number of columns and rows
    numcols = max(1, (screenwidth + SPACING) // (maxwidth + SPACING))
    numrows = (len(textlist) + numcols - 1) // numcols

    # Create rows as a list of empty lists
    rows = [[] for _ in range(numrows)]

    # Distribute items into rows
    idx = 0
    for mem in textlist:
      rows[idx].append(mem.ljust(maxwidth))
      idx = (idx + 1) % numrows

    # Join rows into strings
    rows = ["".join(row).rstrip() for row in rows]

    # Join all rows with newline and indentation
    return (indent * " ") + ("\n" + (indent * " ")).join(rows)


def parse_timespan(timespan):
  """
  Parses a timsspan into a number of seconds.  

  @param timespan: the timespan string to parse
  @type  timespan: string

  @returns: the number of seconds in the timespan
  @rtype: int

  @raises ValueError: if the timespan is unparseable
  """
  match=TIMESPAN_REGEXP.match(timespan)

  if not match:
    raise ValueError("Invalid timespan string.")
    
  timespec = match.groupdict()

  if not timespec["days"] and not timespec["hours"] and not timespec["minutes"] and not timespec["seconds"]:
    raise ValueError("Invalid timespan string.")

  days = timespec["days"]
  if not days:
    days="0"
  elif days.endswith("d"):
    days=days[:-1]
  days=int(days)

  hours = timespec["hours"]
  if not hours:
    hours="0"
  elif hours.endswith("h"):
    hours=hours[:-1]
  hours=int(hours)

  minutes = timespec["minutes"]
  if not minutes:
    minutes="0"
  elif minutes.endswith("m"):
    minutes=minutes[:-1]
  minutes=int(minutes)
    
  seconds = timespec["seconds"]
  if not seconds:
    seconds="0"
  elif seconds.endswith("s"):
    seconds=seconds[:-1]
  seconds=int(seconds)
      
  return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds


def parse_time(timearg):
  """
  Parses a time into the number of seconds since the epoch.
 
  First attempts to parse as a time of day, and if that fails attempts
  to parse as a timespan.  Timespans are interpretted as times from
  time.time() (now). 

  @param timearg: the time string to parse
  @type  timearg: string

  @return: the number of seconds
  @rtype: int

  @raises ValueError: if the time string was unparseable
  """
  match = TIME_REGEXP1.match(timearg) or TIME_REGEXP2.match(timearg) or TIME_REGEXP3.match(timearg)

  if not match:
    try:
      timespan = parse_timespan(timearg)
      return time.time() + timespan
    except:
      raise ValueError("Invalid time string.")

  timespec = match.groupdict()
  currenttime = time.localtime()

  # print timespec

  hour=int(timespec.get("hour",None))
  ampm=timespec.get("ampm",None)
  if hour > 12:
    if ampm:
      raise ValueError("Invalid time string: ampm specified with hours > 12.")
    else:
      ampm="p"
  else:
    if ampm == "p":
      hour = hour + 12

  if hour < 1 or hour > 24:
    raise ValueError("Invalid time string: hours are out of range.")

  minute = timespec.get("minute",None)
  if minute is None:
    minute = 0
  else:
    minute = int(minute)
  
  second = timespec.get("second",None)
  if second is None:
    second = 0
  else:
    second = int(second)

  timetuple = (currenttime[0],currenttime[1],currenttime[2],hour,minute,second,currenttime[6],currenttime[7],currenttime[8])
  if ampm:
    increment=24
  else:
    increment=12
    
  while timetuple < currenttime:
    timetuple = timetuple[:3] + (timetuple[3] + increment,) + timetuple[4:]
  
  try:
    return time.mktime(timetuple)
  except Exception as e:
    raise ValueError("Invalid time string: %s" % e)


def convert_boolean(text):
  """
  Returns 1 if true, 0 if false, or -1 if it's not a boolean.

  @param text: the text to convert to a boolean value
  @type  text: string

  @returns: 1 if true, 0 if false, -1 if not a boolean
  @rtype: int
  """
  if text in constants.TRUE_VALUES:
    return 1
  elif text in constants.FALSE_VALUES:
    return 0
  else:
    return -1

def escape(s):
  r"""
  Takes in a string and escapes all \ (single backslash) by one level 
  (turning it into a double backslash) and all $ (single dollar sign) 
  by one level (turning it into a single backslash and a single dollar
  sign).

  @param s: the string to escape
  @type s: string

  @returns: the escaped string
  @rtype: string
  """
  s = s.replace("\\", "\\\\")

  # FIXME - this is a bit of a hack and won't work if people do
  # funky regexps that involve $ in the middle somewhere.  we
  # should probably build a regexp to do the substitution with
  # which handles the various situations.  or something along
  # those lines.
  rr = REG_REGEXP.match(s)
  if rr:
    s = rr.group(1).replace("$", "\\$") + rr.group(2)
  elif s.endswith("$"):
    s = s[:-1].replace("$", "\\$") + "$"
  else:
    s = s.replace("$", "\\$")

  return s


# --------------------------------------
# variable expansion functions
# --------------------------------------

def expand_vars(text, varmap):
  """
  Note: If you have a text string and you want the variable manager 
  to expand variables in that string according to session variables,
  use 'exported.expand_ses_vars' instead.

  The following functions are used in the command processing pipeline
  at different points:

    1. expand_vars - This expands variables in a function arbitrarily
       according to the desired expansion policy.  It should be safe to
       recusively evaluate this string and not have strings re-expanded.

    2. denest_vars - This finishes expansion of a string and should be
       called after all expansions are done.


  Note that the variablemanager's "expand" function is used for
  general expansion of text when there won't be a recursion on the
  partially expanded (but not yet denested) text.  It consists of
  chaining expand_vars and denest_vars together.

  Looks at user input and expands any variables involved using the Lyntin 
  variable expansion methodology.

  Lyntin variable expansion works by replacing all instances of $blah 
  with the appropriate variable.  Then at a later point, variables 
  preceded by multiple $ are denested one scope and lose a $.

  It returns the (un)adjusted text.

  @param text: the text to expand variables in
  @type  text: string

  @param varmap: the varname to expansion mapping
  @type  varmap: dict

  @return: the text with all variables expanded
  @rtype: string
  """
  if not ("%" in text or "$" in text) or len(text) == 0:
    return text

  varmapkeys = list(varmap.keys())
  # We want to sort them in order of longest first
  varmapkeys.sort(key=len, reverse=True)
  i = 0

  # we go through the text expanding things one at a time.
  while (i < len(text)):
    mem = text[i]
    if i != 0:
      memm1 = text[i-1]
    else:
      memm1 = None

    if (mem == "%" or mem == "$") and memm1 != "\\":
      j = i
      ccount = 0

      # count the $/% thingies first
      while j < len(text) and text[j] == mem:
        ccount += 1
        j += 1
 
      if ccount == 1 and j < len(text):
        closure = -1

        # if we're looking at a variable in the form of ${blah} then
        # we have this wonderful set of closures to play with.
        if text[j] == "{":
          closure = text.find("}", j)
          # if we didn't find a closure, then we set it to the end of
          # the text.
          if closure == -1:
            closure = len(text)-1

          # we found a { and a }, so the textfragment exists between
          # them.
          textfragment = text[j+1:closure]

          if textfragment in varmapkeys:
            repl = str(varmap[textfragment])
            text = text[:i] + repl + text[closure+1:]
            break

        else:
          textfragment = text[j:]

          for mem in varmapkeys:
            if textfragment.find(mem) == 0:
              repl = str(varmap[mem])
              text = text[:i] + repl + text[i+len(mem)+ccount:]
              break
      else:
        i += ccount

    i += 1

  #exported.write_message("utils.lyntin_expand_vars output: %s" % (text,))
  return text


# --------------------------------------
# denesting variables
# --------------------------------------

def denest_vars(text, varmap):
  """
  Replaces all the nested variables with appropriate variables.

  @param text: the string to expand variables in
  @type  text: string

  @param varmap: the varname to expansion mapping (here only in case
      a mode needs it in the future, and for consistency with the other
      var expansion commands.)
  @type varmap: dict

  @return: the text with all variables expanded
  @rtype: string
  """
  return _denest_vars_worker("$", text)


def _denest_vars_worker(varchar, text):
  """
  Handles the actual denesting for different variable types
  depending on the varchar passed in.
  """
  varchar2 = "%s%s" % (varchar, varchar)
  index = text.find(varchar2)

  while (index != -1):
    if (index == 0 or text[index] != "\\") and \
        (index == len(text)-2 or text[index+2] != varchar):
      text = text[:index] + text[index+1:]
    
    index = text.find(varchar2, index+1) 

  return text

# --------------------------------------
# placmeent variable expansion functions
# --------------------------------------

def _get_variable_value(inputsplit, var):
  """
  Takes a list and a var and figures out what the placement var
  is based on the inputsplit list.

  @param inputsplit: the input string list
  @type  inputsplit: list of strings

  @param var: the variable to retrieve
  @type  var: string

  @return: the variable expansion
  @rtype: string
  """
  # handles the 0 case
  if var == "0":
    start = 1
    end = len(inputsplit)

  # handles non splits
  elif var.find(':') == -1:
    start = int(var)
    if start == -1:
      end = len(inputsplit)
    else:
      end = start + 1

  # handles splits
  else:
    startmem,endmem = var.split(':')
    if startmem:
      start = int(startmem)
    else:
      start = 0
    if endmem:
      end = int(endmem)
    else:
      end = max(len(inputsplit),start)

  return ' '.join(inputsplit[start:end])


def _strip_placement_vars(text):
  """
  Returns a list of all the variables in a string.

  @param text: the text to strip placement vars from
  @type  text: string

  @return: list of replacement var strings
  @rtype: list of strings
  """
  global PVAR_REGEXP

  ret = []
  match = PVAR_REGEXP.search(text)
  while match:
    (b,e) = match.span()
    val = match.groups()[0]
    if val not in ret:
      ret.append(val)
    match = PVAR_REGEXP.search(text, e)
  return ret


def expand_placement_vars(input, expansion):
  """
  Takes an user input line and an alias expansion and hands it
  off to the appropriate function for evaluating the placement
  variable replacement.

  Takes an input and an expansion and replaces expansion
  variables with the components from the input.

  Returns the finalized string.

  @param input: the user's input
  @type  input: string

  @param expansion: the expansion of the alias in the input
  @type  expansion: string

  @return: the expansion with all nested_vars replaced and placement
      vars replaced
  @rtype: string
  """
  vars = _strip_placement_vars(expansion)

  if len(vars) > 0:
    varlookup = {}
    inputsplit = input.split(' ')

    # for all the variables, find what it translates to
    for mem in vars:
      if mem.find(':') < 0:
        start = int(mem)
        if start == -1:
          end = len(inputsplit)
        else:
          end = start + 1
      else:
        startmem,endmem = mem.split(':')
        if startmem:
          start = int(startmem)
        else:
          start = 0
        if endmem:
          end = int(endmem)
        else:
          end = max(len(inputsplit),start)

      varlookup[mem] = ' '.join(inputsplit[start:end])

    # run through the replacements
    vars = list(varlookup.keys())
    vars.sort(key=len, reverse=True)
    
    for mem in vars:
      expansion = re.sub("(?<!%)%" + mem, varlookup[mem], expansion)

  else:
    if input.find(' ') > -1:
      expansion = expansion + ' ' + input.split(' ', 1)[1]

  expansion = _denest_vars_worker("%", expansion)

  return expansion


# Local variables:
# mode:python
# py-indent-offset:2
# tab-width:2
# End:
