require 'uri'
require 'mephisto/theme_root'

class Site < ActiveRecord::Base
  @@default_assigns = {}
  @@theme_path      = Object::THEME_ROOT
  cattr_reader :theme_path, :default_assigns

  cattr_accessor :multi_sites_enabled, :cache_sweeper_tracing
  
  # @@template_handlers = HashWithIndifferentAccess.new if @@template_handlers.nil?
  @@template_handlers = {}
  
  # Register a class that knows how to handle template files with the given
  # extension. This can be used to implement new template types.
  # The constructor for the class must take a Site instance
  # as a parameter, and the class must implement a #render method that
  # has the following signature
  # def render(section, layout, template, assigns ={}, controller = nil)
  # and return the rendered template as a string.
  def self.register_template_handler(extension, klass)
    @@template_handlers[extension] = klass
  end
  register_template_handler(".liquid", Mephisto::Liquid::LiquidTemplate)

  def self.extensions
    @@template_handlers.keys
  end

  has_many  :sections, :order => "position", :dependent => :destroy do
    def home
      find_by_path ''
    end

    # orders sections in a site
    def order!(*sorted_ids)
      transaction do
        sorted_ids.flatten.each_with_index do |section_id, pos|
          Section.update_all ['position = ?', pos], ['id = ? and site_id = ?', section_id, proxy_owner.id]
        end
      end
    end
  end

  has_many  :articles, :dependent => :destroy do
    def find_by_permalink(options)
      conditions = 
        returning ["(contents.published_at IS NOT NULL AND contents.published_at <= ?)", Time.now.utc] do |cond|
          if options[:year]
            from, to = Time.delta(options[:year], options[:month], options[:day])
            cond.first << ' AND (contents.published_at BETWEEN ? AND ?)'
            cond << from << to
          end
          
          [:id, :permalink].each do |attr|
            if options[attr]
              cond.first << " AND (contents.#{attr} = ?)"
              cond << options[attr]
            end
          end
        end
      
      find :first, :conditions => conditions, :order => 'published_at desc'
    end
  end
  
  has_many  :comments, :order => 'comments.created_at desc', :dependent => :delete_all
  
  has_many  :events, :dependent => :destroy
  
  has_many  :cached_pages, :dependent => :destroy
  
  has_many  :assets, :order => 'created_at desc', :conditions => 'parent_id is null', :dependent => :destroy

  has_many :memberships, :dependent => :destroy
  has_many :members, :through => :memberships, :source => :user
  has_many :admins,  :through => :memberships, :source => :user, :conditions => ['memberships.admin = ? or users.admin = ?', true, true]

  before_validation :downcase_host
  before_validation :set_default_attributes
  validates_presence_of :permalink_style, :search_path, :tag_path
  validates_format_of     :search_path, :tag_path, :with => Format::STRING
  validates_format_of     :host, :with => Format::DOMAIN
  validates_uniqueness_of :host
  validate :check_permalink_style
  
  after_create :setup_site_theme_directories
  after_create { |site| site.sections.create(:name => 'Home') }
  before_destroy :flush_cache_and_remove_site_directories    

  with_options :order => 'contents.created_at DESC', :class_name => 'Comment' do |comment|
    comment.has_many :comments,            :conditions => ['contents.approved = ?', true]
    comment.has_many :unapproved_comments, :conditions => ['contents.approved = ? or contents.approved is null', false]
    comment.has_many :all_comments
  end
  
  def self.search_by_host_or_title(search_string)
    conditions = search_string.blank? ? nil : ["host LIKE ? OR title LIKE ?"] + ["%#{search_string}%"] * 2
    with_scope( :find => { :conditions => conditions } ) do
      yield
    end
  end

  def users(options = {})
    User.find_all_by_site self, options
  end
  
  def users_with_deleted(options = {})
    User.find_all_by_site_with_deleted self, options
  end
  
  def user(id)
    User.find_by_site self, id
  end
  
  def user_with_deleted(id)
    User.find_by_site_with_deleted self, id
  end

  def user_by_token(token)
    User.find_by_token(self, token)
  end
  
  def user_by_email(email)
    User.find_by_email(self, email)
  end

  def tags
    Tag.find(:all, :select      => "DISTINCT tags.name",
                   :joins       => "INNER JOIN taggings ON taggings.tag_id = tags.id INNER JOIN contents ON (taggings.taggable_id = contents.id AND 
                                    taggings.taggable_type = 'Content')",
                   :conditions  => ['contents.type = ? AND contents.site_id = ?', 'Article', id],
                   :order       => 'tags.name')
  end

  def theme_path
    @theme_path ||= self.class.theme_path + "site-#{id}"
  end

  def attachment_path
    theme.path
  end

  def themes
    return @themes unless @themes.nil?
    @themes = []
    FileUtils.mkdir_p theme_path
    Dir.foreach theme_path do |e|
      next if e.first == '.'
      entry = theme_path + e
      next unless entry.directory?
      @themes << Theme.new(entry, self)
    end
    def @themes.[](key) key = key.to_s ; detect { |t| t.name == key } ; end
    @themes.sort! {|a,b| a.name <=> b.name}
  end

  def theme
    @theme ||= themes[current_theme_path] || themes.first || raise(MissingThemesError.new(self))
  end

  def change_theme_to(new_theme_path)
    new_theme = (new_theme_path.is_a?(Theme) ? new_theme_path : themes[new_theme_path]) || raise("No theme '#{new_theme_path}' found")
    update_attribute :current_theme_path, new_theme.path.basename.to_s
    @theme = nil
    theme
  end

  def import_theme(zip_file, name)
    imported_name = Theme.import zip_file, :to => theme_path + name
    @theme = @themes = @rollback_theme = nil
    themes[imported_name]
  end

  def move_theme(theme, new_name)
    FileUtils.move theme.base_path, theme_path + new_name
  end

  [:attachments, :templates, :resources].each { |m| delegate m, :to => :theme }

  def permalink_for(article)
    Mephisto::Dispatcher.build_permalink_with(permalink_style, article)
  end

  def search_url(query, page = nil)
    "/#{search_path}?q=#{CGI::escapeHTML(query)}#{%(&amp;page=#{CGI::escapeHTML(page.to_s)}) unless page.blank?}"
  end

  def tag_url(*tags)
    ['', tag_path, *tags.collect { |t| URI::escape(t.to_s) }] * '/' 
  end

  def accept_comments?
    comment_age.to_i > -1
  end

  def call_render(section, template_type, assigns = {}, controller = nil, options = {})
    assigns.update('site' => to_liquid(section), 'mode' => template_type)
    assigns.update(default_assigns) unless default_assigns.empty?
    options.reverse_merge!(:layout => true)
    template = set_content_template(section, template_type)
    handler = @@template_handlers[theme.extension] || @@template_handlers[".liquid"]
    
    if options[:layout]
      layout = set_layout_template(section, template_type)
      handler.new(self).render(section, layout, template, assigns, controller)
    else
      handler.new(self).parse_inner_template(template, assigns, controller)
    end
  end
  
  def to_liquid(current_section = nil)
    SiteDrop.new self, current_section
  end

  composed_of :timezone, :class_name => 'TZInfo::Timezone', :mapping => %w(timezone name)

  def timezone_name
    timezone.name
  end

  def timezone_name= name
    self.timezone = TZInfo::Timezone.new(name)
  end

  def page_cache_directory
    multi_sites_enabled ? 
      (RAILS_PATH + (RAILS_ENV == 'test' ? 'tmp' : 'public') + 'cache' + host) :
      (RAILS_PATH + (RAILS_ENV == 'test' ? 'tmp/cache' : 'public'))
  end

  def expire_cached_pages(controller, log_message, pages = nil)
    controller = controller.class unless controller.is_a?(Class)
    pages ||= cached_pages.find_current(:all)
    returning cached_log_message_for(log_message, pages) do |msg|
      controller.logger.warn msg if cache_sweeper_tracing
      pages.each { |p| controller.expire_page(p.url) }
      CachedPage.expire_pages(self, pages)
    end
  end

