require 'delegate'
require 'optparse'
require 'fileutils'
require 'tempfile'
require 'erb'

module RubiGen
  module Commands
    # Here's a convenient way to get a handle on generator commands.
    # Command.instance('destroy', my_generator) instantiates a Destroy
    # delegate of my_generator ready to do your dirty work.
    def self.instance(command, generator)
      const_get(command.to_s.camelize).new(generator)
    end

    # Even more convenient access to commands.  Include Commands in
    # the generator Base class to get a nice #command instance method
    # which returns a delegate for the requested command.
    def self.included(base)
      base.send(:define_method, :command) do |command|
        Commands.instance(command, self)
      end
    end


    # Generator commands delegate RubiGen::Base and implement
    # a standard set of actions.  Their behavior is defined by the way
    # they respond to these actions: Create brings life; Destroy brings
    # death; List passively observes.
    #
    # Commands are invoked by replaying (or rewinding) the generator's
    # manifest of actions.  See RubiGen::Manifest and
    # RubiGen::Base#manifest method that generator subclasses
    # are required to override.
    #
    # Commands allows generators to "plug in" invocation behavior, which
    # corresponds to the GoF Strategy pattern.
    class Base < DelegateClass(RubiGen::Base)
      # Replay action manifest.  RewindBase subclass rewinds manifest.
      def invoke!
        manifest.replay(self)
      end

      def dependency(generator_name, args, runtime_options = {})
        logger.dependency(generator_name) do
          self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke!
        end
      end

      # Does nothing for all commands except Create.
      def class_collisions(*class_names)
      end

      # Does nothing for all commands except Create.
      def readme(*args)
      end

      protected
        def migration_directory(relative_path)
          directory(@migration_directory = relative_path)
        end

        def existing_migrations(file_name)
          Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/)
        end

        def migration_exists?(file_name)
          not existing_migrations(file_name).empty?
        end

        def current_migration_number
          Dir.glob("#{@migration_directory}/[0-9]*.rb").inject(0) do |max, file_path|
            n = File.basename(file_path).split('_', 2).first.to_i
            if n > max then n else max end
          end
        end

        def next_migration_number
          current_migration_number + 1
        end

        def next_migration_string(padding = 3)
          "%.#{padding}d" % next_migration_number
        end

        def gsub_file(relative_destination, regexp, *args, &block)
          path = destination_path(relative_destination)
          content = File.read(path).gsub(regexp, *args, &block)
          File.open(path, 'wb') { |file| file.write(content) }
        end

      private
        # Ask the user interactively whether to force collision.
        def force_file_collision?(destination, src, dst, file_options = {}, &block)
          $stdout.print "overwrite #{destination}? [Ynaiqd] "
          case $stdin.gets
            when /d/i
              Tempfile.open(File.basename(destination), File.dirname(dst)) do |temp|
                temp.write render_file(src, file_options, &block)
                temp.rewind
                $stdout.puts `#{diff_cmd} #{dst} #{temp.path}`
              end
              puts "retrying"
              raise 'retry diff'
            when /a/i
              $stdout.puts "forcing #{spec.name}"
              options[:collision] = :force
            when /i/i
              $stdout.puts "ignoring #{spec.name}"
              options[:collision] = :skip
            when /q/i
              $stdout.puts "aborting #{spec.name}"
              raise SystemExit
            when /n/i then :skip
            when /y/i then :force
            else force_file_collision?(destination, src, dst, file_options, &block)
          end
        rescue
          retry
        end

        def diff_cmd
          ENV['RAILS_DIFF'] || 'diff -u'
        end

        def render_template_part(template_options)
          # Getting Sandbox to evaluate part template in it
          part_binding = template_options[:sandbox].call.sandbox_binding
          part_rel_path = template_options[:insert]
          part_path = source_path(part_rel_path)

          # Render inner template within Sandbox binding
          rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding)
          begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id])
          end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id])
          begin_mark + rendered_part + end_mark
        end

        def template_part_mark(name, id)
          "<!--[#{name}:#{id}]-->\n"
        end
    end

    # Base class for commands which handle generator actions in reverse, such as Destroy.
    class RewindBase < Base
      # Rewind action manifest.
      def invoke!
        manifest.rewind(self)
      end
    end


    # Create is the premier generator command.  It copies files, creates
    # directories, renders templates, and more.
    class Create < Base

      # Check whether the given class names are already taken.  
      # In the future, expand to check other namespaces
      # such as the rest of the user's app.
      def class_collisions(*class_names)
        class_names.flatten.each do |class_name|
          # Convert to string to allow symbol arguments.
          class_name = class_name.to_s

          # Skip empty strings.
          next if class_name.strip.empty?

          # Split the class from its module nesting.
          nesting = class_name.split('::')
          name = nesting.pop

          # Extract the last Module in the nesting.
          last = nesting.inject(Object) { |last, nest|
            break unless last.const_defined?(nest)
            last.const_get(nest)
          }

          # If the last Module exists, check whether the given
          # class exists and raise a collision if so.
          if last and last.const_defined?(name.camelize)
            raise_class_collision(class_name)
          end
        end
      end

      # Copy a file from source to destination with collision checking.
      #
      # The file_options hash accepts :chmod and :shebang and :collision options.
      # :chmod sets the permissions of the destination file:
      #   file 'config/empty.log', 'log/test.log', :chmod => 0664
      # :shebang sets the #!/usr/bin/ruby line for scripts
      #   file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
      # :collision sets the collision option only for the destination file: 
      #   file 'settings/server.yml', 'config/server.yml', :collision => :skip
      #
      # Collisions are handled by checking whether the destination file
      # exists and either skipping the file, forcing overwrite, or asking
      # the user what to do.
      def file(relative_source, relative_destination, file_options = {}, &block)
        # Determine full paths for source and destination files.
        source              = source_path(relative_source)
        destination         = destination_path(relative_destination)
        destination_exists  = File.exists?(destination)

        # If source and destination are identical then we're done.
        if destination_exists and identical?(source, destination, &block)
          return logger.identical(relative_destination) 
        end

        # Check for and resolve file collisions.
        if destination_exists

          # Make a choice whether to overwrite the file.  :force and
          # :skip already have their mind made up, but give :ask a shot.
          choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask
            when :ask   then force_file_collision?(relative_destination, source, destination, file_options, &block)
            when :force then :force
            when :skip  then :skip
            else raise "Invalid collision option: #{options[:collision].inspect}"
          end

          # Take action based on our choice.  Bail out if we chose to
          # skip the file; otherwise, log our transgression and continue.
          case choice
            when :force then logger.force(relative_destination)
            when :skip  then return(logger.skip(relative_destination))
            else raise "Invalid collision choice: #{choice}.inspect"
          end

        # File doesn't exist so log its unbesmirched creation.
        else
          logger.create relative_destination
        end

        # If we're pretending, back off now.
        return if options[:pretend]

        # Write destination file with optional shebang.  Yield for content
        # if block given so templaters may render the source file.  If a
        # shebang is requested, replace the existing shebang or insert a
        # new one.
        File.open(destination, 'wb') do |dest|
          dest.write render_file(source, file_options, &block)
        end

        # Optionally change permissions.
        if file_options[:chmod]
          FileUtils.chmod(file_options[:chmod], destination)
        end

        # Optionally add file to subversion
        system("svn add #{destination}") if options[:svn]
      end

      def file_copy_each(files, path=nil, options = {})
        path = path ? "#{path}/" : ""
        files.each do |file_name|
          file "#{path}#{file_name}", "#{path}#{file_name}", options
        end
      end
      
      def folder(template_path, path=nil, options = {})
        template_path = "/" if template_path.blank?
        source = source_path(template_path)
        files  = Dir[source + '/*'].select { |file| File.file? file }.map { |file| file.sub(/^#{source}/,"") }
        files.each do |file_name|
          file "#{template_path}#{file_name}", "#{path}#{file_name}", options
        end
      end
      
      # Checks if the source and the destination file are identical. If
      # passed a block then the source file is a template that needs to first
      # be evaluated before being compared to the destination.
      def identical?(source, destination, &block)
        return false if File.directory? destination
        source      = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source)
        destination = IO.read(destination)
        source == destination
      end

      # Generate a file using an ERuby template.
      # Looks up and evalutes a template by name and writes the result.
      #
      # The ERB template uses explicit trim mode to best control the
      # proliferation of whitespace in generated code.  <%- trims leading
      # whitespace; -%> trims trailing whitespace including one newline.
      #
      # A hash of template options may be passed as the last argument.
      # The options accepted by the file are accepted as well as :assigns,
      # a hash of variable bindings.  Example:
      #   template 'foo', 'bar', :assigns => { :action => 'view' }
      #
      # Template is implemented in terms of file.  It calls file with a
      # block which takes a file handle and returns its rendered contents.
      def template(relative_source, relative_destination, template_options = {})
        file(relative_source, relative_destination, template_options) do |file|
          # Evaluate any assignments in a temporary, throwaway binding.
          vars = template_options[:assigns] || {}
          b = binding
          vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }

          # Render the source file with the temporary binding.
          ERB.new(file.read, nil, '-').result(b)
        end
      end
      
      def template_copy_each(files, path = nil, options = {})
        path = path ? "#{path}/" : ""
        files.each do |file_name|
          template "#{path}#{file_name}", "#{path}#{file_name.gsub(/\.erb$/,'')}", options
        end
      end

      def complex_template(relative_source, relative_destination, template_options = {})
        options = template_options.dup
        options[:assigns] ||= {}
        options[:assigns]['template_for_inclusion'] = render_template_part(template_options)
        template(relative_source, relative_destination, options)
      end

      # Create a directory including any missing parent directories.
      # Always directories which exist.
      def directory(relative_path)
        path = destination_path(relative_path)
        if File.exists?(path)
          logger.exists relative_path
        else
          logger.create relative_path
          unless options[:pretend]
            FileUtils.mkdir_p(path)
      
            # Subversion doesn't do path adds, so we need to add
            # each directory individually.
            # So stack up the directory tree and add the paths to
            # subversion in order without recursion.
            if options[:svn]
              stack=[relative_path]
              until File.dirname(stack.last) == stack.last # dirname('.') == '.'
                stack.push File.dirname(stack.last)
              end
              stack.reverse_each do |rel_path|
                svn_path = destination_path(rel_path)
                system("svn add -N #{svn_path}") unless File.directory?(File.join(svn_path, '.svn'))
              end
            end
          end
        end
      end

      # Display a README.
      def readme(*relative_sources)
        relative_sources.flatten.each do |relative_source|
          logger.readme relative_source
          puts File.read(source_path(relative_source)) unless options[:pretend]
        end
      end

      # Display a README.
      def write_manifest(relative_destination)
        files = ([relative_destination] + Dir["#{destination_root}/**/*"])
        files.reject! { |file| File.directory?(file) }
        files.map! { |path| path.sub("#{destination_root}/","") }
        files = files.uniq.sort
        
        
        destination         = destination_path(relative_destination)
        destination_exists  = File.exists?(destination)

        # Check for and resolve file collisions.
        if destination_exists
          # Always recreate the Manifest (perhaps we need to give the option... like normal files)
          choice = :force
          logger.force(relative_destination)

        # File doesn't exist so log its unbesmirched creation.
        else
          logger.create relative_destination
        end

        # If we're pretending, back off now.
        return if options[:pretend]
        
        # Write destination file with optional shebang.  Yield for content
        # if block given so templaters may render the source file.  If a
        # shebang is requested, replace the existing shebang or insert a
        # new one.
        File.open(destination, 'wb') do |dest|
          dest.write files.join("\n")
          dest.write "\n"
        end

        # Optionally add file to subversion
        system("svn add #{destination}") if options[:svn]
        
      end

      # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
      def migration_template(relative_source, relative_destination, template_options = {})
        migration_directory relative_destination
        migration_file_name = template_options[:migration_file_name] || file_name
        raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name)
        template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options)
      end

      def route_resources(*resources)
        resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
        sentinel = 'ActionController::Routing::Routes.draw do |map|'

        logger.route "map.resources #{resource_list}"
        unless options[:pretend]
          gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
            "#{match}\n  map.resources #{resource_list}\n"
          end
        end
      end

      private
        def render_file(path, options = {})
          File.open(path, 'rb') do |file|
            if block_given?
              yield file
            else
              content = ''
              if shebang = options[:shebang]
                content << "#!#{shebang}\n"
                if line = file.gets
                  content << "line\n" if line !~ /^#!/
                end
              end
              content << file.read
            end
          end
        end

        # Raise a usage error with an informative WordNet suggestion.
        # Thanks to Florian Gross (flgr).
        def raise_class_collision(class_name)
          message = <<end_message
