class ASHRAE901PRM < Standard
  # @!group SpaceType

  # Sets the selected internal loads to standards-based or typical values.
  # For each category that is selected get all load instances. Remove all
  # but the first instance if multiple instances.  Add a new instance/definition
  # if no instance exists. Modify the definition for the remaining instance
  # to have the specified values. This method does not alter any
  # loads directly assigned to spaces.  This method skips plenums.
  #
  # @param space_type [OpenStudio::Model::SpaceType] space type object
  # @param set_people [Boolean] if true, set the people density.
  #   Also, assign reasonable clothing, air velocity, and work efficiency inputs
  #   to allow reasonable thermal comfort metrics to be calculated.
  # @param set_lights [Boolean] if true, set the lighting density, lighting fraction
  #   to return air, fraction radiant, and fraction visible.
  # @param set_electric_equipment [Boolean] if true, set the electric equipment density
  # @param set_gas_equipment [Boolean] if true, set the gas equipment density
  # @param set_ventilation [Boolean] if true, set the ventilation rates (per-person and per-area)
  # @param set_infiltration [Boolean] if true, set the infiltration rates
  # @return [Boolean] returns true if successful, false if not
  def space_type_apply_internal_loads(space_type, set_people, set_lights, set_electric_equipment, set_gas_equipment, set_ventilation, set_infiltration)
    # Skip plenums
    # Check if the space type name
    # contains the word plenum.
    if space_type.name.get.to_s.downcase.include?('plenum')
      return false
    end

    if space_type.standardsSpaceType.is_initialized
      if space_type.standardsSpaceType.get.downcase.include?('plenum')
        return false
      end
    end

    # Save information about lighting exceptions before removing extra lights objects
    # First get list of all lights objects that are exempt
    regulated_lights = []
    unregulated_lights = []
    user_lights = @standards_data.key?('userdata_lights') ? @standards_data['userdata_lights'] : nil
    if user_lights && user_lights.length >= 1
      user_lights.each do |user_data|
        lights_name = user_data['name']
        lights_obj = space_type.model.getLightsByName(lights_name).get

        if user_data['has_retail_display_exception'].to_s.downcase == 'yes' || user_data['has_unregulated_exception'].to_s.downcase == 'yes'
          # If either exception is applicable
          # Put this one on the unregulated list
          unregulated_lights.push(lights_name)
        end
      end
    end

    # Get all lights objects that are not exempt
    space_type.lights.sort.each do |lights_obj|
      lights_name = lights_obj.name.get
      if !unregulated_lights.include? lights_name
        regulated_lights << lights_obj
      end
    end

    # Pre-process the light instances in the space type
    # Remove all regulated instances but leave one in the space type
    if regulated_lights.size.zero?
      definition = OpenStudio::Model::LightsDefinition.new(space_type.model)
      definition.setName("#{space_type.name} Lights Definition")
      instance = OpenStudio::Model::Lights.new(definition)
      lights_name = "#{space_type.name} Lights"
      instance.setName(lights_name)
      instance.setSpaceType(space_type)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no lights, one has been created.")
      space_type.additionalProperties.setFeature('regulated_lights_name', lights_name)
      regulated_lights << instance
    else
      regulated_lights.each_with_index do |inst, i|
        if i.zero?
          # Save the name of the first instance to use as the baseline lights object
          lights_name = inst.name.get
          space_type.additionalProperties.setFeature('regulated_lights_name', lights_name)
          next
        end

        # Remove all other lights objects that have not been identified as unregulated
        if i == 1
          ref_name = space_type.additionalProperties.getFeatureAsString('regulated_lights_name').to_s
          OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Multiple lights objects found in user model for #{space_type.name}. Baseline schedule will be determined from #{ref_name}")
        end

        OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Removed lighting object #{inst.name} from #{space_type.name}. ")
        inst.remove
      end
    end

    # Get userdata from userdata_space and userdata_spacetype
    user_spaces = @standards_data.key?('userdata_space') ? @standards_data['userdata_space'] : nil
    user_spacetypes = @standards_data.key?('userdata_spacetype') ? @standards_data['userdata_spacetype'] : nil
    if user_spaces && user_spaces.length >= 1 && has_user_lpd_values(user_spaces)
      # if space type has user data & data has lighting data for user space
      # call this function to enforce space-space_type one on one relationship
      new_space_array = space_to_space_type_apply_lighting(user_spaces, user_spacetypes, space_type)
      # process power equipment with new spaces.
      space_to_space_type_apply_power_equipment(user_spacetypes, user_spaces, new_space_array)
      # remove the old space
      space_type.remove
    else
      if user_spacetypes && user_spacetypes.length >= 1 && has_user_lpd_values(user_spacetypes)
        # if space type has user data & data has lighting data for user space type
        user_space_type_index = user_spacetypes.index { |user_spacetype| user_spacetype['name'] == space_type.name.get }
        if user_space_type_index.nil?
          # cannot find a matched user_spacetype to space_type, use space_type to set LPD
          set_lpd_on_space_type(space_type, user_spaces, user_spacetypes)
          space_type_apply_power_equipment(space_type)
        else
          user_space_type = user_spacetypes[user_space_type_index]
          # If multiple LPD value exist - then enforce space-space_type one on one relationship
          if has_multi_lpd_values_user_data(user_space_type, space_type)
            new_space_array = space_to_space_type_apply_lighting(user_spaces, user_spacetypes, space_type)
            space_to_space_type_apply_power_equipment(user_spacetypes, user_spaces, new_space_array)
            space_type.remove
          else
            # Process the user_space type data - at this point, we are sure there is no lighting per length
            # So all the LPD should be identical by space
            # Loop because we need to assign the occupancy control credit to each space for
            # Schedule processing.
            space_type_lighting_per_area = 0.0
            space_type.spaces.each do |space|
              space_lighting_per_area = calculate_lpd_from_userdata(user_space_type, space)
              space_type_lighting_per_area = space_lighting_per_area
            end
            if space_type.hasAdditionalProperties && space_type.additionalProperties.hasFeature('regulated_lights_name')
              lights_name = space_type.additionalProperties.getFeatureAsString('regulated_lights_name').to_s
              lights_obj = space_type.model.getLightsByName(lights_name).get
              lights_obj.lightsDefinition.setWattsperSpaceFloorArea(OpenStudio.convert(space_type_lighting_per_area.to_f, 'W/ft^2', 'W/m^2').get)
            end
          end
          # process power equipment
          space_type_apply_power_equipment(space_type)
        end
      else
        # no user data, set space_type LPD
        set_lpd_on_space_type(space_type, user_spaces, user_spacetypes)
        # process power equipment
        space_type_apply_power_equipment(space_type)
      end
    end
  end

  # A function to calculate electric value for an electric equipment.
  # The function will check whether this electric equipment is motor, refrigeration, elevator or generic electric equipment
  # and decide actions based on the equipment types
  #
  # @param user_equip_data [Hash] user equipment data
  # @param power_equipment [OpenStudio::Model::ElectricEquipment] equipment
  # @param power_schedule_hash [Hash] equipment operation schedule hash
  # @param space_type [OpenStudio::Model:SpaceType] space type
  # @param user_space_data [Hash] user space data
  # @return [Boolean] returns true if successful, false if not
  def calculate_electric_value_by_userdata(user_equip_data, power_equipment, power_schedule_hash, space_type, user_space_data = nil)
    # Check if the plug load represents a motor (check if motorhorsepower exist), if so, record the motor HP and efficiency.
    if !user_equip_data['motor_horsepower'].nil?
      # Pre-processing will ensure these three user data are added correctly (float, float, boolean)
      # @todo move this part to user data processing.
      power_equipment.additionalProperties.setFeature('motor_horsepower', user_equip_data['motor_horsepower'].to_f)
      power_equipment.additionalProperties.setFeature('motor_efficiency', user_equip_data['motor_efficiency'].to_f)
      power_equipment.additionalProperties.setFeature('motor_is_exempt', user_equip_data['motor_is_exempt'])
    elsif !(user_equip_data['fraction_of_controlled_receptacles'].nil? && user_equip_data['receptacle_power_savings'].nil?)
      # If not a motor - update.
      # Update the electric equipment occupancy credit (if it has)
      update_power_equipment_credits(power_equipment, user_equip_data, power_schedule_hash, space_type, user_space_data)
    else
      # The electric equipment is either an elevator or refrigeration
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ElectricEquipment', "#{power_equipment.name} is an elevator or refrigeration according to the user data provided. Skip receptacle power credit.")
      return false
    end
    return true
  end

  # Apply power equipment to space type
  # This is utility function for applying user data to space type
  #
  # @param space_type [OpenStudio::Model:SpaceType]
  # @return [Boolean] returns true if successful, false if not
  def space_type_apply_power_equipment(space_type)
    # save schedules in a hash in case it is needed for new electric equipment
    power_schedule_hash = {}
    # @todo move this part to user data processing
    user_electric_equipment_data = @standards_data.key?('userdata_electric_equipment') ? @standards_data['userdata_electric_equipment'] : nil
    user_gas_equipment_data = @standards_data.key?('userdata_gas_equipment') ? @standards_data['userdata_gas_equipment'] : nil
    if user_electric_equipment_data && user_electric_equipment_data.length >= 1
      space_type_electric_equipments = space_type.electricEquipment
      space_type_electric_equipments.each do |sp_electric_equipment|
        electric_equipment_name = sp_electric_equipment.name.get
        select_user_electric_equipment_array = user_electric_equipment_data.select { |elec| elec['name'].casecmp(electric_equipment_name) == 0 }
        unless select_user_electric_equipment_array.empty?
          select_user_electric_equipment = select_user_electric_equipment_array[0]
          calculate_electric_value_by_userdata(select_user_electric_equipment, sp_electric_equipment, power_schedule_hash, space_type, nil)
        end
      end
    elsif user_gas_equipment_data && user_gas_equipment_data.length >= 1
      space_type_gas_equipments = space_type.gasEquipment
      space_type_gas_equipments.each do |sp_gas_equipment|
        gas_equipment_name = sp_gas_equipment.name.get
        select_user_gas_equipment_array = user_gas_equipment_data.select { |gas| gas['name'].casecmp(gas_equipment_name) == 0 }
        unless select_user_gas_equipment_array.empty?
          select_user_gas_equipment = select_user_gas_equipment_array[0]
          # Update the gas equipment occupancy credit (if it has)
          update_power_equipment_credits(sp_gas_equipment, select_user_gas_equipment, power_schedule_hash, space_type.model, nil)
        end
      end
    end
    return true
  end

  # Apply space to space type power equipment adjustment.
  # NOTE! this function shall only be used if the space to space type is one to one relationship.
  # This function can process both electric equipment and gas equipment
  # and this function will process user data from electric equipment and gas equipment user data
  #
  # @param user_spacetypes [Hash] spacetype user data
  # @param user_spaces [Hash] space user data
  # @param space_array [Array OpenStudio::Model:Space] list of spaces need for process
  # @return [Boolean] returns true if successful, false if not
  def space_to_space_type_apply_power_equipment(user_spacetypes, user_spaces, space_array)
    # Step 1: Set electric / gas equipment
    # save schedules in a hash in case it is needed for new electric equipment
    power_schedule_hash = {}
    # check if electric equipment data is available.
    user_electric_equipment_data = @standards_data.key?('userdata_electric_equipment') ? @standards_data['userdata_electric_equipment'] : nil
    user_gas_equipment_data = @standards_data.key?('userdata_gas_equipment') ? @standards_data['userdata_gas_equipment'] : nil
    if user_electric_equipment_data && user_electric_equipment_data.length >= 1
      space_array.each do |space|
        # Each space has a unique space type
        space_type = space.spaceType.get
        user_spacestypes_index = user_spacetypes.index { |user_spacetype| /#{user_spacetype['name']}/i =~ space_type.name.get }
        user_space_index = user_spaces.index { |user_space| user_space['name'] == space.name.get }
        # Initialize with standard space_type
        user_space_data = space_type.name.get
        unless user_spacestypes_index.nil?
          # override with user space type if specified
          user_space_data = user_spacetypes[user_spacestypes_index]
        end
        unless user_space_index.nil?
          # override with user space if specified
          user_space_data = user_spaces[user_space_index]
        end
        space_type_electric_equipments = space_type.electricEquipment
        space_type_electric_equipments.each do |sp_electric_equipment|
          electric_equipment_name = sp_electric_equipment.name.get
          select_user_electric_equipment_array = user_electric_equipment_data.select { |elec| /#{elec['name']}/i =~ electric_equipment_name }
          unless select_user_electric_equipment_array.empty?
            select_user_electric_equipment = select_user_electric_equipment_array[0]
            calculate_electric_value_by_userdata(select_user_electric_equipment, sp_electric_equipment, power_schedule_hash, space_type, user_space_data)
          end
        end
      end
    elsif user_gas_equipment_data && user_gas_equipment_data.length >= 1
      space_array.each do |space|
        space_type = space.spaceType.get
        user_spacestypes_index = user_spacetypes.index { |user_spacetype| user_spacetype['name'] == space_type.name.get }
        user_space_index = user_spaces.index { |user_space| user_space['name'] == space.name.get }
        user_space_data = space_type.name.get
        unless user_spacestypes_index.nil?
          user_space_data = user_spacetypes[user_spacestypes_index]
        end
        unless user_space_index.nil?
          user_space_data = user_spaces[user_space_index]
        end
        space_type_gas_equipments = space_type.gasEquipment
        space_type_gas_equipments.each do |sp_gas_equipment|
          gas_equipment_name = sp_gas_equipment.name.get
          select_user_gas_equipment_array = user_gas_equipment_data.select { |gas| gas['name'].casecmp(gas_equipment_name) == 0 }
          unless select_user_gas_equipment_array.empty?
            select_user_gas_equipment = select_user_gas_equipment_array[0]
            # Update the gas equipment occupancy credit (if it has)
            update_power_equipment_credits(sp_gas_equipment, select_user_gas_equipment, power_schedule_hash, space_type.model, user_space_data)
          end
        end
      end
    end
    return true
  end

  # Function update a power equipment schedule based on user data.
  # This function works with both electric equipment and gas equipment and applies the ruleset on power equipment
  # The function process user data including the fraction of controlled receptacles and receptacle power savings.
  #
  # @param power_equipment [OpenStudio::Model::ElectricEquipment] or [OpenStudio::Model:GasEquipment]
  # @param user_power_equipment [Hash] user data for the power equipment
  # @param schedule_hash [Hash] power equipment operation schedules in a hash
  # @param space_type [OpenStudio::Model:SpaceType] space type
  # @param user_data [Hash] user space data
  # @return [Boolean] returns true it adjusted, false if not
  def update_power_equipment_credits(power_equipment, user_power_equipment, schedule_hash, space_type, user_data = nil)
    exception_list = ['office - enclosed <= 250 sf', 'conference/meeting/multipurpose', 'copy/print',
                      'lounge/breakroom - all other', 'lounge/breakroom - healthcare facility', 'classroom/lecture/training - all other',
                      'classroom/lecture/training - preschool to 12th', 'office - open']

    receptacle_power_credits = 0.0
    # Check fraction_of_controlled_receptacles or receptacle_power_savings exist
    if user_power_equipment.key?('fraction_of_controlled_receptacles') && !user_power_equipment['fraction_of_controlled_receptacles'].nil?
      rc = user_power_equipment['fraction_of_controlled_receptacles'].to_f
      # receptacle power credits = percent of all controlled receptacles * 10%
      receptacle_power_credits = rc * 0.1
    elsif user_power_equipment.key?('receptacle_power_savings') && !user_power_equipment['receptacle_power_savings'].nil?
      receptacle_power_credits = user_power_equipment['receptacle_power_savings'].to_f
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ElectricEquipment', "#{power_equipment.name.get} has a user specified receptacle power saving credit #{receptacle_power_credits}. The modeler needs to make sure the credit is approved by a rating authority per Table G3.1 section 12.")
    end

    # process user space data
    if user_data.is_a?(Hash)
      if user_data.key?('num_std_ltg_types') && user_data['num_std_ltg_types'].to_f > 0
        adjusted_receptacle_power_credits = 0.0
        num_std_space_types = user_data['num_std_ltg_types'].to_i
        std_space_index = 0 # loop index
        # Loop through standard lighting type in a space
        while std_space_index < num_std_space_types
          std_space_index += 1
          # Retrieve data from user_data
          type_key = format('std_ltg_type%02d', std_space_index)
          frac_key = format('std_ltg_type_frac%02d', std_space_index)
          sub_space_type = user_data[type_key]
          next if exception_list.include?(sub_space_type)

          adjusted_receptacle_power_credits += user_data[frac_key].to_f * receptacle_power_credits
          # Adjust while loop condition factors
        end
        receptacle_power_credits = adjusted_receptacle_power_credits
      end
    elsif user_data.is_a?(String)
      if exception_list.include?(space_type.standardsSpaceType.get)
        # the space type is in the exception list, no credit to the space type
        receptacle_power_credits = 0.0
      end
    end

    # return false if no receptacle power credits
    unless receptacle_power_credits > 0.0
      return false
    end

    # Step 2: check if need to adjust the electric equipment schedule. - apply credit if needed.
    # get current schedule
    power_schedule = power_equipment.schedule.get
    power_schedule_name = power_schedule.name.get
    new_power_schedule_name = format("#{power_schedule_name}_%.4f", receptacle_power_credits)
    if schedule_hash.key?(new_power_schedule_name)
      # In this case, there is a schedule created, can retrieve the schedule object and reset in this space type.
      schedule_rule = schedule_hash[new_power_schedule_name]
      power_equipment.setSchedule(schedule_rule)
    else
      # In this case, create a new schedule
      # 1. Clone the existing schedule
      new_rule_set_schedule = deep_copy_schedule(new_power_schedule_name, power_schedule, receptacle_power_credits, space_type.model)
      if power_equipment.setSchedule(new_rule_set_schedule)
        schedule_hash[new_power_schedule_name] = new_rule_set_schedule
      end
    end
    return true
  end

  # Function to test LPD on default space type. The function assigns lighting power density to an light object.
  # @param space_type [OpenStudio::Model::SpaceType]
  # @param user_spaces [Hash]
  # @param user_spacetypes [Hash]
  # @return [Boolean] returns true if successful, false if not
  def set_lpd_on_space_type(space_type, user_spaces, user_spacetypes)
    if has_multi_lpd_values_space_type(space_type)
      # If multiple LPD value exist - then enforce space-space_type one on one relationship
      space_to_space_type_apply_lighting(user_spaces, user_spacetypes, space_type)
    else
      # use default - loop through space to assign occupancy credit to each space.
      space_type_lighting_per_area = 0.0
      space_type.spaces.each do |space|
        space_lighting_per_area = calculate_lpd_by_space(space_type, space)
        space_type_lighting_per_area = space_lighting_per_area
      end
      if space_type.hasAdditionalProperties && space_type.additionalProperties.hasFeature('regulated_lights_name')
        lights_name = space_type.additionalProperties.getFeatureAsString('regulated_lights_name').to_s
        lights_obj = space_type.model.getLightsByName(lights_name).get
        OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Setting lighting object #{lights_obj.name.get} lighting per area to #{space_type_lighting_per_area} W/ft^2")
        lights_obj.lightsDefinition.setWattsperSpaceFloorArea(OpenStudio.convert(space_type_lighting_per_area.to_f, 'W/ft^2', 'W/m^2').get)
      end
    end
    return true
  end

  # Function that applies user LPD to each space by duplicating space types
  # This function is used when there are user space data available or
  # the spaces under space type has lighting per length value which may cause multiple
  # lighting power densities under one space_type.
  # @param user_spaces [Hash] hash data contained in the user space
  # @param user_spacetypes [Hash] hash data contained in the user spacetypes
  # @param space_type [OpenStudio::Model::SpaceType] object
  # @return [ArrayOpenStudio::Model::Space] List of Spaces
  def space_to_space_type_apply_lighting(user_spaces, user_spacetypes, space_type)
    space_lighting_per_area_hash = {}
    # first priority - user_space data
    if user_spaces && user_spaces.length >= 1
      space_type.spaces.each do |space|
        user_space_index = user_spaces.index { |user_space| user_space['name'] == space.name.get }
        unless user_space_index.nil?
          user_space_data = user_spaces[user_space_index]
          if user_space_data.key?('num_std_ltg_types') && user_space_data['num_std_ltg_types'].to_f > 0
            space_lighting_per_area = calculate_lpd_from_userdata(user_space_data, space)
            space_lighting_per_area_hash[space.name.get] = space_lighting_per_area
          end
        end
      end
    end
    # second priority - user_spacetype
    if user_spacetypes && user_spacetypes.length >= 1
      # if space type has user data
      user_space_type_index = user_spacetypes.index { |user_spacetype| user_spacetype['name'] == space_type.name.get }
      unless user_space_type_index.nil?
        user_space_type_data = user_spacetypes[user_space_type_index]
        if user_space_type_data.key?('num_std_ltg_types') && user_space_type_data['num_std_ltg_types'].to_f > 0
          space_type.spaces.each do |space|
            # unless the space is in the hash, we will add lighting per area to the space
            space_name = space.name.get
            unless space_lighting_per_area_hash.key?(space_name)
              space_lighting_per_area = calculate_lpd_from_userdata(user_space_type_data, space)
              space_lighting_per_area_hash[space_name] = space_lighting_per_area
            end
          end
        end
      end
    end
    # Third priority
    # set space type to every space in the space_type, third priority
    # will also be assigned from the default space type
    space_type.spaces.each do |space|
      space_name = space.name.get
      unless space_lighting_per_area_hash.key?(space_name)
        space_lighting_per_area = calculate_lpd_by_space(space_type, space)
        space_lighting_per_area_hash[space_name] = space_lighting_per_area
      end
    end
    # All space is explored.
    # Now rewrite the space type in each space - might need to change the logic
    space_array = []
    space_type.spaces.each do |space|
      space_name = space.name.get
      new_space_type = space_type.clone.to_SpaceType.get
      space.setSpaceType(new_space_type)
      lighting_per_area = space_lighting_per_area_hash[space_name]
      new_space_type.lights.each do |inst|
        lights_name = inst.name.get
        new_space_type.additionalProperties.setFeature('regulated_lights_name', lights_name)
        definition = inst.lightsDefinition
        unless lighting_per_area.zero?
          new_definition = definition.clone.to_LightsDefinition.get
          new_definition.setWattsperSpaceFloorArea(OpenStudio.convert(lighting_per_area.to_f, 'W/ft^2', 'W/m^2').get)
          inst.setLightsDefinition(new_definition)
          OpenStudio.logFree(OpenStudio::Info, 'log.prm', "#{space_type.name} set LPD to #{lighting_per_area} W/ft^2.")
        end
      end
      space_array.push(space)
    end
    return space_array
  end

  # Modify the lighting schedules for Appendix G PRM for 2016 and later
  #
  # @param model [OpenStudio::Model::Model] OpenStudio model object
  def space_type_light_sch_change(model)
    # set schedule for lighting
    schedule_hash = {}
    model.getSpaces.each do |space|
      space_type = prm_get_optional_handler(space, @sizing_run_dir, 'spaceType')
      if space_type.hasAdditionalProperties && space_type.additionalProperties.hasFeature('regulated_lights_name')
        lights_name = space_type.additionalProperties.getFeatureAsString('regulated_lights_name').to_s
        ltg_option = space_type.model.getLightsByName(lights_name)
        if ltg_option.is_initialized
          ltg = ltg_option.get
        else
          # raise exception if we cannot find the lights in the model
          prm_raise(false, @sizing_run_dir, "Cannot find the lights #{lights_name} in the model")
        end
        # this will raise exception if the ltg has no schedule assigned.
        if ltg.schedule.is_initialized
          ltg_schedule = ltg.schedule.get
        else
          # case such as Attic may have light object but no light schedule assigned
          # Eplus use default 0 so in here we raise Error but continue processing.
          ltg_schedule = nil
          OpenStudio.logFree(OpenStudio::Warn, 'prm.log',
                             "schedule is not available in component #{ltg.name.get}. Skip processing")
        end

        if ltg_schedule
          ltg_schedule_name = ltg_schedule.name.get
          occupancy_sensor_credit = get_additional_property_as_double(space, 'occ_control_credit', 0.0)
          if schedule_hash.key?(ltg_schedule_name)
            # In this case, there is a schedule created, can retrieve the schedule object and reset in this space type
            schedule_rule = schedule_hash[ltg_schedule_name]
            ltg.setSchedule(schedule_rule)
          else
            # In this case, create a new schedule
            # 1. Clone the existing schedule
            new_ltg_schedule_name = format("#{ltg_schedule_name}_%.4f", occupancy_sensor_credit)
            new_rule_set_schedule = deep_copy_schedule(new_ltg_schedule_name, ltg_schedule, occupancy_sensor_credit, model)
            if ltg.setSchedule(new_rule_set_schedule)
              schedule_hash[new_ltg_schedule_name] = new_rule_set_schedule
            end
          end
        end
      end
    end
  end

  def deep_copy_schedule(new_schedule_name, schedule, adjustment_factor, model)
    OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Creating a new lighting schedule that applies occupancy sensor adjustment factor: #{adjustment_factor} based on #{schedule.name.get} schedule")
    sch = OpenstudioStandards::Schedules
    multiplier = 1.0 / (1.0 - adjustment_factor.to_f)
    case schedule.iddObjectType.valueName.to_s
    when 'OS_Schedule_Constant'
      schedule_constant = schedule.to_ScheduleConstant.get
      schedule_value = schedule_constant.value
      return sch.create_constant_schedule_ruleset(model, schedule_value * multiplier, name: new_schedule_name)
    when 'OS_Schedule_Ruleset'
      new_schedule = schedule.clone(model)
      new_schedule.setName(new_schedule_name)
      schedule_ruleset = new_schedule.to_ScheduleRuleset.get
      return sch.schedule_ruleset_simple_value_adjust(schedule_ruleset, multiplier, modification_type = 'Multiplier')
    when 'OS_Schedule_Compact'
      prm_raise(false, @sizing_run_dir, 'PRM does not support using Compact schedule for lighting schedules. Please update it to ruleset based or constant schedules.')
    else
      prm_raise(false, @sizing_run_dir, 'PRM only supports ruleset based or constant schedules for lighting schedules')
    end
  end

  # calculate the lighting power density per area based on space type
  # The function will calculate the LPD based on the space type (STRING)
  # It considers lighting per area, lighting per length as well as occupancy factors in the database.
  # @param space_type [OpenStudio::Model::SpaceType]
  # @param space [OpenStudio::Model::Space]
  # @return [Double] lighting power density in the space
  def calculate_lpd_by_space(space_type, space)
    # get interior lighting data
    space_type_properties = interior_lighting_get_prm_data(space_type)
    OpenStudio.logFree(OpenStudio::Info, 'prm.log', "The lighting properties for space: #{space.name.get} is based on lighting_space_type: #{space_type_properties['lpd_space_type']}, primary_space_type: #{space_type_properties['primary_space_type']}, secondary_space_type: #{space_type_properties['secondary_space_type']}.")
    space_lighting_per_area = 0.0
    # Assign data
    lights_have_info = false
    lighting_per_area = space_type_properties['w/ft^2'].to_f
    lighting_per_length = space_type_properties['w/ft'].to_f
    manon_or_partauto = space_type_properties['manon_or_partauto'].to_i
    lights_have_info = true unless lighting_per_area.zero? && lighting_per_length.zero?
    occ_control_reduction_factor = 0.0

    if lights_have_info
      # Space height
      space_volume = space.volume
      space_area = space.floorArea
      space_height = OpenStudio.convert(space_volume / space_area, 'm', 'ft').get
      # calculate the new lpd values
      space_lighting_per_area = lighting_per_length * space_height + lighting_per_area

      # Adjust the occupancy control sensor reduction factor from dataset
      if manon_or_partauto == 1
        occ_control_reduction_factor = space_type_properties['occup_sensor_savings'].to_f
      else
        occ_control_reduction_factor = space_type_properties['occup_sensor_auto_on_svgs'].to_f
      end
    end
    # add calculated occupancy control credit for later ltg schedule adjustment
    space.additionalProperties.setFeature('occ_control_credit', occ_control_reduction_factor)
    return space_lighting_per_area
  end

  # Function checks whether the user data contains lighting data
  # @param user_space_data [Hash] space data extracted from user csv.
  # @return [Boolean] True if there are user lpd values, False otherwise.
  def has_user_lpd_values(user_space_data)
    user_space_data.each do |user_data|
      if user_data.key?('num_std_ltg_types') && user_data['num_std_ltg_types'].to_f > 0
        return true
      end
    end
    return false
  end

  # Function checks whether there are multi lpd values in the space type
  # multi-lpd value means there are multiple spaces and the lighting_per_length > 0
  # @param space_type [OpenStudio::Model::SpaceType]
  # @return [Boolean] True if there is lighting power defined by w/ft, False otherwise.
  def has_multi_lpd_values_space_type(space_type)
    space_type_properties = interior_lighting_get_prm_data(space_type)
    lighting_per_length = space_type_properties['w/ft'].to_f

    return space_type.spaces.size > 1 && lighting_per_length > 0
  end

  # Function checks whether there are multi lpd values in the space type from user's data
  # The sum of each space fraction in the user_data is assumed to be 1.0
  # multi-lpd value means lighting per area > 0 and lighting_per_length > 0
  # @param user_data [Hash] user data from the user csv
  # @param space_type [OpenStudio::Model::SpaceType]
  # @return [Boolean]
  def has_multi_lpd_values_user_data(user_data, space_type)
    num_std_ltg_types = user_data['num_std_ltg_types'].to_i
    std_ltg_index = 0 # loop index
    # Loop through standard lighting type in a space
    sum_lighting_per_area = 0
    sum_lighting_per_length = 0
    while std_ltg_index < num_std_ltg_types
      # Retrieve data from user_data
      type_key = format('std_ltg_type%02d', (std_ltg_index + 1))
      sub_space_type = user_data[type_key]
      # Adjust while loop condition factors
      std_ltg_index += 1
      # get interior lighting data
      sub_space_type_properties = interior_lighting_get_prm_data(sub_space_type)
      # Assign data
      lighting_per_length = sub_space_type_properties['w/ft'].to_f
      sum_lighting_per_length += lighting_per_length
    end
    return space_type.spaces.size > 1 && sum_lighting_per_length > 0
  end

  # Calculate the lighting power density per area based on user data (space_based)
  # The function will calculate the LPD based on the space type (STRING)
  # It considers lighting per area, lighting per length as well as occupancy factors in the database.
  # The sum of each space fraction in the user_data is assumed to be 1.0
  # @param user_data [Hash] user data from the user csv
  # @param space [OpenStudio::Model::Space]
  # @return [Double] space lighting per area in W per m2
  def calculate_lpd_from_userdata(user_data, space)
    num_std_ltg_types = user_data['num_std_ltg_types'].to_i
    space_lighting_per_area = 0.0
    occupancy_control_credit_sum = 0.0
    std_ltg_index = 0 # loop index
    # Loop through standard lighting type in a space
    while std_ltg_index < num_std_ltg_types
      # Retrieve data from user_data
      type_key = format('std_ltg_type%02d', (std_ltg_index + 1))
      frac_key = format('std_ltg_type_frac%02d', (std_ltg_index + 1))
      sub_space_type = user_data[type_key]
      sub_space_type_frac = user_data[frac_key].to_f
      # Adjust while loop condition factors
      std_ltg_index += 1
      # get interior lighting data
      sub_space_type_properties = interior_lighting_get_prm_data(sub_space_type)
      # Assign data
      lights_have_info = false
      lighting_per_area = sub_space_type_properties['w/ft^2'].to_f
      lighting_per_length = sub_space_type_properties['w/ft'].to_f
      lights_have_info = true unless lighting_per_area.zero? && lighting_per_length.zero?
      manon_or_partauto = sub_space_type_properties['manon_or_partauto'].to_i

      if lights_have_info
        # Space height
        space_volume = space.volume
        space_area = space.floorArea
        space_height = OpenStudio.convert(space_volume / space_area, 'm', 'ft').get
        # calculate and add new lpd values
        user_space_type_lighting_per_area = (lighting_per_length * space_height +
          lighting_per_area) * sub_space_type_frac
        space_lighting_per_area += user_space_type_lighting_per_area

        # Adjust the occupancy control sensor reduction factor from dataset
        occ_control_reduction_factor = 0.0
        if manon_or_partauto == 1
          occ_control_reduction_factor = sub_space_type_properties['occup_sensor_savings'].to_f
        else
          occ_control_reduction_factor = sub_space_type_properties['occup_sensor_auto_on_svgs'].to_f
        end
        # Now calculate the occupancy control credit factor (weighted by frac_lpd)
        occupancy_control_credit_sum += occ_control_reduction_factor * user_space_type_lighting_per_area
      end
    end
    # add calculated occupancy control credit for later ltg schedule adjustment
    # If space_lighting_per_area = 0, it means there is no lights_have_info, and subsequently, the occupancy_control_credit_sum should be 0
    space.additionalProperties.setFeature('occ_control_credit', space_lighting_per_area > 0 ? occupancy_control_credit_sum / space_lighting_per_area : occupancy_control_credit_sum)
    return space_lighting_per_area
  end
end