////////////////////////////////////////////////////////
//
// GEM - Graphics Environment for Multimedia
//
// Implementation file
//
// SPDX-FileCopyrightText: © 2002, James Tittle II and the GEM contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
////////////////////////////////////////////////////////
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#if defined __APPLE__  && defined __x86_64
#warning unconditionally disabling QuickTime/Carbon on OSX/64bit
// with OSX10.6, apple has removed loads of Carbon functionality (in 64bit mode)
// LATER make this a real check in configure
# undef HAVE_QUICKTIME
# undef HAVE_CARBON
#endif

#if defined HAVE_CARBON && defined HAVE_QUICKTIME
# define HAVE_VIDEODARWIN
#endif

#ifdef HAVE_VIDEODARWIN
#include "videoDarwin.h"
#include "Gem/RTE.h"
#include "plugins/PluginFactory.h"

using namespace gem::plugins;

#include <unistd.h> // needed for Unix file open() type functions
#include <stdio.h>
#include <fcntl.h> /* JMZ thinks that _this_ is needed for open() */

#define DEFAULT_WIDTH        320
#define DEFAULT_HEIGHT        240

static std::string pascal2str(const Str255 pstr)
{
  const unsigned char*cstr=static_cast<const unsigned char*>(pstr);
  const char*str=(const char*)(cstr+1);
  const size_t length=cstr[0];
  return std::string(str, length);
}



REGISTER_VIDEOFACTORY("Darwin", videoDarwin);

videoDarwin :: videoDarwin()
  : videoBase("darwin", 0)
  , m_newFrame(false)
  , m_srcGWorld(NULL)
  , m_rowBytes(0)
  , m_quality(channelPlayNormal)
  , m_colorspace(GEM_YUV)
  , m_inputDevice(0)   //set to the first input device
  , m_inputDeviceChannel(0)
{
  m_width= DEFAULT_WIDTH;
  m_height=DEFAULT_HEIGHT;

  m_img.xsize = 800;
  m_img.ysize = 600;
  m_img.setFormat(GEM_RGBA);
  m_img.allocate();

  //initSeqGrabber();
  provide("dv");
  provide("iidc");
  provide("analog");
}

////////////////////////////////////////////////////////
// Destructor
//
/////////////////////////////////////////////////////////
videoDarwin :: ~videoDarwin()
{
  close();
  if (m_vc) {
    if (::SGDisposeChannel(m_sg, m_vc)) {
      logpost(0, 3+0, "[GEM:videoDarwin] Unable to dispose a video channel");
    }
    m_vc = NULL;
  }
  if (m_sg) {
    if (::CloseComponent(m_sg)) {
      logpost(0, 3+0,
              "[GEM:videoDarwin] Unable to dispose a sequence grabber component");
    }
    m_sg = NULL;
    if (m_srcGWorld) {
      ::DisposeGWorld(m_srcGWorld);
      m_srcGWorld = NULL;
    }
  }
}
bool videoDarwin :: openDevice(gem::Properties&props)
{
  applyProperties(props);
  bool success=initSeqGrabber();
  if(NULL==m_sg) {
    success=false;
  }
  if(success) {
    applyProperties(props);
  } else {
    destroySeqGrabber();
  }

  return success;
}
void videoDarwin :: closeDevice(void)
{
  destroySeqGrabber();
}

////////////////////////////////////////////////////////
// render
//
/////////////////////////////////////////////////////////
bool videoDarwin :: grabFrame()
{
  OSErr    err;

  err = SGIdle(m_sg);

  if (err != noErr) {
    pd_error(0, "[GEM:videoDarwin] SGIdle failed with error#%d",err);
    m_haveVideo = 0;
    //    return false;
  } else {
    //this doesn't do anything so far
    //VDCompressDone(m_vdig,frameCount,data,size,similar,time);
    //err = SGGrabFrameComplete(m_vc,frameCount,done);
    //if (err != noErr) pd_error(0, "[GEM:videoDarwin] SGGrabCompressComplete failed with error %d",err);

    m_haveVideo = 1;
    m_newFrame = true;
  }
  if (!m_haveVideo) {
    logpost(0, 3+0, "[GEM:videoDarwin] no video yet");
    return true;
  }
  m_img.copy2Image(&m_image.image);
  m_image.newimage = m_newFrame;
  m_newFrame = false;

  return true;
}

