#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2012-2015 Phusion
#
#  "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

# This file lists all the Phusion Passenger C++ library files and contains
# code for calculating how to compile and how to link them into executables.
# It's used by the build system (build/*.rb) and
# lib/phusion_passenger/standalone/runtime_installer.rb.

class CommonLibraryBuilder
  include Rake::DSL if defined?(Rake::DSL)

  attr_reader :all_components, :selected_components, :output_dir

  def initialize(&block)
    @all_components = {}
    @all_ordered_components = []
    @selected_components = {}
    @namespace = "common"
    if defined?(COMMON_OUTPUT_DIR)
      @output_dir = COMMON_OUTPUT_DIR + "libpassenger_common"
    else
      @output_dir = "."
    end
    instance_eval(&block) if block
  end

  def initialize_copy(other)
    [:all_components, :all_ordered_components, :selected_components, :namespace, :output_dir].each do |name|
      var_name = "@#{name}"
      instance_variable_set(var_name, other.instance_variable_get(var_name).dup)
    end
  end

  def define_component(object_name, options)
    options[:deps] ||= []
    @all_components[object_name] = options
    @all_ordered_components << object_name
    @selected_components[object_name] = options
  end

  def only(*selector)
    return dup.send(:only!, *selector)
  end

  def exclude(*selector)
    return dup.send(:exclude!, *selector)
  end

  def set_namespace(namespace)
    return dup.send(:set_namespace!, namespace)
  end

  def set_output_dir(dir)
    return dup.send(:set_output_dir!, dir)
  end

  def link_objects
    result = []

    selected_categories.each do |category|
      if category_complete?(category) && false
        # Feature disabled: we don't want to waste too much space when
        # packaging the runtime ('passenger package-runtime') so we
        # never generate static libraries.
        if aggregate_sources?
          result << "#{@output_dir}/#{category}.o"
        else
          result << "#{@output_dir}/#{category}.a"
        end
      else
        object_names = selected_objects_beloging_to_category(category)
        result.concat(object_filenames_for(object_names))
      end
    end

    return result
  end

  def link_objects_as_string
    return link_objects.join(' ')
  end

  def enable_optimizations!(lto = false)
    @default_optimization_level = "-O"
    if lto
      @default_extra_optimization_flags = "-flto"
    end
  end

  def define_tasks(extra_compiler_flags = nil)
    flags =  "-Iext -Iext/common #{LIBEV_CFLAGS} #{extra_compiler_flags} "
    cflags = (flags + EXTRA_CFLAGS).strip
    cxxflags = (flags + EXTRA_CXXFLAGS).strip

    group_all_components_by_category.each_pair do |category, object_names|
      define_category_tasks(category, object_names, cflags, cxxflags)
    end

    task("#{@namespace}:clean") do
      sh "rm -rf #{@output_dir}"
    end

    return self
  end

