module Pod
  class Installer

    # This class is responsible for adding linked Xcode dependencies as
    # subprojects in the correct groups and link the Xcode subproject
    # library targets to the respective pod targets.
    #
    class LinkedDependenciesInstaller

      # @return [Project] The Pods project.
      #
      attr_reader :pods_project

      # @return [Pathname] The root path of the pod.
      #
      attr_reader :pod_root

      # @return [PodTarget] The pod target to process.
      #
      attr_reader :pod_target

      # @param [Project] pods_project @see pods_project
      # @param [Pathname] pod_root @see pod_root
      # @param [PodTarget] pod_target @see pod_target
      #
      def initialize(pods_project, pod_root, pod_target)
        @pods_project = pods_project
        @pod_root = pod_root
        @pod_target = pod_target
      end

      # Installs the linked projects for a given pod target.
      #
      # @return [void]
      #
      def install!
        add_linked_projects
        add_libraries_to_target
      end

      #-----------------------------------------------------------------------#

      private

      # @!group Installation Steps

      # Add the Xcode projects that specs refer to as subprojects to the Pods
      # Xcode project, in the appropriate groups.
      #
      # @return [void]
      #
      def add_linked_projects
        linked_project_specs.each do |path, specs|
          specs.each do |spec|
            group = pods_project.group_for_spec(spec.name)
            pods_project.add_file_reference(path, group)
          end
        end
      end

      # Add the linked project targets to the pod target.
      #
      # @return [void]
      #
      def add_libraries_to_target
        pod_target.spec_consumers.map do |consumer|
          xcodeprojs_from_consumer(consumer)
        end.flatten.each do |xcodeproj|
          linked_project = open_linked_xcode_project(xcodeproj['project'])

          # Hide the schemes that would be autogenerated for the subproject's targets.
          linked_project.recreate_user_schemes(false)

          link_targets_with_target(linked_project, xcodeproj['library_targets'] || [])
        end
      end

      #-----------------------------------------------------------------------#

      private

      # @!group Private Helpers

      # Open an Xcode project refered to by a specific pod. Raises an
      # Informative error if it fails to open the Xcode project file.
      #
      # @param [String] The filename of the Xcode project, relative to
      #        the pod's root.
      #
      # @return [Xcodeproj::Project] The opened Xcode project file
      #
      def open_linked_xcode_project(project_filename)
        linked_project = begin
          Xcodeproj::Project.open(pod_root + project_filename)
        rescue RuntimeError => e
          raise Informative, "Could not open project #{project_filename}, specified in #{spec_file}: #{e.message}"
        end

        linked_project
      end

      # Link a number of targets within a subproject to a pod target.
      #
      # @param [Xcodeproj::Project] linked_project The Xcode subproject that
      #        contains the library targets.
      # @param [Array<String>] library_target_names The names of the targets
      #        that should be linked against.
      #
      # @return [void]
      #
      def link_targets_with_target(linked_project, library_target_names)
        library_target_names.each do |library_target_name|
          library_target = find_named_native_target_in_project(linked_project, library_target_name)

          link_target_with_target(pod_target.target, library_target)
        end
      end

      # Make sure a target is linked with another target.
      #
      # @param [PBXNativeTarget] main_target The target that should link against
      #        another target.
      # @param [PBXNativeTarget] library_target The target that should be linked
      #        against.
      #
      # @return [void]
      #
      def link_target_with_target(main_target, library_target)
        frameworks = main_target.project.frameworks_group
        lib_name = library_target.product_reference.path.sub(/^lib/, '').sub(/\.a$/, '')
        library_ref = frameworks.new_product_ref_for_target(lib_name, :static_library)

        main_target.add_dependency(library_target)
        main_target.frameworks_build_phase.add_file_reference(library_ref)
      end

      # @return [Hash<Pathname => Array<Specification>>] A hash of the Xcode
      #         project paths to the specifications that refer to it.
      #
      def linked_project_specs
        specs_by_path = {}

        pod_target.spec_consumers.map do |consumer|
          xcodeprojs_from_consumer(consumer).each do |proj|
            absolute_path = pod_root + proj['project']

            specs_by_path[absolute_path] ||= []
            specs_by_path[absolute_path] << consumer.spec
          end
        end

        specs_by_path
      end

      # Finds the native target in an Xcode project with a given name.
      # Raises an Informative error if the native target can't be found.
      #
      # @param [Project] project The project to search in
      # @param [String] target_name The name of the target that we're
      #        looking for
      #
      # @return [PBXNativeTarget] The native target with the given name
      #
      def find_named_native_target_in_project(project, target_name)
        target = project.targets.find do |target|
          target_name == target.name && target.isa == 'PBXNativeTarget'
        end

        if target.nil?
          raise Informative, "Could not find native target #{target_name} in project #{project.path}, specified in #{spec_file}"
        end

        target
      end

      # Takes a Consumer and extracts an array of xcodeproj descriptors.
      #
      # @param [Specification::Consumer] consumer The consumer to take xcodeprojs from
      #
      # @return [Array<Hash>] A list of valid xcodeproj specifications
      #
      def xcodeprojs_from_consumer(consumer)
        xcodeprojs = consumer.xcodeprojs
        xcodeprojs = [] if xcodeprojs.nil?
        xcodeprojs = [xcodeprojs] if xcodeprojs.kind_of?(Hash)

        xcodeprojs = xcodeprojs.map do |xcodeproj|
          xcodeproj.inject({}) { |memo, (k,v)| memo[k.to_s] = v; memo }
        end

        xcodeprojs.each do |xcodeproj|
          unless xcodeproj.has_key?('project')
            raise Informative, "Missing project parameter to xcodeproj specification: #{xcodeproj.inspect}"
          end
        end

        xcodeprojs
      end

      # @return [String] the path where the pod target's specification is
      #         defined, if loaded from a file. (May be nil)
      #
      def spec_file
        pod_target.root_spec.defined_in_file
      end

      #-----------------------------------------------------------------------#

    end
  end
end
