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

  Audacity: A Digital Audio Editor

  AUControl.mm

  Leland Lucius

  Several ideas and code snippets taken from HairerSoft's HSAUView class:

      http://www.hairersoft.com/Downloads/HSAUView.zip

      Created by Martin on 02/06/2007.
      Copyright 2010 by HairerSoft.
      
      You are most welcome to use this code in your own (open source or not)
      project. Use at your own risk of course, etc. Please acknowledge at an
      appropriate location (manual or about box for example).
      
      Bug reports most welcome: Martin@HairerSoft.com
      
**********************************************************************/

#include "../../Audacity.h"
#include "AUControl.h"
#include <AudioUnit/AudioUnitProperties.h>
#include <AudioUnit/AUCocoaUIView.h>
#include <CoreAudioKit/CoreAudioKit.h>

#if !defined(_LP64)
#include <AudioUnit/AudioUnitCarbonView.h>
#endif

#include "../../MemoryX.h"

@interface AUView : NSView
{
@public
   AUControl *mControl;
   NSView *mView;
   bool mRedraw;
}
@end

@implementation AUView
 
+ (void)initialize
{
   static BOOL initialized = NO;
   if (!initialized)
   {
      initialized = YES;
      wxOSXCocoaClassAddWXMethods(self);
   }
}

- (instancetype)initWithControl:(AUControl *)control;
{
   // Make sure a parameters were provided
   NSParameterAssert(control);

   mControl = control;
   mRedraw = NO;

   [super init];

   return self;
}

- (BOOL)autoresizesSubviews
{
   return NO;
}

- (void)viewWillDraw
{
   // LLL:  Hackage alert!  I have absolutely no idea why the AudioUnitView doesn't
   //       get redrawn after the ClassInfo is updated, but this bit of malarkey
   //       gets around this issue.
   //
   //       To see the problem, comment out the setFrameSize calls below, create/save
   //       a preset for the AUDelay effect with "Invert Feedback" checked, restore
   //       factory defaults and then reselect the save preset.  The display will not
   //       update properly.  But, resize the window and it does.
   //
   //       Again, this is total hackage and I hope to find the real cause soon.
   if (mRedraw)
   {
      NSRect viewRect = [mView frame];
      NSRect bogusRect = {};
      [mView setFrameSize:bogusRect.size];
      [mView setFrameSize:viewRect.size];
      mRedraw = NO;
   }

   [super viewWillDraw];
}

- (void)cocoaViewResized:(NSNotification *)notification
{
   mControl->CocoaViewResized();
}

@end

AUControlImpl::AUControlImpl(wxWindowMac *peer, NSView *view)
:  wxWidgetCocoaImpl(peer, view, false, false)
{
}

AUControlImpl::~AUControlImpl()
{
}

BEGIN_EVENT_TABLE(AUControl, wxControl)
   EVT_SIZE(AUControl::OnSize)
END_EVENT_TABLE()

AUControl::AUControl()
{
   mComponent = NULL;
   mUnit = NULL;

   mAUView = nil;
   mView = nil;

   mSettingSize = false;

#if !defined(_LP64)
   mHIView = NULL;
   mWindowRef = NULL;
#endif
}

AUControl::~AUControl()
{
   Close();
}

void AUControl::Close()
{
#if !defined(_LP64)

   if (mInstance)
   {
      AudioComponentInstanceDispose(mInstance);
      mInstance = nullptr;
   }

#endif

   if (mView)
   {
      NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
      [center removeObserver:mAUView
                        name:NSViewFrameDidChangeNotification
                      object:mView];

      [mView release];
      mView = nullptr;
   }

   if (mAUView)
   {
      [mAUView release];
      mAUView = nullptr;
   }
}

bool AUControl::Create(wxWindow *parent, AudioComponent comp, AudioUnit unit, bool custom)
{
   mComponent = comp;
   mUnit = unit;

   DontCreatePeer();

   if (!wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, wxEmptyString))
   {
      return false;
   }

   mAUView = [AUView alloc];
   if (!mAUView)
   {
      return false;
   }
   [(AUView *)mAUView initWithControl:this];
   [mAUView retain];

   if (custom)
   {
      CreateCocoa();

#if !defined(_LP64)
      if (!mView)
      {
         CreateCarbon();
      }
#endif
   }

   if (!mView
#if !defined(_LP64)
       && !mHIView
#endif
       )
   {
      CreateGeneric();
   }

   if (!mView
#if !defined(_LP64)
       && !mHIView
#endif
       )
   {
      return false;
   }

   ((AUView *)mAUView)->mView = mView;

   // wxWidgets takes ownership so safenew
   SetPeer(safenew AUControlImpl(this, mAUView));

