# *******************************************************************************
# OpenStudio(R), Copyright (c) 2008-2021, Alliance for Sustainable Energy, LLC.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# (1) Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# (2) 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.
#
# (3) Neither the name of the copyright holder nor the names of any contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission from the respective party.
#
# (4) Other than as required in clauses (1) and (2), distributions in any form
# of modifications or other derivative works may not use the "OpenStudio"
# trademark, "OS", "os", or any other confusingly similar designation without
# specific prior written permission from Alliance for Sustainable Energy, LLC.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY 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(S), ANY CONTRIBUTORS, THE
# UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
# THEIR EMPLOYEES, 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.
# *******************************************************************************

# insert your copyright here

# see the URL below for information on how to write OpenStudio measures
# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/

require 'json'

# start the measure
class MergeFloorspaceJsWithModel < OpenStudio::Measure::ModelMeasure
  # human readable name
  def name
    # Measure name should be the title case of the class name.
    return 'Merge FloorspaceJs with Model'
  end

  # human readable description
  def description
    return 'This measure will import a FloorspacJS JSON file into an OpenStudio model. This is meant to function in similar way to the merge function in the geometry editor of the OpenStudio Applicaiton.'
  end

  # human readable description of modeling approach
  def modeler_description
    return 'This measure is based off of the ResidentialGeometryCreateFromFloorspaceJS measure on the OpenStudio-Buildstock repository'
  end

  # define the arguments that the user will input
  def arguments(model)
    args = OpenStudio::Measure::OSArgumentVector.new

    # path to the floorplan JSON file to load
    floorplan_path = OpenStudio::Measure::OSArgument.makeStringArgument('floorplan_path', true)
    floorplan_path.setDisplayName('Floorplan Path')
    floorplan_path.setDescription('Path to the floorplan JSON.')
    args << floorplan_path

    return args
  end

  # define what happens when the measure is run
  def run(model, runner, user_arguments)
    super(model, runner, user_arguments)

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

    # assign the user inputs to variables
    floorplan_path = runner.getStringArgumentValue('floorplan_path', user_arguments)

    # check the floorplan_path for reasonableness
    if floorplan_path.empty?
      runner.registerError('Empty floorplan path was entered.')
      return false
    end

    path = runner.workflow.findFile(floorplan_path)
    if path.empty?
      runner.registerError("Cannot find floorplan path '#{floorplan_path}'.")
      return false
    end

    json = nil
    File.open(path.get.to_s, 'r') do |file|
      json = file.read
    end

    floorplan = OpenStudio::FloorplanJS.load(json)
    if floorplan.empty?
      runner.registerError("Cannot load floorplan from '#{floorplan_path}'.")
      return false
    end

    scene = floorplan.get.toThreeScene(true)
    rt = OpenStudio::Model::ThreeJSReverseTranslator.new
    new_model = rt.modelFromThreeJS(scene)

    unless new_model.is_initialized
      runner.registerError('Cannot convert floorplan to model.')
      return false
    end
    new_model = new_model.get

    runner.registerInitialCondition("Initial model has #{model.getPlanarSurfaceGroups.size} planar surface groups.")

    mm = OpenStudio::Model::ModelMerger.new
    mm.mergeModels(model, new_model, rt.handleMapping)

    mm.warnings.each do |warnings|
      runner.registerWarning(warnings.logMessage)
    end

    # put all of the spaces in the model into a vector
    spaces = OpenStudio::Model::SpaceVector.new
    model.getSpaces.each do |space|
      spaces << space
    end

    # intersect and match surfaces for each space in the vector
    # todo - add in diagnostic intersect as option
    # OpenStudio::Model.intersectSurfaces(spaces)
    # OpenStudio::Model.matchSurfaces(spaces)

    # removing duplicate points in a surface
    model.getPlanarSurfaces.each do |surface|
      array = []
      vertices = surface.vertices
      fixed = false
      vertices.each do |vertex|
        next if fixed
        if array.include?(vertex)
          # create a new set of vertices
          new_vertices = OpenStudio::Point3dVector.new
          array_b = []
          surface.vertices.each do |vertex_b|
            next if array_b.include?(vertex_b)
            new_vertices << vertex_b
            array_b << vertex_b
          end
          surface.setVertices(new_vertices)
          num_removed = vertices.size - surface.vertices.size
          runner.registerWarning("#{surface.name} has duplicate vertices. Started with #{vertices.size} vertices, removed #{num_removed}.")
          fixed = true
        else
          array << vertex
        end
      end
    end

    # remove collinear points in a surface
    model.getPlanarSurfaces.each do |surface|
      new_vertices = OpenStudio.removeCollinear(surface.vertices)
      starting_count = surface.vertices.size
      final_count = new_vertices.size
      if final_count < starting_count
        runner.registerWarning("Removing #{starting_count - final_count} collinear vertices from #{surface.name}.")
        surface.setVertices(new_vertices)
      end
    end

    # remove duplicate surfaces in a space (should be done after remove duplicate and collinear points)
    model.getSpaces.each do |space|
      # secondary array to compare against
      surfaces_b = space.surfaces.sort

      space.surfaces.sort.each do |surface_a|
        # delete from secondary array
        surfaces_b.delete(surface_a)

        surfaces_b.each do |surface_b|
          next if surface_a == surface_b # dont' test against same surface
          if surface_a.equalVertices(surface_b)
            runner.registerWarning("#{surface_a.name} and #{surface_b.name} in #{space.name} have duplicate geometry, removing #{surface_b.name}.")
            surface_b.remove
          elsif surface_a.reverseEqualVertices(surface_b)
            # TODO: - add logic to determine which face naormal is reversed and which is correct
            runner.registerWarning("#{surface_a.name} and #{surface_b.name} in #{space.name} have reversed geometry, removing #{surface_b.name}.")
            surface_b.remove
          end
        end
      end
    end

    # secondary array of spaces that we can remove items from once they have gone through in primary loop
    spaces_b = model.getSpaces.sort

    # looping through vector of each space
    model.getSpaces.sort.each do |space_a|
      runner.registerInfo("Intersecting and matching surfaces for #{space_a.name}.")

      # delete from secondary array
      spaces_b.delete(space_a)

      spaces_b.each do |space_b|
        # runner.registerInfo("Intersecting and matching surfaces between #{space_a.name} and #{space.name}")
        spaces = OpenStudio::Model::SpaceVector.new
        spaces << space_a
        spaces << space_b

        # intersect and match surfaces in pair of spaces
        OpenStudio::Model.intersectSurfaces(spaces)
        OpenStudio::Model.matchSurfaces(spaces)
      end
    end

    json = JSON.parse(File.read(path.get.to_s))

    # error checking
    unless !json['space_types'].empty?
      runner.registerInfo('No space types were created.')
    end

    # set the space type standards fields based on what user wrote in the editor
    json['space_types'].each do |st|
      model.getSpaceTypes.each do |space_type|
        next unless space_type.name.to_s.include? st['name']
        next if space_type.standardsSpaceType.is_initialized

        space_type.setStandardsSpaceType(st['name'])
      end
    end

    # remove any unused space types
    model.getSpaceTypes.each do |space_type|
      if space_type.spaces.empty?
        space_type.remove
      end
    end

    # for any spaces with no assigned zone, create (unless another space of the same space type has an assigned zone) a thermal zone based on the space type
    # todo - add argument to enable disable zone creation
    model.getSpaceTypes.each do |space_type|
      space_type.spaces.each do |space|
        unless space.thermalZone.is_initialized
          thermal_zone = OpenStudio::Model::ThermalZone.new(model)
          thermal_zone.setName(space.name.to_s)
          space.setThermalZone(thermal_zone)
        end
      end
    end

    # report final condition of model
    runner.registerFinalCondition("Final model has #{model.getPlanarSurfaceGroups.size} planar surface groups.")

    return true
  end
end

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