// Programación en C++ para Ingenieros, Ed. Thomson Paraninfo, 2006
// Apéndice B: La Biblioteca CDAudioLib

// Fichero cdaudio.cpp
// Implementación CDAudioLib

#include <cstdlib>
#include "cdaudio.h"
#include <windows.h>

// Definición de un tipo denominado RECURSOSCDAUDIO
// que permite mantener los recursos necesarios para
// manipular el dispositivo CD de audio. Donde:
//       VentanaMensajes: mantiene la ventana a la que
//         se han de enviar los mensajes del dispositivo.
//
//       wpCDAudio: mantiene la función de atención a los
//         mensajes de la ventana proporcionada por la
//         biblioteca CDAudioLib.
//
//       wpOriginal: mantiene la función de atención original
//         de la aplicación que utiliza la biblioteca.
//
//       hThread y hThreadID: mantienen la información del
//         hilo de ejecución que se crea cuando la aplicación
//         que utiliza la biblioteca no tiene ninguna ventana o
//         la función de atención está protegida y no se puede
//         sustituir por la de la biblioteca.
// También se definen dos tipos aputador a un objeto de tipo
// RECURSOSCDAUDIO.
  typedef struct tagPARAMCREATEREMOTO {
    HWND VentanaMensajes;
    CDAUDIOINTPROCEDURE intProcedure;
    WNDPROC wpCDAudio;
    WNDPROC wpOriginal;
    HANDLE hThread;
    DWORD hThreadID;
  } RECURSOSCDAUDIO,*PRECURSOSCDAUDIO,*LPRECURSOSCDAUDIO;

// Variables para mantener la información de los
// recursos, para almacenar los mensajes de error y
// para recojer los resultados de los comandos enviados
// al dispostivo.
  static RECURSOSCDAUDIO rcCDAudio;
  static char mensajeError[256];
  static char resultado[256];

// Declaración de las funciones privadas para crear
// y destruir, respectivamente, los recursos necesarios.
  bool DestruirRecursos(PRECURSOSCDAUDIO rcCDAudio);
  bool CrearRecursos(PRECURSOSCDAUDIO rcCDAudio);

// Declaración de dos funciones privadas auxiliares que, respectivamente,
// permiten consultar el resultado de un MCI Command String y la longitud
// de un track en el formato de tiempo establecido.
  const char* Resultado();
  bool LongTrack(int numTrack);


// Declaración de la función de atención a los mensajes de ventana de
// la biblioteca CDAudioLib.
  LRESULT CALLBACK CDAudioMensajesProcedure (HWND hwnd, UINT message,
                                           WPARAM wParam, LPARAM lParam);

// Declaración de dos funciones auxiliares necesarias para el caso
// en que la aplicación que utiliza la biblioteca no tiene ventana
// o la función de atención a los mensajes no se puede sustituir por
// la de la biblioteca.

  // Funciones para crear la ventana y fución de atención a los mensajes.
     HWND CrearVentanaMensajes(HINSTANCE hinstance, HWND hwndParentWindow);
     bool CrearClaseVentanaMensajes(HINSTANCE hinstance, WNDPROC wp);
     LRESULT CALLBACK CDAudioWindowProcedure (HWND hwnd, UINT message,
                                              WPARAM wParam, LPARAM lParam);
  // Thread que crea la ventana y procesa los mensajes. Recibe como
  // parámetro un apuntador a la variable que mantiene los recursos.
     DWORD WINAPI ThreadMensajes(LPVOID lpParameter);


// Función para enviar un MCI Command String al dispositivo.
  bool EnviarComando(char *Comando, HWND hwnd) {
     MCIERROR error;
     // Se envia el comando utilizando la función mciSendString()
     error = mciSendString(Comando, resultado, sizeof resultado, hwnd);
     // Si se produce un error se obtiene el mensaje de error y se
     // almacena en la variable mensajeError para que pueda ser
     // consultado por el usuario utilizando la función MensajeError().
     if(error)
         mciGetErrorString(error, mensajeError, sizeof mensajeError);
     return error;
  }

