#
# Copyright:: Copyright (c) 2012 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# 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 'forwardable'

module Omnibus
  class Builder

    # Proxies method calls to either a Builder object or the Software that the
    # builder belongs to. Provides compatibility with our DSL where we never
    # yield objects to blocks and hopefully hides some of the confusion that
    # can arise from instance_eval.
    class DSLProxy
      extend Forwardable

      # @todo def_delegators :@builder, :patch, :command, :ruby, ...

      def_delegator :@builder, :patch
      def_delegator :@builder, :command
      def_delegator :@builder, :ruby
      def_delegator :@builder, :gem
      def_delegator :@builder, :bundle
      def_delegator :@builder, :rake
      def_delegator :@builder, :block
      def_delegator :@builder, :name

      def initialize(builder, software)
        @builder, @software = builder, software
      end

      def eval_block(&block)
        instance_eval(&block)
      end

      def respond_to?(method)
        super || @software.respond_to?(method)
      end

      def methods
        super | @software.methods
      end

      def method_missing(method_name, *args, &block)
        if @software.respond_to?(method_name)
          @software.send(method_name, *args, &block)
        else
          super
        end
      end

    end


    # @todo code duplication with {Fetcher::ErrorReporter}
    class ErrorReporter

      # @todo fetcher isn't even used
      def initialize(error, fetcher)
        @error, @fetcher = error, fetcher
      end

      # @todo this isn't necessary
      def e
        @error
      end

      def explain(why)
        $stderr.puts "* " * 40
        $stderr.puts why
        $stderr.puts "Exception:"
        $stderr.puts indent("#{e.class}: #{e.message.strip}", 2)
        Array(e.backtrace).each {|l| $stderr.puts indent(l, 4) }
        $stderr.puts "* " * 40
      end

      private

      def indent(string, n)
        string.split("\n").map {|l| " ".rjust(n) << l }.join("\n")
      end

    end

    # @todo Look at using Bundler.with_clean_env{ ... } instead
    BUNDLER_BUSTER = {  "RUBYOPT"         => nil,
                        "BUNDLE_BIN_PATH" => nil,
                        "BUNDLE_GEMFILE"  => nil,
                        "GEM_PATH"        => nil,
                        "GEM_HOME"        => nil }

    attr_reader :build_commands

    def initialize(software, &block)
      @software = software
      @build_commands = []
      @dsl_proxy = DSLProxy.new(self, software)
      @dsl_proxy.eval_block(&block) if block_given?
    end

    def name
      @software.name
    end

    def command(*args)
      @build_commands << args
    end

    def patch(*args)
      args = args.dup.pop

      # we'll search for a patch file in the project root AND
      # the omnibus-software gem
      candidate_roots = [Omnibus.project_root]
      candidate_roots << Omnibus.omnibus_software_root if Omnibus.omnibus_software_root

      candidate_paths = candidate_roots.map do |root|
        File.expand_path("#{root}/config/patches/#{name}/#{args[:source]}")
      end

      source = candidate_paths.find{|path| File.exists?(path) }

      plevel = args[:plevel] || 1
      if args[:target] 
        target = File.expand_path("#{project_dir}/#{args[:target]}")
        @build_commands << 
         "cat #{source} | patch -p#{plevel} #{target}"
      else
        @build_commands << 
         "patch -d #{project_dir} -p#{plevel} -i #{source}"
      end
    end

    # @todo all these ruby commands (ruby, gem, bundle, rake) could
    #   all be collapsed into a single underlying implementation, since
    #   they all just differ on the executable being called
    def ruby(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/ruby", *args))
    end

    def gem(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/gem", *args))
    end

    def bundle(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/bundle", *args))
    end

    def rake(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/rake", *args))
    end

    def block(&rb_block)
      @build_commands << rb_block
    end

    def project_dir
      @software.project_dir
    end

    def install_dir
      @software.install_dir
    end

    def log(message)
      puts "[builder:#{name}] #{message}"
    end

    def build
      log "building #{name}"
      time_it("#{name} build") do
        @build_commands.each do |cmd|
          execute(cmd)
        end
      end
    end

    def execute(cmd)
      case cmd
      when Proc
        execute_proc(cmd)
      else
        execute_sh(cmd)
      end
    end

    private

    def execute_proc(cmd)
      cmd.call
    rescue Exception => e
      # In Ruby 1.9, Procs have a #source_location method with file/line info.
      # Too bad we can't use it :(
      ErrorReporter.new(e, self).explain("Failed to build #{name} while running ruby block build step")
      raise
    end

    def execute_sh(cmd)
      retries ||= 0
      shell = nil
      cmd_args = Array(cmd)
      options = {
        :cwd => project_dir,
        :timeout => 5400
      }
      options[:live_stream] = STDOUT if ENV['DEBUG']
      if cmd_args.last.is_a? Hash
        cmd_options = cmd_args.last
        cmd_args[cmd_args.size - 1] = options.merge(cmd_options)
      else
        cmd_args << options
      end

      cmd_string = cmd_args[0..-2].join(' ')
      cmd_opts_for_display = to_kv_str(cmd_args.last)

      log "Executing: `#{cmd_string}` with #{cmd_opts_for_display}"

      shell = Mixlib::ShellOut.new(*cmd)
      shell.environment["HOME"] = "/tmp" unless ENV["HOME"]

      cmd_name = cmd_string.split(/\s+/).first
      time_it("#{cmd_name} command") do
        shell.run_command
        shell.error!
      end
    rescue Exception => e
      # Getting lots of errors from github, particularly with erlang/rebar
      # projects fetching tons of deps via git all the time. This isn't a
      # particularly elegant way to solve that problem. But it should work.
      if retries >= 3
        ErrorReporter.new(e, self).explain("Failed to build #{name} while running `#{cmd_string}` with #{cmd_opts_for_display}")
        raise
      else
        time_to_sleep = 5 * (2 ** retries)
        retries +=1
        log "Failed to execute cmd #{cmd} #{retries} time(s). Retrying in #{time_to_sleep}s."
        sleep(time_to_sleep)
        retry
      end
    end

    def prepend_cmd(str, *cmd_args)
      if cmd_args.size == 1
        # command as a string, no opts
        "#{str} #{cmd_args.first}"
      elsif cmd_args.size == 2 && cmd_args.last.is_a?(Hash)
        # command as a string w/ opts
        ["#{str} #{cmd_args.first}", cmd_args.last]
      elsif cmd_args.size == 0
        raise ArgumentError, "I don't even"
      else
        # cmd given as argv array
        cmd_args.dup.unshift(str)
      end
    end

    def bundle_bust(*cmd_args)
      if cmd_args.last.is_a?(Hash)
        cmd_args = cmd_args.dup
        cmd_opts = cmd_args.pop.dup
        cmd_opts[:env] = cmd_opts[:env] ? BUNDLER_BUSTER.merge(cmd_opts[:env]) : BUNDLER_BUSTER
        cmd_args << cmd_opts
      else
        cmd_args << {:env => BUNDLER_BUSTER}
      end
    end


    def time_it(what)
      start = Time.now
      yield
    rescue Exception
      elapsed = Time.now - start
      log "#{what} failed, #{elapsed.to_f}s"
      raise
    else
      elapsed = Time.now - start
      log "#{what} succeeded, #{elapsed.to_f}s"
    end

    # Convert a hash to a string in the form `key=value`. It should work with
    # whatever input is given but is designed to make the options to ShellOut
    # look nice.
    def to_kv_str(hash, join_str=",")
      hash.inject([]) do |kv_pair_strs, (k,v)|
        val_str = case v
        when Hash
          %Q["#{to_kv_str(v, " ")}"]
        else
          v.to_s
        end
        kv_pair_strs << "#{k}=#{val_str}"
      end.join(join_str)
    end

  end

  # @todo What's the point of this class?  Can we not just detect that
  #   there are no commands in {Omnibus::Builder#build} and output the
  #   appropriate message?  Seems like a lot of extra ceremony.
  class NullBuilder < Builder

    def build
      log "Nothing to build for #{name}"
    end

  end

end