require 'yajl/json_gem'

module RCS
  module FilesystemEntry
    FILE = 0
    DIRECTORY = 1
    EMPTY_DIRECTORY = 3

    def file?
      (self[:attr].nil? and self[:size].to_i > 0) or self[:attr] == FILE
    end

    def unknown?
      self[:attr].nil? and self[:size].to_i == 0
    end

    def directory?
      self[:attr] == DIRECTORY
    end

    def empty_directory?
      self[:attr] == EMPTY_DIRECTORY
    end

    def size
      self[:size].to_i if self[:size]
    end

    def path
      self[:path].gsub("\0", "")
    end

    def splitted_path
      FilesystemEntry.split(path)
    end

    def self.split(path)
      return ['/'] if path == '/'
      splitted = path.split(/[\\\/]/)
      splitted[0] = '/' if splitted[0] and splitted[0].empty?
      splitted.pop if splitted.last and splitted.last.empty?
      splitted
    end

    def self.join(path1, path2, separator)
      path = [path1, path2].compact.join(separator)
      path.gsub!(/\/{2,}/, "/")
      path.gsub!(/\\{2,}/, "\\")
      path
    end
  end

  module FilesystemEvidenceMixin
    def agent
      @_agent ||= get_agent
    end

    def target
      @_target ||= agent.get_parent
    end

    # Entry that comes from FILEOPEN, FILECAP and DOWNLOAD evidence
    # for which i can't tell if is a file or a folder
    def unknown_entry?
      self[:data][:entries].nil? && self[:data][:attr].nil? && self[:data][:size].to_i.zero?
    end

    def dr
      self[:dr].to_i
    end

    def da
      self[:da].to_i
    end

    def entries
      (type == :filesystem) ? self[:data][:entries] : [self[:data]]
    end

    def find_filesystem_evidence
      selector = {aid: agent.id.to_s, type: 'filesystem'}
      ::Evidence.target(target).where(selector).first
    end

    def update_filesystem_evidence(purge: false)
      evidence = find_filesystem_evidence

      return false unless evidence

      fs = evidence.data['fs'] ? Yajl::Parser.parse(evidence.data['fs']) : Hash.new

      merge_filesystem_hash(fs)

      if purge
        start_path = detect_updated_path
        trace(:debug, "Updated path is #{start_path}")
        purge_filesystem_hash(fs, start_path) unless root_entries?

        # Send push notification to the console
        RCS::DB::PushManager.instance.notify(:filesystem, path: start_path, aid: agent.id.to_s)
      end

      fs = Yajl::Encoder.encode(fs)

      evidence.data['fs'] = fs
      evidence.save!

      evidence
    end

    def detect_updated_path
      entries.sort! { |a, b| a[:path].size <=> b[:path].size }

      p0 = entries[0][:path].gsub("\0", "") if entries[0]
      p0dir = entries[0][:attr] != 0 if p0
      p1 = entries[1][:path].gsub("\0", "") if entries[1]
      sep = agent.filesystem_separator

      if p1 and p1.start_with?(p0)
        (p0.end_with?(sep) and p0 !~ ROOT_ENTRY_REGEXP) ? p0[0..-2] : p0
      elsif p0 =~ ROOT_ENTRY_REGEXP
        p0
      elsif p1.nil? and p0 and p0dir
        p0
      else
        p0.gsub!('\\', '/')
        p0 << "*" if p0.end_with?("/")
        p0 = File.dirname(p0)
        p0.gsub!('/', '\\') if agent.filesystem_separator == '\\'
        p0
      end
    end

    def create_filesystem_evidence
      evidence = ::Evidence.target(target).create(
        aid: agent.id.to_s,
        type: 'filesystem',
        da: self[:da].to_i,
        dr: self[:dr].to_i,
        rel: 0,
        blo: false,
        note: "",
        data: {fs: merge_filesystem_hash({}).to_json}
      )

      path = detect_updated_path
      trace(:debug, "Received filesystem for path #{path}")

      # Send push notification to the console
      RCS::DB::PushManager.instance.notify(:filesystem, path: path, aid: agent.id.to_s)

      evidence
    end

    ROOT_ENTRY_REGEXP = /^(\/|[a-z]{1}\:)\0{0,1}$/i

    def root_entries?
      entries.each do |entry|
        return false if entry[:path] !~ ROOT_ENTRY_REGEXP
      end
      return true
    end

    # First-level purging.
    # Delete old children of a folder that as just been updated
    def purge_filesystem_hash(hash, start_path)
      FilesystemEntry.split(start_path).each  { |p| hash = hash[p]['c'] }

      hash.keys.each do |k|
        next if hash[k]['d'] >= dr
        # trace(:debug, "Removing old filesystem node #{start_path} => #{k}")
        hash.delete(k)
      end

      hash
    end

    def merge_filesystem_hash(fs)
      entries.each do |entry|
        entry.singleton_class.__send__(:include, FilesystemEntry)

        hash = fs
        # puts "=======> new entry " + entry.path
        next if entry.unknown?

        parts = entry.splitted_path

        # Skip malformed paths like "//./pipe/"
        next if parts.include?("")

        parts.each_with_index do |p, index|
          is_leaf = index == parts.size - 1

          if is_leaf and entry.file?
            hash[p] = {'d' => dr, 'a' => da, 's' => entry.size}
          else
            hash[p] ||= {}
            hash[p]['c'] ||= {}

            if is_leaf
              hash[p]['d'] = dr
              hash[p]['a'] = da
              hash[p]['e'] = 1 if entry.empty_directory?
            else
              hash[p]['d'] ||= dr
              hash[p]['a'] ||= da
              hash[p].delete('e')
            end

            hash = hash[p]['c']
          end
        end
      end

      fs
    end
  end
end
