# frozen_string_literal: true

require "json"
require "open3"

module BarcodePaoWasm
  FORMAT_PNG  = "png"
  FORMAT_JPEG = "jpeg"
  FORMAT_SVG  = "svg"

  class WasmBarcodeError < StandardError; end

  NODE_SCRIPT = <<~'JS'
import Module from './barcode.mjs';

const args = JSON.parse(process.argv[2]);
const {className, method, methodArgs, settings} = args;

try {
    const m = await Module();
    const BarcodeClass = m[className];
    if (!BarcodeClass) {
        console.log(JSON.stringify({error: `Unknown class: ${className}`}));
        process.exit(1);
    }

    const instance = new BarcodeClass();

    // Apply settings
    if (settings.outputFormat) instance.setOutputFormat(settings.outputFormat);
    if (settings.foregroundColor) {
        const [r, g, b, a] = settings.foregroundColor;
        instance.setForegroundColor(r, g, b, a);
    }
    if (settings.backgroundColor) {
        const [r, g, b, a] = settings.backgroundColor;
        instance.setBackgroundColor(r, g, b, a);
    }
    if (settings.showText !== undefined && instance.setShowText) instance.setShowText(settings.showText);
    if (settings.fitWidth !== undefined && instance.setFitWidth) instance.setFitWidth(settings.fitWidth);
    if (settings.pxAdjustBlack !== undefined && instance.setPxAdjustBlack) instance.setPxAdjustBlack(settings.pxAdjustBlack);
    if (settings.pxAdjustWhite !== undefined && instance.setPxAdjustWhite) instance.setPxAdjustWhite(settings.pxAdjustWhite);
    if (settings.textGap !== undefined && instance.setTextGap) instance.setTextGap(settings.textGap);
    if (settings.textFontScale !== undefined && instance.setTextFontScale) instance.setTextFontScale(settings.textFontScale);
    if (settings.textEvenSpacing !== undefined && instance.setTextEvenSpacing) instance.setTextEvenSpacing(settings.textEvenSpacing);
    if (settings.showStartStop !== undefined && instance.setShowStartStop) instance.setShowStartStop(settings.showStartStop);
    if (settings.codeMode !== undefined && instance.setCodeMode) instance.setCodeMode(settings.codeMode);
    if (settings.extendedGuard !== undefined && instance.setExtendedGuard) instance.setExtendedGuard(settings.extendedGuard);
    if (settings.symbolType !== undefined && instance.setSymbolType) instance.setSymbolType(settings.symbolType);
    if (settings.noOfColumns !== undefined && instance.setNoOfColumns) instance.setNoOfColumns(settings.noOfColumns);
    if (settings.stringEncoding !== undefined && instance.setStringEncoding) instance.setStringEncoding(settings.stringEncoding);
    if (settings.errorCorrectionLevel !== undefined && instance.setErrorCorrectionLevel) instance.setErrorCorrectionLevel(settings.errorCorrectionLevel);
    if (settings.version !== undefined && instance.setVersion) instance.setVersion(settings.version);
    if (settings.encodeMode !== undefined && instance.setEncodeMode) instance.setEncodeMode(settings.encodeMode);
    if (settings.codeSize !== undefined && instance.setCodeSize) instance.setCodeSize(settings.codeSize);
    if (settings.encodeScheme !== undefined && instance.setEncodeScheme) instance.setEncodeScheme(settings.encodeScheme);
    if (settings.errorLevel !== undefined && instance.setErrorLevel) instance.setErrorLevel(settings.errorLevel);
    if (settings.columns !== undefined && instance.setColumns) instance.setColumns(settings.columns);
    if (settings.rows !== undefined && instance.setRows) instance.setRows(settings.rows);
    if (settings.aspectRatio !== undefined && instance.setAspectRatio) instance.setAspectRatio(settings.aspectRatio);
    if (settings.yHeight !== undefined && instance.setYHeight) instance.setYHeight(settings.yHeight);

    // Call the method
    const func = instance[method];
    if (!func) {
        console.log(JSON.stringify({error: `Unknown method: ${method}`}));
        process.exit(1);
    }

    const result = func.apply(instance, methodArgs);
    console.log(JSON.stringify({result: result}));
} catch (err) {
    console.log(JSON.stringify({error: err.message || String(err)}));
    process.exit(1);
}
  JS

  # --- Base class ---
  class BarcodeWasmBase
    @@node_path = nil

    def initialize
      @settings = {
        "outputFormat" => "png",
        "foregroundColor" => nil,
        "backgroundColor" => nil
      }
      @wasm_dir = File.join(File.dirname(__FILE__), "..", "..", "wasm")
      ensure_node_available
    end

    def set_output_format(fmt)
      @settings["outputFormat"] = fmt
    end

    def set_foreground_color(r, g, b, a = 255)
      @settings["foregroundColor"] = [r, g, b, a]
    end

    def set_background_color(r, g, b, a = 255)
      @settings["backgroundColor"] = [r, g, b, a]
    end

    private

    def ensure_node_available
      return if @@node_path

      ["node", "nodejs"].each do |cmd|
        begin
          stdout, status = Open3.capture2(cmd, "--version")
          if status.success?
            @@node_path = cmd
            return
          end
        rescue Errno::ENOENT
          next
        end
      end

      raise WasmBarcodeError,
        "Node.js is required for WASM barcode generation. " \
        "Please install Node.js from https://nodejs.org/"
    end

    def call_wasm(method, *args)
      # Write runner script if not exists
      script_path = File.join(@wasm_dir, "_barcode_runner.mjs")
      unless File.exist?(script_path)
        File.write(script_path, NODE_SCRIPT)
      end

      call_args = {
        "className" => self.class::CLASS_NAME,
        "method" => method,
        "methodArgs" => args,
        "settings" => @settings
      }

      stdout, stderr, status = Open3.capture3(
        @@node_path, script_path, JSON.generate(call_args),
        chdir: @wasm_dir
      )

      unless status.success?
        error_msg = stderr.strip.empty? ? "Unknown error" : stderr.strip
        unless stdout.strip.empty?
          begin
            error_data = JSON.parse(stdout.strip)
            error_msg = error_data["error"] if error_data["error"]
          rescue JSON::ParserError
            # ignore
          end
        end
        raise WasmBarcodeError, "WASM execution failed: #{error_msg}"
      end

      # Find JSON line in output
      json_line = nil
      stdout.strip.split("\n").each do |line|
        line = line.strip
        json_line = line if line.start_with?("{") && line.end_with?("}")
      end

      raise WasmBarcodeError, "No JSON response found in WASM output" unless json_line

      begin
        output = JSON.parse(json_line)
        raise WasmBarcodeError, "WASM error: #{output['error']}" if output["error"]
        output["result"] || ""
      rescue JSON::ParserError
        raise WasmBarcodeError, "Invalid JSON response from WASM"
      end
    end
  end

  # --- 1D base ---
  class Barcode1DBase < BarcodeWasmBase
    def set_show_text(show)
      @settings["showText"] = show
    end

    def set_text_gap(scale)
      @settings["textGap"] = scale
    end

    def set_text_font_scale(scale)
      @settings["textFontScale"] = scale
    end

    def set_text_even_spacing(even)
      @settings["textEvenSpacing"] = even
    end

    def set_fit_width(fit)
      @settings["fitWidth"] = fit
    end

    def set_px_adjust_black(adjust)
      @settings["pxAdjustBlack"] = adjust
    end

    def set_px_adjust_white(adjust)
      @settings["pxAdjustWhite"] = adjust
    end

    def draw(code, width, height)
      call_wasm("draw", code, width, height)
    end
  end

  # --- 2D base ---
  class Barcode2DBase < BarcodeWasmBase
    def set_string_encoding(encoding)
      @settings["stringEncoding"] = encoding
    end

    def set_fit_width(fit)
      @settings["fitWidth"] = fit
    end

    def draw(code, size)
      call_wasm("draw", code, size)
    end
  end

  # --- 1D types ---

  class Code39 < Barcode1DBase
    CLASS_NAME = "Code39"

    def set_show_start_stop(show)
      @settings["showStartStop"] = show
    end
  end

  class Code93 < Barcode1DBase
    CLASS_NAME = "Code93"
  end

  class Code128 < Barcode1DBase
    CLASS_NAME = "Code128"

    def set_code_mode(mode)
      @settings["codeMode"] = mode
    end
  end

  class GS1128 < Barcode1DBase
    CLASS_NAME = "GS1_128"
  end

  class NW7 < Barcode1DBase
    CLASS_NAME = "NW7"

    def set_show_start_stop(show)
      @settings["showStartStop"] = show
    end
  end

  class ITF < Barcode1DBase
    CLASS_NAME = "ITF"
  end

  class Matrix2of5 < Barcode1DBase
    CLASS_NAME = "Matrix2of5"
  end

  class NEC2of5 < Barcode1DBase
    CLASS_NAME = "NEC2of5"
  end

  class JAN8 < Barcode1DBase
    CLASS_NAME = "Jan8"

    def set_extended_guard(ext)
      @settings["extendedGuard"] = ext
    end
  end

  class JAN13 < Barcode1DBase
    CLASS_NAME = "Jan13"

    def set_extended_guard(ext)
      @settings["extendedGuard"] = ext
    end
  end

  class UPCA < Barcode1DBase
    CLASS_NAME = "UPC_A"

    def set_extended_guard(ext)
      @settings["extendedGuard"] = ext
    end
  end

  class UPCE < Barcode1DBase
    CLASS_NAME = "UPC_E"

    def set_extended_guard(ext)
      @settings["extendedGuard"] = ext
    end
  end

  class GS1DataBar14 < Barcode1DBase
    CLASS_NAME = "GS1DataBar14"

    def set_symbol_type(type_str)
      @settings["symbolType"] = type_str
    end
  end

  class GS1DataBarLimited < Barcode1DBase
    CLASS_NAME = "GS1DataBarLimited"
  end

  class GS1DataBarExpanded < Barcode1DBase
    CLASS_NAME = "GS1DataBarExpanded"

    def set_symbol_type(type_str)
      @settings["symbolType"] = type_str
    end

    def set_no_of_columns(cols)
      @settings["noOfColumns"] = cols
    end
  end

  # --- Special ---

  class YubinCustomer < BarcodeWasmBase
    CLASS_NAME = "YubinCustomer"

    def set_px_adjust_black(adjust)
      @settings["pxAdjustBlack"] = adjust
    end

    def set_px_adjust_white(adjust)
      @settings["pxAdjustWhite"] = adjust
    end

    def draw(code, height)
      call_wasm("draw", code, height)
    end
  end

  # --- 2D types ---

  class QRCode < Barcode2DBase
    CLASS_NAME = "QR"

    def set_error_correction_level(level)
      @settings["errorCorrectionLevel"] = level
    end

    def set_version(version)
      @settings["version"] = version
    end

    def set_encode_mode(mode)
      @settings["encodeMode"] = mode
    end
  end

  class DataMatrix < Barcode2DBase
    CLASS_NAME = "DataMatrix"

    def set_code_size(size)
      @settings["codeSize"] = size
    end

    def set_encode_scheme(scheme)
      @settings["encodeScheme"] = scheme
    end
  end

  class PDF417 < Barcode2DBase
    CLASS_NAME = "PDF417"

    def set_error_level(level)
      @settings["errorLevel"] = level
    end

    def set_columns(cols)
      @settings["columns"] = cols
    end

    def set_rows(rows)
      @settings["rows"] = rows
    end

    def set_aspect_ratio(ratio)
      @settings["aspectRatio"] = ratio.to_f
    end

    def set_y_height(y)
      @settings["yHeight"] = y
    end

    def draw(code, width, height)
      call_wasm("draw", code, width, height)
    end
  end
end
