# -*- encoding: UTF-8 -*-
# enfeditor version: 0.1.1.1
require 'enfeditor'
require 'logger'
require_relative './daimler_setting_common.rb'
include EnfEditor

module EnfEditor
  class EEComponent
    def masterdata_exist?
      self.user_properties.each do |prop|
        if prop.subtype == "DAI_MasterData"
          return true
        end
      end
      return false
    end

    def delete_masterdata
      self.user_properties.each do |prop|
        prop.delete! if prop.subtype == "DAI_MasterData"
      end    
    end
  end
end

class MaterialPropertyMapper
  def initialize
    @logger = Logger.new( STDOUT )
  end

  def load( tsv_file )
    @material_map = nil
    unless File.exist?( tsv_file )
      @logger.info "TSV file for material mapping doesn't exist. \"" + tsv_file.to_s + "\""
      return false
    end
    @logger.info "Loading TSV file for material mapping. \"" + tsv_file.to_s + "\""
    begin
      # :encoding => "#{Encoding.locale_charmap}:UTF-8" : convert from ACP to UTF8
      # :converters => nil         : avoid numeric values and dates to be converted to specific types (leave as string)
      # :col_sep => "\t"           : modify column direction separator to handle TSV
      # :quote_char => "\x00"      : allow double quotation to be included in a TSV file (\x00 is a dummy)
      tsv_table = CSV.table( tsv_file, :encoding => "#{Encoding.locale_charmap}:UTF-8", :converters => nil, :col_sep => "\t", :quote_char => "\x00" )
    rescue => e
      @logger.error "Invalid TSV file. \"" + e.message.chomp +  "\""      
      return false
    end
    if tsv_table.empty?
      @logger.info "TSV file is empty."
      return false
    end
    unless tsv_table.headers.size == 19
      @logger.error "Invalid TSV file headers. \"" + tsv_table.headers.to_s.chomp + "\""
      return false
    end
    tsv_table.delete_if { |row| row.size != 19 }
    @material_map = Hash.new
    tsv_table.each do |row|
      mat_arr = row.values_at( 4..8 )
      mat_value1 = mat_arr.join( "\t" )
      mat_arr.map! {|item| item == '-' ? '' : item}
      mat_value2 = mat_arr.join( "\t" )
      @material_map[mat_value1] = row
      @material_map[mat_value2] = row if mat_value1 != mat_value2
    end    
  end  

  def apply( compo )
    return if @material_map.nil?
    return if @material_map.empty?
    prop = compo.first_user_property( 'j0Material2' )
    return if prop.nil?
    unless @material_map.include?( prop.value )    
      @logger.info "Corresponding QEVNumber not found for material \"" + prop.value + "\"."
      return
    end
    row = @material_map[prop.value]
    @logger.info "Corresponding QEVNumber \"" + row[1] + "\" found for material \"" + prop.value + "\"."
    compo.overwrite_user_property( 'j0QEVNumber', row[1], :text )
    compo.overwrite_user_property( 'j0Density', (row[11].to_f*1000.0).to_s, :text )
    compo.overwrite_user_property( 'j0Material2', row.values_at( 4..8 ).join( "\t" ), :text )
  end  
end