#if !defined(_LP64)
   if (mHIView)
   {
      CreateCarbonOverlay();
   }
#endif

   // Must get the size again since SetPeer() could cause it to change
   SetInitialSize(GetMinSize());

   MacPostControlCreate(wxDefaultPosition, wxDefaultSize);

   return true;
}

void AUControl::OnSize(wxSizeEvent & evt)
{
   evt.Skip();

   if (mSettingSize)
   {
      return;
   }
   auto vr = valueRestorer( mSettingSize, true );

   wxSize sz = GetSize();

   if (mView)
   {
      int mask = [mView autoresizingMask];

      NSRect viewFrame = [mAUView frame];
      NSRect viewRect = [mView frame];

      if (mask & (NSViewWidthSizable | NSViewHeightSizable))
      {
         if (mask & NSViewWidthSizable)
         {
            viewRect.size.width = sz.GetWidth();
         }

         if (mask & NSViewHeightSizable)
         {
            viewRect.size.height = sz.GetHeight();
         }

         viewRect.origin.x = 0;
         viewRect.origin.y = 0;

         [mView setFrame:viewRect];
      }
      else
      {
         viewRect.origin.x = abs((viewFrame.size.width - viewRect.size.width) / 2);
         viewRect.origin.y = abs((viewFrame.size.height - viewRect.size.height) / 2);

         if (viewRect.origin.x || viewRect.origin.y)
         {
            [mAUView setFrame:viewRect];
         }
      }
   }

#if !defined(_LP64)
   else if (mHIView)
   {
      HIRect rect;
      HIViewGetFrame(mHIView, &rect);

      CGFloat x = (sz.x - rect.size.width) / 2;
      CGFloat y = (sz.y - rect.size.height) / 2;
      
      SizeWindow(mWindowRef, sz.x, sz.y, true);
      HIViewPlaceInSuperviewAt(mHIView, x, y);

      wxWindow *w = wxGetTopLevelParent(this);

      wxSize min = w->GetMinSize();
      min.x += (rect.size.width - mLastMin.GetWidth());
      min.y += (rect.size.height - mLastMin.GetHeight());

      w->SetSizeHints(min, min);

      mLastMin = wxSize(rect.size.width, rect.size.height);
   }
#endif

   return;
}

void AUControl::CreateCocoa()
{
   OSStatus result;
   UInt32 dataSize;

   result = AudioUnitGetPropertyInfo(mUnit,
                                     kAudioUnitProperty_CocoaUI,
                                     kAudioUnitScope_Global, 
                                     0,
                                     &dataSize,
                                     NULL);
   if (result != noErr)
   {
      return;
   }

   int cnt = (dataSize - sizeof(CFURLRef)) / sizeof(CFStringRef);
   if (cnt == 0)
   {
      return;
   }

   ArrayOf<char> buffer{ dataSize };
   auto viewInfo = (AudioUnitCocoaViewInfo *) buffer.get();
   if (viewInfo == NULL)
   {
      return;
   }

   // Get the view info
   result = AudioUnitGetProperty(mUnit,
                                 kAudioUnitProperty_CocoaUI,
                                 kAudioUnitScope_Global,
                                 0,
                                 viewInfo,
                                 &dataSize);
   if (result == noErr)
   {
      // Looks like the AU has a Cocoa UI, so load the factory class
      NSURL *bundleLoc = (NSURL *) viewInfo->mCocoaAUViewBundleLocation;
      NSString *viewClass = (NSString *) viewInfo->mCocoaAUViewClass[0];

      if (bundleLoc != nil && viewClass != nil)
      {
         // Load the bundle
         NSBundle *bundle = [NSBundle bundleWithPath:[bundleLoc path]];
         if (bundle != nil)
         {
            // Load the class from the bundle
            Class factoryClass = [bundle classNamed:viewClass];
            if (factoryClass != nil)
            {      
               // Create an instance of the class
               id factoryInst = [[[factoryClass alloc] init] autorelease];
               if (factoryInst != nil)
               {
                  // Suggest a reasonable size
                  NSSize size = {800, 600};
      
                  // Create the view
                  mView = [factoryInst uiViewForAudioUnit:mUnit withSize:size];
               }
            }
         }
      }

      if (viewInfo->mCocoaAUViewBundleLocation != nil)
      {
         CFRelease(viewInfo->mCocoaAUViewBundleLocation);
      }

      for (int i = 0; i < cnt; i++)
      {
         CFRelease(viewInfo->mCocoaAUViewClass[i]);
      }
   }

   if (!mView)
   {
      return;
   }

   [mView retain];

   NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
   [center addObserver:mAUView
              selector:@selector(cocoaViewResized:)
                  name:NSViewFrameDidChangeNotification
                object:mView];

   [mAUView addSubview:mView];

   NSSize viewSize = [mView frame].size;

   mLastMin = wxSize(viewSize.width, viewSize.height);

   SetMinSize(mLastMin);

   [mAUView setAutoresizingMask:[mView autoresizingMask]];

   return;
}

