# encoding: UTF-8
# FCA Parametrik Kapı 1.0.0 — standalone Ruby Console loader.
# Repeated load is safe; restart SketchUp to replace an already loaded version.
unless defined?(FCA::ParametricDoor::READY)
# encoding: UTF-8
# Pure Ruby dimension / wall topology logic. Internal lengths are millimetres.
module FCA
  module ParametricDoor
    module Core
      extend self
      DEFAULTS = { 'width' => 1000.0, 'height' => 2100.0, 'frame' => 50.0,
                   'depth' => 150.0, 'leaf' => 40.0, 'angle' => 0.0,
                   'hand' => 'Sol' }.freeze

      def number(value)
        result = Float(value.to_s.strip.tr(',', '.'))
        raise ArgumentError, 'Sonlu bir sayı girin.' unless result.finite?
        result
      rescue ArgumentError, TypeError
        raise ArgumentError, 'Ölçüleri birim eklemeden mm olarak girin (ör. 950 veya 950,5).'
      end

      def validate(p)
        %w[width height frame depth leaf angle].each { |key| p[key] = number(p.fetch(key)) }
        raise ArgumentError, 'Genişlik 300–4000 mm, yükseklik 500–5000 mm olmalı.' unless
          (300..4000).cover?(p['width']) && (500..5000).cover?(p['height'])
        raise ArgumentError, 'Kasa 15–150 mm olmalı; kanada yeterli alan bırakmalı.' unless
          (15..150).cover?(p['frame']) && p['width'] > 2 * p['frame'] + 100 && p['height'] > p['frame'] + 100
        raise ArgumentError, 'Kasa derinliği 30–1000 mm; kanat 15–100 mm olmalı.' unless
          (30..1000).cover?(p['depth']) && (15..100).cover?(p['leaf']) && p['leaf'] <= p['depth']
        raise ArgumentError, 'Açılma açısı 0–120 derece olmalı.' unless (0..120).cover?(p['angle'])
        raise ArgumentError, 'Menteşe yönü Sol veya Sağ olmalı.' unless %w[Sol Sağ].include?(p['hand'])
        p
      end

      def validate_openings(wall, doors)
        doors.each do |d|
          validate(d)
          raise ArgumentError, 'Kapı duvar sınırını aşıyor; yanlarda ve üstte en az 10 mm bırakın.' unless
            d['x'] >= 10 && d['x'] + d['width'] <= wall['width'] - 10 && d['height'] <= wall['height'] - 10
        end
        doors.combination(2) do |a, b|
          gap = [b['x'] - a['x'] - a['width'], a['x'] - b['x'] - b['width']].max
          raise ArgumentError, 'İki kapı arasında en az 10 mm duvar kalmalı.' if gap < 10
        end
        true
      end

      # Exposed quads only: no overlapping solids or internal faces.
      def wall_quads(wall, doors)
        validate_openings(wall, doors)
        xs = ([0.0, wall['width']] + doors.flat_map { |d| [d['x'], d['x'] + d['width']] }).uniq.sort
        zs = ([0.0, wall['height']] + doors.map { |d| d['height'] }).uniq.sort
        cells = {}
        (0...xs.length - 1).each do |i|
          (0...zs.length - 1).each do |j|
            x = (xs[i] + xs[i + 1]) / 2
            z = (zs[j] + zs[j + 1]) / 2
            cells[[i, j]] = !doors.any? { |d| x > d['x'] && x < d['x'] + d['width'] && z < d['height'] }
          end
        end
        quads = []
        t = wall['thickness']
        cells.each do |(i, j), filled|
          next unless filled
          a, b, c, d = xs[i], xs[i + 1], zs[j], zs[j + 1]
          quads << ['front', [[a, 0, c], [b, 0, c], [b, 0, d], [a, 0, d]]]
          next if t == 0
          quads << ['back', [[b, t, c], [a, t, c], [a, t, d], [b, t, d]]]
          quads << ['left', [[a, t, c], [a, 0, c], [a, 0, d], [a, t, d]]] unless cells[[i - 1, j]]
          quads << ['right', [[b, 0, c], [b, t, c], [b, t, d], [b, 0, d]]] unless cells[[i + 1, j]]
          quads << ['bottom', [[a, t, c], [b, t, c], [b, 0, c], [a, 0, c]]] unless cells[[i, j - 1]]
          quads << ['top', [[a, 0, d], [b, 0, d], [b, t, d], [a, t, d]]] unless cells[[i, j + 1]]
        end
        quads
      end
    end
  end
