# == Schema Information
#
# Table name: directories
#
#  id          :integer          not null, primary key
#  description :string(255)
#  hidden      :boolean          default(FALSE), not null
#  name        :string(255)
#  path        :string(255)
#  title       :string(255)
#  created_at  :datetime
#  updated_at  :datetime
#  parent_id   :integer
#
# Indexes
#
#  index_directories_on_parent_id  (parent_id)
#
require 'stringio'

ENV['FILES_ROOT'] ||= File.join(Rails.root, 'public', 'files')

class PathValidator < ActiveModel::Validator
  def validate(record)
    record.errors.add :path, "doesn't match generated path" unless \
      record.full_path == record.path
  end
end

class Directory < ActiveRecord::Base
  include Extra

  ROOT = 1
  DEMOS = 5
  DEMOS_DEFAULT = 19
  DEMOS_GATHERS = 92
  MOVIES = 30
  ARTICLES = 39

  attr_accessor :preserve_files

  belongs_to :parent, :class_name => "Directory", :optional => true
  has_many :subdirs, :class_name => "Directory", :foreign_key => :parent_id
  has_many :files, -> { order("name") }, :class_name => "DataFile"

  scope :ordered, ->  { order("name ASC") }
  scope :path_sorted, ->  { order("path ASC") }
  scope :filtered, -> { where(hidden: false) }
  scope :of_parent, -> (parent) { where(parent_id: parent.id) }

  # FIXME: different validation for user?
  validates_length_of [:name, :path, :title], :in => 1..255
  validates_format_of :name, :with => /\A[A-Za-z0-9]{1,20}\z/, :on => :create
  validates_length_of :name, :in => 1..255
  validates_inclusion_of :hidden, :in => [true, false]
  validates_presence_of :title
  validates_with PathValidator
  # TODO: add validation for path

  before_validation :init_variables
  after_create :make_path
  after_save :update_timestamp
  before_destroy :remove_files, unless: Proc.new { preserve_files }
  after_destroy :remove_path

  def to_s
    name
  end

  def parent_root?
    parent.id == Directory::ROOT
  end

  def root?
    id == Directory::ROOT
  end

  def full_title
    output = ""
    Directory.directory_traverse(self).reverse_each do |dir|
      unless dir.title&.empty?
        output << "%s" % dir.title
      else
        output << dir.name
      end
      output << " ยป " unless self == dir
    end
    output
  end

  def self.directory_traverse(directory, list = [])
    unless directory.root?
      list << directory
      return directory_traverse(directory.parent, list)
    else
      return list
    end
  end

  # Use this
  def full_path
    parent ? File.join(parent.full_path, name.downcase) : path
  end

  def relative_path
    parent ? File.join(parent.relative_path, name.downcase).sub(/^\//, '') : ""
  end

  def path_exists?
    File.directory?(full_path)
  end

  def init_variables
    # Force path to use parent which is the authoritative source
    self.path = full_path if parent
    self.title = File.basename(self.path).capitalize
    self.hidden = false if hidden.nil?
  end

  def make_path
    Dir.mkdir(full_path) unless File.exists?(full_path)
  end 

  def update_timestamp
    self.created_at = File.mtime(full_path) if File.exists?(full_path)
  end
  
  def remove_files
    files.each do |subdir|
      subdir.destroy
    end
    subdirs.each do |subdir|
      subdir.preserve_files = self.preserve_files
      subdir.destroy
    end
  end

  def remove_path
    Dir.unlink(full_path) if File.exists?(full_path)
  end

  # TODO: make tests for this, moving etc.
  # TODO: mutate instead of return.
  # TODO: move to its own class
  # TODO: also remove files
  # TODO: need log to rails log too
  def recreate_transaction
    strio = StringIO.new
    logger = Logger.new(strio)
    logger.info 'Starting recreate on Directory(%d): %s.' % [id, name]
    logger.info 'DataFiles: %d Directories: %d' % [DataFile.all.count, Directory.all.count]
    ActiveRecord::Base.transaction do
      # We use destroy lists so technically there can be seperate roots
      destroy_dirs = Hash.new
      if id == Directory::ROOT
        update_attribute :path, ENV['FILES_ROOT']
      end
      logger.info 'Path: %s' % [path]
      destroy_dirs = recreate(destroy_dirs, logger: logger)
      destroy_dirs.each do |key, dir|
        logger.info 'Removed dir: %s' % dir.full_path
        dir.preserve_files = true
        dir.destroy!
      end
    end
    logger.info 'DataFiles: %d Directories: %d' % [DataFile.all.count, Directory.all.count]
    logger.info 'Finish recreate'
    return strio
    # TODO: check items that weren't checked.
  end

  # QUESTION Symlinks?
  def recreate(destroy_dirs, logger: Rails.logger)
    # Convert all subdirs into a hash and mark them to be deleted
    # FIXME: better oneliner
    # logger.debug 'recreate: %s' % full_path
    destroy_dirs.merge!(subdirs.all.map{ |s| [s.id,s] }.to_h)

    # Go through all subdirectories (no recursion)
    Dir.glob(File.join(full_path, '*')).each do |subitem_path|
      subitem_name = File.basename(subitem_path)

      if File.directory? subitem_path
        # logger.debug 'Processing dir: %s' % subitem_path
        # We find by name only, ignore path
        # Find existing subdirs from current path. Keep those we find
        if (subdir = find_existing(subitem_name, subitem_path))
          if subdir.parent_id != self.id
            old_path = subdir.full_path
            subdir.parent = self
            subdir.save!
            logger.info 'Renamed dir: %s -> %s' % [old_path, subdir.full_path]
          elsif !subdir.valid?
            subdir.errors.full_messages.each do |err|
              logger.error err
            end
            subdir.init_variables
            logger.info 'Fixed attributes: %s' % [subdir.full_path]
            subdir.save!
          end
          destroy_dirs.delete subdir.id
        # In case its a new directory
        else
          # Attempt to find it in existing directories
          subdir = subdirs.build(name: subitem_name)
          # FIXME: find a better solution
          subdir.save!(validate: false)
          logger.info 'New dir: %s' % subdir.full_path
        end
        # Recreate the directory
        destroy_dirs = subdir.recreate(destroy_dirs, logger: logger)
      elsif File.file? subitem_path
        # logger.debug 'Processing file: %s' % subitem_path
        if dbfile = DataFile.find_existing(subitem_path, subitem_name)
          if dbfile.directory_id != self.id
            dbfile.directory = self
            dbfile.save!
            logger.info 'Update file: %s' % dbfile.name
          end
        elsif (File.mtime(subitem_path) + 100).past?
          dbfile = DataFile.new
          # dbfile.name = subitem_name
          dbfile.directory = self
          dbfile.manual_upload(subitem_path)
          dbfile.save!
          logger.info 'Added file: %s' % dbfile.name
        end
        # TODO: handle files that are only in database
      end
    end
    return destroy_dirs
  end

  def find_existing(subdir_name, subitem_path)
    # Find by name
    if (dir = subdirs.where(name: subdir_name)).exists?
      return dir.first
    # Problem is we can't find it if haven't got that far
    else
      Directory.where(name: subdir_name).all.each do |dir|
        unless dir.path_exists?
          return dir
        end
      end
      # TODO: use filter_map here
      # NOTE: we don't use the logic from date_file
      file_count = Dir["%s/*" % subitem_path].count{|f| File.file?(f) }
      Directory.joins(:files).group('data_files.directory_id')\
        .having('count(data_files.id) = ? and count(data_files.id) > 0', file_count).each do |dir|
          Dir.glob(File.join(dir.full_path, '*')).each do |filename|
            return false if File.size(file) != dir.files.where(name: filename).first&.size
          end
        return dir
      end
    # TODO: Find by number of files + hash of files
    end
    return false
  end

  # TODO check that you can download files
  
  def can_create? cuser
    cuser and cuser.admin?
  end

  def can_update? cuser, params = {}
    cuser and cuser.admin? and Verification.contain params, [:description, :hidden]
  end

  def can_destroy? cuser
    cuser and cuser.admin?
  end

  def self.params(params, cuser)
    params.require(:directory).permit(:description, :hidden, :name, :parent_id)
  end
end