# *******************************************************************************
# 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 SetSpaceInfiltrationByExteriorSurfaceArea < OpenStudio::Measure::ModelMeasure
  # define the name that a user will see
  def name
    return 'Set Space Infiltration by Exterior Surface Area'
  end

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

    # make an argument for infiltration
    infiltration_ip = OpenStudio::Measure::OSArgument.makeDoubleArgument('infiltration_ip', true)
    infiltration_ip.setDisplayName('Space Infiltration Flow per Exterior Envelope Surface Area (cfm/ft^2).') # (ft^3/min)/(ft^2) = (ft/min)
    infiltration_ip.setDefaultValue(0.05)
    args << infiltration_ip

    # make an argument for material and installation cost
    material_cost_ip = OpenStudio::Measure::OSArgument.makeDoubleArgument('material_cost_ip', true)
    material_cost_ip.setDisplayName('Increase in Material and Installation Costs for Building per Exterior Envelope Area ($/ft^2).')
    material_cost_ip.setDefaultValue(0.0)
    args << material_cost_ip

    # make an argument for o&m cost
    om_cost_ip = OpenStudio::Measure::OSArgument.makeDoubleArgument('om_cost_ip', true)
    om_cost_ip.setDisplayName('O & M Costs for Construction per Area Used ($/ft^2).')
    om_cost_ip.setDefaultValue(0.0)
    args << om_cost_ip

    # make an argument for o&m frequency
    om_frequency = OpenStudio::Measure::OSArgument.makeIntegerArgument('om_frequency', true)
    om_frequency.setDisplayName('O & M Frequency (whole years).')
    om_frequency.setDefaultValue(1)
    args << om_frequency

    return args
  end

  # 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
    if !runner.validateUserArguments(arguments(model), user_arguments)
      return false
    end

    # assign the user inputs to variables
    infiltration_ip = runner.getDoubleArgumentValue('infiltration_ip', user_arguments)
    material_cost_ip = runner.getDoubleArgumentValue('material_cost_ip', user_arguments)
    years_until_costs_start = 0 # removed user argument and set it to 0. For this measure it should always be 0
    om_cost_ip = runner.getDoubleArgumentValue('om_cost_ip', user_arguments)
    om_frequency = runner.getIntegerArgumentValue('om_frequency', user_arguments)

    # check infiltration for reasonableness
    if infiltration_ip < 0
      runner.registerError("The requested space infiltration flow rate of #{infiltration_ip} cfm/ft^2 was below the measure limit. Choose a positive number.")
      return false
    elsif infiltration_ip == 0.001 # put more thought into best warning trigger value
      runner.registerWarning("The requested space infiltration flow rate of  #{infiltration_ip} cfm/ft^2 seems abnormally low.")
    elsif infiltration_ip > 1.0 # put more thought into best warning trigger value
      runner.registerWarning("The requested space infiltration flow rate of  #{infiltration_ip} cfm/ft^2 seems abnormally high.")
    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)
      if roundto == 2
        number = format '%.2f', number
      else
        number = number.round
      end
      # regex to add commas
      number.to_s.reverse.gsub(/([0-9]{3}(?=([0-9])))/, '\\1,').reverse
    end

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

    # get space infiltration objects used in the model
    space_infiltration_objects = model.getSpaceInfiltrationDesignFlowRates

    # reporting initial condition of model
    if !space_infiltration_objects.empty?
      runner.registerInitialCondition("The initial model contained #{space_infiltration_objects.size} space infiltration objects.")
    else
      runner.registerInitialCondition('The initial model did not contain any space infiltration objects.')
    end

    # remove space infiltration objects
    number_removed = 0
    number_left = 0
    space_infiltration_objects.each do |space_infiltration_object|
      opt_space_type = space_infiltration_object.spaceType
      if opt_space_type.empty?
        space_infiltration_object.remove
        number_removed += 1
      elsif !opt_space_type.get.spaces.empty?
        space_infiltration_object.remove
        number_removed += 1
      else
        number_left += 1
      end
    end
    if number_removed > 0
      runner.registerInfo("#{number_removed} infiltration objects were removed.")
    end
    if number_left > 0
      runner.registerInfo("#{number_left} infiltration objects in unused space types were left in the model. They will not be altered.")
    end

    # if no default space type then add an empty one (to hold new space infiltration object)
    building = model.getBuilding
    if building.spaceType.empty?
      new_default = OpenStudio::Model::SpaceType.new(model)
      new_default.setName('Building Default Space Type')
      building.setSpaceType(new_default)
      runner.registerInfo("Adding a building default space type to hold space infiltration for spaces that previously didn't have a space type.")
    end

    # si units for infiltration argument
    infiltration_si = unit_helper(infiltration_ip, 'ft/min', 'm/s')

    # loop through spacetypes used in the model adding space infiltration objects
    space_types = model.getSpaceTypes
    space_types.each do |space_type|
      if !space_type.spaces.empty?
        new_space_type_infil = OpenStudio::Model::SpaceInfiltrationDesignFlowRate.new(model)
        new_space_type_infil.setFlowperExteriorSurfaceArea(infiltration_si)
        new_space_type_infil.setSpaceType(space_type)
        if new_space_type_infil.schedule.empty?
          # TODO: - check if the warning below is falsely triggering when it does not need to.
          runner.registerWarning("The new infiltration object for space type '#{space_type.name}' does not have a schedule. Assigning a default schedule set including an infiltration schedule to the space type or the building will address this.")
        end
      end
    end

    # get area of surfaces with outdoor boundary condition. Take zone multipliers into account
    surfaces = model.getSurfaces
    exterior_surface_gross_area = 0
    space_warning_issued = []

    surfaces.each do |s|
      next if s.outsideBoundaryCondition != 'Outdoors'

      # get surface area adjusting for zone multiplier
      space = s.space
      if !space.empty?
        zone = space.get.thermalZone
      end
      if !zone.empty?
        zone_multiplier = zone.get.multiplier
        if (zone_multiplier > 1) && !space_warning_issued.include?(space.get.name.to_s)
          runner.registerInfo("Space #{space.get.name} in thermal zone #{zone.get.name} has a zone multiplier of #{zone_multiplier}. Adjusting area calculations.")
          space_warning_issued << space.get.name.to_s
        end
      else
        zone_multiplier = 1 # space is not in a thermal zone
        runner.registerWarning("Space #{space.get.name} is not in a thermal zone and won't be included in in the simulation. For area calculations in this measure a zone multiplier of 1 will be assumed.")
      end
      exterior_surface_gross_area += s.grossArea * zone_multiplier
    end

    # ip exterior surface area for reporting and building lifeCycleCostObject
    exterior_surface_gross_area_ip = unit_helper(exterior_surface_gross_area, 'm^2', 'ft^2')

    # only add LifeCyCyleCostItem if the user entered some non 0 cost values
    if (material_cost_ip != 0) || (om_cost_ip != 0)
      material_cost_si = unit_helper(material_cost_ip, '1/ft^2', '1/m^2')
      om_cost_si = unit_helper(om_cost_ip, '1/ft^2', '1/m^2')
      lcc_mat = OpenStudio::Model::LifeCycleCost.createLifeCycleCost('LCC_Mat - Cost to Adjust Infiltration', building, exterior_surface_gross_area * material_cost_si, 'CostPerEach', 'Construction', 0, years_until_costs_start) # 0 for expected life will result infinite expected life
      lcc_om = OpenStudio::Model::LifeCycleCost.createLifeCycleCost('LCC_OM - Cost to Adjust Infiltration', building, exterior_surface_gross_area * om_cost_si, 'CostPerEach', 'Maintenance', om_frequency, years_until_costs_start) # o&m costs start after at sane time that material and installation costs occur
      runner.registerInfo("Costs related to the change in infiltration are attached to the building object. Any subsequent measures that may affect infiltration won't affect these costs.")
    else
      runner.registerInfo('Cost arguments were not provided, no cost objects were added to the model.')
    end

    # reporting final condition of model
    if !lcc_mat.nil?
      cost_per_area_ip = lcc_mat.get.totalCost / exterior_surface_gross_area_ip
    else
      cost_per_area_ip = 0
    end

    runner.registerFinalCondition("The final model has an infiltration rate of #{neat_numbers(infiltration_ip)} (cfm/ft^2). The material and installation cost increase resulting from this measure is $#{neat_numbers(cost_per_area_ip)} ($/ft^2) over #{neat_numbers(exterior_surface_gross_area_ip, 0)} (ft^2) of exterior envelope.")

    return true
  end
end

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