module Ubiquity
  module Daemon
    module Runner
      extend ActiveSupport::Concern

      module ClassMethods
        attr_accessor :options, :command, :executable, :rootdir

        ##
        # Run
        ##
        def execute(args, rootdir)
          throw "Not loggable. Please include Ubiquity::Daemon::Loggable into #{self}" unless respond_to?(:log)

          # set root dir
          @rootdir = rootdir

          log("root=#{@rootdir}")

          # extract the command
          cmd = args.delete_at(0)
          cmd = cmd.to_sym unless cmd.nil?

          # parse the command line options
          @options = {}

          # set the default options
          set_default_options
          parser = create_options_parser
          parser.parse(args)

          # ensure log and pid directories exist
          ensure_directories_exist


          # validate the command line options
          if respond_to?(:command_line_options_valid?)
            unless command_line_options_valid?
              puts parser.help
              exit
            end
          end

          case cmd
          when :start then start
          when :stop then stop
          else
            puts parser.help
          end
        end

        ##
        #
        ##
        def start
          begin
            if options[:daemonize]
              log("Starting daemonized process")
              daemonized_start
            else
              log("Starting interactive process")
              interactive_start
            end
          rescue => e
            log("Exception: #{e.inspect}",:error)

            e.backtrace.each do |line|
              log(line, :error)
            end

            delete_pid
            abort("Exit: fatal exception")
          end
        end



        ##
        #
        ##
        def stop
        end

        ##
        #
        ##
        def server_name(name)
          @server_name = name
        end

        ##
        #
        ##
        def server_executable(exe)
          @server_executable = exe
        end

        ##
        #
        ##
        def default_configuration_file(file)
          @default_config_file = file
        end

        private

        ##
        #
        ##
        def daemonized_start
          ::Process.fork do
            ::Process.setsid
            STDIN.reopen("/dev/null")
            STDOUT.reopen(options[:log_file],'a')
            STDERR.reopen(STDOUT)
            save_pid(::Process.pid)
            options[:pid] = ::Process.pid
            start_reactor(create_instance(options, load_config(options)))
          end
        end

        ##
        #
        ##
        def interactive_start
          save_pid(::Process.pid)
          options[:pid] = ::Process.pid
          start_reactor(create_instance(options, load_config(options)))
        end

        ##
        # Save the pid to file
        ##
        def save_pid(pid)
          if File.file?(options[:pid_file])
            log("Deleting stale PID file")
            File.delete(options[:pid_file])
          end

          File.open(options[:pid_file], 'w') do |f|
            f.write(options[:pid])
          end
        end

        ##
        #
        ##
        def delete_pid
          if File.file?(options[:pid_file])
            log("Deleting PID file")
            File.delete(options[:pid_file])
          else
            log("Error deleting PID file. It does not exist")
          end
        end

        ##
        #
        ##
        def load_config(options)
          begin
            buffer = []
            IO.readlines(options[:config_file]).each do |line|
              line = line.strip
              buffer << line unless line.start_with?("//")
            end
            buffer = buffer.join("\n")
            buffer = apply_config_dynamic_variables(buffer, options)
            config = Yajl::Parser.parse(buffer)
            config.recursive_symbolize_keys!
            return config
          rescue => e
            log("error in config file")
            puts e
            return {}
          end
        end


        ##
        # The config file can have {{token}} tokens in it. The concrete
        # runner should be able to respond to the token.
        # I.e. if the log file has {{host}} in it then the concrete runner
        # should implement a method called host the returns a string.
        ##
        def apply_config_dynamic_variables(buffer, options)
          matches = buffer.scan(/{{.*}}/)
          matches.each do |match|
            token = match[2..match.length-3].to_sym
            if respond_to?(token)
              replace = send(token, options)
              buffer = buffer.gsub(match, replace)
            else
              log "Token: #{token} in configuration file but runner does not respond to such event"
            end
          end
          return buffer
        end

        ##
        #
        ##
        def start_reactor(server)
          log "pid=#{options[:pid]}"
          log "id=#{options[:id]}"

          EM.epoll
          EM.run {
            trap("INT") {
              log("shutting down")
              server.stop
              EM.stop
            }
            server.setup
            server.start
          }
          delete_pid
        end

        ##
        #
        ##
        def expand_path(root, file)
          ['.','/'].each {|prefix| return file if file[0] == prefix}
          File.join(root,file)
        end


        ##
        #
        ##
        def set_default_options
          options_defaults.each {|proc| proc.call(options)}

          options[:id] = "#{::Process.pid}" unless options.include?(:id)
          options[:pid_file] = default_pid_file unless options.include?(:pid_file)
          options[:log_file] = default_log_file unless options.include?(:log_file)
          options[:config_file] = default_config_file unless options.include?(:config_file)
          options[:verbose] = false unless options.include?(:verbose)
          options[:daemonize] = false unless options.include?(:daemonize)

          options[:config_file] = expand_path(rootdir, options[:config_file])
        end

        ##
        # Ensure that the log & pid directories exist. If they do not, create them
        ##
        def ensure_directories_exist
          log = File.dirname(options[:log_file])
          pid = File.dirname(options[:pid_file])
          Dir.mkdir(log) unless File.directory?(log)
          Dir.mkdir(pid) unless File.directory?(pid)
        end

        ##
        #
        ##
        def default_config_file
          @default_config_file ||= ""
        end

        ##
        #
        ##
        def default_pid_file
          File.join(rootdir, 'pid', "#{@server_executable}-#{options[:id]}.pid")
        end

        ##
        #
        ##
        def default_log_file
          File.join(rootdir, 'log',"#{@server_executable}-#{options[:id]}.log")
        end

        ##
        # Callback hooks to build the option parser
        ##
        def options_parser_builders
          @options_parser_builders ||= []
        end

        ##
        # Register a options parser builder callback
        ##
        def register_options_parser_builder(proc)
          options_parser_builders << proc
        end

        ##
        # Callback hooks to set options default values
        ##
        def options_defaults
          @options_defaults ||= []
        end

        ##
        # Register a callback to set defaults
        ##
        def register_options_defaults_handler(proc)
          options_defaults << proc
        end

        ##
        # Create an options parser
        ##
        def create_options_parser
          OptionParser.new do |parser|
            parser.banner = "\nUsage: ./#{executable} <command> <options>\n\nCommands: start, stop, status"

            parser.separator ""
            parser.separator "Server options:"
            parser.on('-c', '--config config_file', "Config file name") {|v| options[:config_file] = v }
            parser.on('-i', '--id id', "Server id (defaults pid)") {|v| options[:id] = v }
            parser.separator ""
            parser.separator "Daemon options:"
            parser.on('-d', '--daemon', "Daemonize the server (default: false)") {|v| options[:daemonize] = true }
            parser.on('-p', '--pid', "Specify the pid file name (default: <root>/log/<id>.pid)") { |v| options[:pid_file] = v }
            parser.on('-l', '--log', "Specify the pid file name (default: <root>/log/<id>.log)") { |v| options[:log_file] = v }



            options_parser_builders.each {|watch| watch.call(parser) }
          end
        end


      end
    end
  end
end
