module Eye::Controller::Load

  def check(filename)
    { filename => catch_load_error(filename) { parse_config(filename).to_h } }
  end

  def explain(filename)
    { filename => catch_load_error(filename) { parse_config(filename).to_h } }
  end

  def load(*args)
    args.extract_options!
    obj_strs = args.flatten
    info "=> loading: #{obj_strs}"

    res = Hash.new

    globbing(*obj_strs).each do |filename|
      res[filename] = catch_load_error(filename) do
        cfg = parse_config(filename)
        load_config(filename, cfg)
        nil
      end
    end

    set_proc_line

    info "<= loading: #{obj_strs}"

    res
  end

private

  # regexp for clean backtrace to show for user
  BT_REGX = %r[/lib/eye/|lib/celluloid|internal:prelude|logger.rb:|active_support/core_ext|shellwords.rb|kernel/bootstrap].freeze

  def catch_load_error(filename = nil, &block)
    { :error => false, :config => yield }

  rescue Eye::Dsl::Error, Exception, NoMethodError => ex
    raise if ex.class.to_s.include?('RR') # skip RR exceptions

    error "loading: config error <#{filename}>: #{ex.message}"

    # filter backtrace for user output
    bt = (ex.backtrace || [])
    bt = bt.reject{|line| line.to_s =~ BT_REGX } unless ENV['EYE_FULL_BACKTRACE']
    error bt.join("\n")

    res = { :error => true, :message => ex.message }
    res[:backtrace] = bt if bt.present?
    res
  end

  def globbing(*obj_strs)
    res = []
    return res if obj_strs.empty?

    obj_strs.each do |filename|
      mask = File.directory?(filename) ? File.join(filename, '{*.eye}') : filename
      debug { "loading: globbing mask #{mask}" }

      sub = []
      Dir[mask].each do |config_path|
        sub << config_path
      end
      sub = [mask] if sub.empty?

      res += sub
    end

    res
  end

  # return: result, config
  def parse_config(filename)
    debug { "parsing: #{filename}" }

    cfg = Eye::Dsl.parse(nil, filename)
    @current_config.merge(cfg).validate! # just validate summary config here
    Eye.parsed_config = nil # remove link on config, for better gc
    cfg
  end

  # !!! exclusive operation
  def load_config(filename, config)
    info "loading: #{filename}"
    new_cfg = @current_config.merge(config)
    new_cfg.validate!(config.application_names)

    load_options(new_cfg.settings)
    create_objects(new_cfg.applications, config.application_names)
    @current_config = new_cfg
  end

  # load global config options
  def load_options(opts)
    return if opts.blank?

    opts.each do |key, value|
      method = "set_opt_#{key}"
      send(method, value) if value && respond_to?(method)
    end
  end

  # create objects as diff, from configs
  def create_objects(apps_config, changed_apps = [])
    debug { 'creating objects' }

    apps_config.each do |app_name, app_cfg|
      update_or_create_application(app_name, app_cfg.clone) if changed_apps.include?(app_name)
    end

    # sorting applications
    @applications.sort_by!(&:name)
  end

  def update_or_create_application(app_name, app_config)
    @old_groups = {}
    @old_processes = {}

    app = @applications.detect { |c| c.name == app_name }

    if app
      app.groups.each do |group|
        @old_groups[group.name] = group
        group.processes.each do |proc|
          @old_processes[group.name + ":" + proc.name] = proc
        end
      end

      @applications.delete(app)

      debug { "updating app: #{app_name}" }
    else
      debug { "creating app: #{app_name}" }
    end

    app = Eye::Application.new(app_name, app_config)
    @applications << app
    @added_groups, @added_processes = [], []

    new_groups = app_config.delete(:groups) || {}
    new_groups.each do |group_name, group_cfg|
      group = update_or_create_group(group_name, group_cfg.clone)
      app.add_group(group)
      group.resort_processes
    end

    # now, need to clear @old_groups, and @old_processes
    @old_groups.each{|_, group| group.clear; group.send_command(:delete) }
    @old_processes.each{|_, process| process.send_command(:delete) if process.alive? }

    # schedule monitoring for new groups, processes
    added_fully_groups = []
    @added_groups.each do |group|
      if group.processes.size > 0 && (group.processes.pure - @added_processes).size == 0
        added_fully_groups << group
        @added_processes -= group.processes.pure
      end
    end

    added_fully_groups.each{|group| group.send_command :monitor }
    @added_processes.each{|process| process.send_command :monitor }

    # remove links to prevent memory leaks
    @old_groups = nil
    @old_processes = nil
    @added_groups = nil
    @added_processes = nil

    app.resort_groups

    app
  end

  def update_or_create_group(group_name, group_config)
    group = if @old_groups[group_name]
      debug { "updating group: #{group_name}" }
      group = @old_groups.delete(group_name)
      group.schedule :update_config, group_config, Eye::Reason::User.new(:'load config')
      group.clear
      group
    else
      debug { "creating group: #{group_name}" }
      gr = Eye::Group.new(group_name, group_config)
      @added_groups << gr
      gr
    end

    processes = group_config.delete(:processes) || {}
    processes.each do |process_name, process_cfg|
      process = update_or_create_process(process_name, process_cfg.clone)
      group.add_process(process)
    end

    group
  end

  def update_or_create_process(process_name, process_cfg)
    postfix = ":" + process_name
    name = process_cfg[:group] + postfix
    key = @old_processes[name] ? name : @old_processes.keys.detect { |n| n.end_with?(postfix) }

    if @old_processes[key]
      debug { "updating process: #{name}" }
      process = @old_processes.delete(key)
      process.schedule :update_config, process_cfg, Eye::Reason::User.new(:'load config')
      process
    else
      debug { "creating process: #{name}" }
      process = Eye::Process.new(process_cfg)
      @added_processes << process
      process
    end
  end

end
