require 'timeout'
require 'open3'

module Calabash
  module Android
    # @!visibility private
    class ADB
      # @!visibility private
      class ADBCallError < StandardError
        attr_reader :stderr, :stdout

        def initialize(message, stderr=nil, stdout=nil)
          super(message)

          @stderr = stderr
          @stdout = stdout
        end
      end

      # @!visibility private
      PROCESS_WAIT_TIME = 10

      # @!visibility private
      def self.open_pipe_with_timeout(timeout, *cmd, &block)
        begin
          i, o, e, t, pid = nil

          Timeout.timeout(timeout, ProcessDidNotExitError) do
            i, o, e, t = Open3.popen3(*cmd)
            i.sync = true
            o.sync = true
            e.sync = true
            pid = t.pid
            block.call(i, o, e)

            t.value.exitstatus
          end
        rescue ProcessDidNotExitError => _
          raise ADBCallError, 'ADB process did not exit'
        ensure
          i.close unless i.nil? || i.closed?
          o.close unless o.nil? || o.closed?
          e.close unless e.nil? || e.closed?

          if pid
            begin
              Process.kill(9, pid)
            rescue Errno::ESRCH => _
              # do nothing
            end
          end

          t.join if t
        end
      end

      # @!visibility private
      def self.open_adb_pipe(*cmd, **options, &block)
        timeout = options.fetch(:timeout, PROCESS_WAIT_TIME)

        open_pipe_with_timeout(timeout, Environment.adb_path, *cmd) do |i, o, e|
          block.call(i, o, e) if block
        end
      end

      # @!visibility private
      DAEMON_STARTED_MESSAGE = "* daemon not running. starting it now on port 5037 *\n* daemon started successfully *\n"

      # @!visibility private
      def self.command(*cmd, **args)
        Logger.debug("ADB Command: #{cmd.join(', ')}")
        Logger.debug("ADB input: #{args[:input]}")
        stderr = nil
        stdout = nil
        exit_code = nil

        input = args[:input]

        begin
          exit_code = open_adb_pipe(*cmd, args) do |i, o, e|
            if input
              input.each do |p_cmd|
                begin
                  i.puts p_cmd
                rescue Errno::EPIPE => err
                  i.close
                  stderr = e.readlines.join
                  stdout = o.readlines.join
                  raise ADBCallError.new(err, stderr, stdout)
                end
              end

              i.close
            end

            unless args.fetch(:no_read, false)
              stdout = o.readlines.join
              stderr = e.readlines.join
            end

            if stdout && stdout.start_with?(DAEMON_STARTED_MESSAGE)
              stdout = stdout[DAEMON_STARTED_MESSAGE.length..-1]
            end
          end
        rescue IOError => e
          raise ADBCallError, e
        end

        if exit_code != 0
          Logger.debug("Adb process exited with #{exit_code}")
          Logger.debug("Error message from ADB: ")
          Logger.debug(stderr)

          stderr_output = if stderr && !stderr.empty?
                            ": #{dot_string(stderr.lines.first, 100)}"
                          else
                            ''
                          end

          raise ADBCallError.new(
                "Adb process exited with #{exit_code}#{stderr_output}", stderr, stdout)
        end

        stdout
      end

      attr_reader :serial

      # @!visibility private
      def initialize(serial)
        @serial = serial
      end

      # @!visibility private
      def command(*argv, **args)
        cmd = argv.dup

        if serial
          cmd.unshift('-s', serial)
        end

        ADB.command(*cmd, args)
      end

      # @!visibility private
      END_STRING = '__CAL_END__'

      # @!visibility private
      def shell(shell_cmd, options={})
        if shell_cmd.nil? || shell_cmd.empty?
          raise ArgumentError, "Invalid shell command '#{shell_cmd}'"
        end

        input =
            [
                "#{shell_cmd}; echo \"#{END_STRING}$?\"; exit 0"
            ]

        args = options.merge(input: input)

        result = command('shell', args)

        # We get a result like this:
        #
        # [0] "getprop ro.build.version.release; echo \"\r\n",
        # [1] "$?\"; exit 0\r\n",
        # [2] "shell@hammerhead:/ $ getprop ro.build.version.release; echo \"\r\r\n",
        # [3] "> $?\"; exit 0\r\r\n",
        # [4] "4.4\r\n",
        # [5] "\r\n",
        # [6] "0\r\n"
        #
        # out =
        # [4] "4.4\r\n"
        # [5] "\r\n",
        # [6] "0\r\n"
        #
        # command_result =
        # [4] "4.4\r\n"
        # [5] "\r\n",
        #
        # exit_code_s =
        # [6] "0\r\n"

        unless shell_name.empty?
          index = result.lines.index {|line| line.start_with?(shell_name)}

          if index.nil?
            raise ADBCallError.new("Could not parse output #{ADB.dot_string(result, 100)}", result)
          end

          # Remove the commands
          out = result.lines[index+1..-1]

          last_line = out.last
          end_index = nil

          15.times do |i|
            if last_line[-(END_STRING.length+i-1)..-i] == END_STRING
              end_index = -i
              break
            end
          end

          if end_index.nil?
            raise ADBCallError.new("Could not parse output #{ADB.dot_string(result, 100)}", result)
          end

          # Get the result from the command
          command_result = out[0..-2].join + last_line[0..(end_index - END_STRING.length)]

          # Get the exit code
          exit_code_s = out[-1][end_index+1..-1]
        else # Some devices (like Android N) have started just giving us the result without all the spam
          out = result.lines
          command_result = out[0..-2].join
          exit_code_s = out[-1][END_STRING.length..-1]
        end

        unless options[:no_exit_code_check]
          unless exit_code_s.to_i.to_s == exit_code_s.chomp
            raise ADBCallError,
                  "Unable to obtain exit code. Result: '#{exit_code_s}'"
          end

          exit_code = exit_code_s.to_i

          if exit_code != 0
            Logger.debug("Adb shell command exited with #{exit_code}")
            Logger.debug("Error message from ADB: ")
            Logger.debug(command_result)

            stderr_output = if command_result && !command_result.empty?
                              ": #{ADB.dot_string(command_result.lines.first, 100)}"
                            else
                              ''
                            end

            raise ADBCallError.new(
                      "Adb shell command exited with #{exit_code}#{stderr_output}", command_result)
          end
        end

        command_result
      end

      private

      def self.dot_string(string, length)
        if string.length > length
          "#{string[0, length-3]}..."
        else
          string
        end
      end

      def shell_name
        if @shell_name
          @shell_name
        else
          result = command('shell', input: ['echo "test"; exit 0'])

          # result.lines =
          # [
          #     [0] "echo \"foo\"; exit 0\r\n",
          #     [1] "shell@hammerhead:/ $ echo \"foo\"; exit 0\r\r\n",
          #     [2] "foo\r\n"
          # ]
          #
          # OR
          #
          # result.lines =
          # [
          #     [1] "shell@hammerhead:/ $ echo \"foo\"; exit 0\r\r\n",
          #     [2] "foo\r\n"
          # ]

          #result.lines.index {|line| !line.start_with?('echo')}

          # Some devices (like Android N) have started just giving us the results
          if result.strip == "test"
            @shell_name = ""
            return @shell_name
          end

          # "shell@hammerhead:/ $ echo \"foo\"; exit 0\r\r\n"
          shell_name_line = result.lines[-2]

          # "shell@hammerhead:/ $ "
          @shell_name = shell_name_line.split('echo').first
        end
      end

      # @!visibility private
      class ProcessDidNotExitError < RuntimeError; end
    end
  end
end
