# code:
# * George Moschovitis  <gm@navel.gr>
#
# (c) 2004 Navel, all rights reserved.
# $Id: script.rb 167 2004-11-23 14:03:10Z gmosx $

require "fileutils"

require "glue/cache"
require "nitro/server/fragment"

module N

# = ScriptExitException
#
# Raise this Exception in your Script to avoid processing.
# Typicaly used in request.redirect scenarios!
#
class ScriptExitException < Exception; end

end # module

module N

# = Script
#
# Base class for scripts. Typically a handler evaluates a script. 
# The base class describing  a script is defined here.
# A generalized fragment caching system is also defined.
#
# see handlers/page-handler::PageScript for more information.
#--
# no need to prepend __ to the attributes! (@ is prepended)
#++
#
class Script
	# gmosx: it would be a good idea to make this a singleton, but we
	# had some problems when redefining the class when monitor_scripts
	# is enabled. investigate this!
	# include Singleton
	
	# the full path to the actual script file
	attr_accessor :path

	# the sub scripts.
	attr_accessor :sub_scripts
	
	attr_accessor :key
	
	# is this script cacheable? NONE, SERVER, DOWNSTREAM, CLIENT
	attr_accessor :cacheability
	
	def initialize(path)
		@path = "#$root_dir/#{path}"

		# gmosx, INVESTIGATE: createtime == filemtime ?
		
		# the creation time for this script-class
		@create_time = Time.now
		
		# when the filename was last modified
		@file_mtime = File.mtime(@path).to_i

		# sub-scripts set: a set of files this script depends on.
		# We use a hash to implement a set.
		@sub_scripts = G::SafeArray.new

		# a cache for the script outputs (fragments). Keeps multiple revisions
		# of the script output according to user, access rights etc.
		# must be thread safe because it is shared accross threads.
		# 
		# gmosx: DONT DEPRECATE THIS! if we switch to FastCGI, perhaps
		# this will be needed!
		#
		# DISK CACHING IS SLOW!
		#
		@fragment_cache = G::LRUCache.new(1000)
		
		__init()
	end

	# This method is called at compile time to perform
	# additional initialization.
	#
	def __init
	end
	
	# This method is called before rendering to perform
	# additional initialization.
	#
	def __init_render(request)
		# nop
	end
	
	# Tag for this request
	#
	def __tag(request)
		# gmosx: dont return nil!
		return ""
	end
	
	# Recursively calculate tag.
	#
	def __calc_tag(request)
		tag = __tag(request)

		if @sub_scripts
			for script in @sub_scripts
				tag << script.__calc_tag(request)
			end
		end
		
		return tag
	end
	
	# Calculate LastModified for HTTP1.1 caching
	# Return the last modified time for this script.
	#
	def __lm(request)
		return nil
	end
	
	# Calculate LastModified for HTTP1.1 caching
	# takes into account all subscripts! also takes into account
	# the script file modification
	#
	def __calc_lm(request)
		unless lm = __lm(request)
			lm = @file_mtime
		else
			lm = @file_mtime if @file_mtime > lm
		end
		
		if @sub_scripts
			for script in @sub_scripts
				if slm = script.__calc_lm(request)
					unless lm
						lm = slm
					else
						lm = slm if slm > lm
					end
				end
			end
		end
		
		return lm
	end

	# Calculates the ETag fof HTTP1.1 caching. This etag is typically 
	# used in the downstream, so we can be VERY granular :-) ie
	# we can even encode a user id!
	#
	# Coded by top level scripts only! 
	# Typically combines __tag and __last_modified
	#
	def __etag(request)
		return "#{__calc_lm(request)}#{__calc_tag(request)}#{request.tag}" # .hash.to_s
	end

	# Is this script cacheable for this request?
	#
	def __cache?(request)
		# By default if lm is overriden the page is cacheable
		return __lm(request) 
	end
		
	#---------------------------------------------------------------------
	# Caching logic:
	#
	
	# gmosx, DEPRECATED: the following caching methods are deprecated!
	# they are still used by rx handler though!

	# clear the fragment_cache
	#
	def __cache_clear
		@fragment_cache.clear
	end

	# get a fragment from the fragment cache in a thread safe manner.
	#
	# Output:
	# the fragment object encapsulating the output of this script.
	#
	def __cache_get(key)
		return @fragment_cache[key]
	end

	# add a new fragment in the fragment cache in a thread safe manner.
	# encapsulate the fragment text (== body) in a fragment object.
	#
	def __cache_put(key, fragment)
		@fragment_cache[key] = fragment
	end

	#--------------------------------------------------------------------------
	# Rendering logic:

	# renders the script as html output to be send to the
	# borwser.
	# the output of this script is the script fragment.
	# this method is constructed dynamically by the handler.
	#
	# Design:
	#
	# the parameters to this method are visible to page scripts.
	# in order to avoid namespace pollution and dangerous bugs in the
	# scripts we prepend an underscore to differentiate them. n1 used
	# @variables (class attributes) but we believe they where thread-unsafe.
	#
	def __render(request)
	end

	# Dynamically include ("inject") a subpage (fragment) in this page.
	# Dynamic means at run-time.
	#
	def __inject(url, request)
		return ""
	end

	# Dynamically include ("inject") a subpage (fragment) in this page.
	# Dynamic means at run-time.

	def __include(url, request)
		return ""
	end

	def __create_time
		return @create_time
	end

	def __file_mtime
		return @file_mtime
	end
	
	def __path
		return @path
	end

	#--------------------------------------------------------------------------
	# Actions:

	def admin?(request)
		return false;
	end
	
	# Encodes an action url to the same page.
	# Params is almost always != nil, no need to optimize the case.
	#
	def __action(request, params = nil)		
		if request.parameters.empty?
			return "#{request.translated_uri}?#{params}"
		else
			return "#{request.translated_uri}?#{request.query_string};#{params}"
		end
	end
	alias_method :_a, :__action
	
	#--------------------------------------------------------------------------
	# Hooks:
	
	# executed prior to script evaluation, even for cached fragments!
	# not used yet.
	#
	def __pre_evaluate(request)
	end

	# executed after script evaluation, even for cached fragments!
	# not used yet.

	def __post_evaluate(request)
	end

	# Pre-render hook. NOT executed for cached fragments
	# not used yet.
	#
	# Returns:
	# text that is prepended to the request, by default the EMPTY_STRING
	#
	def __pre_render(request)
		return EMPTY_STRING
	end

	# Post-render hook. NOT executed for cached fragments.
	# not used yet.
	#
	# Returns:
	# text that is appended to the request, by default the EMPTY_STRING
	#
	def __post_render(request)
		return EMPTY_STRING
	end

	# Calculate a hash modifier (flag) to differentiate caching according
	# to user roles or other dynamic conditions. The calculated flag
	# is appended to the fragmant hash.
	#
	# Design:
	# use this separate method to allow the top level page to
	# collect the flags for all sub-pages and build
	# a super-flag.
	#
	def cache_flag(request)
		return Fragment::ADMIN_FLAG if admin?(request)
	end

	# enable fragment caching for this script?
	# to avoid storing in the cache uncacheable fragments
	#
	def cache?(request)
		# gmosx: AAAAAAAAARGHHH!!! NASTY keep this false!
		# to save memory!
		return false
	end

	# is the output of the script (the fragment) valid for this request?
	# used for cache invalidation calculations. By default returns false.
	#
	# Input:
	# __fragment_key: the server automatically generates a fragment_key (typically
	# the request.real_path) and passes this to this methods for further
	# customization. The key passed is the base key, the method is responsible to
	# build the final key (taking user roles, etc into account).
	# request: used to customize the fragment_key for this request.
	# customized fragment_keys denote request classes.
	#
	# Output:
	# is the fragment valid for this request?
	#
	# Side-effects:
	# WARNING: the method potentially modifies __fragment_key!
	#
	def cache_valid?(fragment, request)
		return false
	end

	# authorize access to this script ?
	#
	def __authorize?(request)
		return true
	end

	# get the shader to use for this script
	#
	def __shader(request)
		return nil
	end

	# force a logged-in user for this page.
	# implemented like the n1-version to avoid checking everytime
	# for login, use the if __force method.
	#
	def __force_login(request)
		if request.session["USER"].anonymous?
			request.redirect("/id/login.sx?_go=#{request.uri}")
			return false
		end
		return true
	end

	# --------------------------------------------------------------------
	# Macros
	# TODO: create more usefull macros for admin?/cache_valid? etc.

	# Sets cache?() to return true
	#
	def self.enable_cache!
		class_eval %{	
			def __cache?(request)
				return true
			end
		}
	end
	
	# Evals a standard admin?() method.
	#
	def self.admin_role(role)
		class_eval %{
			def admin?(request)
				return request.user.role?("#{role}") 
			end
		}
	end
	
end

end # module