#
# Copyright 2012-2014 Chef Software, Inc.
#
# Licensed 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.
#

require 'pathname'

require 'omnibus/exceptions'
require 'omnibus/version'

module Omnibus
  #
  # The path to the default configuration file.
  #
  # @return [String]
  #
  DEFAULT_CONFIG = 'omnibus.rb'

  autoload :Artifact,         'omnibus/artifact'
  autoload :Builder,          'omnibus/builder'
  autoload :BuildVersion,     'omnibus/build_version'
  autoload :BuildVersionDSL,  'omnibus/build_version_dsl'
  autoload :Cleaner,          'omnibus/cleaner'
  autoload :Config,           'omnibus/config'
  autoload :Error,            'omnibus/exceptions'
  autoload :Fetcher,          'omnibus/fetcher'
  autoload :Generator,        'omnibus/generator'
  autoload :HealthCheck,      'omnibus/health_check'
  autoload :InstallPathCache, 'omnibus/install_path_cache'
  autoload :Library,          'omnibus/library'
  autoload :Logger,           'omnibus/logger'
  autoload :Logging,          'omnibus/logging'
  autoload :NullBuilder,      'omnibus/null_builder'
  autoload :Ohai,             'omnibus/ohai'
  autoload :Overrides,        'omnibus/overrides'
  autoload :PackageRelease,   'omnibus/package_release'
  autoload :Project,          'omnibus/project'
  autoload :Reports,          'omnibus/reports'
  autoload :S3Cache,          'omnibus/s3_cache'
  autoload :Software,         'omnibus/software'
  autoload :SoftwareS3URLs,   'omnibus/software_s3_urls'
  autoload :Util,             'omnibus/util'

  # @todo Remove this in the next major release
  autoload :OHAI, 'omnibus/ohai'

  # @todo Refactor these under a +Fetcher module
  autoload :GitFetcher,     'omnibus/fetchers/git_fetcher'
  autoload :NetFetcher,     'omnibus/fetchers/net_fetcher'
  autoload :PathFetcher,    'omnibus/fetchers/path_fetcher'
  autoload :S3CacheFetcher, 'omnibus/fetchers/s3_cache_fetcher'

  module Command
    autoload :Base,    'omnibus/cli/base'
    autoload :Cache,   'omnibus/cli/cache'
    autoload :Release, 'omnibus/cli/release'
  end

  module Packager
    autoload :Base,       'omnibus/packagers/base'
    autoload :MacDmg,     'omnibus/packagers/mac_dmg'
    autoload :MacPkg,     'omnibus/packagers/mac_pkg'
    autoload :WindowsMsi, 'omnibus/packagers/windows_msi'
  end

  class << self
    #
    # Reset the current Omnibus configuration. This is primary an internal API
    # used in testing, but it can also be useful when Omnibus is used as a
    # library.
    #
    # Note - this persists the +Logger+ object by default.
    #
    # @param [true, false] include_logger
    #   whether the logger object should be cleared as well
    #
    # @return [void]
    #
    def reset!(include_logger = false)
      instance_variables.each do |instance_variable|
        unless include_logger
          next if instance_variable == :@logger
        end

        remove_instance_variable(instance_variable)
      end

      Config.reset!
    end

    #
    # The logger for this Omnibus instance.
    #
    # @example
    #   Omnibus.logger.debug { 'This is a message!' }
    #
    # @return [Logger]
    #
    def logger
      @logger ||= Logger.new
    end

    def logger=(logger)
      @logger = logger
    end

    def ui
      @ui ||= Thor::Base.shell.new
    end

    # Configure Omnibus.
    #
    # After this has been called, the {Omnibus::Config} object is
    # available as `Omnibus.config`.
    #
    # @return [void]
    #
    # @deprecated Use {#load_configuration} if you need to process a
    #   config file, followed by {#process_configuration} to act upon it.
    def configure
      load_configuration
      process_configuration
    end

    # Convenience method for access to the Omnibus::Config object.
    # Provided for backward compatibility.
    #
    # @ return [Omnibus::Config]
    #
    # @deprecated Just refer to {Omnibus::Config} directly.
    def config
      Config
    end

    # Load in an Omnibus configuration file.  Values will be merged with
    # and override the defaults defined in {Omnibus::Config}.
    #
    # @param file [String] path to a configuration file to load
    #
    # @return [void]
    def load_configuration(file = nil)
      if file
        Config.from_file(file)
      end
    end

    # Processes the configuration to construct the dependency tree of
    # projects and software.
    #
    # @return [void]
    def process_configuration
      Config.validate
      process_dsl_files
    end

    # All {Omnibus::Project} instances that have been created.
    #
    # @return [Array<Omnibus::Project>]
    def projects
      @projects ||= []
    end

    # Names of all the {Omnibus::Project} instances that have been created.
    #
    # @return [Array<String>]
    def project_names
      projects.map { |p| p.name }
    end

    # Load the {Omnibus::Project} instance with the given name.
    #
    # @param name [String]
    # @return {Omnibus::Project}
    def project(name)
      projects.find { |p| p.name == name }
    end

    # The absolute path to the Omnibus project/repository directory.
    #
    # @return [String]
    def project_root
      Config.project_root
    end

    # The source root is the path to the root directory of the `omnibus` gem.
    #
    # @return [Pathname]
    def source_root
      @source_root ||= Pathname.new(File.expand_path('../..', __FILE__))
    end

    # The source root is the path to the root directory of the `omnibus-software`
    # gem.
    #
    # @return [Pathname]
    def omnibus_software_root
      @omnibus_software_root ||= begin
        if (spec = Gem::Specification.find_all_by_name(Config.software_gem).first)
          Pathname.new(spec.gem_dir)
        else
          nil
        end
      end
    end

    # Return paths to all configured {Omnibus::Project} DSL files.
    #
    # @return [Array<String>]
    def project_files
      ruby_files(File.join(project_root, Config.project_dir))
    end

    # Return paths to all configured {Omnibus::Software} DSL files.
    #
    # @return [Array<String>]
    def software_files
      ruby_files(File.join(project_root, Config.software_dir))
    end

    # Return directories to search for {Omnibus::Software} DSL files.
    #
    # @return [Array<String>]
    def software_dirs
      @software_dirs ||= begin
        software_dirs = [File.join(project_root, Config.software_dir)]
        software_dirs << File.join(omnibus_software_root, 'config', 'software') if omnibus_software_root
        software_dirs
      end
    end

    # Backward compat alias
    #
    # @todo Remve this in the next major release (4.0)
    #
    # @see (Omnibus.project_root)
    def root
      Omnibus.logger.deprecated('Omnibus') do
        'Omnibus.root. Please use Omnibus.project_root instead.'
      end

      project_root
    end

    # Processes all configured {Omnibus::Project} and
    # {Omnibus::Software} DSL files.
    #
    # @return [void]
    def process_dsl_files
      # Do projects first
      expand_projects

      # Then do software
      final_software_map = prefer_local_software(omnibus_software_files, software_files)

      overrides = Config.override_file ? Omnibus::Overrides.overrides : {}

      expand_software(overrides, final_software_map)
    end

    private

    # Generates {Omnibus::Project}s for each project DSL file in
    # `project_specs`.  All projects are then accessible at
    # {Omnibus#projects}
    #
    # @return [void]
    #
    # @see Omnibus::Project
    def expand_projects
      project_files.each do |spec|
        Omnibus.projects << Omnibus::Project.load(spec)
      end
    end

    # Generate {Omnibus::Software} objects for all software DSL files in
    # `software_specs`.
    #
    # @param overrides [Hash] a hash of version override information.
    # @param software_files [Array<String>]
    # @return [void]
    #
    # @see Omnibus::Overrides#overrides
    def expand_software(overrides, software_map)
      unless overrides.is_a? Hash
        raise ArgumentError, "Overrides argument must be a hash!  You passed #{overrides.inspect}."
      end

      Omnibus.projects.each do |project|
        project.dependencies.each do |dependency|
          recursively_load_dependency(dependency, project, overrides, software_map)
        end
      end
    end

    # Return a list of all the Ruby files (i.e., those with an "rb"
    # extension) in the given directory
    #
    # @param dir [String]
    # @return [Array<String>]
    def ruby_files(dir)
      Dir.glob("#{dir}/*.rb")
    end

    # Retrieve the fully-qualified paths to every software definition
    # file bundled in the {https://github.com/opscode/omnibus-software omnibus-software} gem.
    #
    # @return [Array<String>] the list of paths. Will be empty if the
    #   `omnibus-software` gem is not in the gem path.
    def omnibus_software_files
      if omnibus_software_root
        Dir.glob(File.join(omnibus_software_root, 'config', 'software', '*.rb'))
      else
        []
      end
    end

    # Given a list of software definitions from `omnibus-software` itself, and a
    # list of software files local to the current project, create a
    # single list of software definitions.  If the software was defined
    # in both sets, the locally-defined one ends up in the final list.
    #
    # The base name of the software file determines what software it
    # defines.
    #
    # @param omnibus_files [Array<String>]
    # @param local_files [Array<String>]
    # @return [Array<String>]
    def prefer_local_software(omnibus_files, local_files)
      base = software_map(omnibus_files)
      local = software_map(local_files)
      base.merge(local)
    end

    # Given a list of file paths, create a map of the basename (without
    # extension) to the complete path.
    #
    # @param files [Array<String>]
    # @return [Hash<String, String>]
    def software_map(files)
      files.each_with_object({}) do |file, collection|
        software_name = File.basename(file, '.*')
        collection[software_name] = file
      end
    end

    # Loads a project's dependency recursively, ensuring all transitive dependencies
    # are also loaded.
    #
    # @param dependency_name [String]
    # @param project [Omnibus::Project]
    # @param overrides [Hash] a hash of version override information.
    # @param software_map [Hash<String, String>]
    #
    # @return [void]
    def recursively_load_dependency(dependency_name, project, overrides, software_map)
      dep_file = software_map[dependency_name]

      unless dep_file
        raise MissingProjectDependency.new(dependency_name, software_dirs)
      end

      dep_software = Omnibus::Software.load(dep_file, project, overrides)

      # load any transitive deps for the component into the library also
      dep_software.dependencies.each do |dep|
        recursively_load_dependency(dep, project, overrides, software_map)
      end

      project.library.component_added(dep_software)
    end
  end
end

# Sugars must be loaded after everything else has been registered
require 'omnibus/sugar'