require "pathname"
require "fileutils"

module Bowline
  class << self
    # The Configuration instance used to configure the Bowline environment
    def configuration
      @@configuration
    end
    alias_method :conf, :configuration

    def configuration=(configuration) #:nodoc:
      @@configuration = configuration
    end
    
    def initialized?
      @initialized || false
    end

    def initialized=(initialized) #:nodoc:
      @initialized ||= initialized
    end

    # The default Bowline logger. 
    # Also see the Bowline::Logging class.
    def logger
      if defined?(BOWLINE_LOGGER)
        BOWLINE_LOGGER
      else
        nil
      end
    end
    
    # The application's root
    def root
      Pathname.new(APP_ROOT) if defined?(APP_ROOT)
    end
    
    def public_path
      root && root.join("public")
    end
    
    def assets_path
      File.join(library_path, "assets")
    end
    
    def env
      @env ||= ActiveSupport::StringInquirer.new(ENV["APP_ENV"] || "development")
    end
    
    def irb?
      !!ENV["APP_IRB"]
    end
  end
  
  class Initializer #:nodoc:
    # The Configuration instance used by this Initializer instance.
    attr_reader :configuration
    
    # Runs the initializer. By default, this will invoke the #process method,
    # which simply executes all of the initialization routines. Alternately,
    # you can specify explicitly which initialization routine you want:
    #
    #   Bowline::Initializer.run(:set_load_path)
    #
    # This is useful if you only want the load path initialized, without
    # incurring the overhead of completely loading the entire environment.
    def self.run(command = :process, configuration = Configuration.new)
      yield configuration if block_given?
      initializer = new configuration
      initializer.send(command)
      initializer
    end
    
    # Create a new Initializer instance that references the given Configuration
    # instance.
    def initialize(configuration)
      @configuration = configuration
      @loaded_plugins = []
    end
    
    def set_environment
      production_path = Bowline.root.join("app_production")
      ENV["APP_ENV"] ||= begin
        production_path.exist? ? "production" : "development"
      end
    end
    
    def require_frameworks
      configuration.frameworks.each { |framework| require(framework.to_s) }
    end
    
    def set_load_path
      load_paths = configuration.autoload_paths
      load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }
      $LOAD_PATH.uniq!
    end
    
    def load_gems
      if defined?(Bundler)
        # API changed between 0.8 and 0.9
        if defined?(Bundler.require_env)
          Bundler.require_env
        else
          Bundler.require
        end
      end
    end
    
    # Set the paths from which Bowline will automatically load source files, and
    # the load_once paths.
    def set_autoload_paths
      # Rails 3 master support
      if ActiveSupport::Dependencies.respond_to?(:autoload_paths)
        ActiveSupport::Dependencies.autoload_paths      = configuration.autoload_paths.uniq
        ActiveSupport::Dependencies.autoload_once_paths = configuration.autoload_once_paths.uniq
      else
        ActiveSupport::Dependencies.load_paths      = configuration.autoload_paths.uniq
        ActiveSupport::Dependencies.load_once_paths = configuration.autoload_once_paths.uniq        
      end
    end
    
    def add_plugin_load_paths
      Dir.glob(File.join(configuration.plugin_glob, 'lib')).sort.each do |path|
        $LOAD_PATH << path
        ActiveSupport::Dependencies.autoload_paths << path
        unless configuration.reload_plugins?
          ActiveSupport::Dependencies.autoload_once_paths << path
        end
      end
      $LOAD_PATH.uniq!
    end
    
    def disable_dependency_loading
      if configuration.cache_classes && !configuration.dependency_loading
        ActiveSupport::Dependencies.unhook!
      end
    end
    
    def initialize_dependency_mechanism
      ActiveSupport::Dependencies.mechanism = configuration.cache_classes ? :require : :load
    end
    
    # Initializes ActiveRecord databases
    def initialize_database
      if defined?(ActiveRecord)
        ActiveRecord::Base.establish_connection(configuration.database_configuration)
      end
    end
    
    def initialize_logger
      # if the environment has explicitly defined a logger, use it
      return if Bowline.logger

      unless logger = configuration.logger
        begin
          logger = ActiveSupport::BufferedLogger.new(configuration.log_path)
          logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase)
        rescue StandardError => e
          logger = ActiveSupport::BufferedLogger.new(STDERR)
          logger.level = ActiveSupport::BufferedLogger::WARN
          logger.warn(
            "Bowline Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " +
            "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
          )
        end
      end

      silence_warnings { Object.const_set "BOWLINE_LOGGER", logger }
    end
    
    def initialize_framework_logging
      ActiveRecord::Base.logger ||= Bowline.logger if defined?(ActiveRecord)
      ActiveSupport::Dependencies.logger ||= Bowline.logger
    end
    
    # Loads support for "whiny nil" (noisy warnings when methods are invoked
    # on +nil+ values) if Configuration#whiny_nils is true.
    def initialize_whiny_nils
      require("active_support/whiny_nil") if configuration.whiny_nils
    end
    
    # Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes.
    # If assigned value cannot be matched to a TimeZone, an exception will be raised.
    def initialize_time_zone
      if configuration.time_zone
        zone_default = Time.__send__(:get_zone, configuration.time_zone)

        unless zone_default
          raise \
            'Value assigned to config.time_zone not recognized.' +
            'Run "rake -D time" for a list of tasks for finding appropriate time zone names.'
        end

        Time.zone_default = zone_default

        if defined?(ActiveRecord)
          ActiveRecord::Base.time_zone_aware_attributes = true
          ActiveRecord::Base.default_timezone = :utc
        end
      end
    end
    
    def initialize_framework_settings
      (configuration.frameworks - [:active_support]).each do |framework|
        base_class = framework.to_s.camelize.constantize.const_get("Base")
        settings = configuration.send(framework)
        next if !settings
        settings.each do |setting, value|
          base_class.send("#{setting}=", value)
        end
      end
      configuration.active_support.each do |setting, value|
        ActiveSupport.send("#{setting}=", value)
      end
    end
        
    def load_plugins
      Dir.glob(File.join(configuration.plugin_glob, "init.rb")).sort.each do |path|
        config = configuration # Need local config variable
        eval(IO.read(path), binding, path)
      end
    end
    
    def load_application_environment
      config = configuration
      path   = Bowline.root.join("config", "environments", Bowline.env + ".rb").to_s
      eval(IO.read(path), binding, path)
    end
    
    def load_application_initializers
      Dir.glob(configuration.initializer_glob).sort.each do |initializer|
        load(initializer)
      end
    end
    
    def load_application_first_run
      first_run = Bowline.root.join("app_first_run")
      if first_run.exist?
        first_run.unlink if Bowline.root.writable_real?
        Dir.glob(configuration.first_run_glob).sort.each do |initializer|
          load(initializer)
        end
      end
    end
    
    def after_initialize
      configuration.after_initialize_blocks.each do |block|
        block.call
      end
    end
    
    def load_application_helpers
      helpers = configuration.helpers
      helpers = helpers.map(&:constantize)
      helpers.each {|h| Helpers.module_eval { extend h } }
    end
        
    def load_application_classes
      if configuration.cache_classes
        configuration.eager_load_paths.each do |load_path|
          matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/
          Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
            require_dependency file.sub(matcher, '\1')
          end
        end
      end
    end
    
    def initialize_name
      unless configuration.name
        raise "You must provide an application name in environment.rb"
      end
      silence_warnings { Object.const_set "APP_NAME", configuration.name }
    end
    
    # Creates a class called AppConfig from configuration
    # variables found in config/application.yml
    def load_app_config
      Object.const_set("AppConfig", AppConfig.load!(configuration.app_config_file))
    end
    
    def initialize_desktop
      return unless Desktop.enabled?
      Desktop::Runtime.setup!
      Desktop::JS.setup!
    end
    
    def initialize_windows
      return unless Desktop.enabled?
      MainWindow.setup!
      MainWindow.title = configuration.name
    end
    
    def initialize_trap
      return unless Desktop.enabled?
      trap("INT") {
        Desktop::App.exit
      }
    end
    
    def initialize_path
      # Dir::tmpdir/Tempfile uses this
      ENV["TMPDIR"] = Desktop::Path.temp if Desktop.enabled?
      FileUtils.mkdir_p(Desktop::Path.user_data)
    end
    
    def initialize_marshal
      return unless defined?(SuperModel)
      path = configuration.marshal_path
      path = path.call if path.is_a?(Proc)
      SuperModel::Marshal.path = path
      SuperModel::Marshal.load
      at_exit {
        SuperModel::Marshal.dump
      }
    end
    
    def process
      Bowline.configuration = configuration
      
      set_environment
      
      set_load_path
      load_gems
      
      load_application_environment
      
      require_frameworks
      set_autoload_paths
      add_plugin_load_paths
      initialize_dependency_mechanism
      disable_dependency_loading
      
      initialize_database
      
      initialize_logger
      initialize_framework_logging
      
      initialize_whiny_nils

      initialize_time_zone
      
      initialize_framework_settings
      
      initialize_name
      load_app_config
      
      load_plugins
      load_application_classes
      load_application_helpers
      
      initialize_desktop
      initialize_marshal
      initialize_windows
      initialize_trap
      initialize_path
      
      load_application_initializers
      load_application_first_run
      
      after_initialize
      
      Bowline.initialized = true
    end
    
  end
  
  # The Configuration class holds all the parameters for the Initializer and
   # ships with defaults that suites most Bowline applications. But it's possible
   # to overwrite everything. Usually, you'll create an Configuration file
   # implicitly through the block running on the Initializer, but it's also
   # possible to create the Configuration instance in advance and pass it in
   # like this:
   #
   #   config = Bowline::Configuration.new
   #   Bowline::Initializer.run(:process, config)
   class Configuration
     # The application's base directory.
     attr_reader :root_path
    
     # A stub for setting options on ActiveRecord::Base.
     attr_accessor :active_record

     # A stub for setting options on ActiveResource::Base.
     attr_accessor :active_resource

     # A stub for setting options on ActiveSupport.
     attr_accessor :active_support
     
     attr_accessor :bowline
     
     attr_accessor :frameworks

     # Whether or not classes should be cached (set to false if you want
     # application classes to be reloaded on each request)
     attr_accessor :cache_classes
     
     attr_accessor :binder_paths
     
     attr_accessor :marshal_path
     
     # The path to the database configuration file to use. (Defaults to
     # <tt>config/database.yml</tt>.)
     attr_accessor :database_configuration_file
     
     attr_accessor :app_config_file
     
     # An array of additional paths to prepend to the load path. By default,
     # all +app+, +lib+, +vendor+ and mock paths are included in this list.
     attr_accessor :autoload_paths

     # An array of paths from which Bowline will automatically load from only once.
     # All elements of this array must also be in +load_paths+.
     attr_accessor :autoload_once_paths
     
     # An array of paths from which Bowline will eager load on boot if cache
     # classes is enabled. All elements of this array must also be in
     # +autoload_paths+.
     attr_accessor :eager_load_paths
     
     # The log level to use for the default Bowline logger. 
     attr_accessor :log_level

     # The path to the log file to use. Defaults to log/#{environment}.log
     # (e.g. log/development.log or log/production.log).
     attr_accessor :log_path
     
     # The specific logger to use. By default, a logger will be created and
     # initialized using #log_path and #log_level, but a programmer may
     # specifically set the logger to use via this accessor and it will be
     # used directly.
     attr_accessor :logger
     
     # Set to +true+ if you want to be warned (noisily) when you try to invoke
     # any method of +nil+. Set to +false+ for the standard Ruby behavior.
     attr_accessor :whiny_nils
     
     attr_accessor :reload_plugins
     # Returns true if plugin reloading is enabled.
     def reload_plugins?
       !!@reload_plugins
     end
     
     attr_accessor :dependency_loading
     
     def threadsafe!
        self.cache_classes = true
        self.dependency_loading = false
     end
          
     # Sets the default +time_zone+.  Setting this will enable +time_zone+
     # awareness for Active Record models and set the Active Record default
     # timezone to <tt>:utc</tt>.
     attr_accessor :time_zone
     
     attr_accessor :plugin_glob
     attr_accessor :helper_glob
     attr_accessor :first_run_glob
     attr_accessor :initializer_glob
          
     # Set the application's name.
     # This is required.
     attr_accessor :name
     
     # Set the application's globally unique id.
     # This is required.
     # Example:
     #    config.id = "com.maccman.bowline"
     attr_accessor :id
     
     # Set the application's version.
     # Example:
     #    config.version = "0.1.2"
     attr_accessor :version
     
     attr_accessor :description
     attr_accessor :url
     
     # Set the application's icon. 
     # Point this variable to the icons path.
     #
     # If this isn't specified, Bowline's default one will be used.
     # 
     # Supported icon files are PNGs and JPGs, preferably 512px x 512px.
     attr_accessor :icon

     attr_accessor :publisher
     attr_accessor :copyright
          
     # Create a new Configuration instance, initialized with the default values.
     def initialize
       set_root_path!
       
       self.frameworks                   = default_frameworks
       self.autoload_paths               = default_autoload_paths
       self.autoload_once_paths          = default_autoload_once_paths
       self.dependency_loading           = default_dependency_loading
       self.eager_load_paths             = default_eager_load_paths
       self.log_path                     = default_log_path
       self.log_level                    = default_log_level
       self.binder_paths                 = default_binder_paths
       self.marshal_path                 = default_marshal_path
       self.cache_classes                = default_cache_classes
       self.whiny_nils                   = default_whiny_nils
       self.database_configuration_file  = default_database_configuration_file
       self.app_config_file              = default_app_config_file
       self.plugin_glob                  = default_plugin_glob
       self.helper_glob                  = default_helper_glob
       self.initializer_glob             = default_initalizer_glob
       self.first_run_glob               = default_first_run_glob
       self.publisher                    = default_publisher
       self.copyright                    = default_copyright
       
       for framework in default_frameworks
         self.send("#{framework}=", Bowline::OrderedOptions.new)
       end
     end
     
     # Set the root_path to APP_ROOT and canonicalize it.
     def set_root_path!
       raise 'APP_ROOT is not set' unless defined?(::APP_ROOT)
       raise 'APP_ROOT is not a directory' unless File.directory?(::APP_ROOT)

       @root_path =
         # Pathname is incompatible with Windows, but Windows doesn't have
         # real symlinks so File.expand_path is safe.
         if RUBY_PLATFORM =~ /(:?mswin|mingw)/
           File.expand_path(::APP_ROOT)

         # Otherwise use Pathname#realpath which respects symlinks.
         else
           Pathname.new(::APP_ROOT).realpath.to_s
         end

       Object.const_set(:RELATIVE_APP_ROOT, ::APP_ROOT.dup) unless defined?(::RELATIVE_APP_ROOT)
       ::APP_ROOT.replace @root_path
     end
     
     # Loads and returns the contents of the #database_configuration_file. The
     # contents of the file are processed via ERB before being sent through
     # YAML::load.
     def database_configuration
       require 'erb'
       YAML::load(ERB.new(IO.read(database_configuration_file)).result) if File.exists?(database_configuration_file)
     end
     
     def helpers
       Dir[helper_glob].map {|f| File.basename(f, '.rb').classify }
     end
     
     # Adds a block which will be executed after bowline has been fully initialized.
     # Useful for per-environment configuration which depends on the framework being
     # fully initialized.
     def after_initialize(&after_initialize_block)
       after_initialize_blocks << after_initialize_block if after_initialize_block
     end
     
     # Returns the blocks added with Configuration#after_initialize
     def after_initialize_blocks
       @after_initialize_blocks ||= []
     end
     
   private
   
     def default_frameworks
       [:active_support, :bowline]
     end
        
     def default_autoload_paths
       paths = []

       # Followed by the standard includes.
       paths.concat %w(
         app
         app/binders
         app/models
         app/remote
         app/helpers
         app/windows
         lib
         vendor
       ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }

       paths
     end
     
     # Doesn't matter since plugins aren't in load_paths yet.
     def default_autoload_once_paths
       []
     end
     
     def default_dependency_loading
       true
     end

     def default_eager_load_paths
       %w(
         app/models
         app/remote
         app/windows
         app/binders
         app/helpers
       ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }
     end
     
     def default_log_path
       File.join(root_path, 'log', "application.log")
     end
     
     def default_log_level
       :info
     end
     
     def default_database_configuration_file
       File.join(root_path, 'config', 'database.yml')
     end
     
     def default_app_config_file
       File.join(root_path, 'config', 'application.yml')
     end
     
     def default_binder_paths
       File.join(root_path, 'app', 'binders')
     end
     
     def default_marshal_path
       Proc.new { File.join(Desktop::Path.user_data, 'marshal.db') }
     end

     def default_cache_classes
       true
     end

     def default_whiny_nils
       false
     end
     
     def default_plugin_glob
       File.join(root_path, *%w{ vendor plugins * })
     end
     
     def default_helper_glob
       File.join(root_path, *%w{ app helpers *.rb })
     end
     
     def default_initalizer_glob
       File.join(root_path, *%w{ config initializers **/*.rb })
     end
     
     def default_first_run_glob
       File.join(root_path, *%w{ config first_run **/*.rb })
     end
          
     def default_publisher
       "Bowline"
     end
     
     def default_copyright
       "Copyright #{Time.now.year}"
     end
  end
end

# Needs to be duplicated from Active Support since its needed before Active
# Support is available. Here both Options and Hash are namespaced to prevent
# conflicts with other implementations AND with the classes residing in Active Support.
class Bowline::OrderedOptions < Array #:nodoc:
  def []=(key, value)
    key = key.to_sym

    if pair = find_pair(key)
      pair.pop
      pair << value
    else
      self << [key, value]
    end
  end

  def [](key)
    pair = find_pair(key.to_sym)
    pair ? pair.last : nil
  end

  def method_missing(name, *args)
    if name.to_s =~ /(.*)=$/
      self[$1.to_sym] = args.first
    else
      self[name]
    end
  end

  private
    def find_pair(key)
      self.each { |i| return i if i.first == key }
      return false
    end
end