=head1 NAME

CFX5::FrameServ - Frame and PDF Viewer control routines

=head1 SYNOPSIS

Routines for manipulating the Adobe Frame Viewer as used by CFX-5, and
also the Adobe Acrobat PDF viewer.

=cut

package CFX5::FrameServ;

use strict;

use CFX5::Config qw(export_conf %Config query_conf unexport_conf unsetconf prepend_to_execpath);
use CFX5::Output qw(:all);

use CFX5::PathManip qw(join_file dirname basename fullpath findbin_by_path change_cwd);

use CFX5::Commands qw(do_cmd do_cmd_quiet bg_cmd parse_rv handle_rv);

use CFX5::Win32::Registry qw(registry_get);

use vars qw(@FramePIDList);

=head2 start_server

  CFX5::FrameServ::start_server();

On UNIX, starts the frame server process and records information about
it. On NT, this function does nothing, as the server starts fast enough
anyway, and is a lot more intrusive.

If an argument is given, it overrides the default RPC property for the
server.

If we do not need to start a server (see C<need_server>), no server is
started.

=cut

sub start_server {
  return if $Config{windows_nt};

  return if $Config{CFX_NO_FRAME_VIEWER};

  my $rpcprop_force=shift;
  
  return 1 unless need_server();

  my $Prog=$CFX5::Output::Prog;

  #
  # is the help viewer actually installed? If not we return; although
  # this is handled in cfx_helpserver and cfx_helpviewer too, we
  # handle it here so we don't give a nasty message when we try to
  # kill it.
  #
  my $fmhome=$Config{FMHOME};
  unless (-d $fmhome) {
    _note("FrameViewer directory $fmhome missing, not starting help server.");
    return 1;
  }

  #
  # make up an RPC property to use for the server, set some environment
  # variables used by the cfx_help* scripts, and start the server in the
  # background.
  #
  my $rpcProp=defined $rpcprop_force ? $rpcprop_force: "_Cfx_${Prog}_$$";
  $Config{CFX_HELP_WAS_STARTED_FOR}=$Prog;
  $Config{CFX_HELP_RPC_PROP}=$rpcProp;
  export_conf('CFX_HELP_WAS_STARTED_FOR', 'CFX_HELP_RPC_PROP');

  #
  # if we claimed the server, we'll have set CFX_HELP_SUPRESS_SERVER.
  # So we now need to unset it, or the server won't start ...
  #
  unexport_conf('CFX_HELP_SUPRESS_SERVER');

  #
  # we need to export this so that cfx_helpserver doesn't need to run
  # fmarch, which fails at GE
  #
  export_conf('FMARCH');

  #
  # start the server
  #
  my $pid=bg_cmd(join_file($Config{CFX5BINDIR},'cfx_helpserver'),
		 '-rpcProp',$rpcProp);

  _trace("Help server started with PID $pid");

  #
  # make sure cfx_helpclient won't start another copy of the server
  #
  $Config{CFX_HELP_SUPRESS_SERVER}=$pid;
  export_conf('CFX_HELP_SUPRESS_SERVER');

  #
  # and remember the PID
  #
  $Config{help_server_pid}=$pid;

  #
  # and finally, export a couple of other useful variables
  #
  export_conf(qw(CFX_HELP_DIR FMHOME FMVERSION));
}

=head2 need_server

  if (CFX5::FrameServ::need_server()) {
    # start server
  }

This routine returns TRUE if a help server should be started, or FALSE
if (for some reason) this is not necessary.  The conditions are: 

=over

=item 1.

If C<CFX_HELP_WAS_STARTED_FOR> is set to a string which is different
(ignoring case) to the name of the current program, then
C<CFX_HELP_SUPRESS_SERVER> is unset.

=item 2.

If C<CFX_NO_AUTO_HELP> is set, or C<CFX_HELP_SUPRESS_SERVER> is still
set, the answer is FALSE.

=item 3.

Otherwise, the answer is TRUE.

=back

=cut

sub need_server {
  my $Prog=$CFX5::Output::Prog;

  return 1 if $Config{CFX_NO_FRAME_VIEWER};

  #
  # If we claimed earlier that we were going to start it, then we are.
  # The checks below were done at that point.
  #
  return 1 if $Config{help_server_claim};

  #
  # first off, we check if help has already been started, but for a
  # different program; this can occur if (for example) the solver
  # manager fires off Visualise. Unless we handle it specially, the
  # help server gets killed when the solver manager exits, and then
  # help in Vis doesn't work. As we can't (easily) communicate up the
  # process tree, we just remove the evidence of that other server and 
  # carry on as normal.
  #
  if (defined query_conf('CFX_HELP_SUPRESS_SERVER')
      and defined query_conf('CFX_HELP_WAS_STARTED_FOR')) {
    if (lc($Config{CFX_HELP_WAS_STARTED_FOR}) ne lc($Prog)) {
      _debug("Found help server from another program $Prog");
      $Config{CFX_HELP_SUPRESS_SERVER}=undef;
      $Config{CFX_HELP_WAS_STARTED_FOR}=undef;
      $Config{CFX_HELP_RPC_PROP}=undef;
    }
  }

  #
  # if the user has requested that we don't start a server, or if we
  # were called from another script which has already started it, then
  # don't do anything.
  #
  # So, for example, the user could use cfx_helpserver -rpcProp hello,
  # and then export CFX_HELP_RPC_PROP=hello and CFX_NO_AUTO_HELP. All
  # CFX programs she started would then use this help server, and none
  # of them would kill it. (Setting CFX_HELP_RPC_PROP on its own and
  # starting more than one program Can Be Dangerous).
  #
  if (defined query_conf('CFX_HELP_SUPRESS_SERVER')
      or defined query_conf('CFX_NO_AUTO_HELP')) {
    _debug "Requested not to start server";
    return 0;
  }

  1;
}

