require 'net/ssh'
require 'net/sftp'

module Vos
  module Drivers
    class Ssh < Abstract      
      module VfsStorage
        # 
        # Attributes
        # 
        def attributes path

          stat = sftp.stat! fix_path(path)
          attrs = {}
          attrs[:file] = stat.file?
          attrs[:dir] = stat.directory?
          # stat.symlink?
          attrs                  
        rescue Net::SFTP::StatusException
          {}
        end

        def set_attributes path, attrs      
          raise 'not supported'
        end

        # 
        # File
        # 
        def read_file path, &block
          sftp.file.open fix_path(path), 'r' do |is|
            while buff = is.gets
              block.call buff
            end
          end
        end

        def write_file path, append, &block  
          # there's no support for :append in Net::SFTP, so we just mimic it      
          if append          
            attrs = attributes(path)
            data = if attrs
              if attrs[:file]
                os = ""
                read_file(path){|buff| os << buff}
                delete_file path                
                os
              else
                raise "can't append to dir!"
              end
            else
              ''
            end
            write_file path, false do |writer|
              writer.call data
              block.call writer
            end
          else
            sftp.file.open fix_path(path), 'w' do |os|
              writer = -> buff {os.write buff}
              block.call writer
            end
          end          
        end   

        def delete_file remote_file_path
          sftp.remove! fix_path(remote_file_path)
        end

        # def move_file path
        #   raise 'not supported'
        # end


        # 
        # Dir
        # 
        def create_dir path
          sftp.mkdir! path
        end

        def delete_dir path
          exec "rm -r #{path}"
        end

        def each path, &block
          sftp.dir.foreach path do |stat|
            next if stat.name == '.' or stat.name == '..'
            if stat.directory?
              block.call stat.name, :dir
            else
              block.call stat.name, :file
            end
          end
        end
        
        def efficient_dir_copy from, to
          from.storage.open_fs do |from_fs|          
            to.storage.open_fs do |to_fs|
              if from_fs.local? 
                sftp.upload! from.path, fix_path(to.path)
                true
              elsif to_fs.local?
                sftp.download! fix_path(to.path), from.path, :recursive => true
                true
              else
                false
              end
            end
          end
        end

        # def move_dir path
        #   raise 'not supported'
        # end


        # 
        # Special
        # 
        def tmp &block
          tmp_dir = "/tmp/vfs_#{rand(10**3)}"        
          if block
            begin
              create_dir tmp_dir
              block.call tmp_dir
            ensure
              delete_dir tmp_dir
            end
          else
            create_dir tmp_dir
            tmp_dir
          end
        end
        
        def local?; false end
      end
      
      def initialize options = {}
        super
        raise "ssh options not provided!" unless options[:ssh]
        raise "invalid ssh options!" unless options[:ssh].is_a?(Hash)
      end
      
      
      # 
      # Establishing SSH channel
      # 
      def open &block
        if block
          if @ssh
            block.call self
          else
            begin            
              open
              block.call self
            ensure
              close
            end
          end
        else
          unless @ssh
            ssh_options = self.options[:ssh].clone
            host = options[:host] || raise('host not provided!')
            user = ssh_options.delete(:user) || raise('user not provied!')
            @ssh = Net::SSH.start(host, user, ssh_options)
            @sftp = @ssh.sftp.connect
          end
        end
      end            
    
      def close                        
        if @ssh
          @ssh.close
          # @sftp.close not needed
          @ssh, @sftp = nil
        end
      end

      def to_s; options[:host] end
      
      
      # 
      # Vfs
      # 
      include VfsStorage
      alias_method :open_fs, :open
      
      
      # 
      # Shell
      # 
      def exec command
        # somehow net-ssh doesn't executes ~/.profile, so we need to execute it manually
        # command = ". ~/.profile && #{command}"

        stdout, stderr, code, signal = hacked_exec! ssh, command

        return code, stdout, stderr
      end                              
      
      def bash command
        # somehow net-ssh doesn't executes ~/.profile, so we need to execute it manually
        # command = ". ~/.profile && #{command}"

        stdout_and_stderr, stderr, code, signal = hacked_exec! ssh, command, true

        return code, stdout_and_stderr
      end
      
      
      protected
        attr_accessor :ssh, :sftp
      
        def fix_path path
          path.sub(/^\~/, home)
        end
        
        def home
          unless @home
            command = 'cd ~; pwd'
            code, stdout, stderr = exec command
            raise "can't execute '#{command}'!" unless code == 0
            @home = stdout.gsub("\n", '')    
          end
          @home
        end
      
        # taken from here http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library/3386375#3386375
        def hacked_exec!(ssh, command, merge_stdout_and_stderr = false, &block)
          stdout_data = ""
          stderr_data = ""
          exit_code = nil
          exit_signal = nil
        
          channel = ssh.open_channel do |channel|
            channel.exec(command) do |ch, success|
              raise "could not execute command: #{command.inspect}" unless success

              channel.on_data{|ch2, data| stdout_data << data}
              channel.on_extended_data do |ch2, type, data| 
                stdout_data << data if merge_stdout_and_stderr
                stderr_data << data
              end
              channel.on_request("exit-status"){|ch,data| exit_code = data.read_long}
              channel.on_request("exit-signal"){|ch, data| exit_signal = data.read_long}
            end          
          end  
        
          channel.wait      
        
          [stdout_data, stderr_data, exit_code, exit_signal]
        end    
    end
  end
end