require 'rake'
require 'nokogiri'
require 'fileutils'
require 'pathname'
require 'albacore'
require 'albacore/paths'
require 'albacore/cmd_config'
require 'albacore/config_dsl'
require 'albacore/cross_platform_cmd'
require 'albacore/project'
require 'albacore/logging'
require 'albacore/nuget_model'
require 'albacore/task_types/nugets'

module Albacore
  module NugetsPack
    # the nuget command
    class Cmd
      include CrossPlatformCmd

      # executable => the nuget executable
      def initialize executable, *args
        opts = Map.options args
        raise ArgumentError, 'out is nil' if opts.getopt(:out).nil?

        @work_dir   = opts.getopt :work_dir, default: nil
        @executable = executable
        @parameters = [%W{Pack -OutputDirectory #{opts.get(:out)}}].flatten
        @opts = opts

        mono_command unless opts.get(:mono_opt_out)
      end

      # run nuget on the nuspec to create a new package
      # returns: a tuple-array of the package and the symbol package
      #   of which the symbol package is nil if it was not generated
      def execute nuspec_file, nuspec_symbols_file = nil
        debug "NugetsPack::Cmd#execute, opts: #{@opts} [nugets pack: cmd]"
        original_pars = @parameters.dup

        pars = original_pars.dup
        pars << nuspec_file
        pars << '-NoPackageAnalysis' unless @opts.get :package_analysis
        pkg = get_nuget_path_of do
          system @executable, pars, :work_dir => @work_dir
        end

        debug "package at '#{pkg}'"

        # if the symbols flag is set and there's a symbols file specified
        # then run NuGet.exe to generate the .symbols.nupkg file
        if nuspec_symbols_file
          pars = original_pars.dup
          pars << '-Symbols'
          pars << nuspec_symbols_file
          spkg = with_subterfuge pkg do
            get_nuget_path_of do
              system @executable, pars, :work_dir => @work_dir
            end
          end

          debug "symbol package at '#{spkg}'"

          [pkg, spkg]
        else
          info "symbols not configured for generation, use Config#gen_symbols to do so [nugets pack: cmd]"
          [pkg, nil]
        end
      end

      private

      # regexpes the package path from the output
      def get_nuget_path_of
        out = yield
        out.match(/Successfully created package '([:\s\p{Word}\\\/\d\.\-]+\.symbols\.nupkg)'./iu) if out.respond_to? :match
        trace "Got symbols return value: '#{out}', matched: '#{$1}'" if $1
        return $1 if $1

        out.match(/Successfully created package '([:\s\p{Word}\\\/\d\.\-]+\.nupkg)'./iu)if out.respond_to? :match
        trace "Got NOT-symbols return value: '#{out}', matched: '#{$1}'"

        unless $1
          args = ARGV.inject("") { |state, arg| state + " " + '"' + arg + '"' }
          warn do
            %{Couldn't match package, please run
     bundle exec rake DEBUG=true #{args} --trace
and report a bug to albacore with the full output. Here's the nuget process output:
--- START OUTPUT ---
#{out}
--- END OUTPUT ---
}
          end
        end

        $1
      end

      # hide the original like a ninja while NuGet whimpers in a corner
      def with_subterfuge pkg
        FileUtils.mv pkg, "#{pkg}.tmp" if pkg && File.exists?(pkg)
        res = yield
        FileUtils.mv "#{pkg}.tmp", pkg if pkg && File.exists?("#{pkg}.tmp")
        res
      end
    end

    # This tasktype allows you to quickly package project files to nuget
    # packages.
    #
    # Point files to the project files, that should be in MsBuild XML.
    #
    # Examples
    #
    #  nugets_pack :pack => ['build/pkg', :versioning] do |p|
    #    p.files   = FileList['src/**/*.csproj']
    #    p.out     = 'build/pkg'
    #    p.exe     = 'buildsupport/NuGet.exe'
    #    p.with_metadata do |m|
    #      m.version = ENV['NUGET_VERSION']
    #    end
    #    p.gen_symbols
    #    p.no_project_dependencies
    #  end
    class Config
      include CmdConfig
      self.extend ConfigDSL

      # the output directory to place the newfangled nugets in
      attr_path :out

      # the .net target (e.g. net40, mono20, mono3, etc)
      attr_writer :target

      # sets the files to search
      attr_writer :files

      # sets the nuspec file
      attr_writer :nuspec


      # sets the MsBuild configuration that is used to produce the output into
      # <OutputPath>...</OutputPath>
      attr_writer :configuration

      def initialize
        @package = Albacore::NugetModel::Package.new
        @target  = ''
        @symbols = false
        @project_dependencies = true
        @nuget_dependencies = true
        @package_analysis = true
        @leave_nuspec = false
        @mono_opt_out = false
        fill_required
      end

      def mono_opt_out
        @mono_opt_out = true
      end

      def with_metadata &block
        yield @package.metadata
      end

      # configure the package with a block
      def with_package &block
        yield @package
      end

      # generate symbols for the nugets: just call this method to
      # enable generation
      def gen_symbols
        @symbols = true
      end

      # leave the nuspec behind, don't delete it after generating it
      #
      def leave_nuspec
        @leave_nuspec = true
      end

      # call this if you want to cancel 'smart' scanning of the *proj
      # file for its dependencies
      def no_project_dependencies
        @project_dependencies = false
      end

      # call this if you want to cancel inclusion of package dependencies
      # when creating the nuspec file
      def no_nuget_dependencies
        @nuget_dependencies = false
      end

      # call this if you want to disable NuGet's package analysis
      # when creating the nupkg file
      def no_package_analysis
        @package_analysis = false
      end

      def nuget_gem_exe
        @exe = Albacore::Nugets::find_nuget_gem_exe
      end

      # gets the options specified for the task, used from the task
      def opts
        files = @files.respond_to?(:each) ? @files : [@files]

        unless @nuspec
          [:authors, :description, :version].each do |required|
            warn "metadata##{required} is missing from nugets_pack [nugets pack: config]" if @package.metadata.send(required) == 'MISSING'
          end
        end

        Map.new({
          :out           => @out,
          :nuspec        => @nuspec,
          :exe           => @exe,
          :symbols       => @symbols,
          :package       => @package,
          :target        => @target,
          :files         => files,
          :configuration => @configuration,
          :project_dependencies => @project_dependencies,
          :nuget_dependencies => @nuget_dependencies,
          :package_analysis => @package_analysis,
          :mono_opt_out     => @mono_opt_out,
          :original_path => FileUtils.pwd,
          :leave_nuspec  => @leave_nuspec
        })
      end

      private

      def fill_required
        # see http://docs.nuget.org/docs/reference/nuspec-reference
        with_metadata do |m|
          m.authors = m.description = m.version = 'MISSING'
        end
      end
    end

    # a task that handles the generation of nugets from projects or nuspecs.
    class ProjectTask
      include Logging

      def initialize opts, &before_execute

        unless opts.get(:nuspec)
          raise ArgumentError, 'opts is not a map' unless opts.is_a? Map
          raise ArgumentError, 'no files given' unless opts.get(:files).length > 0
        end

        @opts           = opts.apply :out => '.'
        @files          = opts.get :files
        @before_execute = before_execute
      end

      def execute
        unless @opts.get(:nuspec)
          knowns = compute_knowns
          @files.each do |p|
            proj, n, ns = generate_nuspec p, knowns
            execute_inner! proj, n, ns
          end
        else
          create_nuget! "#{Dir.pwd}", @opts.get(:nuspec)
        end
      end

      def path_to path, cwd
        if (Pathname.new path).absolute?
           return path
        else
           return File.expand_path( File.join(@opts.get(:original_path), path), cwd )
        end
      end

      # generate all nuspecs
      def generate_nuspecs
        nuspecs = {}
        knowns = compute_knowns
        @files.each do |p|
          proj, n, ns = generate_nuspec p, knowns
          nuspecs[proj.name] = OpenStruct.new({:proj => proj, :nuspec => n, :nuspec_symbols => ns })
        end
        nuspecs
      end

      private

      # Computes the known *Ids* of the projects; as Name = Title when creating nugets.
      def compute_knowns
        Set.new(@files.map { |f| Albacore::Project.new f }.map { |p| p.id })
      end

      def generate_nuspec p, knowns
        proj = Albacore::Project.new p
        nuspec, nuspec_symbols = create_nuspec proj, knowns
        [proj, nuspec, nuspec_symbols]
      end

      # execute, for each project file
      def execute_inner! proj, nuspec, nuspec_symbols
        nuspec_path = write_nuspec! proj, nuspec, false
        nuspec_symbols_path = write_nuspec! proj, nuspec_symbols, true if nuspec_symbols

        create_nuget! proj.proj_path_base, nuspec_path, nuspec_symbols_path
      rescue => e
        err (e.inspect)
        raise $!
      ensure
        trace do
          %{
 PROJECT #{proj.name} nuspec:
#{nuspec.to_xml}
 PROJECT #{proj.name} symbol nuspec:
#{if nuspec_symbols then nuspec_symbols.to_xml else 'NO SYMBOLS' end}}
        end

        # now remove them all
        [nuspec_path, nuspec_symbols_path].each{|n| cleanup_nuspec n}
      end

      ## Creating

      def create_nuspec proj, knowns
        version = @opts.get(:package).metadata.version
        framework_dependencies = @opts.get(:package).metadata.framework_assemblies
        project_dependencies = @opts.get(:project_dependencies, true)
        nuget_dependencies = @opts.get(:nuget_dependencies, true)
        target = if '' == @opts.get(:target) then proj.target_framework else @opts.get :target end
        if target.start_with? 'v'
          target = "net#{target[1..target.size].gsub('.', '')}"
        end

        trace "creating NON-SYMBOL package for '#{proj.name}', targeting '#{target}' [nugets pack: task]"
        nuspec = Albacore::NugetModel::Package.from_xxproj proj,
          symbols:        false,
          verify_files:   true,
          dotnet_version: target,
          known_projects: knowns,
          version:        version,
          configuration:  (@opts.get(:configuration)),
          project_dependencies: project_dependencies,
          nuget_dependencies: nuget_dependencies,
          framework_dependencies: framework_dependencies

        # take data from package as configured in Rakefile, choosing what is in
        # Rakefile over what is in projfile.
        nuspec = nuspec.merge_with @opts.get(:package)
        trace { "nuspec: #{nuspec.to_s} [nugets pack: task]" }

        if @opts.get(:symbols)
          trace { "creating SYMBOL package for '#{proj.name}' [nugets pack: task]" }
          nuspec_symbols = Albacore::NugetModel::Package.from_xxproj proj,
            symbols:        true,
            verify_files:   true,
            dotnet_version: target,
            known_projects: knowns,
            version:        version,
            configuration:  (@opts.get(:configuration)),
            project_dependencies: project_dependencies,
            nuget_dependencies: nuget_dependencies,
            framework_dependencies: framework_dependencies

          nuspec_symbols = nuspec_symbols.merge_with @opts.get(:package)
          trace { "nuspec symbols: #{nuspec_symbols.to_s} [nugets pack: task]" }

          [nuspec, nuspec_symbols]
        else
          trace { "skipping SYMBOL package for #{proj.name} [nugets pack: task]" }
          [nuspec, nil]
        end
      end

      def write_nuspec! proj, nuspec, symbols
        raise ArgumentError, "no nuspect metadata id, project at path: #{proj.proj_path_base}, nuspec: #{nuspec.inspect}" unless nuspec.metadata.id
        nuspec_path = File.join(proj.proj_path_base, nuspec.metadata.id + "#{ symbols ? '.symbols' : '' }.nuspec")

        File.write(nuspec_path, nuspec.to_xml)

        nuspec_path
      end

      def create_nuget! cwd, nuspec, nuspec_symbols = nil
        # create the command
        exe = path_to(@opts.get(:exe), cwd)
        out = path_to(@opts.get(:out), cwd)
        nuspec = path_to nuspec, cwd
        nuspec_symbols = path_to nuspec_symbols, cwd if nuspec_symbols

        cmd = Albacore::NugetsPack::Cmd.new exe,
                work_dir:         cwd,
                out:              out,
                package_analysis: @opts.get(:package_analysis),
                mono_opt_out:     @opts.get(:mono_opt_out)


        # run any concerns that modify the command
        @before_execute.call cmd if @before_execute

        debug { "generating nuspec at #{nuspec}, and symbols (possibly) at '#{nuspec_symbols}' [nugets pack: task]" }

        # run the command for the file
        pkg, spkg = cmd.execute nuspec, nuspec_symbols

        publish_artifact nuspec, pkg
        publish_artifact nuspec_symbols, spkg if spkg && nuspec_symbols
      end

      ## Cleaning up after generation

      def cleanup_nuspec nuspec
        return if nuspec.nil? or not File.exists? nuspec
        return if @opts.get :leave_nuspec, false
        File.delete nuspec
      end

      def publish_artifact nuspec, nuget
        Albacore.publish :artifact, OpenStruct.new(
          :nuspec   => nuspec,
          :nupkg    => nuget,
          :location => nuget
        )
      end

      def self.accept? f
        File.extname(f).downcase != '.nuspec'
      end
    end

    # generate a nuget from a nuspec
    class NuspecTask
      include Logging

      def initialize command_line, config, nuspec
        @config = config
        @nuspec = nuspec
        # is a NuspecPack::Cmd
        @command_line = command_line
      end

      def read_version_from_nuspec
        begin
          nuspec_file = File.open(@nuspec)
          xml = Nokogiri::XML(nuspec_file)
          nuspec_file.close
          nodes = xml.xpath('.//metadata/version')
          raise "No <version/> found" if nodes.empty?
          nodes.first.text()
        rescue => error
          err "Error reading package version from file: #{error}"
          raise
        end
      end

      def execute
        version = read_version_from_nuspec
        filename = File.basename(@nuspec, File.extname(@nuspec))

        @command_line.execute @nuspec

        path = File.join(@config.opts.get(:out), "#{filename}.#{version}.nupkg")

        Albacore.publish :artifact, OpenStruct.new(
          :nuspec   => @nuspec,
          :nupkg    => path,
          :location => path
        )
      end

      def self.accept? file
        File.extname(file).downcase == '.nuspec'
      end
    end
  end
end