/*!********************************************************************

  Audacity: A Digital Audio Editor

  @file AudioUnitEffectBase.cpp

  Dominic Mazzoni
  Leland Lucius

  Paul Licameli split from AudioUnitEffect

*******************************************************************//**

\class AudioUnitEffectBase
\brief An Effect class that handles a wide range of effects.  ??Mac only??

*//*******************************************************************/

#if USE_AUDIO_UNITS
#include "AudioUnitEffectBase.h"
#include "AudioUnitInstance.h"
#include "ConfigInterface.h"

#include <wx/ffile.h>
#include <wx/osx/core/private.h>

//
// When a plug-in's state is saved to the settings file (as a preset),
// it is in binary and gets base64 encoded before storing.
//
// When exporting, save as XML without base64 encoding.
//
// The advantages of XML format is less chance of failures occurring
// when exporting.  But, it can take a bit more space per preset int
// the Audacity settings file.
//
// Using binary for now.  Use kCFPropertyListXMLFormat_v1_0 if XML
// format is desired.
//
#define PRESET_FORMAT kCFPropertyListBinaryFormat_v1_0

// Name of the settings key to use for the above value
#define PRESET_KEY wxT("Data")

// Where the presets are located
#define PRESET_LOCAL_PATH wxT("/Library/Audio/Presets")
#define PRESET_USER_PATH wxT("~/Library/Audio/Presets")

TranslatableString AudioUnitEffectBase::SaveBlobToConfig(
   const RegistryPath &group, const wxString &path,
   const void *blob, size_t len, bool allowEmpty) const
{
   // Base64 encode the returned binary property list
   auto parms = wxBase64Encode(blob, len);
   if (!allowEmpty && parms.IsEmpty())
      return XO("Failed to encode preset from \"%s\"").Format(path);

   // And write it to the config
   if (!SetConfig(*this, PluginSettings::Private, group, PRESET_KEY, parms))
      return XO("Unable to store preset in config file");
   return {};
}

///////////////////////////////////////////////////////////////////////////////
//
// AudioUnitEffect
//
///////////////////////////////////////////////////////////////////////////////

AudioUnitEffectBase::AudioUnitEffectBase(const PluginPath & path,
   const wxString & name, AudioComponent component,
   Parameters *pParameters, AudioUnitEffectBase *master
)  : AudioUnitWrapper{ component, pParameters }
   , mPath{ path }
   , mName{ name.AfterFirst(wxT(':')).Trim(true).Trim(false) }
   , mVendor{ name.BeforeFirst(wxT(':')).Trim(true).Trim(false) }
{
}

AudioUnitEffectBase::~AudioUnitEffectBase() = default;

// ============================================================================
// ComponentInterface implementation
// ============================================================================

PluginPath AudioUnitEffectBase::GetPath() const
{
   return mPath;
}

ComponentInterfaceSymbol AudioUnitEffectBase::GetSymbol() const
{
   return mName;
}

VendorSymbol AudioUnitEffectBase::GetVendor() const
{
   return { mVendor };
}

wxString AudioUnitEffectBase::GetVersion() const
{
   UInt32 version;

   OSStatus result = AudioComponentGetVersion(mComponent, &version);

   return wxString::Format(wxT("%d.%d.%d"),
                           (version >> 16) & 0xffff,
                           (version >> 8) & 0xff,
                           version & 0xff);
}

TranslatableString AudioUnitEffectBase::GetDescription() const
{
   /* i18n-hint: Can mean "not available," "not applicable," "no answer" */
   return XO("n/a");
}

// ============================================================================
// EffectDefinitionInterface implementation
// ============================================================================

EffectType AudioUnitEffectBase::GetType() const
{
   if (mAudioIns == 0 && mAudioOuts == 0)
   {
      return EffectTypeNone;
   }

   if (mAudioIns == 0)
   {
      return EffectTypeGenerate;
   }

   if (mAudioOuts == 0)
   {
      return EffectTypeAnalyze;
   }

   return EffectTypeProcess;
}

EffectFamilySymbol AudioUnitEffectBase::GetFamily() const
{
   return AUDIOUNITEFFECTS_FAMILY;
}

bool AudioUnitEffectBase::IsInteractive() const
{
   return mInteractive;
}

bool AudioUnitEffectBase::IsDefault() const
{
   return false;
}

auto AudioUnitEffectBase::RealtimeSupport() const -> RealtimeSince
{
   return GetType() == EffectTypeProcess
      ? RealtimeSince::After_3_1
      : RealtimeSince::Never;
}

