require 'thread'
#require 'log4r'
#include Log4r
require 'iowa/SessionStats'

module Iowa

	# Container for an inline generated resource.
	
	class Resource

		attr_accessor :body, :content_type, :headers
		
		def initialize(body,content_type=Capplication_data,headers=nil)
			@body = body
			@content_type = content_type
			@headers = headers
		end
	end
	
	# Custom exception which is thrown if a request does not have a request ID.
	
	class IgnoreRequest < Exception; end

	class NoAction < Exception; end
	
	# Custom exception which is thrown if a request asks for a page which has
	# expired from the cache.
	
	class PageExpired < Exception; end
	
	# Represents a session between the application and the user.
	
	class Session
		
		# Default delay between the presentation of the page expired message
		# (see this below) and the redirect to the top of the application.
	
	
		# Class variable which holds the class of a session.
	
	
		# Define an accessor to make the context object accessible to the
		# application's code.
	
		attr_accessor :context, :application, :currentPage, :requestCount, :pages,
			:resourceCount, :notes, :access_time, :lock
	
		@sessionClass = self

		def Session.cachedPages
			@cachedPages
		end

		def Session.cachedPages=(val)
			@cachedPages = val
		end

		def Session.cacheTTL
			@cacheTTL
		end

		def Session.cacheTTL=(val)
			@cacheTTL = val
		end
	
		# Constructor to return a new instance of @sessionClass.
	
		def Session.newSession(*args)
			@sessionClass.new(*args)
		end
	
		def Session.inherited(subclass)
			@sessionClass = subclass
		end
	
		def self.PageCache
			@page_cache_class ? @page_cache_class.new(@page_class_args) : nil
		end

		def self.PageCache=(args)
			@page_cache_class, @page_class_args = Util.get_cache(args,nil,Iowa.config[Csession],Cpagecache,true)
		end

		FCM = Iowa::Component.methods.inject({}) {|h,k| h[k] = true; h} unless const_defined?(:FCM)

		# Initialize the state of the session.
	
		def initialize
			@cachedPages ||= 10
			@cacheTTL ||= nil
			@expiryDelay ||= 1
			reset
			#@lock = Iowa::Mutex.new
			@lock = Mutex.new
			@creation_time = Time.now
			#@statistics = Iowa::SessionStats.new(@pages)
		end
	
		def statistics
			@statistics
		end
	
		# Handles the current request from the browser.  Essentially all that
		# is happening is thread synchronizations along with some tests to make
		# sure that all the data necessary to handle the request exists.
		# Then the call to handleRequest gets passed on to the object representing
		# the current page.
	
		def handleRequest(context,dispatch_destination = nil)
			dispatch_destination = Iowa::DispatchDestination.new(dispatch_destination) if dispatch_destination.is_a?(::String)
			@fragment = nil
			#@statistics.hit
			@access_time = Time.now
			mylog = Logger[Ciowa_log]
			if @lock.locked?
				mylog.info("Session collision.  Waiting.")
			end
			#mylog.debug("Handle Session for #{dispatch_destination.inspect}")
#			@lock.synchronize do
			@lock.lock
#				begin
					context.session = self
					requestPage = @currentPage
					@context = context
					unless !context.requestID || dispatch_destination
						#begin
							# If the request is for a resource, handle that.
							return handle_resource(context) if @resources.has_key?("#{context.requestID}.#{context.actionID}")
							# Bail out if that page is not in the cache.  Remember that a request
							# runs on the current page; it's the response that generates a new page.
							raise PageExpired unless @pages.include?(context.requestID)
							requestPage = @pages[context.requestID]

							if @requestCount != context.requestID and !requestPage.replaceable?
								requestPage.handleBacktrack
							end

							@currentPage = requestPage.replaceable? ? requestPage : requestPage.dup

							# TODO: Make it a runtime option to reload modified or not.
							@application.reloadModified(@currentPage.class.name)

							context.phase = :handleRequest
							@currentPage.session = self
							@currentPage.handleRequest(context)
							nextPage = context.invokeAction
							if nextPage and nextPage.fragment
								@fragment = nextPage
							else
								@currentPage = nextPage if nextPage
							end

							@requestCount = @requestCount.next
						#end
					else
						@currentPage = requestPage
					end
					handleResponse(context, dispatch_destination)
				rescue PageExpired => e
					expired(@pages.queue.last,context)
				rescue NoAction => e
					mylog.info "Render last page seen because there is no action -- something is probably not right, here."
					#expired(@pages.queue.last,context)
					@currentPage = @pages[@pages.queue.last]
					@currentPage.session = self
					handleResponse(context)
				ensure
					@lock.unlock
