"""
This module holds a basic manual mapper.  It can be linked to actions
to provide a more automated mapping experience.  This mapper has problems
with areas that don't conform to a coordinate plane.  However, this
mapper also shows you the map of the area you've mapped out so far
with a red * where you are in-game.  That's pretty cool.  

It also works over telnet and maps are saveable into a mapfile.  It 
also supports standard exits (n, e, s, w, ne, nw, sw, se) and
allows you to enter notes for each room.

todo:
 - add the ability to set one's coordinates
 - fix the save output so that it matches the atlas input
 - adjust to use the command_response hook instead of the commands
   thing


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.

Copyright 2004 Will Guaraldi
"""

__author__ = "Will Guaraldi <willg@bluesock.org>"
__version__ = "0.4 (15 January, 2004)"
__description__ = "Basic text-based mapper."

from lyntin import exported, config, ansi, utils
from lyntin.modules import modutils
import commandresponse
import re, cPickle

DIRECTIONS = { "north": "n",       "n": "n", 
               "northeast": "ne",  "ne": "ne",
               "east": "e",        "e": "e",
               "southeast": "se",  "se": "se",
               "south": "s",       "s": "s",
               "southwest": "sw",  "sw": "sw",
               "west": "w",        "w": "w",
               "northwest": "nw",  "nw": "nw"}

REVERSE = {"n":  "s",
           "ne": "sw",
           "e":  "w",
           "se": "nw",
           "s":  "n",
           "sw": "ne",
           "w":  "e",
           "nw": "se" }

SYMBOLLOOKUP = {"n":  (0, 1, "|"),
                "ne": (1, 1, "/"),
                "e":  (1, 0, "-"),
                "se": (1, -1, "\\"),
                "s":  (0, -1, "|"),
                "sw": (-1, -1, "/"),
                "w":  (-1, 0, "-"),
                "nw": (-1, 1, "\\")}

DATADIR = config.options["datadir"]

class Room:
  def __init__(self, x, y, symbol):
    self._x = x
    self._y = y
    self._notes = []
    self._exits = []
    self._symbol = symbol