////////////////////////////////////////////////////////
// startTransfer
//
/////////////////////////////////////////////////////////
bool videoDarwin :: startTransfer()
{
  OSErr    err = noErr;

  SGStartPreview(m_sg);
  m_haveVideo = true;
  m_image.newimage = true;
  return true;
}

////////////////////////////////////////////////////////
// stopTransfer
//
/////////////////////////////////////////////////////////
bool videoDarwin :: stopTransfer()
{
  OSErr    err = noErr;

  //might need SGPause or SGStop here
  err = SGStop(m_sg);
  if (err != noErr) {
    pd_error(0, "[GEM:videoDarwin] SGStop failed with error#%d",err);
  }
  return true;
}

bool videoDarwin :: initSeqGrabber()
{
  OSErr anErr;
  Rect srcRect = {0,0, m_height, m_width};
  SGDeviceList devices;
  short        deviceCount = 0;

  m_sg = OpenDefaultComponent(SeqGrabComponentType, 0);
  if(m_sg==NULL) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not open default component");
    return false;
  }
  anErr = SGInitialize(m_sg);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not initialize SG error %d",anErr);
    return false;
  }

  anErr = SGSetDataRef(m_sg, 0, 0, seqGrabDontMakeMovie);
  if (anErr != noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] dataref failed with error %d",anErr);
  }

  anErr = SGNewChannel(m_sg, VideoMediaType, &m_vc);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not make new SG channel error %d",
            anErr);
    return false;
  }

  enumerate();
  anErr = SGGetChannelDeviceList(m_vc, sgDeviceListIncludeInputs, &devices);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not get SG channel Device List");
  } else {
    deviceCount = (*devices)->count;
    m_inputDevice = (*devices)->selectedIndex;
  }

  /* device selection */
  if(m_devicenum>=0) {
    m_inputDevice=m_devicenum;
  } else if (!m_devicename.empty()) {
    const int maxcount=(deviceCount<m_devices.size()?deviceCount:
                        m_devices.size());
    for(int i=0; i<maxcount; i++) {
      if(m_devicename==m_devices[i]) {
        m_inputDevice=i;
        break;
      }
    }
  }

  //this call sets the input device
  if (m_inputDevice >= 0
      && m_inputDevice < deviceCount) {//check that the device is not out of bounds
    std::string devname=pascal2str((*devices)->entry[m_inputDevice].name);
    logpost(0, 3+1, "[GEM:videoDarwin] SGSetChannelDevice trying[%d] %s",
            m_inputDevice, devname.c_str());
  }
  anErr = SGSetChannelDevice(m_vc, (*devices)->entry[m_inputDevice].name);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] SGSetChannelDevice returned error %d",anErr);
  }

  anErr = SGSetChannelDeviceInput(m_vc,m_inputDeviceChannel);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] SGSetChannelDeviceInput returned error %d",
            anErr);
  }

  //grab the VDIG info from the SGChannel
  m_vdig = SGGetVideoDigitizerComponent(m_vc);
  VideoDigitizerError vdigErr = VDGetDigitizerInfo(m_vdig,
                                &m_vdigInfo); //not sure if this is useful

  Str255    vdigName;
  memset(vdigName,0,255);
  vdigErr = VDGetInputName(m_vdig,m_inputDevice,vdigName);
  logpost(0, 3+1, "[GEM:videoDarwin] vdigName is %s",
          pascal2str(vdigName).c_str());

  Rect vdRect;
  vdigErr = VDGetDigitizerRect(m_vdig,&vdRect);
  logpost(0, 3+1,
          "[GEM:videoDarwin] digitizer rect is top %d bottom %d left %d right %d",
          vdRect.top,vdRect.bottom,vdRect.left,vdRect.right);

  vdigErr = VDGetActiveSrcRect(m_vdig,0,&vdRect);
  logpost(0, 3+1,
          "[GEM:videoDarwin] active src rect is top %d bottom %d left %d right %d",
          vdRect.top,vdRect.bottom,vdRect.left,vdRect.right);

  anErr = SGSetChannelBounds(m_vc, &srcRect);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not set SG ChannelBounds ");
  }

  anErr = SGSetVideoRect(m_vc, &srcRect);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not set SG Rect ");
  }

  anErr = SGSetChannelUsage(m_vc, seqGrabPreview);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not set SG ChannelUsage ");
  }
  SGSetChannelPlayFlags(m_vc, m_quality);
  OSType pixelFormat=0;
  m_img.xsize = m_width;
  m_img.ysize = m_height;

  if (m_colorspace==GEM_RGBA) {
    m_img.setFormat(m_colorspace);
    m_rowBytes = m_width*m_img.csize;
    pixelFormat=k32ARGBPixelFormat;
    logpost(0, 3+1, "[GEM:videoDarwin] using RGB");
  } else {
    m_img.setFormat(GEM_YUV);
    m_rowBytes = m_width*2;
    pixelFormat=k422YpCbCr8PixelFormat;
    logpost(0, 3+1, "[GEM:videoDarwin] using YUV");
  }
  m_img.reallocate();
  anErr = QTNewGWorldFromPtr (&m_srcGWorld,
                              pixelFormat,
                              &srcRect,
                              NULL,
                              NULL,
                              0,
                              m_img.data,
                              m_rowBytes);

  if (anErr!= noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] %d error at QTNewGWorldFromPtr", anErr);
    return false;
  }
  if (NULL == m_srcGWorld) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not allocate off screen");
    return false;
  }
  SGSetGWorld(m_sg,(CGrafPtr)m_srcGWorld, NULL);
  SGStartPreview(m_sg); //moved to starttransfer?
  m_haveVideo = true;
  return true;
}

