## Copyright (C) 2026 Dmitri A. Sergatskov <dasergatskov@gmail.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.

## __UNO_get_context__ - Internal function for getting a Java UNO context

function [xContext, launcher, xMCF] = __UNO_get_context__ (action="", varargin)

  persistent managed_url = "";
  persistent managed_profile = "";
  persistent managed_auto_profile = false;
  persistent atexit_registered = false;

  verbose = false;
  if (nargin > 1 && ! isempty (varargin{1}))
    verbose = varargin{1};
  endif

  xMCF = [];

  if (strcmp (action, "cleanup"))
    if (! isempty (managed_url) && ! uno_keepalive ())
      try
        [xContext, xMCF] = resolve_ready_uno_url (managed_url);
        xDesk = uno_query_interface ("com.sun.star.frame.XDesktop", ...
          xMCF.createInstanceWithContext ("com.sun.star.frame.Desktop", ...
                                          xContext));
        xDesk.terminate ();
      catch
      end_try_catch
      if (managed_auto_profile)
        ## Give the office process a moment to release the profile.
        pause (0.25);
        remove_managed_profile (managed_profile);
      endif
    endif
    managed_url = "";
    managed_profile = "";
    managed_auto_profile = false;
    if (atexit_registered)
      try
        atexit ("__exit_io__", false);
      catch
      end_try_catch
      atexit_registered = false;
    endif
    xContext = [];
    launcher = "";
    xMCF = [];
    return
  endif

  uno_url = getenv ("OCTAVE_IO_UNO_URL");
  if (! isempty (uno_url))
    try
      uno_verbose (verbose, "connecting to explicit UNO URL: %s", uno_url);
      [xContext, xMCF] = resolve_ready_uno_url (uno_url);
    catch err
      if (is_uno_classpath_error (err))
        error ("%s", uno_classpath_error_message (err));
      endif
      rethrow (err);
    end_try_catch
    launcher = "external";
    return
  endif

  transport = lower (strtrim (getenv ("OCTAVE_IO_UNO_TRANSPORT")));
  if (isempty (transport))
    transport = "socket";
  endif

  switch (transport)
    case "bootstrap"
      uno_verbose (verbose, "using legacy Bootstrap launcher");
      xContext = javaMethod ("bootstrap", "com.sun.star.comp.helper.Bootstrap");
      xMCF = xContext.getServiceManager ();
      launcher = "bootstrap";
      return

    case "socket"
      port = uno_port ();
      [profile, auto_profile] = uno_profile ();
      uno_url = default_uno_url (port);
      accept_string = sprintf ("socket,host=127.0.0.1,port=%d;urp;", port);
      managed_launcher = "socket-managed";

    case {"pipe", "pipes"}
      pipe_name = uno_pipe_name ();
      [profile, auto_profile] = uno_profile ();
      uno_url = default_pipe_uno_url (pipe_name);
      accept_string = sprintf ("pipe,name=%s;urp;", pipe_name);
      managed_launcher = "pipe-managed";

    otherwise
      error ("UNO: unsupported OCTAVE_IO_UNO_TRANSPORT value '%s'", transport);
  endswitch

  uno_verbose (verbose, "using %s transport", transport);
  uno_verbose (verbose, "UNO URL: %s", uno_url);
  uno_verbose (verbose, "LibreOffice profile: %s", profile);

  try
    [xContext, xMCF] = resolve_ready_uno_url (uno_url);
    if (! isempty (managed_url) && strcmp (uno_url, managed_url))
      launcher = managed_launcher;
    else
      launcher = transport;
    endif
    uno_verbose (verbose, "connected to existing UNO listener");
    return
  catch err
    if (is_uno_classpath_error (err))
      error ("%s", uno_classpath_error_message (err));
    endif
  end_try_catch

  start_uno_listener (accept_string, profile, verbose);

  ## Listener startup should not block Octave for minutes.
  poll_interval = 0.25;
  timeout = 30;
  max_attempts = ceil (timeout / poll_interval);
  uno_verbose (verbose, "waiting up to %.0f seconds for UNO listener", timeout);
  last_err = [];
  for ii = 1:max_attempts
    pause (poll_interval);
    try
      [xContext, xMCF] = resolve_ready_uno_url (uno_url);
      managed_url = uno_url;
      managed_profile = profile;
      managed_auto_profile = auto_profile;
      launcher = managed_launcher;
      uno_verbose (verbose, "UNO listener ready after %.2f seconds", ...
                   ii * poll_interval);
      ## Quitting Octave does not unload packages, so register cleanup in
      ## addition to the package-unload hook.
      if (! atexit_registered)
        try
          atexit ("__exit_io__");
          atexit_registered = true;
        catch
        end_try_catch
      endif
      return
    catch err
      if (is_uno_classpath_error (err))
        error ("%s", uno_classpath_error_message (err));
      endif
      last_err = err;
    end_try_catch
  endfor

  if (auto_profile && ! uno_keepalive ())
    remove_managed_profile (profile);
  endif
  msg = sprintf (["UNO: connection to LibreOffice failed after %.0f seconds at %s.  ", ...
                  "Verify that LibreOffice's Java/UNO components are installed."], ...
                 timeout, uno_url);
  if (isstruct (last_err) && isfield (last_err, "message") ...
      && ! isempty (last_err.message))
    msg = sprintf ("%s  Last error: %s", msg, last_err.message);
  endif
  error ("%s", msg);