#need non protected method for ErbTemplate - psq
  def find_preferred_template(template_type, custom_template)
    preferred = templates.find_preferred(template_type, custom_template)
    return preferred if preferred && preferred.file?
    raise MissingTemplateError.new(template_type, templates.collect_templates(template_type, custom_template).collect(&:basename))
  end

  protected
    def cached_log_message_for(log_message, pages)
      pages.inject([log_message, "Expiring #{pages.size} page(s)"]) { |msg, p| msg << " - #{p.url}" }.join("\n")
    end
  
    def permalink_variable_format?(var)
      Mephisto::Dispatcher.variable_format?(var)
    end

    def permalink_variable?(var)
      Mephisto::Dispatcher.variable?(var)
    end

    def check_permalink_style
      permalink_style.sub! /\A\//, ''
      permalink_style.sub! /\/\z/, ''
      pieces = permalink_style.split('/')
      errors.add :permalink_style, 'cannot have blank paths' if pieces.any?(&:blank?)
      pieces.each do |p|
        errors.add :permalink_style, "cannot contain '#{p}' variable" unless p.blank? || permalink_variable_format?(p).nil? || permalink_variable?(p)
      end
      unless pieces.include?(':id') || pieces.include?(':permalink')
        errors.add :permalink_style, "must contain either :permalink or :id"
      end
      if !pieces.include?(':year') && (pieces.include?(':month') || pieces.include?(':day'))
        errors.add :permalink_style, "must contain :year for any date-based permalinks"
      end
    end

    def downcase_host
      self.host = host.to_s.downcase
    end

    def set_default_attributes
      self.permalink_style = ':year/:month/:day/:permalink' if permalink_style.blank?
      self.search_path     = 'search' if search_path.blank?
      self.tag_path        = 'tags'   if tag_path.blank?
      [:permalink_style, :search_path, :tag_path].each { |a| send(a).downcase! }
      self.timezone_name = 'UTC' if read_attribute(:timezone).blank?
      if new_record?
        self.approve_comments = false unless approve_comments?
        self.comment_age      = 30    unless comment_age
      end
      true
    end
    
    def set_content_template(section, template_type)
      preferred_template = 
        case template_type
          when :page, :section
            template_type = :single if template_type == :page
            section.template
          when :archive
            section.archive_template
        end
      find_preferred_template(template_type, preferred_template)
    end
    
    def set_layout_template(section, template_type)
      layout_template =
        if section
          section.layout
        else
          case template_type
            when :tag    then tag_layout
            when :search then sections.detect(&:home?).layout
          end
        end
      find_preferred_template(:layout, layout_template)
    end

    private
    
    def setup_site_theme_directories
      begin
        current_theme_path = self.theme_path + "simpla"
        FileUtils.mkdir_p(self.theme_path)
        FileUtils.cp_r("#{RAILS_ROOT}/app/themes/default", current_theme_path)
        Dir[File.join(current_theme_path, '**/.svn')].each do |dir|
          FileUtils.rm_rf dir
        end
      rescue
        logger.error "ERROR: removing directories for site #{self.host}, check file permissions."
        errors.add_to_base "Unable to create theme directories."
        false
      end
    end

    def flush_cache_and_remove_site_directories
      begin
        CachedPage.expire_pages self, self.cached_pages
        FileUtils.rm_rf(self.theme_path)
        FileUtils.rm_rf("#{RAILS_ROOT}/public/cache/#{self.host}")
      rescue
        logger.error "ERROR: removing directories for site #{self.host}, check file permissions."
        false
      end
    end

end
