# SPDX-FileCopyrightText: 2012, 2013, 2016, 2020 Michael Pyne <mpyne@kde.org>
#
# SPDX-License-Identifier: GPL-2.0-or-later

package ksb::IPC;

# Handles the asynchronous communications needed to perform update and build
# processes at the same time. This can be thought of as a partially-abstract
# class, really you should use IPC::Null (which is fully synchronous) or
# IPC::Pipe, which both fall back to common methods implemented here.

use ksb;

our $VERSION = '0.20';

use ksb::BuildException; # make_exception
use ksb::Debug;

# IPC message types
use constant {
    MODULE_SUCCESS  => 1, # Used for a successful src checkout
    MODULE_FAILURE  => 2, # Used for a failed src checkout
    MODULE_SKIPPED  => 3, # Used for a skipped src checkout (i.e. build anyways)
    MODULE_UPTODATE => 4, # Used to skip building a module when had no code updates

    # One of these messages should be the first message placed on the queue.
    ALL_SKIPPED     => 5, # Used to indicate a skipped update process (i.e. build anyways)
    ALL_FAILURE     => 6, # Used to indicate a major update failure (don't build)
    ALL_UPDATING    => 7, # Informational message, feel free to start the build.

    # Used to indicate specifically that a source conflict has occurred.
    MODULE_CONFLICT => 8,

    MODULE_LOGMSG   => 9, # Tagged message should be put to TTY for module.

    MODULE_PERSIST_OPT => 10, # Change to a persistent module option

    ALL_DONE        => 11, # Affirmatively flags that all updates are done

    MODULE_POSTBUILD_MSG => 12, # A message to print after all work done
};

sub new
{
    my $class = shift;
    my $defaultOpts = {
        no_update     => 0,
        updated       => { },
        logged_module => 'global',
        messages      => { }, # Holds log output from update process
        postbuild_msg => { }, # Like above but for post-build msgs
        why_refresh   => { }, # If module should build despite not being updated, why?
        updates_done  => 0,
        opt_update_handler => undef, # Callback for persistent option changes
    };

    # Must bless a hash ref since subclasses expect it.
    return bless $defaultOpts, $class;
}

# Sends a message to the main/build process that a persistent option for the
# given module name must be changed. For use by processes that do not control
# the persistent option store upon shutdown.
sub notifyPersistentOptionChange
{
    my $self = shift;
    my ($moduleName, $optName, $optValue) = @_;

    $self->sendIPCMessage(ksb::IPC::MODULE_PERSIST_OPT, "$moduleName,$optName,$optValue");
}

# Sends a message to the main/build process that a given message should be
# shown to the user at the end of the build.
sub notifyNewPostBuildMessage
{
    my ($self, $moduleName, $msg) = @_;

    $self->sendIPCMessage(ksb::IPC::MODULE_POSTBUILD_MSG, "$moduleName,$msg");
}

sub notifyUpdateSuccess
{
    my $self = shift;
    my ($module, $msg) = @_;

    $self->sendIPCMessage(ksb::IPC::MODULE_SUCCESS, "$module,$msg");
}

# Sets which module messages stored by sendLogMessage are supposed to be
# associated with.
sub setLoggedModule
{
    my ($self, $moduleName) = @_;
    $self->{logged_module} = $moduleName;
}

# Sends a message to be logged by the process holding the TTY.
# The logged message is associated with the module set by setLoggedModule.
sub sendLogMessage
{
    my ($self, $msg) = @_;
    my $loggedModule = $self->{logged_module};

    $self->sendIPCMessage(MODULE_LOGMSG, "$loggedModule,$msg");
}

# Prints the given message out (adjusting to have proper whitespace
# if needed). For use with the log-message forwarding facility.
sub _printLoggedMessage
{
    my $msg = shift;

    $msg = "\t$msg" unless $msg =~ /^\s+/;
    ksb::Debug::print_clr($msg);
}

