#!/usr/bin/env ruby
require 'readline'
require 'English'
require 'slop'

ENV['RACK_ENV'] ||= 'production'

# display name for tools like `ps`
$PROGRAM_NAME = 'sequenceserver'

# Given a url, downloads the archive into a temporary file
# and and extracts it into ~/.sequenceserver.
def download_from_url(url)
  archive = File.join('~/.sequenceserver', File.basename(url))
  # -ip4 is required  to avoid this curl bug http://sourceforge.net/p/curl/bugs/1424/?limit=25
  # see also https://github.com/yannickwurm/sequenceserver/issues/139
  cmd = 'mkdir -p ~/.sequenceserver' \
        " && curl --ipv4 -C - #{url} -o #{archive}" \
        " && tar xvf #{archive} -C ~/.sequenceserver"
  system(cmd)
end

def ask_to_join
  asked_to_join = File.join(SequenceServer::DOTDIR, 'asked_to_join')
  unless File.exist?(asked_to_join)
    puts
    puts <<~MSG
    Do you want to be notified of SequenceServer releases and any
    other important announcements (3-12 messages a year)? If yes,
    please provide your email address below or press enter to
    continue (you won't be prompted again).
    MSG
    print '>> '
    response = STDIN.gets.to_s.strip
    puts

    if response =~ /@/
      post_email_cmd = "curl -m 5 https://docs.google.com/forms/u/0/d/e/" \
      "1FAIpQLSe7sOCiKzYbI7LwErid-g3wfIU5Zpi6VTm0ILJyR036RqC8zg/formResponse " \
      "-d ifq -d emailAddress=#{response} -d submit=Submit > /dev/null 2> /dev/null"
      system post_email_cmd
    end
    system "mkdir -p #{SequenceServer::DOTDIR} && touch #{asked_to_join}"
  end
end

begin
  Slop.parse!(strict: true, help: true) do
    banner <<~BANNER
      SUMMARY

      custom, local, BLAST server

      USAGE

      sequenceserver [options]

      EXAMPLE

        # Launch SequenceServer. This will read configuration from
        # ~/.sequenceserver.conf, if present.
        $ sequenceserver

        # Use a different config file.
        $ sequenceserver -c ~/.sequenceserver.ants.conf

        # Set number of threads to use. This will save the number
        # of threads to use in config file.
        $ sequenceserver -s -n 16

        # Search for FASTA files in database dir that haven't been
        # converted into BLAST database yet, and convert them.
        $ sequenceserver -m

      DESCRIPTION

      SequenceServer lets you rapidly set up a BLAST+ server with an intuitive user
      interface for use locally or over the web. If BLAST+ is not installed on your
      system, SequenceServer will offer to install BLAST+ for you.  You should only
      ever have to point it to a directory of FASTA files / BLAST+ databases.

      In a given directory, SequenceServer is able to tell FASTA files that are yet
      to be formatted for use with BLAST+ and format them, and FASTA files that are
      already formatted for use with BLAST+, heuristically skipping all other files
      in the directory. Directories are scanned recursively. Type of sequences in a
      FASTA file is detected automagically. `parse_seqids` and `hash_index` options
      of `makeblastdb` are used to create BLAST+ databases.
    BANNER

    on 'c', 'config_file=',
       'Use the given configuration file',
       argument: true

    on 'config=',
       'Same as --config_file (deprecated)',
       argument: true

    on 'b', 'bin=',
       'Load BLAST+ binaries from this directory',
       argument: true

    on 'd', 'database_dir=',
       'Read FASTA and BLAST database from this directory',
       argument: true

    on 'n', 'num_threads=',
       'Number of threads to use to run a BLAST search',
       argument: true

    on 'r', 'require=',
       'Load extension from this file',
       argument: true

    on 'H', 'host=',
       'Host to run SequenceServer on',
       argument: true

    on 'p', 'port=',
       'Port to run SequenceServer on',
       argument: true

    on 's', 'set',
       'Set configuration value in default or given config file'

    on 'm', 'make-blast-databases',
       'Create BLAST databases'

    on 'download-taxdb',
       'Download the taxdb files'

    on 'l', 'list_databases',
       'List BLAST databases'

    on 'i', 'interactive',
       'Run SequenceServer in interactive mode'

    on 'x', 'import=',
       'Import the xml file',
       argument: true

    # on 'doctor',
       # 'Run SequenceServer doctor'

    on 'D', 'devel',
       'Start SequenceServer in development mode'

    on '-v', '--version',
       'Print version number of SequenceServer that will be loaded'

    on '-h', '--help',
       'Display this help message'

    clean_opts = lambda do |hash|
      hash.delete_if { |k, v| k == :set || v.nil? }
      hash[:config_file] ||= hash.delete(:config)
      hash
    end

    run do
      if version?
        require 'sequenceserver/version'
        puts SequenceServer::VERSION
        exit
      end

      ENV['RACK_ENV'] = 'development' if devel?

      # Exit gracefully on SIGINT.
      stty = `stty -g`.chomp
      trap('INT') do
        puts ''
        puts 'Aborted.'
        system('stty', stty)
        exit
      end

      require 'sequenceserver'

      begin
        SequenceServer.init clean_opts[to_h]

      # The aim of following error recovery scenarios is to guide user to a
      # working SequenceServer installation. We expect to land following
      # error scenarios either when creating a new SequenceServer (first
      # time or later), or updating config values using -s CLI option.
      rescue SequenceServer::BLAST_NOT_INSTALLED_OR_NOT_EXECUTABLE,
             SequenceServer::BLAST_NOT_COMPATIBLE => e

        # Show original error message first.
        puts
        puts e

        # Set a flag so that if we recovered from error resulting config can be
        # saved. Config will be saved unless invoked with -b option.
        fetch_option(:set).value = !bin?

        # Ask user if she already has BLAST+ downloaded or offer to download
        # BLAST+ for her.
        puts
        puts <<~MSG
          If you have downloaded NCBI BLAST+ already, please enter the path
          below. Otherwise just press Enter and SequenceServer will download
          a copy of NCBI BLAST+ for itself.

          Press Ctrl+C to abort setup.
        MSG
        puts
        response = Readline.readline('>> ').to_s.strip
        if response.empty?
          puts
          puts 'Downloading NCBI BLAST+.'
          puts "RUBY_PLATFORM #{RUBY_PLATFORM}"
          puts 'Archive will be extracted in ~/.sequenceserver directory.'
          puts

          version = SequenceServer::BLAST_VERSION
          url = case RUBY_PLATFORM
                when /x86_64-linux/ # 64 bit Linux
                  'ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/' \
                  "#{version.chop}/ncbi-blast-#{version}-x64-linux.tar.gz"
                when /darwin/       # Mac
                  'ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/' \
                  "#{version.chop}/" \
                  "ncbi-blast-#{version}-x64-macosx.tar.gz"
                else
                  puts <<~ERR
                    ------------------------------------------------------------------------
                    FAILED!! to install NCBI BLAST+.

                    We currently support Linux and Mac only (64 bit). If you
                    believe you are running a supported platform, please open a support
                    ticket titled "#{RUBY_PLATFORM}" at:

                    https://github.com/yannickwurm/sequenceserver/issues
                    ------------------------------------------------------------------------

                  ERR
                end
          download_from_url(url)
          unless $CHILD_STATUS.success?
            puts 'Failed to install BLAST+.'
            puts '  You may need to download BLAST+ from - '
            puts '   http://www.ncbi.nlm.nih.gov/blast/Blast.cgi?CMD=Web&PAGE_TYPE=BlastDocs&DOC_TYPE=Download'
            puts "  Please ensure that you download BLAST+ version #{SequenceServer::BLAST_VERSION}."
            exit!
          end
          fetch_option(:bin).value =
            "~/.sequenceserver/ncbi-blast-#{version}/bin/"
        else
          unless File.basename(response) == 'bin'
            response = File.join(response, 'bin')
          end
          fetch_option(:bin).value = response
          puts
        end
        redo
      rescue SequenceServer::DATABASE_DIR_NOT_SET => e
        # Show original error message.
        puts
        puts e

        # Set a flag so that if we recovered from error resulting config can be
        # saved. Config will be saved unless invoked with -d option.
        fetch_option(:set).value = !database_dir?

        # Ask user for the directory containing sequences or BLAST+
        # databases.
        puts
        puts <<~MSG
          SequenceServer needs to know where your FASTA files or BLAST+ databases are.
          Please enter the path to the relevant directory (default: current directory).

          Press Ctrl+C to quit.
        MSG

        puts
        response = Readline.readline('>> ').to_s.strip
        fetch_option(:database_dir).value = response
        redo
      rescue SequenceServer::NO_BLAST_DATABASE_FOUND => e
        unless list_databases? || make_blast_databases?
          # Print error raised.
          puts
          puts e

          # Offer user to format the FASTA files.
          database_dir = SequenceServer.config[:database_dir]
          puts
          puts <<~MSG
            Search for FASTA files (.fa, .fasta, .fna) in '#{database_dir}' and try
            creating BLAST+ databases? [y/n] (Default: y).
          MSG
          puts
          print '>> '
          response = STDIN.gets.to_s.strip
          unless response =~ /^[n]$/i
            puts
            puts 'Searching ...'
            if SequenceServer.makeblastdb.any_unformatted?
              formatted = SequenceServer.makeblastdb.format
              exit! if formatted.empty? && !set?
              redo unless set?
            else
              puts "Couldn't find any FASTA files."
              exit!
            end
          else
            exit! unless set?
          end
        end
      rescue SequenceServer::ENOENT,
        SequenceServer::CONFIG_FILE_ERROR,
        SequenceServer::BLAST_DATABASE_ERROR,
        SequenceServer::NUM_THREADS_INCORRECT => e
        # Known errors with clear error message are handled here.

        puts e
        exit!
      rescue => e
        # This will catch any unhandled error and some very special errors.
        # Ideally we will never hit this block. If we do, there's a bug in
        # SequenceServer or something really weird going on. If we hit this
        # error block we show the stacktrace to the user requesting them to
        # post the same to our Google Group.
        puts <<~MSG
          Something went wonky

          Looks like you have encountered a bug in SequenceServer. Please could you
          report this incident to our Google Group -
          https://groups.google.com/forum/?fromgroups#!forum/sequenceserver

          Error:
            #{e.backtrace.unshift(e.message).join("\n")}
        MSG
        exit!
      end

      if doctor?
        puts '*** Running SequenceServer Doctor.'
        puts '    Initializing an index of databases. This may take a while..'
        SequenceServer::Doctor.new.diagnose
        exit
      end

      if interactive?
        SequenceServer.irb
        exit
      end

      if list_databases?
        puts SequenceServer::Database.all
        exit
      end

      if make_blast_databases?
        if SequenceServer.makeblastdb.scan
          puts
          puts <<~MSG
          SequenceServer has scanned your databases directory and will now offer
          to convert FASTA files into BLAST databases. It will also offer to
          reformat any old-format BLAST databases and those created without
          the -parse_seqids option of makeblastdb (-parse_seqids option is
          required for sequence retrieval to correctly work).

          Note that reformatting process can be slow large BLAST databases and
          fail if sequence identifiers are longer than 50 characters. While we
          exepect the reformatting process to work in most other cases, things
          can inevitably go wrong. Thus, please back up your databases before
          reformatting and post any issues to our Google Group/GitHub so that
          we can all learn from it.

          Proceed? [y/n] (Default: y).
          MSG
          puts
          print '>> '
          response = STDIN.gets.to_s.strip
          SequenceServer.makeblastdb.run unless response =~ /^[n]$/i
          else
          puts "All FASTA files in #{SequenceServer.config[:database_dir]} " \
               'are formatted.'
        end
        exit
      end

      if download_taxdb?
        download_from_url('ftp://ftp.ncbi.nlm.nih.gov/blast/db/'\
                          'taxdb.tar.gz')
        puts
        puts 'taxdb downloaded!'
        exit
      end

      if import?
        xml_file = fetch_option(:import).value
        params = {:xml => xml_file}
        job = SequenceServer::BLAST::Job.new(params)
        puts job.id
        exit
      end

      if set?
        SequenceServer.config.write_config_file
        exit
      end

      SequenceServer.config.write_config_file if fetch_option(:set).value

      ask_to_join

      SequenceServer.run
    end
  end
rescue Slop::Error => e
  puts e
  puts "Run '#{$PROGRAM_NAME} -h' for help with command line options."
  exit
end