// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/webaudio/audio_node_wiring.h"

#include "base/memory/raw_ref.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node_input.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node_output.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"

namespace blink {

namespace {

using AudioNodeOutputSet = HashSet<AudioNodeOutput*>;

struct FindOutputResult {
  const raw_ref<AudioNodeOutputSet> output_set;
  AudioNodeOutputSet::const_iterator iterator;
  bool is_disabled;
};

// Given a connected output, finds it in the "active" or "disabled" set (e.g.
// of the outputs connected to an input). Produces the set in which it was
// found, an iterator into that set (so that it can be erased), and whether or
// not the set it was found in was the disabled set.
//
// It is an error to pass an output which is *not* connected (i.e. is neither
// active nor disabled).
FindOutputResult FindOutput(AudioNodeOutput& output,
                            AudioNodeOutputSet& outputs,
                            AudioNodeOutputSet& disabled_outputs) {
  auto it = outputs.find(&output);
  if (it != outputs.end()) {
    return {raw_ref(outputs), it, false};
  }

  it = disabled_outputs.find(&output);
  if (it != disabled_outputs.end()) {
    return {raw_ref(disabled_outputs), it, true};
  }

  NOTREACHED() << "The output must be connected to the input.";
}

}  // namespace

void AudioNodeWiring::Connect(AudioNodeOutput& output, AudioNodeInput& input) {
  input.AssertGraphOwner();

  const bool input_connected_to_output =
      input.outputs_.Contains(&output) ||
      input.disabled_outputs_.Contains(&output);
  const bool output_connected_to_input = output.inputs_.Contains(&input);
  DCHECK_EQ(input_connected_to_output, output_connected_to_input);

  // Do nothing if already connected.
  if (input_connected_to_output) {
    return;
  }

  (output.is_enabled_ ? input.outputs_ : input.disabled_outputs_)
      .insert(&output);
  output.inputs_.insert(&input);

  // If it has gained an active connection, the input may need to have its
  // rendering state updated.
  if (output.is_enabled_) {
    input.ChangedOutputs();
  }

  // The input node's handler needs to know about this connection. This may
  // cause it to re-enable itself.
  input.Handler().MakeConnection();
}

void AudioNodeWiring::Connect(AudioNodeOutput& output,
                              AudioParamHandler& param) {
  param.AssertGraphOwner();

  const bool param_connected_to_output = param.outputs_.Contains(&output);
  const bool output_connected_to_param = output.params_.Contains(&param);
  DCHECK_EQ(param_connected_to_output, output_connected_to_param);

  // Do nothing if already connected.
  if (param_connected_to_output) {
    return;
  }

  param.outputs_.insert(&output);
  output.params_.insert(&param);

  // The param may need to have its rendering state updated.
  param.ChangedOutputs();
}

void AudioNodeWiring::Disconnect(AudioNodeOutput& output,
                                 AudioNodeInput& input) {
  input.AssertGraphOwner();

  // These must be connected.
  DCHECK(output.inputs_.Contains(&input));
  DCHECK(input.outputs_.Contains(&output) ||
         input.disabled_outputs_.Contains(&output));

  // Find the output in the appropriate place.
  auto result = FindOutput(output, input.outputs_, input.disabled_outputs_);

  // Erase the pointers from both sets.
  result.output_set->erase(result.iterator);
  output.inputs_.erase(&input);

  // If an active connection was disconnected, the input may need to have its
  // rendering state updated.
  if (!result.is_disabled) {
    input.ChangedOutputs();
  }

  // The input node's handler may try to disable itself if this was the last
  // connection. This must happen after the set erasures above, or the disabling
  // logic would observe an inconsistent state.
  input.Handler().BreakConnectionWithLock();
}

void AudioNodeWiring::Disconnect(AudioNodeOutput& output,
                                 AudioParamHandler& param) {
  param.AssertGraphOwner();

  DCHECK(param.outputs_.Contains(&output));
  DCHECK(output.params_.Contains(&param));

  // Erase the pointers from both sets.
  param.outputs_.erase(&output);
  output.params_.erase(&param);

  // The param may need to have its rendering state updated.
  param.ChangedOutputs();
}

scoped_refptr<AudioHandler> AudioNodeWiring::Disable(AudioNodeOutput& output,
                                                     AudioNodeInput& input) {
  input.AssertGraphOwner();

  // These must be connected.
  DCHECK(output.inputs_.Contains(&input));
  DCHECK(input.outputs_.Contains(&output) ||
         input.disabled_outputs_.Contains(&output));

  // The output should have been marked as disabled.
  DCHECK(!output.is_enabled_);

  // Move from the active list to the disabled list.
  // Do nothing if this is the current state.
  if (!input.disabled_outputs_.insert(&output).is_new_entry) {
    return nullptr;
  }
  input.outputs_.erase(&output);

  // Since it has lost an active connection, the input may need to have its
  // rendering state updated.
  input.ChangedOutputs();

  // Return the downstream handler so the caller can propagate the disabled
  // state downstream (e.g. via a worklist) to avoid deep recursion.
  // This must happen after the set manipulations above, or the disabling
  // logic could observe an inconsistent state.
  return base::WrapRefCounted(&input.Handler());
}

scoped_refptr<AudioHandler> AudioNodeWiring::Enable(AudioNodeOutput& output,
                                                    AudioNodeInput& input) {
  input.AssertGraphOwner();

  // These must be connected.
  DCHECK(output.inputs_.Contains(&input));
  DCHECK(input.outputs_.Contains(&output) ||
         input.disabled_outputs_.Contains(&output));

  // The output should have been marked as enabled.
  DCHECK(output.is_enabled_);

  // Move from the disabled list to the active list.
  // Do nothing if this is the current state.
  if (!input.outputs_.insert(&output).is_new_entry) {
    return nullptr;
  }
  input.disabled_outputs_.erase(&output);

  // Since it has gained an active connection, the input may need to have its
  // rendering state updated.
  input.ChangedOutputs();

  // Return the downstream handler so the caller can propagate the enabled
  // state downstream (e.g. via a worklist) to avoid deep recursion.
  // This must happen after the set manipulations above, or the enabling
  // logic could observe an inconsistent state.
  return base::WrapRefCounted(&input.Handler());
}

bool AudioNodeWiring::IsConnected(AudioNodeOutput& output,
                                  AudioNodeInput& input) {
  input.AssertGraphOwner();

  bool is_connected = output.inputs_.Contains(&input);
  DCHECK_EQ(is_connected, input.outputs_.Contains(&output) ||
                              input.disabled_outputs_.Contains(&output));
  return is_connected;
}

bool AudioNodeWiring::IsConnected(AudioNodeOutput& output,
                                  AudioParamHandler& param) {
  param.AssertGraphOwner();

  bool is_connected = output.params_.Contains(&param);
  DCHECK_EQ(is_connected, param.outputs_.Contains(&output));
  return is_connected;
}

void AudioNodeWiring::WillBeDestroyed(AudioNodeInput& input) {
  // This is more or less a streamlined version of calling Disconnect
  // repeatedly. In particular it cannot happen while the input's handler is
  // being destroyed, and so does not require any information about these final
  // changes to its connections.
  //
  // What does matter, however, is ensuring that no AudioNodeOutput holds a
  // dangling pointer to `input`.

  input.AssertGraphOwner();

  for (AudioNodeOutput* output : input.outputs_) {
    output->inputs_.erase(&input);
  }
  for (AudioNodeOutput* output : input.disabled_outputs_) {
    output->inputs_.erase(&input);
  }
  input.outputs_.clear();
  input.disabled_outputs_.clear();
}

}  // namespace blink
