# *********************************************************************************
# 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 'erb'
require 'date'

# This measure is originally from https://github.com/urbanopt/DES_HVAC
# start the measure
class ExportModelicaLoads < OpenStudio::Measure::ReportingMeasure
  # human readable name
  def name
    # Measure name should be the title case of the class name.
    return 'Export Modelica Loads'
  end

  def description
    return 'Use the results from the EnergyPlus simulation to generate a load file for use in Modelica. This will create a MOS and a CSV file of the heating, cooling, and hot water loads.'
  end

  def modeler_description
    return ''
  end

  def log(str)
    puts "#{Time.now}: #{str}"
  end

  def arguments(_model)
    args = OpenStudio::Measure::OSArgumentVector.new

    # this measure does not require any user arguments, return an empty list
    return args
  end

  # return a vector of IdfObject's to request EnergyPlus objects needed by the run method
  def energyPlusOutputRequests(runner, user_arguments)
    super(runner, user_arguments)

    result = OpenStudio::IdfObjectVector.new
    result << OpenStudio::IdfObject.load('Output:Variable,,Site Mains Water Temperature,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Drybulb Temperature,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Relative Humidity,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Meter,Cooling:Electricity,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Meter,Heating:Electricity,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Meter,Heating:NaturalGas,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Meter,InteriorLights:Electricity,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Meter,Fans:Electricity,timestep;').get
    result << OpenStudio::IdfObject.load('Output:Meter,InteriorEquipment:Electricity,timestep;').get # Joules
    result << OpenStudio::IdfObject.load('Output:Meter,ExteriorLighting:Electricity,timestep;').get # Joules
    result << OpenStudio::IdfObject.load('Output:Meter,Electricity:Facility,timestep;').get # Joules
    result << OpenStudio::IdfObject.load('Output:Meter,Electricity:Facility,timestep;').get # #Using this for data at timestep interval
    result << OpenStudio::IdfObject.load('Output:Meter,Gas:Facility,timestep;').get # Joules
    result << OpenStudio::IdfObject.load('Output:Meter,Heating:EnergyTransfer,timestep;').get # Joules
    result << OpenStudio::IdfObject.load('Output:Meter,WaterSystems:EnergyTransfer,timestep;').get # Joules
    # these variables are used for the modelica export.
    result << OpenStudio::IdfObject.load('Output:Variable,*,Zone Predicted Sensible Load to Setpoint Heat Transfer Rate,timestep;').get # watts according to e+
    result << OpenStudio::IdfObject.load('Output:Variable,*,Water Heater Total Demand Heat Transfer Rate,timestep;').get # Watts

    return result
  end

  def extract_timeseries_into_matrix(sqlfile, data, variable_name, key_value = nil, default_if_empty = 0, timestep)
    log "Executing query for #{variable_name}"
    column_name = variable_name
    if key_value
      # ts = sqlfile.timeSeries('RUN PERIOD 1', 'Hourly', variable_name, key_value)
      ts = sqlfile.timeSeries('RUN PERIOD 1', 'Zone Timestep', variable_name, key_value)
      column_name += "_#{key_value}"
    else
      # ts = sqlfile.timeSeries('RUN PERIOD 1', 'Hourly', variable_name)
      ts = sqlfile.timeSeries('RUN PERIOD 1', 'Zone Timestep', variable_name)
    end
    log 'Iterating over timeseries'
    column = [column_name.delete(':').delete(' ')] # Set the header of the data to the variable name, removing : and spaces

    if ts.empty?
      log "No time series for #{variable_name}:#{key_value}... defaulting to #{default_if_empty}"
      # needs to be data.size-1 since the column name is already stored above (+=)
      column += [default_if_empty] * (data.size - 1)
    else
      ts = ts.get if ts.respond_to?(:get)
      ts = ts.first if ts.respond_to?(:first)

      start = Time.now
      # Iterating in OpenStudio can take up to 60 seconds with 10min data. The quick_proc takes 0.03 seconds.
      # for i in 0..ts.values.size - 1
      #   log "... at #{i}" if i % 10000 == 0
      #   column << ts.values[i]
      # end

      quick_proc = ts.values.to_s.split(',')

      # the first and last have some cleanup items because of the Vector method
      quick_proc[0] = quick_proc[0].gsub(/^.*\(/, '')
      quick_proc[-1] = quick_proc[-1].delete(')')
      column += quick_proc

      log "Took #{Time.now - start} to iterate"
    end

    log 'Appending column to data'

    # append the data to the end of the rows
    if column.size == data.size
      data.each_index do |index|
        data[index] << column[index]
      end
    end

    log "Finished extracting #{variable_name}"
  end

  def create_new_variable_sum(data, new_var_name, include_str, options = nil)
    var_info = {
      name: new_var_name,
      var_indexes: []
    }
    data.each_with_index do |row, index|
      if index.zero?
        # Get the index of the columns to add
        row.each do |c|
          if c.include? include_str
            var_info[:var_indexes] << row.index(c)
          end
        end

        # add the new var to the header row
        data[0] << var_info[:name]
      else
        # sum the values
        sum = 0
        var_info[:var_indexes].each do |var|
          temp_v = row[var].to_f
          if options.nil?
            sum += temp_v
          elsif options[:positive_only] && temp_v > 0
            sum += temp_v
          elsif options[:negative_only] && temp_v < 0
            sum += temp_v
          end
        end

        # Also round the data here, because we don't need 10 decimals
        data[index] << sum.round(1)
      end
    end
  end

  def run(runner, user_arguments)
    super(runner, user_arguments)

    # get the last model and sql file
    model = runner.lastOpenStudioModel
    if model.empty?
      runner.registerError('Cannot find last model.')
      return false
    end
    model = model.get

    # use the built-in error checking
    return false unless runner.validateUserArguments(arguments(model), user_arguments)

    # get the last model and sql file
    model = runner.lastOpenStudioModel
    if model.empty?
      runner.registerError('Cannot find last model.')
      return false
    end
    model = model.get

    timesteps_per_hour = model.getTimestep.numberOfTimestepsPerHour.to_i
    timestep = 60 / timesteps_per_hour # timestep in minutes

    sqlFile = runner.lastEnergyPlusSqlFile
    if sqlFile.empty?
      runner.registerError('Cannot find last sql file.')
      return false
    end
    sqlFile = sqlFile.get
    model.setSqlFile(sqlFile)

    # create a new csv with the values and save to the reports directory.
    # assumptions:
    #   - all the variables exist
    #   - data are the same length

    # initialize the rows with the header
    log 'Starting to process Timeseries data'
    # Initial header row
    rows = [
      ['Date Time', 'Month', 'Day', 'Day of Week', 'Hour', 'Minute', 'SecondsFromStart']
    ]

    # just grab one of the variables to get the date/time stamps
    attribute_name = 'Electricity:Facility'
    ts = sqlFile.timeSeries('RUN PERIOD 1', 'Zone Timestep', attribute_name)
    if ts.empty?
      runner.registerError("This feature does not have the attribute '#{attribute_name}' to enable this measure to work." \
      'To resolve, simulate a building with electricity or remove this measure from your workflow.')
    else
      ts = ts.first
      dt_base = nil
      # Save off the date time values
      ts.dateTimes.each_with_index do |dt, index|
        dt_base = DateTime.parse(dt.to_s) if dt_base.nil?
        dt_current = DateTime.parse(dt.to_s)
        rows << [
          DateTime.parse(dt.to_s).strftime('%m/%d/%Y %H:%M'),
          dt.date.monthOfYear.value,
          dt.date.dayOfMonth,
          dt.date.dayOfWeek.value,
          dt.time.hours,
          dt.time.minutes,
          dt_current.to_time.to_i - dt_base.to_time.to_i + timestep * 60
        ]
      end
    end

    # add in the other variables by columns -- should really pull this from the report variables defined above
    extract_timeseries_into_matrix(sqlFile, rows, 'Site Outdoor Air Drybulb Temperature', 'Environment', 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Site Outdoor Air Relative Humidity', 'Environment', 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Heating:Electricity', nil, 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Heating:NaturalGas', nil, 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Cooling:Electricity', nil, 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Electricity:Facility', nil, 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Gas:Facility', nil, 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'Heating:EnergyTransfer', nil, 0, timestep)
    extract_timeseries_into_matrix(sqlFile, rows, 'WaterSystems:EnergyTransfer', nil, 0, timestep)

    # get all zones and save the names for later use in aggregation.
    tz_names = []
    model.getThermalZones.each do |tz|
      tz_names << tz.name.get if tz.name.is_initialized
      extract_timeseries_into_matrix(sqlFile, rows, 'Zone Predicted Sensible Load to Setpoint Heat Transfer Rate', tz_names.last, 0, timestep)
      extract_timeseries_into_matrix(sqlFile, rows, 'Water Heater Heating Rate', tz_names.last, 0, timestep)
    end

    # sum up a couple of the columns and create a new columns
    create_new_variable_sum(rows, 'TotalSensibleLoad', 'ZonePredictedSensibleLoadtoSetpointHeatTransferRate')
    create_new_variable_sum(rows, 'TotalCoolingSensibleLoad', 'ZonePredictedSensibleLoadtoSetpointHeatTransferRate', negative_only: true)
    create_new_variable_sum(rows, 'TotalHeatingSensibleLoad', 'ZonePredictedSensibleLoadtoSetpointHeatTransferRate', positive_only: true)
    create_new_variable_sum(rows, 'TotalWaterHeating', 'WaterHeaterHeatingRate')

    # convert this to CSV object
    File.open('./building_loads.csv', 'w') do |f|
      rows.each do |row|
        f << row.join(',') << "\n"
      end
    end

    # covert the row data into the format needed by modelica
    modelica_data = [['seconds', 'cooling', 'heating', 'waterheating']]
    seconds_index = nil
    total_water_heating_index = nil
    total_cooling_sensible_index = nil
    total_heating_sensible_index = nil
    peak_cooling = 0
    peak_heating = 0
    peak_water_heating = 0
    rows.each_with_index do |row, index|
      if index.zero?
        seconds_index = row.index('SecondsFromStart')
        total_cooling_sensible_index = row.index('TotalCoolingSensibleLoad')
        total_heating_sensible_index = row.index('TotalHeatingSensibleLoad')
        total_water_heating_index = row.index('TotalWaterHeating')
      else
        new_data = [
          row[seconds_index],
          row[total_cooling_sensible_index],
          row[total_heating_sensible_index],
          row[total_water_heating_index]
        ]

        modelica_data << new_data

        # store the peaks
        peak_cooling = row[total_cooling_sensible_index] if row[total_cooling_sensible_index] < peak_cooling
        peak_heating = row[total_heating_sensible_index] if row[total_heating_sensible_index] > peak_heating
        peak_water_heating = row[total_water_heating_index] if row[total_water_heating_index] > peak_water_heating
      end
    end

    File.open('./modelica.mos', 'w') do |f|
      f << "#1\n"
      f << "#Heating and Cooling Model loads from OpenStudio Prototype Buildings\n"
      f << "#  Building Type: {{BUILDINGTYPE}}\n"
      f << "#  Climate Zone: {{CLIMATEZONE}}\n"
      f << "#  Vintage: {{VINTAGE}}\n"
      f << "#  Simulation ID (for debugging): {{SIMID}}\n"
      f << "#  URL: https://github.com/urbanopt/openstudio-prototype-loads\n"
      f << "\n"
      f << "#First column: Seconds in the year (loads are hourly)\n"
      f << "#Second column: cooling loads in Watts (as negative numbers).\n"
      f << "#Third column: space heating loads in Watts\n"
      f << "#Fourth column: water heating in Watts\n"
      f << "\n"
      f << "#Peak space cooling load = #{peak_cooling} Watts\n"
      f << "#Peak space heating load = #{peak_heating} Watts\n"
      f << "#Peak water heating load = #{peak_water_heating} Watts\n"
      f << "double tab1(8760,4)\n"
      modelica_data.each_with_index do |row, index|
        next if index.zero?
        f << row.join(';') << "\n"
      end
    end

    # Find the total runtime for energyplus and save it into a registerValue
    total_time = -999
    location_of_file = ['../eplusout.end', './run/eplusout.end']
    first_index = location_of_file.map { |f| File.exist?(f) }.index(true)
    if first_index
      match = File.read(location_of_file[first_index]).to_s.match(/Elapsed.Time=(.*)hr(.*)min(.*)sec/)
      total_time = match[1].to_i * 3600 + match[2].to_i * 60 + match[3].to_f
    end

    runner.registerValue('energyplus_runtime', total_time, 'sec')
    runner.registerValue('peak_cooling_load', peak_cooling, 'W')
    runner.registerValue('peak_heating_load', peak_heating, 'W')
    runner.registerValue('peak_water_heating', peak_water_heating, 'W')

    return true
  ensure
    sqlFile.close if sqlFile
  end
end

# register the measure to be used by the application
ExportModelicaLoads.new.registerWithApplication