=head2 claim_server

  CFX5::FrameServ::claim_server($myname);

Calling this routine sets up the current environment to indicate that
sub-processes should not start their own help servers.  It does this
by setting various internal environment variables
(C<CFX_HELP_SUPRESS_SERVER> (yes, I know I spelt it wrong),
C<CFX_HELP_WAS_STARTED_FOR>).

This routine is normally called automatically by C<start_server>, but
is provided for the case where the server needs to be started with
knowledge of the process which will call it (ie, for CFX-Build).

The argument is a descriptive name, and is not used for anything much.
C<start_server> sets it to the PID of the existing help server.

If we do not need to start a server (see C<need_server>), this
function does nothing.

=cut

sub claim_server {
  my $name=shift;

  return 1 if $Config{CFX_NO_FRAME_VIEWER};

  return 1 unless need_server();

  my $Prog=$CFX5::Output::Prog;

  unless ($name) {
    $name = $$ . "_" . $Prog;
  }

  $Config{CFX_HELP_WAS_STARTED_FOR}=$Prog;
  export_conf('CFX_HELP_WAS_STARTED_FOR');

  #
  # make sure cfx_helpclient won't start another copy of the server
  #
  $Config{CFX_HELP_SUPRESS_SERVER}="claimed$$";
  export_conf('CFX_HELP_SUPRESS_SERVER');

  #
  # really, we should set the RPC property to what it's going to be
  # (as it is used in informational messages), but we're calling this
  # function because we don't know!  I think it should be OK, as when
  # cfx_helpserver is called it should usually have the -rpcProp
  # argument ... ?
  #
  unless (defined $Config{CFX_HELP_RPC_PROP}) {
    $Config{CFX_HELP_RPC_PROP}='(unknown)';
  }
  export_conf('CFX_HELP_RPC_PROP');

  #
  # because this works by nobbling the cfx_helpserver script, if we
  # want to start up help again later we need to know it was us who
  # nobbled the script so we can un-nobble it again.
  #
  $Config{help_server_claim}=1;

  1;
}

=head2 stop_server

  CFX5::FrameServ::start_server();

On UNIX, starts the frame server process and records information about
it. On NT, this function does nothing.

=cut

sub stop_server {
  return if $Config{windows_nt};
  
  return if $Config{CFX_NO_FRAME_VIEWER};

  my $Prog=$CFX5::Output::Prog;

  # if we didn't start a help server, don't stop one
  return 1 unless defined $Config{help_server_pid};
  
  my $pid=$Config{help_server_pid};
  _trace("Killing help server $pid");

  # CFX-Build shuts down the help server itself
  unless (kill TERM => $pid
	  or lc($Prog) =~ m/^cfx5build/) {
    _warn "Error killing help server $pid";
  }

  unexport_conf(qw(help_server_pid CFX_HELP_SUPRESS_SERVER 
		   CFX_HELP_RPC_PROP CFX_HELP_ERR));
  unsetconf(qw(help_server_pid CFX_HELP_SUPRESS_SERVER 
	       CFX_HELP_RPC_PROP CFX_HELP_ERR));

  1;
}

=head2 request_page

  CFX5::FrameServ::request_page("master_directory.fm", "link-name");

Uses F<cfx_helpclient.exe> on NT, or the F<cfx_helpclient> script on
UNIX, to bring up a page in the help server. The second argument is
optional, and should name a link to go to in the document if present.
The function will not return until the page has been displayed.

Errors other than failure to open the named page will be signalled with
C<_error>. Additionally, TRUE is returned on success, or FALSE
otherwise.

TODO: On UNIX, this should be recoded to call the frame client directly.

=cut

