# *******************************************************************************
# 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.
# *******************************************************************************

# start the measure
class ExteriorWallThermalPropertiesMultiplier < OpenStudio::Measure::ModelMeasure
  # define the name that a user will see
  def name
    'Exterior Wall Thermal Properties Multiplier'
  end

  # human readable description
  def description
    'Change exterior walls by altering the thermal resistance, density, and solar absorptance of the wall constructions by a Multiplier'
  end

  # human readable description of modeling approach
  def modeler_description
    'Change exterior walls by altering the thermal resistance, density, and solar absorptance of the wall constructions by a Multiplier'
  end

  # short def to make numbers pretty (converts 4125001.25641 to 4,125,001.26 or 4,125,001). The definition be called through this measure
  def neat_numbers(number, roundto = 2) # round to 0 or 2)
    number = if roundto == 2
               format '%.2f', number
             else
               number.round
             end
    # regex to add commas
    number.to_s.reverse.gsub(/([0-9]{3}(?=([0-9])))/, '\\1,').reverse
  end # end def neat_numbers

  # helper to make it easier to do unit conversions on the fly
  def unit_helper(number, from_unit_string, to_unit_string)
    OpenStudio.convert(OpenStudio::Quantity.new(number, OpenStudio.createUnit(from_unit_string).get), OpenStudio.createUnit(to_unit_string).get).get.value
  end

  def check_multiplier(runner, multiplier)
    if multiplier < 0
      runner.registerError("Multiplier #{multiplier} cannot be negative.")
      false
    end
  end

  # define the arguments that the user will input
  def arguments(_model)
    args = OpenStudio::Measure::OSArgumentVector.new

    # make an argument insulation R-value
    r_value_mult = OpenStudio::Measure::OSArgument.makeDoubleArgument('r_value_mult', true)
    r_value_mult.setDisplayName('Exterior wall total R-value multiplier')
    r_value_mult.setDefaultValue(1)
    args << r_value_mult

    solar_abs_mult = OpenStudio::Measure::OSArgument.makeDoubleArgument('solar_abs_mult', true)
    solar_abs_mult.setDisplayName('Exterior wall solar absorptance multiplier')
    solar_abs_mult.setDefaultValue(1)
    args << solar_abs_mult

    thermal_mass_mult = OpenStudio::Measure::OSArgument.makeDoubleArgument('thermal_mass_mult', true)
    thermal_mass_mult.setDisplayName('Exterior wall thermal mass multiplier')
    thermal_mass_mult.setDefaultValue(1)
    args << thermal_mass_mult

    args
  end # end the arguments method

  # define what happens when the measure is run
  def run(model, runner, user_arguments)
    super(model, runner, user_arguments)

    # use the built-in error checking
    unless runner.validateUserArguments(arguments(model), user_arguments)
      return false
    end

    # assign the user inputs to variables
    r_value_mult = runner.getDoubleArgumentValue('r_value_mult', user_arguments)
    check_multiplier(runner, r_value_mult)
    solar_abs_mult = runner.getDoubleArgumentValue('solar_abs_mult', user_arguments)
    check_multiplier(runner, solar_abs_mult)
    thermal_mass_mult = runner.getDoubleArgumentValue('thermal_mass_mult', user_arguments)
    check_multiplier(runner, thermal_mass_mult)

    # create an array of exterior surfaces and construction types
    surfaces = model.getSurfaces
    exterior_surfaces = []
    exterior_surface_constructions = []
    surfaces.each do |surface|
      next unless surface.outsideBoundaryCondition == 'Outdoors' && surface.surfaceType == 'Wall'
      exterior_surfaces << surface
      exterior_surface_const = surface.construction.get
      # only add construction if it hasn't been added yet
      unless exterior_surface_constructions.include?(exterior_surface_const)
        exterior_surface_constructions << exterior_surface_const.to_Construction.get
      end
    end

    # nothing will be done if there are no exterior surfaces
    if exterior_surfaces.empty?
      runner.registerAsNotApplicable('Model does not have any exterior walls.')
      return true
    end

    # get initial number of surfaces having each construction type
    initial_condition_string = 'Initial number of surfaces of each construction type: '
    exterior_surface_construction_numbers = []
    exterior_surface_constructions.each_with_index do |construction, index|
      exterior_surface_construction_numbers[index] = 0
      initial_condition_string << "'#{construction.name}': "
      exterior_surfaces.each do |surface|
        exterior_surface_construction_numbers[index] += 1 if surface.construction.get.handle.to_s == construction.handle.to_s
      end
      initial_condition_string << "#{exterior_surface_construction_numbers[index]}, "
    end

    runner.registerInitialCondition(initial_condition_string)

    # get initial sets of construction layers and desired values
    initial_layers = []
    initial_r_val = []
    initial_sol_abs = []
    initial_thm_mass = []
    initial_r_val_d = []
    initial_sol_abs_d = []
    initial_thm_mass_d = []
    exterior_surface_constructions.each_with_index do |_construction, con_index|
      initial_layers[con_index] = exterior_surface_constructions[con_index].layers
      initial_sol_abs[con_index] = initial_layers[con_index][0].to_StandardOpaqueMaterial.get.solarAbsorptance
      initial_r_val[con_index] = []
      initial_thm_mass[con_index] = []
      initial_sol_abs_d[con_index] = neat_numbers(initial_layers[con_index][0].to_StandardOpaqueMaterial.get.solarAbsorptance)
      initial_r_val_d[con_index] = []
      initial_thm_mass_d[con_index] = []
      initial_layers[con_index].each_with_index do |layer, lay_index|
        initial_r_val[con_index][lay_index] = initial_layers[con_index][lay_index].to_OpaqueMaterial.get.thermalResistance
        initial_thm_mass[con_index][lay_index] = initial_layers[con_index][lay_index].to_StandardOpaqueMaterial.get.density if layer.to_StandardOpaqueMaterial.is_initialized
        initial_r_val_d[con_index][lay_index] = neat_numbers(initial_layers[con_index][lay_index].to_OpaqueMaterial.get.thermalResistance) if layer.to_OpaqueMaterial.is_initialized
        initial_thm_mass_d[con_index][lay_index] = neat_numbers(initial_layers[con_index][lay_index].to_StandardOpaqueMaterial.get.density) if layer.to_StandardOpaqueMaterial.is_initialized
      end
    end
    initial_r_val_units = 'm^2*K/W'
    initial_thm_mass_units = 'kg/m3'

    # calculate desired values for each construction and layer
    desired_r_val = []
    desired_sol_abs = []
    desired_thm_mass = []
    initial_r_val.each_index do |index1|
      desired_r_val[index1] = []
      initial_r_val[index1].each_index do |index2|
        desired_r_val[index1][index2] = initial_r_val[index1][index2] * r_value_mult if initial_r_val[index1][index2]
      end
    end
    initial_sol_abs.each_index do |index1|
      next unless initial_sol_abs[index1]
      desired_sol_abs[index1] = initial_sol_abs[index1] * solar_abs_mult
      if desired_sol_abs[index1] > 1
        desired_sol_abs[index1] = 1
        runner.registerWarning("Initial solar absorptance of '#{initial_layers[index1][0].name}' was #{initial_sol_abs[index1]}. Multiplying it by #{solar_abs_mult} results in a number greater than 1, which is outside the allowed range. The value is instead being set to #{desired_sol_abs[index1]}")
      elsif desired_sol_abs[index1] < 0
        desired_sol_abs[index1] = 0
        runner.registerWarning("Initial solar absorptance of '#{initial_layers[index1][0].name}' was #{initial_sol_abs[index1]}. Multiplying it by #{solar_abs_mult} results in a number less than 0, which is outside the allowed range. The value is instead being set to #{desired_sol_abs[index1]}")
      end
    end
    initial_thm_mass.each_index do |index1|
      desired_thm_mass[index1] = []
      initial_thm_mass[index1].each_index do |index2|
        desired_thm_mass[index1][index2] = initial_thm_mass[index1][index2] * thermal_mass_mult if initial_thm_mass[index1][index2]
      end
    end

    # initalize final values arrays
    final_construction = []
    final_r_val = []
    final_sol_abs = []
    final_thm_mass = []
    final_r_val_d = []
    final_sol_abs_d = []
    final_thm_mass_d = []
    initial_r_val.each_with_index { |_, index| final_r_val[index] = [] }
    initial_thm_mass.each_with_index { |_, index| final_thm_mass[index] = [] }
    initial_r_val_d.each_with_index { |_, index| final_r_val_d[index] = [] }
    initial_thm_mass_d.each_with_index { |_, index| final_thm_mass_d[index] = [] }

    # replace exterior surface wall constructions
    exterior_surface_constructions.each_with_index do |construction, con_index|
      # create and name new construction
      new_construction = construction.clone
      new_construction = new_construction.to_Construction.get
      new_construction.setName("#{construction.name} (R #{r_value_mult.round(1)}x Solar #{solar_abs_mult.round(1)}x Therm #{thermal_mass_mult.round(1)}x)")
      # replace layers in new construction
      new_construction.layers.each_with_index do |layer, lay_index|
        new_layer = layer.clone
        new_layer = new_layer.to_Material.get
        # update thermal properties for the layer based on desired arrays
        new_layer.to_StandardOpaqueMaterial.get.setSolarAbsorptance(desired_sol_abs[con_index]) if lay_index == 0 && layer.to_StandardOpaqueMaterial.is_initialized # only apply to outer surface
        new_layer.to_OpaqueMaterial.get.setThermalResistance(desired_r_val[con_index][lay_index]) if layer.to_OpaqueMaterial.is_initialized
        new_layer.to_StandardOpaqueMaterial.get.setDensity(desired_thm_mass[con_index][lay_index]) if layer.to_StandardOpaqueMaterial.is_initialized && desired_thm_mass[con_index][lay_index] != 0
        new_layer.setName("#{layer.name} (R #{r_value_mult.round(1)}x Solar #{solar_abs_mult.round(1)}x Therm #{thermal_mass_mult.round(1)}x)")
        new_construction.setLayer(lay_index, new_layer)
        # calculate properties of new layer and output nice names
        final_r_val[con_index][lay_index] = new_construction.layers[lay_index].to_OpaqueMaterial.get.thermalResistance if layer.to_OpaqueMaterial.is_initialized
        final_sol_abs[con_index] = new_construction.layers[lay_index].to_StandardOpaqueMaterial.get.getSolarAbsorptance.value if lay_index == 0 && layer.to_StandardOpaqueMaterial.is_initialized
        final_thm_mass[con_index][lay_index] = new_construction.layers[lay_index].to_StandardOpaqueMaterial.get.getDensity.value if layer.to_StandardOpaqueMaterial.is_initialized
        final_r_val_d[con_index][lay_index] = neat_numbers(final_r_val[con_index][lay_index])
        final_sol_abs_d[con_index] = neat_numbers(final_sol_abs[con_index]) if lay_index == 0 && layer.to_StandardOpaqueMaterial.is_initialized
        final_thm_mass_d[con_index][lay_index] = neat_numbers(final_thm_mass[con_index][lay_index]) if layer.to_StandardOpaqueMaterial.is_initialized
        runner.registerInfo("Updated material '#{layer.name}' in construction '#{new_construction.name}' to '#{new_layer.name}' as follows:")
        final_r_val[con_index][lay_index] ? runner.registerInfo("R-Value updated from #{initial_r_val_d[con_index][lay_index]} to #{final_r_val_d[con_index][lay_index]} (#{(final_r_val[con_index][lay_index] / initial_r_val[con_index][lay_index]).round(2)} mult)") : runner.registerInfo("R-Value was #{initial_r_val_d[con_index][lay_index]} and now is nil_value")
        final_thm_mass[con_index][lay_index] ? runner.registerInfo("Thermal Mass updated from #{initial_thm_mass_d[con_index][lay_index]} to #{final_thm_mass_d[con_index][lay_index]} (#{(final_thm_mass[con_index][lay_index] / initial_thm_mass[con_index][lay_index]).round(2)} mult)") : runner.registerInfo("Thermal Mass was #{initial_thm_mass[con_index][lay_index]} and now is nil_value")
        if lay_index == 0
          final_sol_abs[con_index] ? runner.registerInfo("Solar Absorptance updated from #{initial_sol_abs_d[con_index]} to #{final_sol_abs_d[con_index]} (#{(final_sol_abs[con_index] / initial_sol_abs[con_index]).round(2)} mult)") : runner.registerInfo("Solar Absorptance was #{initial_sol_abs[con_index][lay_index]} and now is nil_value")
        end
      end
      final_construction[con_index] = new_construction
      # update surfaces with construction = construction to new_construction
      exterior_surfaces.each do |surface|
        surface.setConstruction(new_construction) if surface.construction.get.handle.to_s == construction.handle.to_s
      end
      runner.registerInfo("Using New Construction #{new_construction.name}")
    end

    # report desired condition
    runner.registerFinalCondition("Applied R #{r_value_mult.round(1)}x Solar #{solar_abs_mult.round(1)}x Therm #{thermal_mass_mult.round(1)}x change")

    true
  end # end the run method
end # end the measure

# this allows the measure to be used by the application
ExteriorWallThermalPropertiesMultiplier.new.registerWithApplication