module Pod
  class Command
    class DeepLinter < Specification::Linter

      include Config::Mixin

      # Lints the specification adding a {Result} for any failed check to the
      # {#results} list.
      #
      # @return [Bool] whether the specification passed validation.
      #
      def lint
        super
        unless quick
          check_repo_path
          spec.available_platforms.each do |platform|
            UI.section "\n\n#{spec} - Analyzing on #{platform} platform.".green.reversed do
              current_platform = platform
              set_up_lint_environment
              install_pod
              build_pod
              check_file_patterns
              tear_down_lint_environment
            end
          end
        end
        errors.empty? && warnings.empty? && deprecations.empty?
      end

      # @return [Bool] whether the lint should skip the checks that requires
      # the download or the build of the library.
      #
      attr_accessor :quick

      # @return [Bool] whether the linter should not clean up temporary files
      # for inspection.
      #
      attr_accessor :no_clean

      # @return [Pathname] whether the lint should be performed
      #
      attr_accessor :repo_path

      # @return [Bool] whether the lint should be performed against the root of
      #   the podspec instead to its original source. Uses the `:local` option
      #   of the Podfile.
      #
      attr_writer :local
      def local?; @local; end

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

      # !@group Lint results

      public

      # @return [Array<Result>] all the notes generated by the Linter.
      #
      def notes
        @errors ||= results.select { |r| r.type == :note }
      end

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

      private

      # !@group Lint steps

      def check_repo_path
        return unless repo_path
        expected_path = "#{spec.name}/#{spec.version}/#{spec.name}.podspec"
        path = file.relative_path_from(repo_path).to_s
        unless path.end_with?(expected_path)
          error "Incorrect path, the path is `#{file}` and should be `#{expected_path}`"
        end
      end

      def set_up_lint_environment
        tmp_dir.rmtree if tmp_dir.exist?
        tmp_dir.mkpath
        @original_config = Config.instance.clone
        config.project_root      = tmp_dir
        config.project_pods_root = tmp_dir + 'Pods'
        config.silent            = !config.verbose
        config.integrate_targets = false
        config.generate_docs     = false
      end

      def tear_down_lint_environment
        tmp_dir.rmtree unless no_clean
        Config.instance = @original_config
      end

      # It creates a podfile in memory and builds a library containing
      # the pod for all available platforms with xcodebuild.
      #
      def install_pod
        spec.activate_platform(current_platform)
        podfile = podfile_from_spec
        config.verbose
        config.skip_repo_update = true
        sandbox = Sandbox.new(config.project_pods_root)
        resolver = Resolver.new(podfile, nil, sandbox)
        installer = Installer.new(resolver)
        installer.install!
        @pod = installer.pods.find { |pod| pod.top_specification == spec }
        config.silent
      end

      # Performs platform specific analysis.
      # It requires to download the source at each iteration
      #
      # @note Treat xcodebuild warnings as notes because the spec maintainer
      #       might not be the author of the library
      #
      def build_pod
        if `which xcodebuild`.strip.empty?
          UI.warn "Skipping compilation with `xcodebuild' because it can't be found.\n".yellow
        else
          UI.message "\nBuilding with xcodebuild.\n".yellow if config.verbose? do
            messages      = []
            output        = Dir.chdir(config.project_pods_root) { `xcodebuild clean build 2>&1` }
            clean_output  = parse_xcodebuild_output(output)
            messages     += clean_output
            puts(output) if config.verbose?
            messages.each { |msg| ( msg.include?('error: ') ? @platform_errors[@platform] : @platform_notes[@platform] ) << msg }
          end
        end
      end

      # It checks that every file pattern specified in a spec yields
      # at least one file. It requires the pods to be already present
      # in the current working directory under Pods/spec.name.
      #
      # @return [Array<String>]
      #
      def check_file_patterns
        error "The sources did not match any file" if !spec.source_files.empty? && @pod.source_files.empty?
        error "The resources did not match any file" if !spec.resources.empty? && @pod.resource_files.empty?
        error "The preserve_paths did not match any file" if !spec.preserve_paths.empty? && @pod.preserve_files.empty?
        error "The exclude_header_search_paths did not match any file" if !spec.exclude_header_search_paths.empty? && @pod.headers_excluded_from_search_paths.empty?

        unless @pod.license_file || spec.license && ( spec.license[:type] == 'Public Domain' || spec.license[:text] )
          warning "Unable to find a license file"
        end
      end

      # @errors   += (@platform_errors[platform] - @errors).map {|m| "[#{platform}] #{m}"}

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

      private

      # !@group Helpers

      # @return [Podfile] a podfile that requires the specification on the
      # current platform.
      #
      # @note   The generated podfile takes into account whether the linter is
      #         in local mode.
      #
      def podfile_from_spec
        name     = spec.name
        podspec  = file.realpath
        platform = current_platform
        local    = local?
        podfile  = Pod::Podfile.new do
          platform(platform.to_sym, platform.deployment_target)
          if (local)
            pod name, :local => podspec.dirname.to_s
          else
            pod name, :podspec => podspec.to_s
          end
        end
        podfile
      end

      # @return [Pathname] the temporary directory used by the linter.
      #
      def tmp_dir
        Pathname.new('/tmp/CocoaPods/Lint')
      end

      # @return [Pathname] the root of the installed pod.
      #
      def pod_dir
        tmp_dir + 'Pods' + spec.name
      end

      # Parse the xcode build output to identify the lines which are relevant
      # to the linter. It also removes the indentation and the temporary path.
      #
      # @param  [String] output
      #         the output generated by the xcodebuild tool.
      #
      # @return [Array<String>] the lines that are relevant to the linter.
      #
      def parse_xcodebuild_output(output)
        lines = output.split("\n")
        selected_lines = lines.select do |l|
          l.include?('error: ') &&
            (l !~ /errors? generated\./) && (l !~ /error: \(null\)/)  ||
            l.include?('warning: ') && (l !~ /warnings? generated\./) ||
            l.include?('note: ') && (l !~ /expanded from macro/)
        end
        selected_lines.map do |l|
          new = l.gsub(/\/tmp\/CocoaPods\/Lint\/Pods\//,'') # Remove the unnecessary tmp path
          new.gsub!(/^ */,' ')                              # Remove indentation
          "XCODEBUILD > " << new                            # Mark
        end
      end
    end
  end
end
