if File::ALT_SEPARATOR
  require 'win32/dir'
  require 'win32/file'
  require 'win32/process'
end

require 'sys/admin'

# The DBI module serves as a namespace only.
module DBI

  # The DBRC class encapsulates a database resource config file.
  class DBRC

    # This error is raised if anything fails trying to read the config file.
    class Error < StandardError; end

    # The version of the dbi-dbrc library
    VERSION = '1.1.9'

    @@windows = File::ALT_SEPARATOR

    # The database or host to be connected to.
    attr_accessor :database

    # The user name used for the database or host connection.
    attr_accessor :user

    # The password associated with the database or host.
    attr_accessor :password

    # The driver associated with the database. This is used to internally to
    # construct the DSN.
    attr_accessor :driver

    # Data source name, e.g. "dbi:OCI8:your_database".
    attr_accessor :dsn

    # The maximum number of reconnects a program should make before giving up.
    attr_accessor :maximum_reconnects

    # The timeout, in seconds, for each connection attempt.
    attr_accessor :timeout

    # The interval, in seconds, between each connection attempt.
    attr_accessor :interval

    # The directory where the .dbrc file is stored.
    attr_accessor :dbrc_dir

    # The full path to the .dbrc file.
    attr_accessor :dbrc_file

    # Returns a new DBI::DBRC object. The contents of the object depend on
    # the arguments passed to the constructor. If only a database name is
    # passed, then the first entry found in the .dbrc file that matches that
    # database is parsed. If a user name is also included, then the first
    # entry that matches both the database and user name is parsed.
    #
    # If a directory is passed as the third argument, then DBRC will look
    # in that directory, instead of the default directory, for the .dbrc
    # file.
    #
    # If an entry cannot be found for the database, or database plus user
    # combination, then a Error is raised.  If the .dbrc file cannot be
    # found, or is setup improperly with regards to permissions or properties
    # then a DBI::DBRC::Error is raised.
    #
    # See the README for the rules regarding .dbrc files and permissions.
    #
    # Note that this library can also be used as a general password
    # storage mechanism. In that case simply treat the 'database' as the
    # host name, and ignore the DBI::DBRC#dsn and DBI::DBRC#driver methods.
    #
    # Examples:
    #
    #   # Find the first match for 'some_database'
    #   DBI::DBRC.new('some_database')
    #
    #   # Find the first match for 'foo_user@some_database'
    #   DBI::DBRC.new('some_database', 'foo_user')
    #
    #   # Find the first match for 'foo_user@some_database' under /usr/local
    #   DBI::DBRC.new('some_database', 'foo_usr', '/usr/local')
    #
    def initialize(database, user=nil, dbrc_dir=nil)
      if dbrc_dir.nil?
        uid  = Process.uid
        home = ENV['HOME'] || ENV['USERPROFILE']

        if home.nil?
          if @@windows
            home ||= Sys::Admin.get_user(uid, :localaccount => true).dir
          else
            home ||= Sys::Admin.get_user(uid).dir
          end
        end

        # Default to the app data directory on Windows, or root on Unix, if
        # no home dir can be found.
        if home.nil?
          if @@windows
            home = Dir::APPDATA
          else
            home = '/'
          end
        end

        @dbrc_file = File.join(home, '.dbrc')
      else
        raise Error, 'bad directory' unless File.directory?(dbrc_dir)
        @dbrc_file = File.join(dbrc_dir, '.dbrc')
      end

      @dbrc_dir  = dbrc_dir
      @database  = database
      @user      = user

      file_was_encrypted = false # Win32 only

      @driver   = nil
      @interval = nil
      @timeout  = nil
      @maximum_reconnects = nil

      check_file()

      # Decrypt and re-encrypt the file if we're on MS Windows and the
      # file is encrypted.
      begin
        if @@windows && File.encrypted?(@dbrc_file)
          file_was_encrypted = true
          File.decrypt(@dbrc_file)
        end

        parse_dbrc_config_file()
        validate_data()
        convert_numeric_strings()
        create_dsn_string()
      ensure
        if @@windows && file_was_encrypted
          File.encrypt(@dbrc_file)
        end
      end
    end

    # Inspection of the DBI::DBRC object. This is identical to the standard
    # Ruby Object#inspect, except that the password field is filtered.
    #
    def inspect
      str = instance_variables.map{ |iv|
        if iv == '@password'
          "#{iv}=[FILTERED]"
        else
          "#{iv}=#{instance_variable_get(iv).inspect}"
        end
      }.join(', ')

      "#<#{self.class}:0x#{(self.object_id*2).to_s(16)} " << str << ">"
    end

    private

    # Ensure that the user/password has been set
    def validate_data
      unless @user
        raise Error, "no user found associated with #{@database}"
      end

      unless @password
        raise Error, "password not defined for #{@user}@#{@database}"
      end
    end

    # Converts strings that should be numbers into actual numbers
    def convert_numeric_strings
      @interval   = @interval.to_i if @interval
      @timeout    = @timeout.to_i if @timeout
      @maximum_reconnects = @maximum_reconnects.to_i if @maximum_reconnects
    end

    # Create the dsn string if the driver is defined
    def create_dsn_string
      @dsn = "dbi:#{@driver}:#{@database}" if @driver
    end

    # Check ownership and permissions
    def check_file(file=@dbrc_file)
      File.open(file){ |f|
        # Permissions must be set to 600 or better on Unix systems.
        # Must be hidden on Win32 systems.
        if @@windows
          unless File.hidden?(file)
            raise Error, "The .dbrc file must be hidden"
          end
        else
          unless (f.stat.mode & 077) == 0
            raise Error, "Bad .dbrc file permissions"
          end
        end

        # Only the owner may use it
        unless f.stat.owned?
          raise Error, "Not owner of .dbrc file"
        end
      }
    end

    # Parse the text out of the .dbrc file.  This is the only method you
    # need to redefine if writing your own config handler.
    def parse_dbrc_config_file(file=@dbrc_file)
       IO.foreach(file){ |line|
          next if line =~ /^#/    # Ignore comments
          db, user, pwd, driver, timeout, max, interval = line.split

          next unless @database == db

          if @user
             next unless @user == user
          end

          @user               = user
          @password           = pwd
          @driver             = driver
          @timeout            = timeout
          @maximum_reconnects = max
          @interval           = interval
          return
       }

       # If we reach here it means the database and/or user wasn't found
       if @user
          err = "no record found for #{@user}@#{@database}"
       else
          err = "no record found for #{@database}"
       end

       raise Error, err
    end

    alias_method(:db, :database)
    alias_method(:db=, :database=)
    alias_method(:passwd, :password)
    alias_method(:passwd=, :password=)
    alias_method(:max_reconn, :maximum_reconnects)
    alias_method(:max_reconn=, :maximum_reconnects=)
    alias_method(:time_out, :timeout)
    alias_method(:time_out=, :timeout=)
    alias_method(:host, :database)
  end

  # A subclass of DBRC designed to handle .dbrc files in XML format.  The
  # public methods of this class are identical to DBRC.
  class XML < DBRC
    require "rexml/document"
    include REXML

    private

    def parse_dbrc_config_file(file=@dbrc_file)
      doc = Document.new(File.new(file))
      fields = %w/user password driver interval timeout maximum_reconnects/
      doc.elements.each("/dbrc/database"){ |element|
        next unless element.attributes["name"] == database
        if @user
          next unless element.elements["user"].text == @user
        end
        fields.each{ |field|
          val = element.elements[field]
          unless val.nil?
            send("#{field}=",val.text)
          end
        }
       return
      }
      # If we reach here it means the database and/or user wasn't found
      raise Error, "No record found for #{@user}@#{@database}"
    end
  end

  # A subclass of DBRC designed to handle .dbrc files in YAML format. The
  # public methods of this class are identical to DBRC.
  class YML < DBRC
    require "yaml"

    private

    def parse_dbrc_config_file(file=@dbrc_file)
      config = YAML.load(File.open(file))
      config.each{ |hash|
        hash.each{ |db,info|
          next unless db == @database
          next unless @user == info["user"] if @user
          @user       = info["user"]
          @password   = info["password"]
          @driver     = info["driver"]
          @interval   = info["interval"]
          @timeout    = info["timeout"]
          @maximum_reconnects = info["max_reconn"]
          return
        }
      }
      # If we reach this point, it means the database wasn't found
      raise Error, "No entry found for #{@user}@#{@database}"
    end
  end
end