# *********************************************************************************
# 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 'json'
require_relative 'validator'
require 'json-schema'

module URBANopt
  module Reporting
    module DefaultReports
      ##
      # ConstructionCost include construction cost information.
      ##
      class ConstructionCost
        attr_accessor :category, :item_name, :unit_cost, :cost_units, :item_quantity, :total_cost # :nodoc:

        ##
        # ConstructionCost class intialize all construction_cost attributes:
        # +:category+ , +:item_name+ , +:unit_cost+ , +:cost_units+ , +:item_quantity+ , +:total_cost+
        ##
        # [parameters:]
        # +hash+ - _Hash_ - A hash which may contain a deserialized construction_cost.
        ##
        def initialize(hash = {})
          hash.delete_if { |k, v| v.nil? }
          hash = defaults.merge(hash)

          @category = hash[:category]
          @item_name = hash[:item_name]
          @unit_cost = hash[:unit_cost]
          @cost_units = hash[:cost_units]
          @item_quantity = hash[:item_quantity]
          @total_cost = hash[:total_cost]

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

        ##
        # Assigns default values if attribute values do not exist.
        ##
        def defaults
          hash = {}
          hash[:category] = nil
          hash[:item_name] = nil
          hash[:unit_cost] = nil
          hash[:cost_units] = nil
          hash[:item_quantity] = nil
          hash[:total_cost] = nil

          return hash
        end

        ##
        # Converts to a Hash equivalent for JSON serialization.
        ##
        # - Exclude attributes with nil values.
        # - Validate construct_cost hash properties against schema.
        ##
        def to_hash
          result = {}
          result[:category] = @category if @category
          result[:item_name] = @item_name if @item_name
          result[:unit_cost] = @unit_cost if @unit_cost
          result[:cost_units] = @cost_units if @cost_units
          result[:item_quantity] = @item_quantity if @item_quantity
          result[:total_cost] = @total_cost if @total_cost

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

          return result
        end

        ##
        # Merges an +existing_cost+ with a +new_cost+:
        # - modify the existing_cost by summing the +:total_cost+ and +:item_quantity+ of new_cost and existing_cost.
        # - raise an error if +:category+ , +:cost_units+ and +:unit_cost+ are not identical
        ##
        # [Parameters:]
        # +existing_cost+ - _ConstructionCost_ - An object of ConstructionCost class.
        ##
        # +new_cost+ - _ConstructionCost_ - An object of ConstructionCost class.
        ##
        def self.merge_construction_cost(existing_cost, new_cost)
          # modify the existing_cost by adding the :total_cost and :item_quantity
          existing_cost.total_cost += new_cost.total_cost
          existing_cost.item_quantity += new_cost.item_quantity

          if existing_cost.category != new_cost.category
            raise "Cannot merge existing cost of category \"#{existing_cost.category}\" with new cost of category \"#{new_cost.category}\"."
          end

          if existing_cost.cost_units != new_cost.cost_units
            raise "Cannot merge existing cost with cost units \"#{existing_cost.cost_units}\" with new cost with cost units \"#{new_cost.cost_units}\". "
          end

          if existing_cost.unit_cost != new_cost.unit_cost
            raise "Cannot merge existing cost with unit cost \"#{existing_cost.unit_cost}\" with new cost with unit cost \"#{new_cost.unit_cost}\"; identical items should have identical unit cost."
          end

          return existing_cost
        end

        ##
        # Merges muliple construction costs together.
        # - loops over the new_costs and find the index of the cost with identical +:item_name+.
        # - if +item_name+ is identical then modify the existing_cost array by summing the :total_cost and :item_quantity. Else add the new_cost to existing_costs array.
        ##
        # [Parameters:]
        # +existing_costs+ - _Array_ - An array of ConstructionCost objects.
        ##
        # +new_costs+ - _Array_ - An array of ConstructionCost objects.
        def self.merge_construction_costs(existing_costs, new_costs)
          item_name_list = []
          item_name_list = existing_costs.collect(&:item_name)

          new_costs.each do |x_new|
            if item_name_list.include?(x_new.item_name)

              # when looping over the new_cost item_names find the index of the item_name_list with the same item name
              id = item_name_list.find_index(x_new.item_name) # the order of the item_name_list is the same as the order of the existing_cost hash-array

              # modify the existing_cost array by adding the :total_cost and :item_quantity when looping over costs
              existing_costs[id] = merge_construction_cost(existing_costs[id], x_new)

            else

              # insert the new hash in to the array
              existing_costs << x_new

            end
          end

          return existing_costs
        end
      end
    end
  end
end