void videoDarwin :: setupCapture()
{
  if(stop()) {
    SGSetChannelUsage(m_vc, 0);
    SGSetChannelUsage(m_vc, seqGrabPreview);
    SGUpdate(m_sg,0);
    start();
  }
}

void videoDarwin :: destroySeqGrabber()
{
  if (m_vc) {
    if (::SGDisposeChannel(m_sg, m_vc)) {
      logpost(0, 3+0, "[GEM:videoDarwin] Unable to dispose a video channel");
    }
    m_vc = NULL;
  }
  if (m_sg) {
    if (::CloseComponent(m_sg)) {
      logpost(0, 3+0,
              "[GEM:videoDarwin] Unable to dispose a sequence grabber component");
    }
    m_sg = NULL;
    if (m_srcGWorld) {
      ::DisposeGWorld(m_srcGWorld);
      m_srcGWorld = NULL;
    }
  }
}

void videoDarwin :: resetSeqGrabber()
{
  OSErr anErr;
  logpost(0, 3+1, "[GEM:videoDarwin] starting reset");

  destroySeqGrabber();
  initSeqGrabber();
}

/////////////////////////////////////////////////////////
// colorspaceMess
//
/////////////////////////////////////////////////////////
bool videoDarwin :: setColor(int format)
{
  m_colorspace = format;
  if(stop()) {
    resetSeqGrabber();
    start();
  }
}


/////////////////////////////////////////////////////////
// dialog
//
/////////////////////////////////////////////////////////
bool videoDarwin :: dialog(std::vector<std::string>dlg)
{
  Rect    newActiveVideoRect;
  Rect    curBounds, curVideoRect, newVideoRect;
  ComponentResult    err;

  // Get our current state - do i need this???
  err = SGGetChannelBounds (m_vc, &curBounds);
  err = SGGetVideoRect (m_vc, &curVideoRect);

  // Pause
  err = SGPause (m_sg, true);

  // Do the dialog thang
  err = SGSettingsDialog( m_sg, m_vc, 0, nil, 0, nil, 0);

  // What happened?
  err = SGGetVideoRect (m_vc, &newVideoRect);
  err = SGGetSrcVideoBounds (m_vc, &newActiveVideoRect);

  err = SGPause (m_sg, false);

  return true;
}

