require 'delegate'

module Padrino
  ##
  # Represents a particular mounted Padrino application.
  # Stores the name of the application (app folder name) and url mount path.
  #
  # @example
  #   Mounter.new("blog_app", :app_class => "Blog").to("/blog")
  #   Mounter.new("blog_app", :app_file => "/path/to/blog/app.rb").to("/blog")
  #
  class Mounter
    DEFAULT_CASCADE = [404, 405]
    class MounterException < RuntimeError
    end

    class ApplicationWrapper < SimpleDelegator
      attr_accessor :uri_root
      attr_writer :public_folder

      def initialize(app, options = {})
        @options = options
        super(app)
      end

      def dependencies
        @__dependencies ||= Dir["#{root}/**/*.rb"]
      end

      def prerequisite
        @__prerequisite ||= []
      end

      def app_file
        return @__app_file if @__app_file
        obj = __getobj__
        @__app_file = obj.respond_to?(:app_file) ? obj.app_file : @options[:app_file]
      end

      def root
        return @__root if @__root
        obj = __getobj__
        @__root = obj.respond_to?(:root) ? obj.root : File.expand_path("#{app_file}/../")
      end

      def public_folder
        return @public_folder if @public_folder
        obj = __getobj__
        @public_folder = obj.respond_to?(:public_folder) ? obj.public_folder : ""
      end

      def app_name
        @__app_name ||= @options[:app_name] || __getobj__.to_s.underscore.to_sym
      end

      def setup_application!
        @configured ||=
          begin
            $LOAD_PATH.concat(prerequisite)
            Padrino.require_dependencies(dependencies, :force => true)
            true
          end
      end
    end

    attr_accessor :name, :uri_root, :app_file, :app_class, :app_root, :app_obj, :app_host, :cascade

    ##
    # @param [String, Padrino::Application] name
    #   The app name or the {Padrino::Application} class.
    #
    # @param [Hash] options
    # @option options [Symbol] :app_class (Detected from name)
    # @option options [Symbol] :app_file (Automatically detected)
    # @option options [Symbol] :app_obj (Detected)
    # @option options [Symbol] :app_root (Directory of :app_file)
    # @option options [Symbol] :gem The gem to load the app from (Detected from name)
    #
    def initialize(name, options={})
      @name      = name.to_s
      @app_class = options[:app_class] || @name.camelize
      @gem       = options[:gem]       || @app_class.split("::").first.underscore
      @app_file  = options[:app_file]  || locate_app_file
      @app_obj   = options[:app_obj]   || app_constant || locate_app_object
      ensure_app_file! || ensure_app_object!
      @app_obj   = ApplicationWrapper.new(@app_obj, options) unless padrino_application?
      @app_root  = options[:app_root]  || (@app_obj.respond_to?(:root) && @app_obj.root || File.dirname(@app_file))
      @uri_root  = "/"
      @cascade   = options[:cascade] ? true == options[:cascade] ? DEFAULT_CASCADE.dup : Array(options[:cascade]) : []
      Padrino::Reloader.exclude_constants << @app_class
    end

    def padrino_application?
      @app_obj.ancestors.include?(Padrino::Application)
    rescue NameError
      false
    end

    ##
    # Registers the mounted application onto Padrino.
    #
    # @param [String] mount_url
    #   Path where we mount the app.
    #
    # @example
    #   Mounter.new("blog_app").to("/blog")
    #
    def to(mount_url)
      @uri_root  = mount_url
      Padrino.insert_mounted_app(self)
      self
    end

    ##
    # Registers the mounted application onto Padrino for the given host.
    #
    # @param [String] mount_host
    #   Host name.
    #
    # @example
    #   Mounter.new("blog_app").to("/blog").host("blog.padrino.org")
    #   Mounter.new("blog_app").host("blog.padrino.org")
    #   Mounter.new("catch_all").host(/.*\.padrino.org/)
    #
    def host(mount_host)
      @app_host = mount_host
      Padrino.insert_mounted_app(self)
      self
    end

    ##
    # Maps Padrino application onto a Padrino::Router.
    # For use in constructing a Rack application.
    #
    # @param [Padrino::Router]
    #
    # @return [Padrino::Router]
    #
    # @example
    #   @app.map_onto(router)
    #
    def map_onto(router)
      app_data = self
      app_obj = @app_obj
      if padrino_application?
        app_obj.set :uri_root,       app_data.uri_root
        app_obj.set :app_name,       app_data.app_obj.app_name.to_s
        app_obj.set :app_file,       app_data.app_file unless ::File.exist?(app_obj.app_file)
        app_obj.set :root,           app_data.app_root unless app_data.app_root.blank?
        app_obj.set :public_folder,  Padrino.root('public', app_data.uri_root) unless File.exist?(app_obj.public_folder)
        app_obj.set :static,         File.exist?(app_obj.public_folder) if app_obj.nil?
        app_obj.set :cascade,        app_data.cascade
      else
        app_obj.uri_root      = app_data.uri_root
        app_obj.public_folder = Padrino.root('public', app_data.uri_root) unless ::File.exist?(app_obj.public_folder)
      end
      app_obj.setup_application! # Initializes the app here with above settings.
      router.map(:to => app_obj, :path => app_data.uri_root, :host => app_data.app_host)
    end

    ###
    # Returns the route objects for the mounted application.
    #
    def routes
      app_obj.routes
    end

    ###
    # Returns the basic route information for each named route.
    #
    # @return [Array]
    #   Array of routes.
    #
    def named_routes
      app_obj.routes.map { |route|
        route_name = route.name.to_s
        route_name = 
          if route.controller
            route_name.split(" ", 2).map{|name| ":#{name}" }.join(", ")
          else
            ":#{route_name}"
          end
        name_array = "(#{route_name})"
        request_method = route.request_methods.first
        next if route.name.blank? || request_method == 'HEAD'
        original_path = route.original_path.is_a?(Regexp) ? route.original_path.inspect : route.original_path
        full_path = File.join(uri_root, original_path)
        OpenStruct.new(:verb => request_method, :identifier => route.name, :name => name_array, :path => full_path)
      }.compact
    end

    ##
    # Makes two Mounters equal if they have the same name and uri_root.
    #
    # @param [Padrino::Mounter] other
    #
    def ==(other)
      other.is_a?(Mounter) && self.app_class == other.app_class && self.uri_root == other.uri_root
    end

    ##
    # @return [Padrino::Application]
    #  the class object for the app if defined, nil otherwise.
    #
    def app_constant
      klass = Object
      for piece in app_class.split("::")
        piece = piece.to_sym
        if klass.const_defined?(piece, false)
          klass = klass.const_get(piece)
        else
          return
        end
      end
      klass
    end

    protected
    ##
    # Locates and requires the file to load the app constant.
    #
    def locate_app_object
      @_app_object ||= begin
        ensure_app_file!
        Padrino.require_dependencies(app_file)
        app_constant
      end
    end

    ##
    # Returns the determined location of the mounted application main file.
    #
    def locate_app_file
      candidates  = []
      candidates << app_constant.app_file if app_constant.respond_to?(:app_file) && File.exist?(app_constant.app_file.to_s)
      candidates << Padrino.first_caller if File.identical?(Padrino.first_caller.to_s, Padrino.called_from.to_s)
      candidates << Padrino.mounted_root(name.downcase, "app.rb")
      simple_name = name.split("::").last.downcase
      mod_name = name.split("::")[0..-2].join("::")
      Padrino.modules.each do |mod|
        if mod.name == mod_name
          candidates << mod.root(simple_name, "app.rb")
        end
      end
      candidates << Padrino.root("app", "app.rb")
      candidates.find { |candidate| File.exist?(candidate) }
    end

    ###
    # Raises an exception unless app_file is located properly.
    #
    def ensure_app_file!
      message = "Unable to locate source file for app '#{app_class}', try with :app_file => '/path/app.rb'"
      raise MounterException, message unless @app_file
    end

    ###
    # Raises an exception unless app_obj is defined properly.
    #
    def ensure_app_object!
      message = "Unable to locate app for '#{app_class}', try with :app_class => 'MyAppClass'"
      raise MounterException, message unless @app_obj
    end
  end

  class << self
    attr_writer :mounted_root # Set root directory where padrino searches mounted apps

    ##
    # @param [Array] args
    #
    # @return [String]
    #   the root to the mounted apps base directory.
    #
    def mounted_root(*args)
      Padrino.root(@mounted_root ||= "", *args)
    end

    ##
    # @return [Array]
    #   the mounted padrino applications (MountedApp objects)
    #
    def mounted_apps
      @mounted_apps ||= []
    end

    ##
    # Inserts a Mounter object into the mounted applications (avoids duplicates).
    #
    # @param [Padrino::Mounter] mounter
    #
    def insert_mounted_app(mounter)
      Padrino.mounted_apps.push(mounter) unless Padrino.mounted_apps.include?(mounter)
    end

    ##
    # Mounts a new sub-application onto Padrino project.
    #
    # @see Padrino::Mounter#new
    #
    # @example
    #   Padrino.mount("blog_app").to("/blog")
    #
    def mount(name, options={})
      Mounter.new(name, options)
    end
  end
end
