# *********************************************************************************
# URBANopt (tm), Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
# contributors. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# 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.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 OR CONTRIBUTORS 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 'urbanopt/scenario/scenario_runner_base'
require 'json'

require 'fileutils'
# require 'hash_parser'

module URBANopt
  module Scenario
    class ScenarioRunnerOSW < ScenarioRunnerBase
      ##
      # ScenarioRunnerOSW is a class to create and run SimulationFileOSWs
      ##
      def initialize; end

      ##
      # Create all OSWs for Scenario.
      ##
      # [parameters:]
      # * +scenario+ - _ScenarioBase_ - Scenario to create simulation input files for.
      # * +force_clear+ - _Bool_ - Clear Scenario before creating simulation input files.
      ##
      # [return:] _Array_ Returns array of all SimulationDirs, even those created previously, for Scenario.
      def create_simulation_files(scenario, force_clear = false)
        if force_clear
          scenario.clear
        end

        FileUtils.mkdir_p(scenario.run_dir) if !File.exist?(scenario.run_dir)
        FileUtils.rm_rf(File.join(scenario.run_dir, 'run_status.json')) if File.exist?(File.join(scenario.run_dir, 'run_status.json'))

        simulation_dirs = scenario.simulation_dirs

        simulation_dirs.each do |simulation_dir|
          if simulation_dir.out_of_date?
            puts "simulation_dir #{simulation_dir.run_dir} is out of date, regenerating input files"
            simulation_dir.create_input_files
          end
        end
        return simulation_dirs
      end

      ##
      # Create and run all SimulationFileOSW for Scenario.
      # A staged runner is implented to run buildings, then transformers then district systems.
      # - instantiate openstudio runner to run .osw files.
      # - create simulation files for this scenario.
      # - get feature_type value from in.osw files
      # - cretae 3 groups to store .osw files (+building_osws+ , +transformer_osws+ , +district_system_osws+)
      # - add each osw file to its corresponding group id +simulation_dir+ is out_of_date
      # - Run osw file groups in order and store simulation failure in a array.
      ##
      # [parameters:]
      # * +scenario+ - _ScenarioBase_ - Scenario to create and run SimulationFiles for.
      # * +force_clear+ - _Bool_ - Clear Scenario before creating SimulationFiles.
      ##
      # [return:] _Array_ Returns array of all SimulationFiles, even those created previously, for Scenario.
      def run(scenario, force_clear = false, options = {})
        # instantiate openstudio runner - use the defaults for now. If need to change then create
        # the runner.conf file (i.e. run `rake openstudio:runner:init`)
        # allow passing gemfile_path and bundle_install_path in options
        runner = OpenStudio::Extension::Runner.new(scenario.root_dir, [], options)

        # create simulation files
        simulation_dirs = create_simulation_files(scenario, force_clear)

        # osws = []
        # simulation_dirs.each do |simulation_dir|
        #   if !simulation_dir.is_a?(SimulationDirOSW)
        #     raise "ScenarioRunnerOSW does not know how to run #{simulation_dir.class}"
        #   end
        #   if simulation_dir.out_of_date?
        #     osws << simulation_dir.in_osw_path
        #   end
        # end

        # cretae 3 groups to store .osw files (+building_osws+ , +transformer_osws+ , +district_system_osws+)
        building_osws = []
        transformer_osws = []
        district_system_osws = []

        simulation_dirs.each do |simulation_dir|
          in_osw = File.read(simulation_dir.in_osw_path)
          in_osw_hash = JSON.parse(in_osw, symbolize_names: true)

          if !simulation_dir.is_a?(SimulationDirOSW)
            raise "ScenarioRunnerOSW does not know how to run #{simulation_dir.class}"
          end

          # get feature_type value from in.osw files
          feature_type = nil
          in_osw_hash[:steps].each { |x| feature_type = x[:arguments][:feature_type] if x[:arguments][:feature_type] }

          # add each osw file to its corresponding group id +simulation_dir+ is out_of_date
          if simulation_dir.out_of_date?

            if feature_type == 'Building'
              building_osws << simulation_dir.in_osw_path
            elsif feature_type == 'District System'
              district_system_osws << simulation_dir.in_osw_path
            elsif feature_type == 'Transformer'
              transformer_osws << simulation_dir.in_osw_path
            else
              raise "ScenarioRunnerOSW does not know how to run a #{feature_type} feature"
            end

          end
        end

        # Run osw groups in order and store simulation failure in an array.
        # Return simulation_dirs after running all simulations.

        # failures
        failures = []
        # run building_osws
        # building_failures = runner.run_osws(building_osws, num_parallel = Extension::NUM_PARALLEL, max_to_run = Extension::MAX_DATAPOINTS)
        building_failures = runner.run_osws(building_osws)
        failures + building_failures
        # run district_system_osws
        # district_system_failures = runner.run_osws(district_system_osws, num_parallel = Extension::NUM_PARALLEL, max_to_run = Extension::MAX_DATAPOINTS)
        district_system_failures = runner.run_osws(district_system_osws)
        failures + district_system_failures
        # run transformer_osws
        # transformer_failures = runner.run_osws(transformer_osws, num_parallel = Extension::NUM_PARALLEL, max_to_run = Extension::MAX_DATAPOINTS)
        transformer_failures = runner.run_osws(transformer_osws)
        failures + transformer_failures

        puts 'Done Running Scenario'

        # if failures.size > 0
        #   puts "DATAPOINT FAILURES: #{failures}"
        # end

        # write results to file and to command line
        get_results(scenario, simulation_dirs)

        return simulation_dirs
      end

      def get_results(scenario, simulation_dirs)
        # look for other failed datapoints (command line display)
        # also compile datapoint status for latest_run.json file
        status_arr = []
        failed_sims = []
        simulation_dirs.each do |sim_dir|
          if File.exist?(sim_dir.failed_job_path)
            failed_sims << sim_dir.run_dir.split('/')[-1]
          end
          status_arr << { "id": sim_dir.feature_id, "status": sim_dir.simulation_status, "mapper_class": sim_dir.mapper_class }
        end

        # write to file
        File.open(File.join(scenario.run_dir, 'run_status.json'), 'w') { |f| f.write JSON.pretty_generate("timestamp": Time.now.strftime('%Y-%m-%dT%k:%M:%S.%L'), "results": status_arr) }

        if !failed_sims.empty?
          puts "FAILED SIMULATION IDs: #{failed_sims.join(',')}"
        end
      end
    end
  end
end