# frozen_string_literal: true

require "json"
require "optparse"
require "rexml/document"
require "tempfile"
require "yaml"

require "libptouch"

module Libptouch
  module Cli
    # PNG/SVG を扱う ptouch-print 相当の CLI（1bit ラスター経路なし）。
    module LabelPrint
      module_function

      def run(argv)
        opts = parse(argv)
        return 2 if opts.nil?

        return run_version if opts[:version]
        return run_help if opts[:help]

        return run_media_info(opts) if opts[:media_info]

        if opts[:status]
          warn_unused_file_options(opts)
          return run_status(opts)
        end

        if opts[:template] || opts[:data]
          return usage_error("--template and --data must be used together") if opts[:template].to_s.empty? || opts[:data].to_s.empty?
          return usage_error("-f and --template/--data cannot be used together") unless opts[:file].to_s.empty?
          return run_template_print(opts)
        end

        return usage_error("-f is required (or use --template/--data, or --status)") if opts[:file].to_s.empty?

        path = opts[:file]
        kind = image_kind(path)
        return usage_error("not a PNG/SVG file: #{path}") if kind.nil?

        run_print(path, opts, kind)
      end

      def png_file?(path)
        return true if path.downcase.end_with?(".png")

        File.open(path, "rb") do |f|
          sig = f.read(8)
          sig == "\x89PNG\r\n\x1a\n".b
        end
      rescue Errno::ENOENT, Errno::EACCES => e
        warn "open #{path}: #{e.message}"
        false
      end

      def svg_file?(path)
        path.downcase.end_with?(".svg")
      end

      def image_kind(path)
        return :png if png_file?(path)
        return :svg if svg_file?(path)

        nil
      end

      def parse(argv)
        opts_hash = default_cli_opts
        build_cli_parser(opts_hash).parse!(argv)
        return nil unless threshold_option_ok?(opts_hash)
        return nil unless usb_pid_option_ok?(opts_hash)

        opts_hash.delete(:usb_pid_invalid)
        opts_hash
      end

      def default_cli_opts
        {
          file: nil,
          template: nil,
          data: nil,
          threshold: nil,
          usb_pid: nil,
          usb_pid_invalid: false,
          dry_run: false,
          media_info: false,
          status: false,
          version: false,
          help: false
        }
      end

      def pid_option_description
        p900 = Libptouch::USB_PID_PTP900W
        p750 = Libptouch::USB_PID_PTP750W
        p710 = Libptouch::USB_PID_PTP710BT
        "USB 製品 ID（16 進可）。既定 P900W 0x#{p900.to_s(16)}; " \
          "P750W 0x#{p750.to_s(16)}; P710BT 0x#{p710.to_s(16)}"
      end

      def apply_usb_pid_option(opts_hash, pid_str)
        opts_hash[:usb_pid] = Integer(pid_str, 0)
      rescue ArgumentError
        warn "invalid --pid: #{pid_str.inspect}"
        opts_hash[:usb_pid_invalid] = true
      end

      def threshold_option_ok?(opts_hash)
        return true if opts_hash[:threshold].nil? || (0..255).cover?(opts_hash[:threshold])

        warn "-t must be 0..255"
        false
      end

      def usb_pid_option_ok?(opts_hash)
        return false if opts_hash[:usb_pid_invalid]
        return true if opts_hash[:usb_pid].nil?
        return true if opts_hash[:usb_pid].between?(1, 0xFFFF)

        warn "-p/--pid must be 1..0xFFFF"
        false
      end

      def build_cli_parser(opts_hash)
        OptionParser.new do |p|
          p.banner = usage_banner
          p.separator ""
          p.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v }
          p.on("--template PATH", "差込用 SVG テンプレート") { |v| opts_hash[:template] = v }
          p.on("--data PATH", "差込データ JSON/YAML ファイル") { |v| opts_hash[:data] = v }
          p.on("-t", "--threshold N", Integer,
               "二値化しきい値 0–255（既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG）") do |v|
            opts_hash[:threshold] = v
          end
          p.on("-p", "--pid PID", pid_option_description) do |v|
            apply_usb_pid_option(opts_hash, v)
          end
          p.on("-n", "--dry-run", "読み込みと検証のみ（USB なし）") { opts_hash[:dry_run] = true }
          p.on("-M", "--media-info", "現在テープの幅(mm)・DPI・最小余白(mm)を JSON で表示して終了") do
            opts_hash[:media_info] = true
          end
          p.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do
            opts_hash[:status] = true
          end
          p.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true }
          p.on("-h", "--help", "このヘルプ") { opts_hash[:help] = true }
        end
      end

      def usage_banner
        <<~BANNER
          Usage: ptouch-label [options]

          PNG/SVG 対応。幅・高さは画像から取得します（-w/-H はありません）。
          または --template/--data で SVG 差込印刷ができます。
          SVG は現在テープ幅に合わせて自動拡大・縮小します（USB 接続必須）。
          --status / --media-info のときは -f は不要です。
        BANNER
      end

      def warn_unused_file_options(opts)
        return unless opts[:file] || opts[:template] || opts[:data] || opts[:dry_run] || !opts[:threshold].nil?

        warn "warning: options other than --status are ignored"
      end

      def run_version
        puts "ptouch-label #{Libptouch::VERSION}"
        0
      end

      def run_help
        puts usage_banner
        puts ""
        puts parser_help_text
        0
      end

      def parser_help_text
        opts_hash = default_cli_opts
        p = OptionParser.new do |parser|
          parser.banner = "ptouch-label [options]"
          parser.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v }
          parser.on("--template PATH", "差込用 SVG テンプレート") { |v| opts_hash[:template] = v }
          parser.on("--data PATH", "差込データ JSON/YAML ファイル") { |v| opts_hash[:data] = v }
          parser.on("-t", "--threshold N", Integer,
                    "二値化しきい値 0–255（既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG）") do |v|
            opts_hash[:threshold] = v
          end
          parser.on("-p", "--pid PID", pid_option_description) do |v|
            apply_usb_pid_option(opts_hash, v)
          end
          parser.on("-n", "--dry-run", "読み込みと検証のみ（USB なし）") { opts_hash[:dry_run] = true }
          parser.on("-M", "--media-info", "現在テープの幅(mm)・DPI・最小余白(mm)を JSON で表示して終了") do
            opts_hash[:media_info] = true
          end
          parser.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do
            opts_hash[:status] = true
          end
          parser.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true }
          parser.on("-h", "--help", "このヘルプ") { opts_hash[:help] = true }
        end
        p.help
      end

      def open_usb_for_opts(ctx, opts)
        if opts[:usb_pid]
          ctx.open_usb_vid_pid(Libptouch::USB_VID_BROTHER, opts[:usb_pid])
        else
          ctx.open_usb
        end
      end

      def run_template_print(opts)
        svg_text = render_svg_template(opts[:template], load_merge_data(opts[:data]))
        Tempfile.create(["ptouch-merge-", ".svg"]) do |tmp|
          tmp.binmode
          tmp.write(svg_text)
          tmp.flush
          run_print(tmp.path, opts, :svg)
        end
      rescue REXML::ParseException => e
        warn "template parse error: #{e.message}"
        1
      rescue Errno::ENOENT, Errno::EACCES => e
        warn e.message
        1
      rescue JSON::ParserError, Psych::SyntaxError => e
        warn "data parse error: #{e.message}"
        1
      rescue ArgumentError => e
        warn "data error: #{e.message}"
        1
      end

      def load_merge_data(path)
        text = File.read(path, encoding: "UTF-8")
        ext = File.extname(path).downcase
        parsed = if ext == ".json"
                   JSON.parse(text)
                 else
                   YAML.safe_load(text, permitted_classes: [], aliases: false)
                 end
        unless parsed.is_a?(Hash)
          raise ArgumentError, "data must be a key-value object/hash"
        end

        parsed.transform_keys(&:to_s)
      end

      def render_svg_template(path, data)
        xml = File.read(path, encoding: "UTF-8")
        doc = REXML::Document.new(xml)
        doc.elements.each("//text[@data-field]") do |el|
          # If descendants also have data-field (e.g. tspan placeholders),
          # keep node structure and let element-level replacements handle them.
          next if el.elements[".//*[@data-field]"]

          key = el.attributes["data-field"].to_s
          next unless data.key?(key)

          replace_text_element_content(el, data[key].to_s)
        end
        doc.elements.each("//tspan[@data-field]") do |el|
          key = el.attributes["data-field"].to_s
          next unless data.key?(key)

          replace_text_element_content(el, data[key].to_s)
        end
        out = +""
        formatter = REXML::Formatters::Default.new
        formatter.write(doc, out)
        out
      end

      def replace_text_element_content(text_element, value)
        # Remove all child nodes first so mixed content (<tspan>, text nodes, etc.)
        # gets replaced consistently by merge data.
        text_element.children.to_a.each { |child| text_element.delete(child) }
        text_element.add(REXML::Text.new(value, true))
      end

      def run_status(opts)
        ctx = nil
        begin
          ctx = Libptouch::Context.new
          open_usb_for_opts(ctx, opts)
          h = Libptouch.parse_status(ctx.status_bytes)
          h.delete(:raw_bytes)
          puts JSON.pretty_generate(h)
          0
        rescue Libptouch::Error => e
          warn "get_status: #{e.message}"
          1
        ensure
          ctx&.dispose
        end
      end

      def run_media_info(opts)
        warn_unused_file_options(opts)
        ctx = nil
        begin
          ctx = Libptouch::Context.new
          open_usb_for_opts(ctx, opts)
          puts JSON.pretty_generate(ctx.current_media_info)
          0
        rescue Libptouch::Error => e
          warn "media_info: #{e.message}"
          1
        ensure
          ctx&.dispose
        end
      end

      def run_print(path, opts, kind)
        ctx = nil
        begin
          ctx = Libptouch::Context.new
          threshold = opts[:threshold]
          data, width, height = if kind == :svg
                                  open_usb_for_opts(ctx, opts)
                                  if threshold.nil?
                                    ctx.svg_file_to_raster_fit_current_tape(path)
                                  else
                                    ctx.svg_file_to_raster_fit_current_tape(path, threshold: threshold)
                                  end
                                elsif threshold.nil?
                                  ctx.png_file_to_raster(path)
                                else
                                  ctx.png_file_to_raster(path, threshold: threshold)
                                end

          ctx.check_raster(data, width_dots: width, height_dots: height)

          if opts[:dry_run]
            puts "dry-run OK: #{data.bytesize} bytes, src #{width}x#{height} dots (print lengthxwidth #{width}x#{height})"
            return 0
          end

          open_usb_for_opts(ctx, opts) if kind == :png
          ctx.print_raster(data, width_dots: width, height_dots: height)
          0
        rescue Libptouch::Error => e
          warn e.message
          1
        ensure
          ctx&.dispose
        end
      end

      def usage_error(msg)
        warn msg
        warn "(try ptouch-label --help)"
        2
      end
    end
  end
end