endfunction


function [xContext, xMCF] = resolve_ready_uno_url (uno_url)

  xContext = resolve_uno_url (uno_url);
  ## Resolution can precede full remote context readiness on some systems.
  xMCF = xContext.getServiceManager ();

endfunction


function xContext = resolve_uno_url (uno_url)

  xLocalContext = javaMethod ("createInitialComponentContext", ...
                              "com.sun.star.comp.helper.Bootstrap", ...
                              javaObject ("java.util.HashMap"));
  xResolver = javaMethod ("create", "com.sun.star.bridge.UnoUrlResolver", ...
                          xLocalContext);
  xContext = xResolver.resolve (uno_url);
  ## Socket/pipe connections return a generic remote proxy in Octave.
  xContext = uno_query_interface ("com.sun.star.uno.XComponentContext", ...
                                  xContext);

endfunction


function uno_verbose (verbose, fmt, varargin)

  if (verbose)
    printf ("UNO: ");
    printf (fmt, varargin{:});
    printf ("\n");
  endif

endfunction


function iface = uno_query_interface (type_name, obj)

  iface = javaMethod ("queryInterface", "com.sun.star.uno.UnoRuntime", ...
                      javaObject ("com.sun.star.uno.Type", type_name), obj);

endfunction


function tf = is_uno_classpath_error (err)

  tf = false;
  if (! isstruct (err) || ! isfield (err, "message"))
    return
  endif
  msg = lower (err.message);
  patterns = {"classnotfoundexception", ...
              "noclassdeffounderror", ...
              "unable to find java class", ...
              "class not found"};
  for ii = 1:numel (patterns)
    if (! isempty (strfind (msg, patterns{ii})))
      tf = true;
      return
    endif
  endfor

endfunction


function msg = uno_classpath_error_message (err)

  msg = ["UNO: Java UNO class libraries are not loaded in the ", ...
         "javaclasspath.  Run chk_spreadsheet_support ([], 0, ", ...
         "PATH_TO_OOO) before using the ", ...
         "UNO interface.  Original error: ", err.message];

endfunction


function uno_url = default_pipe_uno_url (pipe_name)

  uno_url = sprintf ("uno:pipe,name=%s;urp;StarOffice.ComponentContext", ...
                     pipe_name);

endfunction


function uno_url = default_uno_url (port)

  uno_url = sprintf ("uno:socket,host=127.0.0.1,port=%d;urp;StarOffice.ComponentContext", ...
                     port);

endfunction


