#
# Copyright 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 'fileutils'
require 'forwardable'
require 'erb'

module Omnibus
  class Packager::Base
    include Logging
    include Util

    extend Forwardable

    # The Omnibus::Project instance that we're packaging.
    attr_reader :project

    # The commands/steps to setup the file system.
    def self.setup(&block)
      if block_given?
        @setup = block
      else
        @setup
      end
    end

    # The commands/steps to validate any arguments.
    def self.validate(&block)
      if block_given?
        @validate = block
      else
        @validate
      end
    end

    # The commands/steps to build the package.
    def self.build(&block)
      if block_given?
        @build = block
      else
        @build || raise(AbstractMethod.new("#{self.class.name}.build"))
      end
    end

    # The commands/steps to cleanup any temporary files/directories.
    def self.clean(&block)
      if block_given?
        @clean = block
      else
        @clean
      end
    end

    # Create a new packager object.
    #
    # @param [Project] project
    def initialize(project)
      @project = project
    end

    # Hacky way to get around the fact that older versions of Omnibus used to
    # delegate magical methods to this DSL.
    #
    # @deprecated Use +project+ methods instead
    def method_missing(m, *args, &block)
      if project.respond_to?(m)
        log.deprecated("#{log_key}##{m}") { "#{m}. Please use project.#{m} instead." }
        project.send(m, *args, &block)
      else
        super
      end
    end

    # @deprecated
    def version
      log.deprecated("#{log_key}#version") { 'version. Please use project.build_version instead.' }
      project.build_version
    end

    #
    # Generation methods
    # ------------------------------

    # Create a directory at the given +path+.
    #
    # @param [String] path
    def create_directory(path)
      FileUtils.mkdir_p(path)
      path
    end

    # Remove the directory at the given +path+.
    #
    # @param [String] path
    def remove_directory(path)
      FileUtils.rm_rf(path)
    end

    # Purge the directory of all contents.
    #
    # @param [String] path
    def purge_directory(path)
      remove_directory(path)
      create_directory(path)
    end

    # Copy the +source+ file to the +destination+.
    #
    # @param [String] source
    # @param [String] destination
    def copy_file(source, destination)
      FileUtils.cp(source, destination)
      destination
    end

    # Remove the file at the given path.
    #
    # @param [String] pah
    def remove_file(path)
      FileUtils.rm_f(path)
    end

    # Copy the +source+ directory to the +destination+.
    #
    # @param [String] source
    # @param [String] destination
    def copy_directory(source, destination)
      FileUtils.cp_r(Dir["#{source}/*"], destination)
    end

    # Execute the command using shellout!
    #
    # @param [String] command
    def execute(command, options = {})
      options.merge! timeout: 3600, cwd: staging_dir
      shellout!(command,  options)
    end

    # Render an erb template at +source_path+ to +destination_path+ if
    # given. Otherwise template is rendered next to +source_path+
    # by removing the 'erb' extension of the template
    #
    # @param [String] source_path
    # @param [String] destination_path
    def render_template(source_path, destination_path = nil)
      return unless source_path.end_with?('.erb')

      destination_path = source_path.chomp('.erb') if destination_path.nil?

      File.open(source_path) do |file|
        erb = ERB.new(file.read)
        File.open(destination_path, 'w') do |out|
          out.write(erb.result(binding))
        end

        remove_file(source_path)
      end
    end

    #
    # Validations
    # ------------------------------

    # Validate the presence of a file.
    #
    # @param [String] path
    def assert_presence!(path)
      raise MissingAsset.new(path) unless File.exist?(path)
    end

    # Execute this packager by running the following phases in order:
    #
    #   - setup
    #   - validate
    #   - build
    #   - clean
    #
    def run!
      instance_eval(&self.class.setup)    if self.class.setup
      instance_eval(&self.class.validate) if self.class.validate
      instance_eval(&self.class.build)    if self.class.build
      instance_eval(&self.class.clean)    if self.class.clean
    end

    # The ending name of this package on disk. +Omnibus::Project+ uses this to
    # generate metadata about the package after it is built.
    #
    # @return [String]
    def package_name
      raise AbstractMethod.new("#{self.class.name}#package_name")
    end

    private

    # The path to the directory where we can throw staged files.
    #
    # @return [String]
    def staging_dir
      File.expand_path("#{Config.package_tmp}/#{underscore_name}")
    end

    # The path to the directory where the packager resources are
    # copied into from source.
    #
    # @return [String]
    def staging_resources_path
      File.expand_path("#{staging_dir}/Resources")
    end

    # The path to a resource in staging directory.
    #
    # @param [String]
    #   the name or path of the resource
    # @return [String]
    def resource(path)
      File.expand_path(File.join(staging_resources_path, path))
    end

    # The path to all the resources on the packager source.
    # Uses `resources_path` if specified in the project otherwise
    # uses the project root set in global config.
    #
    # @return [String]
    def resources_path
      base_path = if project.resources_path
                    project.resources_path
                  else
                    project.files_path
                  end

      File.expand_path(File.join(base_path, underscore_name, 'Resources'))
    end

    # The underscored equivalent of this class. This is mostly used by file
    # paths.
    #
    # @return [String]
    def underscore_name
      @underscore_name ||= self.class.name
        .split('::')
        .last
        .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
        .gsub(/([a-z\d])([A-Z])/, '\1_\2')
        .tr('-', '_')
        .downcase
    end
  end
end