require 'rails/generators/app_base'

module Rails
  module ActionMethods # :nodoc:
    attr_reader :options

    def initialize(generator)
      @generator = generator
      @options   = generator.options
    end

    private
      %w(template copy_file directory empty_directory inside
         empty_directory_with_keep_file create_file chmod shebang).each do |method|
        class_eval <<-RUBY, __FILE__, __LINE__ + 1
          def #{method}(*args, &block)
            @generator.send(:#{method}, *args, &block)
          end
        RUBY
      end

      # TODO: Remove once this is fully in place
      def method_missing(meth, *args, &block)
        @generator.send(meth, *args, &block)
      end
  end

  # The application builder allows you to override elements of the application
  # generator without being forced to reverse the operations of the default
  # generator.
  #
  # This allows you to override entire operations, like the creation of the
  # Gemfile, README, or JavaScript files, without needing to know exactly
  # what those operations do so you can create another template action.
  class AppBuilder
    def rakefile
      template "Rakefile"
    end

    def readme
      copy_file "README.md", "README.md"
    end

    def gemfile
      template "Gemfile"
    end

    def configru
      template "config.ru"
    end

    def gitignore
      template "gitignore", ".gitignore"
    end

    def app
      directory 'app'

      keep_file  'app/assets/images'
      keep_file  'app/assets/javascripts/channels' unless options[:skip_action_cable]

      keep_file  'app/controllers/concerns'
      keep_file  'app/models/concerns'
    end

    def bin
      directory "bin" do |content|
        "#{shebang}\n" + content
      end
      chmod "bin", 0755 & ~File.umask, verbose: false
    end

    def config
      empty_directory "config"

      inside "config" do
        template "routes.rb"
        template "application.rb"
        template "environment.rb"
        template "secrets.yml"

        directory "environments"
        directory "initializers"
        directory "locales"
        directory "redis" unless options[:skip_action_cable]
      end
    end

    def config_when_updating
      cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb')
      callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb')
      active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb')

      config

      unless callback_terminator_config_exist
        remove_file 'config/initializers/callback_terminator.rb'
      end

      unless cookie_serializer_config_exist
        gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal'
      end

      unless active_record_belongs_to_required_by_default_config_exist
        remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
      end
    end

    def database_yml
      template "config/databases/#{options[:database]}.yml", "config/database.yml"
    end

    def db
      directory "db"
    end

    def lib
      empty_directory 'lib'
      empty_directory_with_keep_file 'lib/tasks'
      empty_directory_with_keep_file 'lib/assets'
    end

    def log
      empty_directory_with_keep_file 'log'
    end

    def public_directory
      directory "public", "public", recursive: false
    end

    def test
      empty_directory_with_keep_file 'test/fixtures'
      empty_directory_with_keep_file 'test/fixtures/files'
      empty_directory_with_keep_file 'test/controllers'
      empty_directory_with_keep_file 'test/mailers'
      empty_directory_with_keep_file 'test/models'
      empty_directory_with_keep_file 'test/helpers'
      empty_directory_with_keep_file 'test/integration'

      template 'test/test_helper.rb'
    end

    def tmp
      empty_directory_with_keep_file "tmp"
      empty_directory "tmp/cache"
      empty_directory "tmp/cache/assets"
    end

    def vendor
      vendor_javascripts
      vendor_stylesheets
    end

    def vendor_javascripts
      unless options[:skip_javascript]
        empty_directory_with_keep_file 'vendor/assets/javascripts'
      end
    end

    def vendor_stylesheets
      empty_directory_with_keep_file 'vendor/assets/stylesheets'
    end
  end

  module Generators
    # We need to store the RAILS_DEV_PATH in a constant, otherwise the path
    # can change in Ruby 1.8.7 when we FileUtils.cd.
    RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__))
    RESERVED_NAMES = %w[application destroy plugin runner test]

    class AppGenerator < AppBase # :nodoc:
      add_shared_options_for "application"

      # Add bin/rails options
      class_option :version, type: :boolean, aliases: "-v", group: :rails,
                             desc: "Show Rails version number and quit"

      class_option :api, type: :boolean,
                         desc: "Preconfigure smaller stack for API only apps"

      def initialize(*args)
        super

        unless app_path
          raise Error, "Application name should be provided in arguments. For details run: rails --help"
        end

        if !options[:skip_active_record] && !DATABASES.include?(options[:database])
          raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
        end

        # Force sprockets to be skipped when generating API only apps.
        # Can't modify options hash as it's frozen by default.
        self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze if options[:api]
      end

      public_task :set_default_accessors!
      public_task :create_root

      def create_root_files
        build(:readme)
        build(:rakefile)
        build(:configru)
        build(:gitignore) unless options[:skip_git]
        build(:gemfile)   unless options[:skip_gemfile]
      end

      def create_app_files
        build(:app)
      end

      def create_bin_files
        build(:bin)
      end

      def create_config_files
        build(:config)
      end

      def update_config_files
        build(:config_when_updating)
      end
      remove_task :update_config_files

      def create_boot_file
        template "config/boot.rb"
      end

      def create_active_record_files
        return if options[:skip_active_record]
        build(:database_yml)
      end

      def create_db_files
        build(:db)
      end

      def create_lib_files
        build(:lib)
      end

      def create_log_files
        build(:log)
      end

      def create_public_files
        build(:public_directory)
      end

      def create_test_files
        build(:test) unless options[:skip_test]
      end

      def create_tmp_files
        build(:tmp)
      end

      def create_vendor_files
        build(:vendor)
      end

      def delete_app_assets_if_api_option
        if options[:api]
          remove_dir 'app/assets'
          remove_dir 'lib/assets'
          remove_dir 'tmp/cache/assets'
          remove_dir 'vendor/assets'
        end
      end

      def delete_app_helpers_if_api_option
        if options[:api]
          remove_dir 'app/helpers'
          remove_dir 'test/helpers'
        end
      end

      def delete_app_views_if_api_option
        if options[:api]
          remove_dir 'app/views'
        end
      end

      def delete_js_folder_skipping_javascript
        if options[:skip_javascript]
          remove_dir 'app/assets/javascripts'
        end
      end

      def delete_assets_initializer_skipping_sprockets
        if options[:skip_sprockets]
          remove_file 'config/initializers/assets.rb'
        end
      end

      def delete_application_record_skipping_active_record
        if options[:skip_active_record]
          remove_file 'app/models/application_record.rb'
        end
      end

      def delete_action_mailer_files_skipping_action_mailer
        if options[:skip_action_mailer]
          remove_file 'app/mailers/application_mailer.rb'
          remove_file 'app/views/layouts/mailer.html.erb'
          remove_file 'app/views/layouts/mailer.text.erb'
        end
      end

      def delete_active_record_initializers_skipping_active_record
        if options[:skip_active_record]
          remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
        end
      end

      def delete_non_api_initializers_if_api_option
        if options[:api]
          remove_file 'config/initializers/session_store.rb'
          remove_file 'config/initializers/cookies_serializer.rb'
          remove_file 'config/initializers/request_forgery_protection.rb'
        end
      end

      def finish_template
        build(:leftovers)
      end

      public_task :apply_rails_template, :run_bundle
      public_task :generate_spring_binstubs

      def run_after_bundle_callbacks
        @after_bundle_callbacks.each(&:call)
      end

    protected

      def self.banner
        "rails new #{self.arguments.map(&:usage).join(' ')} [options]"
      end

      # Define file as an alias to create_file for backwards compatibility.
      def file(*args, &block)
        create_file(*args, &block)
      end

      def app_name
        @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', '').tr(". ", "_")
      end

      def defined_app_name
        defined_app_const_base.underscore
      end

      def defined_app_const_base
        Rails.respond_to?(:application) && defined?(Rails::Application) &&
          Rails.application.is_a?(Rails::Application) && Rails.application.class.name.sub(/::Application$/, "")
      end

      alias :defined_app_const_base? :defined_app_const_base

      def app_const_base
        @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, '_').squeeze('_').camelize
      end
      alias :camelized :app_const_base

      def app_const
        @app_const ||= "#{app_const_base}::Application"
      end

      def valid_const?
        if app_const =~ /^\d/
          raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers."
        elsif RESERVED_NAMES.include?(app_name)
          raise Error, "Invalid application name #{app_name}. Please give a " \
                       "name which does not match one of the reserved rails " \
                       "words: #{RESERVED_NAMES.join(", ")}"
        elsif Object.const_defined?(app_const_base)
          raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name."
        end
      end

      def app_secret
        SecureRandom.hex(64)
      end

      def mysql_socket
        @mysql_socket ||= [
          "/tmp/mysql.sock",                        # default
          "/var/run/mysqld/mysqld.sock",            # debian/gentoo
          "/var/tmp/mysql.sock",                    # freebsd
          "/var/lib/mysql/mysql.sock",              # fedora
          "/opt/local/lib/mysql/mysql.sock",        # fedora
          "/opt/local/var/run/mysqld/mysqld.sock",  # mac + darwinports + mysql
          "/opt/local/var/run/mysql4/mysqld.sock",  # mac + darwinports + mysql4
          "/opt/local/var/run/mysql5/mysqld.sock",  # mac + darwinports + mysql5
          "/opt/lampp/var/mysql/mysql.sock"         # xampp for linux
        ].find { |f| File.exist?(f) } unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
      end

      def get_builder_class
        defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder
      end
    end

    # This class handles preparation of the arguments before the AppGenerator is
    # called. The class provides version or help information if they were
    # requested, and also constructs the railsrc file (used for extra configuration
    # options).
    #
    # This class should be called before the AppGenerator is required and started
    # since it configures and mutates ARGV correctly.
    class ARGVScrubber # :nodoc:
      def initialize(argv = ARGV)
        @argv = argv
      end

      def prepare!
        handle_version_request!(@argv.first)
        handle_invalid_command!(@argv.first, @argv) do
          handle_rails_rc!(@argv.drop(1))
        end
      end

      def self.default_rc_file
        File.expand_path('~/.railsrc')
      end

      private

        def handle_version_request!(argument)
          if ['--version', '-v'].include?(argument)
            require 'rails/version'
            puts "Rails #{Rails::VERSION::STRING}"
            exit(0)
          end
        end

        def handle_invalid_command!(argument, argv)
          if argument == "new"
            yield
          else
            ['--help'] + argv.drop(1)
          end
        end

        def handle_rails_rc!(argv)
          if argv.find { |arg| arg == '--no-rc' }
            argv.reject { |arg| arg == '--no-rc' }
          else
            railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
          end
        end

        def railsrc(argv)
          if (customrc = argv.index{ |x| x.include?("--rc=") })
            fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
            yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
          else
            yield argv, self.class.default_rc_file
          end
        end

        def read_rc_file(railsrc)
          extra_args = File.readlines(railsrc).flat_map(&:split)
          puts "Using #{extra_args.join(" ")} from #{railsrc}"
          extra_args
        end

        def insert_railsrc_into_argv!(argv, railsrc)
          return argv unless File.exist?(railsrc)
          extra_args = read_rc_file railsrc
          argv.take(1) + extra_args + argv.drop(1)
        end
    end
  end
end