sub request_page {
  my ($page, $link, $fullpath_on_nt)=@_;

  internal_error("fullpath specified but not on NT!")
    if ($fullpath_on_nt and !$Config{windows_nt});

  my $helpclient;

  if (-x join_file($Config{CFX5EXECDIR},'cfx_helpclient.exe')) {
    $helpclient = join_file($Config{CFX5EXECDIR},'cfx_helpclient.exe');
  } elsif (-x join_file($Config{CFX_CMD_DIR},'cfx_helpclient')) {
    $helpclient = join_file($Config{CFX_CMD_DIR},'cfx_helpclient');
  } else {
    _error("Could not locate help client program.");
    return 0;
  }

  my @args=($helpclient);
  push @args, '-fullpath' if $fullpath_on_nt;

  internal_error("Page not given to request_page") unless defined $page;
  push @args, $page;
  push @args, $link if defined $link;

  # the help client on NT won't show books, so don't even try
  if ($Config{windows_nt} and $page =~ m/\.book$/i) {
    _error("Book files cannot be automatically displayed under Windows NT.\n\n",
	   "The frame viewer will be started for you, please use the File>Open\n",
	   "command to look at the following file:\n\t$page");
    change_cwd(dirname($page));
  }

  #
  # on Windows NT, find the frame viewer executable first.  We don't
  # run it, as we don't know if it's already running, but if we cannot
  # locate it we die with a nice message rather than a nasty one.
  #
  if ($Config{windows_nt}) {
    # make sure we can find the executable
    my $fmhome=$Config{FMHOME};
    if (defined $fmhome and -x join_file($fmhome,'FrameViewer.exe')) {
      _debug("Found FrameViewer.exe in $fmhome");
      prepend_to_execpath($fmhome);
    } elsif (findbin_by_path('FrameViewer.exe') eq '.') {
      # nope, no such luck
      _error("ANSYS CFX was unable to locate the Adobe Frame Viewer, which is required to view the ANSYS CFX online help text.  Please check your installation of the frame viewer and try again.");
      return 0;
    }
  }

  my $rh = parse_rv(do_cmd_quiet(@args));
  my $rv=handle_rv($rh);
  if ($rv) {
    _error("Error $rv occurred while attempting to start the Frame Viewer.");
  }
  return $rh->{success};
}

=head2 view_page

  CFX5::FrameServ::view_page(join_file($Config{CFX_HELP_DIR},"master_directory.fm"));

Uses F<cfx_helpclient.exe> on NT, or the frame viewer directly on UNIX,
to bring up a page. The function will not return until the page has been
displayed and closed (?).

This differs from request_page on UNIX, in that it does not require a
running server; instead it opens a viewer with a control panel, so the
user can shut it down herself when she has finished, rather than the
script having to worry about it. This is a non-issue on NT. Also, it
handles more errors verbosely via C<_error> and C<_abort>.

Errors other than failure to open the named page will be signalled with
C<_error>. Additionally, TRUE is returned on success, or FALSE
otherwise.

Do not use this function in conjunction with C<start_server> and
C<stop_server>; if you are using those, you should use the
C<request_page> function to display the page using the running server.

=cut

sub view_page {
  my ($page)=@_;

  internal_error("Page not given to view_page") unless defined $page;

  return request_page($_[0],$_[1],1) if $Config{windows_nt};
  
  #
  # UNIX
  #
  my @cmd;
  my $file=join_file($Config{FMHOME}, 'bin', 'viewer');
  CFX5::FileTests::execfile($file, 'Online help viewer')
    or return 0;
  push @cmd, $file;
  push @cmd, '-noapi', '-log_file_only';

  CFX5::FileTests::datafile($page, 'Requested help page')
    or return 0;
  push @cmd, '-f', $page;

  return handle_rv(do_cmd(@cmd));
}

=head2 get_fmhome

  add_to_path(CFX5::FrameServ::get_fmhome());

Tries to get the location of the Frame Viewer binary directory from
the Registry under Windows NT. If it fails, or is called on UNIX,
returns undef.

=cut

sub get_fmhome {
  return undef unless $Config{windows_nt};

  my $fmhome = registry_get('SOFTWARE\Adobe\FrameViewer\5.5\FMHome');

  if (defined $fmhome and -d $fmhome) {
    return fullpath($fmhome);
  } elsif (defined $fmhome) {
    _warn("Frame viewer home directory in the registry is\n\t", $fmhome,
	  "\nbut this directory does not exist.");
    return undef;
  } else {
    #_note("Unable to locate Frame Viewer in registry, check it is installed correctly");
    return undef;
  }
}

=head2 view_pdf

Opens a PDF document.  Returns TRUE on success or FALSE on failure,
and signals its own errors.

=cut

sub view_pdf {
  my $file=shift;

  # check we've got everything we need
  unless (defined $Config{CFX_PDF_READCMD}) {
    _error(<<"EOF");
The Adobe Acrobat Reader could not be found.  You will not be able to
read the CFX $Config{CFXREVISION} on-line documentation unless you
install the Adobe Acrobat Reader in its default location as
recommended by Adobe or you install it (or another PDF reader)
elsewhere and tell CFX where to find the command, by setting a
suitable value for CFX_PDF_READCMD in a CFX resource configuration
file.
EOF
    return 0;
  }

  CFX5::FileTests::datafile($file,"Requested help file $file")
      or return 0;

  my $rv=handle_rv
    (do_cmd($Config{CFX_PDF_READCMD},
	    (defined $Config{CFX_PDF_READOPT} ? $Config{CFX_PDF_READOPT} : ()),
	    $file));
  if ($rv) {
    _error("The PDF reader command",
           $Config{CFX_PDF_READCMD},
           (defined $Config{CFX_PDF_READOPT} ? $Config{CFX_PDF_READOPT} : ()),
           $file, "exited with return code $rv");
    return 0;
  }

  1;
}

1;