class Mapper:
  def __init__(self):
    self._enabled = 0
    self._undo_list = []
    self._redo_list = []

    self._map = {}
    self._x = 0
    self._y = 0

    self._symbols = dict(SYMBOLLOOKUP)
    self._dsymbol = "o"

  def startMapping(self):
    self._enabled = 1
    if not self._map.has_key((self._x, self._y)):
      self._map[(self._x, self._y)] = Room(self._x, self._y, self._dsymbol)

  def stopMapping(self):
    self._enabled = 0

  def addRoom(self, coords):
    if not self._map.has_key(coords):
      self._map[coords] = Room(coords[0], coords[1], self._dsymbol)

  def removeRoom(self, coords):
    if self._map.has_key(coords):
      del self._map[coords]

  def move(self, direction):
    self._undo_list.append(direction)
    if len(self._undo_list) > 10:
      adj = len(self._undo_list) - 10
      self._undo_list = self._undo_list[adj:]
    xoff, yoff, symbol = self._symbols[direction]

    self.addRoom((self._x + xoff, self._y + yoff))
    self._x += xoff
    self._y += yoff

  def setexits(self, exits):
    if self._enabled == 1 and self._map.has_key((self._x, self._y)):
      self._map[(self._x, self._y)]._exits = exits
      return 1
    return 0

  def setsymbol(self, sym):
    if self._enabled == 1 and self._map.has_key((self._x, self._y)):
      self._map[(self._x, self._y)]._symbol = sym
    
  def setnotes(self, notes):
    if self._enabled == 1 and self._map.has_key((self._x, self._y)):
      self._map[(self._x, self._y)]._notes.append(notes)
      # self._map[(self._x, self._y)]._symbol = "n"

  def clear(self):
    self._map = {}
    self._undo_list = []
    self.__list = []
    self._x = 0
    self._y = 0

  def exportMap(self, name="unknown"):
    """
    Gets a map for exporting to a file.
    """
    mapstring = """mapname: """ + name + """
template: template.html
maptype: thin
exits: yes
showgrid: no

o = outdoor
i = indoor
g = grass
r = road
m = mountain
a = marsh
d = desert
b = beach
f = forest
t = tundra

BEGIN-MAP
"""
    maptuple = self.getMap(color=0, withnotes=0, smallview=0, markme=0)
    xoff = maptuple[1]
    yoff = maptuple[2]
    mapstring = mapstring + maptuple[0]
    mapstring = mapstring + """END-MAP

# DOTS
#    x  y  dot
"""
    notes = []
    
    for key in self._map.keys():
      value = self._map[key]
      if value._notes:
        x = str(key[0] - xoff)
        y = str((key[1] - yoff) * -1)
        mapstring += "dot: %s %s notes\n" % (x, y)
        vnotes = list(value._notes)
        vnotes = [utils.wrap_text(m) for m in vnotes]
        notes.append("(x%s, y%s) %s" % (x, y, "\n    ".join(vnotes)))

    mapstring += "\n\nBEGIN-NOTES\n"
    mapstring += "\n".join(notes)
    
    return mapstring


  def getMap(self, color=0, withnotes=0, smallview=0, markme=1):
    """
    @param color: whether (1) or not (0) to do color markup
    @type  color: boolean

    @param withnotes: whether (1) or not (0) to display notes
    @type  withnotes: boolean

    @param smallview: whether (1) or not (0) to display the area around
        where we are
    @type  smallview: boolean

    @param markme: whether (1) or not (0) to mark our position
    @type  markme: boolean

    @returns: the map (as a big string) and the coordinate offset (x, y)
    @rtype: (string, int, int)
    """
    keys = self._map.keys()
    if not keys:
      return "no map.", 0, 0

    xlist = [mem[0] for mem in keys]
    xlist = dict(zip(xlist, range(len(xlist)))).keys()
    xlist.sort()
    if smallview == 1:
      i = xlist.index(self._x)
      if i + 8 < len(xlist):
        xlist = xlist[:i+8]
      if i > 8:
        xlist = xlist[i-8:]
    maxx = xlist[-1] + 1
    minx = xlist[0] - 1

    ylist = [mem[1] for mem in keys]
    ylist = dict(zip(ylist, range(len(ylist)))).keys()
    ylist.sort()
    if smallview == 1:
      i = ylist.index(self._y)
      if i + 4 < len(ylist):
        ylist = ylist[:i+4]
      if i > 4:
        ylist = ylist[i-4:]
    maxy = ylist[-1] + 1
    miny = ylist[0] - 1

    # initialize the map
    mymap = []
    for mem in range(((maxy - miny) *2) +1):
      mymap.append(" " * (((maxx - minx) *2) +3))

    def fixpart(part):
      if not part or part == []:
        return ""
      else:
        return part

    # go through all the rooms
    for y in ylist:
      for x in xlist:
        if not self._map.has_key((x, y)):
          continue

        yoff = y - miny
        xoff = x - minx 
        room = self._map[(x,y)]

        line = mymap[(yoff*2)]
        if room._notes and markme:
          line = fixpart(line[:(xoff*2)]) + "n" + fixpart(line[(xoff*2)+1:])
        else:
          line = fixpart(line[:(xoff*2)]) + room._symbol + fixpart(line[(xoff*2)+1:])
        mymap[(yoff*2)] = line

        # handle exits here
        for mem in room._exits:
          yoffoff = 0
          xoffoff = 0
          symbol = ""

          xoffoff, yoffoff, symbol = self._symbols[mem]

          line = mymap[(yoff*2) + yoffoff]
          begin = fixpart(line[:(xoff*2) + xoffoff])
          end = fixpart(line[(xoff*2)+1 + xoffoff:])

          space = line[(xoff*2) + xoffoff]
          if ((space == "/" and symbol == "\\") or (space == "\\" and symbol == "/")):
            symbol = "x"

          line = begin + symbol + end
          mymap[(yoff*2) + yoffoff] = line

    # now we mark where we are
    if markme == 1:
      y = ((self._y - miny) *2)
      x = ((self._x - minx) *2)
      mymap[y] = mymap[y][:x] + "*" + mymap[y][x+1:]

    # flip the map over (because it's upside down right now)
    mymap.reverse()

    # y axis labels
    for i in range(len(mymap)):
      num = i+1
      if num % 2 == 1:
        num = (num / 2)
        mymap[i] = "%s%s" % (str(num).rjust(2), mymap[i])
      else:
        mymap[i] = "  %s" % mymap[i]

    # x axis labels
    line = []
    tenline = []
    for i in range(len(mymap[0]) / 2):
      num = i - 1
      if num != -1:
        one = num % 10
        line.append("%s" % str(one).rjust(2))

        if one == 0:
          ten = num / 10
          tenline.append("%s" % (str(ten).rjust(2)))
        else:
          tenline.append("  ")
      else:
        line.append(" ")
        tenline.append(" ")
    
    mymap.insert(0, "".join(line))
    mymap.insert(0, "".join(tenline))

    # go through the map file and colorize the important letters
    if color == 1:
      mymap = [mem.replace("*", ansi.get_color("red") + "*" + ansi.get_color("default")) for mem in mymap]
      mymap = [mem.replace("n", ansi.get_color("blue") + "n" + ansi.get_color("default")) for mem in mymap]

    if withnotes == 1:
      mymap.append("")
      mymap.append("")
      for mem in self._map.keys():
        y = (mem[1] - maxy) * -1
        x = mem[0] - minx
        room = self._map[mem]
        if room._notes:
          mymap.append("(%d, %d)\n%s" % (x, y, "\n".join(room._notes)))

    return ("\n".join(mymap) + "\n", minx, maxy)

  def undo(self):
    global REVERSE
    if self._enabled == 1 and len(self._undo_list) > 0:
      temp = self._undo_list[-1]
      del self._undo_list[-1]
      self._redo_list.append(temp)

      reverse = REVERSE[temp]
      xoff, yoff, symbol = self._symbols[reverse]
      self.removeRoom((self._x, self._y))
      self._x += xoff
      self._y += yoff

      return temp

  def redo(self):
    if self._enabled == 1 and len(self._redo_list) > 0:
      temp = self._redo_list[-1]
      del self._redo_list[-1]
      self._undo_list.append(temp)

      self.move(temp)
      return 
    