# Called any time we're waiting for an IPC message from a sub process. This can
# occur during a module build (waiting for messages from update process) or
# while we're near the end of the script execution. There is no way to tell
# which module we'll be about to receive messages for from the other end.
sub _updateSeenModulesFromMessage
{
    my ($self, $ipcType, $buffer) = @_;

    my $updated     = $self->{'updated'};
    my $messagesRef = $self->{'messages'};
    my $message;

    croak_runtime("IPC failure: no IPC mechanism defined") unless $ipcType;

    if ($ipcType eq ksb::IPC::MODULE_SUCCESS) {
        my ($ipcModuleName, $msg) = split(/,/, $buffer);
        $message = $msg;
        $updated->{$ipcModuleName} = 'success';
    } elsif ($ipcType eq ksb::IPC::MODULE_SKIPPED) {
        # The difference between success here and 'skipped' below
        # is that success means we should build even though we
        # didn't perform an update, while 'skipped' means the
        # *build* should be skipped even though there was no
        # failure.
        $message = 'skipped';
        $updated->{$buffer} = 'success';
    } elsif ($ipcType eq ksb::IPC::MODULE_CONFLICT) {
        $message = 'conflicts present';
        $updated->{$buffer} = 'failed';
    } elsif ($ipcType eq ksb::IPC::MODULE_FAILURE) {
        $message = 'update failed';
        $updated->{$buffer} = 'failed';
    } elsif ($ipcType eq ksb::IPC::MODULE_UPTODATE) {
        # Although the module source hasn't changed, the user might be forcing a
        # rebuild, so our message should reflect what's actually going to happen.
        $message = 'no files affected';
        my ($ipcModuleName, $refreshReason) = split(',', $buffer);

        if ($refreshReason) {
            $updated->{$ipcModuleName} = 'success';
            $self->{why_refresh}->{$ipcModuleName} = $refreshReason;
        } else {
            $updated->{$ipcModuleName} = 'skipped';
        }
    } elsif ($ipcType eq ksb::IPC::MODULE_PERSIST_OPT) {
        my ($ipcModuleName, $optName, $value) = split(',', $buffer);
        if ($self->{opt_update_handler}) {
            # Call into callback to update persistent options
            $self->{opt_update_handler}->($ipcModuleName, $optName, $value);
        }
    } elsif ($ipcType eq ksb::IPC::MODULE_LOGMSG) {
        my ($ipcModuleName, $logMessage) = split(',', $buffer, 2);

        # Save it for later if we can't print it yet.
        $messagesRef->{$ipcModuleName} //= [ ];
        push @{$messagesRef->{$ipcModuleName}}, $logMessage;
    } elsif ($ipcType eq ksb::IPC::ALL_DONE) {
        $self->{updates_done} = 1;
    } elsif ($ipcType eq ksb::IPC::MODULE_POSTBUILD_MSG) {
        my ($ipcModuleName, $postBuildMsg) = split(',', $buffer, 2);

        $self->{postbuild_msg}->{$ipcModuleName} //= [ ];
        push @{$self->{postbuild_msg}->{$ipcModuleName}}, $postBuildMsg;
    } else {
        croak_internal("Unhandled IPC type: $ipcType");
    }

    return $message;
}

# Used to assign a callback / subroutine to use for updating persistent
# options based on IPC update messages.  The sub should itself take a
# key and value pair.
sub setPersistentOptionHandler
{
    my ($self, $handler) = @_;
    $self->{opt_update_handler} = $handler;
}

# Returns a text reason to refresh a non-updated module, or an empty string if
# the module has been updated or has not yet been seen.
sub refreshReasonFor ($self, $module)
{
    return $self->{why_refresh}->{$module} // '';
}

sub waitForEnd
{
    my $self = shift;

    $self->waitForStreamStart();
    while(!$self->{no_update} && !$self->{updates_done}) {
        my ($ipcType, $buffer) = $self->receiveIPCMessage();

        # We ignore the return value in favor of ->{updates_done}
        $self->_updateSeenModulesFromMessage($ipcType, $buffer);
    }
}

# Waits for an update for a module with the given name.
# Returns a list containing whether the module was successfully updated,
# and any specific string message (e.g. for module update success you get
# number of files affected)
# Will throw an exception for an IPC failure or if the module should not be
# built.
sub waitForModule
{
    my ($self, $module) = @_;

    my $moduleName = $module->name();
    my $updated = $self->{'updated'};

    # Wait for the initial phase to complete, if it hasn't.
    $self->waitForStreamStart();

    # No update? Just mark as successful
    if ($self->{'no_update'} || !$module->phases()->has('update')) {
        $updated->{$moduleName} = 'success';
        return ('success', 'Skipped');
    }

    my $message;
    while(! defined $updated->{$moduleName} && !$self->{updates_done}) {
        my ($ipcType, $buffer) = $self->receiveIPCMessage();

        $message = $self->_updateSeenModulesFromMessage($ipcType, $buffer);

        # If we have 'global' messages they are probably for the first module and
        # include standard setup messages, etc. Print first and then print module's
        # messages.
        my $messagesRef = $self->{'messages'};
        for my $item ('global', $moduleName) {
            for my $msg (@{$messagesRef->{$item}}) {
                _printLoggedMessage($msg);
            }

            delete $messagesRef->{$item};
        }
    }

    # We won't print post-build messages now but we need to save them for when
    # they can be printed.
    for my $msg (@{$self->{postbuild_msg}->{$moduleName}}) {
        $module->addPostBuildMessage($msg);
    }
    delete $self->{postbuild_msg}->{$moduleName};

    return ($updated->{$moduleName}, $message);
}