void AUControl::CreateGeneric()
{
   OSStatus result;
   AudioComponentDescription desc;

   result = AudioComponentGetDescription(mComponent, &desc);
   if (result == noErr && desc.componentType == kAudioUnitType_Panner)
   {
      mView = [AUPannerView AUPannerViewWithAudioUnit:mUnit];
      if (mView == nil)
      {
         return;
      }
   }
   else
   {
      // Create a generic AU view
      AUGenericView *view = [AUGenericView alloc];
      if (view == nil)
      {
         return;
      }

      int flags = AUViewPropertiesDisplayFlag |
                  AUViewParametersDisplayFlag;
   
      [view initWithAudioUnit:mUnit displayFlags:flags];
   
      [view setShowsExpertParameters:YES];  

      mView = view;
   }

   [mView retain];

   NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
   [center addObserver:mAUView
              selector:@selector(cocoaViewResized:)
                  name:NSViewFrameDidChangeNotification
                object:mView];

   [mAUView addSubview:mView];

   NSSize viewSize = [mView frame].size;

   mLastMin = wxSize(viewSize.width, viewSize.height);

   SetMinSize(mLastMin);

   [mAUView setAutoresizingMask:[mView autoresizingMask]];

   return;
}

void AUControl::CocoaViewResized()
{
   if (mSettingSize)
   {
      return;
   }
   auto vr = valueRestorer( mSettingSize, true );

   NSSize viewSize = [mView frame].size;
   NSSize frameSize = [mAUView frame].size;

   [mAUView setFrameSize:viewSize];

   SetMinSize(wxSize(viewSize.width, viewSize.height));;

   int diffW = (viewSize.width - frameSize.width);
   int diffH = (viewSize.height - frameSize.height);

   wxWindow *w = wxGetTopLevelParent(this);

   wxSize min = w->GetMinSize();
   if ([mView autoresizingMask] & (NSViewWidthSizable | NSViewHeightSizable))
   {
      min.x += diffW;
      min.y += diffH;
   }
   else
   {
      min.x += (viewSize.width - mLastMin.GetWidth());
      min.y += (viewSize.height - mLastMin.GetHeight());
      mLastMin = wxSize(viewSize.width, viewSize.height);;
   }
   w->SetMinSize(min);

   // Resize the dialog as well
   w->Fit();

   // Send a "dummy" event to have the OnSize() method recalc mView position
   GetEventHandler()->AddPendingEvent(wxSizeEvent(GetSize()));
}

void AUControl::ForceRedraw()
{
   ((AUView *)mAUView)->mRedraw = YES;
}

#if !defined(_LP64)