mapper = Mapper()
commands_dict = {}


EXITRE = re.compile(r"There (is|are) (.+?) obvious exit[s]?: (.+?)$")

class lookprocesser(commandresponse.ProcessingTag):
  """
  Pretty much just a wrapper for the process method which
  takes the response and pulls the gxp from it.
  """
  def __init__(self):
    pass

  def process(self, ses, cmd, resp):
    """
    We read through the response from doing "gscore" and 
    pull the gxp value from it and toss it in our list
    of (time, gxp) tuples.  Then we print everything.
    """
    global mapper, DIRECTIONS

    if mapper._enabled == 1:
      exits = None
      resp = resp.splitlines()
      for i in range(0, len(resp)):
        
        exits = EXITRE.search(resp[i])
        if exits:
          exits = exits.group(3)
          if i +1 < len(resp) and resp[i+1].startswith(" "):
            exits = exits + " " + resp[i+1].strip()
          break

      if not exits:
        exported.write_message("mapper: no exits found.")
        return

      exits = exits.replace(",", " ")
      exits = exits.replace("(", " ")
      exits = exits.replace(")", " ")
      exits = exits.replace(".", "")
      exits = exits.split()
    
      nexits = [DIRECTIONS[mem] for mem in exits if DIRECTIONS.has_key(mem)]
      nonexits = [mem for mem in exits if not DIRECTIONS.has_key(mem)]

      ret = mapper.setexits(nexits)
      if ret == 1:
        if "and" in nonexits:
          nonexits.remove("and")
        if nonexits:
          exported.write_error("mapper: %r aren't valid exits" % nonexits)
          mapper.setnotes("Additional exits: %r" % nonexits)
        exported.write_message("mapper: setting exits %r" % nexits)


def look_cmd(ses, args, input):
  """
  If mapping is enabled, this will perform a "look" and pull out
  interesting information.

  category: mapper
  """
  global mapper

  if mapper._enabled == 1:
    ses.writeSocket("look\n", lookprocesser())
  else:
    ses.writeSocket("look\n")

commands_dict["look"] = (look_cmd, "")


class goprocesser(commandresponse.ProcessingTag):
  def __init__(self, mapper, direction):
    self._mapper = mapper
    self._direction = direction

  def process(self, ses, cmd, resp):
    """
    Checks to see if we couldn't go that way.  If we couldn't
    then it does nothing.  Otherwise it "moves" us that direction.
    """
    global DIRECTIONS

    if resp.find("You cannot go " + self._direction) != -1:
      return

    if self._direction in DIRECTIONS.keys():
      mapper.move(DIRECTIONS[self._direction])


def go_cmd(ses, args, input):
  """
  If mapping is enabled, maps in a specific direction.

  category: mapper
  """
  global mapper

  direction = args["direction"]

  if mapper._enabled == 1:
    ses.writeSocket(direction + "\n", goprocesser(mapper, direction))
  else:
    ses.writeSocket(direction + "\n")

commands_dict["go"] = (go_cmd, "direction")


def mstart_cmd(ses, args, input):
  """
  Starts the mapper and puts it in mapping mode.

  category: mapper
  """
  global mapper
  mapper.startMapping()
  exported.write_message("mapper: starting.")

commands_dict["mstart"] = (mstart_cmd, "")

