# encoding: utf-8

require 'adhearsion/punchblock_plugin'
require 'adhearsion/linux_proc_name'
require 'rbconfig'

module Adhearsion
  class Initializer

    class << self
      def start(*args, &block)
        new(*args, &block).start
      end
    end

    DEFAULT_PID_FILE_NAME = 'adhearsion.pid'

    attr_reader :path, :daemon, :pid_file

    # Creation of pid_files
    #
    #  - You may want to have Adhearsion create a process identification
    #    file when it boots so that a crash monitoring program such as
    #    Monit can reboot if necessary or so the init script can kill it
    #    for system shutdowns.
    #  - To have Adhearsion create a pid file in the default location (i.e.
    #    AHN_INSTALL_DIR/adhearsion.pid), supply :pid_file with 'true'. Otherwise
    #    one is not created UNLESS it is running in daemon mode, in which
    #    case one is created. You can force Adhearsion to not create one
    #    even in daemon mode by supplying "false".
    def initialize(options = {})
      @@started = true
      @path     = path
      @mode     = options[:mode]
      @pid_file = options[:pid_file].nil? ? ENV['PID_FILE'] : options[:pid_file]
      @loaded_init_files  = options[:loaded_init_files]
      Adhearsion.ahn_root = '.'
    end

    def start
      resolve_pid_file_path
      load_lib_folder
      load_config
      initialize_log_paths
      daemonize! if should_daemonize?
      start_logging
      debugging_log
      launch_console if need_console?
      catch_termination_signal
      create_pid_file
      set_ahn_proc_name
      initialize_exception_logger
      update_rails_env_var
      init_plugins

      run_plugins
      trigger_after_initialized_hooks

      Adhearsion::Process.booted if Adhearsion.status == :booting

      logger.info "Adhearsion v#{Adhearsion::VERSION} initialized in \"#{Adhearsion.config.platform.environment}\"!" if Adhearsion.status == :running

      # This method will block until all important threads have finished.
      # When it does, the process will exit.
      join_important_threads
      self
    end

    def debugging_items
      [
        "OS: #{RbConfig::CONFIG['host_os']} - RUBY: #{RUBY_ENGINE} #{RUBY_VERSION}",
        "Environment: #{ENV.inspect}",
        Adhearsion.config.description(:all),
        "Gem versions: #{Gem.loaded_specs.inject([]) { |c,g| c << "#{g[0]} #{g[1].version}" }}"
      ]
    end

    def debugging_log
      debugging_items.each do |item|
        logger.trace item
      end
    end

    def update_rails_env_var
      env = ENV['AHN_ENV']
      if env && Adhearsion.config.valid_environment?(env.to_sym)
        unless ENV['RAILS_ENV']
          logger.info "Copying AHN_ENV (#{env}) to RAILS_ENV"
          ENV['RAILS_ENV'] = env
        end
      else
        unless ENV['RAILS_ENV']
          env = Adhearsion.config.platform.environment.to_s
          ENV['AHN_ENV'] = env
          logger.info "Setting RAILS_ENV to \"#{env}\""
          ENV['RAILS_ENV'] = env
        end
      end
      logger.warn "AHN_ENV(#{ENV['AHN_ENV']}) does not match RAILS_ENV(#{ENV['RAILS_ENV']})!" unless ENV['RAILS_ENV'] == ENV['AHN_ENV']
      env
    end

    def default_pid_path
      File.join Adhearsion.config.root, DEFAULT_PID_FILE_NAME
    end

    def resolve_pid_file_path
      @pid_file = if pid_file.equal?(true)
        default_pid_path
      elsif pid_file.equal?(false)
        nil
      elsif pid_file
        File.expand_path pid_file
      else
        should_daemonize? ? default_pid_path : nil
      end
    end

    def resolve_log_file_path
      _log_file = Adhearsion.config.platform.logging.outputters
      _log_file = _log_file[0] if _log_file.is_a?(Array)
      _log_file = File.expand_path(Adhearsion.config.root.dup.concat("/").concat(_log_file)) unless _log_file.start_with?("/")
      _log_file
    end

    def catch_termination_signal
      %w'INT TERM'.each do |process_signal|
        trap process_signal do
          logger.info "Received SIG#{process_signal}. Shutting down."
          Adhearsion::Process.shutdown
        end
      end

      trap 'HUP' do
        logger.debug "Received SIGHUP. Reopening logfiles."
        Adhearsion::Logging.reopen_logs
      end

      trap 'ALRM' do
        logger.debug "Received SIGALRM. Toggling trace logging."
        Adhearsion::Logging.toggle_trace!
      end

      trap 'ABRT' do
        logger.info "Received ABRT signal. Forcing stop."
        Adhearsion::Process.force_stop
      end
    end

    ##
    # Loads files in application lib folder
    # @return [Boolean] if files have been loaded (lib folder is configured to not nil and actually exists)
    def load_lib_folder
      return false if Adhearsion.config.platform.lib.nil?

      lib_folder = [Adhearsion.config.platform.root, Adhearsion.config.platform.lib].join '/'
      return false unless File.directory? lib_folder

      Dir.chdir lib_folder do
        rbfiles = File.join "**", "*.rb"
        Dir.glob(rbfiles).each do |file|
          require "#{lib_folder}/#{file}"
        end
      end
      true
    end

    def load_config
      require "#{Adhearsion.config.root}/config/adhearsion.rb"
    end

    def init_get_logging_appenders
      @file_loggers ||= memoize_logging_appenders
    end

    def memoize_logging_appenders
      appenders = Array(Adhearsion.config.platform.logging.outputters.dup)
      # Any filename in the outputters array is mapped to a ::Logging::Appenders::File instance
      appenders.map! do |a|
        case a
        when String
          f = File.expand_path(Adhearsion.config.root.dup.concat("/").concat(a)) unless a.start_with?("/")
          ::Logging.appenders.file(f,
            :layout => ::Logging.layouts.pattern(
              :pattern => Adhearsion::Logging.adhearsion_pattern
            ),
           :auto_flushing => 2,
           :flush_period => 2
          )
        else
         a
        end
      end

      if should_daemonize?
        appenders
      else
        appenders += Adhearsion::Logging.default_appenders
      end
    end

    def init_plugins
      Plugin.init_plugins
    end

    def run_plugins
      Plugin.run_plugins
    end

    def should_daemonize?
      @mode == :daemon
    end

    def need_console?
      @mode == :console
    end

    def daemonize!
      logger.info "Daemonizing now!"
      logger.debug "Creating PID file #{pid_file}"
      extend Adhearsion::CustomDaemonizer
      daemonize resolve_log_file_path
    end

    def launch_console
      Adhearsion::Process.important_threads << Thread.new do
        catching_standard_errors do
          Adhearsion::Console.run
        end
      end
    end

    # Creates the relative paths associated to log files
    # i.e.
    # - log_file = "log/adhearsion.log"      => creates 'log' folder
    # - log_file = "log/test/adhearsion.log" => creates 'log' and 'log/test' folders
    def initialize_log_paths
      outputters = Array(Adhearsion.config.platform.logging.outputters)
      outputters.select{|o| o.is_a?(String)}.each do |o|
        o = o.split("/")
        unless o[0].empty? # only if relative path
          o.pop # not consider filename
          o.inject("") do |path, folder|
            path = path.concat(folder).concat("/")
            Dir.mkdir(path) unless File.directory? path
            path
          end
        end
      end
    end

    def start_logging
      outputters = init_get_logging_appenders
      Adhearsion::Logging.start outputters, Adhearsion.config.platform.logging.level, Adhearsion.config.platform.logging.formatter
    end

    def initialize_exception_logger
      Events.register_handler :exception do |e, l|
        (l || logger).error e
      end
    end

    def create_pid_file
      return unless pid_file

      File.open pid_file, 'w' do |file|
        file.puts ::Process.pid
      end

      Events.register_callback :shutdown do
        File.delete(pid_file) if File.exists?(pid_file)
      end
    end

    def set_ahn_proc_name
      Adhearsion::LinuxProcName.set_proc_name Adhearsion.config.platform.process_name
    end

    def trigger_after_initialized_hooks
      Events.trigger_immediately :after_initialized
    end

    ##
    # This method will block Thread.main() until calling join() has returned for all Threads in Adhearsion::Process.important_threads.
    # Note: important_threads won't always contain Thread instances. It simply requires the objects respond to join().
    #
    def join_important_threads
      # Note: we're using this ugly accumulator to ensure that all threads have ended since IMPORTANT_THREADS will almost
      # certainly change sizes after this method is called.
      index = 0
      until index == Adhearsion::Process.important_threads.size
        begin
          Adhearsion::Process.important_threads[index].join
        rescue => e
          logger.error "Error after joining Thread #{Thread.inspect}. #{e.message}"
        ensure
          index = index + 1
        end
      end
    end

    InitializationFailedError = Class.new Adhearsion::Error
  end
end
