# encoding: UTF-8
#
# dzw_task_loader.rb  -- 도진웍스 무콘솔 루비 실행 로더
#
# 목적: SketchUp 루비 콘솔을 여는 것을 영구히 없앤다.
#       (콘솔을 열면 한글 IME가 켜져 단축키가 죽는 사고 발생 -> 원천 차단)
#
# 동작: 아래 INBOX 폴더들을 1초 타이머로 감시.
#       .rb 파일이 새로 생기면 읽어서 실행하고,
#       표준출력 / 반환값 / 예외를 같은 폴더의 _result.txt 에 누적 기록한 뒤
#       처리한 파일을 done\ 으로 옮긴다.
#
# 절대 원칙: UI 창(메시지박스/콘솔/다이얼로그)을 절대 열지 않는다.
#
# 제출 규칙: 반드시 xxx.rb.tmp 로 쓴 뒤 xxx.rb 로 rename 할 것.
#            (1초 타이머가 절반만 쓰인 파일을 집는 것을 막는다)

require 'fileutils'
require 'stringio'

module DZW
  module TaskLoader

    VERSION = '1.1.0'

    INBOXES = [
      'E:/도진팩토리/3D스캔및티칭시스템/_자동화/inbox',
      'C:/dzw/inbox'
    ]

    # --- 자동저장 (2026-09-19 크래시 4회 대응) ---
    # SketchUp 자체 자동저장이 꺼져 있어 9/17 이후 저장본이 없었다.
    # 5분마다 save_copy 로 사본을 남긴다. 원본 .skp 는 건드리지 않는다(무수정).
    # 슬롯 3개 순환: 저장 도중 죽어도 직전 2개는 온전하다.
    AUTOSAVE_DIR   = 'E:/도진팩토리/3D스캔및티칭시스템/_자동화/autosave'
    AUTOSAVE_EVERY = 300   # 틱(1초) 기준 = 약 5분
    AUTOSAVE_SLOTS = 3

    @busy = false
    @ticks = 0
    @as_slot = 0

    class << self

      def stamp
        Time.now.strftime('%Y-%m-%d %H:%M:%S')
      end

      # 결과 파일에 안전하게 append (실패해도 절대 예외를 밖으로 내보내지 않음)
      def write_out(dir, text)
        begin
          path = File.join(dir, '_result.txt')
          File.open(path, 'a:UTF-8') { |f| f.write(text) }
        rescue Exception
          nil
        end
      end

      def ensure_dirs
        INBOXES.each do |d|
          begin
            FileUtils.mkdir_p(d) unless File.directory?(d)
            done = File.join(d, 'done')
            FileUtils.mkdir_p(done) unless File.directory?(done)
          rescue Exception
            nil
          end
        end
      end

      # 하트비트: 로더가 실제로 살아있다는 증거 (추측 금지 원칙)
      def heartbeat(tag)
        INBOXES.each do |d|
          next unless File.directory?(d)
          write_out(d, "[#{stamp}] LOADER ALIVE v#{VERSION} #{tag} ticks=#{@ticks}\n")
        end
      end

      # 자동저장: 절대 예외를 밖으로 내보내지 않는다(타이머 보호).
      def autosave
        begin
          return unless defined?(Sketchup)
          m = Sketchup.active_model
          return if m.nil?
          return unless m.modified?          # 바뀐 게 없으면 건너뛴다
          FileUtils.mkdir_p(AUTOSAVE_DIR) unless File.directory?(AUTOSAVE_DIR)
          base = m.path.to_s.empty? ? 'untitled' : File.basename(m.path, '.skp')
          @as_slot = ((@as_slot || 0) + 1) % AUTOSAVE_SLOTS
          dest = File.join(AUTOSAVE_DIR, "AUTOSAVE_#{base}_slot#{@as_slot}.skp")
          t0 = Time.now
          m.save_copy(dest)                  # 원본 무수정 / modified? 유지
          dt = (Time.now - t0).round(2)
          INBOXES.each do |d|
            next unless File.directory?(d)
            write_out(d, "[#{stamp}] AUTOSAVE slot#{@as_slot} #{dt}s -> #{dest}\n")
          end
        rescue Exception => e
          INBOXES.each do |d|
            next unless File.directory?(d)
            write_out(d, "[#{stamp}] AUTOSAVE FAIL #{e.class}: #{e.message}\n")
          end
          nil
        end
      end

      def run_one(dir, file)
        banner  = "\n=== #{stamp} | #{File.basename(file)} ===\n"
        stdout_buf = StringIO.new
        old_stdout = $stdout
        retval = nil
        err    = nil

        begin
          src = File.open(file, 'r:BOM|UTF-8') { |f| f.read }
          src = src.force_encoding('UTF-8')
          $stdout = stdout_buf
          begin
            # load 가 아니라 eval: 반환값을 기록해야 하므로
            retval = eval(src, TOPLEVEL_BINDING, file)
          ensure
            $stdout = old_stdout
          end
        rescue Exception => e
          $stdout = old_stdout
          err = e
        end

        body = banner.dup
        body << "-- STDOUT --\n"
        body << (stdout_buf.string.to_s.empty? ? "(없음)\n" : (stdout_buf.string + "\n"))
        body << "-- RETURN --\n"
        begin
          body << "#{retval.inspect}\n"
        rescue Exception
          body << "(inspect 실패)\n"
        end
        if err
          body << "-- ERROR --\n"
          body << "#{err.class}: #{err.message}\n"
          begin
            body << err.backtrace.first(15).join("\n") << "\n"
          rescue Exception
            nil
          end
          body << "-- RESULT: FAIL --\n"
        else
          body << "-- RESULT: OK --\n"
        end
        write_out(dir, body)

        # 처리한 파일 done\ 으로 이동 (타임스탬프 붙여 충돌 방지)
        begin
          done = File.join(dir, 'done')
          FileUtils.mkdir_p(done) unless File.directory?(done)
          base = File.basename(file, '.rb')
          dest = File.join(done, "#{base}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.rb")
          FileUtils.mv(file, dest)
        rescue Exception => e2
          write_out(dir, "-- MOVE FAIL: #{e2.class}: #{e2.message}\n")
          # 이동 실패 시 무한 재실행을 막기 위해 삭제 시도
          begin
            File.delete(file)
          rescue Exception
            nil
          end
        end
      end

      def scan_dir(dir)
        return unless File.directory?(dir)
        files = Dir.glob(File.join(dir, '*.rb'))
        files.sort.each { |f| run_one(dir, f) if File.file?(f) }
      end

      def tick
        return if @busy
        @busy = true
        begin
          @ticks += 1
          heartbeat('heartbeat') if (@ticks % 300) == 0   # 약 5분마다
          autosave if (@ticks % AUTOSAVE_EVERY) == 0      # 약 5분마다 사본 저장
          INBOXES.each do |d|
            begin
              scan_dir(d)
            rescue Exception
              # 한 폴더가 죽어도 다른 폴더는 계속 감시 (경로/인코딩 문제 대비)
              nil
            end
          end
        rescue Exception
          # 타이머 프로시저에서 예외가 새어나가면 타이머가 조용히 죽는다 -> 전부 흡수
          nil
        ensure
          @busy = false
        end
      end

      def start
        begin
          UI.stop_timer($dzw_task_loader_timer) if $dzw_task_loader_timer
        rescue Exception
          nil
        end
        ensure_dirs
        $dzw_task_loader_timer = UI.start_timer(1.0, true) { tick }
        heartbeat('START')
        true
      end
    end
  end
end

# 중복 로드 방지 가드 (단, 재설치/버전업 시에는 start 를 다시 돌린다)
if $dzw_task_loader_loaded
  DZW::TaskLoader.start
else
  $dzw_task_loader_loaded = true
  DZW::TaskLoader.start
end