def mstop_cmd(ses, args, input):
  """
  Stops the mapper and takes it out of mapping mode.

  category: mapper
  """
  global mapper
  mapper.stopMapping()
  exported.write_message("mapper: stopping.")

commands_dict["mstop"] = (mstop_cmd, "")

def mmove_cmd(ses, args, input):
  """
  Moves the marker on the map x spaces on the x axis and
  y spaces on the y axis.

  category: mapper
  """
  global mapper

  x = args["x"]
  y = args["y"] * -1

  x = mapper._x + x
  y = mapper._y + y

  if mapper._map.has_key((x, y)):
    mapper._x = x
    mapper._y = y
    exported.write_message("mapper: moving %d %d." % (args["x"], args["y"]))
    exported.lyntin_command("#mshow")
  else:
    exported.write_error("mapper: that space is not valid.")

commands_dict["mmove"] = (mmove_cmd, "x:int y:int")

def mdelete_cmd(ses, args, input):
  """
  Removes a room and its exits from the map.

  category: mapper
  """
  x = args["x"]
  y = args["y"]

  if mapper._map.has_key((x, y)):
    mapper.removeRoom((x, y))
    exported.write_message("mapper: room (%d, %d) removed." % (x, y))

  else:
    exported.write_error("mapper: that space is not valid.")

commands_dict["mdelete"] = (mdelete_cmd, "x:int y:int")

def madd_cmd(ses, args, input):
  """
  Adds a room to the map.

  category: mapper
  """
  x = args["x"]
  y = args["y"]

  if mapper._map.has_key((x, y)):
    exported.write_error("mapper: that room already exists.")
  else:
    mapper.addRoom((x, y))
    exported.write_message("mapper: room (%d, %d) created." % (x, y))

commands_dict["madd"] = (madd_cmd, "x:int y:int")


def mclear_cmd(ses, args, input):
  """
  Clears the mapper and takes it out of mapping mode.

  category: mapper
  """
  global mapper
  mapper.clear()
  exported.write_message("mapper: cleared map.")

commands_dict["mclear"] = (mclear_cmd, "")

def mshow_cmd(ses, args, input):
  """
  Shows the current map.  The blue n's are rooms that have
  notes.  The red x is where you are.

  If you want to see the notes too, pass a true in.

  category: mapper
  """
  global mapper

  withnotes = args["withnotes"]
  smallview = args["smallview"]

  temp = mapper.getMap(1, withnotes, smallview)[0]

  exported.write_message("mapper: showing map\n%s" % temp)
  exported.write_message("coordinates are x%s, y%s." % (mapper._x, mapper._y))

commands_dict["mshow"] = (mshow_cmd, "smallview:boolean=true withnotes:boolean=false")

def mredo_cmd(ses, args, input):
  """
  Redoes the last undone command.

  category: mapper
  """
  global mapper
  temp = mapper.redo()
  if temp:
    exported.write_message("mapper: redid %s." % temp)
  else:
    exported.write_message("mapper: nothing to redo.")

commands_dict["mredo"] = (mredo_cmd, "")

def mundo_cmd(ses, args, input):
  """
  Removes the last room created.

  category: mapper
  """
  global mapper
  temp = mapper.undo()
  if temp:
    exported.write_message("mapper: undid %s." % temp)
  else:
    exported.write_message("mapper: nothing to undo.")

commands_dict["mundo"] = (mundo_cmd, "")


def msetexits_cmd(ses, args, input):
  """
  Sets exits on a room.

  category: mapper
  """
  global mapper, DIRECTIONS

  if mapper._enabled == 1:
    exits = args["exits"]
    exits = exits.replace(",", " ")
    exits = exits.replace("(", " ")
    exits = exits.replace(")", " ")
    exits = exits.replace(".", "")
    exits = exits.split()
    
    nexits = [DIRECTIONS[mem] for mem in exits if DIRECTIONS.has_key(mem)]
    nonexits = [mem for mem in exits if not DIRECTIONS.has_key(mem)]

    if nonexits:
      exported.write_error("mapper: %r aren't valid exits" % nonexits)
    mapper.setexits(nexits)
    exported.write_message("mapper: setting exits %r" % exits)

commands_dict["msetexits"] = (msetexits_cmd, "exits=")


def msetsymbol_cmd(ses, args, input):
  """
  Lets you change the symbol for a room.

  category: mapper
  """
  global mapper
  if mapper._enabled == 1:
    sym = args["sym"]
    mapper.setsymbol(sym)
    exported.write_message("mapper: symbol set to '%s'." % sym)
  else:
    exported.write_error("mapper: mapper is disabled--you cannot set the symbol.")
    return

  if args["default"] == 1:
    mapper._dsymbol = sym
    exported.write_message("mapper: default symbol set to '%s'." % sym)

