class Standard
  # @!group SubSurface

  # Determine the component infiltration rate for this surface
  #
  # @param sub_surface [OpenStudio::Model::SubSurface] sub surface object
  # @param type [String] choices are 'baseline' and 'advanced'
  # @return [Double] infiltration rate in m^3/s
  def sub_surface_component_infiltration_rate(sub_surface, type)
    comp_infil_rate_m3_per_s = 0.0

    # Define the envelope component infiltration rates
    component_infil_rates_cfm_per_ft2 = {
      'baseline' => {
        'opaque_door' => 0.40,
        'loading_dock_door' => 0.40,
        'swinging_or_revolving_glass_door' => 1.0,
        'vestibule' => 1.0,
        'sliding_glass_door' => 0.40,
        'window' => 0.40,
        'skylight' => 0.40
      },
      'advanced' => {
        'opaque_door' => 0.20,
        'loading_dock_door' => 0.20,
        'swinging_or_revolving_glass_door' => 1.0,
        'vestibule' => 1.0,
        'sliding_glass_door' => 0.20,
        'window' => 0.20,
        'skylight' => 0.20
      }
    }

    boundary_condition = sub_surface.outsideBoundaryCondition
    # Skip non-outdoor surfaces
    return comp_infil_rate_m3_per_s unless outsideBoundaryCondition == 'Outdoors' || sub_surface.outsideBoundaryCondition == 'Ground'

    # Per area infiltration rate for this surface
    surface_type = sub_surface.subSurfaceType
    infil_rate_cfm_per_ft2 = nil
    case boundary_condition
    when 'Outdoors'
      case surface_type
      when 'Door'
        infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['opaque_door']
      when 'OverheadDoor'
        infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['loading_dock_door']
      when 'GlassDoor'
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.Model', "For #{sub_surface.name}, assuming swinging_or_revolving_glass_door for infiltration calculation.")
        infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['swinging_or_revolving_glass_door']
      when 'FixedWindow', 'OperableWindow'
        infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['window']
      when 'Skylight', 'TubularDaylightDome', 'TubularDaylightDiffuser'
        infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['skylight']
      end
    end
    if infil_rate_cfm_per_ft2.nil?
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.Model', "For #{sub_surface.name}, could not determine surface type for infiltration, will not be included in calculation.")
      return comp_infil_rate_m3_per_s
    end

    # Area of the surface
    area_m2 = sub_surface.netArea
    area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get

    # Rate for this surface
    comp_infil_rate_cfm = area_ft2 * infil_rate_cfm_per_ft2

    comp_infil_rate_m3_per_s = OpenStudio.convert(comp_infil_rate_cfm, 'cfm', 'm^3/s').get

    # OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "......#{self.name}, infil = #{comp_infil_rate_cfm.round(2)} cfm @ rate = #{infil_rate_cfm_per_ft2} cfm/ft2, area = #{area_ft2.round} ft2.")

    return comp_infil_rate_m3_per_s
  end

  # Reduce the area of the subsurface by shrinking it toward the centroid.
  # @author Julien Marrec
  #
  # @param sub_surface [OpenStudio::Model::SubSurface] sub surface object
  # @param percent_reduction [Double] the fractional amount to reduce the area
  # @return [Bool] returns true if successful, false if not
  def sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(sub_surface, percent_reduction)
    mult = 1 - percent_reduction
    scale_factor = mult**0.5

    # Get the centroid (Point3d)
    g = sub_surface.centroid

    # Create an array to collect the new vertices
    new_vertices = []

    # Loop on vertices (Point3ds)
    sub_surface.vertices.each do |vertex|
      # Point3d - Point3d = Vector3d
      # Vector from centroid to vertex (GA, GB, GC, etc)
      centroid_vector = vertex - g

      # Resize the vector (done in place) according to scale_factor
      centroid_vector.setLength(centroid_vector.length * scale_factor)

      # Move the vertex toward the centroid
      vertex = g + centroid_vector

      new_vertices << vertex
    end

    # Assign the new vertices to the self
    sub_surface.setVertices(new_vertices)
  end

  # Reduce the area of the subsurface by raising the sill height.
  #
  # @param sub_surface [OpenStudio::Model::SubSurface] sub surface object
  # @param percent_reduction [Double] the fractional amount to reduce the area.
  # @return [Bool] returns true if successful, false if not
  def sub_surface_reduce_area_by_percent_by_raising_sill(sub_surface, percent_reduction)
    mult = 1 - percent_reduction

    # Calculate the original area
    area_original = sub_surface.netArea

    # Find the min and max z values
    min_z_val = 99_999
    max_z_val = -99_999
    sub_surface.vertices.each do |vertex|
      # Min z value
      if vertex.z < min_z_val
        min_z_val = vertex.z
      end
      # Max z value
      if vertex.z > max_z_val
        max_z_val = vertex.z
      end
    end

    # Calculate the window height
    height = max_z_val - min_z_val

    # Calculate the new sill height
    new_sill_z = max_z_val - (height * mult)

    # Reset the z value of the lowest points
    new_vertices = []
    sub_surface.vertices.each do |vertex|
      new_x = vertex.x
      new_y = vertex.y
      new_z = vertex.z
      if new_z == min_z_val
        new_z = new_sill_z
      end
      new_vertices << OpenStudio::Point3d.new(new_x, new_y, new_z)
    end

    # Reset the vertices
    sub_surface.setVertices(new_vertices)

    return true
  end

  # Determine if the sub surface is a vertical rectangle,
  # meaning a rectangle where the bottom is parallel to the ground.
  #
  # @param sub_surface [OpenStudio::Model::SubSurface] sub surface object
  # @return [Bool] returns true if the surface is a vertical rectangle, false if not
  def sub_surface_vertical_rectangle?(sub_surface)
    # Get the vertices once
    verts = sub_surface.vertices

    # Check for 4 vertices
    return false unless verts.size == 4

    # Check if the 2 lowest z-values
    # are the same
    z_vals = []
    verts.each do |vertex|
      z_vals << vertex.z
    end
    z_vals = z_vals.sort
    return false unless z_vals[0] == z_vals[1]

    # Check if the diagonals are equal length
    diag_a = verts[0] - verts[2]
    diag_b = verts[1] - verts[3]
    return false unless diag_a.length == diag_b.length

    # If here, we have a rectangle
    return true
  end

  # This method adds a subsurface (a window or a skylight depending on the surface) to the centroid of a surface.  The
  # shape of the subsurface is the same as the surface but is scaled so the area of the subsurface is the defined
  # fraction of the surface (set by area_fraction).  Note that this only works for surfaces that do not fold into
  # themselves (like an 'L' or a 'V').
  #
  # @param surface [OpenStudio::Model::Surface] surface object
  # @param area_fraction [Double] fraction of area of the larger surface
  # @return [Bool] returns true if successful, false if not
  def sub_surface_create_centered_subsurface_from_scaled_surface(surface, area_fraction)
    # Get rid of all existing subsurfaces.
    remove_all_subsurfaces(surface: surface)
    # What is the centroid of the surface.
    surf_cent = surface.centroid
    scale_factor = Math.sqrt(area_fraction)

    # Create an array to collect the new vertices
    new_vertices = []

    # Loop on vertices (Point3ds)
    surface.vertices.each do |vertex|
      # Point3d - Point3d = Vector3d
      # Vector from centroid to vertex (GA, GB, GC, etc)
      centroid_vector = vertex - surf_cent

      # Resize the vector (done in place) according to scale_factor
      centroid_vector.setLength(centroid_vector.length * scale_factor)

      # Move the vertex toward the centroid
      new_vertex = surf_cent + centroid_vector

      # Add the new vertices to an array of vertices.
      new_vertices << new_vertex
    end

    # Create a new subsurface with the vertices determined above.
    new_sub_surface = OpenStudio::Model::SubSurface.new(new_vertices, surface.model)
    # Put this sub-surface on the surface.
    new_sub_surface.setSurface(surface)
    # Set the name of the subsurface to be the surface name plus the subsurface type (likely either 'fixedwindow' or
    # 'skylight').
    new_name = surface.name.to_s + '_' + new_sub_surface.subSurfaceType.to_s
    new_sub_surface.setName(new_name)
    # There is now only one surface on the subsurface.  Enforce this
    new_sub_surface.setMultiplier(1)
    return true
  end

  # This method adds a subsurface (a window or a skylight depending on the surface) to the centroid of a surface.  The
  # shape of the subsurface is the same as the surface but is scaled so the area of the subsurface is the defined
  # fraction of the surface (set by area_fraction).  This method is different than the
  # 'sub_surface_create_centered_subsurface_from_scaled_surface' method because it can handle concave surfaces.
  # However, it takes longer because it uses BTAP::Geometry::Surfaces.make_convex_surfaces which includes many nested
  # loops that cycle through the verticies in a surface.
  #
  # @param surface [OpenStudio::Model::Surface] surface object
  # @param area_fraction [Double] fraction of area of the larger surface
  # @param construction [OpenStudio::Model::Construction] construction to use for the new surface
  # @return [Bool] returns true if successful, false if not
  def sub_surface_create_scaled_subsurfaces_from_surface(surface:, area_fraction:, construction:)
    # Set geometry tolerences:
    geometry_tolerence = 12
    # Get rid of all existing subsurfaces.
    remove_all_subsurfaces(surface: surface)
    # Return vertices of smaller surfaces that fit inside this surface.  This is done in case the surface is
    # concave.

    # Throw an error if the roof is not flat.
    surface.vertices.each do |surf_vert|
      surface.vertices.each do |surf_vert_2|
        return OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.Model', "Currently skylights can only be added to buildings with non-plenum flat roofs.  No skylight added to surface #{surface.name}") if surf_vert_2.z.to_f.round(geometry_tolerence) != surf_vert.z.to_f.round(geometry_tolerence)
      end
    end
    new_surfaces = BTAP::Geometry::Surfaces.make_convex_surfaces(surface: surface, tol: geometry_tolerence)

    # What is the centroid of the surface.
    new_surf_cents = []
    for i in 0..(new_surfaces.length - 1)
      new_surf_cents << BTAP::Geometry::Surfaces.surf_centroid(surf: new_surfaces[i])
    end

    # Turn everything back into OpenStudio stuff
    os_surf_points = []
    os_surf_cents = []
    for i in 0..(new_surfaces.length - 1)
      os_surf_point = []
      for j in 0..(new_surfaces[i].length - 1)
        os_surf_point << OpenStudio::Point3d.new(new_surfaces[i][j][:x].to_f, new_surfaces[i][j][:y].to_f, new_surfaces[i][j][:z].to_f)
      end
      os_surf_cents << OpenStudio::Point3d.new(new_surf_cents[i][:x].to_f, new_surf_cents[i][:y].to_f, new_surf_cents[i][:z].to_f)
      os_surf_points << os_surf_point
    end
    scale_factor = Math.sqrt(area_fraction)

    new_sub_vertices = []
    os_surf_points.each_with_index do |new_surf, index|
      # Create an array to collect the new vertices
      new_vertices = []
      # Loop on vertices
      new_surf.each do |vertex|
        # Point3d - Point3d = Vector3d
        # Vector from centroid to vertex (GA, GB, GC, etc)
        centroid_vector = vertex - os_surf_cents[index]

        # Resize the vector (done in place) according to scale_factor
        centroid_vector.setLength(centroid_vector.length * scale_factor)

        # Move the vertex toward the centroid
        new_vertex = os_surf_cents[index] + centroid_vector

        # Add the new vertices to an array of vertices.
        new_vertices << new_vertex
      end
      # Check if the new surface/subsurface is too small to model.  If it is then skip it.
      new_area = BTAP::Geometry::Surfaces.getSurfaceAreafromVertices(vertices: new_vertices)
      if new_area < 0.0001
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.Model', "Attempting to create a subsurface in surface #{surface.name} with an area of #{new_area}m2.  This subsurface is too small so will be skipped")
        next
      end

      # Create a new subsurface with the vertices determined above.
      new_sub_surface = OpenStudio::Model::SubSurface.new(new_vertices, surface.model)
      # Put this sub-surface on the surface.
      new_sub_surface.setSurface(surface)
      # Set the name of the subsurface to be the surface name plus the subsurface type (likely either 'fixedwindow' or
      # 'skylight').  If there will be more than one subsurface then add a counter at the end.
      new_name = surface.name.to_s + '_' + new_sub_surface.subSurfaceType.to_s
      if new_surfaces.length > 1
        new_name = surface.name.to_s + '_' + new_sub_surface.subSurfaceType.to_s + '_' + index.to_s
      end
      # Set the skylight type to 'Skylight'
      new_sub_surface.setSubSurfaceType('Skylight')
      # Set the skylight construction to whatever was passed (should be the default skylight construction)
      new_sub_surface.setConstruction(construction)
      new_sub_surface.setName(new_name)
      # There is now only one surface on the subsurface.  Enforce this
      new_sub_surface.setMultiplier(1)
    end
    return true
  end

  # This just uses applies 'setWindowToWallRatio' method from the OpenStudio SDK.  The only addition is that it changes
  # the name of the window to be the surface name plus the subsurface type (always 'fixedwindow').
  #
  # @param surface [OpenStudio::Model::Surface] surface object
  # @param area_fraction [Double] fraction of area of the larger surface
  # @param construction [OpenStudio::Model::Construction] construction to use for the new surface
  # @return [Bool] returns true if successful, false if not
  def set_window_to_wall_ratio_set_name(surface:, area_fraction:, construction:)
    surface.setWindowToWallRatio(area_fraction)
    surface.subSurfaces.sort.each do |sub_surf|
      sub_surf.setSubSurfaceType('FixedWindow')
      sub_surf.setConstruction(construction)
      new_name = surface.name.to_s + '_' + sub_surf.subSurfaceType.to_s
      sub_surf.setName(new_name)
    end
    return true
  end

  # This removes all of the subsurfaces from a surface.
  # Is a preparation for replaceing windows or clearing doors before adding windows.
  #
  # @param surface [OpenStudio::Model::Surface] surface object
  # @return [Bool] returns true if successful, false if not
  def remove_all_subsurfaces(surface:)
    surface.subSurfaces.sort.each(&:remove)
    return true
  end
end