# A module to validate the command line Arguments
## CREDIT: some of these methods have been adapted from SequenceServer
module GeneValidator
  # TODO: If a tabular file is provided, ensure that a tabular file has the
  #       right number of columns
  # TODO: assert_if_ruby_version_is_supported
  # A module to validate the arguments passed to the Validation Class
  module GVArgValidation
    class << self
      def validate_args(opt)
        @opt = opt
        assert_output_dir_does_not_exist
        assert_file_present('input file', opt[:input_fasta_file])
        assert_input_file_probably_fasta
        assert_input_contains_single_type_sequence
        assert_BLAST_output_files

        assert_validations_arg
        check_num_threads

        Blast.validate(opt) unless @opt[:test]
        Mafft.assert_mafft_installation(opt)
        @opt
      end

      private

      def assert_validations_arg
        validations = %w(lenc lenr frame merge dup orf align)
        if @opt[:validations]
          val = @opt[:validations].collect { |v| v.strip.downcase }
          validations = val unless val.include? 'all'
        end
        @opt[:validations] = validations
      end

      def check_num_threads
        @opt[:num_threads] = Integer(@opt[:num_threads])
        unless @opt[:num_threads] > 0
          puts 'Number of threads can not be lower than 0'
        end
        return unless @opt[:num_threads] > 256
        puts "Number of threads set at #{@opt[:num_threads]} is unusually high."
      end

      def assert_BLAST_output_files
        return unless @opt[:blast_xml_file] || @opt[:blast_tabular_file]
        if @opt[:blast_xml_file]
          assert_file_present('BLAST XML file', @opt[:blast_xml_file])
        elsif @opt[:blast_tabular_file]
          assert_file_present('BLAST tabular file', @opt[:blast_tabular_file])
          assert_tabular_options_exists
        end
      end

      def assert_output_dir_does_not_exist
        output_dir = "#{@opt[:input_fasta_file]}.html"
        return unless File.exist?(output_dir)
        puts "The output directory already exists for this fasta file.\n"
        puts "Please remove the following directory: #{output_dir}\n"
        puts "You can run the following command to remove the folder.\n"
        puts "\n   $ rm -r #{output_dir} \n"
        exit 1
      end

      def assert_tabular_options_exists
        return if @opt[:blast_tabular_options]
        puts '*** Error: BLAST tabular options (-o) have not been set.'
        puts '    Please set the "-o" option with the custom format'
        puts '    used in the BLAST -outfmt argument'
        exit 1
      end

      def assert_input_file_probably_fasta
        File.open(@opt[:input_fasta_file], 'r') do |file_stream|
          (file_stream.readline[0] == '>') ? true : false
        end
      end

      def assert_file_present(desc, file, exit_code = 1)
        return if file && File.exist?(File.expand_path(file))
        puts "*** Error: Couldn't find the #{desc}: #{file}."
        exit exit_code
      end

      alias_method :assert_dir_present, :assert_file_present

      def assert_input_contains_single_type_sequence
        fasta_content = IO.binread(@opt[:input_fasta_file])
        type = BlastUtils.type_of_sequences(fasta_content)
        return if type == :nucleotide || type == :protein
        puts '*** Error: The input files does not contain just protein or'
        puts '    nucleotide data. Please correct this and try again.'
        exit 1
      end
    end

    # Validates BLAST Installation (And BLAST databases)
    class Blast
      class << self
        # Use a fixed minimum version of BLAST+
        MINIMUM_BLAST_VERSION           = '2.2.30+'
        # Use the following exit codes, or 1.
        EXIT_BLAST_NOT_INSTALLED        = 2
        EXIT_BLAST_NOT_COMPATIBLE       = 3
        EXIT_NO_BLAST_DATABASE          = 4

        def validate(opt)
          @opt = opt
          assert_blast_installation
          assert_blast_database_provided
          assert_local_blast_database_exists if @opt[:db] !~ /remote/
        end

        def assert_blast_installation
          # Validate BLAST installation
          if @opt[:blast_bin].nil?
            assert_blast_installed
            assert_blast_compatible
          else
            export_bin_dir
          end
        end

        def assert_blast_database_provided
          return unless @opt[:db].nil?
          puts '*** Error: A BLAST database is required. Please pass a local or'
          puts '    remote BLAST database to GeneValidator as follows:'
          puts # a blank line
          puts "      $ genevalidator -d '~/blastdb/SwissProt' Input_File"
          puts # a blank line
          puts '    Or use a remote database:'
          puts # a blank line
          puts "      $ genevalidator -d 'swissprot -remote' Input_File"
          exit 1
        end

        def assert_local_blast_database_exists
          return if system("blastdbcmd -db #{@opt[:db]} -info > /dev/null 2>&1")
          puts '*** No BLAST database found at the provided path.'
          puts '    Please ensure that the provided path is correct and then' \
               ' try again.'
          exit EXIT_NO_BLAST_DATABASE
        end

        private

        def assert_blast_installed
          return if GVArgValidation.command?('blastdbcmd')
          puts '*** Could not find BLAST+ binaries.'
          exit EXIT_BLAST_NOT_INSTALLED
        end

        def assert_blast_compatible
          version = `blastdbcmd -version`.split[1]
          return if version >= MINIMUM_BLAST_VERSION
          puts "*** Your BLAST+ version #{version} is outdated."
          puts '    GeneValidator needs NCBI BLAST+ version' \
               " #{MINIMUM_BLAST_VERSION} or higher."
          exit EXIT_BLAST_NOT_COMPATIBLE
        end

        def export_bin_dir
          if File.directory?(@opt[:blast_bin])
            GVArgValidation.add_to_path(@opt[:blast_bin])
          else
            puts '*** The provided BLAST bin directory does not exist.'
            puts '    Please ensure that the provided BLAST bin directory is' \
                 ' correct and try again.'
            exit EXIT_BLAST_NOT_INSTALLED
          end
        end
      end
    end

    # Validates Mafft installation
    class Mafft
      class << self
        def assert_mafft_installation(opt)
          @opt = opt
          if @opt[:mafft_bin].nil?
            assert_mafft_installed
          else
            export_bin_dir
          end
        end

        private

        def assert_mafft_installed
          return if GVArgValidation.command?('mafft')
          puts '*** Could not find Mafft binaries.'
          puts '    Ignoring error and continuing - Please note that some' \
               ' validations may be skipped.'
          puts # a blank line
        end

        def export_bin_dir
          if File.directory?(@opt[:mafft_bin])
            GVArgValidation.add_to_path(@opt[:mafft_bin])
          else
            puts '*** The provided Mafft bin directory does not exist.'
            puts '    Ignoring error and continuing - Please note that some' \
                 ' validations may be skipped.'
            puts # a blank line
          end
        end
      end
    end

    class << self
      ## Checks if dir is in $PATH and if not, it adds the dir to the $PATH.
      def add_to_path(bin_dir)
        return if ENV['PATH'].split(':').include?(bin_dir)
        ENV['PATH'] = "#{bin_dir}:#{ENV['PATH']}"
      end

      # Return `true` if the given command exists and is executable.
      def command?(command)
        system("which #{command} > /dev/null 2>&1")
      end
    end
  end
end