private
  def define_category_tasks(category, object_names, cflags, cxxflags)
    object_filenames = object_filenames_for(object_names)

    object_names.each do |object_name|
      options     = @all_components[object_name]
      source_file = "ext/common/#{options[:source]}"
      object_file = "#{@output_dir}/#{object_name}"

      file(object_file => dependencies_for(options)) do
        case options[:optimize]
        when :light
          optimization_level = "-O"
        when true, :heavy
          optimization_level = "-O2"
        when :very_heavy
          optimization_level = "-O3"
        when nil
          optimization_level = @default_optimization_level
        else
          raise "Unknown optimization level #{options[:optimize]}"
        end

        optimize = "#{optimization_level} #{@default_extra_optimization_flags}".strip

        if options[:strict_aliasing] == false # and not nil
          optimize = "#{optimize} -fno-strict-aliasing"
          # Disable link-time optimization so that we can no-strict-aliasing
          # works: http://stackoverflow.com/a/25765338/20816
          optimize.sub!(/-flto/, "")
        end

        ensure_directory_exists(File.dirname(object_file))
        # We put 'optimize' at the end of the command string so that it overrides
        # the default optimization level embedded in 'cflags'.
        if source_file =~ /\.c$/
          compile_c(source_file, "#{cflags} #{optimize} -o #{object_file}".strip)
        else
          compile_cxx(source_file, "#{cxxflags} #{optimize} -o #{object_file}".strip)
        end
      end
    end

    task "#{@namespace}:clean" do
      sh "rm -f #{object_filenames.join(' ')}"
    end

    if aggregate_sources?
      aggregate_source = "#{@output_dir}/#{category}.cpp"
      aggregate_object = "#{@output_dir}/#{category}.o"

      file(aggregate_object => dependencies_for(object_names)) do
        ensure_directory_exists(File.dirname(aggregate_source))
        ensure_directory_exists(File.dirname(aggregate_object))

        File.open(aggregate_source, "w") do |f|
          f.puts %q{
            #ifndef _GNU_SOURCE
              #define _GNU_SOURCE
            #endif
          }
          object_names.each do |object_name|
            options = @all_components[object_name]
            source_file = options[:source].sub(%r(^ext/common), '')
            f.puts "#include \"#{source_file}\""
          end
        end

        compile_cxx(aggregate_source, "#{flags} -o #{aggregate_object}")
      end

      task "#{@namespace}:clean" do
        sh "rm -f #{aggregate_source} #{aggregate_object}"
      end
    elsif false
      # Feature disabled: we don't want to waste too much space when
      # packaging the runtime ('passenger package-runtime') so we
      # never generate static libraries.
      library = "#{@output_dir}/#{category}.a"

      file(library => object_filenames) do
        create_static_library(library, object_filenames.join(' '))
      end

      task "#{@namespace}:clean" do
        sh "rm -f #{library}"
      end
    end
  end

  def set_namespace!(namespace)
    @namespace = namespace
    return self
  end

  def set_output_dir!(dir)
    @output_dir = dir
    return self
  end

  def only!(*selector)
    new_components = apply_selector(*selector)
    @selected_components = new_components
    return self
  end

  def exclude!(*selector)
    apply_selector(*selector).each_key do |object_name|
      @selected_components.delete(object_name)
    end
    return self
  end

  def apply_selector(*selector)
    result = {}
    selector = [selector].flatten
    selector.each do |condition|
      @selected_components.each do |object_name, options|
        if component_satisfies_condition?(object_name, options, condition)
          result[object_name] = options
        end
      end
    end
    return result
  end

  def component_satisfies_condition?(object_name, options, condition)
    case condition
    when Symbol
      return condition == :all || options[:category] == condition
    when String
      return object_name == condition
    else
      raise ArgumentError, "Invalid condition #{condition.inspect}"
    end
  end

  def ensure_directory_exists(dir)
    sh("mkdir -p #{dir}") if !File.directory?(dir)
  end

  def selected_categories
    categories = {}
    @selected_components.each_value do |options|
      categories[options[:category]] = true
    end
    return categories.keys
  end

  def category_complete?(category)
    expected = 0
    actual   = 0
    @all_components.each_value do |options|
      if options[:category] == category
        expected += 1
      end
    end
    @selected_components.each_value do |options|
      if options[:category] == category
        actual += 1
      end
    end
    return expected == actual
  end

  def selected_objects_beloging_to_category(category)
    result = []
    @selected_components.each_pair do |object_name, options|
      if options[:category] == category
        result << object_name
      end
    end
    return result
  end

  def dependencies_for(component_options_or_object_names)
    result = nil
    case component_options_or_object_names
    when Hash
      component_options = component_options_or_object_names
      result = ["ext/common/#{component_options[:source]}"]
      component_options[:deps].each do |dependency|
        result << "ext/common/#{dependency}"
      end
    when Array
      result = []
      object_names = component_options_or_object_names
      object_names.each do |object_name|
        options = @all_components[object_name]
        result.concat(dependencies_for(options))
      end
      result.uniq!
    end
    return result
  end

  def object_filenames_for(object_names)
    return object_names.map { |name| "#{@output_dir}/#{name}" }
  end

  def group_all_components_by_category
    categories = {}
    @all_ordered_components.each do |object_name|
      options  = @all_components[object_name]
      category = options[:category]
      categories[category] ||= []
      categories[category] << object_name
    end
    return categories
  end

  def aggregate_sources?
    # Feature disabled: it's too hard to make it work because
    # lots of executables have to be linked to individual objects
    # anyway.
    return false
  end
end