std::vector<std::string>videoDarwin :: dialogs(void)
{
  std::vector<std::string>result;
  return result;
}

bool videoDarwin::enumProperties(gem::Properties&readable,
                                 gem::Properties&writeable)
{
  bool iidc=false;
  gem::any typ;

  readable.clear();
  writeable.clear();

  typ=1.;
  if(m_vdig) {
    ComponentDescription    desc;
    GetComponentInfo((Component)m_vdig, &desc, NULL, NULL, NULL);

    iidc=vdSubtypeIIDC == desc.componentSubType;
  }
#define SETPROP(key, value) typ=value; readable.set(key, typ); writeable.set(key, typ)
#define SETRPROP(key, value) typ=value; readable.set(key, typ)
#define SETWPROP(key, value) typ=value; writeable.set(key, typ)
  SETPROP("Hue", 1);
  SETPROP("Sharpness", 1);
  SETPROP("Saturation", 1);
  SETPROP("Brightness", 1);
  if(!iidc) {
    SETPROP("Contrast", 1);
    SETPROP("KeyColor", 1);
    //SETPROP("ClipState", 1);
    //SETPROP("ClipRng", 1);
    //SETPROP("PLLFilterType", 1);
    SETWPROP("MasterBlendLevel", 1);
    //SETPROP("PlayThroughOnOff", 1);
    //SETPROP("FieldPreference", 1);
    SETPROP("BlackLevelValue", 1);
    SETPROP("WhiteLevelValue", 1);
    //SETPROP("Input", 1);
    //SETPROP("InputStandard", 1);
  } else {
    SETWPROP("Gain", 1);
    SETWPROP("Iris", 1);
    SETWPROP("Shutter", 1);
    SETWPROP("Exposure", 1);
    SETWPROP("WhiteBalanceU", 1);
    SETWPROP("WhiteBalanceV", 1);
    SETWPROP("Gamma", 1);
    SETWPROP("Temperature", 1);
    SETWPROP("Zoom", 1);
    SETWPROP("Focus", 1);
    SETWPROP("Pan", 1);
    SETWPROP("Tilt", 1);
    SETWPROP("OpticalFilter", 1);
    SETWPROP("EdgeEnhancement", 1);
  }
  return true;
}

bool videoDarwin::setIIDCProperty(OSType specifier, double value)
{
  QTAtomContainer         atomContainer;
  QTAtom                  featureAtom;
  VDIIDCFeatureSettings   settings;
  ComponentDescription    desc;
  ComponentResult         result = paramErr;

  //IIDC stuff
  result = VDIIDCGetFeaturesForSpecifier(m_vdig, specifier, &atomContainer);
  if (noErr != result) {
    return false;
  }

  featureAtom = QTFindChildByIndex(atomContainer, kParentAtomIsContainer,
                                   vdIIDCAtomTypeFeature, 1, NULL);
  if (0 == featureAtom) {
    return false;  //pd_error(0, "[GEM:videoDarwin] featureAtom vdIIDCFeatureSaturation not found");
  }

  result = QTCopyAtomDataToPtr(atomContainer,
                               QTFindChildByID(atomContainer, featureAtom,
                                   vdIIDCAtomTypeFeatureSettings,
                                   vdIIDCAtomIDFeatureSettings, NULL),
                               true, sizeof(settings), &settings, NULL);

  settings.state.flags = (vdIIDCFeatureFlagOn |
                          vdIIDCFeatureFlagManual |
                          vdIIDCFeatureFlagRawControl);

  settings.state.value = value;

  result = QTSetAtomData(atomContainer,
                         QTFindChildByID(atomContainer, featureAtom,
                                         vdIIDCAtomTypeFeatureSettings,
                                         vdIIDCAtomIDFeatureSettings, NULL),
                         sizeof(settings), &settings);

  result = VDIIDCSetFeatures(m_vdig, atomContainer);

  return true;
}
inline unsigned short d2us(double x)
{
  return (unsigned short)(65535.*((x > 1.f) ? 1.f : ( (x < 0.f) ? 0.f : x)));
}
inline double us2d(unsigned short x)
{
  static double factor=1./65535.;
  return (x*factor);
}