The name '#{class_name}' is reserved.
Please choose an alternative and run this generator again.
end_message
          if suggest = find_synonyms(class_name)
            message << "\n  Suggestions:  \n\n"
            message << suggest.join("\n")
          end
          raise UsageError, message
        end

        SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/cgi-bin/webwn2.0?stage=2&word=%s&posnumber=1&searchtypenumber=2&senses=&showglosses=1"

        # Look up synonyms on WordNet.  Thanks to Florian Gross (flgr).
        def find_synonyms(word)
          require 'open-uri'
          require 'timeout'
          timeout(5) do
            open(SYNONYM_LOOKUP_URI % word) do |stream|
              data = stream.read.gsub("&nbsp;", " ").gsub("<BR>", "")
              data.scan(/^Sense \d+\n.+?\n\n/m)
            end
          end
        rescue Exception
          return nil
        end
    end


    # Undo the actions performed by a generator.  Rewind the action
    # manifest and attempt to completely erase the results of each action.
    class Destroy < RewindBase
      # Remove a file if it exists and is a file.
      def file(relative_source, relative_destination, file_options = {})
        destination = destination_path(relative_destination)
        if File.exists?(destination)
          logger.rm relative_destination
          unless options[:pretend]
            if options[:svn]
              # If the file has been marked to be added
              # but has not yet been checked in, revert and delete
              if options[:svn][relative_destination]
                system("svn revert #{destination}")
                FileUtils.rm(destination)
              else
              # If the directory is not in the status list, it
              # has no modifications so we can simply remove it
                system("svn rm #{destination}")
              end  
            else
              FileUtils.rm(destination)
            end
          end
        else
          logger.missing relative_destination
          return
        end
      end

      # Templates are deleted just like files and the actions take the
      # same parameters, so simply alias the file method.
      alias_method :template, :file

      # Remove each directory in the given path from right to left.
      # Remove each subdirectory if it exists and is a directory.
      def directory(relative_path)
        parts = relative_path.split('/')
        until parts.empty?
          partial = File.join(parts)
          path = destination_path(partial)
          if File.exists?(path)
            if Dir[File.join(path, '*')].empty?
              logger.rmdir partial
              unless options[:pretend]
                if options[:svn]
                  # If the directory has been marked to be added
                  # but has not yet been checked in, revert and delete
                  if options[:svn][relative_path]
                    system("svn revert #{path}")
                    FileUtils.rmdir(path)
                  else
                  # If the directory is not in the status list, it
                  # has no modifications so we can simply remove it
                    system("svn rm #{path}")
                  end
                else
                  FileUtils.rmdir(path)
                end
              end
            else
              logger.notempty partial
            end
          else
            logger.missing partial
          end
          parts.pop
        end
      end

      def complex_template(*args)
        # nothing should be done here
      end

      # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
      def migration_template(relative_source, relative_destination, template_options = {})
        migration_directory relative_destination

        migration_file_name = template_options[:migration_file_name] || file_name
        unless migration_exists?(migration_file_name)
          puts "There is no migration named #{migration_file_name}"
          return
        end


        existing_migrations(migration_file_name).each do |file_path|
          file(relative_source, file_path, template_options)
        end
      end

      def route_resources(*resources)
        resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
        look_for = "\n  map.resources #{resource_list}\n"
        logger.route "map.resources #{resource_list}"
        gsub_file 'config/routes.rb', /(#{look_for})/mi, ''
      end
    end


    # List a generator's action manifest.
    class List < Base
      def dependency(generator_name, args, options = {})
        logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})"
      end

      def class_collisions(*class_names)
        logger.class_collisions class_names.join(', ')
      end

      def file(relative_source, relative_destination, options = {})
        logger.file relative_destination
      end

      def template(relative_source, relative_destination, options = {})
        logger.template relative_destination
      end

      def complex_template(relative_source, relative_destination, options = {})
        logger.template "#{options[:insert]} inside #{relative_destination}"
      end

      def directory(relative_path)
        logger.directory "#{destination_path(relative_path)}/"
      end

      def readme(*args)
        logger.readme args.join(', ')
      end
      
      def migration_template(relative_source, relative_destination, options = {})
        migration_directory relative_destination
        logger.migration_template file_name
      end

      def route_resources(*resources)
        resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
        logger.route "map.resources #{resource_list}"
      end
    end

    # Update generator's action manifest.
    class Update < Create
      def file(relative_source, relative_destination, options = {})
        # logger.file relative_destination
      end

      def template(relative_source, relative_destination, options = {})
        # logger.template relative_destination
      end

      def complex_template(relative_source, relative_destination, template_options = {})

         begin
           dest_file = destination_path(relative_destination)
           source_to_update = File.readlines(dest_file).join
         rescue Errno::ENOENT
           logger.missing relative_destination
           return
         end

         logger.refreshing "#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"

         begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id]))
         end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id]))

         # Refreshing inner part of the template with freshly rendered part.
         rendered_part = render_template_part(template_options)
         source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part)

         File.open(dest_file, 'w') { |file| file.write(source_to_update) }
      end

      def directory(relative_path)
        # logger.directory "#{destination_path(relative_path)}/"
      end
    end

  end
end