# -*- encoding: UTF-8 -*-
# enfeditor version: 0.1.1.1
require 'enfeditor'
require 'logger'
require_relative './daimler_setting_common.rb'
include 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

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

  def adjust_layer_filters( session )
    default_found = false
    session.model.layer_filters.each do |lf|
      if lf.active?
        default_found = true
        break
      end
    end
    # Leave layer filter as-is if default layer filter is found
    return if default_found

    # Remove all layer filters
    session.model.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)
    # Reset layer numbers for following elements: 
    #   FeatureGroup, IsoEdge, IsoVertex, Axis, IsoSur, Annotation
    session.model.components.each do |compo|
      elem_array = []      
      elem_array.concat compo.feature_groups
      elem_array.concat compo.isolated_edges
      elem_array.concat compo.isolated_vertices
      elem_array.concat compo.axes      
      compo.comprep_list.each do |comprep|
        next if comprep.comprep_type != 'brep'        
        elem_array.concat comprep.child_element_list.tsurfaces.map{|tsur| tsur.surface if tsur.faces.empty?}.compact
      end
      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
    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

      exec_with_log( "Adjusting layer filters" ) do
        adjust_layer_filters( session )
      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