require 'rbconfig'

if Config::CONFIG['host_os'].match('mswin')
   require 'win32/file'
   require 'win32/dir'
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.
      # the error that is raised. 
      class Error < StandardError; end

      # The version of this library
      VERSION = '1.1.3'

      # 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, 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?
            if Config::CONFIG['host_os'].match('mswin')
               home = ENV['USERPROFILE'] || ENV['HOME']
               file = nil
               
               if home
                  file = File.join(home, '.dbrc')
               else
                  file = File.join(File.basename(Dir::APPDATA), '.dbrc')
               end
               
               @dbrc_file = file
            else
               @dbrc_file = File.join(
                  Sys::Admin.get_user(Process.uid).dir,
                  '.dbrc'
               )
            end
         else
            @dbrc_file = File.join(dbrc_dir, '.dbrc')
         end
         
         @dbrc_dir  = dbrc_dir
         @database  = database
         @user      = user
         encrypted  = false # Win32 only

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

         check_file()
         
         # If on Win32 and the file is encrypted, decrypt it.
         if Config::CONFIG['host_os'].match("mswin") && File.encrypted?(@dbrc_file)
            encrypted = true
            File.decrypt(@dbrc_file)
         end
         
         parse_dbrc_config_file()
         validate_data()
         convert_numeric_strings()
         create_dsn_string()
         
         # If on Win32 and the file was encrypted, re-encrypt it
         if Config::CONFIG['host_os'].match("mswin") && encrypted
            File.encrypt(@dbrc_file)
         end
      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 Config::CONFIG['host_os'].match("mswin")
               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
               if @user
                  next unless @user == info["user"]
               end
               @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