bool AudioUnitEffectBase::SupportsAutomation() const
{
   bool supports = false;
   ForEachParameter(
   [&supports](const ParameterInfo &pi, AudioUnitParameterID) {
      if (pi.mInfo.flags & kAudioUnitParameterFlag_IsWritable)
         supports = true;
      // Search only until we find one, that's all we need to know
      return !supports;
   });
   return supports;
}

std::shared_ptr<EffectInstance> AudioUnitEffectBase::MakeInstance() const
{
   bool useLatency;
   GetConfig(*this, PluginSettings::Shared, OptionsKey, UseLatencyKey,
      useLatency, true);

   return std::make_shared<AudioUnitInstance>(*this, mComponent, mParameters,
      GetSymbol().Internal(), mAudioIns, mAudioOuts, useLatency);
}

bool AudioUnitEffectBase::InitializePlugin()
{
   // To implement the services of EffectPlugin -- such as, a query of the
   // set of effect parameters, so that we can implement MakeSettings -- we
   // also need what is called an AudioComponentInstance, also called an
   // AudioUnit.
   // It's not just for implementing EffectInstance.  AudioUnits is unlike other
   // third party effect families that distinguish the notions of plug-in and
   // instance.

   GetChannelCounts();

   if (!CreateAudioUnit())
      return false;

   // Use an arbitrary rate while completing the discovery of channel support
   if (!SetRateAndChannels(44100.0, GetSymbol().Internal()))
      return false;

   // Determine interactivity
   mInteractive = (Count(mParameters) > 0);
   if (!mInteractive) {
      // Check for a Cocoa UI
      // This could retrieve a variable-size property, but we only look at
      // the first element.
      AudioUnitCocoaViewInfo cocoaViewInfo;
      mInteractive =
         !GetFixedSizeProperty(kAudioUnitProperty_CocoaUI, cocoaViewInfo);
      if (!mInteractive) {
         // Check for a Carbon UI
         // This could retrieve a variable sized array but we only need the
         // first
         AudioComponentDescription compDesc;
         mInteractive = !GetFixedSizeProperty(
            kAudioUnitProperty_GetUIComponentList, compDesc);
      }
   }

   return true;
}

#if 0
size_t AudioUnitInstance::GetTailSize() const
{
   // Retrieve the tail time
   Float64 tailTime = 0.0;
   if (!GetFixedSizeProperty(kAudioUnitProperty_TailTime, tailTime))
      return tailTime * mSampleRate;
   return 0;
}
#endif

// Don't use the template-generated MakeSettings(), which default-constructs
// the structure.  Instead allocate a number of values chosen by the plug-in
EffectSettings AudioUnitEffectBase::MakeSettings() const
{
   AudioUnitEffectSettings settings;
   FetchSettings(settings, true);
   return EffectSettings::Make<AudioUnitEffectSettings>(std::move(settings));
}

bool AudioUnitEffectBase::CopySettingsContents(
   const EffectSettings &, EffectSettings &) const
{
   // Not needed -- rely on EffectInstance::Message instead
   return true;
}

constexpr auto PresetStr = "_PRESET";

RegistryPath AudioUnitEffectBase::ChoosePresetKey(
   const EffectSettings &settings)
{
   // Find a key to use for the preset that does not collide with any
   // parameter name
   wxString result = PresetStr;

   // That string probably works but be sure
   const auto &map = GetSettings(settings).values;
   using Pair = decltype(*map.begin());
   while (std::any_of(map.begin(), map.end(), [&](Pair &pair){
      return pair.second && pair.second->first == result;
   }))
      result += "_";

   return result;
}

RegistryPath AudioUnitEffectBase::FindPresetKey(const CommandParameters & parms)
{
   RegistryPath result;
   auto len = strlen(PresetStr);
   if (auto [index, key] = std::tuple(0L, wxString{})
       ; parms.GetFirstEntry(key, index)
   ) do {
      if (key.StartsWith(PresetStr)
          && key.Mid(len).find_first_not_of("_") == wxString::npos
          && key.length() > result.length())
         result = key;
   } while(parms.GetNextEntry(key, index));
   return result;
}

