#!/usr/bin/env ruby

################################################################
# rbenv support:
# If this file is a symlink, and bound to a specific ruby
# version via rbenv (indicated by RBENV_VERSION),
# I want to resolve the symlink and re-exec
# the original executable respecting the .ruby_version
# which should indicate the right version.
#
if File.symlink?(__FILE__) and ENV["RBENV_VERSION"]
  ENV["RBENV_VERSION"] = nil
  shims_path = File.expand_path("shims", ENV["RBENV_ROOT"])
  ENV["PATH"] = shims_path + ":" + ENV["PATH"]
  exec(File.readlink(__FILE__), *ARGV)
end

gemfile = File.expand_path("../../Gemfile", __FILE__)

if File.exists?(gemfile + ".lock")
  ENV["BUNDLE_GEMFILE"] = gemfile
  require "bundler/setup"
end

require "rubygems"
require "thor"
require "mhc"

Encoding.default_external="UTF-8"

class MhcCLI < Thor
  ################################################################
  # constants

  DEFAULT_CONFIG_HOME = File.join((ENV["XDG_CONFIG_HOME"] || "~/.config"), "mhc")
  DEFAULT_CONFIG_FILE = "config.yml"
  DEFAULT_CONFIG_PATH = File.join(DEFAULT_CONFIG_HOME, DEFAULT_CONFIG_FILE)

  package_name 'MHC'

  ################################################################
  # class methods

  class << self
    attr_accessor :calendar
    attr_accessor :popular_options
  end

  def self.register_option(name, options)
    @popular_options ||= {}
    @popular_options[name] = options
  end

  def self.named_option(*names)
    names.each do |name|
      method_option name, @popular_options[name]
    end
  end

  ################################################################
  # global options

  class_option :debug,   :desc => "Set debug flag", :type => :boolean
  class_option :profile, :desc => "Set profiler flag", :type => :boolean
  class_option :config,  :desc => "Set config path (default: #{DEFAULT_CONFIG_PATH})", :banner => "FILE"

  check_unknown_options! :except => :completions

  ################################################################
  # frequently used options

  register_option :repository, :desc => "Set MHC top directory", :banner => "DIRECTORY"
  register_option :calendar,   :desc => "Set source CALENDAR"
  register_option :category,   :desc => "Pick items only in CATEGORY"
  register_option :format,     :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json)
  register_option :search,     :desc => "Search items by complex expression"
  register_option :dry_run,    :desc => "Perform a trial run with no changes made", :type => :boolean

  ################################################################
  # command name mappings

  map ["--version", "-v"] => :version

  map ["--help", "-h"] => :help
  default_command :help

  ################################################################
  # Command: help
  ################################################################

  desc "help [COMMAND]", "Describe available commands or one specific command"
  def help(command = nil)
    super(command)
  end

  ################################################################
  # Command: version
  ################################################################
  desc "version", "Show version"

  def version
    puts Mhc::VERSION
  end

  ################################################################
  # Command: cache
  ################################################################
  desc "cache", "Dump cache file"

  named_option :repository

  def cache
    Mhc::Command::Cache.new(builder.datastore)
  end

  # Command: todo
  desc "todo", "List Todo entries in MHC calendar"

  named_option :repository
  method_option :show_all, :desc => "Include all finished tasks."

  def todo
    todos = []
    calendar.tasks.each do |task|
      if task.recurring?
        # Yearly: today - 90days .. today + 365d - 90days ?
        # Weekly: today - 7days .. today + 7days
        search_range = Mhc::PropertyValue::Date.parse_range("today+365d")
        # search_range = nil
      else
        search_range = nil
      end
      next if task.in_category?("done") && !options[:show_all]
      todos << task.occurrences(range: search_range).first
    end
    todos.each.sort{|a, b| a.dtstart <=> b.dtstart}.each do |t|
      deadline = t.dtstart
      deadline_string = ""
      remaining = (deadline - Mhc::PropertyValue::Date.today).to_i
      if remaining == 0
        deadline_string = " (due this date)"
      elsif remaining > 0
        deadline_string = format(" (%d days to go)", remaining)
      else
        deadline_string = format(" (%d days overdue)", -remaining)
      end
      location_string = " [#{t.location}]" if !t.location.empty?
      puts format("%s %-11s %s%s%s",
                  deadline.strftime("%Y/%m/%d %a"),
                  t.time_range.to_mhc_string,
                  t.subject, location_string, deadline_string)
    end
  end # todo

  ################################################################
  # Command: completions
  ################################################################
  desc "completions [COMMAND]", "List available commands or options for COMMAND", :hide => true

  long_desc <<-LONGDESC
    List available commands or options for COMMAND
    This is supposed to be a zsh compsys helper"
  LONGDESC

  def completions(*command)
    help = self.class.commands
    global_options = self.class.class_options
    Mhc::Command::Completions.new(help, global_options, command, config)
  end

  ################################################################
  # Command: config
  ################################################################
  desc "configuration", "Show current configuration in various formats."

  named_option :format

  def configuration(name = nil)
    puts Mhc::Converter::Emacs.new.to_emacs(config.get_value(name))
  end

  ################################################################
  # Command: init
  ################################################################
  desc "init DIRECTORY", "Initialize MHC repository and configuration template"

  def init(top_dir)
    Mhc::Command::Init.new(top_dir, options[:config] || DEFAULT_CONFIG_PATH, ENV["MHC_TZID"])
  end

  ################################################################
  # Command: scan
  ################################################################
  desc "scan RANGE", "Scan events in date RANGE"

  long_desc <<-LONGDESC
    scan events in date RANGE.

    RANGE is one of:
    \x5 + START-YYYYMMDD
    \x5 + START[+LENGTH]

    START is one of:
    \x5 + today, tomorrow, sun ... sat, yyyymmdd
    \x5 + thismonth, nextmonth, yyyymm

    LENGTH is a number followed by a SUFFIX. SUFFIX is one of:
    \x5 + d (days)
    \x5 + w (weeks)
    \x5 + m (months)

    If LENGTH is omitted, it is treated as '1d' or '1m' depending on
    which type of START is set.

    Examples:
    \x5 mhc scan 20140101-20141231
    \x5 mhc scan 2140101+3d
    \x5 mhc scan today --category 'Business'
    \x5 mhc scan thismonth --search 'category:Business & !subject:"Trip"'
  LONGDESC

  named_option :calendar, :category, :format, :repository, :search

  def scan(range)
    begin
      Mhc::Command::Scan.new(calendar, range, **symbolize_keys(options))
    rescue Mhc::PropertyValue::ParseError, Mhc::Formatter::NameError, Mhc::Query::ParseError => e
      STDERR.print "Error: " + e.message + "\n"
    end
    return self
  end

  ################################################################
  # Command: server
  ################################################################
  desc "server", "Invoked as server (backend of emacs)"

  named_option :repository

  def server
    require "shellwords"
    while line = STDIN.gets # STDIN.noecho(&:gets)
      argv = line.chomp.shellsplit
      self.class.start(argv)
      STDOUT.flush
    end
  end

  ################################################################
  # Command: show
  ################################################################
  desc "show MESSAGE_ID", "Show article found by MESSAGE_ID"

  named_option :calendar, :repository

  def show(message_id)
    event = exit_on_error do
      calendar.find(uid: message_id)
    end
    print event.dump if event
  end

  ################################################################
  # Command: sync
  ################################################################
  desc "sync SYNC_CHANNEL", "Synchronize DBs via SYNC_CHANNEL"

  named_option :dry_run

  def sync(channel_name)
    driver = exit_on_error do
      builder.sync_driver(channel_name)
    end
    driver.sync_all(options[:dry_run])
    return self
  end

  ################################################################
  # Command: validate
  ################################################################
  desc "validate FILE", "Validate event FILE"

  named_option :format

  def validate(file)
    full_path = File.expand_path(file)

    unless File.exist?(full_path)
      puts Mhc::Converter::Emacs.new.to_emacs("No such file #{file}.")
      return 1
    end

    errors = Mhc::Event.validate(File.open(full_path) {|f| f.read})

    string = ""
    exit_on_error do
      errors.each do |err, key|
        string += "#{err.to_s.capitalize}"
        string += " in X-SC-#{key.capitalize}" if key
        string += ".\n"
      end
    end
    if errors.empty?
      puts Mhc::Converter::Emacs.new.to_emacs("OK")
      return 0
    end

    puts Mhc::Converter::Emacs.new.to_emacs(string)
    return 1
  end

  ################################################################
  # add some hooks to Thor

  no_commands do
    def invoke_command(command, *args)
      setup_global_options unless command.name == "init"
      result = super
      teardown
      result
    end
  end

  ################################################################
  # private

  private

  def exit_on_error(&block)
    begin
      yield if block_given?
    rescue Mhc::ConfigurationError => e
      STDERR.print "ERROR: #{e.message}.\n"
      exit 1
    end
  end

  attr_reader :builder, :config, :calendar

  def setup_global_options
    exit_on_error do
      @config = Mhc::Config.create_from_file(options[:config] || DEFAULT_CONFIG_PATH)
      @builder ||= Mhc::Builder.new(@config)
      if @config.general.tzid
        Mhc.default_tzid = @config.general.tzid
      end

      calname  = options[:calendar] || @config.calendars.first.name
      @config.general.repository = options[:repository] if options[:repository]

      self.class.calendar ||= builder.calendar(calname)
      @calendar = self.class.calendar
    end

    load_plugins

    if options[:profile]
      require 'profiler'
      Profiler__.start_profile
    end
    if options[:debug]
      require "pp"
      $MHC_DEBUG = true
      $MHC_DEBUG_FOR_DEVELOPER = true if ENV["MHC_DEBUG_FOR_DEVELOPER"]
    end
  end

  def load_plugins
    config_path = options[:config] || DEFAULT_CONFIG_PATH
    plugin_dir  = File.dirname(config_path)

    Dir.glob(File.expand_path("plugins/*.rb", plugin_dir)) do |rb|
      require rb
    end
  end

  def teardown
    if options[:profile]
      Profiler__.print_profile($stdout)
    end
  end

  def symbolize_keys(hash)
    Hash[hash.map {|k,v| [k.to_sym, v]}]
  end
end

result = MhcCLI.start(ARGV)

if result.is_a?(Numeric)
  exit result
else
  exit 0
end
