require "activerecord_gfdnavi"


class Node < ActiveRecord::Base
  acts_as_tree :order => "name"

  belongs_to :owner, :class_name => "User", :foreign_key => :owner_id

  has_many :keyword_attributes, :dependent => :destroy
  has_many :spatial_and_time_attributes, :dependent => :destroy
  has_many :draw_parameters, :dependent => :destroy

  has_many :references, :through => :node_relations_reference
  has_many :referenced_by, :through => :node_relations_referenced_by
  has_many :node_relations_reference, :class_name => "NodeRelation", :foreign_key => :referenced_by
  has_many :node_relations_referenced_by, :class_name => "NodeRelation", :foreign_key => :reference


  validates_presence_of  :path, :name
  validates_uniqueness_of :path

  before_create :before_create, :update_permission
  before_update :update_permission
  after_update  :update_children
  after_create :save_yaml
  before_destroy :destroy_entity


  DIRECTORY = 0
  VARIABLE = 1
  IMAGE = 2
  KNOWLEDGE = 3
  FUNCTION = 4
  DRAW_METHOD = 5

  NODE_TYPES = %w(directory variable image knowledge function draw_method)

  LOCAL_DRIVE = 100
  OPENDAP = 101


  #<< class methods >>

  class << self

    def find(*args)
      args.push(Hash.new) unless Hash === args[-1]
      hash = args[-1]
      user = hash.delete(:user)
      if args.length == 3 && Hash === args[2] \
        && Hash === args[1] && args[1].length == 1 && args[1].key?(:user)
        user = args.delete_at(1).delete(:user)
      end
      conditions = conditions_to_read(user) and \
          hash[:conditions] = add_conditions(hash[:conditions], conditions)
      super(*args)
    end

    def count(*args)
      args.push(Hash.new) unless Hash === args[-1]
      hash = args[-1]
      user = hash.delete(:user)
      conditions = conditions_to_read(user) and \
          hash[:conditions] = add_conditions(hash[:conditions], conditions)
      super(*args)
    end

    def top_directory_nodes
      nodes = Node.find(:all, :conditions => "parent_id is NULL AND node_type=#{Node::DIRECTORY}", :order => 'path')
      if nodes.length == 0
        nodes = Node.find(:all, :conditions => "parent_id=0 AND node_type=#{Node::DIRECTORY}", :order => 'path')
      end
      return nodes
    end

    # size [Integer] size in bytes or nil
    def size2str(size)
      if size.nil?
        ''
      elsif size < 1000
        size.to_s
      elsif size < 10000
        (size / 100.0).round.to_s.insert(1,'.') + 'K'
      elsif size < 1000000
        (size / 1000).to_s + 'K'
      elsif size < 10000000
        (size / 1e5).round.to_s.insert(1,'.') + 'M'
      elsif size < 1000000000
        (size / 1000000).to_s + 'M'
      elsif size < 1e10
        (size / 1e8).round.to_s.insert(1,'.') + 'G'
      else
        (size / 1000000000).to_s + 'G'
      end
    end

    def make_user_directory(path, user, other_mode=0, rgroups=0)
      /(\/usr)\/(.+)/ =~ path or 
             raise(ArgumentError, "#{path} cannot be a user directory")
      full_path = $1    # initially /usr
      reldir = $2
      parent = Node.find(:first,:conditions=>["path=?",full_path]) or
             raise("Cannot find #{full_path}")
      dir = nil
      reldir.split(/\//).each do |dname|
        full_path = File.join(full_path, dname)
        dir = Directory.find(:first, :conditions=>["path=?",full_path], 
                             :user=>user)
        unless dir
          dir = Directory.new
          dir.name = dname
          dir.path = full_path
          dir.parent = parent
          dir.owner = user
          dir.other_mode = other_mode
          if rgroups.is_a?(Integer)
            dir.rgroups = rgroups
          else  
            # assume an array of groups
            dir.set_rgroups(*rgroups)
          end
          File.makedirs(dir.fname) or raise("failed to makedir")
          dir.save!
        end
        parent = dir.node
      end
      dir
    end

    protected
    # ARGUMENTS
    # * user : a User or nil (assuming non-login case) or :all.
    #   Do not use :all, if you do not understand what this means.
    # * prefix (String) : Use this if you neede to explicitly specify the
    #   name of the node table. (Useful if you use find_by_sql.)
    #   E.g. 'nodes.',
    def conditions_to_read(user, prefix='')
      if User === user
        if user.super_user
          conditions = nil
        else
          groups = user.groups
          if groups == 0
            # the user is not a member of any group
            conditions = "( (#{prefix}owner_id = #{user.id}) OR " + boolean_condition("#{prefix}other_readable") + ")"
          else
            conditions = "( (#{prefix}owner_id = #{user.id}) OR NOT ((#{prefix}groups_readable & #{groups}) = 0) OR " + boolean_condition("#{prefix}other_readable") + ')'
          end
        end
      elsif user == :all
        conditions = nil
      elsif user.nil?
        conditions = '(' + boolean_condition("#{prefix}other_readable") + ')'
      else
        raise ArgumentError, "invalid user argument"
      end
      conditions
    end

  end

  #<< instance methods >>

  def set_rgroups(*groups)
    groups = groups[0] if groups.length==1 and groups[0].is_a?(Array)
    self.rgroups = Group.bit_mask_for(*groups)
  end
  def set_wgroups(*groups)
    groups = groups[0] if groups.length==1 and groups[0].is_a?(Array)
    self.wgroups = Group.bit_mask_for(*groups)
  end


  alias _children children
  def children(reload=false, hash={})
    ch = _children(reload)
    if hash.length > 0
      ch = ch.find(:all, :user=>hash[:user])
    end
    return ch
  end

  @@parent = Hash.new
  def parent
    return nil if parent_id == 0
    return @@parent[parent_id] ||= parent_id && Node.find(:first,:conditions=>["id=?",parent_id])
  end

  def entity
    return @entity if @entity
    NODE_TYPES.each {|typ|
      if node_type == Node.const_get(typ.upcase)
        @entity = ActiveRecord.class_eval(typ.classify)._find(:first,:conditions=>"node_id=#{self.id}")
        return @entity
      end
    }
    return nil
  end

  def entity=(ent)
    @entity = ent
  end

  NODE_TYPES.each do |typ|
    type_num = Node.const_get(typ.upcase)
    pluralized_name = typ.pluralize
    eval <<-"EOF"
    def self.find_#{pluralized_name}(*args)
      hash = Hash===args[-1] ? args.pop : Hash.new
      hash[:select] = "nodes.*"
      hash[:from] = "nodes, #{pluralized_name}"
      hash[:conditions] = add_conditions(hash[:conditions], "#{pluralized_name}.node_id=nodes.id")
      hash[:conditions] = add_conditions(hash[:conditions], "node_type=#{type_num}")
      args.push hash
      self.find(*args)
    end
    def #{typ}?
      node_type == #{type_num}
    end
    @@#{typ}_nodes = Hash.new
    def #{typ}_nodes(hash={})
      refind = hash.delete(:refind)
      keys = hash.keys
      keys.delete(:user)
      user = hash[:user]
      user = user.login if User === user
      if keys.length == 0 
        h = ( @@#{typ}_nodes[self.path] ||= Hash.new )
        if (obj=h[user]) && (!refind)
          return obj
        end
      end
      hash[:conditions] = add_conditions(hash[:conditions], "node_type=#{type_num}")
      obj = children.find(:all, hash)
      h[user] = obj if keys.length == 0
      return obj
    end
    def have_#{typ}_nodes?(hash={})
      hash[:conditions] = add_conditions(hash[:conditions], "node_type=#{type_num} AND parent_id=\#\{self.id\}")
      hash[:select] = "1"
      Node.find(:first, hash) != nil
    end
    @@#{typ}_node_length = Hash.new
    def count_#{typ}_nodes(hash={})
      refind = hash.delete(:refind)
      keys = hash.keys
      keys.delete(:user)
      user = hash[:user]
      user = user.login if User === user
      if keys.length == 0 
        h = ( @@#{typ}_node_length[self.path] ||= Hash.new )
        if (obj=h[user]) && (!refind)
          return obj
        end
      end
      hash[:conditions] = add_conditions(hash[:conditions], "node_type=#{type_num}")
      len = children.count(:all, hash)
      h[user] = len if keys.length == 0
      return len
    end
    @@#{pluralized_name} = Hash.new
    def #{pluralized_name}(hash={})
      refind = hash[:refind]
      keys = hash.keys
      keys.delete(:user)
      user = hash[:user]
      user = user.login if User === user
      if keys.length == 0 
        h = ( @@#{pluralized_name}[self.path] ||= Hash.new )
        if (obj=h[user]) && (!refind)
          return obj
        end
      end
      obj = #{typ}_nodes(hash).collect{|node| node.entity}
      h[user] = obj if keys.length == 0
      return obj
    end
    EOF
  end

  def fname
    if self.file && self.file!="NULL"
      return add_prefix(self.file)
    else
      return add_prefix(path)
    end
  end

  def remote?
    /^http:\/\// =~ fname
  end

  def opendap?
    remote?  # equivalent at this moment
  end

  def target
    # for bug of rails
    self
  end

  def add_prefix(name)
    if /^temporary:(.*)/ =~ name
      name = $1
      if variable?
        return GFDNAVI_WORK_PATH + name
      elsif image?
        return GFDNAVI_DIAGRAM_PATH + name
      else
        raise "[bug]"
      end
    elsif /^http:\/\// =~ name
      return name
    elsif /^\/usr(.*)/ =~ name
      if $1
        GFDNAVI_USER_PATH + $1
      else
        GFDNAVI_USER_PATH.dup
      end
    else
      return GFDNAVI_DATA_PATH + name
    end
  end


  protected
  def validate
    if path =~ /^temporary/
      errors.add(:path, "temporary variable cannot be saved")
    end
    unless /^\// =~ path  || /^http:\/\// =~ path 
      errors.add(:path, "path must be started with '/' or 'http://'")
    end
  end

  def before_create
    unless parent
      ary = path.split("/")[0..-2]
      unless ary.length == 0
        if ary.length == 1
          parent_path = "/"
        else
          parent_path = ary.join("/")
        end
        (parent = Node.find(:first,:conditions=>["path=?",parent_path])) && (self.parent = parent)
      end
    end
    self.owner = parent.owner if !owner && parent
    self.mtime = Time.new if !mtime
  end

  def update_permission
    @change_permission = false
    if !parent
      # a root node
      if self.other_mode.nil? or (self.other_mode & 4)!=0  # r ok for others
        # anyone can read
        unless self.other_readable == true
          self.other_readable = true
          @change_permission = true
        end
        unless self.groups_readable == -1
          self.groups_readable = -1   # == unsigned 0xffffffffffffffff
          @change_permission = true
        end
      else
        # read limited only to permited groups
        unless self.other_readable == false
          self.other_readable = false
          @change_permission = true
        end
        unless self.groups_readable == self.rgroups
          self.groups_readable = self.rgroups 
          @change_permission = true
        end
      end
    else
      if self.other_mode.nil? or (self.other_mode & 4)!=0  # r ok for others
        # settings same as the direct parent
        unless self.other_readable == parent.other_readable
          self.other_readable = parent.other_readable
          @change_permission = true
        end
        unless self.groups_readable == parent.groups_readable
          self.groups_readable = parent.groups_readable
          @change_permission = true
        end
      else
        # read limited only to permited groups
        unless self.other_readable == false
          self.other_readable = false
          @change_permission = true
        end
        unless self.groups_readable == parent.groups_readable & self.rgroups 
          self.groups_readable = parent.groups_readable & self.rgroups
          @change_permission = true
        end
      end
    end
  end

  def update_children
    if @change_permission
      c = children(false,{:user=>:all})
      if c.length > 0
        c.each{|child|
          if child.respond_to?(:update)
            child.update
          else
            child.save
          end
        }
      end
    end
    @change_permission = nil
  end

  def destroy_entity
    entity.destroy if entity
  end

  def save_yaml
    return if knowledge? || function? || draw_method?
    return if file or remote?
    yaml_fname = fname + ".yml"
    sigen_fname = fname + ".SIGEN"
    hash = make_attribute_hash
    unless hash.length==0 || file || (variable? && entity.actual_files.length>0)
      unless File.exist?(yaml_fname) || File.exist?(sigen_fname)
        File.open(yaml_fname,"w"){|file| file.print hash.to_yaml}
      end
    end
  end


  def make_attribute_hash
    hash = Hash.new
    keyword_attributes.each{|key|
      hash[key.name] = key.value
    }
    vh = Hash.new
    variable_nodes(:user => :all).each{|var|
      vh[var.name] = var.make_attribute_hash
    }
    if vh.length > 0
      hash['contains'] = vh
    end
    chash = Node.columns_hash
    gh = Hash.new
    gh['user'] = owner.login if owner && (parent && owner != parent.owner)
    gh['other_mode'] = other_mode if other_mode && other_mode!=chash['other_mode'].default
    if (gs = Group.find_by_bit_flag(rgroups)) && gs != []
      gh['rgroups'] = gs.collect{|g| g.name}
    end
    if (gs = Group.find_by_bit_flag(wgroups)) && gs != []
      gh['wgroups'] = gs.collect{|g| g.name}
    end
    gh['guest_owner_id'] = guest_owner_id if guest_owner_id
    if references.length > 0
      gh['references'] = references.collect{|ref|
        {'path' => ref.path, 'name' => ref.name}
      }
    end
    if image? && entity.vizshot
      gh['vizshot'] = entity.vizshot
    end
    hash['gfdnavi'] = gh if gh.length > 0
    return hash
  end


  def make_directories
    if file && file!=""
      apath = file.splite("/")[1..-2]
    else
      apath = path.splite("/")[1..-1]
    end
    dir = Node.find_by_sql("SELECT * FROM nodes WHERE path='/' LIMIT 1")[0]
    user = "root"
    if apath[0] = "usr"
      user = apath[1]
      apath = apath[2..-1]
      dir = make_directory(dir, "usr", "root")
      dir = make_directory(dir, user, user)
    end
    apath = apath.split("/")
    apath.each{|name|
      dir = make_directory(dir, name, user)
    }
  end

  def make_directory(parent, name, uname)
    user = User.find(:first,:conditions=>["login=?",uname])
    path = File.join(parent.path, name)
    node = Node.find(:first, :conditions=>["path=?",path], :usr=>user)
    if node
      return node.entity
    else
      dir = Directory.new(:name=>name, :path=>path)
      dir.user = user
      return dir
    end
  end


end
