# = TITLE:
#
#   Project
#
# = COPYING:
#
#   Copyright (c) 2007 Tiger Ops
#
#   This file is part of the ProUtils' Ratch program.
#
#   Ratch is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   Ratch is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with Ratch.  If not, see <http://www.gnu.org/licenses/>.

require 'facets/dir/multiglob'
require 'reap/iobject'

module Reap

class Project

  # = Project Metadata
  #
  # The Project Metadata class stores project information. This information includes
  # the general information about a project, such as title, description, homepage, etc.
  # which is essentially static. Once set, it will probably will never change.
  # The class also contains default settings for packaging; information that is
  # usually static, but may vary for a partciular package platform or format.
  #
  # When utilizing this class it is important not confuse oneself thinking that a
  # project is not a project just becuase it is a sub-project. A sub-project is a
  # project, it just happens to belong to a master project.

  class Metadata < InfoObject

    PROJECT_FILE = '{,meta/}{project}{info,}{.yaml,.yml,}'
    VERSION_FILE = '{,meta/}{version}{.text,.txt,}'

    #

    def self.read(location)
      metadata = read_project(location)
      versdata = read_version(location)

      data = {}
      data.update(metadata)
      data.update(versdata)

      new(location, data)
    end

    # Parse release file for release information.

    def self.read_project(location)
      glob = File.join(location, PROJECT_FILE)
      file = Dir.glob(glob, File::FNM_CASEFOLD).first
      if file
        YAML::load(File.open(file))
      else
        raise LoadError, "project file not found"
      end
    end

    # Parse version file for current release information.

    def self.read_version(location)
      glob = File.join(location, VERSION_FILE)
      file = Dir.glob(glob, File::FNM_CASEFOLD).first
      if file
        str = File.read(file)
        version, status, date, *null = *str.strip.split(/\s+/)
        date = Date.parse(date).strftime("%Y-%m-%d")
        data = {'version' => version, 'status' => status, 'date' => date}
      else
        data = {}
      end
      return data
    end

    # New Project.

    def initialize(location, data={})
      @location = location
      super(data)
    end

    # Location is needed to calculate some conventional defaults.

    attr_accessor :location


    # General
    #------------------------------------------------------------------------

    # The title of the project (free-form, defaults to name).
    attr_accessor :title do
      @title || (
        name if respond_to?(:name)
      )
    end

    # Subtitle is limited to 60 characters.
    attr_accessor :subtitle do
      @subtitle.to_s[0..59]
    end

    # Brief one-line description of the package (Max 80 chars.)
    attr_accessor :summary, :brief do
      if @summary
        @summary.to_s[0..79]
      else
        i = @description.index('.') || 79
        i = 79 if i > 79
        @description[0..i]
      end
    end

    # More detailed description of the package.
    attr_accessor :description, :synopsis

    # "Unix" name of this project.
    attr_accessor :project, :name

    # Overrides Project#name.
    # TODO: Fit release name into name or package_name (?)
    #def name
    #  @name ||= release.name
    #end

    # If this is a sub-project, then +master+ is the
    # "Unix" name of the master project to which this
    # sub-project belongs.
    attr_accessor :master

    # The date the project was started.
    attr_accessor :created

    # Copyright notice.
    attr_accessor :copyright do
      @copyright || "Copyright (c) #{Time.now.strftime('%Y')} #{author}"
    end

    # Distribution License.
    attr_accessor :license do
      @license || 'GPLv3'
    end

    # Slogan or "trademark" phrase.
    attr_accessor :slogan

    # General one-word software category.
    attr_accessor :category

    # Author(s) of this project.
    # (Usually in "name <email>" format.)
    attr_accessor :author

    # Contact(s) (defaults to authors).
    # TODO Move to Variants?
    attr_accessor :contact do
      @contact || author
    end

    # Gerneral email address.
    attr_accessor :email do
      if md = /<(.*?)>/.match(contact)
        md[1]
      else
        "ruby-talk@ruby-lang.org"
      end
    end

    # Official domain associated with this package.
    attr_accessor :domain

    # Project's homepage.
    attr_accessor :homepage, :website

    # Project's development site.
    attr_accessor :development, :devsite

    # Internet address(es) to online documentation.
    attr_accessor :documentation, :docs

    # Internet address(es) to downloadable packages.
    attr_accessor :download

    # Internet address for project wiki.
    attr_accessor :wiki

    # Project's mailing list.
    attr_accessor :userlist, :mailinglist, :list

    # Developer's mailing list.
    attr_accessor :devlist do
      @devlist || @userlist
    end

    # Returns a standard taguri id for the library and release.
    def project_taguri
      "tag:#{name}.#{domain},#{created}"  # or released?
    end


    # Version
    #------------------------------------------------------------------------

    # Version number (eg. '1.0.0').
    attr_accessor :version

    # Current version code name.
    attr_accessor :codename

    # Build number can br set to an arbitrar number, or if set to true,
    # it will defaults to a number based on current date-time.
    attr_accessor :buildno do
      @buildno = Time.now.strftime("%y%m%d%H%M") if TrueClass === @buildno
      @buildno
    end

    # Status of this release: alpha, beta, RC1, etc.
    attr_accessor :status do
      @status || 'alpha'
    end

    # Date of release.
    attr_accessor :date, :released do
      @date
    end


    # Content Classification
    #------------------------------------------------------------------------

    # Files in this package that are executables.
    # These files must in the packages bin/ directory.
    # If left blank all bin/ files are included.

    attr_accessor :executable, :executables do
      return [@executable].flatten.compact if @executable
      exes = []
      dir = File.join(location, 'bin')
      if File.directory?(dir)
        Dir.chdir(dir) do
          exes = Dir.glob('*')
        end
      end
      @executable = exes
    end

    # Library files in this package that are *public*.
    # This is akin to load_path but specifies specific files
    # that can be loaded from the outside --where as those
    # not listed are considerd *private*.
    #
    # NOTE: This is not enforced --and may never be. It
    # complicates library loading. Ie. how to distinguish public
    # loading from external loading. But it something that can be
    # consider more carfully in the future. For now it can serve
    # as an optional reference.
    attr_accessor :library, :libraries do
      [@library || 'lib/**/*'].flatten
    end

    # Location(s) of executables.
    attr_accessor :bin_path, :bin_paths, :binpath, :binpaths

    # Location(s) of libraries (used by Rolls).
    # In most cases this is something like:
    #
    #   'lib/myapp'
    #
    # It would be nice if this could just be lib/ as it would mean one less
    # layer in a project heirarchy. But RubyGems and traditional installers
    # could not handle this, so this isn't a reasonable course at this point.
    attr_accessor :lib_path, :lib_paths, :libpath, :libpaths do
      [@lib_path || "lib/#{name}"].flatten
    end

    # The traditional load path(s) (used by Ruby's own site loading and RubyGems).
    # The default is lib/, which is usually correct.
    attr_accessor :load_path, :load_paths, :loadpath, :loadpaths, :gem_path, :gem_paths, :gempath, :gempaths do
      [@load_path || "lib"].flatten
    end

    # This only applys to Rolls. It is the default file to load.
    # TODO: Think of a more descirptive name than 'default'.
    attr_accessor :default


    # Security
    #------------------------------------------------------------------------

    # Encryption digest type used.
    #   (md5, sha1, sha128, sha256, sha512).
    attr_accessor :digest do
      @digest || 'md5'
    end

    # Public key file associated with this library. This is useful
    # for security purposes especially remote loading. [pubkey.pem]
    attr_accessor :public_key do
      @public_key || 'pubkey.pem'
    end

    # Private key file associated with this library. This is useful
    # for security purposes especially remote loading. [_privkey.pem]
    attr_accessor :private_key
    #   @private_key  || '_privkey.pem'
    # end


    # Source Management
    #------------------------------------------------------------------------

    # Specify which verison control system is being used.
    # Sometimes this is autmatically detectable, but it
    # is better to specify it.

    # Specifices the type of revision control system used.
    #   darcs, svn, cvs, etc.
    # Will try to determine which version control system is being used.
    attr_accessor :scm do
      return @scm unless @scm.nil?
      @scm = if File.directory?('.svn')
        'svn'
      elsif File.directory?('_darcs')
        'darcs'
      else
        false
      end
    end

    # Files that are tracked under revision control.
    # Default is all less standard exceptions.
    # '+' and '-' prefixes can be used to augment the list
    # rather than fully override it.
    attr_accessor :track, :scm_files

    # Internet address to source code repository.
    # (http://, ftp://, etc.)
    attr_accessor :repository, :repo

    # Changelog file.
    attr_accessor :changelog

    # Manifest file. Defaults to 'MANIFEST'.
    # (I like to put it in meta/MANIFEST, personally.)
    attr_accessor :manifest do
      @manifest ||= 'MANIFEST'
    end


    # Dependencies
    #------------------------------------------------------------------------
    # Package inter-relationship data. Generally refered to as package
    # "dependencies", but also includes +recommendations+, +suggestions+,
    # +replacements+, +provisions+, and +build-dependencies+, as well
    # as a few other fields that set a package apart.
    #------------------------------------------------------------------------

    # What other packages *must* this package have in order to function.
    attr_accessor :dependency, :dependencies do
      @dependency || []
    end

    # What other packages *should* be used with this package.
    attr_accessor :recommend, :recommends, :recommendations do
      @recommend || []
    end

    # What other packages *could* be useful with this package.
    attr_accessor :suggest, :suggests, :suggestions do
      @suggest || []
    end

    # What other packages does this package conflict.
    attr_accessor :conflict, :conflicts do
      @conflict || []
    end

    # What other packages does this package replace.
    attr_accessor :replace, :replaces, :replacements do
      @replace  || []
    end

    # What other package(s) does this package provide the same dependency fulfilment.
    # For example, a package 'bar-plus' might fulfill the same dependency criteria
    # as package 'bar', so 'bar-plus' is said to provide 'bar'.
    attr_accessor :provide, :provides, :provisions do
      @provide || []
    end

    # Abirtary information about what might be needed to use this package.
    # This is strictly information for the end-user to consider.
    #   Eg. "Fast graphics card"
    attr_accessor :requirement, :requirements do
      @requirement || []
    end

    # What packages does this package need to build? (eg. 'rake', 'reap', etc.)
    attr_accessor :build_dependency, :build_dependencies do
      @build_dependency  || []
    end

    # Abirtary information about what might be needed to build this package.
    attr_accessor :build_requirement, :build_requirements do
      @build_requirement || []
    end


    # Packaging
    #------------------------------------------------------------------------

    # Package name. This defaults to project name, but it may vary under
    # different package formats --deb vs. gem, for instance.
    attr_accessor :package do
      @package || name
      #@package || (
      #  name if respond_to?(:name)
      #)
    end

    # Platform. The default is nil, which is considered cross-platform.
    # This tends to only change for special builds.
    #
    # TODO: if current?

    attr_accessor :platform

    # Architecture(s) this package can be run on: any, i386, i686, ppc, etc.
    # This is strictly informational and is inteded to indicate the possiblities,
    # not the particular platform this package runs on.

    #attr_accessor :arch, :architecture do
    #  @arch || "any"
    #end

    # Script to run prior to build. No entry indicates no compilation.
    attr_accessor :compile

    # Packages that are intended to compile on install may need this. It is a list
    # of "extension scripts" which generate Makefiles for use in compilation.
    attr_accessor :extensions do
      [@extensions || Dir.glob(File.join(location, 'ext/**/extconf.rb'))].flatten.compact
    end

    #
    #validate "compile script not found" do
    #  compile ? File.file?(compile) : true
    #end

    # Generate documentation on installation?
    attr_accessor :document, :has_rdoc

    #
    #attr_accessor :package_directory, :package_store do
    #  @package_directory || 'pkg'
    #end

    # Package name is generally in the form of +name-version+, or
    # +name-version-platform+ if +platform+ is specified.
    #
    # TODO: Improve buildno support.

    def package_name
      if buildno
        buildno = Time.now.strftime("%H*60+%M")
        versnum = "#{version}.#{buildno}"
      else
        versnum = version
      end

      if platform 
        "#{package}-#{versnum}-#{platform}"
      else
        "#{package}-#{versnum}"
      end
    end

    alias_method :stage_name, :package_name


    # Distribution
    #------------------------------------------------------------------------

    # Files to be distributed in a package. Defaults to all files.
    # If an entry is a directory then all it's contents are also included.
    # This along with @exclude@ and @ignore@ is used to generate a manifest.
    attr_accessor :distribute, :include do
      [@distribute || '**/*'].flatten.compact
    end

    # File to exclude from package. This is usually more useful than
    # @distribute@, as it allows you to remove from all files, rather then
    # explicitly designate everything to be included. Exlcusions have priority
    # over dsitribute's inclusions. If an entry is a directory then all
    # it's contents are also excluded.
    attr_accessor :exclude do
      [@exclude].flatten.compact
    end

    # Files to generally ignore, mainly used for manifest collection. Ignore
    # has priority over @exclude@ and @distribute@.
    attr_accessor :ignore do
      @ignore || %w{ **/.svn _darcs .config .installed }
    end

    # Manifest file.
    #def manifest
    #  @manifest #||= Manifest.open
    #end

    # Set manifest file, which will load it.
    #def manifest=(file)
    #  @manifest = file
    #  @filelist = File.read_list(file) #Manifest.open(file)
    #  return file
    #end

    # List of file included in a package. This is generated using
    # @distribute@, @exlude@ and @ignore@.
    def filelist
      @filelist ||= collect_files(true)
    end

    # Validate that the files in the manifest actually exist.
    #def validate_manifest
    #  missing = []
    #  filelist.each do |f|
    #    missing << f unless File.exist?(f)
    #  end
    #  unless missing.empty?
    #    raise ValidationError, "manifest lists non-existent files -- " + missing.join(" ")
    #  end
    #end

    # Access to binding for use with ERB.
    def get_binding
      binding
    end

    private

      # Collect distribution files.

      def collect_files(with_dirs=false)
        files = []

        Dir.chdir(location) do
          files += Dir.multiglob_r(*distribute)
          files -= Dir.multiglob_r(*exclude)
          files -= Dir.multiglob_r(*ignore)
          files -= Dir.multiglob_r('pkg') #package_directory
        end

        # Do not include symlinks.
        files.reject!{ |f| FileTest.symlink?(f) }

        unless with_dirs
          files = files.select{ |f| !File.directory?(f) }
        end

        return files
      end


    # Validation
    #------------------------------------------------------------------------

    public

    #
    validate "version is required" do
      version
    end

    #
    validate "location is required" do
      location
    end

    #
    validate "executables do not exist" do
      exes = []
      dir = File.join(location, 'bin')
      if File.directory?(dir)
        Dir.chdir(dir) do
          exes = Dir.glob('*')
        end
      end
      (executables - exes).empty?
    end

  end

end

end