# *********************************************************************************
# URBANopt™, Copyright (c) 2019-2021, Alliance for Sustainable Energy, 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:

# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.

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

# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.

# Redistribution of this software, without modification, must refer to the software
# by the same designation. Redistribution of a modified version of this software
# (i) may not refer to the modified version by the same designation, or by any
# confusingly similar designation, and (ii) must refer to the underlying software
# originally provided by Alliance as “URBANopt”. Except to comply with the foregoing,
# the term “URBANopt”, or any confusingly similar designation may not be used to
# refer to any modified version of this software or any modified version of the
# underlying software originally provided by Alliance without the prior written
# consent of Alliance.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 OR CONTRIBUTORS 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_relative 'validator'

require 'json-schema'
require 'json'

module URBANopt
  module Reporting
    module DefaultReports
      ##
      # Program includes all building program related information.
      ##
      class Program
        attr_accessor :site_area_sqft, :floor_area_sqft, :conditioned_area_sqft, :unconditioned_area_sqft, :footprint_area_sqft, :maximum_roof_height_ft,
                      :maximum_number_of_stories, :maximum_number_of_stories_above_ground, :parking_area_sqft, :number_of_parking_spaces,
                      :number_of_parking_spaces_charging, :parking_footprint_area_sqft, :maximum_parking_height_ft, :maximum_number_of_parking_stories,
                      :maximum_number_of_parking_stories_above_ground, :number_of_residential_units, :building_types, :building_type, :maximum_occupancy,
                      :area_sqft, :window_area_sqft, :north_window_area_sqft, :south_window_area_sqft, :east_window_area_sqft, :west_window_area_sqft, :wall_area_sqft, :roof_area_sqft, :equipment_roof_area_sqft,
                      :photovoltaic_roof_area_sqft, :available_roof_area_sqft, :total_roof_area_sqft, :orientation_deg, :aspect_ratio, :total_construction_cost_dollar # :nodoc:

        # Program class initialize building program attributes: +:site_area_sqft+ , +:floor_area_sqft+ , +:conditioned_area_sqft+ , +:unconditioned_area_sqft+ ,
        # +:footprint_area_sqft+ , +:maximum_roof_height_ft, +:maximum_number_of_stories+ , +:maximum_number_of_stories_above_ground+ , +:parking_area_sqft+ ,
        # +:number_of_parking_spaces+ , +:number_of_parking_spaces_charging+ , +:parking_footprint_area_sqft+ , +:maximum_parking_height_ft+ , +:maximum_number_of_parking_stories+ ,
        # +:maximum_number_of_parking_stories_above_ground+ , +:number_of_residential_units+ , +:building_types+ , +:building_type+ , +:maximum_occupancy+ ,
        # +:area_sqft+ , +:window_area_sqft+ , +:north_window_area_sqft+ , +:south_window_area_sqft+ , +:east_window_area_sqft+ , +:west_window_area_sqft+ , +:wall_area_sqft+ , +:roof_area_sqft+ ,
        # +:equipment_roof_area_sqft+ , +:photovoltaic_roof_area_sqft+ , +:available_roof_area_sqft+ , +:total_roof_area_sqft+ , +:orientation_deg+ , +:aspect_ratio+
        ##
        # [parameters:]
        # +hash+ - _Hash_ - A hash which may contain a deserialized program.
        ##
        def initialize(hash = {})
          hash.delete_if { |k, v| v.nil? }
          hash = defaults.merge(hash)

          @site_area_sqft = hash[:site_area_sqft]
          @floor_area_sqft = hash[:floor_area_sqft]
          @conditioned_area_sqft = hash[:conditioned_area_sqft]
          @unconditioned_area_sqft = hash[:unconditioned_area_sqft]
          @footprint_area_sqft = hash[:footprint_area_sqft]
          @maximum_roof_height_ft = hash[:maximum_roof_height_ft]
          @maximum_number_of_stories = hash[:maximum_number_of_stories]
          @maximum_number_of_stories_above_ground = hash[:maximum_number_of_stories_above_ground]
          @parking_area_sqft = hash[:parking_area_sqft]
          @number_of_parking_spaces = hash[:number_of_parking_spaces]
          @number_of_parking_spaces_charging = hash[:number_of_parking_spaces_charging]
          @parking_footprint_area_sqft = hash[:parking_footprint_area_sqft]
          @maximum_parking_height_ft = hash[:maximum_parking_height_ft]
          @maximum_number_of_parking_stories = hash[:maximum_number_of_parking_stories]
          @maximum_number_of_parking_stories_above_ground = hash[:maximum_number_of_parking_stories_above_ground]
          @number_of_residential_units = hash[:number_of_residential_units]
          @building_types = hash[:building_types]
          @window_area_sqft = hash[:window_area_sqft]
          @wall_area_sqft = hash[:wall_area_sqft]
          @roof_area_sqft = hash[:roof_area_sqft]
          @orientation_deg = hash[:orientation_deg]
          @aspect_ratio = hash[:aspect_ratio]
          @total_construction_cost_dollar = hash[:total_construction_cost_dollar]

          # initialize class variables @@validator and @@schema
          @@validator ||= Validator.new
          @@schema ||= @@validator.schema
        end

        ##
        # Assigns default values if values do not exist.
        ##
        def defaults
          hash = {}
          hash[:site_area_sqft] = nil
          hash[:floor_area_sqft] = nil
          hash[:conditioned_area_sqft] = nil
          hash[:unconditioned_area_sqft] = nil
          hash[:footprint_area_sqft] = nil
          hash[:maximum_roof_height_ft] = nil
          hash[:maximum_number_of_stories] = nil
          hash[:maximum_number_of_stories_above_ground] = nil
          hash[:parking_area_sqft] = nil
          hash[:number_of_parking_spaces] = nil
          hash[:number_of_parking_spaces_charging] = nil
          hash[:parking_footprint_area_sqft] = nil
          hash[:maximum_parking_height_ft] = nil
          hash[:maximum_number_of_parking_stories] = nil
          hash[:maximum_number_of_parking_stories_above_ground] = nil
          hash[:number_of_residential_units] = nil
          hash[:building_types] = [{ building_type: nil, maximum_occupancy: nil, floor_area_sqft: nil }]
          hash[:window_area_sqft] = { north_window_area_sqft: nil, south_window_area_sqft: nil, east_window_area_sqft: nil, west_window_area_sqft: nil, total_window_area_sqft: nil }
          hash[:wall_area_sqft] = { north_wall_area_sqft: nil, south_wall_area_sqft: nil, east_wall_area_sqft: nil, west_wall_area_sqft: nil, total_wall_area_sqft: nil }
          hash[:roof_area_sqft] = { equipment_roof_area_sqft: nil, photovoltaic_roof_area_sqft: nil, available_roof_area_sqft: nil, total_roof_area_sqft: nil }
          hash[:orientation_deg] = nil
          hash[:aspect_ratio] = nil
          hash[:total_construction_cost_dollar] = nil
          return hash
        end

        ##
        # Convert to a Hash equivalent for JSON serialization.
        ##
        # - Exclude attributes with nil values.
        # - Validate program hash properties against schema.
        ##
        def to_hash
          result = {}
          result[:site_area_sqft] = @site_area_sqft if @site_area_sqft
          result[:floor_area_sqft] = @floor_area_sqft if @floor_area_sqft
          result[:conditioned_area_sqft] = @conditioned_area_sqft if @conditioned_area_sqft
          result[:unconditioned_area_sqft] = @unconditioned_area_sqft if @unconditioned_area_sqft
          result[:footprint_area_sqft] = @footprint_area_sqft if @footprint_area_sqft
          result[:maximum_roof_height_ft] = @maximum_roof_height_ft if @maximum_roof_height_ft
          result[:maximum_number_of_stories] = @maximum_number_of_stories if @maximum_number_of_stories
          result[:maximum_number_of_stories_above_ground] = @maximum_number_of_stories_above_ground if @maximum_number_of_stories_above_ground
          result[:parking_area_sqft] = @parking_area_sqft if @parking_area_sqft
          result[:number_of_parking_spaces] = @number_of_parking_spaces if @number_of_parking_spaces
          result[:number_of_parking_spaces_charging] = @number_of_parking_spaces_charging if @number_of_parking_spaces_charging
          result[:parking_footprint_area_sqft] = @parking_footprint_area_sqft if @parking_footprint_area_sqft
          result[:maximum_parking_height_ft] = @maximum_parking_height_ft if @maximum_parking_height_ft
          result[:maximum_number_of_parking_stories] = @maximum_number_of_parking_stories if @maximum_number_of_parking_stories
          result[:maximum_number_of_parking_stories_above_ground] = @maximum_number_of_parking_stories_above_ground if @maximum_number_of_parking_stories_above_ground
          result[:number_of_residential_units] = @number_of_residential_units if @number_of_residential_units

          if @building_types.any?
            result[:building_types] = @building_types
            @building_types.each do |bt|
              bt&.delete_if { |k, v| v.nil? }
            end
          end

          # result[:window_area_sqft] = @window_area_sqft if @window_area_sqft
          window_area_sqft_hash = @window_area_sqft if @window_area_sqft
          window_area_sqft_hash.delete_if { |k, v| v.nil? }
          result[:window_area_sqft] = window_area_sqft_hash if @window_area_sqft

          # result[:wall_area_sqft] = @wall_area_sqft if @wall_area_sqft
          wall_area_sqft_hash = @wall_area_sqft if @wall_area_sqft
          wall_area_sqft_hash.delete_if { |k, v| v.nil? }
          result[:wall_area_sqft] = wall_area_sqft_hash if @wall_area_sqft

          # result[:roof_area_sqft] = @roof_area_sqft if @roof_area_sqft
          roof_area_sqft_hash = @roof_area_sqft if @roof_area_sqft
          roof_area_sqft_hash.delete_if { |k, v| v.nil? }
          result[:roof_area_sqft] = roof_area_sqft_hash if @roof_area_sqft

          result[:orientation_deg] = @orientation_deg if @orientation_deg
          result[:aspect_ratio] = @aspect_ratio if @aspect_ratio

          result[:total_construction_cost_dollar] = @total_construction_cost_dollar if @total_construction_cost_dollar

          # validate program properties against schema
          if @@validator.validate(@@schema[:definitions][:Program][:properties], result).any?
            raise "program properties does not match schema: #{@@validator.validate(@@schema[:definitions][:Program][:properties], result)}"
          end

          return result
        end

        ##
        # Return the maximum value from +existing_value+ and +new_value+.
        ##
        # [parameters:]
        # +existing_value+ - _Float_ - A value corresponding to a Program attribute.
        ##
        # +new_value+ - _Float_ - A value corresponding to a Program attribute.
        ##
        def max_value(existing_value, new_value)
          if existing_value && new_value
            [existing_value, new_value].max
          elsif new_value
            existing_value = new_value
          end
          return existing_value
        end

        ##
        # Adds up +existing_value+ and +new_values+ if not nill.
        ##
        # [parameters:]
        # +existing_value+ - _Float_ - A value corresponding to a Program attribute.
        ##
        # +new_value+ - _Float_ - A value corresponding to a Program attribute.
        ##
        def add_values(existing_value, new_value)
          if existing_value && new_value
            existing_value += new_value
          elsif new_value
            existing_value = new_value
          end
          return existing_value
        end

        ##
        # Merges program objects to each other by summing up values or taking the maximum value of the attributes.
        ##
        # [parameters:]
        # +other+ - _Program_ - An object of Program class.
        ##
        def add_program(other)
          @site_area_sqft = add_values(@site_area_sqft, other.site_area_sqft)

          @floor_area_sqft = add_values(@floor_area_sqft, other.floor_area_sqft)
          @conditioned_area_sqft = add_values(@conditioned_area_sqft, other.conditioned_area_sqft)
          @unconditioned_area_sqft = add_values(@unconditioned_area_sqft, other.unconditioned_area_sqft)
          @footprint_area_sqft = add_values(@footprint_area_sqft, other.footprint_area_sqft)
          @maximum_roof_height_ft = max_value(@maximum_roof_height_ft, other.maximum_roof_height_ft)
          @maximum_number_of_stories = max_value(@maximum_number_of_stories, other.maximum_number_of_stories)
          @maximum_number_of_stories_above_ground = max_value(@maximum_number_of_stories_above_ground, other.maximum_number_of_stories_above_ground)
          @parking_area_sqft = add_values(@parking_area_sqft, other.parking_area_sqft)
          @number_of_parking_spaces = add_values(@number_of_parking_spaces, other.number_of_parking_spaces)
          @number_of_parking_spaces_charging = add_values(@number_of_parking_spaces_charging, other.number_of_parking_spaces_charging)
          @parking_footprint_area_sqft = add_values(@parkig_footprint_area_sqft, other.parking_footprint_area_sqft)
          @maximum_parking_height_ft = max_value(@maximum_parking_height_ft, other.maximum_parking_height_ft)
          @maximum_number_of_parking_stories = max_value(@maximum_number_of_parking_stories, other.maximum_number_of_parking_stories)
          @maximum_number_of_parking_stories_above_ground = max_value(maximum_number_of_parking_stories_above_ground, other.maximum_number_of_parking_stories_above_ground)
          @number_of_residential_units = add_values(@number_of_residential_units, other.number_of_residential_units)
          @total_construction_cost_dollar = add_values(@total_construction_cost_dollar, other.total_construction_cost_dollar)

          @building_types = other.building_types

          @window_area_sqft[:north_window_area_sqft] = add_values(@window_area_sqft[:north_window_area_sqft], other.window_area_sqft[:north_window_area_sqft])
          @window_area_sqft[:south_window_area_sqft] = add_values(@window_area_sqft[:south_window_area_sqft], other.window_area_sqft[:south_window_area_sqft])
          @window_area_sqft[:east_window_area_sqft] = add_values(@window_area_sqft[:east_window_area_sqft], other.window_area_sqft[:east_window_area_sqft])
          @window_area_sqft[:west_window_area_sqft] = add_values(@window_area_sqft[:west_window_area_sqft], other.window_area_sqft[:west_window_area_sqft])
          @window_area_sqft[:total_window_area_sqft] =  add_values(@window_area_sqft[:total_window_area_sqft], other.window_area_sqft[:total_window_area_sqft])

          @wall_area_sqft[:north_wall_area_sqft] = add_values(@wall_area_sqft[:north_wall_area_sqft], other.wall_area_sqft[:north_wall_area_sqft])
          @wall_area_sqft[:south_wall_area_sqft] = add_values(@wall_area_sqft[:south_wall_area_sqft], other.wall_area_sqft[:south_wall_area_sqft])
          @wall_area_sqft[:east_wall_area_sqft] = add_values(@wall_area_sqft[:east_wall_area_sqft], other.wall_area_sqft[:east_wall_area_sqft])
          @wall_area_sqft[:west_wall_area_sqft] = add_values(@wall_area_sqft[:west_wall_area_sqft], other.wall_area_sqft[:west_wall_area_sqft])
          @wall_area_sqft[:total_wall_area_sqft] = add_values(@wall_area_sqft[:total_wall_area_sqft], other.wall_area_sqft[:total_wall_area_sqft])

          @roof_area_sqft[:equipment_roof_area_sqft] = add_values(@roof_area_sqft[:equipment_roof_area_sqft], other.roof_area_sqft[:equipment_roof_area_sqft])
          @roof_area_sqft[:photovoltaic_roof_area_sqft] = add_values(@roof_area_sqft[:photovoltaic_roof_area_sqft], other.roof_area_sqft[:photovoltaic_roof_area_sqft])
          @roof_area_sqft[:available_roof_area_sqft] = add_values(@roof_area_sqft[:available_roof_area_sqft], other.roof_area_sqft[:available_roof_area_sqft])
          @roof_area_sqft[:total_roof_area_sqft] = add_values(@roof_area_sqft[:total_roof_area_sqft], other.roof_area_sqft[:total_roof_area_sqft])
        end
      end
    end
  end
end