commands_dict["msetsymbol"] = (msetsymbol_cmd, "sym default:boolean=no")


def msetnotes_cmd(ses, args, input):
  """
  Adds notes to a room.

  category: mapper
  """
  global mapper

  if mapper._enabled == 1:
    notes = args["input"]
    if not notes:
      exported.write_error("mapper: requires a note.")
      return

    mapper.setnotes(notes)
    exported.write_message("mapper: adding notes '%s'" % notes)

commands_dict["msetnotes"] = (msetnotes_cmd, "input=", "limitparsing=0")
  

def mstats_cmd(ses, args, input):
  """
  Gives some mapper stats.

  category: mapper
  """
  global mapper

  if mapper._enabled == 1:
    exported.write_message("mapper is enabled.")
  else:
    exported.write_message("mapper is disabled.")

  exported.write_message("there are %d room(s) mapped." % len(mapper._map.keys()))
  exported.write_message("coordinates are x%s, y%s." % (mapper._x, mapper._y))
  exported.write_message("default symbol is %s." % mapper._dsymbol)

commands_dict["mstats"] = (mstats_cmd, "")

def mexport_cmd(ses, args, input):
  """
  Exports a map file into an ascii file.

  category: mapper
  """
  global mapper
  filename = args["filename"]

  mapfile = mapper.exportMap()

  f = open(DATADIR + filename, "w")
  f.write(mapfile)
  f.close()
  exported.write_message("mapper: map saved as %s." % filename)

commands_dict["mexport"] = (mexport_cmd, "filename")

def msave_cmd(ses, args, input):
  """
  Saves the mapfile into the pickle file of map files.

  category: mapper
  """
  global mapper

  mapname = args["mapname"]

  try:
    f = open(DATADIR + "maps.sav", "r")
    maps = cPickle.load(f)
    f.close()
  except:
    maps = {}

  maps[mapname] = mapper

  f = open(DATADIR + "maps.sav", "w")
  cPickle.dump(maps, f)
  f.close()
  exported.write_message("mapper: map saved to %s." % mapname)

commands_dict["msave"] = (msave_cmd, "mapname")

def mlist_cmd(ses, args, input):
  """
  Lists maps we have in the pickle of map files.

  category: mapper
  """
  f = open(DATADIR + "maps.sav", "r")
  maps = cPickle.load(f)
  f.close()

  listing = maps.keys()
  listing.sort()

  exported.write_message("Maps available:")
  exported.write_message(utils.columnize(listing, indent=3))

commands_dict["mlist"] = (mlist_cmd, "")

def mcompile_cmd(ses, args, input):
  """
  Goes through all the maps and exports them all.

  category: mapper
  """
  f = open(DATADIR + "maps.sav", "r")
  maps = cPickle.load(f)
  f.close()

  exported.write_message("mapper: Compiling all maps.")
  for m in maps.keys():
    exported.write_message("mapper:    exporting %s." % (m + ".amap"))
    mapfile = maps[m].exportMap(m)
    f = open(DATADIR + m + ".amap", "w")
    f.write(mapfile)
    f.close()

  exported.write_message("mapper: done.")

commands_dict["mcompile"] = (mcompile_cmd, "")

def mload_cmd(ses, args, input):
  """
  Loads the mapfile from a pickle of map files.

  category: mapper
  """
  global mapper

  mapname = args["mapname"]

  f = open(DATADIR + "maps.sav", "r")
  maps = cPickle.load(f)
  f.close()

  mapper = maps[mapname]
  if not hasattr(mapper, "_dsymbol"):
    mapper._dsymbol = "o"

  exported.write_message("mapper: map %s loaded." % mapname)

commands_dict["mload"] = (mload_cmd, "mapname")

def mremovemap_cmd(ses, args, input):
  """
  Removes a map from the map file.  There is no undoing this action.

  category: mapper
  """
  global mapper

  mapname = args["mapname"]

  f = open(DATADIR + "maps.sav", "r")
  maps = cPickle.load(f)
  f.close()

  del maps[mapname]

  f = open(DATADIR + "maps.sav", "w")
  cPickle.dump(maps, f)
  f.close()

  exported.write_message("mapper: map %s removed." % mapname)

commands_dict["mremovemap"] = (mremovemap_cmd, "mapname")

def load():
  """ Initializes the module by binding all the commands."""
  global mapper

  modutils.load_commands(commands_dict)

def unload():
  """ Unload things."""
  global mapper

  modutils.unload_commands(commands_dict)