// Implementación de las funciones del API, sus funcionalidades están
// comentadas en el fichero de cabecera cdaudio.h
  bool OpenCDAudio(void) {
     if(rcCDAudio.VentanaMensajes != NULL) {
         // Si hay una ventana que procesa los mensajes del dispositivo
         // quiere decir que el dispositivo ya está abierto.
         wsprintf(mensajeError, "El dipositivo ya está abierto");
         return true;
     }
     // El dispositivo no está abierto.
     // Se intenta crear los recursos necesarios.
     if(!CrearRecursos(&rcCDAudio)) {
         // Si por algún motivo no se pueden crear los recursos se
         // devuelve error.
         wsprintf(mensajeError, "No se han podido crear los recursos \
                                 necesarios");
         return true;
     }
     // Se han creado los recursos necesarios.
     // Se envía al dispositivo el comando de apertura.
     if(EnviarComando("open cdaudio wait", NULL)) {
         // Si se produce algún error en la apertura del dispositivo
         // se recoje el mensaje de error, se destruyen los recursos
         // y se devuelve error.
         wsprintf(mensajeError, "No se ha podido abrir el dispositivo");
         DestruirRecursos(&rcCDAudio);
         return true;
     }
     // El dispositivo ya está abierto.
     // Se establece el formato de tiempo a tt:mm:ss:ff
     return EnviarComando("set cdaudio wait time format tmsf",NULL);
  }

  bool CloseCDAudio(void) {
     if(rcCDAudio.VentanaMensajes == NULL) {
         // Si no hay una ventana que procesa los mensajes del dispositivo
         // quiere decir que el dispositivo no está abierto.
         wsprintf(mensajeError, "El dipositivo no está abierto");
         return true;
     }
     // El dispositivo está abierto.
     // Se detiene la reproducción.
     Stop();
     // Se envía al dispositivo el comando para cerrarlo.
     if(EnviarComando("close cdaudio wait", NULL)) {
         // Si se produce algún error en el cierre del dispositivo
         // se recoje el mensaje de error y se devuelve error.
         wsprintf(mensajeError, "No se ha podido cerrar el dispositivo");
         return true;
     }
     // El dispositivo se ha cerrado correctamente.
     // Se intenta liberar los recursos utilizados.
     if(!DestruirRecursos(&rcCDAudio)) {
         // Si por algún motivo no se pueden liberar los recursos se
         // devuelve error.
         wsprintf(mensajeError, "No se han podido liberar los \
                                 recursos necesarios");
         return true;
     }
     return false;
  }

  // El resto de funciones del API solamente crean el MCI Command String indicado
  // para la función y lo envían utilizando la función EnviarComando(), sus
  // funcionalidades están comentadas en el fichero de cabecera cdaudio.h. En el
  // caso de las funciones asíncronas se añade o no la opción notify dependiendo
  // de si el parámetro correspondiente a la función de atención es NULL o no.
  bool OpenDoorCD(void) {
     bool error;
     return EnviarComando("set cdaudio door open wait", NULL);
  }

  bool CloseDoorCD(void) {
     return EnviarComando("set cdaudio door closed", NULL);
  }

  bool Stop(void) {
     bool error;
     error = EnviarComando("stop cdaudio wait", NULL);
     if(!error) error = EnviarComando("seek cdaudio to start wait",NULL);
  }

  bool Pause(void){
     return EnviarComando("pause cdaudio wait", NULL);
  }

  bool Play(CDAUDIOINTPROCEDURE intPro) {
     char Comando[128];
     bool error;
     HWND hwnd = NULL;
     if(intPro != NULL) {
         rcCDAudio.intProcedure = intPro;
         wsprintf(Comando, "play cdaudio notify");
         hwnd = rcCDAudio.VentanaMensajes;
     }
     else {
         wsprintf(Comando, "play cdaudio");
     }
     error = EnviarComando(Comando,hwnd);
     return error;
  }

  bool PlayTrack(int numTrack, CDAUDIOINTPROCEDURE intPro) {
     char Comando[128];
     HWND hwnd = NULL;
     bool error;
     error = LongTrack(numTrack);
     if(error) return error;
     if(intPro != NULL) {
         rcCDAudio.intProcedure = intPro;
         wsprintf(Comando, "play cdaudio from %u to %u:%s notify",
                  numTrack,numTrack,Resultado());
         hwnd = rcCDAudio.VentanaMensajes;
     }
     else  {
         wsprintf(Comando, "play cdaudio from %u to %u:%s",numTrack,
                  numTrack,Resultado());
     }
     error = EnviarComando(Comando,hwnd);
     return error;
  }

  bool PlayTracksDesdeA(int primerTrack,int ultimoTrack,
                        CDAUDIOINTPROCEDURE intPro){
     char Comando[128];
     HWND hwnd = NULL;
     bool error;
     error = LongTrack(ultimoTrack);
     if(error) return error;
     if(intPro != NULL) {
         rcCDAudio.intProcedure = intPro;
         wsprintf(Comando, "play cdaudio from %u to %u:%s notify",primerTrack,
                  ultimoTrack,Resultado());
         hwnd = rcCDAudio.VentanaMensajes;
     }
     else   {
         wsprintf(Comando, "play cdaudio from %u to %u:%s",primerTrack,
                  ultimoTrack,Resultado());
     }
     return EnviarComando(Comando,hwnd);
  }

  int NumeroDeTracks(void) {
     bool error;
     error = EnviarComando("status cdaudio number of tracks", NULL);
     if(!error) return atoi(resultado);
     return 0;
  }

  int TrackActual(void) {
     bool error;
     error = EnviarComando("status cdaudio current track", NULL);
     if(!error) return atoi(resultado);
     return 0;
  }

  bool IsOpen(void) {
     return rcCDAudio.VentanaMensajes != NULL;
  }

  bool IsReady(void) {
     EnviarComando("status cdaudio mode", NULL);
     return strcmpi("not ready", Resultado()) != 0;
  }

  bool IsPlaying(void) {
     EnviarComando("status cdaudio mode", NULL);
     return strcmpi("playing", Resultado()) == 0;
  }

  bool IsStopped(void) {
     EnviarComando("status cdaudio mode", NULL);
     return strcmpi("stopped", Resultado()) == 0;
  }

  bool IsPaused(void) {
     EnviarComando("status cdaudio mode", NULL);
     return strcmpi("paused", Resultado()) == 0;
  }

  bool EsCDMedia(void) {
     EnviarComando("status cdaudio media present", NULL);
     return strcmpi("true", Resultado()) == 0;
  }

  bool Play(void) {
     return EnviarComando("play cdaudio wait",NULL);
  }

  bool PlayTrack(int numTrack) {
     char Comando[128];
     bool error;
     error = LongTrack(numTrack);
     if(error) return error;
     wsprintf(Comando, "play cdaudio from %u to %u:%s wait",numTrack,
              numTrack,Resultado());
     return EnviarComando(Comando,NULL);
  }

  bool PlayTracksDesdeA(int primerTrack, int ultimoTrack) {
     char Comando[128];
     bool error;
     error = LongTrack(ultimoTrack);
     if(error) return error;
     wsprintf(Comando, "play cdaudio from %u to %u:%s wait",primerTrack,
              ultimoTrack,Resultado());
     return EnviarComando(Comando,NULL);
  }

  bool Resume(CDAUDIOINTPROCEDURE intPro) {
     char Comando[128];
     bool error;
     HWND hwnd = NULL;
     if(intPro != NULL) {
         rcCDAudio.intProcedure = intPro;
         wsprintf(Comando, "resume cdaudio notify");
         hwnd = rcCDAudio.VentanaMensajes;
     }
     else {
         wsprintf(Comando, "resume cdaudio");
     }
     error = EnviarComando(Comando,hwnd);
     return error;
  }

  const char* MensajeError() {
     return mensajeError;
  }