# Just in case we somehow have messages to display after all modules are
# processed, we have this function to show any available messages near the end
# of the script run.
sub outputPendingLoggedMessages
{
    my $self = shift;
    my $messages = $self->{messages};

    while (my ($module, $logMessages) = each %{$messages}) {
        my @nonEmptyMessages = grep { !!$_ } @{$logMessages};
        if (@nonEmptyMessages) {
            debug ("Unhandled messages for module $module:");
            ksb::Debug::print_clr($_) foreach @nonEmptyMessages;
        }
    }

    $self->{messages} = { };
}

# Flags the given module as something that can be ignored from now on.  For use
# after the module has been waited on
sub forgetModule
{
    my ($self, $module) = @_;

    my $modulename = $module->name();
    delete $self->{'updated'}->{$modulename};
}

# Returns a hashref mapping module *names* to update statuses, for modules that
# have not already been marked as ignorable using forgetModule()
sub unacknowledgedModules
{
    my $self = shift;

    return $self->{'updated'};
}

# Waits on the IPC connection until one of the ALL_* IPC codes is returned.
# If ksb::IPC::ALL_SKIPPED is returned then the 'no_update' entry will be set in
# $self to flag that you shouldn't wait.
# If ksb::IPC::ALL_FAILURE is returned then an exception will be thrown due to the
# fatal error.
# This method can be called multiple times, but only the first time will
# result in a wait.
sub waitForStreamStart
{
    my $self = shift;
    state $waited = 0;

    return if $waited;

    my $buffer  = '';
    my $ipcType = 0;
    $waited = 1;

    while ($ipcType != ksb::IPC::ALL_UPDATING) {
        ($ipcType, $buffer) = $self->receiveIPCMessage();

        if (!$ipcType) {
            croak_internal("IPC Failure waiting for stream start :( $!");
        }
        if ($ipcType == ksb::IPC::ALL_FAILURE)
        {
            croak_runtime("Unable to perform source update for any module:\n\t$buffer");
        }
        elsif ($ipcType == ksb::IPC::ALL_SKIPPED)
        {
            $self->{'no_update'} = 1;
            $self->{'updates_done'} = 1;
        }
        elsif ($ipcType == ksb::IPC::MODULE_LOGMSG) {
            my ($ipcModuleName, $logMessage) = split(",", $buffer, 2);
            $self->{messages}->{$ipcModuleName} //= [ ];
            push @{$self->{messages}->{$ipcModuleName}}, $logMessage;
        }
        elsif ($ipcType != ksb::IPC::ALL_UPDATING)
        {
            croak_runtime("IPC failure while expecting an update status: Incorrect type: $ipcType");
        }
    }
}

# Sends an IPC message along with some IPC type information.
#
# First parameter is the IPC type to send.
# Second parameter is the actual message.
# All remaining parameters are sent to the object's sendMessage()
#  procedure.
sub sendIPCMessage
{
    my ($self, $ipcType, $msg) = @_;

    my $encodedMsg = pack("l! a*", $ipcType, $msg);
    return $self->sendMessage($encodedMsg);
}

# Static class function to unpack a message.
#
# First parameter is the message.
#
# Returns the IPC message type and message content.
sub unpackMsg
{
    my $msg = shift;

    my ($returnType, $outBuffer) = unpack("l! a*", $msg);

    return ($returnType, $outBuffer);
}

# Receives an IPC message and decodes it into the message and its
# associated type information.
#
# Returns the list with IPC type and message content, or list with two undef on failure.
sub receiveIPCMessage
{
    my $self = shift;

    croak_internal("Trying to pull message from closed IPC channel!") if $self->{updates_done};
    my $msg = $self->receiveMessage();
    return ($msg ? unpackMsg($msg) : (undef, undef));
}

# These must be reimplemented.  They must be able to handle scalars without
# any extra frills.
#
# sendMessage should accept one parameter (the message to send) and return
# true on success, or false on failure.  $! should hold the error information
# if false is returned.
sub sendMessage { croak_internal("Unimplemented."); }

# receiveMessage should return a message received from the other side, or
# undef for EOF or error.  On error, $! should be set to hold the error
# information.
sub receiveMessage { croak_internal("Unimplemented."); }

# Should be reimplemented if default does not apply.
sub supportsConcurrency
{
    return 0;
}

# Should be reimplemented if default does not apply.
sub close
{
}

1;
