# *******************************************************************************
# OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# (1) Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# (2) Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# (3) Neither the name of the copyright holder nor the names of any contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission from the respective party.
#
# (4) Other than as required in clauses (1) and (2), distributions in any form
# of modifications or other derivative works may not use the "OpenStudio"
# trademark, "OS", "os", or any other confusingly similar designation without
# specific prior written permission from Alliance for Sustainable Energy, LLC.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
# UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
# THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# *******************************************************************************

require 'rake'
require 'rake/tasklib'
require 'rake/testtask'
require_relative '../extension'

module OpenStudio
  module Extension
    class RakeTask < Rake::TaskLib
      attr_accessor :name, :measures_dir, :core_dir, :doc_templates_dir, :files_dir

      def initialize(*args, &task_block)
        @name = args.shift || :openstudio

        setup_subtasks(@name)
      end

      def set_extension_class(extension_class, github_repo = '')
        @extension_class = extension_class
        @extension = extension_class.new
        @root_dir = @extension.root_dir
        # Catch if measures_dir is nil, then just make it an empty string
        @measures_dir = @extension.measures_dir || ''
        @staged_path = @measures_dir + '/staged'
        @core_dir = @extension.core_dir
        @doc_templates_dir = @extension.doc_templates_dir
        @files_dir = @extension.files_dir
        @github_repo = github_repo
      end

      private

      def setup_subtasks(name)
        namespace name do
          desc 'Run the CLI task to check for measure updates'
          task update_measures: ['measures:add_license', 'measures:add_readme', 'measures:copy_resources', 'update_copyright'] do
            puts 'updating measures'
            runner = OpenStudio::Extension::Runner.new(Dir.pwd)
            runner.update_measures(@measures_dir)
          end

          desc 'List measures'
          task :list_measures do
            puts 'Listing measures'
            runner = OpenStudio::Extension::Runner.new(Dir.pwd)
            runner.list_measures(@measures_dir)
          end

          desc 'Use openstudio system ruby to run tests'
          task :test_with_openstudio do
            puts 'testing with openstudio'
            runner = OpenStudio::Extension::Runner.new(Dir.pwd)
            result = runner.test_measures_with_cli(@measures_dir)

            if !result
              exit 1
            end
          end

          # namespace for measure operations
          namespace 'measures' do
            desc 'Copy the resources files to individual measures'
            task :copy_resources do
              # make sure we don't have conflicting resource file names
              OpenStudio::Extension.check_for_name_conflicts

              puts 'Copying resource files from the core library to individual measures'
              runner = OpenStudio::Extension::Runner.new(Dir.pwd)
              runner.copy_core_files(@measures_dir, @core_dir)
            end

            desc 'Add License File to measures'
            task :add_license do
              # copy license file
              puts 'Adding license file to measures'
              runner = OpenStudio::Extension::Runner.new(Dir.pwd)
              runner.add_measure_license(@measures_dir, @doc_templates_dir)
            end

            desc 'Add README.md.erb file if it and README.md do not already exist for a measure'
            task :add_readme do
              # copy README.md.erb file
              puts 'Adding README.md.erb to measures where it and README.md do not exist.'
              puts 'Only files that have actually been changed will be listed.'
              runner = OpenStudio::Extension::Runner.new(Dir.pwd)
              runner.add_measure_readme(@measures_dir, @doc_templates_dir)
            end
          end

          # namespace for anything runner related
          namespace 'runner' do
            desc 'Initialize a local runner.conf file to set custom runner parameters'
            task :init do
              puts 'Creating runner.conf'
              OpenStudio::Extension::RunnerConfig.init(Dir.pwd)
            end
          end

          desc 'Update copyright on files'
          task :update_copyright do
            # update copyright
            puts 'Updating COPYRIGHT in files'
            runner = OpenStudio::Extension::Runner.new(Dir.pwd)
            runner.update_copyright(@root_dir, @doc_templates_dir)
          end

          desc 'Print the change log from GitHub. Date format: yyyy-mm-dd'
          task :change_log, [:start_date, :end_date, :apikey] do |t, args|
            require 'change_log'
            cl = ChangeLog.new(@github_repo, *args)
            cl.process
            cl.print_issues
          end

          namespace 'bcl' do
            desc 'Test BCL login'
            task :test_login do
              puts 'test BCL login'
              bcl = ::BCL::ComponentMethods.new
              bcl.login
            end

            # for custom search, populate env var: bcl_search_keyword
            desc 'Search BCL'
            task :search_measures do
              puts 'test search BCL'
              bcl = ::BCL::ComponentMethods.new
              bcl.login

              # check for env var specifying keyword first
              if ENV['bcl_search_keyword']
                keyword = ENV['bcl_search_keyword']
              else
                keyword = 'Space'
              end
              num_results = 10
              # bcl.search params: search_string, filter_string, return_all_results?
              puts "searching BCL measures for keyword: #{keyword}"
              results = bcl.search(keyword, "fq[]=bundle:nrel_measure&show_rows=#{num_results}", false)
              puts "there are #{results[:result].count} results"
              results[:result].each do |res|
                puts(res[:measure][:name]).to_s
              end
            end

            # to call with argument: "openstudio:bcl:stage[true]" (true = remove existing staged content)
            desc 'Copy the measures/components to a location that can be uploaded to BCL'
            task :stage, [:reset] do |t, args|
              puts 'Staging measures for BCL'
              # initialize BCL and login
              bcl = ::BCL::ComponentMethods.new
              bcl.login

              # process reset options: true to clear out old staged content
              options = { reset: false }
              if args[:reset].to_s == 'true'
                options[:reset] = true
              end

              # ensure staged dir exists
              FileUtils.mkdir_p(@staged_path)

              # delete existing tarballs if reset is true
              if options[:reset]
                puts 'Deleting existing staged content'
                FileUtils.rm_rf(Dir.glob("#{@staged_path}/*"))
              end

              # create new and existing directories
              FileUtils.mkdir_p(@staged_path.to_s + '/update')
              FileUtils.mkdir_p(@staged_path.to_s + '/push/component')
              FileUtils.mkdir_p(@staged_path.to_s + '/push/measure')

              # keep track of noop, update, push
              noops = 0
              new_ones = 0
              updates = 0

              # get all content directories to process
              dirs = Dir.glob("#{@measures_dir}/*")

              dirs.each do |dir|
                next if dir.include?('Rakefile') || File.basename(dir) == 'staged'
                current_d = Dir.pwd
                content_name = File.basename(dir)
                puts '', '---'
                puts "Generating #{content_name}"

                Dir.chdir(dir)

                # figure out whether to upload new or update existing
                files = Pathname.glob('**/*')
                uuid = nil
                vid = nil
                content_type = 'measure'

                paths = []
                files.each do |file|
                  # don't tar tests/outputs directory
                  next if file.to_s.start_with?('tests/output') # From measure testing process
                  next if file.to_s.start_with?('tests/test') # From openstudio-measure-tester-gem
                  next if file.to_s.start_with?('tests/coverage') # From openstudio-measure-tester-gem
                  next if file.to_s.start_with?('test_results') # From openstudio-measure-tester-gem
                  paths << file.to_s
                  if file.to_s =~ /^.{0,2}component.xml$/ || file.to_s =~ /^.{0,2}measure.xml$/
                    if file.to_s.match?(/^.{0,2}component.xml$/)
                      content_type = 'component'
                    end
                    # extract uuid  and vid
                    uuid, vid = bcl.uuid_vid_from_xml(file)
                  end
                end
                puts "UUID: #{uuid}, VID: #{vid}"

                # note: if uuid is missing, will assume new content
                action = bcl.search_by_uuid(uuid, vid)
                puts "#{content_name} ACTION TO TAKE: #{action}"
                # new content functionality needs to know if measure or component.  update is agnostic.
                if action == 'noop' # ignore up-to-date content
                  puts "  - WARNING: local #{content_name} uuid and vid match BCL... no update will be performed"
                  noops += 1
                  next
                elsif action == 'update'
                  # puts "#{content_name} labeled as update for BCL"
                  destination = @staged_path + '/' + action + '/' + "#{content_name}.tar.gz"
                  updates += 1
                elsif action == 'push'
                  # puts "#{content_name} labeled as new content for BCL"
                  destination = @staged_path + '/' + action + '/' + content_type + "/#{content_name}.tar.gz"
                  new_ones += 1
                end

                puts "destination: #{destination}"

                # copy over only if 'reset_receipts' is set to TRUE. otherwise ignore if file exists already
                if File.exist?(destination)
                  if reset
                    FileUtils.rm(destination)
                    ::BCL.tarball(destination, paths)
                  else
                    puts "*** WARNING: File #{content_name}.tar.gz already exists in staged directory... keeping existing file. To overwrite, set reset_receipts arg to true ***"
                  end
                else
                  ::BCL.tarball(destination, paths)
                end
                Dir.chdir(current_d)
              end
              puts '', "****STAGING DONE**** #{new_ones} new content, #{updates} updates, #{noops} skipped (already up-to-date on BCL)", ''
            end

            desc 'Upload measures from the specified location.'
            task :push do
              puts 'Push measures to BCL'

              # initialize BCL and login
              bcl = ::BCL::ComponentMethods.new
              bcl.login
              reset = false

              total_count = 0
              successes = 0
              errors = 0
              skipped = 0

              # grab all the new measure and component tar files and push to bcl
              ['measure', 'component'].each do |content_type|
                items = []
                paths = Pathname.glob(@staged_path.to_s + "/push/#{content_type}/*.tar.gz")
                paths.each do |path|
                  # puts path
                  items << path.to_s
                end

                items.each do |item|
                  puts item.split('/').last
                  total_count += 1

                  receipt_file = File.dirname(item) + '/' + File.basename(item, '.tar.gz') + '.receipt'
                  if !reset && File.exist?(receipt_file)
                    skipped += 1
                    puts 'SKIP: receipt file found'
                    next
                  end

                  valid, res = bcl.push_content(item, true, "nrel_#{content_type}")
                  if valid
                    successes += 1
                  else
                    errors += 1
                    if res.key?(:error)
                      puts "  ERROR MESSAGE: #{res[:error]}"
                    else
                      puts "ERROR: #{res.inspect.chomp}"
                    end
                  end
                  puts '', '---'
                end
              end

              # grab all the updated content (measures and components) tar files and push to bcl
              items = []
              paths = Pathname.glob(@staged_path.to_s + '/update/*.tar.gz')
              paths.each do |path|
                # puts path
                items << path.to_s
              end
              items.each do |item|
                puts item.split('/').last
                total_count += 1

                receipt_file = File.dirname(item) + '/' + File.basename(item, '.tar.gz') + '.receipt'
                if !reset && File.exist?(receipt_file)
                  skipped += 1
                  puts 'SKIP: receipt file found'
                  next
                end

                valid, res = bcl.update_content(item, true)
                if valid
                  successes += 1
                else
                  errors += 1
                  if res.key?(:error)
                    puts "  ERROR MESSAGE: #{res[:error]}"
                  else
                    puts "ERROR MESSAGE: #{res.inspect.chomp}"
                  end
                end
                puts '', '---'
              end

              puts "****UPLOAD DONE**** #{total_count} total, #{successes} success, #{errors} failures, #{skipped} skipped"
            end
          end
        end
      end
    end
  end
end