function pipe_name = uno_pipe_name ()

  persistent default_pipe_name = "";

  if (isempty (default_pipe_name))
    default_pipe_name = sprintf ("octave_io_uno_%s_%d_%d", ...
      user_token (), process_id (), floor (1e6 * rand ()));
  endif
  pipe_name = default_pipe_name;

endfunction


function [port, explicit_port] = uno_port ()

  persistent default_port = [];

  explicit_port = false;
  port = str2double (getenv ("OCTAVE_IO_UNO_PORT"));
  if (isfinite (port) && port > 0 && port <= 65535)
    explicit_port = true;
    return
  endif

  if (isempty (default_port))
    default_port = 20000 + mod (process_id () + user_hash (), 40000);
  endif
  port = default_port;

endfunction


function [profile, auto_profile] = uno_profile ()

  persistent default_profile = "";

  profile = getenv ("OCTAVE_IO_UNO_PROFILE");
  auto_profile = isempty (profile);
  if (auto_profile)
    if (isempty (default_profile))
      default_profile = fullfile (tempdir (), ...
        sprintf ("octave-io-uno-profile-%s-%d", user_token (), process_id ()));
    endif
    profile = default_profile;
  endif

endfunction


function remove_managed_profile (profile)

  if (isempty (profile) || ! exist (profile, "dir"))
    return
  endif
  ## This is an Octave-created temporary profile.  Avoid an interactive
  ## recursive-rmdir confirmation during package unload or Octave exit.
  confirm_recursive_rmdir (false, "local");
  try
    rmdir (profile, "s");
  catch
  end_try_catch

endfunction


function tf = uno_keepalive ()

  val = lower (strtrim (getenv ("OCTAVE_IO_UNO_KEEPALIVE")));
  tf = any (strcmp (val, {"1", "true", "yes", "on"}));

endfunction


function start_uno_listener (accept_string, profile, verbose=false)

  soffice = getenv ("OCTAVE_IO_SOFFICE");
  if (isempty (soffice))
    soffice = find_soffice ();
  endif
  if (isempty (soffice))
    error ("UNO: could not find LibreOffice soffice executable");
  endif
  uno_verbose (verbose, "starting LibreOffice listener");
  uno_verbose (verbose, "soffice executable: %s", soffice);

  if (strncmp (profile, "file://", 7))
    profile_arg = ["-env:UserInstallation=" profile];
  else
    profile_arg = ["-env:UserInstallation=" path_to_file_url(profile)];
  endif

  accept_arg = ["--accept=" accept_string];
  if (ispc ())
    null_device = "nul";
  else
    null_device = "/dev/null";
  endif
  app_bundle = "";
  if (ismac ())
    app_bundle = macos_app_bundle (soffice);
  endif
  if (! isempty (app_bundle))
    uno_verbose (verbose, "macOS app bundle: %s", app_bundle);
    cmd = sprintf ("open -n -a %s --args --headless %s --nologo --nodefault --norestore --nolockcheck --nofirststartwizard %s > %s 2>&1", ...
                   shell_quote (app_bundle), shell_quote (accept_arg), ...
                   shell_quote (profile_arg), null_device);
  else
    cmd = sprintf ("%s --headless %s --nologo --nodefault --norestore --nolockcheck --nofirststartwizard %s > %s 2>&1", ...
                   shell_quote (soffice), shell_quote (accept_arg), ...
                   shell_quote (profile_arg), null_device);
  endif
  uno_verbose (verbose, "accept string: %s", accept_string);
  system (cmd, false, "async");

endfunction


