//
//  Mixer.app
// 
//  Copyright (c) 1998-2002 Per Liden
// 
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, 
//  USA.
//

#include <X11/Xlib.h>
#include <sys/ioctl.h>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
#include <csignal>
#include <fcntl.h>
#include <unistd.h>
#include "Xpm.h"
#include "Mixer.h"

#if defined(__Linux__)
#include <linux/soundcard.h>
#elif defined(__FreeBSD__)
#include <machine/soundcard.h>
#elif defined(__OpenBSD__)
#include <soundcard.h>
#else
#include <sys/soundcard.h>
#endif

#include "pixmaps/main.xpm"
#include "pixmaps/button.xpm"
#include "pixmaps/mutebutton.xpm"
#include "pixmaps/redlight.xpm"

using namespace std;

static const int ButtonX[] = {6, 24, 42};
static char* MixerDevices[] = { MIXERDEVICES };
static const char* SourceNames[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_NAMES;

extern Mixer* app;

void catchBrokenPipe(int sig) 
{
   app->saveVolumeSettings();
   exit(0);
}

Mixer::Mixer(int argc, char** argv) 
{
   XClassHint classHint;
   XSizeHints sizeHints;
   XWMHints   wmHints;
   Atom       deleteWindow;
   Xpm*       image;
   char*      displayName = NULL;

   mError = 0;
   mInstanceName = INSTANCENAME;
   mVolumeSource[0] = -1;
   mVolumeSource[1] = -1;
   mVolumeSource[2] = -1;
   mVolumeMute[0] = 0;
   mVolumeMute[1] = 0;
   mVolumeMute[2] = 0;
   mWheelButton = 1;
   mLabelText = 0;
   mSettingsFile = 0;

   findMixerDevice();

   // Parse command line
   if (argc>1) {
      for (int i=1; i<argc; i++) {
         // Display
         if (!strcmp(argv[i], "-d")) {
            checkArgument(argv, argc, i);
            displayName = argv[i+1];
            i++;
         }

         // Sound source
         else if (!strcmp(argv[i], "-1") || !strcmp(argv[i], "-2") || !strcmp(argv[i], "-3")) {
            checkArgument(argv, argc, i);
            for (int j=0; j<SOUND_MIXER_NRDEVICES; j++) {
               if (!strcasecmp(SourceNames[j], argv[i+1])) {
                  mVolumeSource[argv[i][1]-'1'] = j;
               }
            }

            if (mVolumeSource[argv[i][1]-'1'] == -1) {
               cerr << APPNAME << ": invalid sound source " << argv[i+1] << endl;
               tryHelp(argv[0]);
               exit(0);
            }
            i++;
         }

         // Wheel binding
         else if (!strcmp(argv[i], "-w")) {
            checkArgument(argv, argc, i);
            mWheelButton = atoi(argv[i+1]);

            if (mWheelButton < 1 || mWheelButton > 3) {
               cerr << APPNAME << ": invalid wheel binding, must be 1, 2 or 3, not " << argv[i+1] << endl;
               tryHelp(argv[0]);
               exit(0);
            }

            i++;
         }

         // Label text
         else if (!strcmp(argv[i], "-l")) {
            checkArgument(argv, argc, i);
            mLabelText = argv[i+1];
            i++;
         }

         // Load/Save settings (default file)
         else if (!strcmp(argv[i], "-s")) {
            char* home = getenv("HOME");
            if (home) {
               mSettingsFile = new char[strlen(home) + strlen(SETTINGS) + 1];
               strcpy(mSettingsFile, home);
               strcat(mSettingsFile, SETTINGS);
            } else {
               cerr << APPNAME << ": $HOME not set, could not find saved settings" << endl;
            }
         }

         // Load/Save settings
         else if (!strcmp(argv[i], "-S")) {
            checkArgument(argv, argc, i);
            mSettingsFile = argv[i+1];
            i++;
         }

         // Mixer deice
         else if (!strcmp(argv[i], "-m")) {
            checkArgument(argv, argc, i);
            mMixerDevice = argv[i+1];
            i++;
         }

         // Instance name
         else if (!strcmp(argv[i], "-n")) {
            checkArgument(argv, argc, i);
            mInstanceName = argv[i+1];
            i++;
         }

         // Version
         else if (!strcmp(argv[i], "-v")) {
            cerr << APPNAME << " version " << VERSION << endl;
            exit(0);
         }

         // Help
         else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
            showHelp();
            exit(0);
         }

         // Unknown option
         else {
            cerr << APPNAME << ": invalid option '" << argv[i] << "'" << endl;
            tryHelp(argv[0]);
            exit(0);
         }
      }
   }

   // Set default if not specified in cmd-line
   if (mVolumeSource[0] == -1) {
      mVolumeSource[0] = SOUND_MIXER_VOLUME;
   }

   if (mVolumeSource[1] == -1) {
      mVolumeSource[1] = SOUND_MIXER_CD;
   }

   if (mVolumeSource[2] == -1) {
      mVolumeSource[2] = SOUND_MIXER_PCM;
   }

   // Open display
   if ((mDisplay = XOpenDisplay(displayName)) == NULL) {
      cerr << APPNAME << ": could not open display " << displayName << endl;
      exit(0);
   }
 
   // Get root window
   mRoot = RootWindow(mDisplay, DefaultScreen(mDisplay));

   // Create windows
   mAppWin = XCreateSimpleWindow(mDisplay, mRoot, 1, 1, 64, 64, 0, 0, 0);
   mIconWin = XCreateSimpleWindow(mDisplay, mAppWin, 0, 0, 64, 64, 0, 0, 0);

   // Set classhint
   classHint.res_name =  mInstanceName;
   classHint.res_class = CLASSNAME;
   XSetClassHint(mDisplay, mAppWin, &classHint);

   // Create delete atom
   deleteWindow = XInternAtom(mDisplay, "WM_DELETE_WINDOW", False);
   XSetWMProtocols(mDisplay, mAppWin, &deleteWindow, 1);
   XSetWMProtocols(mDisplay, mIconWin, &deleteWindow, 1);

   // Set windowname
   XStoreName(mDisplay, mAppWin, APPNAME);
   XSetIconName(mDisplay, mAppWin, APPNAME);

   // Set sizehints
   sizeHints.flags= USPosition;
   sizeHints.x = 0;
   sizeHints.y = 0;
   XSetWMNormalHints(mDisplay, mAppWin, &sizeHints);

   // Set wmhints
   wmHints.initial_state = WithdrawnState;
   wmHints.icon_window = mIconWin;
   wmHints.icon_x = 0;
   wmHints.icon_y = 0;
   wmHints.window_group = mAppWin;
   wmHints.flags = StateHint | IconWindowHint | IconPositionHint | WindowGroupHint;
   XSetWMHints(mDisplay, mAppWin, &wmHints);

   // Set command
   XSetCommand(mDisplay, mAppWin, argv, argc);

   // Set background image
   image = new Xpm(mDisplay, mRoot, main_xpm);
   if (mLabelText) {
      image->drawString(LABEL_X, LABEL_Y, mLabelText);
   }
   image->setWindowPixmapShaped(mIconWin);
   delete image;

   // Create buttons
   mButton[0] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[0], BUTTON_MIN, 5, 5, 0, 0, 0);
   mButton[1] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[1], BUTTON_MIN, 5, 5, 0, 0, 0);
   mButton[2] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[2], BUTTON_MIN, 5, 5, 0, 0, 0);

   image = new Xpm(mDisplay, mRoot, button_xpm);
   image->setWindowPixmap(mButton[0]);
   image->setWindowPixmap(mButton[1]);
   image->setWindowPixmap(mButton[2]);
   delete image;

   XSelectInput(mDisplay, mButton[0], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
   XSelectInput(mDisplay, mButton[1], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
   XSelectInput(mDisplay, mButton[2], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
   XSelectInput(mDisplay, mIconWin, ButtonPressMask);

   XMapWindow(mDisplay, mButton[0]);
   XMapWindow(mDisplay, mButton[1]);
   XMapWindow(mDisplay, mButton[2]);

   XMapWindow(mDisplay, mIconWin);
   XMapWindow(mDisplay, mAppWin);
   XSync(mDisplay, False);

   // Catch broker pipe signal
   signal(SIGPIPE, catchBrokenPipe);

   // Check if error
   if (mError) {
      showErrorLed();
   } else {
      getVolume();
      loadVolumeSettings();
   }
}

void Mixer::tryHelp(char* appname)
{
   cerr << "Try `" << appname << " --help' for more information" << endl;
}

void Mixer::showHelp() 
{
   cerr << APPNAME << " Copyright (c) 1998-2002 by Per Liden (per@fukt.bth.se)" << endl << endl
        << "options:" << endl
        << " -1 <source>     set sound source for control 1 (default is vol)" << endl
        << " -2 <source>     set sound source for control 2 (default is cd)" << endl
        << " -3 <source>     set sound source for control 3 (default is pcm)" << endl
        << " -w 1|2|3        bind a control button to the mouse wheel (default is 1)" << endl
        << " -l <text>       set label text" << endl
        << " -s              load/save volume settings using ~/GNUstep/Defaults/Mixer" << endl
        << " -S <file>       load/save volume settings using <file>" << endl
        << " -m <device>     set mixer device" << endl
        << " -n <name>       set client instance name" << endl
        << " -d <disp>       set display" << endl
        << " -v              print version and exit" << endl
        << " -h, --help      display this help and exit" << endl << endl
        << "sound sources:" << endl;
   for (int i = 0; i < SOUND_MIXER_NRDEVICES; i++) {
      cerr << " " << SourceNames[i];
      if (i > 0 && i % 10 == 0)
         cerr << "," << endl;
      else if (i == SOUND_MIXER_NRDEVICES - 1)
         cerr << "." << endl;
      else
         cerr << ",";
   }
}

void Mixer::checkArgument(char** argv, int argc, int index)
{
   if (argc-1 < index+1) {
      cerr << APPNAME << ": option '" << argv[index] << "' requires an argument" << endl;
      tryHelp(argv[0]);
      exit(0);
   }
}

void Mixer::showErrorLed() 
{
   Window led;
   Xpm*   image;

   led = XCreateSimpleWindow(mDisplay, mIconWin, LED_X, LED_Y, 3, 2, 0, 0, 0);

   // Set background image
   image = new Xpm(mDisplay, mRoot, redlight_xpm);
   image->setWindowPixmap(led);
   delete image;

   // Show window
   XMapWindow(mDisplay, led);
   mError = 1;
}

void Mixer::findMixerDevice()
{
   for (int i = 0; MixerDevices[i]; i++) {
      mMixerDevice = MixerDevices[i];
      int fd = open(mMixerDevice, 0);
      if (fd != -1) {
         close(fd);
         return;
      }
   }

   cerr << APPNAME << ": unable to open mixer device, tried the following: ";
   for (int i = 0; MixerDevices[i]; i++)
      cerr << MixerDevices[i] << " ";
   cerr << endl;
}

void Mixer::loadVolumeSettings()
{
   if (mSettingsFile) {
      ifstream file(mSettingsFile);
      if (file) {
         // This could fail if the user has edited the file by hand and destroyed the structure
         char dummy[1024];
         file >> dummy; // {
         file >> dummy; // Volume1
         file >> dummy; // =
         file >> mVolumePos[0];
         file >> dummy; // ;

         file >> dummy; // Volume2
         file >> dummy; // =
         file >> mVolumePos[1];
         file >> dummy; // ;

         file >> dummy; // Volume3
         file >> dummy; // =
         file >> mVolumePos[2];

         file.close();
         setVolume(0, mVolumePos[0]);
         setVolume(1, mVolumePos[1]);
         setVolume(2, mVolumePos[2]);
      }
   }
}

void Mixer::saveVolumeSettings()
{
   if (mSettingsFile) {
      ofstream file(mSettingsFile);
      if (file) {
         // Files in ~/GNUstep/Defaults/ should follow the property list format
         file << "{" << endl 
              << "  Volume1 = " << mVolumePos[0] << ";" << endl
              << "  Volume2 = " << mVolumePos[1] << ";" << endl
              << "  Volume3 = " << mVolumePos[2] << ";" << endl
              << "}" << endl;
         file.close();
      } else {
         cerr << APPNAME << ": failed to save volume settings in " << mSettingsFile << endl;
      }
   }
}

void Mixer::getVolume() 
{
   static int lastVolume[3] = {1, 1, 1};
   int        fd;

   if (mError) {
      return;
   }

   // Open device
   fd = open(mMixerDevice, 0);
   if (fd < 0) {
      showErrorLed();
      return;
   }

   // Read from device
   for (int i=0; i<3; i++) {
      if (ioctl(fd, MIXER_READ(mVolumeSource[i]), &mVolume[i]) < 0) {
         mError = 1;
      }

      mVolume[i] = mVolume[i] >> 8;

      if (lastVolume[i] != mVolume[i]) {
         int y;

         // Set button position
         if (mError) {
            y = BUTTON_MIN;
         } else {
            y = BUTTON_MIN - (mVolume[i] * (BUTTON_MIN - BUTTON_MAX)) / 100;
         }

         if (mVolumeMute[i] == 1) {
            if (y != BUTTON_MIN) {
               // Release mute if the volume was changed from another program
               mVolumePos[i] = y;  // Reset button position
               mute(i);		// Toggle mute
            } else {
               // Skip if muted
               continue;
            }
         }

         mVolumePos[i] = y;
         XMoveWindow(mDisplay, mButton[i], ButtonX[i], y);
         XFlush(mDisplay);
         lastVolume[i] = mVolume[i];
      }
   }

   // Close device
   close(fd);

   if (mError) {
      cerr << APPNAME << ": unable to read from " << mMixerDevice << endl;
      showErrorLed();
      return;
   }
}

void Mixer::setVolume(int button, int volume) 
{
   int fd;

   if (mError) {
      return;
   }

   // Open device
   fd = open(mMixerDevice, 0);

   // Calculate volume
   mVolume[button] = 100 - (((volume - BUTTON_MAX) * 100) / (BUTTON_MIN - BUTTON_MAX));
   mVolume[button] |= mVolume[button] << 8;

   // Write to device
   ioctl(fd, MIXER_WRITE(mVolumeSource[button]), &mVolume[button]);

   // Close devicw
   close(fd);
}

void Mixer::mute(int button) 
{
   Xpm* image;

   if (mVolumeMute[button] == 0) {
      // Mute
      mVolumeMute[button] = 1;
      setVolume(button, BUTTON_MIN);

      image = new Xpm(mDisplay, mRoot, mutebutton_xpm);
      image->setWindowPixmap(mButton[button]);
      delete image;

      XClearWindow(mDisplay, mButton[button]);
   } else {
      // Restore volume
      mVolumeMute[button] = 0;
      setVolume(button, mVolumePos[button]);

      image = new Xpm(mDisplay, mRoot, button_xpm);
      image->setWindowPixmap(mButton[button]);
      delete image;

      XClearWindow(mDisplay, mButton[button]);
   }
}

void Mixer::setButtonPosition(int button, int relativePosition) 
{
   int y;
    
   // Calc new button position
   y = mVolumePos[button] + relativePosition;

   if (y > BUTTON_MIN) {
      y = BUTTON_MIN;
   } else if (y < BUTTON_MAX) {
      y = BUTTON_MAX;
   }
    
   // Set button position and volume
   XMoveWindow(mDisplay, mButton[button], ButtonX[button], y);

   mVolumePos[button] = y;
    
   // Don't really set volume if muted
   if (mVolumeMute[button] == 0) {
      setVolume(button, y);
   }
}

void Mixer::run() 
{
   XEvent event;
   int    buttonDown = 0;
   int    buttonDownPosition = 0;

   // Start handling events
   while(1) {
      while(XPending(mDisplay) || buttonDown) {
         XNextEvent(mDisplay, &event);
	    
         switch(event.type) {
         case ButtonPress:
            if (event.xbutton.button == Button4 || event.xbutton.button == Button5) {
               // Wheel scroll
               setButtonPosition(mWheelButton - 1, event.xbutton.button == Button5? 3: -3);
            } else if (event.xbutton.button == Button1 && event.xbutton.window != mIconWin) {
               // Volume change
               buttonDown = 1;
               buttonDownPosition = event.xbutton.y;
            } else if (event.xbutton.button == Button3 && buttonDown == 0 && event.xbutton.window != mIconWin) {
               // Mute
               for (int i=0; i<3; i++) {
                  if (mButton[i] == event.xbutton.window) {
                     mute(i);
                     break;
                  }
               }
            }
            break;
		
         case ButtonRelease:
            if (event.xbutton.button == Button1) {
               buttonDown = 0;
            }
            break;
		
         case MotionNotify:
            if (buttonDown) {
               // Find button
               for (int i=0; i<3; i++) {
                  if (mButton[i] == event.xmotion.window) {
                     setButtonPosition(i, event.xmotion.y - buttonDownPosition);
                     break;
                  }
               }
            }
            break;
         }
      }

      // Idle for a moment
      usleep(50000);

      // Update volume status
      getVolume();
      XSync(mDisplay, False);
   }
}

