module Pod

  # The Installer is responsible of taking a Podfile and transform it in the
  # Pods libraries. It also integrates the user project so the Pods
  # libraries can be used out of the box.
  #
  # The Installer is capable of doing incremental updates to an existing Pod
  # installation.
  #
  # The Installer gets the information that it needs mainly from 3 files:
  #
  #   - Podfile: The specification written by the user that contains
  #     information about targets and Pods.
  #   - Podfile.lock: Contains information about the pods that were previously
  #     installed and in concert with the Podfile provides information about
  #     which specific version of a Pod should be installed. This file is
  #     ignored in update mode.
  #   - Manifest.lock: A file contained in the Pods folder that keeps track of
  #     the pods installed in the local machine. This files is used once the
  #     exact versions of the Pods has been computed to detect if that version
  #     is already installed. This file is not intended to be kept under source
  #     control and is a copy of the Podfile.lock.
  #
  # The Installer is designed to work in environments where the Podfile folder
  # is under source control and environments where it is not. The rest of the
  # files, like the user project and the workspace are assumed to be under
  # source control.
  #
  class Installer

    autoload :Analyzer,                'cocoapods/installer/analyzer'
    autoload :FileReferencesInstaller, 'cocoapods/installer/file_references_installer'
    autoload :PodSourceInstaller,      'cocoapods/installer/pod_source_installer'
    autoload :TargetInstaller,         'cocoapods/installer/target_installer'
    autoload :UserProjectIntegrator,   'cocoapods/installer/user_project_integrator'

    include Config::Mixin

    # @return [Sandbox] The sandbox where the Pods should be installed.
    #
    attr_reader :sandbox

    # @return [Podfile] The Podfile specification that contains the information
    #         of the Pods that should be installed.
    #
    attr_reader :podfile

    # @return [Lockfile] The Lockfile that stores the information about the
    #         Pods previously installed on any machine.
    #
    attr_reader :lockfile

    # @param  [Sandbox]  sandbox     @see sandbox
    # @param  [Podfile]  podfile     @see podfile
    # @param  [Lockfile] lockfile    @see lockfile
    #
    def initialize(sandbox, podfile, lockfile = nil)
      @sandbox  =  sandbox
      @podfile  =  podfile
      @lockfile =  lockfile
    end

    # @return [Bool] Whether the installer is in update mode. In update mode
    #         the contents of the Lockfile are not taken into account for
    #         deciding what Pods to install.
    #
    attr_accessor :update_mode

    # Installs the Pods.
    #
    # The installation process of is mostly linear with few minor complications
    # to keep in mind:
    #
    # - The stored podspecs need to be cleaned before the resolution step
    #   otherwise the sandbox might return an old podspec and not download
    #   the new one from an external source.
    # - The resolver might trigger the download of Pods from external sources
    #   necessary to retrieve their podspec (unless it is instructed not to
    #   do it).
    #
    # @return [void]
    #
    def install!
      analyze
      detect_pods_to_install
      prepare_for_legacy_compatibility
      prepare_sandbox

      UI.section "Downloading dependencies" do
        create_file_accessors
        install_pod_sources
      end

      UI.section "Generating Pods Project" do
        prepare_pods_project
        run_pre_install_hooks
        install_file_references
        install_targets
        run_post_install_hooks
        write_pod_project
        write_lockfiles
      end

      if config.integrate_targets?
        UI.section "Integrating client projects" do
          integrate_user_project
        end
      end
    end

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

    public

    # @!group Installation results

    # @return [Analyzer] the analyzer which provides the information about what
    #         needs to be installed.
    #
    attr_reader :analyzer

    # @return [Pod::Project] the `Pods/Pods.xcodeproj` project.
    #
    attr_reader :pods_project

    # @return [Array<String>] The Pods that should be installed.
    #
    attr_reader :names_of_pods_to_install

    # @return [Array<Library>] The libraries generated by the installation
    #         process.
    #
    attr_reader :libraries

    # @return [Array<Specification>] The specifications that where installed.
    #
    attr_accessor :installed_specs

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

    private

    # TODO: This is recreating the file accessors
    # TODO: the file accessor should be initializable without a path list

    # @!group Installation steps

    # @return [void]
    #
    def analyze
      @analyzer = Analyzer.new(sandbox, podfile, lockfile)
      @analyzer.update_mode = update_mode
      @analyzer.analyze
      @libraries = analyzer.libraries
    end

    # Computes the list of the Pods that should be installed or reinstalled in
    # the {Sandbox}.
    #
    # The pods to install are identified as the Pods that don't exist in the
    # sandbox or the Pods whose version differs from the one of the lockfile.
    #
    # @note   In update mode specs originating from external dependencies and
    #         or from head sources are always reinstalled.
    #
    # @return [void]
    #
    # @todo   [#534] Detect if the folder of a Pod is empty (even if it exits).
    #
    # @todo   There could be issues with the current implementation regarding
    #         external specs.
    #
    def detect_pods_to_install
      names = []

      analyzer.specifications.each do |spec|
        root_name = spec.root.name

        if update_mode
          if spec.version.head? #|| TODO resolver.pods_from_external_sources.include?(root_name)
            names << root_name
          end
        end

        unless sandbox.pod_dir(root_name).exist?
          names << root_name
        end
      end

      names += analyzer.sandbox_state.added + analyzer.sandbox_state.changed
      names = names.map { |name| Specification.root_name(name) }
      names = names.flatten.uniq
      @names_of_pods_to_install = names
    end

    # Prepares the Pods folder in order to be compatible with the most recent
    # version of CocoaPods.
    #
    # @return [void]
    #
    def prepare_for_legacy_compatibility
      # move_target_support_files_if_needed
      # move_Local_Podspecs_to_Podspecs_if_needed
      # move_pods_to_sources_folder_if_needed
    end

    # @return [void] In this step we clean all the folders that will be
    #         regenerated from scratch and any file which might not be
    #         overwritten.
    #
    # @todo   [#247] Clean the headers of only the pods to install.
    #
    # @todo   [#534] Clean all the Pods folder that are not unchanged
    #
    # @todo   Use the local pod implode.
    #
    # @todo   Clean the podspecs of all the pods that aren't unchanged so the
    #         resolution process doesn't get confused by them.
    #
    def prepare_sandbox
      sandbox.build_headers.implode!
      sandbox.public_headers.implode!

      # TODO local option
      unless analyzer.sandbox_state.deleted.empty?
        UI.section "Removing deleted dependencies" do
          analyzer.sandbox_state.deleted.each do |pod_name|
            UI.section("Removing #{pod_name}", "-> ".red) do
              path = sandbox.root + pod_name
              path.rmtree if path.exist?
            end
          end
        end
      end
    end

    # Creates the Pods project from scratch if it doesn't exists.
    #
    # @return [void]
    #
    # @todo   Clean and modify the project if it exists.
    #
    def prepare_pods_project
      UI.message "- Creating Pods project" do
        @pods_project = Pod::Project.new(sandbox.project_path)
        if config.podfile_path.exist?
          @pods_project.add_podfile(config.podfile_path)
        end
        sandbox.project = @pods_project
      end
    end

    #
    #
    def create_file_accessors
      libraries.each do |library|
        library.specs.each do |spec|
          if spec.local?
            pod_root = Pathname.new(spec.source[:local]).expand_path
          else
            pod_root = sandbox.pod_dir(spec.root.name)
          end
          path_list = Sandbox::PathList.new(pod_root)
          file_accessor = Sandbox::FileAccessor.new(path_list, spec.consumer(library.platform))
          library.file_accessors ||= []
          library.file_accessors << file_accessor
        end
      end
    end

    # Downloads, installs the documentation and cleans the sources of the Pods
    # which need to be installed.
    #
    # @return [void]
    #
    def install_pod_sources
      @installed_specs = []
      root_specs = analyzer.specifications.map { |spec| spec.root }.uniq
      root_specs.each do |spec|
        if names_of_pods_to_install.include?(spec.name)
          UI.section("Installing #{spec}".green, "-> ".green) do
            install_source_of_pod(spec.name)
          end
        else
          UI.section("Using #{spec}", "-> ".green)
        end
      end
    end

    # Install the Pods. If the resolver indicated that a Pod should be
    # installed and it exits, it is removed an then reinstalled. In any case if
    # the Pod doesn't exits it is installed.
    #
    # @return [void]
    #
    def install_source_of_pod(pod_name)
      specs_by_platform = {}
      libraries.each do |library|
        specs = library.specs.select { |spec| spec.root.name == pod_name }

        unless specs.empty?
          specs_by_platform[library.platform] ||= []
          specs_by_platform[library.platform].concat(specs)
        end
      end

      pod_installer = PodSourceInstaller.new(sandbox, specs_by_platform)
      pod_installer.clean = config.clean?
      pod_installer.aggressive_cache = config.aggressive_cache?
      pod_installer.generate_docs = config.generate_docs?
      pod_installer.install_docs = config.install_docs?
      pod_installer.install!
      @installed_specs.concat(specs_by_platform.values.flatten)
    end

    # Runs the pre install hooks of the installed specs and of the Podfile.
    #
    # @return [void]
    #
    def run_pre_install_hooks
      UI.message "- Running pre install hooks" do
        installed_specs.each do |spec|
          executed = spec.pre_install!(pod_data(spec), library_data(nil))
          UI.message "- #{spec.name}" if executed
        end

        executed = @podfile.pre_install!(installer_data)
        UI.message "- Podfile" if executed
      end
    end

    # Runs the post install hooks of the installed specs and of the Podfile.
    #
    # @note   Post install hooks run _before_ saving of project, so that they
    #         can alter it before it is written to the disk.
    #
    # @return [void]
    #
    def run_post_install_hooks
      UI.message "- Running post install hooks" do

        installed_specs.each do |spec|
          target_installer_data = target_installers_data.first #TODO
          executed = spec.post_install!(target_installer_data)
          UI.message "- #{spec.name}" if executed
        end
        executed = @podfile.post_install!(installer_data)
        UI.message "- Podfile" if executed
      end
    end

    # Installs the file references in the Pods project. This is done once per
    # Pod as the same file reference might be shared by multiple targets.
    #
    # @return [void]
    #
    def install_file_references
      installer = FileReferencesInstaller.new(sandbox, libraries, pods_project)
      installer.install!
    end

    # Installs the targets of the Pods projects and generates their support
    # files.
    #
    # @return [void]
    #
    def install_targets
      UI.message"- Installing targets" do
        libraries.each do |library|
          next if library.target_definition.empty?
          target_installer = TargetInstaller.new(sandbox, library)
          target_installer.install!
        end
      end
    end

    # Writes the Pods project to the disk.
    #
    # @return [void]
    #
    def write_pod_project
      UI.message "- Writing Xcode project file to #{UI.path sandbox.project_path}" do
        pods_project.main_group.sort_by_type!
        pods_project['Frameworks'].sort_by_type!
        pods_project.save_as(sandbox.project_path)
      end
    end

    # Writes the Podfile and the lock files.
    #
    # @return [void]
    #
    def write_lockfiles
      # checkout_options = sandbox.checkout_options
      # TODO pass the options to the Lockfile
      @lockfile = Lockfile.generate(podfile, analyzer.specifications)

      UI.message "- Writing Lockfile in #{UI.path config.lockfile_path}" do
        @lockfile.write_to_disk(config.lockfile_path)
      end

      UI.message "- Writing Manifest in #{UI.path sandbox.manifest_path}" do
        @lockfile.write_to_disk(sandbox.manifest_path)
      end
    end

    # Integrates the user projects adding the dependencies on the CocoaPods
    # libraries, setting them up to use the xcconfigs and performing other
    # actions. This step is also responsible of creating the workspace if
    # needed.
    #
    # @return [void]
    #
    # @todo   [#397] The libraries should be cleaned and the re-added on every
    #         installation. Maybe a clean_user_project phase should be added.
    #         In any case it appears to be a good idea store target definition
    #         information in the lockfile.
    #
    def integrate_user_project
      installation_root = config.installation_root
      libraries = analyzer.libraries
      UserProjectIntegrator.new(podfile, sandbox, installation_root, libraries).integrate!
    end

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

    public

    # @!group Hooks

    def installer_data
      Hooks::InstallerData.new(self)
    end

    def target_installers_data
      @target_installers_data ||= libraries.map do |lib|
        data = Hooks::TargetInstallerData.new
        data.sandbox = sandbox
        data.library = lib
        data
      end
    end

    def pods_data
      specs = libraries.map(&:specs).flatten
      root_specs = specs.map { |spec| spec.root }.uniq
      root_specs.map do |spec|
        pod_data(spec)
      end
    end

    def pod_data(spec)
      all_file_accessors = libraries.map(&:file_accessors).flatten.compact
      file_accessors = all_file_accessors.select { |fa| fa.spec.root == spec.root }
      Hooks::PodData.new(file_accessors)
    end

    def library_data(library)
      Hooks::LibraryData.new
    end

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

  end
end
