# Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
#
# This program 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.
#
# This program 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/>.

import string
import re
import os
ALLOWED_KEYS = string.ascii_letters + string.digits + "`'"

class Bookmarks(object):
	"""Bookmarks is a container which associates keys with bookmarks.

		A key is a string with: len(key) == 1 and key in ALLOWED_KEYS.

		A bookmark is an object with: bookmark == bookmarktype(str(instance))
		Which is true for str or FileSystemObject. This condition is required
		so bookmark-objects can be saved to and loaded from a file.

		Optionally, a bookmark.go() method is used for entering a bookmark.
		"""

	last_mtime = None
	autosave = True
	load_pattern = re.compile(r"^[\d\w']:.")

	def __init__(self, bookmarkfile, bookmarktype=str, autosave=False):
		"""<bookmarkfile> specifies the path to the file where
		bookmarks are saved in.
		"""
		self.autosave = autosave
		self.dct = {}
		self.path = bookmarkfile
		self.bookmarktype = bookmarktype

	def load(self):
		"""Load the bookmarks from path/bookmarks"""
		try:
			new_dict = self._load_dict()
		except OSError:
			return

		self._set_dict(new_dict, original=new_dict)

	def delete(self, key):
		"""Delete the bookmark with the given key"""
		if key == '`':
			key = "'"
		if key in self.dct:
			del self.dct[key]
			if self.autosave: self.save()

	def enter(self, key):
		"""Enter the bookmark with the given key.
		Requires the bookmark instance to have a go() method.
		"""

		try:
			return self[key].go()
		except (IndexError, KeyError, AttributeError):
			return False

	def update_if_outdated(self):
		if self.last_mtime != self._get_mtime():
			self.update()

	def remember(self, value):
		"""Bookmarks <value> to the key '"""
		self["'"] = value
		if self.autosave: self.save()

	def __iter__(self):
		return iter(self.dct.items())

	def __getitem__(self, key):
		"""Get the bookmark associated with the key"""
		if key == '`':
			key = "'"
		if key in self.dct:
			return self.dct[key]
		else:
			raise KeyError("Nonexistant Bookmark: `%s'!" % key)

	def __setitem__(self, key, value):
		"""Bookmark <value> to the key <key>.
		key is expected to be a 1-character string and element of ALLOWED_KEYS.
		value is expected to be a filesystemobject.
		"""
		if key == '`':
			key = "'"
		if key in ALLOWED_KEYS:
			self.dct[key] = value
			if self.autosave: self.save()

	def __contains__(self, key):
		"""Test whether a bookmark-key is defined"""
		return key in self.dct

	def update(self):
		"""Update the bookmarks from the bookmark file.
		Useful if two instances are running which define different bookmarks.
		"""

		try:
			real_dict = self._load_dict()
			real_dict_copy = real_dict.copy()
		except OSError:
			return

		for key in set(self.dct.keys()) | set(real_dict.keys()):
			# set some variables
			if key in self.dct:
				current = self.dct[key]
			else:
				current = None

			if key in self.original_dict:
				original = self.original_dict[key]
			else:
				original = None

			if key in real_dict:
				real = real_dict[key]
			else:
				real = None

			# determine if there have been changes
			if current == original and current != real:
				continue   # another ranger instance has changed the bookmark

			if key not in self.dct:
				del real_dict[key]   # the user has deleted it
			else:
				real_dict[key] = current   # the user has changed it

		self._set_dict(real_dict, original=real_dict_copy)

	def save(self):
		"""Save the bookmarks to the bookmarkfile.
		This is done automatically after every modification if autosave is True."""
		self.update()
		if self.path is None:
			return
		if os.access(self.path, os.W_OK):
			f = open(self.path+".new", 'w')
			for key, value in self.dct.items():
				if type(key) == str\
						and key in ALLOWED_KEYS:
					try:
						f.write("{0}:{1}\n".format(str(key), str(value)))
					except:
						pass

			f.close()
			os.rename(self.path+".new", self.path)
		self._update_mtime()

	def _load_dict(self):
		dct = {}

		if self.path is None:
			return dct

		if not os.path.exists(self.path):
			try:
				f = open(self.path, 'w')
			except:
				raise OSError('Cannot read the given path')
			f.close()

		if os.access(self.path, os.R_OK):
			f = open(self.path, 'r')
			for line in f:
				if self.load_pattern.match(line):
					key, value = line[0], line[2:-1]
					if key in ALLOWED_KEYS: 
						dct[key] = self.bookmarktype(value)
			f.close()
			return dct
		else:
			raise OSError('Cannot read the given path')

	def _set_dict(self, dct, original):
		if original is None:
			original = {}

		self.dct.clear()
		self.dct.update(dct)
		self.original_dict = original
		self._update_mtime()

	def _get_mtime(self):
		if self.path is None:
			return None
		try:
			return os.stat(self.path).st_mtime
		except OSError:
			return None

	def _update_mtime(self):
		self.last_mtime = self._get_mtime()