end

# encoding: UTF-8
require 'sketchup.rb'
require 'json'
require 'digest'


module FCA
  module ParametricDoor
    extend self
    DICT = 'FCA_ParametricDoor_v1'.freeze
    READY = true

    def transaction(title)
      model = Sketchup.active_model
      model.start_operation(title, true)
      begin
        result = yield
        model.commit_operation
        result
      rescue StandardError
        model.abort_operation
        raise
      end
    end

    def guarded
      yield
    rescue StandardError => e
      UI.messagebox("FCA Parametrik Kapı\n\n#{e.message}")
      puts "FCA Kapı: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}" 
      nil
    end

    def context
      model = Sketchup.active_model
      path = model.active_path || []
      raise 'Duvarı ayrı bir grup yapın ve çift tıklayarak grubun içine girin.' if path.empty?
      path.each do |instance|
        raise 'Kilitli grup içinde işlem yapılamaz.' if instance.locked?
        raise 'Tekrarlanan duvar bileşenini önce Benzersiz Yap (Make Unique) ile ayırın.' if instance.definition.instances.length > 1
      end
      t = model.edit_transform
      axes = [t.xaxis, t.yaxis, t.zaxis]
      rigid = axes.all? { |v| (v.length - 1).abs < 1e-6 } &&
        axes.combination(2).all? { |a, b| a.dot(b).abs < 1e-6 } && t.xaxis.cross(t.yaxis).dot(t.zaxis) > 0.99999
      raise 'Ölçeklenmiş/aynalı grubu önce normal boyutlu geometriye dönüştürün.' unless rigid
      [model.active_entities, model.active_entities.parent]
    end

    def owned?(entity)
      entity.is_a?(Sketchup::Group) && entity.get_attribute(DICT, 'door', false)
    end

    def geometry(entities)
      entities.select { |e| e.is_a?(Sketchup::Face) || e.is_a?(Sketchup::Edge) }
    end

    def fingerprint(entities)
      records = geometry(entities).map do |e|
        pts = e.vertices.map { |v| v.position.to_a.map { |n| n.round(7) } }.sort
        orientation = e.is_a?(Sketchup::Face) ? e.normal.to_a : [e.soft?, e.smooth?]
        [e.typename, e.persistent_id, pts, orientation, e.layer.name, e.hidden?,
         e.respond_to?(:material) && e.material ? e.material.name : nil,
         e.is_a?(Sketchup::Face) && e.back_material ? e.back_material.name : nil]
      end
      Digest::SHA256.hexdigest(JSON.generate(records.sort_by { |r| r[1] }))
    end

    def load_wall(owner)
      value = owner.get_attribute(DICT, 'wall')
      value && JSON.parse(value)
    end

    def check_wall(entities, owner, wall)
      raise 'Duvar elle değiştirilmiş. İşlemi geri alın veya yeni, temiz bir duvar grubuyla başlayın.' unless
        wall['fingerprint'] == fingerprint(entities)
      ids = entities.select { |e| owned?(e) }.map(&:persistent_id).sort
      raise 'Kapı taşınmış, kopyalanmış veya normal Sil ile silinmiş. Önce bu işlemi geri alın; eklentinin düzenle/sil komutunu kullanın.' unless ids == wall['ids'].sort
      entities.select { |e| owned?(e) }.each do |g|
        raise 'Kilitli kapı düzenlenemez.' if g.locked?
        p = JSON.parse(g.get_attribute(DICT, 'params'))
        raise 'Kapı grubunun dönüşümü değişmiş. Önce Taşı/Ölçekle işlemini geri alın.' unless
          g.transformation.to_a.zip(p['transform']).all? { |a, b| (a - b).abs < 1e-7 }
      end
      wall
    end

    def wall_transform(wall)
      Geom::Transformation.new(wall['transform'])
    end

    def inspect_wall(entities, face)
      faces = entities.grep(Sketchup::Face)
      edges = entities.grep(Sketchup::Edge)
      raise 'İlk yerleştirmede grupta yalnızca tek dikdörtgen yüz veya altı yüzlü dikdörtgen duvar olmalı.' unless
        entities.length == faces.length + edges.length && [1, 6].include?(faces.length)
      raise 'Delikli, eğri veya bölünmüş duvar desteklenmiyor.' unless faces.all? { |f| f.loops.length == 1 && f.vertices.length == 4 }
      raise 'Gizli/yumuşatılmış kenarlı duvar desteklenmiyor.' if faces.any?(&:hidden?) || edges.any? { |e| e.hidden? || e.soft? || e.smooth? }
      # Vertical is world vertical, converted to the active editing context.
      z = Z_AXIS.transform(Sketchup.active_model.edit_transform.inverse).normalize
      n = face.normal
      raise 'Duvarın dikey geniş yüzünü seçin.' unless n.dot(z).abs < 1e-6
      y = n.reverse
      x = y.cross(z).normalize
      basis = Geom::Transformation.axes(ORIGIN, x, y, z)
      inv = basis.inverse
      vertices = edges.flat_map(&:vertices).uniq
      coordinates = vertices.map { |v| v.position.transform(inv).to_a.map(&:to_mm) }
      mins = (0..2).map { |i| coordinates.map { |v| v[i] }.min }
      maxs = (0..2).map { |i| coordinates.map { |v| v[i] }.max }
      dimensions = maxs.zip(mins).map { |a, b| a - b }
      single = faces.length == 1
      raise 'Duvar düzenli bir dikdörtgen prizma olmalı.' unless
        vertices.length == (single ? 4 : 8) && edges.length == (single ? 4 : 12) &&
        coordinates.all? { |v| (0..2).all? { |i| (v[i] - mins[i]).abs < 0.01 || (v[i] - maxs[i]).abs < 0.01 } }
      raise 'Duvar yüzleri birbirine dik ve dikdörtgen olmalı.' unless faces.all? do |f|
        pts = f.vertices.map { |v| v.position.transform(inv).to_a.map(&:to_mm) }
        (0..2).count { |i| pts.map { |p| p[i] }.max - pts.map { |p| p[i] }.min < 0.01 } == 1
      end
      raise 'Duvar geniş yüzünü seçin; kalınlık 10–1000 mm olmalı.' unless single ||
        (10..1000).cover?(dimensions[1]) && dimensions[0] > dimensions[1]
      raise 'Duvar en az 500 mm genişlik ve 600 mm yükseklik gerektirir.' unless dimensions[0] >= 500 && dimensions[2] >= 600
      raise 'Dokulu yüz desteklenmiyor; dokuyu kaldırın veya düz renk kullanın.' if
        faces.any? { |f| [f.material, f.back_material].compact.any?(&:texture) }
      raise 'Yüzey/kenar öznitelikleri olan duvar desteklenmiyor.' if
        (faces + edges).any? { |e| e.attribute_dictionaries && e.attribute_dictionaries.length > 0 }
      origin = Geom::Point3d.new(mins.map(&:mm)).transform(basis)
      transform = Geom::Transformation.axes(origin, x, y, z)
      styles = {}
      normals = { 'front' => [0, -1, 0], 'back' => [0, 1, 0], 'left' => [-1, 0, 0],
                  'right' => [1, 0, 0], 'bottom' => [0, 0, -1], 'top' => [0, 0, 1] }
      faces.each do |f|
        local = f.normal.transform(transform.inverse)
        key = normals.max_by { |_, v| local.dot(Geom::Vector3d.new(v)) }.first
        styles[key] = [f.material && f.material.name, f.back_material && f.back_material.name, f.layer.name]
      end
      { 'width' => dimensions[0], 'height' => dimensions[2], 'thickness' => single ? 0.0 : dimensions[1],
        'transform' => transform.to_a, 'styles' => styles, 'ids' => [] }
    end

    def dialog(params = Core::DEFAULTS)
      keys = %w[width height frame depth leaf angle hand]
      labels = ['Duvar boşluğu genişliği (mm)', 'Duvar boşluğu yüksekliği (mm)', 'Kasa profil eni (mm)',
                'Kasa derinliği (mm)', 'Kanat kalınlığı (mm)', 'Kanat açısı (derece)', 'Menteşe yönü (ön yüzden)']
      result = UI.inputbox(labels, keys.map { |k| params[k].to_s }, ['', '', '', '', '', '', 'Sol|Sağ'], 'Parametrik Kapı — mm')
      return nil unless result
      Core.validate(keys.zip(result).to_h)
    end

    def door_params(entities)
      entities.select { |e| owned?(e) }.map { |g| JSON.parse(g.get_attribute(DICT, 'params')) }
    end

    def rebuild_wall(entities, owner, wall)
      quads = Core.wall_quads(wall, door_params(entities))
      entities.erase_entities(geometry(entities))
      t = wall_transform(wall)
      quads.each do |side, points|
        pts = points.map { |p| Geom::Point3d.new(p.map(&:mm)).transform(t) }
        f = entities.add_face(pts)
        raise 'Duvar yüzü oluşturulamadı; işlem geri alındı.' unless f
        desired = (pts[1] - pts[0]).cross(pts[2] - pts[0])
        f.reverse! if f.normal.dot(desired) < 0
        style = wall['styles'][side] || wall['styles']['front']
        if style
          f.material = Sketchup.active_model.materials[style[0]] if style[0]
          f.back_material = Sketchup.active_model.materials[style[1]] if style[1]
          f.layer = Sketchup.active_model.layers[style[2]] || Sketchup.active_model.layers[0]
        end
      end
      wall['ids'] = entities.select { |e| owned?(e) }.map(&:persistent_id).sort
      wall['fingerprint'] = fingerprint(entities)
      owner.set_attribute(DICT, 'wall', JSON.generate(wall))
    end

    def box(entities, name, x, y, z, w, d, h)
      g = entities.add_group
      g.name = name
      pts = [[x, y, z], [x + w, y, z], [x + w, y + d, z], [x, y + d, z]]
      f = g.entities.add_face(pts.map { |p| p.map(&:mm) })
      f.reverse! if f.normal.z < 0
      f.pushpull(h.mm)
      g
    end

    def build_door(g, p)
      g.make_unique
      g.entities.clear!
      w, h, f, d = p.values_at('width', 'height', 'frame', 'depth')
      box(g.entities, 'Sol kasa', 0, 0, 0, f, d, h)
      box(g.entities, 'Sağ kasa', w - f, 0, 0, f, d, h)
      box(g.entities, 'Üst kasa', f, 0, h - f, w - 2 * f, d, f)
      leaf = box(g.entities, 'Kapı kanadı', f + 3, 3, 8, w - 2 * f - 6, p['leaf'], h - f - 11)
      right = p['hand'] == 'Sağ'
      hinge = Geom::Point3d.new((right ? w - f - 3 : f + 3).mm, 3.mm, 0)
      leaf.transform!(Geom::Transformation.rotation(hinge, Z_AXIS, (right ? p['angle'] : -p['angle']).degrees))
      g.name = "Kapı #{w.round} × #{h.round} mm"
      g.set_attribute(DICT, 'door', true)
      p['transform'] = g.transformation.to_a
      g.set_attribute(DICT, 'params', JSON.generate(p))
    end

    def create_at(face, position, params)
      entities, owner = context
      raise 'Duvar grubuna çift tıklayıp içindeki yüzün üzerine gelin.' unless face && entities.include?(face)
      wall = load_wall(owner)
      wall ? check_wall(entities, owner, wall) : wall = inspect_wall(entities, face)
      local = position.transform(Sketchup.active_model.edit_transform.inverse).transform(wall_transform(wall).inverse)
      raise 'Duvarın ön veya arka geniş yüzünü seçin.' unless face.normal.dot(wall_transform(wall).yaxis).abs > 0.99999
      p = params.merge('x' => local.x.to_mm - params['width'] / 2)
      Core.validate_openings(wall, door_params(entities) + [p])
      transaction('Parametrik kapı yerleştir') do
        g = entities.add_group
        g.transformation = wall_transform(wall) * Geom::Transformation.translation([p['x'].mm, 0, 0])
        build_door(g, p)
        rebuild_wall(entities, owner, wall)
        Sketchup.active_model.selection.clear
        Sketchup.active_model.selection.add(g)
      end
    end

    def selected_door
      entities, owner = context
      selection = Sketchup.active_model.selection.to_a
      raise 'Duvar grubunun içindeyken tek bir FCA kapı grubu seçin.' unless
        selection.length == 1 && owned?(selection.first) && entities.include?(selection.first)
      wall = load_wall(owner)
      raise 'Bu kapının duvar ilişkisi bulunamadı.' unless wall
      check_wall(entities, owner, wall)
      [entities, owner, wall, selection.first]
    end

    def edit
      guarded do
        entities, owner, wall, g = selected_door
        old = JSON.parse(g.get_attribute(DICT, 'params'))
        p = dialog(old)
        next unless p
        # Keep the left jamb fixed when resizing.
        p['x'] = old['x']
        others = entities.select { |e| owned?(e) && e != g }.map { |e| JSON.parse(e.get_attribute(DICT, 'params')) }
        Core.validate_openings(wall, others + [p])
        transaction('Kapı ve duvar boşluğunu güncelle') do
          build_door(g, p)
          rebuild_wall(entities, owner, wall)
        end
      end
    end

    def remove
      guarded do
        entities, owner, wall, g = selected_door
        transaction('Kapıyı sil ve duvarı kapat') do
          g.erase!
          rebuild_wall(entities, owner, wall)
        end
      end
    end

    def start
      guarded do
        context
        p = dialog
        Sketchup.active_model.select_tool(PlacementTool.new(p)) if p
      end
    end

    class PlacementTool
      def initialize(params)
        @params = params
        @ip = Sketchup::InputPoint.new
      end

      def activate
        Sketchup.status_text = 'Duvarın geniş yüzüne tıklayın: yatay merkez seçilir, alt kot duvar tabanına oturur. Esc: iptal.'
      end

      def onMouseMove(flags, x, y, view)
        @ip.pick(view, x, y)
        @preview = nil
        @valid = false
        begin
          entities, owner = ParametricDoor.context
          face = @ip.face
          raise 'Duvar grubunun içindeki geniş yüzü seçin.' unless face && entities.include?(face)
          wall = ParametricDoor.load_wall(owner) || ParametricDoor.inspect_wall(entities, face)
          t = ParametricDoor.wall_transform(wall)
          raise 'Geniş yüzü seçin.' unless face.normal.dot(t.yaxis).abs > 0.99999
          local = @ip.position.transform(Sketchup.active_model.edit_transform.inverse).transform(t.inverse)
          left = local.x.to_mm - @params['width'] / 2
          right = left + @params['width']
          h = @params['height']
          # Preview on the picked wall side, world coordinates.
          yside = local.y.to_mm
          world = Sketchup.active_model.edit_transform * t
          @preview = [[left, yside, 0], [right, yside, 0], [right, yside, h], [left, yside, h]].map do |p|
            Geom::Point3d.new(p.map(&:mm)).transform(world)
          end
          Core.validate_openings(wall, ParametricDoor.door_params(entities) + [@params.merge('x' => left)])
          @valid = true
          view.tooltip = 'Tıkla: kapıyı yerleştir (tabana oturur)'
        rescue StandardError => e
          view.tooltip = e.message
        end
        view.invalidate
      end

      def draw(view)
        return unless @preview
        view.drawing_color = @valid ? 'lime' : 'red'
        view.line_width = 3
        view.draw(GL_LINE_LOOP, @preview)
      end

      def onLButtonDown(flags, x, y, view)
        onMouseMove(flags, x, y, view)
        return UI.beep unless @valid
        success = ParametricDoor.guarded { ParametricDoor.create_at(@ip.face, @ip.position, @params); true }
        Sketchup.active_model.select_tool(nil) if success
      end

      def onCancel(reason, view)
        Sketchup.active_model.select_tool(nil)
      end

      def deactivate(view)
        view.invalidate
        Sketchup.status_text = ''
      end
    end

    unless @ui_loaded
      menu = UI.menu('Extensions').add_submenu('FCA Parametrik Kapı')
      commands = [['Kapı yerleştir', :start], ['Seçili kapıyı düzenle', :edit], ['Kapıyı sil ve boşluğu kapat', :remove]]
      toolbar = UI::Toolbar.new('FCA Parametrik Kapı')
      commands.each do |label, method|
        command = UI::Command.new(label) { ParametricDoor.public_send(method) }
        command.tooltip = label
        command.status_bar_text = label
        menu.add_item(command)
        toolbar.add_item(command)
      end
      toolbar.restore
      UI.add_context_menu_handler do |context_menu|
        if Sketchup.active_model.selection.length == 1 && owned?(Sketchup.active_model.selection.first)
          context_menu.add_item('Kapı ölçülerini değiştir…') { edit }
          context_menu.add_item('Kapıyı sil ve boşluğu kapat') { remove }
        end
      end
      @ui_loaded = true
    end
  end
end

end
puts 'FCA Parametrik Kapı hazır. Extensions > FCA Parametrik Kapı menüsünü kullanın.'