bool AudioUnitEffectBase::SaveSettings(
   const EffectSettings &settings, CommandParameters & parms) const
{
   const auto &mySettings = GetSettings(settings);
   if (mySettings.mPresetNumber) {
      const auto key = ChoosePresetKey(settings);
      parms.Write(key, *mySettings.mPresetNumber);
   }

   // Save settings into CommandParameters
   // Iterate the map only, not using any AudioUnit handles
   for (auto &[ID, pPair] : mySettings.values)
      if (pPair)
         // Write names, not numbers, as keys in the config file
         parms.Write(pPair->first, pPair->second);
   return true;
}

bool AudioUnitEffectBase::LoadSettings(
   const CommandParameters & parms, EffectSettings &settings) const
{
   // First clean all settings, in case any are not defined in parms
   auto &mySettings = GetSettings(settings);
   mySettings.ResetValues();
   auto &map = mySettings.values;

   // Reload preset first
   if (auto presetKey = FindPresetKey(parms); !presetKey.empty()) {
      SInt32 value = 0;
      if (parms.Read(presetKey, &value))
         AudioUnitWrapper::LoadFactoryPreset(*this, value, &settings);
   }

   // Load settings from CommandParameters
   // Iterate the config only, not using any AudioUnit handles
   if (auto [index, key, value] = std::tuple(
         0L, wxString{}, AudioUnitParameterValue{})
       ; parms.GetFirstEntry(key, index)
   ) do {
      if (auto pKey = ParameterInfo::ParseKey(key)
         ; pKey && parms.Read(key, &value)
      )
         map[*pKey].emplace(mySettings.Intern(key), value);
   } while(parms.GetNextEntry(key, index));
   return true;
}

OptionalMessage AudioUnitEffectBase::LoadUserPreset(
   const RegistryPath & name, EffectSettings &settings) const
{
   // To do: externalize state so const_cast isn't needed
   return const_cast<AudioUnitEffectBase*>(this)->LoadPreset(name, settings);
}

bool AudioUnitEffectBase::SaveUserPreset(
   const RegistryPath & name, const EffectSettings &settings) const
{
   return SavePreset(name, GetSettings(settings));
}

OptionalMessage
AudioUnitEffectBase::LoadFactoryPreset(int id, EffectSettings &settings) const
{
   if (AudioUnitWrapper::LoadFactoryPreset(*this, id, &settings))
      return { nullptr };
   return {};
}

RegistryPaths AudioUnitEffectBase::GetFactoryPresets() const
{
   RegistryPaths presets;

   // Retrieve the list of factory presets
   CF_ptr<CFArrayRef> array;
   if (!GetFixedSizeProperty(kAudioUnitProperty_FactoryPresets, array))
      for (CFIndex i = 0, cnt = CFArrayGetCount(array.get()); i < cnt; ++i)
         presets.push_back(wxCFStringRef::AsString(
            static_cast<const AUPreset*>(CFArrayGetValueAtIndex(array.get(), i))
               ->presetName));
   return presets;
}

bool AudioUnitEffectBase::CanExportPresets() const
{
   return true;
}

bool AudioUnitEffectBase::HasOptions() const
{
   return true;
}

// ============================================================================
// AudioUnitEffect Implementation
// ============================================================================

bool AudioUnitEffectBase::MigrateOldConfigFile(
   const RegistryPath & group, EffectSettings &settings) const
{
   // Migration of very old format configuration file, should not normally
   // happen and perhaps this code can be abandoned
   // Attempt to load old preset parameters and resave using new method
   constexpr auto oldKey = L"Parameters";
   wxString parms;
   if (GetConfig(*this, PluginSettings::Private,
      group, oldKey, parms, wxEmptyString)) {
      CommandParameters eap;
      if (eap.SetParameters(parms))
         if (LoadSettings(eap, settings))
            if (SavePreset(group, GetSettings(settings)))
               RemoveConfig(*this, PluginSettings::Private, group, oldKey);
      return true;
   }
   return false;
}

OptionalMessage AudioUnitEffectBase::LoadPreset(
   const RegistryPath & group, EffectSettings &settings) const
{
   if (MigrateOldConfigFile(group, settings))
      return { nullptr };

   if (AudioUnitWrapper::LoadPreset(*this, group, settings))
      return { nullptr };
   return {};
}

bool AudioUnitEffectBase::SavePreset(
   const RegistryPath & group, const AudioUnitEffectSettings &settings) const
{
   wxCFStringRef cfname(wxFileNameFromPath(group));
   const auto &[data, _] = MakeBlob(*this, settings, cfname, true);
   if (!data)
      return false;

   // Nothing to do if we don't have any data
   if (const auto length = CFDataGetLength(data.get())) {
      auto error =
         SaveBlobToConfig(group, {}, CFDataGetBytePtr(data.get()), length);
      if (!error.empty())
         return false;
   }
   return true;
}

