require 'ore/exceptions/project_not_found'
require 'ore/exceptions/invalid_metadata'
require 'ore/naming'
require 'ore/paths'
require 'ore/checks'
require 'ore/inferences'
require 'ore/settings'
require 'ore/document_file'

require 'pathname'
require 'yaml'
require 'find'
require 'fileutils'
require 'rubygems/specification'
require 'rubygems/builder'

module Ore
  #
  # Combines the metadata from the `gemspec.yml` file and the inferred
  # information from the project directory.
  #
  class Project

    include Naming
    include Paths
    include Checks
    include Inferences
    include Settings

    # The project metadata file
    @@metadata_file = 'gemspec.yml'

    # The root directory of the project
    attr_reader :root

    # The SCM which the project is currently under
    attr_reader :scm

    # The files of the project
    attr_reader :project_files

    # The fully-qualified namespace of the project
    attr_reader :namespace

    # The inferred namespace modules of the project
    attr_reader :namespace_modules

    # The directory contain the project code.
    attr_reader :namespace_dir

    # The name of the project
    attr_reader :name

    # The version of the project
    attr_reader :version

    # The project summary
    attr_reader :summary

    # The project description
    attr_reader :description

    # The licenses of the project
    attr_reader :licenses

    # The authors of the project
    attr_reader :authors

    # The homepage for the project
    attr_reader :homepage

    # The email contacts for the project
    attr_reader :emails

    # The build date for any project gems
    attr_reader :date

    # The parsed `.document` file
    attr_reader :document

    # The directories to search within the project when requiring files
    attr_reader :require_paths

    # The names of the executable scripts
    attr_reader :executables

    # The default executable
    attr_reader :default_executable

    # The documentation of the project
    attr_reader :documentation

    # Any extra files to include in the project documentation
    attr_reader :extra_doc_files

    # The files of the project
    attr_reader :files

    # The test files for the project
    attr_reader :test_files

    # Any external requirements needed by the project
    attr_reader :requirements

    # The version of Ruby required by the project
    attr_reader :required_ruby_version

    # The version of RubyGems required by the project
    attr_reader :required_rubygems_version

    # The dependencies of the project
    attr_reader :dependencies

    # The runtime-dependencies of the project
    attr_reader :runtime_dependencies

    # The development-dependencies of the project
    attr_reader :development_dependencies

    # The post-installation message
    attr_reader :post_install_message

    #
    # Creates a new {Project}.
    #
    # @param [String] root
    #   The root directory of the project.
    #
    def initialize(root=Dir.pwd)
      @root = Pathname.new(root).expand_path

      unless @root.directory?
        raise(ProjectNotFound,"#{@root} is not a directory")
      end

      infer_scm!
      infer_project_files!

      metadata_file = @root.join(@@metadata_file)

      unless metadata_file.file?
        raise(ProjectNotFound,"#{@root} does not contain #{@@metadata_file}")
      end

      metadata = YAML.load_file(metadata_file)

      unless metadata.kind_of?(Hash)
        raise(InvalidMetadata,"#{metadata_file} did not contain valid metadata")
      end

      if metadata['name']
        @name = metadata['name'].to_s
      else
        infer_name!
      end

      # infer the namespace from the project name
      infer_namespace!

      if metadata['version']
        set_version! metadata['version']
      else
        infer_version!
      end

      @summary = (metadata['summary'] || metadata['description'])
      @description = (metadata['description'] || metadata['summary'])

      @licenses = []

      if metadata['license']
        set_license!(metadata['license'])
      end

      @authors = []

      if metadata['authors']
        set_authors! metadata['authors']
      end

      @homepage = metadata['homepage']
      @emails = []
      
      if metadata['email']
        set_emails! metadata['email']
      end

      if metadata['date']
        set_date! metadata['date']
      else
        infer_date!
      end

      @document = DocumentFile.find(self)

      @require_paths = []

      if metadata['require_paths']
        set_require_paths! metadata['require_paths']
      else
        infer_require_paths!
      end

      @executables = []

      if metadata['executables']
        set_executables! metadata['executables']
      else
        infer_executables!
      end

      @default_executable = nil

      if metadata['default_executable']
        set_default_executable! metadata['default_executable']
      else
        infer_default_executable!
      end

      if metadata['has_yard']
        @documentation = :yard
      elsif metadata.has_key?('has_rdoc')
        @documentation = if metadata['has_rdoc']
                           :rdoc
                         end
      else
        infer_documentation!
      end

      @extra_doc_files = []

      if metadata['extra_doc_files']
        set_extra_doc_files! metadata['extra_doc_files']
      else
        infer_extra_doc_files!
      end

      @files = []

      if metadata['files']
        set_files! metadata['files']
      else
        infer_files!
      end

      @test_files = []

      if metadata['test_files']
        set_test_files! metadata['test_files']
      else
        infer_test_files!
      end

      if metadata['post_install_message']
        @post_install_message = metadata['post_install_message']
      end

      @requirements = []

      if metadata['requirements']
        set_requirements! metadata['requirements']
      end

      if metadata['required_ruby_version']
        @required_ruby_version = metadata['required_ruby_version']
      end

      if metadata['required_rubygems_version']
        @required_rubygems_version = metadata['required_rubygems_version']
      else
        infer_required_rubygems_version!
      end

      @dependencies = []

      if metadata['dependencies']
        set_dependencies! metadata['dependencies']
      end

      @runtime_dependencies = []

      if metadata['runtime_dependencies']
        set_runtime_dependencies! metadata['runtime_dependencies']
      end

      @development_dependencies = []

      if metadata['development_dependencies']
        set_development_dependencies! metadata['development_dependencies']
      end
    end

    #
    # Finds the project metadata file and creates a new {Project} object.
    #
    # @param [String] dir (Dir.pwd)
    #   The directory to start searching upward from.
    #
    # @return [Project]
    #   The found project.
    #
    # @raise [ProjectNotFound]
    #   No project metadata file could be found.
    #
    def self.find(dir=Dir.pwd)
      Pathname.new(dir).ascend do |root|
        return self.new(root) if root.join(@@metadata_file).file?
      end

      raise(ProjectNotFound,"could not find #{@@metadata_file}")
    end

    #
    # Executes code within the project.
    #
    # @param [String] sub_dir
    #   An optional sub-directory within the project to execute from.
    #
    # @yield []
    #   The given block will be called once the current working-directory
    #   has been switched. Once the block finishes executing, the current
    #   working-directory will be switched back.
    #
    # @see http://ruby-doc.org/core/classes/Dir.html#M002314
    #
    def within(sub_dir=nil,&block)
      dir = if sub_dir
              @root.join(sub_dir)
            else
              @root
            end

      Dir.chdir(dir,&block)
    end

    #
    # The primary license of the project.
    #
    # @return [String, nil]
    #   The primary license for the project.
    #
    def license
      @licenses.first
    end

    #
    # The primary email address of the project.
    #
    # @return [String, nil]
    #   The primary email address for the project.
    #
    # @since 0.1.3
    #
    def email
      @emails.first
    end

    #
    # Determines whether the project prefers using
    # [RVM](http://rvm.beginrescueend.com/).
    #
    # @return [Boolean]
    #   Specifies whether the project prefers being developed under RVM.
    #
    # @since 0.1.2
    #
    def rvm?
      file?('.rvmrc')
    end

    #
    # Determines whether the project uses Bundler.
    #
    # @return [Boolean]
    #   Specifies whether the project uses Bundler.
    #
    def bundler?
      file?('Gemfile')
    end

    #
    # Determines whether the project has been bundled using Bundler.
    #
    # @return [Boolean]
    #   Specifies whether the project has been bundled.
    #
    def bundled?
      file?('Gemfile.lock')
    end

    #
    # Determines if the project contains RDoc documentation.
    #
    # @return [Boolean]
    #   Specifies whether the project has RDoc documentation.
    #
    def has_rdoc
      @documentation == :rdoc
    end

    alias has_rdoc? has_rdoc

    #
    # Determines if the project contains YARD documentation.
    #
    # @return [Boolean]
    #   Specifies whether the project has YARD documentation.
    #
    def has_yard
      @documentation == :yard
    end

    alias has_yard? has_yard

    #
    # Populates a Gem Specification using the metadata of the project.
    #
    # @yield [gemspec]
    #   The given block will be passed the populated Gem Specification
    #   object.
    #
    # @yieldparam [Gem::Specification] gemspec
    #   The newly created Gem Specification.
    #
    # @return [Gem::Specification]
    #   The Gem Specification.
    #
    # @see http://rubygems.rubyforge.org/rdoc/Gem/Specification.html
    #
    def to_gemspec
      Gem::Specification.new do |gemspec|
        gemspec.name = @name.to_s
        gemspec.version = @version.to_s
        gemspec.summary = @summary.to_s
        gemspec.description = @description.to_s
        gemspec.licenses = @licenses
        gemspec.authors = @authors
        gemspec.homepage = @homepage
        gemspec.email = @emails
        gemspec.date = @date

        @require_paths.each do |path|
          unless gemspec.require_paths.include?(path)
            gemspec.require_paths << path
          end
        end

        gemspec.executables = @executables
        gemspec.default_executable = @default_executable

        # forcibly set the @has_rdoc ivar, as RubyGems 1.5.x disables the
        # #has_rdoc= writer method.
        if gemspec.instance_variable_defined?('@has_rdoc')
          case @documentation
          when :yard
            gemspec.instance_variable_set('@has_rdoc','yard')
          when :rdoc
            gemspec.instance_variable_set('@has_rdoc',true)
          when nil
            gemspec.instance_variable_set('@has_rdoc',false)
          end
        end

        gemspec.extra_rdoc_files = @extra_doc_files
        gemspec.files = @files
        gemspec.test_files = @test_files
        gemspec.post_install_message = @post_install_message

        gemspec.requirements = @requirements

        if gemspec.respond_to?(:required_ruby_version=)
          gemspec.required_ruby_version = @required_ruby_version
        end

        if gemspec.respond_to?(:required_rubygems_version=)
          gemspec.required_rubygems_version = @required_rubygems_version
        end

        @dependencies.each do |dep|
          gemspec.add_dependency(dep.name,*dep.versions)
        end

        if gemspec.respond_to?(:add_runtime_dependency)
          @runtime_dependencies.each do |dep|
            gemspec.add_runtime_dependency(dep.name,*dep.versions)
          end
        else
          @runtime_dependencies.each do |dep|
            gemspec.add_dependency(dep.name,*dep.versions)
          end
        end

        if gemspec.respond_to?(:add_development_dependency)
          @development_dependencies.each do |dep|
            gemspec.add_development_dependency(dep.name,*dep.versions)
          end
        else
          @development_dependencies.each do |dep|
            gemspec.add_dependency(dep.name,*dep.versions)
          end
        end

        # legacy information
        if gemspec.respond_to?(:rubyforge_project=)
          gemspec.rubyforge_project = gemspec.name
        end

        yield gemspec if block_given?
      end
    end

    #
    # Builds a gem for the project.
    #
    # @return [Pathname]
    #   The path to the built gem file within the `pkg/` directory.
    #
    def build!
      pkg_dir = @root.join(@@pkg_dir)
      FileUtils.mkdir_p(pkg_dir)

      gem_file = Gem::Builder.new(self.to_gemspec).build
      pkg_path = @root.join(pkg_file)

      FileUtils.mv(gem_file,pkg_path)
      return pkg_path
    end

    protected

    #
    # Prints multiple warning messages.
    #
    # @param [Array] messages
    #   The messages to print.
    #
    def warn(*messages)
      messages.each { |mesg| STDERR.puts("WARNING: #{mesg}") }
    end

    #
    # Adds a require-path to the project.
    #
    # @param [String] path
    #   A directory path relative to the project.
    #
    def add_require_path(path)
      check_directory(path) { |dir| @require_paths << dir }
    end

    #
    # Adds an executable to the project.
    #
    # @param [String] name
    #   The name of the executable.
    #
    def add_executable(name)
      path = File.join(@@bin_dir,name)

      check_executable(path) { |exe| @executables << exe }
    end

    #
    # Adds an extra documentation file to the project.
    #
    # @param [String] path
    #   The path to the file, relative to the project.
    #
    def add_extra_doc_file(path)
      check_file(path) { |file| @extra_doc_files << file }
    end

    #
    # Adds a file to the project.
    #
    # @param [String] path
    #   The path to the file, relative to the project.
    #
    def add_file(path)
      check_file(path) { |file| @files << file }
    end

    #
    # Adds a testing-file to the project.
    #
    # @param [String] path
    #   The path to the testing-file, relative to the project.
    #
    def add_test_file(path)
      check_file(path) { |file| @test_files << file }
    end

  end
end