// Implementación de las funciones privadas de la biblioteca CDAudioLib.

  // Función que permite consultar el resultado de un MCI Command String.
  const char* Resultado() {
      return resultado;
  }

  // Función que envía un MCI Command String al dispositivo para obtener la
  // longitud de un track en el formato de tiempo establecido. Para obtener el
  // resultado se ha de utilizar la función Resultado() después de llamar a
  // LongTrack();
  bool LongTrack(int numTrack) {
     bool error;
     char Comando[128];
     wsprintf(Comando, "status cdaudio length track %u", numTrack);
     error = EnviarComando(Comando, NULL);
     return error;
  }

  // Función para crear los recursos necesarios para el control y manipulación
  // del dispositivo. Devuelve true cuando se han podido crear todos los
  // recursos. En caso contrario devuelve false
  bool CrearRecursos(PRECURSOSCDAUDIO prcCDAudio) {
    // Se consultan la ventana activa y la última ventana utilizada de la
    // aplicación que utiliza la biblioteca.
    HWND hwndActiveWindow = GetActiveWindow();
    HWND hwndForegroundWindow = GetForegroundWindow();

    // Se escoje, como ventana a enviar los mensajes del dispositivo, en primer
    // lugar la activa y si no hay ventana activa la última. Si no hay ninguna
    // no se pueden crear los recursos.
    if(hwndActiveWindow != NULL)
        prcCDAudio->VentanaMensajes = hwndActiveWindow;
    else if(hwndForegroundWindow != NULL)
        prcCDAudio->VentanaMensajes = hwndForegroundWindow;
    else return false;

    // Se sustituye la función original de atención a los mensajes de ventana por
    // la de la biblioteca.
    prcCDAudio->wpOriginal = NULL;
    prcCDAudio->wpCDAudio = CDAudioMensajesProcedure;
    prcCDAudio->wpOriginal =(WNDPROC)SetWindowLongPtr(prcCDAudio->VentanaMensajes,
                                                      GWL_WNDPROC,
                                                      (LONG)prcCDAudio->wpCDAudio);
    // Si se ha podido realizar la sustitución ya no se necesitan más recursos y
    // se devuelve true.
    if(prcCDAudio->wpOriginal != NULL) return true;

    // Si no se ha podido realizar la sustitución se crea un hilo de ejecución
    // (thread) que creará una ventana para procesar los mensajes del
    // dispositivo y procesará los mensajes.
    prcCDAudio->hThread = CreateThread(NULL,0,ThreadMensajes,prcCDAudio,0,
                                       &prcCDAudio->hThreadID);
    // Esperamos que el thread cree la ventana y devolvemos si se ha podido
    // crear o no.
    WaitForSingleObject(prcCDAudio->hThread,200);
    return prcCDAudio->VentanaMensajes!=NULL;
  }


  // Función de la biblioteca CDAudioLib para atender los mensajes de la
  // ventana que sustituye a la función original. Si el mensaje es del
  // dispositivo se ejecuta la función de atención en caso de que se
  // haya definido en la petición asíncrona. Cuando el mensaje no es
  // del dispositivo llama a la función de atención original de la ventana.
  LRESULT CALLBACK CDAudioMensajesProcedure (HWND hwnd, UINT message,
                                             WPARAM wParam, LPARAM lParam) {
    if(message == MM_MCINOTIFY) {
        if(rcCDAudio.intProcedure != NULL) {
            rcCDAudio.intProcedure(wParam);
        }
        return 0;
    }
    return CallWindowProc(rcCDAudio.wpOriginal,hwnd,message,wParam,lParam);
  }

  // Función de atención de los mensajes de ventana de la ventana que se crea
  // cuando no se puede sustituir la función original.
  LRESULT CALLBACK CDAudioWindowProcedure (HWND hwnd, UINT message,
                                           WPARAM wParam, LPARAM lParam) {
    switch(message) {
        case WM_PAINT:
             return 0;
        case MM_MCINOTIFY:
            if(rcCDAudio.intProcedure != NULL) {
                rcCDAudio.intProcedure(wParam);
            }
            return 0;
        case WM_DESTROY:
            PostQuitMessage (0);
            return 0;
        default:
            return DefWindowProc(hwnd,message,wParam,lParam);
    }
  }

  // Hilo de ejecución (thread) que se crea cuando no se puede sustituir la
  // función original de atención a los mensajes de ventana por la de la
  // biblioteca. Crea una ventana de mensajes (ventana oculta que solo sirve
  // para recibir mensajes) y procesa los mensajes hasta que recibe el mensaje
  // WM_DESTROY (que envía a la ventana la función DestruirRecursos()). Una vez
  // recibido el mensaje WM_DESTROY destruye la ventana y finaliza.
  DWORD WINAPI ThreadMensajes(LPVOID lpParameter) {
    PRECURSOSCDAUDIO prcCDAudio = (PRECURSOSCDAUDIO) lpParameter;
    MSG msg;
    HINSTANCE hinst;
    bool quit = false;

    hinst = (HINSTANCE) GetClassLong(prcCDAudio->VentanaMensajes,GCL_HMODULE);
    if(hinst == NULL) {
        prcCDAudio->VentanaMensajes = NULL;
        return 1;
    }
    if(!CrearClaseVentanaMensajes(hinst,CDAudioWindowProcedure)) {
        prcCDAudio->VentanaMensajes = NULL;
        return 2;
    }
    prcCDAudio->VentanaMensajes = CrearVentanaMensajes(hinst,HWND_MESSAGE);
    HWND hwnd = prcCDAudio->VentanaMensajes;
    if(hwnd == NULL) {
        UnregisterClass("VentanaMensajesClass",hinst);
        prcCDAudio->VentanaMensajes = NULL;
        return 3;
    }
    SendMessage(hwnd, WM_PAINT, (LPARAM)prcCDAudio, 1);
    while(GetMessage(&msg,NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    bool ok = DestroyWindow(prcCDAudio->VentanaMensajes);
    if(ok) {
        ok = UnregisterClass("VentanaMensajesClass",0);
        CloseHandle(prcCDAudio->VentanaMensajes);
        prcCDAudio->VentanaMensajes = NULL;
        prcCDAudio->wpOriginal = NULL;
        prcCDAudio->wpCDAudio = NULL;
    }
    return 0;
  }

  // Función que libera los recursos utilizados. Devuelve un valor lógico que
  // indica si se han podido liberar los recursos.
  bool DestruirRecursos(PRECURSOSCDAUDIO prcCDAudio) {
    // Si no hay ventana no hay recursos a liberar.
    if(prcCDAudio->VentanaMensajes == NULL) return false;
    // Si se había sustituido la función de atención original se reestablece,
    // se ponen las variables a NULL y se devuelve true.
    if(prcCDAudio->wpOriginal != NULL) {
        WNDPROC aux = (WNDPROC)SetWindowLongPtr(prcCDAudio->VentanaMensajes,
                                                GWL_WNDPROC,
                                                (LONG)prcCDAudio->wpOriginal);
        if (aux == NULL) return false;
        prcCDAudio->VentanaMensajes = NULL;
        prcCDAudio->wpOriginal = NULL;
        prcCDAudio->wpCDAudio = NULL;
        return true;
    }
    // Si no se había podido sustituir la función de atención se envia el
    // mensaje WM_DESTROY a la ventana creada por el thread.
    PostMessage(prcCDAudio->VentanaMensajes,WM_DESTROY,0,0);
    // Se espera a que finalize el thread y se devuelve si éste a podido
    // destruir la ventana.
    WaitForSingleObject(prcCDAudio->hThread,INFINITE);
    return prcCDAudio->VentanaMensajes == NULL;
  }

  // Funciones para crear una clase ventana y una ventana. Ver la API de
  // windows para consultar el significado de cada atributo.
  inline bool CrearClaseVentanaMensajes(HINSTANCE hinstance, WNDPROC wp) {
    WNDCLASSEX wincl;
    wincl.lpszClassName = "VentanaMensajesClass";
    wincl.lpfnWndProc = wp;
    wincl.hInstance = hinstance;
    wincl.hCursor = NULL;
    wincl.hIcon = NULL;
    wincl.hbrBackground = NULL;
    wincl.lpszMenuName = NULL;
    wincl.style = 0;
    wincl.cbSize = sizeof (WNDCLASSEX);
    wincl.hIconSm = NULL;
    wincl.cbClsExtra = 0;
    wincl.cbWndExtra = 0;
    return RegisterClassEx (&wincl);
  }


  inline HWND CrearVentanaMensajes(HINSTANCE hinstance, HWND hwndParentWindow) {
    HWND hwnd = CreateWindowEx (
           0,
           "VentanaMensajesClass",
           NULL,
           WS_OVERLAPPEDWINDOW,
           0,
           0,
           0,
           0,
           hwndParentWindow,
           NULL,
           hinstance,
           NULL
    );
    return hwnd;
  }