require 'yaml'
require 'log4r'
require 'breaktime/schedule'
require 'dante'
require 'breaktime/cli'

# Handles configuration options, CLI stuff and process daemonizing.
class Breaktime::Main
  include Log4r

  attr_reader :options, :log, :cli

  # Default options that can be overridden in the YAML file.
  DEFAULT_OPTIONS = {'interval' => 60,
                     'daemonize' => true,
                     'days' => ['monday',
                                'tuesday',
                                'wednesday',
                                'thursday',
                                'friday',
                                'saturday',
                                'sunday']}

  # Set up the logger, parse CLI options and the YAML file.
  def initialize
    create_logger 'error'
    @cli = Breaktime::CLI.new
    @options = DEFAULT_OPTIONS
    set_log_level @cli.options[:level]

    parse_yaml_file

    @options['log_path'] = @cli.options[:log_file]
    @options['pid_path'] = @cli.options[:pid_file]
  end

  # Exit with a trollop message.
  def die(message)
    Trollop::die message
  end

  # Print out the gem motto and exit with the given exit code.
  def say_goodbye(exit_code)
    puts "\n\s\sBreaktime says, \"Have a break, have an unbranded chocolate snack.\""
    exit exit_code
  end

  # Run the given mode.
  #
  # The mode is one of the command line modes (run with the --help flag).
  def run_mode(mode)
    command = Breaktime::Command.new @options['command'], @log

    # Switch on CLI mode.
    case mode
    # Schedule the breaktime.
    when "start"
      @log.info { "When it's breaktime I'll run: `#{command.command}`" }
      startd

    # Stop a currently running daemonized process.
    when "stop"
      @log.info { "Stopping breaktime background process" }
      stopd
      say_goodbye Breaktime::EX_OK

    # Open a dialog to notify the user about an impending screen break.
    when "dialog"
			@log.info { "Opening dialog" }
      require 'breaktime/dialog'
      # Automatically loads green shoes window

    # Run the command that will start the break.
    when "now"
      command.execute

    # Unknown mode.
    else
      die "unknown mode #{mode.inspect}"

    end
  end

  # Check if Process has the fork method, but also check for Java
  #
  # Apparently jRuby doesn't implement this correctly, hence the secondary
  # check.
  def fork_implemented?
    if Process.respond_to?(:fork)
      RUBY_PLATFORM !~ /java/ && RUBY_ENGINE != 'macruby'
    else
      false
    end
  end

  # Start the scheduler as a daemon.
  #
  # The process can be kept on top if the "daemonize" option is set to be false
  # in the configuration YAML file.
  #
  # The logger output format is changed to add the time, as this is more
  # helpful for debugging.
  #
  # Uses Dante for daemonizing.
  def startd
    dante_opts = {:daemonize => @options['daemonize'],
                  :pid_path => @options['pid_path'],
                  :log_path => @options['log_path']}

    if dante_opts[:daemonize]
      if not fork_implemented?
        dante_opts[:daemonize] = false
        @log.warn { "Daemonizing is not available, as fork is not implemented - running on top" }
      else
        @log.info { "Starting daemon, PID file => #{dante_opts[:pid_path]}, log file => #{dante_opts[:log_path]}" }
      end
    end

    schedule = Breaktime::Schedule.new(@options['interval'], @options['days'], @cli.options, @log)

    Dante::Runner.new('breaktime').execute(dante_opts) do
      schedule.start
    end
  end

  # Stop the daemonized process, if it is running.
  def stopd
    dante_opts = {:kill => true,
                  :pid_path => @options['pid_path']}
    Dante::Runner.new('breaktime').execute(dante_opts)
  end

  class << self
    # Create a new Main object and run the mode given as a CLI parameter.
    #
    # Also rescue exceptions and display helpful messages.
    #
    # TODO: tidy this up
    def start
      main = self.new
      main.log.debug { "Starting cli mode: #{main.cli.mode}" }

      begin
        main.run_mode main.cli.mode
      # Exception handling and appropriate exit codes.
      rescue Breaktime::LinuxWinManager::ManagerUnknown
        main.log.fatal do
          <<-FATAL
    It looks like you're using Linux, but I'm unable to detect your window manager to determine how to start your screensaver.

    To get round this problem, just specify a "command" in your $HOME/.breaktime.yml file, and this will be executed at the start of your break.
          FATAL
        end
        exit Breaktime::EX_LINUX_WM_UNKNOWN

      rescue Breaktime::Command::OSUnknown
        main.log.fatal do
          <<-FATAL
    I can't work out which operating system you're using. If you think this is unreasonable then please let me know on Github.

    To get round this problem in the meantime, just specify a "command" in your $HOME/.breaktime.yml file, and this will be executed at the start of your break.
            FATAL
        end
        exit Breaktime::EX_OS_UNKNOWN

      rescue Interrupt
        main.log.warn { "Caught Control-C, shutting down..." }
        main.say_goodbye Breaktime::EX_INTERRUPT

      rescue SignalException => e
        main.log.warn { "Caught signal #{e.message}, shutting down..." }
        main.say_goodbye Breaktime::EX_SIGNAL

      rescue SystemExit
        raise

      rescue Exception => e
        main.log.fatal { "Unexpected exception {#{e.class.name}}: #{e.message}" }
        main.log.debug { $!.backtrace.join("\n\t") }
        exit Breaktime::EX_UNKNOWN

      end
    end
  end

  private

  # Create a Log4r logger with the given log level.
  #
  # The logger prints to stdout by default, and spits out the level, time and
  # message.
  def create_logger(level)
    @log = Logger.new 'breaktime'
    outputter = Outputter.stdout
    outputter.formatter = PatternFormatter.new(:pattern => "[%l] %d :: %m")
    @log.outputters << outputter
    @log.level = ERROR
  end

  # Overwrite the current logger level.
  def set_log_level(level)
    @log.level = self.class.const_get(level.upcase)
  end

  # Parse the YAML configuration file.
  #
  # Any errors in the parsing cause the program to exit, but a YAML file is not
  # required - the defaults are used if it doesn't exist.
  #
  # TODO: separate into separate module/class?
  def parse_yaml_file
    @log.debug { "Configuration yaml file: #{@cli.options[:config]}" }
    if File.exist? @cli.options[:config]
      begin
        @options.merge! YAML.load_file(@cli.options[:config])
      rescue Exception => e
        @log.debug { e.message }
        Trollop::die :config, "must be a valid yaml file"
      end
    elsif @cli.options[:config] != Breaktime::CLI::DEFAULT_CONFIG
      Trollop::die :config, "must be a valid yaml file"
    else
      @log.warn { "No configuration file found at #{Breaktime::CLI::DEFAULT_CONFIG}, using defaults" }
      @log.info { "Check the README for info on creating a configuration file" }
    end
  end
end