#				end
#			end
		end
	
		# Specify a starting page name.
		def startingPageName; 'Main'; end
		
		# Return a component for the starting page; should use startingPageName.
		def startingPage
			Component.pageNamed(startingPageName,self)
		rescue Exception => e
			Logger[Ciowa_log].error "Starting page selection failed with #{e}\n#{e.backtrace.join("\n")}"
			raise e
		end
		
		# Invokes the handleResponse() method on the object representing the page
		# and updates the page cache.
	
		def handleResponse(context, dispatch_destination = nil)
			context.phase = :handleResponse
			return if context.do_not_render
			@currentPage = Component.pageNamed(dispatch_destination.component,self,nil,nil,nil) if dispatch_destination and dispatch_destination.component
			@currentPage = startingPage unless @currentPage
			if !dispatch_destination.nil? and (ddm = dispatch_destination.method) and !FCM.has_key?(ddm)
				context.phase = :handleRequest
				verb_specific_method = "#{ddm}_#{context.request.request_method.upcase}"
				ddm = verb_specific_method if @currentPage.respond_to?(verb_specific_method)
				if @currentPage.respond_to?(ddm)
					@application.reloadModified(@currentPage.class.name)
					@currentPage = context.invokeAction(@currentPage,ddm,dispatch_destination.args) || @currentPage
				else
					@currentPage = startingPage
				end
				context.phase = :handleResponse
			end
				
			fp = @currentPage.fingerprint
			if fp and @application.rendered_content.has_key?(fp)
				rc = @application.rendered_content[fp]
				context.response_buffer << rc[0]
				context.response.content_type = rc[1]
				rc[2].each do |k,v|
					context.response.headers[k] = v
				end
			else
				context.requestID = @requestCount unless @currentPage.replaceable? or (@fragment and @fragment.replaceable?)
				unless @fragment
					@currentPage.handleResponse(context)
				else
					@fragment.handleResponse(context)
				end
	
				# The Session delays creating the pagecache until it is needed.
				# This is a performance boost for the most common case, which is
				# a page with some dynamic content, but no form elements or
				# dynamic urls that depend on a cached page for meaning.
			
				context[:allow_docroot_caching] = @currentPage.allow_docroot_caching?

				unless context[:skip_pagecache]
					initialize_pagecache if !@pages
					@pages[context.requestID] = @currentPage if @currentPage.cacheable?
					@pages[context.requestID].session = nil if @pages[context.requestID].respond_to?(:session) and @currentPage.cacheable?
				else
					@application.rendered_content[fp] = [context.response_buffer,context.response.content_type,context.response.headers] if fp
				end
			end
		rescue Exception => e
			Logger[Ciowa_log].error "Application#handleResponse exception: #{e}\n#{e.backtrace.join("\n")}"
			raise e
		end

		# If one passes into resource_url content of some sort plus an optional
		# content type (default is application/data if not specified), it will
		# return a URL that can be used to access that content.  If, on the
		# other hand, one passes an arbitrary set of arguments and a block,
		# a url will be returned that, when accesed, will cause the block to
		# be executed.  If the block returns an instance of Iowa::Resource,
		# the content in the resource will be returned as a result of calling
		# that URL, with the content type specified in the resource object.
		# Any other return value will have that value returned as the result
		# of calling the URL, with a content type of 'application/data'.
		# The resources are tied to the page that created them, so they are
		# available until the page expires.  Once the page expires, the
		# resource is deleted as well.
		def resource_url(*args,&block)
			if block_given?
				resource = [block,args]
			else
				resource = Iowa::Resource.new(args[0], args[1], args[2])
			end
			resourceID = "r_#{@requestCount}.#{@resourceCount}"
			@resources[resourceID] = resource
			@resources_by_component[@context.requestID] ||= []
			@resources_by_component[@context.requestID].push resourceID
			@resourceCount = @resourceCount.next
			return "#{@context.sessionURL}.#{resourceID}#{@context.locationFlag}"
		end
		alias :resourceUrl :resource_url
		
		def handle_resource(context)			
			r = @resources["#{context.requestID}.#{context.actionID}"]
			resource = r.kind_of?(Array) ? r[0].call(*r[1]) : r
			if resource.kind_of? Iowa::Resource
				context.response_buffer << resource.body
				context.response.content_type = resource.content_type
				if resource.headers
					resource.headers.each do |k,v|
						context.response.headers[k] = v
					end
				end
			else
				context.response << resource.to_s
				context.response.content_type = Capplication_data
			end
		end
		
		# Redirects the browser to the given URL.  Second optional param is a true/false
		# flag to indicate whether the redirect should be presented as permanent (301) or
		# not (302).
		def redirect(url,permanent = false)
			context.response.headers[CLocation] = url
			context.response.status = permanent ? 301 : 302
		end
		
		# Resets the session.  Useful as part of logging someone out of an application.
		# Once the page cache is cleared, they can not backtrack.
		
		def reset
			@currentPage = nil
			@requestCount = 'a'
			@resourceCount = 'a'
			@resources = {}
			@resources_by_component = {}
			@notes = {C__keystack => []}
			unless Iowa.config[Csession][Cpagecache][Cclass]
				Iowa.config[Csession][Cpagecache][Cclass] = 'iowa/caches/LRUCache'
				Iowa.config[Csession][Cpagecache][Cmaxsize] = @cachedPages
				Iowa.config[Csession][Cpagecache][Cttl] = @cacheTTL
			end
			self.class.PageCache = nil unless self.class.PageCache
		end

		# Clears the page cache.  Takes one optional argument.  The
		# first optional argument specifies how many entries to leave in the
		# page cache; it defaults to 0.
		def clear_history(leave = 0)
			ps = @pages.size
			@pages.size = leave
			@pages.size = ps
			GC.start
		end
		
		def [](v)
			@notes[v]
		end

		def []=(k,v)
			@notes[k] = v
		end

		private
	
		def initialize_pagecache
			@pages = self.class.PageCache
			@pages.add_finalizer(@resources,@resources_by_component) do |key,obj,resources,resources_by_component|
				if resources_by_component.has_key? key
					resources_by_component[key].each do |res_id|
						resources.delete res_id
					end
					resources_by_component.delete key
				end
			end
		end

		# Returns an explanation that the page being requested has expired, then
		# issues a redirect to the head of the app.  It'd be neat if there were
		# some easy way to configure the contents of this page for an app.

		def expired(most_recent_page,context)
			context.response << "<html><head><meta http-equiv=REFRESH content='#{@expiryDelay}; URL=#{context.sessionURL}.#{most_recent_page}.0#{context.locationFlag}'></head><body><b>That page has expired.<p>You are being forwarded to your <a href='#{context.sessionURL}.#{most_recent_page}.0#{context.locationFlag}'>current point</a> in the session.  Please continue from there.</b></body></html>"
		end
	end

end