COMMON_LIBRARY = CommonLibraryBuilder.new do
  define_component 'Logging.o',
    :source   => 'Logging.cpp',
    :category => :base,
    :optimize => :light,
    :deps     => %w(
      Logging.cpp
      Logging.h
    )
  define_component 'Exceptions.o',
    :source   => 'Exceptions.cpp',
    :category => :base,
    :deps     => %w(
      Exceptions.h
    )
  define_component 'Utils/SystemTime.o',
    :source   => 'Utils/SystemTime.cpp',
    :category => :base,
    :deps     => %w(
      Utils/SystemTime.h
    )
  define_component 'Utils/StrIntUtils.o',
    :source   => 'Utils/StrIntUtils.cpp',
    :category => :base,
    :optimize => :very_heavy,
    :deps     => %w(
      Utils/StrIntUtils.h
    )
  define_component 'Utils/StrIntUtilsNoStrictAliasing.o',
    :source   => 'Utils/StrIntUtilsNoStrictAliasing.cpp',
    :category => :base,
    # Compiling with -O3 causes segfaults on RHEL 6
    :optimize => :heavy,
    :strict_aliasing => false,
    :deps     => %w(
      Utils/StrIntUtils.h
    )
  define_component 'Utils/IOUtils.o',
    :source   => 'Utils/IOUtils.cpp',
    :optimize => :light,
    :category => :base,
    :deps     => %w(
      Utils/IOUtils.h
    )
  define_component 'Utils.o',
    :source   => 'Utils.cpp',
    :category => :base,
    :deps     => %w(
      Utils.h
      Utils/StrIntUtils.h
      ResourceLocator.h
    )

  define_component 'Utils/modp_b64.o',
    :source   => 'Utils/modp_b64.cpp',
    :category => :other,
    :optimize => true,
    :deps     => %w(
      Utils/modp_b64.h
      Utils/modp_b64_data.h
    )
  define_component 'Utils/CachedFileStat.o',
    :source   => 'Utils/CachedFileStat.cpp',
    :category => :other,
    :deps     => %w(
      Utils/CachedFileStat.h
      Utils/CachedFileStat.hpp
    )
  define_component 'Utils/LargeFiles.o',
    :source   => 'Utils/LargeFiles.cpp',
    :category => :other,
    :deps     => %w(
      Utils/LargeFiles.h
    )
  define_component 'ApplicationPool2/Implementation.o',
    :source   => 'ApplicationPool2/Implementation.cpp',
    :category => :other,
    :deps     => %w(
      ApplicationPool2/Spawner.h
      ApplicationPool2/Common.h
      ApplicationPool2/Pool.h
      ApplicationPool2/Pool/AnalyticsCollection.h
      ApplicationPool2/Pool/GarbageCollection.h
      ApplicationPool2/Pool/GeneralUtils.h
      ApplicationPool2/Pool/ProcessUtils.h
      ApplicationPool2/Pool/Inspection.h
      ApplicationPool2/Pool/Debug.h
      ApplicationPool2/SuperGroup.h
      ApplicationPool2/Group.h
      ApplicationPool2/Process.h
      ApplicationPool2/Socket.h
      ApplicationPool2/Session.h
      ApplicationPool2/Options.h
      ApplicationPool2/PipeWatcher.h
      ApplicationPool2/AppTypes.h
      ApplicationPool2/Spawner.h
      ApplicationPool2/SpawnerFactory.h
      ApplicationPool2/SmartSpawner.h
      ApplicationPool2/DirectSpawner.h
      ApplicationPool2/DummySpawner.h
    )
  define_component 'ApplicationPool2/AppTypes.o',
    :source   => 'ApplicationPool2/AppTypes.cpp',
    :category => :other,
    :deps     => %w(
      ApplicationPool2/AppTypes.h
      Utils/StrIntUtils.h
      Utils/CachedFileStat.h
    )
  define_component 'AgentsStarter.o',
    :source   => 'AgentsStarter.cpp',
    :category => :other,
    :deps     => %w(
      AgentsStarter.h
      ResourceLocator.h
      MessageClient.h
      Utils/IniFile.h
      Utils/VariantMap.h
    )
  define_component 'AgentsBase.o',
    :source   => 'agents/Base.cpp',
    :category => :other,
    :deps     => %w(
      agents/Base.h
      Utils/VariantMap.h
    )
  define_component 'agents/LoggingAgent/FilterSupport.o',
    :source   => 'agents/LoggingAgent/FilterSupport.cpp',
    :category => :logging_agent,
    :deps     => %w(
      agents/LoggingAgent/FilterSupport.h
    )
  define_component 'MemoryKit/mbuf.o',
    :source   => 'MemoryKit/mbuf.cpp',
    :category => :other,
    :optimize => true,
    :deps     => %w(
      MemoryKit/palloc.h
    )
  define_component 'MemoryKit/palloc.o',
    :source   => 'MemoryKit/palloc.cpp',
    :category => :other,
    :optimize => true,
    :deps     => %w(
      MemoryKit/palloc.h
    )
  define_component 'ServerKit/http_parser.o',
    :source   => 'ServerKit/http_parser.cpp',
    :category => :other,
    :optimize => :very_heavy,
    :deps     => %w(
      ServerKit/http_parser.h
    )
  define_component 'ServerKit/Implementation.o',
    :source   => 'ServerKit/Implementation.cpp',
    :category => :other,
    :optimize => true

  define_component 'Utils/MD5.o',
    :source   => 'Utils/MD5.cpp',
    :category => :other,
    :optimize => true,
    :deps     => %w(
      Utils/MD5.h
    )
  define_component 'Utils/Hasher.o',
    :source   => 'Utils/Hasher.cpp',
    :category => :other,
    :optimize => :very_heavy,
    :deps     => %w(
      Utils/Hasher.h
    )
  define_component 'Utils/jsoncpp.o',
    :source   => 'Utils/jsoncpp.cpp',
    :category => :other,
    :optimize => true,
    :deps     => %w(
      Utils/json.h
      Utils/json-forwards.h
    )

  #'BCrypt.o' => %w(
  # BCrypt.cpp
  # BCrypt.h
  # Blowfish.h
  # Blowfish.c)
end

# Objects that must be linked into the Nginx binary.
NGINX_LIBS_SELECTOR = [:base, 'AgentsStarter.o', 'ApplicationPool2/AppTypes.o',
  'Utils/CachedFileStat.o', 'agents/LoggingAgent/FilterSupport.o']
