=begin rdoc
A Node is the root class of all elements in the zena application. Class inheritance diagram:


FIXME: some parts are not correct (Partial, Task, Request, Milestone). Either correct this tree or add these classes.
Node (manages access and publication cycle)
  |
  +-- Page (web pages)
  |     |
  |     +--- Project (has it's own project_id. Can contain notes, collaborators, etc)
  |     |
  |     +--- Section (has it's own section_id = group of pages)
  |            |
  |            +--- Skin (theme: contains css, templates, etc)
  |
  +--- Document
  |      |
  |      +--- Image
  |      |
  |      +--- TextDocument       (for css, scripts)
  |             |
  |             +--- Partial     (uses the zafu templating language)
  |                    |
  |                    +--- Template  (entry for rendering)
  |
  +-- Note (date related information, event)
  |     |
  |     +--- Post (blog entry)
  |     |
  |     +--- Task
  |     |      |
  |     |      +--- Letter
  |     |      |
  |     |      +--- Request
  |     |             |
  |     |             +--- Bug
  |     |
  |     +--- Milestone
  |
  +-- Reference
        |
        +-- Contact (address, name, phone)

=== Node, Version and Content

The +nodes+ table only holds columns to secure the access. This table does not hold every possible data for every sub-class of Node. The text data is stored into the +versions+ table and any other specific content goes in its own table (+document_contents+ for example). This is an example of how an Image is stored :

Node         o-----------   Version   o---------  Content
dgroup_id                   title                 width
wgroup_id                   text                  height
user_id                     summary               content_type
...                         ...                   ...

=== Acessing version and content data

To ease the work to set/retrieve the data from the version and or content, we use some special notation. This notation abstracts this Node/Version/Content structure so you can use a version's attribute as if it was in the node directly.

TODO: DOC removed (was out of sync)

=== Dynamic attributes

The Version class uses dynamic attributes. These let you add any attribute you like to the versions (see DynAttribute for details). These attributes can be accessed by using the +d_+ prefix :

 @node.d_whatever  ===> @node.version.dyn[:whatever]

=== Attributes

Each node uses the following basic attributes:

Base attributes:

zip:: unique id (incremented in each site's scope).
name:: used to build the node's url when 'custom_base' is set. Used for document names.
site_id:: site to which this node belongs to.
parent_id:: parent node (every node except root is inserted in a unique place through this attribute).
user_id:: owner of the node.
ref_lang:: original node language.
created_at:: creation date.
updated_at:: modification date.
custom_base:: boolean value. When set to true, the node's url becomes it's fullpath. All it descendants will use this node's fullpath as their base url. See below for an example.
inherit:: inheritance mode (0=custom, 1=inherit, -1=private).

Attributes inherited from the parent:
section_id:: reference project (cannot be overwritten even if inheritance mode is custom).
rgroup_id:: id of the readers group.
wgroup_id:: id of the writers group.
dgroup_id:: id of the publishers group.
skin:: name of theSkin to use when rendering the pate ('theme').

Attributes used internally:
publish_from:: earliest publication date from all published versions.
kpath:: inheritance hierarchy. For example an Image has 'NPDI' (Node, Page, Document, Image), a Letter would have 'NNTL' (Node, Note, Task. Letter). This is used to optimize sql queries.
fullpath:: cached full path made of ancestors' names (<gdparent name>/<parent name>/<self name>).
basepath:: cached base path (the base path is used to build the url depending on the 'custom_base' flag).

=== Node url
A node's url is made of it's class and +zip+. For the examples below, this is our site tree:
 root
   |
   +--- projects (Page)
           |
           +--- worldTour (Project)
           |      |
           |      +--- photos (Page)
           |
           +--- music (Project)

The worldTour project's url would look like:
 /en/project21.html

The 'photos' url would be:
 /en/page23.html

When custom base is set (only for descendants of Page), worldTour url becomes its fullpath:
 /en/projects/worldTour

and the 'photos' url is now in the worldTour project's basepath:
 /en/projects/worldTour/page23.html

Setting 'custom_base' on a node should be done with caution as the node's zip is on longer in the url and when you move the node around, there is no way to find the new location from the old url. Custom_base should therefore only be used for nodes that are not going to move.
=end
class Node < ActiveRecord::Base
  extend Zena::Use::Upload::UploadedFile
  extend Zena::Use::Search::NodeClassMethods

  include RubyLess::SafeClass
  safe_attribute :created_at, :updated_at, :event_at, :log_at, :publish_from, :basepath, :inherit

  # we use safe_method because the columns can be null, but the values are never null
  safe_method   :name => String, :kpath => String, :user_zip => Number, :parent_zip => Number,
                :project_zip => Number, :section_zip => Number, :skin => String, :ref_lang => String,
                :fullpath => String, :rootpath => String, :position => Number, :rgroup_id => Number,
                :wgroup_id => Number, :dgroup_id => Number, :custom_base => Boolean, :klass => String,
                :score => Number, :comments_count => Number,
                :custom_a => Number, :custom_b => Number,
                :m_text => String, :m_title => String, :m_author => String,
                :zip => Number
  # FIXME: remove 'zip' and use :id => {:class => Number, :method => 'zip'}
  # same with parent_zip, section_zip, etc...

  #attr_accessible    :version_content
  has_many           :discussions, :dependent => :destroy
  has_many           :links
  has_and_belongs_to_many :cached_pages
  belongs_to         :virtual_class, :foreign_key => 'vclass_id'
  belongs_to         :site
  before_validation  :set_defaults
  before_validation  :node_before_validation
  validates_presence_of :name
  validate           :validate_node
  before_create      :node_before_create
  before_save        :change_klass
  after_save         :spread_project_and_section
  after_save         :rebuild_children_fullpath
  after_create       :node_after_create
  attr_protected     :site_id, :zip, :id, :section_id, :project_id, :publish_from
  attr_protected     :site_id

  include Zena::Use::Dates::ModelMethods
  parse_date_attribute :event_at, :log_at

  include Zena::Use::NestedAttributesAlias::ModelMethods
  nested_attributes_alias %r{^v_(\w+)} => ['version']
  nested_attributes_alias %r{^c_(\w+)} => ['version', 'content']
  nested_attributes_alias %r{^d_(\w+)} => ['version', 'dyn']

  safe_context       :author => 'Contact', :parent => 'Node',
                     :project => 'Project', :section => 'Section',
                     :real_project => 'Project', :real_section => 'Section',
                     :user => 'User', :version => 'Version', :comments => ['Comment'],
                     :data   => {:class => ['DataEntry'], :data_root => 'node_a'},
                     :data_a => {:class => ['DataEntry'], :data_root => 'node_a'},
                     :data_b => {:class => ['DataEntry'], :data_root => 'node_b'},
                     :data_c => {:class => ['DataEntry'], :data_root => 'node_c'},
                     :data_d => {:class => ['DataEntry'], :data_root => 'node_d'}

  extend  Zena::Acts::SecureNode
  extend  Zena::Acts::Multiversion
  include Zena::Use::Relations::ModelMethods

  acts_as_secure_node
  acts_as_multiversioned
  use_node_query

  @@native_node_classes = {'N' => self}
  @@unhandled_children  = []
  class << self

    # needed for compatibility with virtual classes
    alias create_instance create
    alias new_instance new

    def inherited(child)
      super
      @@unhandled_children << child
    end

    # Return the list of (kpath,subclasses) for the current class.
    def native_classes
      # this is to make sure subclasses are loaded before the first call
      [Note,Page,Project,Section,Reference,Contact,Document,Image,TextDocument,Skin,Template]
      while child = @@unhandled_children.pop
        @@native_node_classes[child.kpath] = child
      end
      @@native_node_classes.reject{|kpath,klass| !(kpath =~ /^#{self.kpath}/) }
    end

    # check inheritance chain through kpath
    def kpath_match?(kpath)
      self.kpath =~ /^#{kpath}/
    end

    # Class list to which this class can change to
    def change_to_classes_for_form
      classes_for_form(:class => 'Node', :without => 'Document, Contact')
    end

    # List of classes that a node can change to.
    def allowed_change_to_classes
      change_to_classes_for_form.map {|k,v| v}
    end

    # FIXME: how to make sure all sub-classes of Node are loaded before this is called ?
    def classes_for_form(opts={})
      if klass = opts.delete(:class)
        if klass = get_class(klass)
          klass.classes_for_form(opts)
        else
          return ['', ''] # bad class
        end
      else
        all_classes(opts).map{|a,b| [a[0..-1].sub(/^#{self.kpath}/,'').gsub(/./,'  ') + b.to_s, b.to_s] } # white spaces are insecable spaces (not ' ')
      end
    end

    # FIXME: how to make sure all sub-classes of Node are loaded before this is called ?
    def kpaths_for_form(opts={})
      all_classes(opts).map{|a,b| [a[1..-1].gsub(/./,'  ') + b.to_s, a.to_s] } # white spaces are insecable spaces (not ' ')
    end

    def all_classes(opts={})
      virtual_classes = VirtualClass.find(:all, :conditions => ["site_id = ? AND create_group_id IN (?) AND kpath LIKE '#{self.kpath}%'", current_site[:id], visitor.group_ids])
      classes = (virtual_classes.map{|r| [r.kpath, r.name]} + native_classes.to_a).sort{|a,b| a[0] <=> b[0]}
      if opts[:without]
        reject_kpath =  opts[:without].split(',').map(&:strip).map {|name| Node.get_class(name) }.compact.map { |kla| kla.kpath }.join('|')
        classes.reject! {|k,c| k =~ /^#{reject_kpath}/ }
      end
      classes
    end

    # Return class or virtual class from name.
    def get_class(rel, opts={})
      class_name = rel.singularize.camelize # mushroom_types ==> MushroomType
      begin
        klass = Module.const_get(class_name)
        raise NameError unless klass.ancestors.include?(Node)
      rescue NameError
        # find the virtual class
        if opts[:create]
          klass = VirtualClass.find(:first, :conditions=>["site_id = ? AND create_group_id IN (?) AND name = ?",current_site[:id], visitor.group_ids, class_name])
        else
          klass = VirtualClass.find(:first, :conditions=>["site_id = ? AND name = ?",current_site[:id], class_name])
        end
      end
      klass
    end

    # Return a new object of the class name or nil if the class name does not exist.
    def new_from_class(rel)
      if k = get_class(rel, :create => true)
        k.new
      else
        nil
      end
    end

    def get_class_from_kpath(kp)
      native_classes[kp] || VirtualClass.find(:first, :conditions=>["site_id = ? AND kpath = ?",current_site[:id], kp])
    end

    # Find a node's attribute based on a pseudo (id or path). Used by zazen to create a link for ""::art or "":(people/ant) for example.
    def translate_pseudo_id(id, sym = :id, base_node = nil)
      if id.to_s =~ /\A(-?)(\d+)\Z/
        # zip
        # FIXME: this is not secure
        res = Zena::Db.fetch_row("SELECT #{sym} FROM nodes WHERE site_id = #{current_site[:id]} AND zip = '#{$2}'")
        res ? ($1.blank? ? res.to_i : -res.to_i) : nil
      elsif node = find_node_by_pseudo(id,base_node)
        node[sym]
      else
        nil
      end
    end

    # Find a node based on a query shortcut. Used by zazen to create a link for ""::art for example.
    def find_node_by_pseudo(id, base_node = nil)
      raise Zena::AccessViolation if self.scoped_methods == []
      str = id.to_s
      if str =~ /\A\d+\Z/
        # zip
        find_by_zip(str)
      elsif str =~ /\A:?([0-9a-zA-Z ]+)(\+*)\Z/
        offset = $2.to_s.size
        Node.search_records($1.gsub('-',' '), :offset => offset, :limit => 1).first
      elsif path = str[/\A\(([^\)]*)\)\Z/,1]
        if path[0..0] == '/'
          find_by_path(path[1..-1])
        elsif base_node
          find_by_path(path.abs_path(base_node.fullpath))
        else
          # do not use (path) pseudo when there is no base_node (during create_or_update_node for example).
          # FIXME: path pseudo is needed for links... and it should be done here (egg and hen problem)
          nil
        end
      end
    end

    # def attr_public?(attribute)
    #   if attribute.to_s =~ /(.*)_zips?$/
    #     return true if self.ancestors.include?(Node) && RelationProxy.find_by_role($1.singularize)
    #   end
    #   super
    # end

    def create_or_update_node(new_attributes)
      attributes = transform_attributes(new_attributes)
      unless attributes['name'] && attributes['parent_id']
        node = Node.new
        node.errors.add('name', "can't be blank") unless attributes['name']
        node.errors.add('parent_id', "can't be blank") unless attributes['parent_id']
        return node
      end

      begin
        klass = Node.get_class(attributes['klass'] || 'Node')
        klass = klass.real_class if klass.kind_of?(VirtualClass)
      rescue NameError
        klass = Node
      end

      # FIXME: remove 'with_exclusive_scope' once scopes are clarified and removed from 'secure'
      node = klass.send(:with_exclusive_scope) do
        klass.find(:first, :conditions => ['site_id = ? AND name = ? AND parent_id = ?',
                                          current_site[:id], attributes['name'].url_name, attributes['parent_id']])
      end

      if node
        visitor.visit(node) # secure
        # TODO: class ignored (could be used to transform from one class to another...)
        attributes.delete('class')
        attributes.delete('klass')
        updated_date = node.updated_at
        node.update_attributes(attributes)

        if updated_date != node.updated_at
          node[:create_or_update] = 'updated'
        else
          node[:create_or_update] = 'same'
        end
      else
        node = create_node(new_attributes)
        node[:create_or_update] = 'new'
      end

      node
    end

    # TODO: cleanup and rename with something indicating the attrs cleanup that this method does.
    def create_node(new_attributes)
      attributes = transform_attributes(new_attributes)

      # TODO: replace this hack with a proper class method 'secure' behaving like the
      # instance method. It would get the visitor and scope from the same hack below.
      scope   = self.scoped_methods[0] || {}

      klass_name   = attributes.delete('class') || attributes.delete('klass') || 'Page'
      unless klass = get_class(klass_name, :create => true)
        node = Node.new
        node.instance_eval { @attributes = attributes }
        node.errors.add('klass', 'invalid')
        # This is to show the klass in the form seizure
        node.instance_variable_set(:@klass, klass_name.to_s)
        def node.klass; @klass; end
        return node
      end

      node = if klass != self
        # FIXME: remove 'with_exclusive_scope' once scopes are clarified and removed from 'secure'
        klass.send(:with_exclusive_scope, scope) { klass.create_instance(attributes) }
      else
        self.create_instance(attributes)
      end

      node
    end

    # Create new nodes from the data in a folder or archive.
    def create_nodes_from_folder(opts)
      # TODO: all this method needs cleaning, it's a mess.
      return [] unless (opts[:folder] || opts[:archive]) && (opts[:parent] || opts[:parent_id])
      scope = self.scoped_methods[0] || {}
      parent_id = opts[:parent_id] || opts[:parent][:id]
      folder    = opts[:folder]
      defaults  = (opts[:defaults] || {}).stringify_keys
      klass     = opts[:klass] || "Page"
      res       = {}

      # create from archive
      unless folder
        archive = opts[:archive]
        n       = 0
        while true
          folder = File.join(RAILS_ROOT, 'tmp', sprintf('%s.%d.%d', 'import', $$, n))
          break unless File.exists?(folder)
        end

        begin
          FileUtils::mkpath(folder)

          if archive.kind_of?(StringIO)
            filename = archive.original_filename
            tempf = Tempfile.new(archive.original_filename)
            File.open(tempf.path, 'wb') { |f| f.syswrite(archive.read) }
            archive = tempf
          else
            filename = archive.original_filename
          end

          # extract file in this temporary folder.
          # FIXME: is there a security risk here ?
          if filename =~ /\.tgz$/
            `tar -C '#{folder}' -xz < '#{archive.path}'`
          elsif filename =~ /\.tar$/
            `tar -C '#{folder}' -x < '#{archive.path}'`
          elsif filename =~ /\.zip$/
            `unzip -d '#{folder}' '#{archive.path}'`
          elsif filename =~ /(.*)(\.gz|\.z)$/
            `gzip -d '#{archive.path}' -c > '#{folder}/#{$1.gsub("'",'')}'`
          else
            # FIXME: send errors back
            puts "BAD #{archive.inspect}"
          end
          res = create_nodes_from_folder(:folder => folder, :parent_id => parent_id, :defaults => defaults, :klass => klass)
        ensure
          FileUtils::rmtree(folder)
        end
        return res
      end

      entries = Dir.entries(folder).reject { |f| f =~ /^([\._~]|[^\w])/ }.sort
      index  = 0

      while entries[index]
        type = current_obj = sub_folder = document_path = nil
        versions = []
        filename = entries[index]

        path     = File.join(folder, filename)

        if File.stat(path).directory?
          type   = :folder
          name   = filename
          sub_folder = path
          attrs = defaults.dup
        elsif filename =~ /^(.+?)(\.\w\w|)(\.\d+|)\.zml$/  # bird.jpg.en.zml
          # node content in yaml
          type   = :node
          name   = "#{$1}#{$4}"
          lang   = $2.blank? ? nil : $2[1..-1]

          # no need for base_node (this is done after all with parse_assets in the controller)
          attrs  = defaults.merge(get_attributes_from_yaml(path))
          attrs['name']     = name
          attrs['v_lang']   = lang || attrs['v_lang'] || visitor.lang
          versions << attrs
        elsif filename =~ /^((.+?)\.(.+?))(\.\w\w|)(\.\d+|)$/ # bird.jpg.en
          type   = :document
          name   = $1
          attrs  = defaults.dup
          lang   = $4.blank? ? nil : $4[1..-1]
          attrs['v_lang'] = lang || attrs['v_lang'] || visitor.lang
          attrs['c_ext']  = $3
          document_path   = path
        end

        index += 1
        while entries[index] =~ /^#{name}(\.\w\w|)(\.\d+|)\.zml$/ # bird.jpg.en.zml
          lang   = $1.blank? ? visitor.lang : $1[1..-1]
          path   = File.join(folder,entries[index])

          # we have a zml file. Create a version with this file
          # no need for base_node (this is done after all with parse_assets in the controller)
          attrs = defaults.merge(get_attributes_from_yaml(path))
          attrs['name']     = name
          attrs['v_lang'] ||= lang
          versions << attrs

          index += 1
        end

        if versions.empty?
          if type == :folder
            # minimal node for a folder
            attrs['name']     = name
            attrs['v_lang'] ||= lang
            attrs['class']    = klass
            versions << attrs
          elsif type == :document
            # minimal node for a document
            attrs['name']     = name
            attrs['v_lang'] ||= lang
            versions << attrs
          end
        end

        new_object = false
        versions.each do |attrs|
          # FIXME: same lang: remove before update current_obj.remove if current_obj.v_lang == attrs['v_lang'] && current_obj.v_status != Zena::Status[:red]
          # FIXME: current_obj.publish if attrs['v_status'].to_i == Zena::Status[:pub]
          if type == :document
            attrs['name' ] = attrs['name'].split('.')[0..-2].join('.')
            if document_path
              attrs['c_ext'] ||= document_path.split('.').last
              # file
              insert_zafu_headings = false
              if opts[:parent_class] == 'Skin' && ['html','xhtml'].include?(attrs['c_ext']) && attrs['name'] == 'index'
                attrs['c_ext']    = 'zafu'
                attrs['name']     = 'Node'
                insert_zafu_headings = true
              end

              ctype = Zena::EXT_TO_TYPE[attrs['c_ext']]
              ctype = ctype ? ctype[0] : "application/octet-stream"
              attrs['c_content_type'] = ctype


              File.open(document_path) do |f|
                file = uploaded_file(f, filename, ctype)
                (class << file; self; end;).class_eval do
                  alias o_read read
                  define_method(:read) do
                    if insert_zafu_headings
                      o_read.sub(%r{</head>},"  <r:stylesheets/>\n  <r:javascripts/>\n  <r:uses_datebox/>\n</head>")
                    else
                      o_read
                    end
                  end
                end
                current_obj = create_or_update_node(attrs.merge(:c_file => file, :klass => 'Document', :_parent_id => parent_id))
              end
              document_path = nil
            else
              current_obj = create_or_update_node(attrs.merge(:_parent_id => parent_id, :klass => 'Document'))
            end
          else
            # :folder, :node
            current_obj = create_or_update_node(attrs.merge(:_parent_id => parent_id))
          end
          new_object = new_object || current_obj.instance_variable_get(:@new_record_before_save)
        end
        current_obj.instance_variable_set(:@new_record_before_save, new_object)

        current_obj.instance_variable_set(:@versions_count, versions.size)
        res[current_obj[:id].to_i] = current_obj

        res.merge!(create_nodes_from_folder(:folder => sub_folder, :parent_id => current_obj[:id], :defaults => defaults, :parent_class => opts[:klass])) if sub_folder && !current_obj.new_record?
      end
      res
    end

    def find_by_zip(zip)
      node = find(:first, :conditions=>"zip = #{zip.to_i}")
      raise ActiveRecord::RecordNotFound unless node
      node
    end

    # Find a node by it's full path. Cache 'fullpath' if found. This is useless now
    # that fullpath is properly cached. REMOVE !
    def find_by_path(path)
      return nil unless scope = scoped_methods[0]
      return nil unless scope[:find]   # not secured find. refuse.
      self.find_by_fullpath(path)
    end

    # FIXME: Where is this used ?
    def class_for_relation(rel)
      case rel
      when 'author'
        User
      when 'traductions'
        Version
      when 'versions'
        Version
      else
        Node
      end
    end

    def plural_relation?(rel)
      rel = rel.split(/\s/).first
      if ['root', 'parent', 'self', 'children', 'documents_only', 'all_pages'].include?(rel) || Node.get_class(rel)
        rel.pluralize == rel
      elsif rel =~ /\A\d+\Z/
        false
      else
        relation = RelationProxy.find_by_role(rel.singularize)
        return rel =~ /s$/ unless relation
        relation.target_role == rel.singularize ? !relation.target_unique : !relation.source_unique
      end
    end

    # Translate attributes from the visitor's reference to the application.
    # This method translates dates, zazen shortcuts and zips and returns a stringified hash.
    def transform_attributes(new_attributes, base_node = nil)
      res = {}
      res['parent_id'] = new_attributes[:_parent_id] if new_attributes[:_parent_id] # real id set inside zena.

      attributes = new_attributes.stringify_keys

      if attributes['copy'] || attributes['copy_id']
        copy_node = attributes.delete('copy')
        copy_node ||= Node.find_by_zip(attributes.delete('copy_id'))
        attributes = copy_node.replace_attributes_in_values(attributes)
      end

      if !res['parent_id'] && p = attributes['parent_id']
        res['parent_id'] = Node.translate_pseudo_id(p, :id, base_node) || p
      end

      attributes.keys.each do |key|
        next if ['_parent_id', 'parent_id'].include?(key)

        if ['rgroup_id', 'wgroup_id', 'dgroup_id'].include?(key)
          res[key] = Group.translate_pseudo_id(attributes[key], :id) || attributes[key]
        elsif ['rgroup', 'wgroup', 'dgroup'].include?(key)
          res["#{key}_id"] = Group.translate_pseudo_id(attributes[key], :id) || attributes[key]
        elsif ['user_id'].include?(key)
          res[key] = User.translate_pseudo_id(attributes[key], :id) || attributes[key]
        elsif ['date'].include?(key)
          # FIXME: this is a temporary hack because date in links do not support timezones/formats properly
          if attributes[key].kind_of?(Time)
            res[key] = attributes[key]
          elsif attributes[key]
            # parse date
            res[key] = attributes[key].to_utc("%Y-%m-%d %H:%M:%S")
          end
        elsif key =~ /^(\w+)_id$/
          if key[0..1] == 'd_'
            res[key] = Node.translate_pseudo_id(attributes[key], :zip, base_node) || attributes[key]
          else
            res[key] = Node.translate_pseudo_id(attributes[key],  :id, base_node) || attributes[key]
          end
        elsif key =~ /^(\w+)_ids$/
          # Id list. Bad ids are removed.
          values = attributes[key].kind_of?(Array) ? attributes[key] : attributes[key].split(',')
          if key[0..1] == 'd_'
            values.map! {|v| Node.translate_pseudo_id(v, :zip, base_node) }
          else
            values.map! {|v| Node.translate_pseudo_id(v,  :id, base_node) }
          end
          res[key] = values.compact
        elsif key == 'file'
          unless attributes[key].blank?
            res[key] = attributes[key]
          end
        elsif attributes[key].kind_of?(Hash)
          res[key] = transform_attributes(attributes[key], base_node)
        else
          # translate zazen
          value = attributes[key]
          if value.kind_of?(String)
            # FIXME: ignore if 'v_text' of a TextDocument...
            res[key] = ZazenParser.new(value,:helper=>self).render(:translate_ids=>:zip, :node=>base_node)
          else
            res[key] = value
          end
        end
      end

      res
    end

    def get_attributes_from_yaml(filepath, base_node = nil)
      attributes = YAML::load( File.read( filepath ) )
      attributes.delete(:_parent_id)
      transform_attributes(attributes, base_node)
    end

    def safe_method_type(signature)
      if signature.size > 1
        RubyLess::SafeClass.safe_method_type_for(self, signature)
      else
        method = signature.first
        # if model_names = nested_model_names_for_alias(method)
        #   # ...
        # end
        case method[0..1]
        when 'v_'
          method = method[2..-1]
          if type = version_class.safe_method_type([method])
            type.merge(:method => "version.#{type[:method]}")
          else
            # might be readable by sub-classes
            # what is the expected return type ?
            {:method => "version.safe_read(#{method.inspect})", :nil => true, :class => String}
          end
        when 'c_'
          method = method[2..-1]
          klass = version_class.content_class
          if klass && type = klass.safe_method_type([method])
            type.merge(:method => "version.content.#{type[:method]}")
          else
            {:method => "version.safe_content_read(#{method.inspect})", :nil => true, :class => String}
          end
        when 'd_'
          {:method => "version.dyn[#{method[2..-1].inspect}]", :nil => true, :class => String}
        else
          if method =~ /^(.+)_((id|zip|status|comment)(s?))\Z/ && !instance_methods.include?(method)
            {:method => "rel[#{$1.inspect}].try(:other_#{$2})", :nil => true, :class => ($4.blank? ? Number : [Number])}
          else
            RubyLess::SafeClass.safe_method_type_for(self, signature)
          end
        end
      end
    end

    # Return a safe string to access node attributes in compiled templates and compiled sql.
    def zafu_attribute(node, attribute)
      if node.kind_of?(String)
        raise Exception.new("You should use safe_method_type...")
      else
        node.safe_read(attribute)
      end
    end


    def auto_create_discussion
      false
    end
  end

  # TODO: remove when :inverse_of works.
  def versions_with_secure(*args)
    proxy = versions_without_secure(*args)
    if frozen?
      proxy = []
    elsif proxy.loaded?
      proxy.each do |v|
        v.node = self
      end
    end
    proxy
  end
  alias_method_chain :versions, :secure

  # Additional security so that unsecure finders explode when trying to update/save or follow relations.
  # def visitor
  #   return @visitor if @visitor
  #   @visitor = Thread.current[:visitor] || Zena::RecordNotSecured.new("Visitor not set, record not secured.")
  #   # We need to be more tolerant during object creation since 'v_foo' can be
  #   # set before 'visitor' and we need visitor.lang when creating versions.
  #   #return Thread.current[:visitor] #if new_record?
  #   #raise Zena::RecordNotSecured.new("Visitor not set, record not secured.")
  # end

  # Return an attribute if it is safe (RubyLess allowed). Return nil otherwise.
  # This is mostly used when the zafu compiler cannot decide whether a method is safe or not at compile time.
  def safe_read(attribute)
    case attribute[0..1]
    when 'v_'
      version.safe_read(attribute[2..-1])
    when 'c_'
      version.safe_content_read(attribute[2..-1])
    when 'd_'
      version.dyn[attribute[2..-1]]
    else
      if @attributes.has_key?(attribute)             &&
         !self.class.column_names.include?(attribute) &&
         !methods.include?(attribute)                 &&
         !self.class.safe_method_type([attribute])
      # db fetch only: select 'created_at AS age' ----> 'age' can be read
        @attributes[attribute]
      else
        super
      end
    end
  end

  # check inheritance chain through kpath
  def kpath_match?(kpath)
    vclass.kpath =~ /^#{kpath}/
  end

  # virtual class
  def vclass
    virtual_class || self.class
  end

  def klass
    @new_klass || @set_klass || vclass.to_s
  end

  def dyn_attribute_keys
    (version.dyn.keys + (virtual_class ? virtual_class.dyn_keys.to_s.split(',').map(&:strip) : [])).uniq.sort
  end

  def klass=(str)
    return if str == klass
    @new_klass = str
  end

  # include virtual classes to check inheritance chain
  def vkind_of?(klass)
    if self.class.ancestors.map{|k| k.to_s}.include?(klass)
      true
    elsif virt = VirtualClass.find(:first, :conditions=>["site_id = ? AND name = ?",current_site[:id], klass])
      kpath_match?(virt.kpath)
    end
  end

  # Update a node's attributes, transforming the attributes first from the visitor's context to Node context.
  def update_attributes_with_transformation(new_attributes)
    update_attributes(secure(Node) {Node.transform_attributes(new_attributes, self)})
  end

  # Replace [id], [v_title], etc in attributes values
  def replace_attributes_in_values(hash)
    hash.each do |k,v|
      v.gsub!(/\[([^\]]+)\]/) do
        attribute = $1
        real_attribute = attribute =~ /\Ad_/ ? attribute : attribute.gsub(/\A(|[\w_]+)id(s?)\Z/, '\1zip\2')
        Node.zafu_attribute(self, real_attribute)
      end
    end
  end


  # Parse text content and replace all relative urls ('../projects/art') by ids ('34')
  def parse_assets(text, helper, key)
    # helper is used in textdocuments
    ZazenParser.new(text,:helper=>helper).render(:translate_ids => :zip, :node => self)
  end

  # Parse text and replace ids '!30!' by their pseudo path '!(img/bird)!'
  def unparse_assets(text, helper, key)
    ZazenParser.new(text,:helper=>helper).render(:translate_ids => :relative_path, :node=>self)
  end

  # Return the list of ancestors (without self): [root, obj, obj]
  # ancestors to which the visitor has no access are removed from the list
  def ancestors(start=[])
    raise Zena::InvalidRecord, "Infinit loop in 'ancestors' (#{start.inspect} --> #{self[:id]})" if start.include?(self[:id])
    start += [self[:id]]
    if self[:id] == current_site[:root_id]
      []
    elsif self[:parent_id].nil?
      []
    else
      parent = @parent || Node.find(self[:parent_id])
      parent.visitor = visitor
      if parent.can_read?
        parent.ancestors(start) + [parent]
      else
        parent.ancestors(start)
      end
    end
  end


  # url base path. If a node is in 'projects' and projects has custom_base set, the
  # node's basepath becomes 'projects', so the url will be 'projects/node34.html'.
  # The basepath is cached. If rebuild is set to true, the cache is updated.
  def basepath
    self[:basepath]
  end

  # Same as fullpath, but the path includes the root node.
  def rootpath
    current_site.name + (fullpath != "" ? "/#{fullpath}" : "")
  end

  alias path rootpath

  # Return an array with the node name and the last two parents' names.
  def short_path
    path = self.rootpath.split('/')
    if path.size > 2
      ['..'] + path[-2..-1]
    else
      path
    end
  end

  def pseudo_id(root_node, sym)
    case sym
    when :zip
      self.zip
    when :relative_path
      full = self.fullpath
      root = root_node ? root_node.fullpath : ''
      "(#{full.rel_path(root)})"
    end
  end

  # Return save path for an asset (element produced by text like a png file from LateX)
  def asset_path(asset_filename)
    # It would be nice to move this outside 'self[:id]' so that the same asset can
    # be used by many pages... But then, how do we expire unused assets ?
    "#{SITES_ROOT}#{site.data_path}/asset/#{self[:id]}/#{asset_filename}"
  end

  # Used by zafu to find the search score
  # def score
  #   self[:score]
  # end

  def all_relations
    @all_relations ||= self.vclass.all_relations(self)
  end

  # Find parent
  def parent(is_secure = true)
    # make sure the cache is in sync with 'parent_id' (used during validation)
    if self[:parent_id].nil?
      nil
    elsif is_secure
      # cache parent result (done through secure query)
      return @parent if @parent && @parent[:id] == self[:parent_id]
      @parent = secure(Node) { Node.find(self[:parent_id]) }
    else
      # not secured (inside an exclusive scope)
      return @parent_insecure if @parent_insecure && @parent_insecure[:id] == self[:parent_id]
      @parent_insecure = secure(Node, :secure => false) { Node.find(self[:parent_id]) }
    end
  end

  # Return self if the current node is a section else find section.
  def section
    self.kind_of?(Section) ? self : real_section
  end

  # Find real section
  def real_section(is_secure = true)
    return self if self[:parent_id].nil? # root
    # we cannot use Section to find because the root node behaves like a Section but is a Project.
    if is_secure
      secure(Node) { Node.find(self[:section_id]) }
    else
      secure(Node, :secure => false) { Node.find(self[:section_id]) }
    end
  end

  # Return self if the current node is a project else find project.
  def project
    self.kind_of?(Project) ? self : real_project
  end

  # Find real project (Project's project if node is a Project)
  def real_project(is_secure = true)
    return self if self[:parent_id].nil?
    if is_secure
      secure(Project) { Project.find(self[:project_id]) }
    else
      secure(Node, :secure => false) { Project.find(self[:project_id]) }
    end
  end

  # Create a child and let him inherit from rwp groups and section_id
  def new_child(opts={})
    klass = opts.delete(:class) || Page
    c = klass.new(opts)
    c.parent_id  = self[:id]
    c.instance_variable_set(:@parent, self)

    c.visitor    = visitor

    c.inherit = 1
    c.rgroup_id  = self.rgroup_id
    c.wgroup_id  = self.wgroup_id
    c.dgroup_id  = self.dgroup_id

    c.section_id = self.get_section_id
    c.project_id = self.get_project_id
    c
  end

  # ACCESSORS
  def author
    user.contact
  end

  # Find icon through a relation named 'icon' or use first image child
  def icon
    return nil if new_record?
    return @icon if defined? @icon
    query = Node.build_find(:first, ['icon group by id,l_id order by l_id desc, position asc, name asc', 'image'], :node_name => 'self')
    sql_str, uses_node_name = query.to_s, query.uses_node_name
    @icon = sql_str ? do_find(:first, eval(sql_str), :ignore_source => !uses_node_name) : nil
  end

  alias o_user user

  def user
    secure!(User) { o_user }
  end

  # Find all data entries linked to the current node
  def data
    list = DataEntry.find(:all, :conditions => "node_a_id = #{id} OR node_b_id = #{id} OR node_c_id = #{id} OR node_d_id = #{id}", :order => 'date ASC,created_at ASC')
    list == [] ? nil : list
  end

  if Node.connection.tables.include?('data_entries')
    # We need this guard during initial migration (Node loaded before data entries table is created).
    # FIXME: remove in [1.1] when we 'squash' all migrations

    DataEntry::NodeLinkSymbols.each do |sym|
      # Find data entries through a specific slot (node_a, node_b). "data_entries_a" finds all data entries link through 'node_a_id'.
      class_eval "def #{sym.to_s.gsub('node', 'data')}
        return nil if new_record?
        list = DataEntry.find(:all, :conditions=>\"#{sym}_id = '\#{self[:id]}'\")
        list == [] ? nil : list
      end"
    end
  end

  def ext
    (name && name != '' && name =~ /\./ ) ? name.split('.').last : ''
  end

  # set name: remove all accents and camelize
  def name=(str)
    return unless str && str != ""
    self[:name] = str.url_name
  end

  # Return current discussion id (used by query_builder)
  def get_discussion_id
    (discussion && !discussion.new_record?) ? discussion[:id] : '0'
  end

  # Return self[:id] if the node is a kind of Section. Return section_id otherwise.
  def get_section_id
    # root node is it's own section and project
    self[:parent_id].nil? ? self[:id] : self[:section_id]
  end

  # Return self[:id] if the node is a kind of Project. Return project_id otherwise.
  def get_project_id
    # root node is it's own section and project
    self[:parent_id].nil? ? self[:id] : self[:project_id]
  end

  # Id to zip mapping for parent_id. Used by zafu and forms.
  def parent_zip
    parent ? parent[:zip] : nil
  end

  # Id to zip mapping for section_id. Used by zafu and forms.
  def section_zip
    section[:zip]
  end

  # Id to zip mapping for project_id. Used by zafu and forms.
  def project_zip
    project[:zip]
  end

  # Id to zip mapping for user_id. Used by zafu and forms.
  def user_zip; self[:user_id]; end

  # transform to another class
  # def vclass=(new_class)
  #   if new_class.kind_of?(String)
  #     klass = Module.const_get(new_class)
  #   else
  #     klass = new_class
  #   end
  #   raise NameError if !klass.ancestors.include?(Node) || klass.version_class != self.class.content_class
  #
  #
  #
  # rescue NameError
  #   errors.add('klass', 'invalid')
  # end

  # transform an Node into another Object. This is a two step operation :
  # 1. create a new object with the attributes from the old one
  # 2. move old object out of the way (setting parent_id and section_id to -1)
  # 3. try to save new object
  # 4. delete old and set new object id to old
  # THIS IS DANGEROUS !! NEEDS TESTING
  # def change_to(klass)
  #   return nil if self[:id] == current_site[:root_id]
  #   # ==> Check for class specific information (file to remove, participations, tags, etc) ... should we leave these things and
  #   # not care ?
  #   # ==> When changing into something else : update version type and data !!!
  #   my_id = self[:id].to_i
  #   my_parent = self[:parent_id].to_i
  #   my_project = self[:section_id].to_i
  #   connection = self.class.connection
  #   # 1. create a new object with the attributes from the old one
  #   new_obj = secure!(klass) { klass.new(self.attributes) }
  #   # 2. move old object out of the way (setting parent_id and section_id to -1)
  #   self.class.connection.execute "UPDATE #{self.class.table_name} SET parent_id='0', section_id='0' WHERE id=#{my_id}"
  #   # 3. try to save new object
  #   if new_obj.save
  #     tmp_id = new_obj[:id]
  #     # 4. delete old and set new object id to old. Delete tmp Version.
  #     self.class.connection.execute "DELETE FROM #{self.class.table_name} WHERE id=#{my_id}"
  #     self.class.connection.execute "DELETE FROM #{Version.table_name} WHERE node_id=#{tmp_id}"
  #     self.class.connection.execute "UPDATE #{self.class.table_name} SET id='#{my_id}' WHERE id=#{tmp_id}"
  #     self.class.connection.execute "UPDATE #{self.class.table_name} SET section_id=id WHERE id=#{my_id}" if new_obj.kind_of?(Section)
  #     self.class.logger.info "[#{self[:id]}] #{self.class} --> #{klass}"
  #     if new_obj.kind_of?(Section)
  #       # update section_id for children
  #       sync_section(my_id)
  #     elsif self.kind_of?(Section)
  #       # update section_id for children
  #       sync_section(parent[:section_id])
  #     end
  #     secure ( klass ) { klass.find(my_id) }
  #   else
  #     # set object back
  #     self.class.connection.execute "UPDATE #{self.class.table_name} SET parent_id='#{my_parent}', section_id='#{my_project}' WHERE id=#{my_id}"
  #     self
  #   end
  # end

  # Find the discussion for the current context (v_status and v_lang). This automatically creates a new #Discussion if there is
  # no closed or open discussion for the current lang and Node#can_auto_create_discussion? is true
  def discussion
    return @discussion if defined?(@discussion)

    @discussion = Discussion.find(:first, :conditions=>[ "node_id = ? AND inside = ? AND lang = ?",
      self[:id], v_status != Zena::Status[:pub], v_lang ], :order=>'id DESC') ||
      if can_auto_create_discussion?
        Discussion.new(:node_id=>self[:id], :lang=>v_lang, :inside=>(v_status != Zena::Status[:pub]))
      else
        nil
      end
  end

  # Automatically create a discussion if any of the following conditions are met:
  # - there already exists an +outside+, +open+ discussion for another language
  # - the node is not published (creates an internal discussion)
  # - the user has drive access to the node
  def can_auto_create_discussion?
    can_drive? ||
    (v_status != Zena::Status[:pub]) ||
    Discussion.find(:first, :conditions=>[ "node_id = ? AND inside = ? AND open = ?",
                             self[:id], false, true ])
  end

  # FIXME: use nested_attributes_alias and try to use native Rails to create the comment
  # comment_attributes=, ...
  def m_text; ''; end
  def m_title; ''; end
  def m_author; ''; end

  def m_text=(str)
    @add_comment ||= {}
    @add_comment[:text] = str
  end

  def m_title=(str)
    @add_comment ||= {}
    @add_comment[:title] = str
  end

  def m_author=(str)
    @add_comment ||= {}
    @add_comment[:author] = str
  end

  # Comments for the current context. Returns nil when there is no discussion.
  def comments
    if discussion
      res = discussion.comments(:with_prop=>can_drive?)
      res == [] ? nil : res
    else
      nil
    end
  end

  # TODO: remove, replace by relation proxy: proxy.count...
  def comments_count
    if discussion
      discussion.comments_count(:with_prop=>can_drive?)
    else
      0
    end
  end

  # Return true if it is allowed to add comments to the node in the current context
  def can_comment?
    visitor.commentator? && discussion && discussion.open?
  end

  # TODO: test
  def sweep_cache
    return if current_site.being_created?
    # zafu 'erb' rendering cache expire
    # TODO: expire only 'dev' rendering if version is a redaction
    CachedPage.expire_with(self) if self.kind_of?(Template)

    # Clear element cache
    Cache.sweep(:visitor_id=>self[:user_id], :visitor_groups=>[rgroup_id, wgroup_id, dgroup_id], :kpath=>self.vclass.kpath)

    # Clear full result cache

    # we want to be sure to find the project and parent, even if the visitor does not have an
    # access to these elements.
    # FIXME: use self + modified relations instead of parent/project
    [self, self.real_project(false), self.real_section(false), self.parent(false)].compact.uniq.each do |obj|
      # destroy all pages in project, parent and section !
      CachedPage.expire_with(obj)
      # this destroys less cache but might miss things like 'changes in project' that are displayed on every page.
      # CachedPage.expire_with(self, [self[:project_id], self[:section_id], self[:parent_id]].compact.uniq)
    end

    # clear assets
    FileUtils::rmtree(asset_path(''))
  end

  # Include data entry verification in multiversion's empty? method.
  def empty?
    return true if new_record?
    super && 0 == self.class.count_by_sql("SELECT COUNT(*) FROM #{DataEntry.table_name} WHERE node_a_id = #{id} OR node_b_id = #{id} OR node_c_id = #{id} OR node_d_id = #{id}")
  end

  # create a 'tgz' archive with node content and children, returning temporary file path
  def archive
    n = 0
    while true
      folder_path = File.join(RAILS_ROOT, 'tmp', sprintf('%s.%d.%d', 'archive', $$, n))
      break unless File.exists?(folder_path)
    end

    begin
      FileUtils::mkpath(folder_path)
      export_to_folder(folder_path)
      tempf = Tempfile.new(name)
      `cd #{folder_path}; tar czf #{tempf.path} *`
    ensure
      FileUtils::rmtree(folder_path)
    end
    tempf
  end

  # export node content and children into a folder
  def export_to_folder(path)
    children = secure(Node) { Node.find(:all, :conditions=>['parent_id = ?', self[:id] ]) }

    if kind_of?(Document) && version.title == name && (kind_of?(TextDocument) || version.text.blank? || version.text == "!#{zip}!")
      # skip zml
      # TODO: this should better check that version content is really useless
    elsif version.title == name && version.text.blank? && klass == 'Page' && children
      # skip zml
    else
      File.open(File.join(path, name + '.zml'), 'wb') do |f|
        f.puts self.to_yaml
      end
    end

    if kind_of?(Document)
      data = kind_of?(TextDocument) ? StringIO.new(version.text) : version.content.file
      File.open(File.join(path, filename), 'wb') { |f| f.syswrite(data.read) }
    end

    if children
      content_folder = File.join(path,name)
      FileUtils::mkpath(content_folder)
      children.each do |child|
        child.export_to_folder(content_folder)
      end
    end
  end

  # export node as a hash
  def to_yaml
    hash = {}
    export_keys[:zazen].each do |k, v|
      hash[k] = unparse_assets(v, self, k)
    end

    export_keys[:dates].each do |k, v|
      hash[k] = visitor.tz.utc_to_local(v).strftime("%Y-%m-%d %H:%M:%S")
    end

    hash.merge!('class' => self.klass)
    hash.to_yaml
  end

  # List of attribute keys to export in a zml file.
  def export_keys
    {
      :zazen => version.export_keys[:zazen],
      :dates => version.export_keys[:dates],
    }
  end

  # List of attribute keys to transform (change references, etc).
  def parse_keys
    export_keys[:zazen].keys
  end

  # This is needed during 'unparse_assets' when the node is it's own helper
  def find_node_by_pseudo(string, base_node = nil)
    secure(Node) { Node.find_node_by_pseudo(string, base_node || self) }
  end

  protected

    # after node is saved, make sure it's children have the correct section set
    def spread_project_and_section
      if @spread_section_id || @spread_project_id
        # update children
        sync_section_and_project(@spread_section_id, @spread_project_id)
        remove_instance_variable :@spread_section_id if @spread_section_id
        remove_instance_variable :@spread_project_id if @spread_project_id
      end
    end

    #  node                       [change project and section]
    #    |
    #    +-- node                 [set project] [set section]
    #          |
    #          +-- section 4      [set project] [  keep     ]
    #          |     |
    #          |     +-- node     [set project] [  keep     ]
    #          |     |
    #          |     +-- project  [  keep     ] [  keep     ] => skip
    #          |
    #          +-- page           [set project] [set section]
    #          |
    #          +-- project        [  keep     ] [set section]
    #                |
    #                +-- node     [  keep     ] [set section]
    #                |
    #                +-- section  [  keep     ] [  keep     ] => skip
    def sync_section_and_project(section_id, project_id)

      # If this code is optimized, do not forget to sweep_cache for each modified child.
      all_children.each do |child|
        if child.kind_of?(Section)                     # [keep section] [set  project]
          next unless project_id                       # => skip
          # needed when doing 'sweep_cache'.
          visitor.visit(child)

          child[:project_id] = project_id              #                [set  project]
          child.save_with_validation(false)
          child.sync_section_and_project(    nil    , project_id)

        elsif child.kind_of?(Project)                  # [set  section] [keep project]
          next unless section_id                       # => skip
          # needed when doing 'sweep_cache'.
          visitor.visit(child)

          child[:section_id] = section_id              # [set  section]
          child.save_with_validation(false)
          child.sync_section_and_project(section_id,     nil   )
        else                                           # [set  section] [set  project]
          # needed when doing 'sweep_cache'.
          visitor.visit(child)

          child[:section_id] = section_id if section_id #[set  section]
          child[:project_id] = project_id if project_id #               [set  project]
          child.save_with_validation(false)
          child.sync_section_and_project(section_id, project_id)
        end
      end
    end

  private

    def rebuild_fullpath
      return unless new_record? || name_changed? || parent_id_changed? || fullpath.nil?
      if parent = parent(false)
        path = parent.fullpath.split('/') + [name]
      else
        path = []
      end
      self[:fullpath] = path.join('/')
    end

    def rebuild_basepath
      return unless new_record? || name_changed? || parent_id_changed? || custom_base_changed? || basepath.nil?
      if custom_base
        self[:basepath] = self.fullpath
      elsif parent = parent(false)
        self[:basepath] = parent.basepath
      else
        self[:basepath] = ""
      end
    end

    def set_defaults
      # sync version title and name
      if ref_lang == version.lang &&
         ((full_drive? && version.status == Zena::Status[:pub]) ||
          (can_drive?  && vhash['r'][ref_lang].nil?))
        if name_changed? && !name.blank?
          version.title = self.name
        elsif !version.title.blank?
          self.name = version.title.url_name
          if !new_record? && kind_of?(Page) && name_changed?
            # we only rebuild Page names on update
            get_unique_name_in_scope('NP%')
          end
        end
      end

      self[:custom_base] = false unless kind_of?(Page)
      true
    end

    def node_before_validation
      self[:kpath] = self.vclass.kpath

      self.name ||= (version.title || '').url_name

      unless name.blank?
        # rebuild cached fullpath / basepath
        rebuild_fullpath
        rebuild_basepath
        # we should use a full rebuild when there are corrupt values,
        # if fullpath was blank, we have no way to find all children
        @need_rebuild_children_fullpath = !new_record? && (fullpath_changed? || basepath_changed?) && !fullpath_was.blank?
      end

      # make sure section is the same as the parent
      if self[:parent_id].nil?
        # root node
        self[:section_id] = self[:id]
        self[:project_id] = self[:id]
      elsif ref = parent
        self[:section_id] = ref.get_section_id
        self[:project_id] = ref.get_project_id
      else
        # bad parent will be caught later.
      end

      if !new_record? && self[:parent_id]
        # node updated and it is not the root node
        if !kind_of?(Section) && section_id_changed?
          @spread_section_id = self[:section_id]
        end
        if !kind_of?(Project) && project_id_changed?
          @spread_project_id = self[:project_id]
        end
      end

      # set position
      if klass != 'Node'
        # 'Node' does not have a position scope (need two first letters of kpath)
        if new_record?
          if self[:position].to_f == 0
            pos = Zena::Db.fetch_row("SELECT `position` FROM #{Node.table_name} WHERE parent_id = #{Node.connection.quote(self[:parent_id])} AND kpath like #{Node.connection.quote("#{self.class.kpath[0..1]}%")} ORDER BY position DESC LIMIT 1").to_f
            self[:position] = pos > 0 ? pos + 1.0 : 0.0
          end
        elsif parent_id_changed?
          # moved, update position
          pos = Zena::Db.fetch_row("SELECT `position` FROM #{Node.table_name} WHERE parent_id = #{Node.connection.quote(self[:parent_id])} AND kpath like #{Node.connection.quote("#{self.class.kpath[0..1]}%")} ORDER BY position DESC LIMIT 1").to_f
          self[:position] = pos > 0 ? pos + 1.0 : 0.0
        end
      end

    end

    # Make sure the node is complete before creating it (check parent and project references)
    def validate_node
      # when creating root node, self[:id] and :root_id are both nil, so it works.
      if parent_id_changed? && self[:id] == current_site[:root_id]
        errors.add("parent_id", "root should not have a parent") unless self[:parent_id].blank?
      end

      errors.add(:base, 'You do not have the rights to post comments.') if @add_comment && !can_comment?

      if @new_klass
        if !can_drive? || !self[:parent_id]
          errors.add('klass', 'You do not have the rights to do this.')
        else
          errors.add('klass', 'invalid') if !self.class.allowed_change_to_classes.include?(@new_klass)
        end
      end
    end

    # Called before destroy. An node must be empty to be destroyed
    def secure_on_destroy
      return false unless super
      # expire cache
      # TODO: test, use observer instead...
      CachedPage.expire_with(self)
      true
    end

    # Get unique zip in the current site's scope
    def node_before_create
      self[:zip] = Zena::Db.next_zip(self[:site_id])
    end

    # Create an 'outside' discussion if the virtual class has auto_create_discussion set
    def node_after_create
      if vclass.auto_create_discussion
        Discussion.create(:node_id=>self[:id], :lang=>v_lang, :inside => false)
      end
    end

    # Called after a node is 'unpublished'
    def after_unpublish
      if !self[:publish_from] && !@new_record_before_save
        # not published any more. 'unpublish' documents
        sync_documents(:unpublish)
      else
        true
      end
    end

    def after_redit
      return true if @new_record_before_save
      sync_documents(:redit)
    end

    # Called after a node is 'removed'
    def after_remove
      return true if @new_record_before_save
      sync_documents(:remove)
    end

    # Called after a node is 'proposed'
    def after_propose
      return true if @new_record_before_save
      sync_documents(:propose)
    end

    # Called after a node is 'refused'
    def after_refuse
      return true if @new_record_before_save
      sync_documents(:refuse)
    end

    # Called after a node is published
    def after_publish
      return true if @new_record_before_save
      sync_documents(:publish)
    end

    # Publish, refuse, propose the Documents of a redaction
    def sync_documents(action)
      allOK = true
      documents = secure_drive(Document) { Document.find(:all, :conditions=>"parent_id = #{self[:id]}") } || []
      case action
      when :propose
        documents.each do |doc|
          if doc.can_propose?
            allOK = doc.propose(Zena::Status[:prop_with]) && allOK
          end
        end
      when :unpublish
        # FIXME: use a 'before_unpublish' callback to make sure all sub-nodes can be unpublished...
        documents.each do |doc|
          unless doc.unpublish
            doc.errors.each do |k, v|
              errors.add('document', "#{k} #{v}")
            end
            allOK = false
          end
        end
      else
        documents.each do |doc|
          if doc.can_apply?(action)
            allOK = doc.apply(action) && allOK
          end
        end
      end
      allOK
    end

    # Whenever something changed (publication/proposition/redaction/link/...)
    def after_all
      sweep_cache
      if @add_comment
        # add comment
        @discussion ||= self.discussion
        @discussion.save if @discussion.new_record?
        @add_comment[:author_name] = nil unless visitor.is_anon? # only anonymous user should set 'author_name'
        @add_comment[:discussion_id] = @discussion[:id]
        @add_comment[:user_id]       = visitor[:id]

        @comment = secure!(Comment) { Comment.create(@add_comment) }

        remove_instance_variable(:@add_comment)
      end
      remove_instance_variable(:@discussion) if defined?(@discussion) # force reload

      true
    end

    def change_klass

      if @new_klass && !new_record?
        old_kpath = self.kpath

        klass = Node.get_class(@new_klass)
        if klass.kind_of?(VirtualClass)
          self[:vclass_id] = klass.kind_of?(VirtualClass) ? klass[:id] : nil
          self[:type]      = klass.real_class.to_s
        else
          self[:vclass_id] = klass.kind_of?(VirtualClass) ? klass[:id] : nil
          self[:type]      = klass.to_s
        end
        self[:kpath] = klass.kpath

        if old_kpath[/^NPS/] && !self[:kpath][/^NPS/]
          @spread_section_id = self[:section_id]
        elsif !old_kpath[/^NPS/] && self[:kpath][/^NPS/]
          @spread_section_id = self[:id]
        end

        if old_kpath[/^NPP/] && !self[:kpath][/^NPP/]
          @spread_project_id = self[:project_id]
        elsif !old_kpath[/^NPP/] && self[:kpath][/^NPP/]
          @spread_project_id = self[:id]
        end

        @set_klass = @new_klass
        remove_instance_variable(:@new_klass)
      end

      true
    end

    # Find all children, whatever visitor is here (used to check if the node can be destroyed or to update section_id)
    def all_children
      # FIXME: remove 'with_exclusive_scope' once scopes are clarified and removed from 'secure'
      Node.send(:with_exclusive_scope) do
        Node.find(:all, :conditions=>['parent_id = ?', self[:id] ])
      end
    end

    def rebuild_children_fullpath
      return true unless @need_rebuild_children_fullpath

      # Update descendants
      fullpath_new = self.fullpath
      fullpath_new = "#{fullpath_new}/" if fullpath_was == ''
      fullpath_re  = fullpath_changed? ? %r{\A#{self.fullpath_was}} : nil

      bases = [self.basepath]

      i = 0
      batch_size = 100
      while true
        list  = Zena::Db.fetch_attributes(['id', 'fullpath', 'basepath', 'custom_base'], 'nodes', "fullpath LIKE #{Zena::Db.quote("#{fullpath_was}%")} AND site_id = #{current_site.id} ORDER BY fullpath ASC LIMIT #{batch_size} OFFSET #{i * batch_size}")
        break if list.empty?
        list.each do |rec|
          rec['fullpath'].sub!(fullpath_re, fullpath_new) if fullpath_re
          if rec['custom_base'].to_i == 1
            rec['basepath'] = rec['fullpath']
            bases << rec['basepath']
          else
            while rec['fullpath'].size <= bases.last.size
              bases.pop
            end
            rec['basepath'] = bases.last
          end
          id = rec.delete('id')
          Zena::Db.execute "UPDATE nodes SET #{rec.map {|k,v| "#{Zena::Db.connection.quote_column_name(k)}=#{Zena::Db.quote(v)}"}.join(', ')} WHERE id = #{id}"
        end
        # 50 more
        i += 1
      end
      true
    end

    # Base class
    def base_class
      Node
    end

    # Reference class
    def ref_class
      Node
    end

    # return the id of the reference
    def ref_field(for_heirs=false)
      if !for_heirs && (self[:id] == current_site[:root_id])
        :id # root is it's own reference
      else
        :parent_id
      end
    end

    def get_unique_name_in_scope(kpath)
      Node.send(:with_exclusive_scope) do
        if new_record?
          cond = ["name = ? AND parent_id = ? AND kpath LIKE ?", self[:name], self[:parent_id], kpath]
        else
          cond = ["name = ? AND parent_id = ? AND kpath LIKE ? AND id != ? ", self[:name], self[:parent_id], kpath, self[:id]]
        end

        if taken_name = Node.find(:first, :select => 'name', :conditions => cond, :order => "LENGTH(name) DESC")
          if taken_name =~ /^#{self[:name]}-(\d)/
            self[:name] = "#{self[:name]}-#{$1.to_i + 1}"
            i = $1.to_i + 1
          else
            self[:name] = "#{self[:name]}-1"
            i = 1
          end
          version.title = "#{version.title}-#{i}" unless version.title.blank?
        end
      end
    end

end

Bricks.apply_patches