class ApplyDaimlerSetting
  @@unit_to_scale_table = {
    "kg"      => 1.0e3,
    "g"       => 1.0,
    "mg"      => 1.0e-3,
    "T"       => 1.0e6,
    "lb"      => 453.59237,
    "oz"      => 28.3495231,
    "kg_m3"   => 1.0e3 / (1.0e3**3),
    "lb_ft3"  => 453.59237 / (304.8**3),
    "lb_in3"  => 453.59237 / (25.4**3),
    "T_mm3"   => 1.0e6 / (1.0**3),
    "kg_mm3"  => 1.0e3 / (1.0**3),
    "g_cm3"   => 1.0 / (1.0e1**3),
    "g_mm3"   => 1.0 / (1.0**3),
    "kgxm2"   => 1.0e3 * (1.0e3**2),
    "gxmm2"   => 1.0 * (1.0**2),
    "lbxin2"  => 453.59237 * (25.4**2),
    "Txmm2"   => 1.0e6 * (1.0**2),
    "kgxmm2"  => 1.0e3 * (1.0**2),
    "gxcm2"   => 1.0 * (1.0e1**2),
    "mm"      => 1.0,
    "m"       => 1.0e3,
    "cm"      => 1.0e1,
    "km"      => 1.0e6,
    "in"      => 25.4,
    "ft"      => 304.8,
  }

  def initialize
    @logger = Logger.new( STDOUT )
  end

  def exec_with_log( message, &block )
    @logger.info "#{message} Start..."
    yield
    @logger.info "#{message} done"
  end

  def replace_invalid_character( name )
    new_name = nil
    # valid characters
    valid_chr = /[^a-zA-Z0-9\.\+_\-\(\)@]/
    new_name = name.gsub( valid_chr, '_' )
    return new_name
  end  
  
  def map_mass_properties( compo )
    # Mass properties are now correctly set by V5 to ENF.
    return
    return if compo.component_type != "PART"        

    # collect mass property info
    density = nil
    mass = nil        
    inertia = Array.new( 6 )    
    cog = Array.new( 3 )
    compo.user_properties.each do |prop|
      case prop.key
        when 'Density' then if prop.type == :text then density = prop.value end
        when 'Mass' then if prop.type == :text then mass = prop.value end
        when 'Ixx' then if prop.type == :text then inertia[0] = prop.value end
        when 'Iyy' then if prop.type == :text then inertia[1] = prop.value end
        when 'Izz' then if prop.type == :text then inertia[2] = prop.value end
        when 'Ixy' then if prop.type == :text then inertia[3] = prop.value end
        when 'Iyz' then if prop.type == :text then inertia[4] = prop.value end
        when 'Izx' then if prop.type == :text then inertia[5] = prop.value end
        when 'x' then if prop.type == :text then cog[0] = prop.value end
        when 'y' then if prop.type == :text then cog[1] = prop.value end
        when 'z' then if prop.type == :text then cog[2] = prop.value end
        when 'Iyx', 'Izy', 'Ixz', 'Surface', 'Volume', 'Center of Gravity', 'Inertia Matrix'
        else next
      end
      # remove mass properties
      prop.delete!
    end        

    # set inertia data    
    inertia_arr = []
    inertia.each do |elem|
      if elem.nil?        
        inertia_arr = nil
        @logger.warn "Lack of inertia data : " + compo.name
        break
      end
      if md = elem.match(/^([\d\.,\+\-e]+)(.+)$/)
        inertia_value = md[1]
        inertia_unit = md[2]
        if !@@unit_to_scale_table.include?(inertia_unit)          
          inertia_arr = nil
          @logger.warn "Unsupported inertia data unit(#{inertia_unit}) : " + compo.name
          break
        end              

        inertia_scale = @@unit_to_scale_table[inertia_unit]        
        inertia_arr << inertia_value.to_f * inertia_scale        
      else        
        inertia_arr = nil
        @logger.warn "Unable to parse inertia data (#{elem}) : " + compo.name
      end         
    end
    
    if inertia_arr
      compo.comprep_list.each do |comprep|
        if comprep.comprep_type == 'brep'          
          comprep.inertia_array = inertia_arr
        end
      end
    end    
    
    # set cog data
    cog_obj = EECoord.new
    cog.each_with_index do |elem, index|
      if elem.nil?
        cog_obj = nil
        @logger.warn "Lack of cog data : " + compo.name
        break
      end
      if md = elem.match(/^([\d\.,\+\-e]+)(.+)$/)
        cog_value = md[1]
        cog_unit = md[2]
        if !@@unit_to_scale_table.include?(cog_unit)
          cog_obj = nil
          @logger.warn "Unsupported cog data unit(#{cog_unit}) : " + compo.name
          break
        end

        cog_scale = @@unit_to_scale_table[cog_unit]
        real_value = cog_value.to_f * cog_scale
        case index
          when 0 then cog_obj.x = real_value
          when 1 then cog_obj.y = real_value
          when 2 then cog_obj.z = real_value
        end
      else
        cog_obj = nil
        @logger.warn "Unable to parse cog data (#{elem}) : " + compo.name
      end
    end

    if cog_obj
      compo.comprep_list.each do |comprep|
        if comprep.comprep_type == 'brep'
          comprep.gravity_point = cog_obj
        end
      end
    end

    # set density    
    if density.nil?      
      @logger.warn "Density data is lacking : " + compo.name
    else
      if md = density.match(/^([\d\.,\+\-e]+)(.+)$/)
        density_value = md[1]
        density_unit = md[2]
        if @@unit_to_scale_table.include?(density_unit)
          density_scale = @@unit_to_scale_table[density_unit]
          real_value = density_value.to_f * density_scale
          compo.comprep_list.each do |comprep|
            if comprep.comprep_type == 'brep'
              comprep.density = real_value
            end
          end
        else
          @logger.warn "Unsupported density unit(#{density_unit}) : " + compo.name          
        end        
      else                
        @logger.warn "Unable to parse density (#{density}) : " + compo.name      
      end
    end

    # set mass    
    if mass.nil?      
      @logger.warn "Mass data is lacking : " + compo.name
    else
      if md = mass.match(/^([\d\.,\+\-e]+)(.+)$/)
        mass_value = md[1]
        mass_unit = md[2]
        if @@unit_to_scale_table.include?(mass_unit) 
          mass_scale = @@unit_to_scale_table[mass_unit]
          real_value = mass_value.to_f * mass_scale
          compo.comprep_list.each do |comprep|
            if comprep.comprep_type == 'brep'
              comprep.mass = real_value
            end
          end
        else
          @logger.warn "Unsupported mass unit(#{mass_unit}) : " + compo.name          
        end
      else
        @logger.warn "Unable to parse mass (#{mass}) : " + compo.name
      end      
    end
  end

  def adjust_properties( session )
    avoid_jtsp_warning = false
    env_value = ENV['ELY_AVOID_JTSP_WARNING']
    if env_value && env_value.to_i > 0
      avoid_jtsp_warning = true
    end    

    base_dir = File.expand_path( File.dirname(__FILE__) )
    env_base_dir = ENV['ELY_MAPPING_FILE_BASE_DIR']
    base_dir = env_base_dir.gsub('\\', '/') if env_base_dir    
    mapper1 = UserPropertyMapper.new
    if avoid_jtsp_warning
      mapper1.load( base_dir + '/v5_jt_masterdata2.csv' )
    else
      mapper1.load( base_dir + '/v5_jt_masterdata.csv' )
    end
    mapper2 = UserPropertyMapper.new
    mapper2.load( base_dir + '/v5_jt.csv' )

    mapper3 = UserPropertyMapper.new
    material_mapper = MaterialPropertyMapper.new
    custom_dir = base_dir + '/../../../../../custom'
    if File.directory?( custom_dir )
      mapper3.load( custom_dir + '/v5_jt.csv' )
      material_mapper.load( custom_dir + '/DCMMstMaterial_v2.tbl' )
    else
      mapper3.load( base_dir + '/custom_v5_jt.csv' )
      material_mapper.load( base_dir + '/DCMMstMaterial_v2.tbl' )
    end                    

    session.model.components.each do |compo|
      compo.name = replace_invalid_character( compo.name )

      # create user properties from system properties
      unless compo.part_number.nil? || compo.part_number.empty?
        compo.overwrite_user_property( 'PartNumber', compo.part_number, :text )
      end
      unless compo.part_name.nil? || compo.part_name.empty?
        compo.overwrite_user_property( 'Nomenclature', compo.part_name, :text )
      end
      unless compo.part_revision.nil? || compo.part_revision.empty?
        compo.overwrite_user_property( 'Revision', compo.part_revision, :text )
      end
      unless compo.description.nil? || compo.description.empty?
        compo.overwrite_user_property( 'Description', compo.description, :text )
      end      

      # in case component has masterdata attributes
      if compo.masterdata_exist?
        # map masterdata attributes                
        mapper1.apply( compo, "DAI_MasterData" )        

        # delete masterdata
        compo.delete_masterdata

        # map daimler attributes
        mapper2.apply( compo )        

        # map material attributes
        material_mapper.apply( compo )
      end
      
      # map mass properties      
      map_mass_properties( compo )

      # general attribute mapping
      mapper3.apply( compo )      
    end
  end

  class FeatureGroupReplacer
    def initialize(compo)
      @compo = compo
      @compo.comprep_list.each do |comprep|
        next if comprep.comprep_type != 'brep'
        @comprep = comprep
        break
      end    
      @fgs = @compo.feature_groups      
      @fg_to_newfgs = {}
      @elem_to_newfg = {}

      # Rescue FG user attributes to volume
      child_fgs = []
      @fgs.each do |fg|
        fg.child_elements.each do |child_elem|
          child_elem = child_elem.cast # important
          next unless child_elem.instance_of?(EnfEditor::EEFeatureGroup)
          child_fgs << child_elem
        end
      end
      top_fgs = @fgs - child_fgs      
      top_fgs.each do |fg|
        volumes = []
        collect_child_volumes(fg, volumes)
        next unless volumes.size == 1
        fg.user_properties.each do |up|
          volumes[0].create_user_property(up.key, up.value, up.type)
        end
      end  
    end

    def traverse_views_and_replace
      views = @compo.cameras + @compo.captures
      return if views.empty?
      fgs_hidden = @fgs.select {|fg| fg.display? == false}
      newfgs_hidden = []
      fgs_hidden.each do |fg|
        newfgs_hidden += traverse_fg_and_replace(fg, false)
      end
      views.each do |view|
        # Handle invisible feature groups referenced from view
        newfgs_invis = []
        view.visibility_specified_element_get(:invisible).each do |elem|
          newfgs_invis += traverse_fg_and_replace(elem, false)
        end
        newfgs_invis.uniq!
        newfgs_invis.each {|newfg| view.element_visibility_set(newfg, :invisible)}
        # Handle visible feature groups referenced from view
        newfgs_vis = []
        view.visibility_specified_element_get(:visible).each do |elem|            
          newfgs_vis += traverse_fg_and_replace(elem, true)
        end
        newfgs_vis.uniq!
        newfgs_vis -= newfgs_invis # Invisible has higher priority
        newfgs_vis.each {|newfg| view.element_visibility_set(newfg, :visible)}
        # Force hidden feature groups to be hidden if not explicitly specified
        newfgs_hidden.each do |newfg|
          next if newfgs_invis.include?(newfg)
          next if newfgs_vis.include?(newfg)
          view.element_visibility_set(newfg, :invisible)
        end        
      end
    end

    def traverse_fg_and_replace(elem, visible_only)
      fg = elem.cast
      return [] unless fg.instance_of?(EnfEditor::EEFeatureGroup)      
      newfgs = []
      elems = []
      fg.child_elements.each do |child_elem|
        child_elem = child_elem.cast # important
        if child_elem.instance_of?(EnfEditor::EEFeatureGroup)
          next if visible_only && child_elem.display? == false
          newfgs += traverse_fg_and_replace(child_elem, visible_only)
        else
          elems << child_elem
        end
      end
      if elems.size > 0
        newfgs += replace_feature_group(fg, elems)
      end
      return newfgs
    end

    def replace_feature_group(fg, elems)
      ret = @fg_to_newfgs[fg]
      return ret unless ret.nil?
      newfgs = []
      # Make sure volumes are individually separated into feature groups
      newfgs += elems.map {|elem| create_parent_feature_group(elem) if elem.instance_of?(EnfEditor::EEBrepVolume)}.compact
      elems.delete_if {|elem| elem.instance_of?(EnfEditor::EEBrepVolume)}
      # Store in same feature group only if display and layer are identical
      display = elems.map {|elem| display?(elem)}.uniq
      layer = elems.map {|elem| layer(elem)}.uniq
      if display.size == 1 && layer.size == 1        
        newfg = @comprep.create_feature_group(fg.name, :general, elems)
        apply_attr(newfg, display[0], layer[0])        
        newfgs << newfg
      else
        newfgs += elems.map {|elem| create_parent_feature_group(elem)}
      end
      @fg_to_newfgs[fg] = newfgs
      return newfgs
    end

    def create_parent_feature_group(elem)
      ret = @elem_to_newfg[elem]
      return ret unless ret.nil?
      name = elem.name
      if name.nil? || name.empty?
        if elem.instance_of?(EnfEditor::EEBrepVolume)
          name = "Volume" 
        elsif elem.instance_of?(EnfEditor::EEBrepTSurface)
          name = "IsolatedSurface"        
        elsif elem.instance_of?(EnfEditor::EEBrepEdge)
          name = "IsolatedCurve"
        elsif elem.instance_of?(EnfEditor::EEBrepVertex)
          name = "IsolatedPoint"         
        else
          name = "Unknown"
        end
        name += elem.enf2_index.to_s
      end
      newfg = @comprep.create_feature_group(name, :general, [elem])
      apply_attr(newfg, display?(elem), layer(elem))
      # Restore volume user attributes to FG
      if elem.instance_of?(EnfEditor::EEBrepVolume)
        elem.user_properties.each do |up|
          newfg.create_user_property(up.key, up.value, up.type)
        end
      end
      @elem_to_newfg[elem] = newfg
      return newfg
    end    

    def display?(elem)
      if elem.instance_of?(EnfEditor::EEBrepTSurface)
        return elem.surface.display?
      else
        return elem.display?
      end
    end

    def layer(elem)
      if elem.instance_of?(EnfEditor::EEBrepTSurface)
        return elem.surface.layer
      else
        return elem.layer
      end
    end

    def apply_attr(newfg, display, layer)
      newfg.display = display unless display.nil?
      newfg.layer = layer unless layer.nil?      
    end

    def collect_child_volumes(fg, volumes)
      fg.child_elements.each do |child_elem|
        child_elem = child_elem.cast # important
        if child_elem.instance_of?(EnfEditor::EEFeatureGroup)
          collect_child_volumes(child_elem, volumes)
        elsif child_elem.instance_of?(EnfEditor::EEBrepVolume)
          volumes << child_elem
        end        
      end
    end

    def delete_original_feature_groups      
      @fgs.each do |fg|
        fg.user_properties.each{|up| up.delete!}
        fg.delete!
      end
    end
  end

  def adjust_layer_filters( session, keep_layer_filter )
    # Do not modify layer filters if keep_layer_filter=1
    layer_filters = session.model.layer_filters
    return if keep_layer_filter == 1 && layer_filters.size > 0      
    # Remove all layer filters
    layer_filters.each do |lf|
      lf.delete!
    end
    # Create default layer filter
    dlf = session.model.create_layer_filter("Show_Only", true)
    dlf.set_layer_number_visibility(0, true)        
    session.model.components.each do |compo|
      next if compo.assembly?      
      # Force following elements to be hidden:
      #   Sheet(Volume), IsoEdge, IsoVertex, Axis, IsoSurface
      elem_array_hidden = []
      elem_array_hidden += compo.volumes.select {|vol| vol.open_close_flag == :open}
      elem_array_hidden += compo.isolated_edges
      elem_array_hidden += compo.isolated_vertices
      elem_array_hidden += compo.axes
      compo.comprep_list.each do |comprep|
        next if comprep.comprep_type != 'brep'        
        elem_array_hidden += comprep.child_element_list.tsurfaces.map{|tsur| tsur.surface if tsur.faces.empty?}.compact
        break
      end      
      elem_array_hidden.each{|elem| elem.display = false}      
      # Reset layer numbers for following elements: 
      #   Volume, IsoEdge, IsoVertex, Axis, IsoSurface, Annotation
      elem_array = []
      elem_array += compo.volumes.select {|vol| vol.open_close_flag == :close}      
      elem_array += elem_array_hidden
      elem_array.each do |elem|
        if elem.display?
          elem.layer = 0
        else
          elem.layer = 1
        end
      end
      # Just reset for annotations as their visibility is controlled by views
      compo.annotations.each do |anno|
        anno.layer_number = 1
      end
      
      # Replace feature groups referenced from views to newly created flat feature groups
      replacer = FeatureGroupReplacer.new(compo)
      replacer.traverse_views_and_replace
      # Delete original feature groups to enforce 1to1 mapping of BodyNode and Volume in JT
      replacer.delete_original_feature_groups                  
    end
  end

  def validate_face_transparency( session )
    session.model.faces.each do |face|
      if face.has_material_color?
        color = face.material_color
        if color.color_type == :alpha
          if color.alpha[0] - 0.2 < 0.0
            face.material_color = EEMaterialColor.new(:alpha, [0.2])
          end
        elsif color.color_type == :rgba
          if color.rgba[3] - 0.2 < 0.0
            face.material_color = EEMaterialColor.new(:rgba, [color.rgba[0],color.rgba[1],color.rgba[2],0.2])
          end
        else
          @logger.warn "Alpha is not set"
        end
      else
        @logger.warn "Color is not set"
      end
    end
  end  

  def execute( argv )
    EnfEditor::EESession.open( argv ) do |session|
      input = session.parameter['inputfile']
      outputfile = session.parameter['outputfile']
      exec_with_log( "Reading enf:" + input.to_s ) do
        session.read_enf( input )
      end

      exec_with_log( "Validating face transparency" ) do
        validate_face_transparency( session )
      end

      exec_with_log( "Adjusting properties" ) do
        adjust_properties( session )
      end      

      keep_layer_filter = session.parameter['KeepLayerFilter']&.to_i || 0
      exec_with_log( "Adjusting layer filters" ) do
        adjust_layer_filters( session, keep_layer_filter )
      end

      exec_with_log( "Writing enf:" + outputfile.to_s ) do
        session.write_enf( outputfile )
      end
    end
  rescue Exception => ex
    @logger.error "Failed."
    @logger.error ex
    raise ex
  else
    @logger.info "Complete."
  end
end

if $0 == __FILE__
  # main program
  exec = ApplyDaimlerSetting.new
  exec.execute( ARGV )
end