bool videoDarwin::applyProperties(gem::Properties&props)
{
  bool restart=false;

  bool iidc=false;
  if(m_vdig) {
    ComponentDescription    desc;
    GetComponentInfo((Component)m_vdig, &desc, NULL, NULL, NULL);
    iidc=(vdSubtypeIIDC == desc.componentSubType);
  }

  std::vector<std::string>keys=props.keys();
  for(int i=0; i<keys.size(); i++) {
    double value_d=0.;
    unsigned short value_us=0;
    std::string key=keys[i];
    if("width"==key) {
      if(props.get(key, value_d)) {
        unsigned int width=value_d;
        if(m_width!=width) {
          restart=true;
        }
        m_width=width;
      }
    } else if("height"==key) {
      if(props.get(key, value_d)) {
        unsigned int height=value_d;
        if(m_height!=height) {
          restart=true;
        }
        m_height=height;
      }
    } else if("channel"==key) {
      if(props.get("channel", value_d)) {
        unsigned int channel=value_d;
        if(channel!=m_inputDeviceChannel) {
          restart=true;
        }
        m_inputDeviceChannel=channel;
      }
    } else if("quality"==key) {
      if(props.get(key, value_d)) {
        unsigned int quality=value_d;
        bool doit=false;
        switch (quality) {
        case 0:
          m_quality=channelPlayNormal;
          doit=true;
          break;
        case 1:
          m_quality=channelPlayHighQuality;
          doit=true;
          break;
        case 2:
          m_quality=channelPlayFast;
          doit=true;
          break;
        case 3:
          m_quality=channelPlayAllData;
          doit=true;
          break;
        default:
          break;
        }
        if(doit&&m_vc) {
          SGSetChannelPlayFlags(m_vc, m_quality);
        }
      }
#define PROPSET_IIDC_VD(NAME) \
      } else if (#NAME == key && props.get(key, value_d) && m_vdig) {   \
        if(iidc){setIIDCProperty(vdIIDCFeature ## NAME, value_d);}      \
        else {value_us = d2us(value_d);                                 \
          VDSet ## NAME (m_vdig,&value_us); } value_d=0
#define PROPSET_VD(NAME)                                                \
      } else if (#NAME == key && props.get(key, value_d) && m_vdig) {   \
        if(!iidc) {value_us = d2us(value_d);                            \
          VDSet ## NAME (m_vdig,&value_us); } value_d=0
#define PROPSET_IIDC(NAME)                                             \
      } else if (#NAME == key && props.get(key, value_d) && iidc) {    \
        setIIDCProperty(vdIIDCFeature ## NAME, value_d); value_d=0

      PROPSET_IIDC_VD(Hue);
      PROPSET_IIDC_VD(Sharpness);
      PROPSET_VD(Contrast);
      //PROPSET_VD(KeyColor);
      //PROPSET_VD(ClipState);
      //PROPSET_VD(ClipRng);
      //PROPSET_VD(PLLFilterType);
      PROPSET_VD(MasterBlendLevel);
      //PROPSET_VD(PlayThroughOnOff);
      //PROPSET_VD(FieldPreference);
      PROPSET_VD(BlackLevelValue);
      PROPSET_VD(WhiteLevelValue);
      //PROPSET_VD(Input);
      //PROPSET_VD(InputStandard);
      PROPSET_IIDC_VD(Saturation);
      PROPSET_IIDC_VD(Brightness);
      PROPSET_IIDC(Gain);
      PROPSET_IIDC(Iris);
      PROPSET_IIDC(Shutter);
      PROPSET_IIDC(Exposure);
      PROPSET_IIDC(WhiteBalanceU);
      PROPSET_IIDC(WhiteBalanceV);
      PROPSET_IIDC(Gamma);
      PROPSET_IIDC(Temperature);
      PROPSET_IIDC(Zoom);
      PROPSET_IIDC(Focus);
      PROPSET_IIDC(Pan);
      PROPSET_IIDC(Tilt);
      PROPSET_IIDC(OpticalFilter);
      PROPSET_IIDC(EdgeEnhancement);
    }
  }
  return restart;
}
void videoDarwin::setProperties(gem::Properties&props)
{
  bool restart=applyProperties(props);
  if(restart) {
    if(stop()) {
      resetSeqGrabber();
      start();
    }
  }
}


void videoDarwin::getProperties(gem::Properties&props)
{
  std::vector<std::string>keys=props.keys();
  bool iidc=false;
  if(m_vdig) {
    ComponentDescription    desc;
    GetComponentInfo((Component)m_vdig, &desc, NULL, NULL, NULL);
    iidc=(vdSubtypeIIDC == desc.componentSubType);
  }

  for(int i=0; i<keys.size(); i++) {
    std::string key=keys[i];
    unsigned short value_us=0;
    if(0) {
#define PROPGET_VD(NAME)                                                \
      } else if (#NAME == key && m_vdig && !iidc) {                     \
        if(0==VDGet ## NAME (m_vdig,&value_us)) {props.set(key, us2d(value_us)); }
      PROPGET_VD(Hue);
      PROPGET_VD(Sharpness);
      PROPGET_VD(Saturation);
      PROPGET_VD(Brightness);
      PROPGET_VD(Contrast);
      //PROPGET_VD(KeyColor);
      //PROPGET_VD(ClipState);
      //PROPGET_VD(ClipRng);
      //PROPGET_VD(PLLFilterType);
      //PROPGET_VD(PlayThroughOnOff);
      //PROPGET_VD(FieldPreference);
      PROPGET_VD(BlackLevelValue);
      PROPGET_VD(WhiteLevelValue);
      //PROPGET_VD(Input);
      //PROPGET_VD(InputStandard);
    }
  }
}

std::vector<std::string> videoDarwin::enumerate()
{
  std::vector<std::string> result;
  OSErr anErr;
  SGDeviceList    devices;

  anErr = SGGetChannelDeviceList(m_vc, sgDeviceListIncludeInputs, &devices);
  if(anErr!=noErr) {
    logpost(0, 3+0, "[GEM:videoDarwin] could not get SG channel Device List");
  } else {
    short deviceCount = (*devices)->count;
    short deviceIndex = (*devices)->selectedIndex;
    short inputIndex;
    logpost(0, 3+1, "[GEM:videoDarwin] SG channel Device List count %d index %d",
            deviceCount,deviceIndex);
    m_devices.clear();
    for (int i = 0; i < deviceCount; i++) {
      m_devices.push_back(pascal2str((*devices)->entry[i].name));
      logpost(0, 3+1, "[GEM:videoDarwin] SG channel Device List[%d]  %s", i,
              m_devices[i].c_str());
    }
    SGGetChannelDeviceAndInputNames(m_vc, NULL, NULL, &inputIndex);

    bool showInputsAsDevices = ((*devices)->entry[deviceIndex].flags) &
                               sgDeviceNameFlagShowInputsAsDevices;

    SGDeviceInputList theSGInputList = ((SGDeviceName *)(&((
                                          *devices)->entry[deviceIndex])))->inputs; //fugly

    //we should have device names in big ass undocumented structs
    //walk through the list
    for (int i = 0; i < inputIndex; i++) {
      std::string input=pascal2str((*theSGInputList)->entry[i].name);
      logpost(0, 3+1, "[GEM:videoDarwin] SG channel Input Device List %d %s",
              i, input.c_str());
    }
  }

  result=m_devices;
  return result;
}
#endif // HAVE_VIDEODARWIN