TranslatableString AudioUnitEffectBase::Export(
   const AudioUnitEffectSettings &settings, const wxString & path) const
{
   // Create the file
   wxFFile f(path, wxT("wb"));
   if (!f.IsOpened())
      return XO("Couldn't open \"%s\"").Format(path);

   // First set the name of the preset
   wxCFStringRef cfname(wxFileName(path).GetName());

   const auto &[data, message] = MakeBlob(*this, settings, cfname, false);
   if (!data || !message.empty())
      return message;

   // Write XML data
   auto length = CFDataGetLength(data.get());
   if (f.Write(CFDataGetBytePtr(data.get()), length) != length || f.Error())
      return XO("Failed to write XML preset to \"%s\"").Format(path);

   f.Close();
   return {};
}

TranslatableString AudioUnitEffectBase::Import(
   AudioUnitEffectSettings &settings, const wxString & path) const
{
   // Open the preset
   wxFFile f(path, wxT("r"));
   if (!f.IsOpened())
      return XO("Couldn't open \"%s\"").Format(path);

   // Load it into the buffer
   size_t len = f.Length();
   wxMemoryBuffer buf(len);
   if (f.Read(buf.GetData(), len) != len || f.Error())
      return XO("Unable to read the preset from \"%s\"").Format(path);
   buf.SetDataLen(len);

   const auto error = InterpretBlob(settings, path, buf);
   if (!error.empty())
      return error;

   return {};
}

void AudioUnitEffectBase::GetChannelCounts()
{
   // Does AU have channel info
   PackedArray::Ptr<AUChannelInfo> info;
   if (GetVariableSizeProperty(kAudioUnitProperty_SupportedNumChannels, info)) {
      // None supplied.  Apparently all FX type units can do any number of INs
      // and OUTs as long as they are the same number.  In this case, we'll
      // just say stereo.
      //
      // We should probably check to make sure we're dealing with an FX type.
      mAudioIns = 2;
      mAudioOuts = 2;
      return;
   }

   // This is where it gets weird...not sure what is the best
   // way to do this really.  If we knew how many ins/outs we
   // really needed, we could make a better choice.

   bool haven2m = false;   // nothing -> mono
   bool haven2s = false;   // nothing -> stereo
   bool havem2n = false;   // mono -> nothing
   bool haves2n = false;   // stereo -> nothing
   bool havem2m = false;   // mono -> mono
   bool haves2s = false;   // stereo -> stereo
   bool havem2s = false;   // mono -> stereo
   bool haves2m = false;   // stereo -> mono

   mAudioIns = 2;
   mAudioOuts = 2;

   // Look only for exact channel constraints
   for (auto &ci : info) {
      int ic = ci.inChannels;
      int oc = ci.outChannels;

      if (ic < 0 && oc >= 0)
         ic = 2;
      else if (ic >= 0 && oc < 0)
         oc = 2;
      else if (ic < 0 && oc < 0) {
         ic = 2;
         oc = 2;
      }

      if (ic == 2 && oc == 2)
         haves2s = true;
      else if (ic == 1 && oc == 1)
         havem2m = true;
      else if (ic == 1 && oc == 2)
         havem2s = true;
      else if (ic == 2 && oc == 1)
         haves2m = true;
      else if (ic == 0 && oc == 2)
         haven2s = true;
      else if (ic == 0 && oc == 1)
         haven2m = true;
      else if (ic == 1 && oc == 0)
         havem2n = true;
      else if (ic == 2 && oc == 0)
         haves2n = true;
   }

   if (haves2s) {
      mAudioIns = 2;
      mAudioOuts = 2;
   }
   else if (havem2m) {
      mAudioIns = 1;
      mAudioOuts = 1;
   }
   else if (havem2s) {
      mAudioIns = 1;
      mAudioOuts = 2;
   }
   else if (haves2m) {
      mAudioIns = 2;
      mAudioOuts = 1;
   }
   else if (haven2m) {
      mAudioIns = 0;
      mAudioOuts = 1;
   }
   else if (haven2s) {
      mAudioIns = 0;
      mAudioOuts = 2;
   }
   else if (haves2n) {
      mAudioIns = 2;
      mAudioOuts = 0;
   }
   else if (havem2n) {
      mAudioIns = 1;
      mAudioOuts = 0;
   }

   return;
}
#endif
