# *******************************************************************************
# Honeybee OpenStudio Gem, Copyright (c) 2020, Alliance for Sustainable
# Energy, LLC, Ladybug Tools 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:
#
# (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.
#
# 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 'honeybee/simulation/parameter_model'

require 'openstudio'

module Honeybee
  class SimulationParameter
    @@standard_mapper = {
      DOE_Ref_Pre_1980: 'DOE Ref Pre-1980',
      DOE_Ref_1980_2004: 'DOE Ref 1980-2004',
      ASHRAE_2004: '90.1-2004',
      ASHRAE_2007: '90.1-2007',
      ASHRAE_2010: '90.1-2010',
      ASHRAE_2013: '90.1-2013',
      ASHRAE_2016: '90.1-2016',
      ASHRAE_2019: '90.1-2019'
    }

    # convert to openstudio model, clears errors and warnings
    def to_openstudio_model(openstudio_model=nil, log_report=false)
      @errors = []
      @warnings = []
      $bypass_eff_sizing = false  # default value to bypass efficiency standard

      if log_report
        puts 'Starting SimulationParameter translation from Honeybee to OpenStudio'
      end
      @openstudio_model = if openstudio_model
                            openstudio_model
                          else
                            OpenStudio::Model::Model.new
                          end

      create_openstudio_objects

      if log_report
        puts 'Done with SimulationParameter translation!'
      end

      @openstudio_model
    end

    def create_openstudio_objects
      # get the defaults for each sub-object
      simct_defaults = defaults[:SimulationControl][:properties]
      shdw_defaults = defaults[:ShadowCalculation][:properties]
      siz_defaults = defaults[:SizingParameter][:properties]
      out_defaults = defaults[:SimulationOutput][:properties]
      runper_defaults = defaults[:RunPeriod][:properties]
      simpar_defaults = defaults[:SimulationParameter][:properties]

      # set defaults for the Model's SimulationControl object
      os_sim_control = @openstudio_model.getSimulationControl
      os_sim_control.setDoZoneSizingCalculation(simct_defaults[:do_zone_sizing][:default])
      os_sim_control.setDoSystemSizingCalculation(simct_defaults[:do_system_sizing][:default])
      os_sim_control.setDoPlantSizingCalculation(simct_defaults[:do_plant_sizing][:default])
      os_sim_control.setRunSimulationforWeatherFileRunPeriods(simct_defaults[:run_for_run_periods][:default])
      os_sim_control.setRunSimulationforSizingPeriods(simct_defaults[:run_for_sizing_periods][:default])
      os_sim_control.setSolarDistribution(shdw_defaults[:solar_distribution][:default])

      # override any SimulationControl defaults with lodaded JSON
      if @hash[:simulation_control]
        unless @hash[:simulation_control][:do_zone_sizing].nil?
          os_sim_control.setDoZoneSizingCalculation(@hash[:simulation_control][:do_zone_sizing])
        end
        unless @hash[:simulation_control][:do_system_sizing].nil?
          os_sim_control.setDoSystemSizingCalculation(@hash[:simulation_control][:do_system_sizing])
        end
        unless @hash[:simulation_control][:do_plant_sizing].nil?
          os_sim_control.setDoPlantSizingCalculation(@hash[:simulation_control][:do_plant_sizing])
        end
        unless @hash[:simulation_control][:run_for_run_periods].nil?
          os_sim_control.setRunSimulationforWeatherFileRunPeriods(@hash[:simulation_control][:run_for_run_periods])
        end
        unless @hash[:simulation_control][:run_for_sizing_periods].nil?
          os_sim_control.setRunSimulationforSizingPeriods(@hash[:simulation_control][:run_for_sizing_periods])
        end
      end

      # set defaults for the Model's ShadowCalculation object
      os_shadow_calc = @openstudio_model.getShadowCalculation
      os_shadow_calc.setShadingCalculationMethod(
        shdw_defaults[:calculation_method][:default])
      os_shadow_calc.setShadingCalculationUpdateFrequencyMethod(
        shdw_defaults[:calculation_update_method][:default])
      os_shadow_calc.setShadingCalculationUpdateFrequency(
        shdw_defaults[:calculation_frequency][:default])
      os_shadow_calc.setMaximumFiguresInShadowOverlapCalculations(
        shdw_defaults[:maximum_figures][:default])

      # override any ShadowCalculation defaults with lodaded JSON
      if @hash[:shadow_calculation]
        if @hash[:shadow_calculation][:calculation_method]
          os_shadow_calc.setShadingCalculationMethod(
            @hash[:shadow_calculation][:calculation_method])
        end
        if @hash[:shadow_calculation][:calculation_update_method]
          os_shadow_calc.setShadingCalculationUpdateFrequencyMethod(
            @hash[:shadow_calculation][:calculation_update_method])
        end
        if @hash[:shadow_calculation][:calculation_frequency]
          os_shadow_calc.setShadingCalculationUpdateFrequency(
            @hash[:shadow_calculation][:calculation_frequency])
        end
        if @hash[:shadow_calculation][:maximum_figures]
          os_shadow_calc.setMaximumFiguresInShadowOverlapCalculations(
            @hash[:shadow_calculation][:maximum_figures])
        end
        if @hash[:shadow_calculation][:solar_distribution]
          os_sim_control.setSolarDistribution(
            @hash[:shadow_calculation][:solar_distribution])
        end
      end

      # set defaults for the Model's SizingParameter object
      os_sizing_par = @openstudio_model.getSizingParameters
      os_sizing_par.setHeatingSizingFactor(siz_defaults[:heating_factor][:default])
      os_sizing_par.setCoolingSizingFactor(siz_defaults[:cooling_factor][:default])

      # override any SizingParameter defaults with lodaded JSON
      db_temps = []
      if @hash[:sizing_parameter]
        if @hash[:sizing_parameter][:heating_factor]
          os_sizing_par.setHeatingSizingFactor(@hash[:sizing_parameter][:heating_factor])
        end
        if @hash[:sizing_parameter][:cooling_factor]
          os_sizing_par.setCoolingSizingFactor(@hash[:sizing_parameter][:cooling_factor])
        end
        # set any design days
        if @hash[:sizing_parameter][:design_days]
          if @hash[:simulation_control][:do_zone_sizing].nil? || @hash[:simulation_control][:do_zone_sizing] == true
            @hash[:sizing_parameter][:design_days].each do |des_day|
              des_day_object = DesignDay.new(des_day)
              os_des_day = des_day_object.to_openstudio(@openstudio_model)
              db_temps << des_day[:dry_bulb_condition][:dry_bulb_max]
            end
          end
        end
      end

      # use the average of design day temperatures to set the water mains temperature
      os_water_mains = @openstudio_model.getSiteWaterMainsTemperature
      os_version_water_fix = OpenStudio::VersionString.new(3, 4)
      if @openstudio_model.version() >= os_version_water_fix
        os_water_mains.setCalculationMethod('CorrelationFromWeatherFile')
      else
        os_water_mains.setCalculationMethod('Correlation')
        if db_temps.length > 0
          os_water_mains.setAnnualAverageOutdoorAirTemperature((db_temps.max + db_temps.min) / 2)
        else  # just use some dummy values so that the simulation does not fail
          os_water_mains.setAnnualAverageOutdoorAirTemperature(12)
        end
        os_water_mains.setMaximumDifferenceInMonthlyAverageOutdoorAirTemperatures(4)
      end

      # set the climate zone from design days assuming 0.4% extremes and normal distribution
      climate_zone_objs = @openstudio_model.getClimateZones
      ashrae_zones = climate_zone_objs.getClimateZones('ASHRAE')
      if ashrae_zones.empty? && db_temps.length > 0
        # generate temperatures according to a normal distribution
        mean_temp = (db_temps.max + db_temps.min) / 2
        dist_to_mean = db_temps.max - mean_temp
        st_dev = dist_to_mean / 2.65
        vals = []
        for i in 0..4379
          step_seed = i.to_f / 4380
          add_val1, add_val2 = gaussian(mean_temp, st_dev, step_seed)
          vals << add_val1
          vals << add_val2
        end

        # compute the number of heating and cooling degree days
        cooling_deg_days, heating_deg_days = 0, 0
        vals.each do |temp|
          if temp > 10
            cdd = (temp - 10) / 24
            cooling_deg_days += cdd
          end
          if temp < 18
            hdd = (18 - temp) / 24
            heating_deg_days += hdd
          end
        end

        # determine the climate zone from the degree-day distribution
        if cooling_deg_days > 5000
          cz = '1'
        elsif cooling_deg_days > 3500
          cz = '2A'
        elsif cooling_deg_days > 2500
          cz = '3A'
        elsif cooling_deg_days <= 2500 and heating_deg_days <= 2000
          cz = '3C'
        elsif cooling_deg_days <= 2500 and heating_deg_days <= 3000
          cz = '4A'
        elsif heating_deg_days <= 3000
          cz = '4C'
        elsif heating_deg_days <= 4000
          cz = '5A'
        elsif heating_deg_days <= 5000
          cz = '6A'
        elsif heating_deg_days <= 7000
          cz = '7'
        else
            cz = '8'
        end

        # set the climate zone
        climate_zone_objs.setClimateZone('ASHRAE', cz)
      end

      # note any efficency standards that have been assigned to the 
      if @hash[:sizing_parameter]
        if @hash[:sizing_parameter][:efficiency_standard] && @hash[:sizing_parameter][:efficiency_standard] != 0
          std_gem_standard = @@standard_mapper[@hash[:sizing_parameter][:efficiency_standard].to_sym]
          building = @openstudio_model.getBuilding
          building.setStandardsTemplate(std_gem_standard)
        end
        if @hash[:sizing_parameter][:climate_zone] && @hash[:sizing_parameter][:climate_zone] != 0
          climate_zone_objs.setClimateZone('ASHRAE', @hash[:sizing_parameter][:climate_zone])
        end
        if @hash[:sizing_parameter][:building_type] && @hash[:sizing_parameter][:building_type] != 0
          building = @openstudio_model.getBuilding
          building.setStandardsBuildingType(@hash[:sizing_parameter][:building_type])
        end
        unless @hash[:sizing_parameter][:bypass_efficiency_sizing].nil?
          $bypass_eff_sizing = @hash[:sizing_parameter][:bypass_efficiency_sizing]
        end
      end

      # set defaults for the simulation output
      os_unmet_tol = @openstudio_model.getOutputControlReportingTolerances
      default_unmet_tol = out_defaults[:unmet_setpoint_tolerance][:default]
      os_unmet_tol.setToleranceforTimeHeatingSetpointNotMet(default_unmet_tol)
      os_unmet_tol.setToleranceforTimeCoolingSetpointNotMet(default_unmet_tol)

      # set Outputs for the simulation
      if @hash[:output]
        if @hash[:output][:outputs]
          @hash[:output][:outputs].each do |output|
            os_output = OpenStudio::Model::OutputVariable.new(output, @openstudio_model)
            if @hash[:output][:reporting_frequency]
              os_output.setReportingFrequency(@hash[:output][:reporting_frequency])
            else
              os_output.setReportingFrequency(out_defaults[:reporting_frequency][:default])
            end
          end
        end
        if @hash[:output][:summary_reports]
          os_report = @openstudio_model.getOutputTableSummaryReports
          @hash[:output][:summary_reports].each do |report|
            os_report.addSummaryReport(report)
          end
        end
        if @hash[:output][:unmet_setpoint_tolerance]
          unmet_tol = @hash[:output][:unmet_setpoint_tolerance]
          os_unmet_tol.setToleranceforTimeHeatingSetpointNotMet(unmet_tol)
          os_unmet_tol.setToleranceforTimeCoolingSetpointNotMet(unmet_tol)
        end
      end

      # set defaults for the year description
      year_description = @openstudio_model.getYearDescription
      year_description.setDayofWeekforStartDay(runper_defaults[:start_day_of_week][:default])

      # set up the simulation RunPeriod
      if @hash[:run_period]
        # set the leap year
        if @hash[:run_period][:leap_year]
          year_description.setIsLeapYear(@hash[:run_period][:leap_year])
        end

        # set the start day of the week
        if @hash[:run_period][:start_day_of_week]
          year_description.setDayofWeekforStartDay(@hash[:run_period][:start_day_of_week])
        end

        # set the run preiod start and end dates
        openstudio_runperiod = @openstudio_model.getRunPeriod
        openstudio_runperiod.setBeginMonth(@hash[:run_period][:start_date][0])
        openstudio_runperiod.setBeginDayOfMonth(@hash[:run_period][:start_date][1])
        openstudio_runperiod.setEndMonth(@hash[:run_period][:end_date][0])
        openstudio_runperiod.setEndDayOfMonth(@hash[:run_period][:end_date][1])

        # set the daylight savings time
        if @hash[:run_period][:daylight_saving_time]
          os_dl_saving = @openstudio_model.getRunPeriodControlDaylightSavingTime
          os_dl_saving.setStartDate(
            OpenStudio::MonthOfYear.new(@hash[:run_period][:daylight_saving_time][:start_date][0]),
            @hash[:run_period][:daylight_saving_time][:start_date][1])
          os_dl_saving.setEndDate(
            OpenStudio::MonthOfYear.new(@hash[:run_period][:daylight_saving_time][:end_date][0]),
            @hash[:run_period][:daylight_saving_time][:end_date][1])
        end

        # Set the holidays
        if @hash[:run_period][:holidays]
          @hash[:run_period][:holidays].each do |hol|
            os_hol = OpenStudio::Model::RunPeriodControlSpecialDays.new(
              OpenStudio::MonthOfYear.new(hol[0]), hol[1], @openstudio_model)
            os_hol.setDuration(1)
            os_hol.setSpecialDayType('Holiday')
          end
        end
      end

      # set the simulation timestep
      os_timestep = @openstudio_model.getTimestep
      if @hash[:timestep]
        os_timestep.setNumberOfTimestepsPerHour(@hash[:timestep])
      else
        os_timestep.setNumberOfTimestepsPerHour(simpar_defaults[:timestep][:default])
      end

      # assign the north
      if @hash[:north_angle]
        @openstudio_model.getBuilding.setNorthAxis(@hash[:north_angle])
      end

      # assign the terrain
      os_site = @openstudio_model.getSite
      os_site.setTerrain(simpar_defaults[:terrain_type][:default])
      if @hash[:terrain_type]
        os_site.setTerrain(@hash[:terrain_type])
      end

    end

    def gaussian(mean, stddev, seed)
      # generate a gaussian distribution of values
      theta = 2 * Math::PI * seed
      rho = Math.sqrt(-2 * Math.log(1 - seed))
      scale = stddev * rho
      x = mean + scale * Math.cos(theta)
      y = mean + scale * Math.sin(theta)
      return x, y
    end

  end #SimulationParameter
end #Honeybee