require 'securerandom'

module Mittsu
  class Geometry
    include EventDispatcher

    MorphNormal = Struct.new(:face_normals, :vertex_normals)
    Normal = Struct.new(:a, :b, :c)

    attr_accessor :name, :vertices, :colors, :faces, :face_vertex_uvs, :morph_targets, :morph_colors, :morph_normals, :skin_weights, :skin_indices, :line_distances, :bounding_box, :bounding_sphere, :has_tangents, :dynamic

    attr_accessor(*%w(vertices morph_targets elements uvs normals tangents colors line_distances groups).map { |s| "#{s}_need_update".to_sym })

    attr_reader :id, :uuid, :type

    def initialize
      super

      @id = (@@id ||= 1).tap { @@id += 1 }

      @uuid = SecureRandom.uuid

      @name = ''
      @type = 'Geometry'

      @vertices = []
      @colors = [] # one-to-one vertex colors, used in Points and Line

      @faces = []

      @face_vertex_uvs = [[]]

      @morph_targets = []
      @morph_colors = []
      @morph_normals = []

      @skin_weights = []
      @skin_indices = []

      @line_distances = []

      @has_tangents = false

      @vertices_need_update = false
      @morph_targets_need_update = false
      @elements_need_update = false
      @uvs_need_update = false
      @normals_need_update = false
      @tangents_need_update = false
      @colors_need_update = false
      @line_distances_need_update = false
      @groups_need_update = false

      @_listeners = {}
    end

    def apply_matrix(matrix)
      normal_matrix = Mittsu::Matrix3.new.get_normal_matrix(matrix)
      @vertices.each do |vertex|
        vertex.apply_matrix4(matrix)
      end
      @faces.each do |face|
        face.normal.apply_matrix3(normal_matrix).normalize
        face.vertex_normals.each do |vertex_normal|
          vertex_normal.apply_matrix3(normal_matrix).normalize
        end
      end
      if @bounding_box
        self.compute_bounding_box
      end
      if @bounding_sphere
        self.compute_bounding_sphere
      end
      @vertices_need_update = true
      @normals_need_update = true
    end

    def from_buffer_geometry(geometry)
      scope = self
      vertices = geometry[:position].array
      indices = geometry[:index].nil? ? nil : geometry[:index].array
      normals = geometry[:normal].nil? ? nil : geometry[:normal].array
      colors = geometry[:color].nil? ? nil : geometry[:color].array
      uvs = geometry[:uv].nil? ? nil : geometry[:uv].array
      temp_normals = []
      temp_uvs = []
      i, j = 0, 0
      while i < vertices.length
        scope.vertices << Mittsu::Vector3.new(vertices[i], vertices[i + 1], vertices[i + 2])
        if normals
          temp_normals << Mittsu::Vector3.new(normals[i], normals[i + 1], normals[i + 2])
        end
        if colors
          scope.colors << Mittsu::Color.new(colors[i], colors[i + 1], colors[i + 2])
        end
        if uvs
          temp_uvs << Mittsu::Vector2.new(uvs[j], uvs[j + 1])
        end
        i += 3; j += 2
      end
      add_face = -> (a, b, c) {
        vertex_normals = normals.nil? ? [] : [temp_normals[a].clone, temp_normals[b].clone, temp_normals[c].clone]
        vertex_colors = colors.nil? ? [] : [scope.colors[a].clone, scope.colors[b].clone, scope.colors[c].clone]
        scope.faces << Mittsu::Face3.new(a, b, c, vertex_normals, vertex_colors)
        if uvs
          scope.face_vertex_uvs[0] << [temp_uvs[a].clone, temp_uvs[b].clone, temp_uvs[c].clone]
        end
      }
      if indices
        draw_calls = geometry.draw_calls
        if !draw_calls.empty?
          draw_calls.each do |draw_call|
            start = draw_call.start
            count = draw_call.count
            index = draw_call.index
            j = start
            jl = start + count
            while j < jl
              add_face[index + indices[j], index + indices[j + 1], index + indices[j + 2]]
              j += 3
            end
          end
        else
          indices.each_slice(3).with_index do |index|
            add_face[*index]
          end
        end
      else
        i = 0, il = vertices.length / 3
        while i < il
          add_face[i, i + 1, i + 2]
          i += 3
        end
      end
      self.compute_face_normals
      if geometry.bounding_box
        @bounding_box = geometry.bounding_box.clone
      end
      if geometry.bounding_sphere
        @bounding_sphere = geometry.bounding_sphere.clone
      end
      self
    end

    def center
      self.compute_bounding_box
      offset = @bounding_box.center.negate
      self.apply_matrix(Mittsu::Matrix4.new.set_position(offset))
      offset
    end

    def compute_face_normals
      cb, ab = Mittsu::Vector3.new, Mittsu::Vector3.new
      @faces.each do |face|
        v_a = @vertices[face.a]
        v_b = @vertices[face.b]
        v_c = @vertices[face.c]
        cb.sub_vectors(v_c, v_b)
        ab.sub_vectors(v_a, v_b)
        cb.cross(ab)
        cb.normalize
        face.normal.copy(cb)
      end
    end

    def compute_vertex_normals(area_weighted = false)
      vertices = Array.new(@vertices.length)
      @vertices.length.times do |v|
        vertices[v] = Mittsu::Vector3.new
      end
      if area_weighted
        # vertex normals weighted by triangle areas
        # http:#www.iquilezles.org/www/articles/normals/normals.htm
        cb = Mittsu::Vector3.new, ab = Mittsu::Vector3.new
        @faces.each do |face|
          v_a = @vertices[face.a]
          v_b = @vertices[face.b]
          v_c = @vertices[face.c]
          cb.sub_vectors(v_c, v_b)
          ab.sub_vectors(v_a, v_b)
          cb.cross(ab)
          vertices[face.a].add(cb)
          vertices[face.b].add(cb)
          vertices[face.c].add(cb)
        end
      else
        @faces.each do |face|
          vertices[face.a].add(face.normal)
          vertices[face.b].add(face.normal)
          vertices[face.c].add(face.normal)
        end
      end
      @vertices.each(&:normalize)
      @faces.each do |face|
        face.vertex_normals[0] = vertices[face.a].clone
        face.vertex_normals[1] = vertices[face.b].clone
        face.vertex_normals[2] = vertices[face.c].clone
      end
    end

    def compute_morph_normals
      # save original normals
      # - create temp variables on first access
      #   otherwise just copy (for faster repeated calls)
      @_original_face_normal ||= []
      @_original_vertex_normals ||= []
      @faces.each_with_index do |face, f|
        if !@_original_face_normal[f]
          @_original_face_normal[f] = face.normal.clone
        else
          @_original_face_normal[f].copy(face.normal)
        end
        @_original_vertex_normals[f] ||= []
        face.vertex_normals.each_with_index do |vnorm, i|
          if !@_original_vertex_normals[f][i]
            @_original_vertex_normals[f][i] = vnorm.clone
          else
            @_original_vertex_normals[f][i].copy(face.vertex_normals[i])
          end
        end
      end

      # use temp geometry to compute face and vertex normals for each morph
      tmp_geo = Mittsu::Geometry.new
      tmp_geo.faces = @faces
      @morph_targets.each_with_index do |morph_target, i|
        # create on first access
        if !@morph_normals[i]
          @morph_normals[i] = MorphNormal.new([], [])
          dst_normals_face = @morph_normals[i].face_normals
          dst_normals_vertex = @morph_normals[i].vertex_normals
          @faces.each do
            face_normal = Mittsu::Vector3.new
            vertex_normals = Normal.new(Mittsu::Vector3.new, Mittsu::Vector3.new, Mittsu::Vector3.new)
            dst_normals_face << face_normal
            dst_normals_vertex << vertex_normals
          end
        end
        # set vertices to morph target
        tmp_geo.vertices = @morph_targets[i].vertices
        # compute morph normals
        tmp_geo.compute_face_normals
        tmp_geo.compute_vertex_normals
        # store morph normals
        @faces.each_with_index do |face, f|
          face_normal = @morph_normals[i].face_normals[f]
          vertex_normals = @morph_normals[i].vertex_normals[f]
          face_normal.copy(face.normal)
          vertex_normals.a.copy(face.vertex_normals[0])
          vertex_normals.b.copy(face.vertex_normals[1])
          vertex_normals.c.copy(face.vertex_normals[2])
        end
      end
      # restore original normals
      @faces.each_with_index do |face, f|
        face.normal = @_original_face_normal[f]
        face.vertex_normals = @_original_vertex_normals[f]
      end
    end

    def compute_tangents
      # based on http:#www.terathon.com/code/tangent.html
      # tangents go to vertices
      tan1 = []; tan2 = [],
      sdir = Mittsu::Vector3.new; tdir = Mittsu::Vector3.new
      tmp = Mittsu::Vector3.new; tmp2 = Mittsu::Vector3.new
      n = Mittsu::Vector3.new
      uv = []
      @vertices.each_index do |v|
        tan1[v] = Mittsu::Vector3.new
        tan2[v] = Mittsu::Vector3.new
      end
      handle_triangle = -> (context, a, b, c, ua, ub, uc) {
        v_a = context.vertices[a]
        v_b = context.vertices[b]
        v_c = context.vertices[c]
        uv_a = uv[ua]
        uv_b = uv[ub]
        uv_c = uv[uc]
        x1 = v_b.x - v_a.x
        x2 = v_c.x - v_a.x
        y1 = v_b.y - v_a.y
        y2 = v_c.y - v_a.y
        z1 = v_b.z - v_a.z
        z2 = v_c.z - v_a.z
        s1 = uv_b.x - uv_a.x
        s2 = uv_c.x - uv_a.x
        t1 = uv_b.y - uv_a.y
        t2 = uv_c.y - uv_a.y
        r = 1.0 / (s1 * t2 - s2 * t1)
        sdir.set((t2 * x1 - t1 * x2) * r,
              (t2 * y1 - t1 * y2) * r,
              (t2 * z1 - t1 * z2) * r)
        tdir.set((s1 * x2 - s2 * x1) * r,
              (s1 * y2 - s2 * y1) * r,
              (s1 * z2 - s2 * z1) * r)
        tan1[a].add(sdir)
        tan1[b].add(sdir)
        tan1[c].add(sdir)
        tan2[a].add(tdir)
        tan2[b].add(tdir)
        tan2[c].add(tdir)
      }
      @faces.each_with_index do |face, f|
        uv = @face_vertex_uvs[0][f] # use UV layer 0 for tangents
        handle_triangle[self, face.a, face.b, face.c, 0, 1, 2]
      end
      face_index = ['a', 'b', 'c', 'd']
      @faces.each_with_index do |face, f|
        [face.vertex_normals.length, 3].min.times do |i|
          n.copy(face.vertex_normals[i])
          vertex_index = face[face_index[i]]
          t = tan1[vertex_index]
          # Gram-Schmidt orthogonalize
          tmp.copy(t)
          tmp.sub(n.multiply_scalar(n.dot(t))).normalize
          # Calculate handedness
          tmp2.cross_vectors(face.vertex_normals[i], t)
          test = tmp2.dot(tan2[vertex_index])
          w = (test < 0.0) ? - 1.0 : 1.0
          face.vertex_tangents[i] = Mittsu::Vector4.new(tmp.x, tmp.y, tmp.z, w)
        end
      end
      @has_tangents = true
    end

    def compute_line_distances
      d = 0
      @vertices.each_with_index do |vertex, i|
        if i > 0
          d += vertex.distance_to(@vertices[i - 1])
        end
        @line_distances[i] = d
      end
    end

    def compute_bounding_box
      @bounding_box ||= Mittsu::Box3.new
      @bounding_box.set_from_points(@vertices)
    end

    def compute_bounding_sphere
      @bounding_sphere ||= Mittsu::Sphere.new
      @bounding_sphere.set_from_points(@vertices)
    end

    def merge(geometry, matrix = nil, material_index_offset = nil)
      if !geometry.is_a? Mittsu::Geometry
        puts('ERROR: Mittsu::Geometry#merge: geometry not an instance of Mittsu::Geometry.', geometry.inspect)
        return
      end
      vertexOffset = @vertices.length
      vertices1 = @vertices
      vertices2 = geometry.vertices
      faces1 = @faces
      faces2 = geometry.faces
      uvs1 = @face_vertex_uvs[0]
      uvs2 = geometry.face_vertex_uvs[0]
      material_index_offset ||= 0
      normal_matrix = matrix.nil? ? nil : Mittsu::Matrix3.new.get_normal_matrix(matrix)
      # vertices
      vertices2.each do |vertex|
        vertex_copy = vertex.clone
        vertex_copy.apply_matrix4(matrix) unless matrix.nil?
        vertices1 << vertex_copy
      end
      # faces
      faces2.each do |face|
        face_vertex_normals = face.vertex_normals
        face_vertex_colors = face.vertex_colors
        face_copy = Mittsu::Face3.new(face.a + vertexOffset, face.b + vertexOffset, face.c + vertexOffset)
        face_copy.normal.copy(face.normal)
        if normal_matrix != nil
          face_copy.normal.apply_matrix3(normal_matrix).normalize unless normal_matrix.nil?
        end
        face_vertex_normals.each do |normal|
          normal = normal.clone
          normal.apply_matrix3(normal_matrix).normalize unless normal_matrix.nil?
          face_copy.vertex_normals << normal
        end
        face_copy.color.copy(face.color)
        face_vertex_colors.each do |color|
          face_copy.vertex_colors << color.clone
        end
        face_copy.materialIndex = face.materialIndex + material_index_offset
        faces1 << face_copy
      end
      # uvs
      uvs2.each do |uv|
        continue if uv.nil?
        uv_copy = []
        uv.each do |u|
          uv_copy << u.clone
        end
        uvs1 << uv_copy
      end
    end

    def merge_mesh(mesh)
      if mesh.is_a?(Mittsu::Mesh) == false
        puts('ERROR: Mittsu::Geometry#merge_mesh: mesh not an instance of Mittsu::Mesh.', mesh.inspect)
        return
      end
      mesh.matrix_auto_update && mesh.update_matrix
      self.merge(mesh.geometry, mesh.matrix)
    end

    def merge_vertices
      vertices_map = {} # Hashmap for looking up vertice by position coordinates (and making sure they are unique)
      unique = []; changes = []
      precision_points = 4 # number of decimal points, eg. 4 for epsilon of 0.0001
      precision = 10 ** precision_points
      @vertices.each_with_index do |v, i|
        key = "#{(v.x * precision).round}_#{(v.y * precision).round}_#{(v.z * precision).round}"
        if vertices_map[key].nil?
          vertices_map[key] = i
          unique << v
          changes[i] = unique.length - 1
        else
          #console.log('Duplicate vertex found. ', i, ' could be using ', vertices_map[key])
          changes[i] = changes[vertices_map[key]]
        end
      end
      # if faces are completely degenerate after merging vertices, we
      # have to remove them from the geometry.
      face_indices_to_remove = []
      @faces.each_with_index do |face, i|
        face.a = changes[face.a]
        face.b = changes[face.b]
        face.c = changes[face.c]
        indices = [face.a, face.b, face.c]
        # if any duplicate vertices are found in a Face3
        # we have to remove the face as nothing can be saved
        3.times do |n|
          if indices[n] == indices[(n + 1) % 3]
            face_indices_to_remove << i
            break
          end
        end
      end
      face_indices_to_remove.reverse_each do |idx|
        @faces.delete_at idx
        @face_vertex_uvs.each do |uv|
          uv.delete_at idx
        end
      end
      # Use unique set of vertices
      diff = @vertices.length - unique.length
      @vertices = unique
      diff
    end

    def to_json
      output = {
        metadata: {
          version: 4.0,
          type: 'BufferGeometry',
          generator: 'BufferGeometryExporter'
        },
        uuid: self.uuid,
        type: @type
      }
      output.name = @name unless @name.nil? || @name.empty?
      if @parameters
        @parameters.each do |(key, parameter)|
          output[key] = parameter unless parameter.nil?
        end
        return output
      end
      vertices = []
      @vertices.each do |vert|
        vertices << vert.x << vert.y << vert.z
      end
      faces = []
      normals = []
      normals_hash = {}
      colors = []
      colors_hash = {}
      uvs = []
      uvs_hash = {}
      @faces.each_with_index do |face, i|
        has_material = false # face.materialIndex != nil
        has_face_uv = false # deprecated
        has_face_vertex_uv = @face_vertex_uvs[0][i] != nil
        has_face_normal = face.normal.length > 0
        has_face_vertex_normal = face.vertex_normals.length > 0
        has_face_color = face.color.r != 1 || face.color.g != 1 || face.color.b != 1
        has_face_vertex_color = face.vertex_colors.length > 0
        face_type = 0
        face_type = set_bit(face_type, 0, 0)
        face_type = set_bit(face_type, 1, has_material)
        face_type = set_bit(face_type, 2, has_face_uv)
        face_type = set_bit(face_type, 3, has_face_vertex_uv)
        face_type = set_bit(face_type, 4, has_face_normal)
        face_type = set_bit(face_type, 5, has_face_vertex_normal)
        face_type = set_bit(face_type, 6, has_face_color)
        face_type = set_bit(face_type, 7, has_face_vertex_color)
        faces << face_type
        faces << face.a << face.b << face.c
        # if has_material
        #   faces << face.materialIndex
        # end
        if has_face_vertex_uv
          face_vertex_uvs = @face_vertex_uvs[0][i]
          faces << get_uv_index(face_vertex_uvs[0], uvs_hash, uvs)
          faces << get_uv_index(face_vertex_uvs[1], uvs_hash, uvs)
          faces << get_uv_index(face_vertex_uvs[2], uvs_hash, uvs)
        end
        if has_face_normal
          faces << get_normal_index(face.normal, normals_hash, normals)
        end
        if has_face_vertex_normal
          vertex_normals = face.vertex_normals
          faces << get_normal_index(vertex_normals[0], normals_hash, normals)
          faces << get_normal_index(vertex_normals[1], normals_hash, normals)
          faces << get_normal_index(vertex_normals[2], normals_hash, normals)
        end
        if has_face_color
          faces << get_color_index(face.color)
        end
        if has_face_vertex_color
          vertex_colors = face.vertex_colors
          faces << get_color_index(vertex_colors[0], colors_hash, colors)
          faces << get_color_index(vertex_colors[1], colors_hash, colors)
          faces << get_color_index(vertex_colors[2], colors_hash, colors)
        end
      end
      output[:data] = {}
      output[:data][:vertices] = vertices
      output[:data][:normals] = normals
      output[:data][:colors] = colors unless colors.empty?
      output[:data][:uvs] = [uvs] unless uvs.empty? # temporal backward compatibility
      output[:data][:faces] = faces

      #

      output
    end

    def clone
      geometry = Mittsu::Geometry.new
      @vertices.each do |v|
        geometry.vertices << v.clone
      end
      @faces.each do |f|
        geometry.faces << f.clone
      end
      @face_vertex_uvs.each_with_index do |face_vertex_uvs, i|
        geometry.face_vertex_uvs[i] ||= []
        face_vertex_uvs.each do |uvs|
          uvs_copy = []
          uvs.each do |uv|
            uvs_copy << uv.clone
          end
          geometry.face_vertex_uvs[i] << uvs_copy
        end
      end
      geometry
    end

    def dispose
      self.dispatch_event type: :dispose
    end

    private

    def set_bit(value, position, enabled)
      enabled ? value | (1 << position) : value & (~(1 << position))
    end

    def get_normal_index(normal, normals_hash, normals)
      hash = normal.x.to_s + normal.y.to_s + normal.z.to_s
      return normals_hash[hash] unless normals_hash[hash].nil?
      normals_hash[hash] = normals.length / 3
      normals << normal.x << normal.y << normal.z
      normals_hash[hash]
    end

    def get_color_index(color, color_hash, colors)
      hash = color.r.to_s + color.g.to_s + color.b.to_s
      return colors_hash[hash] unless colors_hash[hash].nil?
      colors_hash[hash] = colors.length
      colors << color.get_hex
      colors_hash[hash]
    end

    def get_uv_index(uv, uvs_hash, uvs)
      hash = uv.x.to_s + uv.y.to_s
      return uvs_hash[hash] unless uvs_hash[hash].nil?
      uvs_hash[hash] = uvs.length / 2
      uvs << uv.x << uv.y
      return uvs_hash[hash]
    end
  end
end