void AUControl::CreateCarbon()
{
   OSStatus result;
   UInt32 dataSize;

   result = AudioUnitGetPropertyInfo(mUnit,
                                     kAudioUnitProperty_GetUIComponentList,
                                     kAudioUnitScope_Global, 
                                     0,
                                     &dataSize,
                                     NULL);
   if (result != noErr)
   {
      return;
   }

   int cnt = (dataSize - sizeof(CFURLRef)) / sizeof(CFStringRef);
   if (cnt == 0)
   {
      return;
   }

   ArrayOf<char> buffer{ dataSize };
   auto compList = (AudioComponentDescription *) buffer.get();
   if (compList == NULL)
   {
      return;
   }

   // Get the view info
   result = AudioUnitGetProperty(mUnit,
                                 kAudioUnitProperty_GetUIComponentList,
                                 kAudioUnitScope_Global,
                                 0,
                                 compList,
                                 &dataSize);
   if (result != noErr)
      return;

   // Get the component
   AudioComponent comp = AudioComponentFindNext(NULL, &compList[0]);

   // Try to create an instance
   result = AudioComponentInstanceNew(comp, &mInstance);

   if (result != noErr)
   {
      return;
   }

   Rect bounds = { 100, 100, 200, 200 };

   result = CreateNewWindow(kOverlayWindowClass,
                            kWindowStandardHandlerAttribute |
                            kWindowCompositingAttribute |
                            kWindowOpaqueForEventsAttribute,
                            &bounds,
                            &mWindowRef);
   if (result != noErr)
   {
      AudioComponentInstanceDispose(mInstance);
      mInstance = NULL;

      return;
   }

   // Get the root control
   ControlRef root = HIViewGetRoot(mWindowRef);

   // Find the content view within our window
   HIViewRef content;
   result = HIViewFindByID(root, kHIViewWindowContentID, &content);
   if (result != noErr)
   {
      DisposeWindow(mWindowRef);
      mWindowRef = NULL;

      AudioComponentInstanceDispose(mInstance);
      mInstance = NULL;

      return;
   }

   SetWindowActivationScope(mWindowRef, kWindowActivationScopeIndependent);

   // Suggest a reasonable size
   Float32Point loc = {0.0, 0.0};
   Float32Point size = {800.0, 600.0};

   // And create it
   result = AudioUnitCarbonViewCreate(mInstance,
                                      mUnit,
                                      mWindowRef,
                                      root,
                                      &loc,
                                      &size,
                                      &mHIView);
   if (result != noErr)
   {
      DisposeWindow(mWindowRef);
      mWindowRef = NULL;

      AudioComponentInstanceDispose(mInstance);
      mInstance = NULL;

      return;
   }

   HIViewAddSubview(root, mHIView);
   HIViewPlaceInSuperviewAt(mHIView, 0, 0);
   HIViewSetVisible(mHIView, TRUE);

   HIRect rect;
   HIViewGetFrame(mHIView, &rect);
   SetMinSize(wxSize(rect.size.width, rect.size.height));

   mLastMin = GetMinSize();

   EventTypeSpec controlEventList[] =
   {
      {kEventClassControl, kEventControlBoundsChanged},
   };

   InstallControlEventHandler(mHIView,
                              ControlEventHandlerCallback,
                              GetEventTypeCount(controlEventList),
                              controlEventList,
                              (void *) this,
                              NULL);

   return;
}

void AUControl::CreateCarbonOverlay()
{
   NSWindow *parent = [mAUView window];
   WindowRef parentRef = (WindowRef)[parent windowRef];

   NSWindow *host = [[[NSWindow alloc] initWithWindowRef:mWindowRef] autorelease];
   [parent addChildWindow:host ordered:NSWindowAbove];

   WindowGroupRef group;

   CreateWindowGroup(0, &group);
   SetWindowGroupParent(group, GetWindowGroup(parentRef));
   ChangeWindowGroupAttributes(group,
                               kWindowGroupAttrLayerTogether |
                               kWindowGroupAttrSharedActivation |
                               kWindowGroupAttrHideOnCollapse,
                               0);
   SetWindowGroup(parentRef, group);
   SetWindowGroup(mWindowRef, group);

   Rect location;
   GetWindowBounds(parentRef, kWindowContentRgn, &location);
   MoveWindow(mWindowRef, location.left, location.top, true);
   ShowWindow(mWindowRef);
}

pascal OSStatus
AUControl::ControlEventHandlerCallback(EventHandlerCallRef handler, EventRef event, void *data)
{
   ((AUControl *) data)->CarbonViewResized();

   return eventNotHandledErr;
}

void AUControl::CarbonViewResized()
{
   if (mSettingSize)
   {
      return;
   }
   auto vr = valueRestorer( mSettingSize, true );

   // resize and move window
   HIRect rect;
   HIViewGetFrame(mHIView, &rect);

   HIViewPlaceInSuperviewAt(mHIView, 0, 0);
   SizeWindow(mWindowRef, rect.size.width, rect.size.height, true);

   [mAUView setFrameSize:NSMakeSize(rect.size.width, rect.size.height)];

   NSSize frameSize = [mAUView frame].size;

   wxWindow *w = wxGetTopLevelParent(this);
   wxSize size = w->GetSize();
   size.x += (rect.size.width - frameSize.width);
   size.y += (rect.size.height - frameSize.height);

   // Reset the current max/min
   w->SetSizeHints(wxDefaultSize, wxDefaultSize);

   // Set the dialog size
   w->SetSize(size);

   // And finally set the NEW max/min
   w->SetSizeHints(size, size);

   mLastMin = wxSize(rect.size.width, rect.size.height);
}

#endif
