# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

module Buildr #:nodoc:
  module Doc
    include Extension

    class << self
      def select_by_lang(lang)
        fail 'Unable to define doc task for nil language' if lang.nil?
        engines.detect { |e| e.language.to_sym == lang.to_sym }
      end

      alias_method :select, :select_by_lang

      def select_by_name(name)
        fail 'Unable to define doc task for nil' if name.nil?
        engines.detect { |e| e.to_sym == name.to_sym }
      end

      def engines
        @engines ||= []
      end
    end


    # Base class for any documentation provider.  Defines most
    # common functionality (things like @into@, @from@ and friends).
    class Base
      class << self
        attr_accessor :language, :source_ext

        def specify(options)
          @language = options[:language]
          @source_ext = options[:source_ext]
        end

        def to_sym
          @symbol ||= name.split('::').last.downcase.to_sym
        end
      end

      attr_reader :project

      def initialize(project)
        @project = project
      end
    end


    class DocTask < Rake::Task

      # The target directory for the generated documentation files.
      attr_reader :target

      # Classpath dependencies.
      attr_accessor :classpath

      # Additional sourcepaths that are not part of the documented files.
      attr_accessor :sourcepath

      # Returns the documentation tool options.
      attr_reader :options

      attr_reader :project # :nodoc:

      def initialize(*args) #:nodoc:
        super
        @options = {}
        @classpath = []
        @sourcepath = []
        @files = FileList[]
        enhance do |task|
          rm_rf target.to_s
          mkdir_p target.to_s

          engine.generate(source_files, File.expand_path(target.to_s),
            options.merge(:classpath => classpath, :sourcepath => sourcepath))

          touch target.to_s
        end
      end

      # :call-seq:
      #   into(path) => self
      #
      # Sets the target directory and returns self. This will also set the Javadoc task
      # as a prerequisite to a file task on the target directory.
      #
      # For example:
      #   package :zip, :classifier=>'docs', :include=>doc.target
      def into(path)
        @target = file(path.to_s).enhance([self]) unless @target && @target.to_s == path.to_s
        self
      end

      # :call-seq:
      #   include(*files) => self
      #
      # Includes additional source files and directories when generating the documentation
      # and returns self. When specifying a directory, includes all source files in that directory.
      def include(*files)
        files.each do |file|
          if file.respond_to? :to_ary
            include(*file.to_ary)
          else
            @files.include *files.flatten.compact.collect { |f| File.expand_path(f.to_s) }
          end
        end
        self
      end

      # :call-seq:
      #   exclude(*files) => self
      #
      # Excludes source files and directories from generating the documentation.
      def exclude(*files)
        @files.exclude *files.collect{|f|File.expand_path(f)}
        self
      end

      # :call-seq:
      #   with(*artifacts) => self
      #
      # Adds files and artifacts as classpath dependencies, and returns self.
      def with(*specs)
        @classpath |= Buildr.artifacts(specs.flatten).uniq
        self
      end

      # :call-seq:
      #   using(options) => self
      #
      # Sets the documentation tool options from a hash and returns self.
      #
      # For example:
      #   doc.using :windowtitle=>'My application'
      #   doc.using :vscaladoc
      def using(*args)
        args.pop.each { |key, value| @options[key.to_sym] = value } if Hash === args.last

        until args.empty?
          new_engine = Doc.select_by_name(args.pop)
          @engine = new_engine.new(project) unless new_engine.nil?
        end

        self
      end

      def engine
        @engine ||= guess_engine
      end

      # :call-seq:
      #   engine?(clazz) => boolean
      #
      # Check if the underlying engine is an instance of the given class
      def engine?(clazz)
        begin
          @engine ||= guess_engine if project.compile.language
        rescue
          return false
        end
        @engine.is_a?(clazz) if @engine
      end

      # :call-seq:
      #   from(*sources) => self
      #
      # Includes files, directories and projects in the documentation and returns self.
      #
      # You can call this method with source files and directories containing source files
      # to include these files in the documentation, similar to #include. You can also call
      # this method with projects. When called with a project, it includes all the source files compiled
      # by that project and classpath dependencies used when compiling.
      #
      # For example:
      #   doc.from projects('myapp:foo', 'myapp:bar')
      def from(*sources)
        sources.flatten.each do |source|
          case source
          when Project
            self.enhance source.prerequisites
            self.include source.compile.sources
            self.with source.compile.dependencies
          when Rake::Task, String
            self.include source
          else
            fail "Don't know how to generate documentation from #{source || 'nil'}"
          end
        end
        self
      end

      def prerequisites #:nodoc:
        super + @files + classpath + sourcepath
      end

      def source_files #:nodoc:
        @source_files ||= @files.map(&:to_s).map do |file|
          Array(engine.class.source_ext).map do |ext|
            File.directory?(file) ? FileList[File.join(file, "**/*.#{ext}")] : File.expand_path(file)
          end
        end.flatten.reject { |file| @files.exclude?(file) }
      end

      def needed? #:nodoc:
        return false if source_files.empty?
        return true unless File.exist?(target.to_s)
        source_files.map { |src| File.stat(src.to_s).mtime }.max > File.stat(target.to_s).mtime
      end

    private

      def guess_engine
        doc_engine = Doc.select project.compile.language
        fail 'Unable to guess documentation provider for project.' unless doc_engine
        doc_engine.new project
      end

      def associate_with(project)
        @project ||= project
      end
    end


    first_time do
      desc 'Create the documentation for this project'
      Project.local_task :doc
    end

    before_define(:doc) do |project|
      DocTask.define_task('doc').tap do |doc|
        doc.send(:associate_with, project)
        doc.into project.path_to(:target, :doc)
      end
    end

    after_define(:doc) do |project|
      project.doc.from project
    end

    # :call-seq:
    #   doc(*sources) => JavadocTask
    #
    # This method returns the project's documentation task. It also accepts a list of source files,
    # directories and projects to include when generating the docs.
    #
    # By default the doc task uses all the source directories from compile.sources and generates
    # documentation in the target/doc directory. This method accepts sources and adds them by calling
    # Buildr::Doc::Base#from.
    #
    # For example, if you want to generate documentation for a given project that includes all source files
    # in two of its sub-projects:
    #   doc projects('myapp:foo', 'myapp:bar').using(:windowtitle=>'Docs for foo and bar')
    def doc(*sources, &block)
      task('doc').from(*sources).enhance &block
    end

    def javadoc(*sources, &block)
      warn 'The javadoc method is deprecated and will be removed in a future release.'
      doc(*sources, &block)
    end
  end

  class Project #:nodoc:
    include Doc
  end
end