function soffice = find_soffice ()

  soffice = "";
  exe = "soffice";
  if (ispc ())
    exe = "soffice.exe";
  endif

  jcp = javaclasspath ();
  for ii = 1:numel (jcp)
    candidate = fullfile (jcp{ii}, exe);
    if (exist (candidate, "file"))
      soffice = candidate;
      return
    endif
  endfor

  candidate = file_in_path (getenv ("PATH"), exe);
  if (! isempty (candidate))
    soffice = candidate;
    return
  endif

  if (! ispc ())
    candidate = file_in_path (getenv ("PATH"), "libreoffice");
    if (! isempty (candidate))
      soffice = candidate;
      return
    endif
  endif

  candidates = {};
  if (ispc ())
    candidates = windows_soffice_candidates ();
  elseif (ismac ())
    candidates = macos_soffice_candidates ();
  elseif (isunix ())
    candidates = {"/usr/lib64/libreoffice/program/soffice", ...
                  "/usr/lib/libreoffice/program/soffice", ...
                  "/opt/libreoffice/program/soffice"};
  endif

  for ii = 1:numel (candidates)
    if (exist (candidates{ii}, "file"))
      soffice = candidates{ii};
      return
    endif
  endfor

  if (isunix () && ! ismac ())
    matches = dir ("/opt/libreoffice*/program/soffice");
    if (! isempty (matches))
      soffice = [matches(1).folder filesep matches(1).name];
    endif
  endif

endfunction


function app_bundle = macos_app_bundle (soffice)

  app_bundle = "";

  if (exist (soffice, "dir") && ! isempty (regexp (soffice, "\\.app$", "once")))
    app_bundle = soffice;
    return
  endif

  marker = [".app" filesep];
  idx = strfind (soffice, marker);
  if (! isempty (idx))
    app_bundle = soffice(1:idx(1)+3);
    if (exist (app_bundle, "dir"))
      return
    endif
  endif

  candidates = macos_app_candidates ();
  for ii = 1:numel (candidates)
    if (exist (candidates{ii}, "dir"))
      app_bundle = candidates{ii};
      return
    endif
  endfor

endfunction


function candidates = macos_app_candidates ()

  candidates = {"/Applications/LibreOffice.app"};
  cask_dirs = {"/opt/homebrew/Caskroom/libreoffice", ...
               "/usr/local/Caskroom/libreoffice"};

  for ii = 1:numel (cask_dirs)
    if (exist (cask_dirs{ii}, "dir"))
      apps = dir ([cask_dirs{ii} filesep "*" filesep "LibreOffice.app"]);
      for jj = 1:numel (apps)
        candidates{end+1} = [apps(jj).folder filesep apps(jj).name];
      endfor
    endif
  endfor

endfunction


function candidates = macos_soffice_candidates ()

  app_candidates = macos_app_candidates ();
  candidates = {};

  for ii = 1:numel (app_candidates)
    candidates{end+1} = [app_candidates{ii} filesep "Contents" filesep ...
                         "MacOS" filesep "soffice"];
  endfor

endfunction


function candidates = windows_soffice_candidates ()

  candidates = {};
  bases = {getenv ("ProgramFiles"), getenv ("ProgramFiles(x86)")};
  apps = {"LibreOffice"};

  for ii = 1:numel (bases)
    if (! isempty (bases{ii}))
      for jj = 1:numel (apps)
        candidates{end+1} = fullfile (bases{ii}, apps{jj}, "program", ...
                                      "soffice.exe");
      endfor
    endif
  endfor

endfunction


function pid = process_id ()

  try
    pid = getpid ();
  catch
    pid = floor (1e6 * rand ());
  end_try_catch

endfunction


function h = user_hash ()

  user = user_token ();
  h = 0;
  for ii = 1:numel (user)
    h += double (user(ii)) * ii;
  endfor

endfunction


function user = user_token ()

  user = getenv ("USER");
  if (isempty (user))
    user = getenv ("USERNAME");
  endif
  if (isempty (user))
    user = "user";
  endif
  user = regexprep (user, "[^A-Za-z0-9_.-]", "_");

endfunction


function url = path_to_file_url (path)

  if (ispc ())
    path = strrep (path, "\\", "/");
    url = ["file:///" path];
  else
    url = ["file://" path];
  endif

endfunction


function str = shell_quote (str)

  if (ispc ())
    str = ['"', strrep(str, '"', '""'), '"'];
  else
    squote = char (39);
    str = [squote, strrep(str, squote, [squote, char(92), squote, squote]), squote];
  endif

endfunction
