module Timetrap
  module CLI
    extend Helpers
    attr_accessor :args
    extend self

    USAGE = <<-EOF

Timetrap - Simple Time Tracking

Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]

COMMAND can be abbreviated. For example `t in` and `t i` are equivalent.

COMMAND is one of:

  * archive - Move entries to a hidden sheet (by default named '_[SHEET]') so
      they're out of the way.
    usage: t archive [--start DATE] [--end DATE] [SHEET]
    -s, --start <date:qs>     Include entries that start on this date or later
    -e, --end <date:qs>       Include entries that start on this date or earlier
    -g, --grep <regexp>       Include entries where the note matches this regexp.

  * backend - Open an sqlite shell to the database.
    usage: t backend

  * configure - Write out a YAML config file. Print path to config file.  The
      file may contain ERB.
    usage: t configure
    Currently supported options are:
      round_in_seconds:       The duration of time to use for rounding with
                              the -r flag
      database_file:          The file path of the sqlite database
      append_notes_delimiter: delimiter used when appending notes via
                              t edit --append
      formatter_search_paths: an array of directories to search for user
                              defined fomatter classes
      default_formatter:      The format to use when display is invoked without a
                              `--format` option
      default_command:        The default command to run when calling t.
      auto_checkout:          Automatically check out of running entries when
                              you check in or out
      require_note:           Prompt for a note if one isn't provided when
                              checking in

  * display - Display the current timesheet or a specific. Pass `all' as SHEET
      to display all unarchived sheets or `full' to display archived and
      unarchived sheets.
    usage: t display [--ids] [--start DATE] [--end DATE] [--format FMT] [SHEET | all | full]
    -v, --ids                 Print database ids (for use with edit)
    -s, --start <date:qs>     Include entries that start on this date or later
    -e, --end <date:qs>       Include entries that start on this date or earlier
    -f, --format <format>     The output format.  Valid built-in formats are
                              ical, csv, json, ids, factor, and text (default).
                              Documentation on defining custom formats can be
                              found in the README included in this
                              distribution.
    -g, --grep <regexp>       Include entries where the note matches this regexp.

  * edit - Alter an entry's note, start, or end time. Defaults to the active
    entry. Defaults to the last entry to be checked out of if no entry is active.
    usage: t edit [--id ID] [--start TIME] [--end TIME] [--append] [NOTES]
    -i, --id <id:i>           Alter entry with id <id> instead of the running entry
    -s, --start <time:qs>     Change the start time to <time>
    -e, --end <time:qs>       Change the end time to <time>
    -z, --append              Append to the current note instead of replacing it
                                the delimiter between appended notes is
                                configurable (see configure)
    -m, --move <sheet>        Move to another sheet

  * in - Start the timer for the current timesheet.
    usage: t in [--at TIME] [NOTES]
    -a, --at <time:qs>        Use this time instead of now

  * kill - Delete a timesheet or an entry.
    usage: t kill [--id ID] [TIMESHEET]
    -i, --id <id:i>           Alter entry with id <id> instead of the running entry

  * list - Show the available timesheets.
    usage: t list

  * now - Show all running entries.
    usage: t now

  * out - Stop the timer for a timesheet.
    usage: t out [--at TIME] [TIMESHEET]
    -a, --at <time:qs>        Use this time instead of now

  * resume - Start the timer for the current time sheet for an entry. Defaults
      to the active entry.
    usage: t resume [--id ID] [--at TIME]
    -i, --id <id:i>           Resume entry with id <id> instead of the last entry
    -a, --at <time:qs>        Use this time instead of now

  * sheet - Switch to a timesheet creating it if necessary. When no sheet is
      specified list all sheets. The special sheetname '-' will switch to the
      last active sheet.
    usage: t sheet [TIMESHEET]

  * today - Shortcut for display with start date as the current day
    usage: t today [--ids] [--format FMT] [SHEET | all]

  * yesterday - Shortcut for display with start and end dates as the day before the current day
    usage: t yesterday [--ids] [--format FMT] [SHEET | all]

  * week - Shortcut for display with start date set to monday of this week.
    usage: t week [--ids] [--end DATE] [--format FMT] [SHEET | all]

  * month - Shortcut for display with start date set to the beginning of either
      this month or a specified month.
    usage: t month [--ids] [--start MONTH] [--format FMT] [SHEET | all]

  OTHER OPTIONS

  -h, --help              Display this help.
  -r, --round             Round output to 15 minute start and end times.
  -y, --yes               Noninteractive, assume yes as answer to all prompts.
  --debug                 Display stack traces for errors.

  EXAMPLES

  # create the "MyTimesheet" timesheet
  $ t sheet MyTimesheet

  # check in 5 minutes ago with a note
  $ t in --at '5 minutes ago' doing some stuff

  # check out
  $ t out

  # view current timesheet
  $ t display

  Submit bugs and feature requests to http://github.com/samg/timetrap/issues
    EOF

    def parse arguments
      args.parse arguments
    end

    def invoke
      args['-h'] ? puts(USAGE) : invoke_command_if_valid
    rescue StandardError, LoadError => e
      raise e if args['--debug']
      warn e.message
      exit 1 unless defined? TEST_MODE
    end

    def commands
      Timetrap::CLI::USAGE.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
    end

    def deprecated_commands
      {
        'switch' => 'sheet',
        'running' => 'now',
        'format' => 'display'
      }
    end

    def invoke_command_if_valid
      if args.unused.empty? && Timetrap::Config['default_command']
        self.args = Getopt::Declare.new(USAGE.dup, Timetrap::Config['default_command'])
      end
      command = args.unused.shift
      set_global_options
      case (valid = commands.select{|name| name =~ %r|^#{command}|}).size
      when 1 then send valid[0]
      else
        handle_invalid_command(command)
      end
    end

    def valid_command(command)
       return commands.include?(command)
    end

    def handle_invalid_command(command)
      if !command
        puts USAGE
      elsif mapping = deprecated_commands.detect{|(k,v)| k =~ %r|^#{command}|}
        deprecated, current = *mapping
        warn "The #{deprecated.inspect} command is deprecated in favor of #{current.inspect}. Sorry for the inconvenience."
        send current
      else
        warn "Invalid command: #{command.inspect}"
      end
    end

    # currently just sets whether output should be rounded to 15 min intervals
    def set_global_options
      Timetrap::Entry.round = true if args['-r']
    end

    def archive
      ee = selected_entries
      if ask_user "Archive #{ee.count} entries? "
        ee.each do |e|
          next unless e.end
          e.update :sheet => "_#{e.sheet}"
        end
      else
        warn "archive aborted!"
      end
    end

    def configure
      Config.configure!
      puts "Config file is at #{Config::PATH.inspect}"
    end

    def edit
      entry = case
              when args['-i']
                warn "Editing entry with id #{args['-i'].inspect}"
                Entry[args['-i']]
              when Timer.active_entry
                warn "Editing running entry"
                Timer.active_entry
              when Timer.last_checkout
                warn  "Editing last entry you checked out of"
                Timer.last_checkout
              end

      unless entry
        warn "Can't find entry"
        return
      end
      warn ""

      entry.update :start => args['-s'] if args['-s'] =~ /.+/
      entry.update :end => args['-e'] if args['-e'] =~ /.+/

      # update sheet
      if args['-m'] =~ /.+/
        if entry == Timer.active_entry
          Timer.current_sheet = args['-m']
        end
        entry.update :sheet => args['-m']
      end

      # update notes
      if unused_args =~ /.+/
        note = unused_args
        if args['-z']
          note = [entry.note, note].join(Config['append_notes_delimiter'])
        end
        entry.update :note => note
      end

      puts format_entries(entry)
    end

    def backend
      exec "sqlite3 #{DB_NAME}"
    end

    def in
      if Config['auto_checkout']
        Timer.stop_all(args['-a']).each do |checked_out_of|
          warn "Checked out of sheet #{checked_out_of.sheet.inspect}."
        end
      end

      if Config['require_note'] && !Timer.running? && unused_args.empty?
        $stderr.print("Please enter a note for this entry:\n> ")
        self.unused_args = $stdin.gets
      end

      Timer.start unused_args, args['-a']
      warn "Checked into sheet #{Timer.current_sheet.inspect}."
    end

    def resume
      entry = case
              when args['-i']
                entry = Entry[args['-i']]
                warn "Resuming entry with id #{args['-i'].inspect} (#{entry.note})"
                entry
              when Timer.last_checkout
                last = Timer.last_checkout
                warn "Resuming last entry you checked out of (#{last.note})"
                last
              end

      unless entry
        warn "Can't find entry"
        return
      end

      self.unused_args = entry.note || unused_args

      self.in
    end

    def out
      if Config['auto_checkout']
        stopped = Timer.stop_all(args['-a']).each do |checked_out_of|
          warn "Checked out of sheet #{checked_out_of.sheet.inspect}."
        end
        if stopped.empty?
          warn "No running entries to stop."
        end
      else
        sheet = sheet_name_from_string(unused_args)
        if Timer.stop sheet, args['-a']
          warn "Checked out of sheet #{sheet.inspect}."
        else
          warn "No running entry on sheet #{sheet.inspect}."
        end
      end
    end

    def kill
      if e = Entry[args['-i']]
        out = "are you sure you want to delete entry #{e.id}? "
        out << "(#{e.note}) " if e.note.to_s =~ /.+/
        if ask_user out
          e.destroy
          warn "it's dead"
        else
          warn "will not kill"
        end
      elsif (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet = unused_args)
        victims = Entry.filter(:sheet => sheet).count
        if ask_user "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
          Entry.filter(:sheet => sheet).destroy
          warn "killed #{victims} entries"
        else
          warn "will not kill"
        end
      else
        victim = args['-i'] ? args['-i'].to_s.inspect : sheet.inspect
        warn ["can't find #{victim} to kill", 'sheets:', *sheets].join("\n")
      end
    end

    def display
      entries = selected_entries
      if entries == []
        warn "No entries were selected to display."
      else
        puts format_entries(entries)
      end
    end

    def sheet
      sheet = unused_args
      case sheet
      when nil, ''
        list
        return
      when '-'
        if Timer.last_sheet
          sheet = Timer.last_sheet
        else
          warn 'LAST_SHEET is not set'
          return
        end
      end

      Timer.current_sheet = sheet
      warn "Switching to sheet #{sheet.inspect}"
    end

    def list
      sheets = ([Timer.current_sheet] | Entry.sheets).map do |sheet|
        sheet_atts = {:total => 0, :running => 0, :today => 0}
        entries = Timetrap::Entry.filter(:sheet => sheet)
        if entries.empty?
          sheet_atts.merge(:name => sheet)
        else
          entries.inject(sheet_atts) do |m, e|
            e_end = e.end_or_now
            m[:name] ||= sheet
            m[:total] += (e_end.to_i - e.start.to_i)
            m[:running] += (e_end.to_i - e.start.to_i) unless e.end
            m[:today] += (e_end.to_i - e.start.to_i) if same_day?(Time.now, e.start)
            m
          end
        end
      end.sort_by{|sheet| sheet[:name].downcase}
      width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
      width = 10 if width < 10
      puts " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
      sheets.each do |sheet|
        star = sheet[:name] == Timer.current_sheet ? '*' : sheet[:name] == Timer.last_sheet ? '-' : ' '
        puts "#{star}%-#{width}s%-12s%-12s%s" % [
          sheet[:running],
          sheet[:today],
          sheet[:total]
        ].map(&method(:format_seconds)).unshift(sheet[:name])
      end
    end

    def now
      if !Timer.running?
        warn "*#{Timer.current_sheet}: not running"
      end
      Timer.running_entries.each do |entry|
        current = entry.sheet == Timer.current_sheet
        out = current ? '*' : ' '
        out << "#{entry.sheet}: #{format_duration(entry.duration)}".gsub(/  /, ' ')
        out << " (#{entry.note})" if entry.note =~ /.+/
        puts out
      end
    end

    def today
        args['-s'] = Date.today.to_s
        display
    end

    def yesterday
      yesterday = (Date.today - 1).to_s
      args['-s'] = yesterday
      args['-e'] = yesterday
      display
    end

    def week
      args['-s'] = Date.today.wday == 1 ? Date.today.to_s : Date.parse(Chronic.parse(%q(last monday)).to_s).to_s
      display
    end

    def month
      d = Chronic.parse( args['-s'] || Date.today )

      beginning_of_month = Date.new( d.year, d.month )
      end_of_month = if d.month == 12 # handle edgecase
        Date.new( d.year + 1, 1) - 1
      else
        Date.new( d.year, d.month+1 ) - 1
      end
      args['-s'] = beginning_of_month.to_s
      args['-e'] = end_of_month.to_s
      display
    end

    private

    def unused_args
      args.unused.join(' ')
    end

    def unused_args=(str)
      args.unused = str.split
    end

    def ask_user question
      return true if args['-y']
      $stderr.print question
      $stdin.gets =~ /\Aye?s?\Z/i
    end

    extend Helpers::AutoLoad
    def format_entries(entries)
      load_formatter(args['-f'] || Config['default_formatter']).new(Array(entries)).output
    end

  end
end