#include "hbclass.ch"
#include "Xailer.ch"
#include "WinINet.api"
#include "hbcurl.ch"

REQUEST HB_CODEPAGE_ESWIN

#define TAPI_LLDOMINUS_VERSION "20260803-designer-native-close-1"
#define ENCODING_WINDOWS_1252  "WINDOWS-1252"
#define ENCODING_UTF8          "UTF-8"

CREATE CLASS TApiDisenadorLLDominus

   DATA cHost         INIT ""
   DATA cBasePath     INIT ""
   DATA lSecure       INIT .T.
   DATA cApiKey       INIT ""
   DATA cSession      INIT ""
   DATA cDesignerUrl  INIT ""
   DATA cLastError    INIT ""
   DATA nLastHttpCode INIT 0
   DATA hLastResponse INIT {=>}
   DATA lDebug        INIT .F.
   DATA cInputEncoding  INIT ENCODING_WINDOWS_1252
   DATA cOutputEncoding INIT ENCODING_WINDOWS_1252
   DATA cEncodingError  INIT ""

   METHOD New( cHost, cBasePath, lSecure, cApiKey ) CONSTRUCTOR
   METHOD Create()
   METHOD End()

   METHOD CrearSesion( uTemplate, hVariables, cFileName, nTtlSeconds )
   METHOD ConvertirLL12( cContenido, cNombreOrigen )
   METHOD AbrirDisenador()
   METHOD SetDebug( lDebug )
   METHOD SetEncodingWIN()
   METHOD SetEncodingUTF8()
   METHOD SetEncoding( cInputEncoding, cOutputEncoding )
   METHOD SetInputEncoding( cEncoding )
   METHOD SetOutputEncoding( cEncoding )
   METHOD ConsultarEstado()
   METHOD ObtenerResultado()
   METHOD GuardarResultado( cFileName )
   METHOD CerrarSesion()

PROTECTED:
   METHOD Request( cAction, cMethod, hPayload, hResponse )
   METHOD RequestCurl( cAction, cMethod, hPayload, hResponse )
   METHOD NormalizarUtf8( uValor, nConversiones )
   METHOD EsAscii( cValor )
   METHOD NormalizarEncoding( cEncoding )
   METHOD ConvertirEntradaUtf8( cContenido )
   METHOD ConvertirSalidaRecursiva( uValor )
   METHOD PrepararVariablesDisenador( hVariables )
   METHOD PrepararImagenesLocales( uValor, oImageUtil )
   METHOD RespuestaConversionError( cError )
   METHOD NormalizarRespuestaConversion( hResponse )
   METHOD SepararPlantillaFisica( cContenido )
   METHOD ComponerPlantillaFisica( cJson, cUserSection, lEnsureUserSection )

ENDCLASS

//------------------------------------------------------------------------------

METHOD New( cHost, cBasePath, lSecure, cApiKey ) CLASS TApiDisenadorLLDominus

   ::cHost         := ""
   ::cBasePath     := ""
   ::lSecure       := .T.
   ::cApiKey       := ""
   ::cSession      := ""
   ::cDesignerUrl  := ""
   ::cLastError    := ""
   ::nLastHttpCode := 0
   ::hLastResponse := {=>}
   ::lDebug        := .F.
   ::SetEncodingWIN()

   IF HB_ISCHAR( cHost )
      ::cHost := AllTrim( cHost )
   ENDIF

   IF HB_ISCHAR( cBasePath )
      ::cBasePath := AllTrim( cBasePath )
   ENDIF

   IF HB_ISLOGICAL( lSecure )
      ::lSecure := lSecure
   ENDIF

   IF HB_ISCHAR( cApiKey )
      ::cApiKey := AllTrim( cApiKey )
   ENDIF

RETURN Self

//------------------------------------------------------------------------------

METHOD Create() CLASS TApiDisenadorLLDominus

   IF Empty( ::cHost )
      ::cHost := "servidorcvs.duckdns.org"
   ENDIF

   IF Empty( ::cBasePath )
      ::cBasePath := "/LL-Dominus/public/apiLLDominus.php"
   ENDIF

   IF Left( ::cBasePath, 1 ) != "/"
      ::cBasePath := "/" + ::cBasePath
   ENDIF

RETURN Self

//------------------------------------------------------------------------------

METHOD End() CLASS TApiDisenadorLLDominus

   ::cSession      := ""
   ::cDesignerUrl  := ""
   ::cLastError    := ""
   ::nLastHttpCode := 0
   ::hLastResponse := {=>}

RETURN NIL

//------------------------------------------------------------------------------

METHOD CrearSesion( uTemplate, hVariables, cFileName, nTtlSeconds ) CLASS TApiDisenadorLLDominus

   LOCAL hResponse         := {=>}
   LOCAL hPayload          := {=>}
   LOCAL hTemplate         := {=>}
   LOCAL hPartes
   LOCAL cUserSection      := ""
   LOCAL cContenidoFisico
   LOCAL cApiFileName
   LOCAL uJsonResult

   DEFAULT hVariables TO {=>}
   DEFAULT cFileName TO ""
   DEFAULT nTtlSeconds TO 14400

   IF HB_ISHASH( uTemplate )
      hTemplate := uTemplate
      IF ! Empty( cFileName ) .AND. File( cFileName )
         cContenidoFisico := hb_MemoRead( cFileName )
         IF HB_ISSTRING( cContenidoFisico )
            hPartes       := ::SepararPlantillaFisica( cContenidoFisico )
            cUserSection  := hPartes[ "cUserSection" ]
         ENDIF
      ENDIF
   ELSEIF HB_ISSTRING( uTemplate )
      hPartes       := ::SepararPlantillaFisica( uTemplate )
      cUserSection  := hPartes[ "cUserSection" ]
      uJsonResult   := hb_jsonDecode( hPartes[ "cJson" ], @hTemplate )
      IF uJsonResult == NIL .OR. ! HB_ISHASH( hTemplate )
         ::cLastError := "El contenido no contiene una plantilla LL-Dominus valida."
         RETURN .F.
      ENDIF
   ELSE
      ::cLastError := "La plantilla debe ser un hash Harbour o contenido fisico."
      RETURN .F.
   ENDIF

   IF ! HB_ISHASH( hVariables )
      ::cLastError := "Las variables no son un hash Harbour."
      RETURN .F.
   ENDIF

   hVariables := ::PrepararVariablesDisenador( hVariables )

   IF Empty( cFileName ) .AND. hb_HHasKey( hTemplate, "cFileName" )
      cFileName := hTemplate[ "cFileName" ]
   ENDIF

   cApiFileName := hb_FNameNameExt( cFileName )

   hPayload := { ;
      "cFileName"   => cApiFileName, ;
      "hTemplate"   => hTemplate, ;
      "hVariables"  => hVariables, ;
      "nTtlSeconds" => nTtlSeconds ;
   }

   IF ! Empty( cUserSection )
      hPayload[ "cUserSectionBase64" ] := hb_base64Encode( cUserSection )
   ENDIF

   IF ! ::RequestCurl( "create", "POST", hPayload, @hResponse )
      RETURN .F.
   ENDIF

   ::cSession      := ""
   ::cDesignerUrl  := ""
   ::hLastResponse := hResponse

   IF hb_HHasKey( hResponse, "cSession" )
      ::cSession := hResponse[ "cSession" ]
   ENDIF

   IF hb_HHasKey( hResponse, "cDesignerUrl" )
      ::cDesignerUrl := hResponse[ "cDesignerUrl" ]
   ENDIF

   IF Empty( ::cSession ) .OR. Empty( ::cDesignerUrl )
      ::cLastError := "La API no ha devuelto la sesion o la URL del disenador."
      RETURN .F.
   ENDIF

RETURN .T.

//------------------------------------------------------------------------------

METHOD ConvertirLL12( cContenido, cNombreOrigen ) CLASS TApiDisenadorLLDominus

   LOCAL hResponse := {=>}
   LOCAL hPayload
   LOCAL hResultado
   LOCAL hPlantilla := {=>}
   LOCAL cPlantillaUtf8
   LOCAL cPlantillaFisica
   LOCAL cUserSection   := ""
   LOCAL uJsonResult
   LOCAL lPlantillaValida

   IF ! HB_ISSTRING( cContenido ) .OR. ! HB_ISSTRING( cNombreOrigen )
      ::cLastError := "El contenido y el nombre de origen deben ser cadenas."
      RETURN ::ConvertirSalidaRecursiva( ::RespuestaConversionError( ::cLastError ) )
   ENDIF

   hPayload := { ;
      "cContenidoBase64" => hb_base64Encode( cContenido ), ;
      "cNombreOrigen"    => ::ConvertirEntradaUtf8( cNombreOrigen ) ;
   }

   IF ! ::RequestCurl( "convertll12", "POST", hPayload, @hResponse ) .AND. Len( hResponse ) == 0
      hResponse := ::RespuestaConversionError( ::cLastError )
   ENDIF

   hResultado := ::NormalizarRespuestaConversion( hResponse )
   cPlantillaUtf8 := iif( HB_ISSTRING( hResultado[ "cPlantilla" ] ), hResultado[ "cPlantilla" ], "" )

   IF hResultado[ "lOk" ]
      uJsonResult := hb_jsonDecode( cPlantillaUtf8, @hPlantilla )
      lPlantillaValida := ( ::EsAscii( cPlantillaUtf8 ) .OR. hb_StrIsUTF8( cPlantillaUtf8 ) ) .AND. ;
         Left( cPlantillaUtf8, 3 ) != Chr( 239 ) + Chr( 187 ) + Chr( 191 ) .AND. ;
         uJsonResult != NIL .AND. ;
         HB_ISHASH( hPlantilla ) .AND. ;
         hb_HHasKey( hPlantilla, "cFormatVersion" ) .AND. ;
         ! Empty( hPlantilla[ "cFormatVersion" ] ) .AND. ;
         hb_HHasKey( hPlantilla, "aObjects" ) .AND. ;
         HB_ISARRAY( hPlantilla[ "aObjects" ] )

      IF ! lPlantillaValida
         hResultado[ "lOk" ]    := .F.
         hResultado[ "cError" ] := "La API ha devuelto una plantilla LL-Dominus no valida."
      ENDIF
   ENDIF

   hResultado[ "cPlantilla" ] := ""
   hResultado[ "cPlantillaFisica" ] := ""
   hResultado := ::ConvertirSalidaRecursiva( hResultado )
   hResultado[ "cPlantilla" ] := cPlantillaUtf8
   IF hResultado[ "lOk" ]
      IF HB_ISSTRING( hResultado[ "cUserSectionBase64" ] ) .AND. ;
         ! Empty( hResultado[ "cUserSectionBase64" ] )
         cUserSection := hb_base64Decode( hResultado[ "cUserSectionBase64" ] )
      ENDIF
      cPlantillaFisica := ::ComponerPlantillaFisica( cPlantillaUtf8, cUserSection )
      hResultado[ "cPlantillaFisica" ] := cPlantillaFisica
   ENDIF
   ::hLastResponse := hResultado
   IF ! hResultado[ "lOk" ]
      ::cLastError := hResultado[ "cError" ]
   ENDIF

RETURN hResultado

//------------------------------------------------------------------------------

METHOD AbrirDisenador() CLASS TApiDisenadorLLDominus

   LOCAL nResult

   IF Empty( ::cDesignerUrl )
      ::cLastError := "No hay una sesion de diseno activa."
      RETURN .F.
   ENDIF

   IF XA_IsWin7()
      nResult := LLDOpenDesignerSupermiumWindow( ::cDesignerUrl )

      IF nResult == -3
         ::cLastError := "LL-Dominus necesita Supermium para abrir el diseñador en Windows 7."
         RETURN .F.
      ENDIF

      IF nResult == -1
         ::cLastError := "Windows no ha podido iniciar Supermium."
         RETURN .F.
      ENDIF

      IF nResult == -2
         ::cLastError := "Supermium se ha iniciado, pero no se ha encontrado la ventana del diseñador."
         RETURN .F.
      ENDIF

      RETURN .T.
   ENDIF

   nResult := LLDOpenDesignerAppWindow( ::cDesignerUrl )

   IF nResult == -1
      ::cLastError := "Windows no ha podido iniciar Microsoft Edge."
      RETURN .F.
   ENDIF

   IF nResult == -2
      ::cLastError := "Microsoft Edge se ha iniciado, pero no se ha encontrado la ventana del disenador."
      RETURN .F.
   ENDIF

RETURN .T.

//------------------------------------------------------------------------------

METHOD SetDebug( lDebug ) CLASS TApiDisenadorLLDominus

   ::lDebug := HB_ISLOGICAL( lDebug ) .AND. lDebug

RETURN Self

//------------------------------------------------------------------------------

METHOD SetEncodingWIN() CLASS TApiDisenadorLLDominus

   ::cInputEncoding  := ENCODING_WINDOWS_1252
   ::cOutputEncoding := ENCODING_WINDOWS_1252
   ::cEncodingError  := ""

RETURN Self

//------------------------------------------------------------------------------

METHOD SetEncodingUTF8() CLASS TApiDisenadorLLDominus

   ::cInputEncoding  := ENCODING_UTF8
   ::cOutputEncoding := ENCODING_UTF8
   ::cEncodingError  := ""

RETURN Self

//------------------------------------------------------------------------------

METHOD SetEncoding( cInputEncoding, cOutputEncoding ) CLASS TApiDisenadorLLDominus

   LOCAL cInput  := ::NormalizarEncoding( cInputEncoding )
   LOCAL cOutput := ::NormalizarEncoding( cOutputEncoding )

   IF Len( cInput ) == 0 .OR. Len( cOutput ) == 0
      ::cEncodingError := "Codificacion no admitida. Use WINDOWS-1252, CP1252, WIN, UTF-8 o UTF8."
      RETURN .F.
   ENDIF

   ::cInputEncoding  := cInput
   ::cOutputEncoding := cOutput
   ::cEncodingError  := ""

RETURN Self

//------------------------------------------------------------------------------

METHOD SetInputEncoding( cEncoding ) CLASS TApiDisenadorLLDominus

   LOCAL cInput := ::NormalizarEncoding( cEncoding )

   IF Len( cInput ) == 0
      ::cEncodingError := "Codificacion de entrada no admitida: " + AllTrim( hb_ValToStr( cEncoding ) )
      RETURN .F.
   ENDIF

   ::cInputEncoding := cInput
   ::cEncodingError := ""

RETURN Self

//------------------------------------------------------------------------------

METHOD SetOutputEncoding( cEncoding ) CLASS TApiDisenadorLLDominus

   LOCAL cOutput := ::NormalizarEncoding( cEncoding )

   IF Len( cOutput ) == 0
      ::cEncodingError := "Codificacion de salida no admitida: " + AllTrim( hb_ValToStr( cEncoding ) )
      RETURN .F.
   ENDIF

   ::cOutputEncoding := cOutput
   ::cEncodingError  := ""

RETURN Self

//------------------------------------------------------------------------------

METHOD ConsultarEstado() CLASS TApiDisenadorLLDominus

   LOCAL hResponse := {=>}

   IF ! ::RequestCurl( "status", "GET", {=>}, @hResponse )
      RETURN NIL
   ENDIF

   ::hLastResponse := hResponse

RETURN hResponse

//------------------------------------------------------------------------------

METHOD ObtenerResultado() CLASS TApiDisenadorLLDominus

   LOCAL hResponse := {=>}

   IF ! ::Request( "result", "GET", {=>}, @hResponse )
      RETURN NIL
   ENDIF

   ::hLastResponse := hResponse

RETURN hResponse

//------------------------------------------------------------------------------

METHOD GuardarResultado( cFileName ) CLASS TApiDisenadorLLDominus

   LOCAL hResponse    := ::ObtenerResultado()
   LOCAL cJson
   LOCAL cUserSection := ""
   LOCAL cContenido

   IF ! HB_ISHASH( hResponse )
      RETURN .F.
   ENDIF

   IF ! hb_HHasKey( hResponse, "cStatus" ) .OR. hResponse[ "cStatus" ] != "saved"
      ::cLastError := "La plantilla todavia no se ha guardado en el disenador."
      RETURN .F.
   ENDIF

   IF ! hb_HHasKey( hResponse, "hTemplate" ) .OR. ! HB_ISHASH( hResponse[ "hTemplate" ] )
      ::cLastError := "La API no ha devuelto una plantilla valida."
      RETURN .F.
   ENDIF

   cJson := hb_jsonEncode( hResponse[ "hTemplate" ], .T. )
   IF hb_HHasKey( hResponse, "cUserSectionBase64" ) .AND. ;
      HB_ISSTRING( hResponse[ "cUserSectionBase64" ] ) .AND. ;
      ! Empty( hResponse[ "cUserSectionBase64" ] )
      cUserSection := hb_base64Decode( hResponse[ "cUserSectionBase64" ] )
   ENDIF

   cContenido := ::ComponerPlantillaFisica( cJson, cUserSection )
   IF ! HB_MemoWrit( cFileName, cContenido, .F. )
      ::cLastError := "No se ha podido escribir la plantilla: " + cFileName
      RETURN .F.
   ENDIF

RETURN .T.

//------------------------------------------------------------------------------

METHOD CerrarSesion() CLASS TApiDisenadorLLDominus

   LOCAL hResponse := {=>}
   LOCAL lOk

   IF Empty( ::cSession )
      RETURN .T.
   ENDIF

   lOk := ::Request( "delete", "POST", {=>}, @hResponse )
   IF lOk
      ::cSession      := ""
      ::cDesignerUrl  := ""
      ::hLastResponse := hResponse
   ENDIF

RETURN lOk

//------------------------------------------------------------------------------

METHOD Request( cAction, cMethod, hPayload, hResponse ) CLASS TApiDisenadorLLDominus

   LOCAL oInternet
   LOCAL hRequest       := 0
   LOCAL hRequestData   := {=>}
   LOCAL cRequestPath   := ::cBasePath + "?action=" + cAction
   LOCAL cRequestBody   := ""
   LOCAL cResponse      := ""
   LOCAL cBuffer        := Space( 65536 )
   LOCAL cResponseStart := ""
   LOCAL nRequestFlags  := INTERNET_FLAG_KEEP_CONNECTION
   LOCAL nHttpCode      := 0
   LOCAL uJsonResult
   LOCAL lInternetOpen
   LOCAL lConnected
   LOCAL lSent
   LOCAL lStatusRead
   LOCAL lPayloadUtf8   := .T.
   LOCAL nConversiones  := 0
   LOCAL lOk            := .F.
   LOCAL lRequestOpened := .F.

   hResponse          := {=>}
   ::cLastError       := ""
   ::nLastHttpCode    := 0
   cMethod            := Upper( AllTrim( cMethod ) )

   IF ::lSecure
      nRequestFlags := nOr( nRequestFlags, INTERNET_FLAG_SECURE )
   ENDIF

   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.Request.INICIO", ;
         TAPI_LLDOMINUS_VERSION, ;
         cMethod, ;
         cRequestPath, ;
         "HTTPS", ;
         ::lSecure, ;
         "FLAGS", ;
         nRequestFlags ;
      )
   ENDIF

   IF Empty( ::cSession ) .AND. cAction != "create" .AND. cAction != "convertll12"
      ::cLastError := "No hay una sesion de diseno activa."
      RETURN .F.
   ENDIF

   IF cMethod == "GET"
      IF ! Empty( ::cSession )
         cRequestPath += "&cSession=" + ::cSession
      ENDIF
   ELSE
      IF HB_ISHASH( hPayload )
         hRequestData := ::NormalizarUtf8( hPayload, @nConversiones )
      ENDIF
      IF cAction != "convertll12" .AND. ! Empty( ::cSession ) .AND. ! hb_HHasKey( hRequestData, "cSession" )
         hRequestData[ "cSession" ] := ::cSession
      ENDIF
      cRequestBody := hb_jsonEncode( hRequestData, .F. )
      lPayloadUtf8 := ::EsAscii( cRequestBody ) .OR. hb_StrIsUTF8( cRequestBody )
   ENDIF

   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.Request.PAYLOAD", ;
         "BYTES", ;
         Len( cRequestBody ), ;
         "TIPO", ;
         ValType( cRequestBody ), ;
         "UTF8", ;
         lPayloadUtf8, ;
         "CONVERSIONES", ;
         nConversiones ;
      )
   ENDIF

   IF ! lPayloadUtf8
      ::cLastError := "No se ha podido convertir la peticion a UTF-8."
      IF ::lDebug
         LogDebug( ;
            "TApiDisenadorLLDominus.Request.PAYLOAD_INVALIDO", ;
            ::cLastError ;
         )
      ENDIF
      RETURN .F.
   ENDIF

   oInternet       := TInternet():Create()
   oInternet:nPort := iif( ::lSecure, inetHTTPS, inetHTTP )

   lInternetOpen := oInternet:Open()
   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.Request.OPEN", ;
         lInternetOpen, ;
         "ERROR", ;
         oInternet:nLastError ;
      )
   ENDIF

   IF ! lInternetOpen
      ::cLastError := "No se ha podido iniciar WinINet."
   ELSE
      lConnected := oInternet:Connect( ::cHost )
      IF ::lDebug
         LogDebug( ;
            "TApiDisenadorLLDominus.Request.CONNECT", ;
            lConnected, ;
            ::cHost, ;
            "PORT", ;
            oInternet:nPort, ;
            "ERROR", ;
            oInternet:nLastError ;
         )
      ENDIF

      IF ! lConnected
         ::cLastError := "No se ha podido conectar con " + ::cHost + "."
      ELSE
         hRequest := oInternet:OpenRequest( cMethod, cRequestPath, nRequestFlags )
         IF ::lDebug
            LogDebug( ;
               "TApiDisenadorLLDominus.Request.OPEN_REQUEST", ;
               hRequest, ;
               "ERROR", ;
               oInternet:nLastError ;
            )
         ENDIF

         IF Empty( hRequest )
            ::cLastError := "No se ha podido crear la peticion HTTP."
         ELSE
            lRequestOpened := .T.
            oInternet:AddRequestHeader( hRequest, "Accept: application/json" + CRLF )
            IF cMethod != "GET"
               oInternet:AddRequestHeader( hRequest, "Content-Type: application/json; charset=utf-8" + CRLF )
            ENDIF
            IF ! Empty( ::cApiKey )
               oInternet:AddRequestHeader( hRequest, "X-LLDominus-Key: " + ::cApiKey + CRLF )
            ENDIF

            lSent := oInternet:SendRequest( hRequest, , cRequestBody )
            IF ::lDebug
               LogDebug( ;
                  "TApiDisenadorLLDominus.Request.SEND", ;
                  lSent, ;
                  "ERROR", ;
                  oInternet:nLastError ;
               )
            ENDIF

            IF ! lSent
               ::cLastError := "La API no ha aceptado la peticion HTTP."
            ELSE
               lStatusRead := oInternet:QueryInfo( ;
                  hRequest, ;
                  nOr( HTTP_QUERY_STATUS_CODE, HTTP_QUERY_FLAG_NUMBER ), ;
                  @nHttpCode ;
               )
               ::nLastHttpCode := nHttpCode

               IF ::lDebug
                  LogDebug( ;
                     "TApiDisenadorLLDominus.Request.STATUS", ;
                     lStatusRead, ;
                     ::nLastHttpCode, ;
                     "ERROR", ;
                     oInternet:nLastError ;
                  )
               ENDIF

               DO WHILE oInternet:ReadFile( hRequest, @cBuffer, Len( cBuffer ) )
                  cResponse += cBuffer
                  cBuffer := Space( 65536 )
               ENDDO

               IF ::lDebug
                  LogDebug( ;
                     "TApiDisenadorLLDominus.Request.RESPONSE", ;
                     "BYTES", ;
                     Len( cResponse ), ;
                     "ERROR", ;
                     oInternet:nLastError ;
                  )
               ENDIF

               IF Empty( cResponse )
                  ::cLastError := "La API ha devuelto una respuesta vacia."
               ELSE
                  uJsonResult := hb_jsonDecode( cResponse, @hResponse )
                  IF ::lDebug
                     LogDebug( ;
                        "TApiDisenadorLLDominus.Request.JSON", ;
                        "RESULTADO", ;
                        uJsonResult, ;
                        "TIPO", ;
                        ValType( hResponse ), ;
                        "HASH", ;
                        HB_ISHASH( hResponse ) ;
                     )
                  ENDIF

                  IF ! HB_ISHASH( hResponse )
                     cResponseStart := Left( AllTrim( cResponse ), 300 )
                     IF ::lDebug
                        LogDebug( ;
                           "TApiDisenadorLLDominus.Request.RESPONSE_INVALIDA", ;
                           cResponseStart ;
                        )
                     ENDIF
                     ::cLastError := "La respuesta de la API no es JSON valido."
                     IF ::nLastHttpCode > 0
                        ::cLastError += " HTTP " + AllTrim( Str( ::nLastHttpCode ) ) + "."
                     ENDIF
                     IF ! Empty( cResponseStart )
                        ::cLastError += hb_eol() + cResponseStart
                     ENDIF
                  ELSEIF hb_HHasKey( hResponse, "lOk" ) .AND. ! hResponse[ "lOk" ]
                     ::cLastError := "Error de la API."
                     IF hb_HHasKey( hResponse, "cError" ) .AND. ! Empty( hResponse[ "cError" ] )
                        ::cLastError := hResponse[ "cError" ]
                     ELSEIF hb_HHasKey( hResponse, "cMessage" )
                        ::cLastError := hResponse[ "cMessage" ]
                     ENDIF
                     IF ::lDebug
                        LogDebug( ;
                           "TApiDisenadorLLDominus.Request.API_ERROR", ;
                           ::cLastError ;
                        )
                     ENDIF
                  ELSE
                     lOk := .T.
                  ENDIF
               ENDIF
            ENDIF
         ENDIF
      ENDIF
   ENDIF

   IF lRequestOpened
      oInternet:CloseRequest( hRequest )
   ENDIF
   oInternet:Destroy()

   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.Request.FIN", ;
         lOk, ;
         "HTTP", ;
         ::nLastHttpCode, ;
         "ERROR", ;
         ::cLastError ;
      )
   ENDIF

RETURN lOk

//------------------------------------------------------------------------------

METHOD RequestCurl( cAction, cMethod, hPayload, hResponse ) CLASS TApiDisenadorLLDominus

   LOCAL hCurl
   LOCAL hRequestData   := {=>}
   LOCAL aHeaders       := { "Accept: application/json" }
   LOCAL cUrl           := iif( ::lSecure, "https://", "http://" ) + ::cHost + ::cBasePath + "?action=" + cAction
   LOCAL cRequestBody   := ""
   LOCAL cResponse      := ""
   LOCAL cResponseStart := ""
   LOCAL nCurlError     := 0
   LOCAL nConversiones  := 0
   LOCAL lPayloadUtf8   := .T.
   LOCAL lOk            := .F.
   LOCAL uJsonResult

   hResponse        := {=>}
   ::cLastError     := ""
   ::nLastHttpCode  := 0
   cMethod          := Upper( AllTrim( cMethod ) )

   IF Empty( ::cSession ) .AND. cAction != "create" .AND. cAction != "convertll12"
      ::cLastError := "No hay una sesion de diseno activa."
      RETURN .F.
   ENDIF

   IF cMethod == "GET"
      IF ! Empty( ::cSession )
         cUrl += "&cSession=" + ::cSession
      ENDIF
   ELSE
      IF HB_ISHASH( hPayload )
         hRequestData := ::NormalizarUtf8( hPayload, @nConversiones )
      ENDIF
      IF cAction != "convertll12" .AND. ! Empty( ::cSession ) .AND. ! hb_HHasKey( hRequestData, "cSession" )
         hRequestData[ "cSession" ] := ::cSession
      ENDIF
      cRequestBody := hb_jsonEncode( hRequestData, .F. )
      lPayloadUtf8 := ::EsAscii( cRequestBody ) .OR. hb_StrIsUTF8( cRequestBody )
      AAdd( aHeaders, "Content-Type: application/json; charset=utf-8" )
   ENDIF

   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.RequestCurl.INICIO", ;
         TAPI_LLDOMINUS_VERSION, ;
         cMethod, ;
         cUrl, ;
         "PAYLOAD_BYTES", ;
         Len( cRequestBody ), ;
         "UTF8", ;
         lPayloadUtf8, ;
         "CONVERSIONES", ;
         nConversiones ;
      )
   ENDIF

   IF ! lPayloadUtf8
      ::cLastError := "No se ha podido convertir la peticion a UTF-8."
      RETURN .F.
   ENDIF

   IF ! Empty( ::cApiKey )
      AAdd( aHeaders, "X-LLDominus-Key: " + ::cApiKey )
   ENDIF

   hCurl := curl_easy_init()
   IF Empty( hCurl )
      ::cLastError := "No se ha podido iniciar libcurl."
      RETURN .F.
   ENDIF

   curl_easy_setopt( hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
   curl_easy_setopt( hCurl, HB_CURLOPT_URL, cUrl )
   curl_easy_setopt( hCurl, HB_CURLOPT_TIMEOUT, 15 )
   curl_easy_setopt( hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )
   curl_easy_setopt( hCurl, HB_CURLOPT_SSL_VERIFYHOST, .F. )
   curl_easy_setopt( hCurl, HB_CURLOPT_DL_BUFF_SETUP )
   curl_easy_setopt( hCurl, HB_CURLOPT_VERBOSE, .F. )
   curl_easy_setopt( hCurl, HB_CURLOPT_FRESH_CONNECT, .T. )
   curl_easy_setopt( hCurl, HB_CURLOPT_FORBID_REUSE, .T. )

   IF cMethod == "GET"
      curl_easy_setopt( hCurl, HB_CURLOPT_HTTPGET, .T. )
   ELSE
      curl_easy_setopt( hCurl, HB_CURLOPT_POST, .T. )
      curl_easy_setopt( hCurl, HB_CURLOPT_POSTFIELDS, cRequestBody )
   ENDIF

   nCurlError := curl_easy_perform( hCurl )
   ::nLastHttpCode := curl_easy_getinfo( hCurl, HB_CURLINFO_RESPONSE_CODE )
   cResponse := curl_easy_dl_buff_get( hCurl )

   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.RequestCurl.RESULTADO", ;
         "CURL", ;
         nCurlError, ;
         "HTTP", ;
         ::nLastHttpCode, ;
         "BYTES", ;
         Len( cResponse ) ;
      )
   ENDIF

   IF nCurlError != 0
      ::cLastError := curl_easy_strerror( nCurlError )
      IF ::lDebug
         LogDebug( ;
            "TApiDisenadorLLDominus.RequestCurl.TRANSPORTE", ;
            "CURL", ;
            nCurlError, ;
            ::cLastError ;
         )
      ENDIF
   ELSEIF Empty( cResponse )
      ::cLastError := "La API ha devuelto una respuesta vacia."
   ELSE
      uJsonResult := hb_jsonDecode( cResponse, @hResponse )
      IF uJsonResult == NIL .OR. ! HB_ISHASH( hResponse )
         cResponseStart := Left( AllTrim( cResponse ), 300 )
         ::cLastError := "La respuesta de la API no es JSON valido."
         IF ::nLastHttpCode > 0
            ::cLastError += " HTTP " + AllTrim( Str( ::nLastHttpCode ) ) + "."
         ENDIF
         IF ! Empty( cResponseStart )
            ::cLastError += hb_eol() + cResponseStart
         ENDIF
      ELSEIF hb_HHasKey( hResponse, "lOk" ) .AND. ! hResponse[ "lOk" ]
         ::cLastError := "Error de la API."
         IF hb_HHasKey( hResponse, "cError" ) .AND. ! Empty( hResponse[ "cError" ] )
            ::cLastError := hResponse[ "cError" ]
         ELSEIF hb_HHasKey( hResponse, "cMessage" )
            ::cLastError := hResponse[ "cMessage" ]
         ENDIF
      ELSE
         lOk := .T.
      ENDIF
   ENDIF

   curl_easy_cleanup( hCurl )

   IF ::lDebug
      LogDebug( ;
         "TApiDisenadorLLDominus.RequestCurl.FIN", ;
         lOk, ;
         "HTTP", ;
         ::nLastHttpCode, ;
         "ERROR", ;
         ::cLastError ;
      )
   ENDIF

RETURN lOk

//------------------------------------------------------------------------------

METHOD NormalizarUtf8( uValor, nConversiones ) CLASS TApiDisenadorLLDominus

   LOCAL hResultado
   LOCAL aResultado
   LOCAL uClave
   LOCAL uClaveUtf8
   LOCAL uElemento

   DO CASE
   CASE HB_ISSTRING( uValor )
      IF ! ::EsAscii( uValor ) .AND. ! hb_StrIsUTF8( uValor )
         nConversiones++
         RETURN hb_StrToUTF8( uValor, "ESWIN" )
      ENDIF

   CASE HB_ISHASH( uValor )
      hResultado := {=>}
      FOR EACH uClave IN hb_HKeys( uValor )
         uClaveUtf8 := ::NormalizarUtf8( uClave, @nConversiones )
         hResultado[ uClaveUtf8 ] := ::NormalizarUtf8( uValor[ uClave ], @nConversiones )
      NEXT
      RETURN hResultado

   CASE HB_ISARRAY( uValor )
      aResultado := {}
      FOR EACH uElemento IN uValor
         AAdd( aResultado, ::NormalizarUtf8( uElemento, @nConversiones ) )
      NEXT
      RETURN aResultado
   ENDCASE

RETURN uValor

//------------------------------------------------------------------------------

METHOD EsAscii( cValor ) CLASS TApiDisenadorLLDominus

   LOCAL nIndex
   LOCAL nLen

   IF ! HB_ISSTRING( cValor )
      RETURN .F.
   ENDIF

   nLen := Len( cValor )
   FOR nIndex := 1 TO nLen
      IF Asc( SubStr( cValor, nIndex, 1 ) ) > 127
         RETURN .F.
      ENDIF
   NEXT

RETURN .T.

//------------------------------------------------------------------------------

METHOD NormalizarEncoding( cEncoding ) CLASS TApiDisenadorLLDominus

   LOCAL cValor := Upper( StrTran( AllTrim( iif( HB_ISSTRING( cEncoding ), cEncoding, "" ) ), "_", "-" ) )

   DO CASE
   CASE cValor == "WIN" .OR. cValor == "WINDOWS-1252" .OR. cValor == "CP1252"
      RETURN ENCODING_WINDOWS_1252
   CASE cValor == "UTF8" .OR. cValor == "UTF-8"
      RETURN ENCODING_UTF8
   ENDCASE

RETURN ""

//------------------------------------------------------------------------------

METHOD ConvertirEntradaUtf8( cContenido ) CLASS TApiDisenadorLLDominus

   IF ! HB_ISSTRING( cContenido ) .OR. ::cInputEncoding == ENCODING_UTF8
      RETURN cContenido
   ENDIF

RETURN hb_StrToUTF8( cContenido, "ESWIN" )

//------------------------------------------------------------------------------

METHOD ConvertirSalidaRecursiva( uValor ) CLASS TApiDisenadorLLDominus

   LOCAL hResultado
   LOCAL aResultado
   LOCAL uClave
   LOCAL uElemento

   DO CASE
   CASE HB_ISSTRING( uValor )
      RETURN iif( ::cOutputEncoding == ENCODING_UTF8, uValor, hb_UTF8ToStr( uValor, "ESWIN" ) )

   CASE HB_ISHASH( uValor )
      hResultado := {=>}
      FOR EACH uClave IN hb_HKeys( uValor )
         hResultado[ uClave ] := ::ConvertirSalidaRecursiva( uValor[ uClave ] )
      NEXT
      RETURN hResultado

   CASE HB_ISARRAY( uValor )
      aResultado := {}
      FOR EACH uElemento IN uValor
         AAdd( aResultado, ::ConvertirSalidaRecursiva( uElemento ) )
      NEXT
      RETURN aResultado
   ENDCASE

RETURN uValor

//------------------------------------------------------------------------------

METHOD PrepararVariablesDisenador( hVariables ) CLASS TApiDisenadorLLDominus

   LOCAL oImageUtil := TImageUtil():New()

RETURN ::PrepararImagenesLocales( hVariables, oImageUtil )

//------------------------------------------------------------------------------

METHOD PrepararImagenesLocales( uValor, oImageUtil ) CLASS TApiDisenadorLLDominus

   LOCAL hResultado
   LOCAL aResultado
   LOCAL hImagen
   LOCAL uClave
   LOCAL uElemento
   LOCAL cTipo
   LOCAL cValor
   LOCAL lLocalFile

   DO CASE
   CASE HB_ISHASH( uValor )
      hResultado := {=>}
      FOR EACH uClave IN hb_HKeys( uValor )
         hResultado[ uClave ] := ::PrepararImagenesLocales( uValor[ uClave ], oImageUtil )
      NEXT

      IF hb_HHasKey( hResultado, "tipo" ) .AND. hb_HHasKey( hResultado, "valor" )
         cTipo      := Lower( AllTrim( iif( HB_ISSTRING( hResultado[ "tipo" ] ), hResultado[ "tipo" ], "" ) ) )
         cValor     := iif( HB_ISSTRING( hResultado[ "valor" ] ), hResultado[ "valor" ], "" )
         lLocalFile := ! Empty( cValor ) .AND. ;
                       ( hb_FileExists( cValor ) .OR. hb_FileExists( StrTran( cValor, "/", hb_ps() ) ) )

         IF ( cTipo == "imagen" .OR. lLocalFile ) .AND. ;
            ! Empty( cValor ) .AND. ;
            Lower( Left( cValor, 11 ) ) != "data:image/"
            hImagen := oImageUtil:Resolver( cValor )
            IF HB_ISHASH( hImagen ) .AND. ;
               hb_HHasKey( hImagen, "dataUri" ) .AND. ;
               ! Empty( hImagen[ "dataUri" ] ) .AND. ;
               ( ! hb_HHasKey( hImagen, "error" ) .OR. Empty( hImagen[ "error" ] ) )
               hResultado[ "valorDisenoImagen" ] := hImagen[ "dataUri" ]
            ENDIF
         ENDIF
      ENDIF

      RETURN hResultado

   CASE HB_ISARRAY( uValor )
      aResultado := {}
      FOR EACH uElemento IN uValor
         AAdd( aResultado, ::PrepararImagenesLocales( uElemento, oImageUtil ) )
      NEXT
      RETURN aResultado
   ENDCASE

RETURN uValor

//------------------------------------------------------------------------------

METHOD RespuestaConversionError( cError ) CLASS TApiDisenadorLLDominus

RETURN { ;
   "lOk"                 => .F., ;
   "cPlantilla"          => "", ;
   "cPlantillaFisica"    => "", ;
   "cDocumentKind"       => "", ;
   "cExtensionPropuesta" => "", ;
   "aWarnings"           => {}, ;
   "cUserSectionBase64"  => "", ;
   "cError"              => iif( HB_ISSTRING( cError ), cError, "Error al convertir la plantilla LL12." ) ;
}

//------------------------------------------------------------------------------

METHOD NormalizarRespuestaConversion( hResponse ) CLASS TApiDisenadorLLDominus

   LOCAL hDefecto   := ::RespuestaConversionError( ::cLastError )
   LOCAL hResultado
   LOCAL cClave

   IF ! HB_ISHASH( hResponse )
      RETURN hDefecto
   ENDIF

   hResultado := hb_HClone( hResponse )
   FOR EACH cClave IN hb_HKeys( hDefecto )
      IF ! hb_HHasKey( hResultado, cClave )
         hResultado[ cClave ] := hDefecto[ cClave ]
      ENDIF
   NEXT
   IF ! HB_ISARRAY( hResultado[ "aWarnings" ] )
      hResultado[ "aWarnings" ] := {}
   ENDIF
   IF ! HB_ISLOGICAL( hResultado[ "lOk" ] )
      hResultado[ "lOk" ] := .F.
   ENDIF
   IF ! hResultado[ "lOk" ] .AND. Empty( hResultado[ "cError" ] )
      hResultado[ "cError" ] := iif( Empty( ::cLastError ), "Error al convertir la plantilla LL12.", ::cLastError )
   ENDIF

RETURN hResultado

//------------------------------------------------------------------------------

METHOD SepararPlantillaFisica( cContenido ) CLASS TApiDisenadorLLDominus

   LOCAL hPartes := { ;
      "cJson"        => cContenido, ;
      "cUserSection" => "" ;
   }
   LOCAL cMarker      := "[UserSection]"
   LOCAL nMarkerLen   := Len( cMarker )
   LOCAL nContentLen  := Len( cContenido )
   LOCAL nOffset      := 1
   LOCAL nRelative
   LOCAL nStart
   LOCAL cPrevious
   LOCAL cNext
   LOCAL lLineStart
   LOCAL lLineEnd

   IF ! HB_ISSTRING( cContenido )
      RETURN hPartes
   ENDIF

   DO WHILE nOffset <= nContentLen
      nRelative := At( cMarker, SubStr( cContenido, nOffset ) )
      IF nRelative == 0
         EXIT
      ENDIF

      nStart     := nOffset + nRelative - 1
      cPrevious  := iif( nStart > 1, SubStr( cContenido, nStart - 1, 1 ), "" )
      cNext      := iif( nStart + nMarkerLen <= nContentLen, ;
         SubStr( cContenido, nStart + nMarkerLen, 1 ), "" )
      lLineStart := nStart == 1 .OR. cPrevious == Chr( 13 ) .OR. cPrevious == Chr( 10 )
      lLineEnd   := cNext == "" .OR. cNext == Chr( 13 ) .OR. cNext == Chr( 10 )

      IF lLineStart .AND. lLineEnd
         hPartes[ "cJson" ]        := Left( cContenido, nStart - 1 )
         hPartes[ "cUserSection" ] := SubStr( cContenido, nStart )
         EXIT
      ENDIF

      nOffset := nStart + nMarkerLen
   ENDDO

RETURN hPartes

//------------------------------------------------------------------------------

METHOD ComponerPlantillaFisica( cJson, cUserSection, lEnsureUserSection ) CLASS TApiDisenadorLLDominus

   LOCAL cMarker := "[UserSection]"

   DEFAULT lEnsureUserSection TO .T.

   IF ! HB_ISSTRING( cUserSection ) .OR. Empty( cUserSection )
      RETURN iif( lEnsureUserSection, cJson + Chr( 13 ) + Chr( 10 ) + cMarker, cJson )
   ENDIF

   IF Left( cUserSection, Len( cMarker ) ) != cMarker
      cUserSection := cMarker + Chr( 13 ) + Chr( 10 ) + cUserSection
   ENDIF

RETURN cJson + Chr( 13 ) + Chr( 10 ) + cUserSection

#pragma BEGINDUMP

#include <windows.h>
#include <shellapi.h>
#include <hbapi.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#define LLD_MAX_BROWSER_WINDOWS 256

typedef struct
{
   HWND aWindows[ LLD_MAX_BROWSER_WINDOWS ];
   int nCount;
} LLD_WINDOW_SNAPSHOT;

typedef struct
{
   const LLD_WINDOW_SNAPSHOT *pBefore;
   HWND hWindow;
   HWND hFallbackWindow;
   DWORD nExpectedProcessId;
} LLD_WINDOW_SEARCH;

typedef struct
{
   HWND hWindow;
} LLD_WINDOW_WATCH;

static BOOL lld_is_edge_app_window( HWND hWindow )
{
   char cClassName[ 64 ];

   if( ! IsWindowVisible( hWindow ) || GetWindow( hWindow, GW_OWNER ) != NULL )
      return FALSE;

   if( GetClassNameA( hWindow, cClassName, sizeof( cClassName ) ) == 0 )
      return FALSE;

   return strcmp( cClassName, "Chrome_WidgetWin_1" ) == 0;
}

static BOOL CALLBACK lld_collect_edge_windows( HWND hWindow, LPARAM lParam )
{
   LLD_WINDOW_SNAPSHOT *pSnapshot = ( LLD_WINDOW_SNAPSHOT * ) lParam;

   if( lld_is_edge_app_window( hWindow ) && pSnapshot->nCount < LLD_MAX_BROWSER_WINDOWS )
      pSnapshot->aWindows[ pSnapshot->nCount++ ] = hWindow;

   return TRUE;
}

static BOOL lld_window_was_present( const LLD_WINDOW_SNAPSHOT *pSnapshot, HWND hWindow )
{
   int nIndex;

   for( nIndex = 0; nIndex < pSnapshot->nCount; ++nIndex )
   {
      if( pSnapshot->aWindows[ nIndex ] == hWindow )
         return TRUE;
   }

   return FALSE;
}

static BOOL CALLBACK lld_find_new_edge_window( HWND hWindow, LPARAM lParam )
{
   LLD_WINDOW_SEARCH *pSearch = ( LLD_WINDOW_SEARCH * ) lParam;

   if( lld_is_edge_app_window( hWindow ) &&
       ! lld_window_was_present( pSearch->pBefore, hWindow ) )
   {
      pSearch->hWindow = hWindow;
      return FALSE;
   }

   return TRUE;
}

static BOOL lld_is_supermium_app_window( HWND hWindow )
{
   char cClassName[ 64 ];

   if( ! IsWindowVisible( hWindow ) || GetWindow( hWindow, GW_OWNER ) != NULL )
      return FALSE;

   if( GetClassNameA( hWindow, cClassName, sizeof( cClassName ) ) == 0 )
      return FALSE;

   return strcmp( cClassName, "Chrome_WidgetWin_1" ) == 0;
}

static BOOL CALLBACK lld_collect_supermium_windows( HWND hWindow, LPARAM lParam )
{
   LLD_WINDOW_SNAPSHOT *pSnapshot = ( LLD_WINDOW_SNAPSHOT * ) lParam;

   if( lld_is_supermium_app_window( hWindow ) && pSnapshot->nCount < LLD_MAX_BROWSER_WINDOWS )
      pSnapshot->aWindows[ pSnapshot->nCount++ ] = hWindow;

   return TRUE;
}

static BOOL CALLBACK lld_find_new_supermium_window( HWND hWindow, LPARAM lParam )
{
   LLD_WINDOW_SEARCH *pSearch = ( LLD_WINDOW_SEARCH * ) lParam;
   DWORD nProcessId = 0;

   if( ! lld_is_supermium_app_window( hWindow ) ||
       lld_window_was_present( pSearch->pBefore, hWindow ) )
      return TRUE;

   GetWindowThreadProcessId( hWindow, &nProcessId );
   if( pSearch->nExpectedProcessId != 0 && nProcessId == pSearch->nExpectedProcessId )
   {
      pSearch->hWindow = hWindow;
      return FALSE;
   }

   if( pSearch->hFallbackWindow == NULL )
      pSearch->hFallbackWindow = hWindow;

   return TRUE;
}

static BOOL lld_is_regular_file( const char *cPath )
{
   DWORD nAttributes = GetFileAttributesA( cPath );

   return nAttributes != INVALID_FILE_ATTRIBUTES && ( nAttributes & FILE_ATTRIBUTE_DIRECTORY ) == 0;
}

static BOOL lld_path_contains_supermium( const char *cPath )
{
   char cLowerPath[ MAX_PATH ];
   size_t nIndex;

   if( cPath == NULL || strlen( cPath ) >= sizeof( cLowerPath ) )
      return FALSE;

   strcpy( cLowerPath, cPath );
   for( nIndex = 0; cLowerPath[ nIndex ] != '\0'; ++nIndex )
      cLowerPath[ nIndex ] = ( char ) tolower( ( unsigned char ) cLowerPath[ nIndex ] );

   return strstr( cLowerPath, "supermium" ) != NULL;
}

static BOOL lld_read_supermium_app_path( HKEY hRoot, REGSAM nView, const char *cExecutableName, char *cExecutable, DWORD nExecutableLength )
{
   char cKey[ 256 ];
   DWORD nType = 0;
   DWORD nLength = nExecutableLength;
   HKEY hKey;

   _snprintf( cKey, sizeof( cKey ) - 1,
              "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\%s",
              cExecutableName );
   cKey[ sizeof( cKey ) - 1 ] = '\0';

   if( RegOpenKeyExA( hRoot, cKey, 0, KEY_READ | nView, &hKey ) != ERROR_SUCCESS )
      return FALSE;

   if( RegQueryValueExA( hKey, NULL, NULL, &nType, ( LPBYTE ) cExecutable, &nLength ) != ERROR_SUCCESS )
   {
      RegCloseKey( hKey );
      return FALSE;
   }

   RegCloseKey( hKey );
   cExecutable[ nExecutableLength - 1 ] = '\0';
   if( nType == REG_EXPAND_SZ )
   {
      char cExpanded[ MAX_PATH ];

      if( ExpandEnvironmentStringsA( cExecutable, cExpanded, sizeof( cExpanded ) ) == 0 )
         return FALSE;
      strncpy( cExecutable, cExpanded, nExecutableLength - 1 );
      cExecutable[ nExecutableLength - 1 ] = '\0';
   }

   if( cExecutable[ 0 ] == '"' )
   {
      size_t nLengthWithoutQuotes = strlen( cExecutable );

      if( nLengthWithoutQuotes > 1 && cExecutable[ nLengthWithoutQuotes - 1 ] == '"' )
      {
         memmove( cExecutable, cExecutable + 1, nLengthWithoutQuotes - 2 );
         cExecutable[ nLengthWithoutQuotes - 2 ] = '\0';
      }
   }

   return lld_path_contains_supermium( cExecutable ) && lld_is_regular_file( cExecutable );
}

static BOOL lld_find_supermium_under_application( const char *cApplicationDirectory, char *cExecutable, DWORD nExecutableLength )
{
   WIN32_FIND_DATAA oFindData;
   HANDLE hFind;
   char cCandidate[ MAX_PATH ];
   char cPattern[ MAX_PATH ];

   _snprintf( cCandidate, sizeof( cCandidate ) - 1, "%s\\chrome.exe", cApplicationDirectory );
   cCandidate[ sizeof( cCandidate ) - 1 ] = '\0';
   if( lld_is_regular_file( cCandidate ) )
   {
      strncpy( cExecutable, cCandidate, nExecutableLength - 1 );
      cExecutable[ nExecutableLength - 1 ] = '\0';
      return TRUE;
   }

   _snprintf( cPattern, sizeof( cPattern ) - 1, "%s\\*", cApplicationDirectory );
   cPattern[ sizeof( cPattern ) - 1 ] = '\0';
   hFind = FindFirstFileA( cPattern, &oFindData );
   if( hFind == INVALID_HANDLE_VALUE )
      return FALSE;

   do
   {
      if( ( oFindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) != 0 &&
          strcmp( oFindData.cFileName, "." ) != 0 && strcmp( oFindData.cFileName, ".." ) != 0 )
      {
         _snprintf( cCandidate, sizeof( cCandidate ) - 1, "%s\\%s\\chrome.exe", cApplicationDirectory, oFindData.cFileName );
         cCandidate[ sizeof( cCandidate ) - 1 ] = '\0';
         if( lld_is_regular_file( cCandidate ) )
         {
            FindClose( hFind );
            strncpy( cExecutable, cCandidate, nExecutableLength - 1 );
            cExecutable[ nExecutableLength - 1 ] = '\0';
            return TRUE;
         }
      }
   }
   while( FindNextFileA( hFind, &oFindData ) != 0 );

   FindClose( hFind );
   return FALSE;
}

static BOOL lld_find_supermium_in_environment( const char *cEnvironmentName, char *cExecutable, DWORD nExecutableLength )
{
   char cRoot[ MAX_PATH ];
   char cApplicationDirectory[ MAX_PATH ];
   DWORD nResult;

   nResult = GetEnvironmentVariableA( cEnvironmentName, cRoot, sizeof( cRoot ) );
   if( nResult == 0 || nResult >= sizeof( cRoot ) )
      return FALSE;

   _snprintf( cApplicationDirectory, sizeof( cApplicationDirectory ) - 1, "%s\\Supermium\\Application", cRoot );
   cApplicationDirectory[ sizeof( cApplicationDirectory ) - 1 ] = '\0';
   if( lld_find_supermium_under_application( cApplicationDirectory, cExecutable, nExecutableLength ) )
      return TRUE;

   _snprintf( cApplicationDirectory, sizeof( cApplicationDirectory ) - 1, "%s\\Supermium", cRoot );
   cApplicationDirectory[ sizeof( cApplicationDirectory ) - 1 ] = '\0';
   return lld_find_supermium_under_application( cApplicationDirectory, cExecutable, nExecutableLength );
}

static BOOL lld_find_supermium_executable( char *cExecutable, DWORD nExecutableLength )
{
   static const HKEY aRoots[] = { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE };
   static const REGSAM aViews[] = { 0, KEY_WOW64_32KEY, KEY_WOW64_64KEY };
   static const char *aAppPathNames[] = { "supermium.exe", "chrome.exe" };
   size_t nRoot;
   size_t nView;
   size_t nName;

   cExecutable[ 0 ] = '\0';
   for( nRoot = 0; nRoot < sizeof( aRoots ) / sizeof( aRoots[ 0 ] ); ++nRoot )
   {
      for( nView = 0; nView < sizeof( aViews ) / sizeof( aViews[ 0 ] ); ++nView )
      {
         for( nName = 0; nName < sizeof( aAppPathNames ) / sizeof( aAppPathNames[ 0 ] ); ++nName )
         {
            if( lld_read_supermium_app_path( aRoots[ nRoot ], aViews[ nView ], aAppPathNames[ nName ], cExecutable, nExecutableLength ) )
               return TRUE;
         }
      }
   }

   return lld_find_supermium_in_environment( "LOCALAPPDATA", cExecutable, nExecutableLength ) ||
          lld_find_supermium_in_environment( "ProgramFiles", cExecutable, nExecutableLength ) ||
          lld_find_supermium_in_environment( "ProgramFiles(x86)", cExecutable, nExecutableLength ) ||
          lld_find_supermium_in_environment( "ProgramW6432", cExecutable, nExecutableLength );
}

static void lld_log_supermium_executable( const char *cExecutable )
{
   char cMessage[ MAX_PATH + 64 ];

   _snprintf( cMessage, sizeof( cMessage ) - 1, "LL-Dominus Supermium: %s\\n", cExecutable );
   cMessage[ sizeof( cMessage ) - 1 ] = '\0';
   OutputDebugStringA( cMessage );
}

static void lld_configure_designer_window( HWND hWindow )
{
   ShowWindow( hWindow, SW_MAXIMIZE );
   SetForegroundWindow( hWindow );
}

static DWORD WINAPI lld_watch_designer_close( LPVOID pParameter )
{
   LLD_WINDOW_WATCH *pWatch = ( LLD_WINDOW_WATCH * ) pParameter;
   char cWindowTitle[ 512 ];

   while( IsWindow( pWatch->hWindow ) )
   {
      cWindowTitle[ 0 ] = '\0';
      GetWindowTextA( pWatch->hWindow, cWindowTitle, sizeof( cWindowTitle ) );
      if( strstr( cWindowTitle, "LLDOMINUS_CLOSE_REQUESTED" ) != NULL )
      {
         PostMessage( pWatch->hWindow, WM_CLOSE, 0, 0 );
         break;
      }
      Sleep( 100 );
   }

   free( pWatch );
   return 0;
}

static void lld_start_designer_close_watch( HWND hWindow )
{
   LLD_WINDOW_WATCH *pWatch;
   HANDLE hThread;

   pWatch = ( LLD_WINDOW_WATCH * ) malloc( sizeof( LLD_WINDOW_WATCH ) );
   if( pWatch == NULL )
      return;

   pWatch->hWindow = hWindow;
   hThread = CreateThread( NULL, 0, lld_watch_designer_close, pWatch, 0, NULL );
   if( hThread == NULL )
   {
      free( pWatch );
      return;
   }

   CloseHandle( hThread );
}

HB_FUNC( LLDOPENDESIGNERAPPWINDOW )
{
   const char *cUrl = hb_parc( 1 );
   LLD_WINDOW_SNAPSHOT oBefore;
   LLD_WINDOW_SEARCH oSearch;
   SHELLEXECUTEINFOA oExecute;
   char *cParameters;
   const char *cQuerySeparator;
   size_t nParametersLength;
   DWORD nStartedAt;

   if( cUrl == NULL || cUrl[ 0 ] == '\0' )
   {
      hb_retnl( -1 );
      return;
   }

   ZeroMemory( &oBefore, sizeof( oBefore ) );
   EnumWindows( lld_collect_edge_windows, ( LPARAM ) &oBefore );

   cQuerySeparator = strchr( cUrl, '?' ) == NULL ? "?" : "&";
   nParametersLength = strlen( cUrl ) + 160;
   cParameters = ( char * ) malloc( nParametersLength );
   if( cParameters == NULL )
   {
      hb_retnl( -1 );
      return;
   }

   _snprintf( cParameters, nParametersLength - 1,
              "--kiosk \"%s%skiosk=1\" --edge-kiosk-type=fullscreen --no-first-run",
              cUrl, cQuerySeparator );
   cParameters[ nParametersLength - 1 ] = '\0';

   ZeroMemory( &oExecute, sizeof( oExecute ) );
   oExecute.cbSize       = sizeof( oExecute );
   oExecute.fMask        = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
   oExecute.lpVerb       = "open";
   oExecute.lpFile       = "msedge.exe";
   oExecute.lpParameters = cParameters;
   oExecute.nShow        = SW_SHOWNORMAL;

   if( ! ShellExecuteExA( &oExecute ) )
   {
      free( cParameters );
      hb_retnl( -1 );
      return;
   }

   free( cParameters );
   nStartedAt = GetTickCount();
   ZeroMemory( &oSearch, sizeof( oSearch ) );
   oSearch.pBefore = &oBefore;

   while( oSearch.hWindow == NULL && GetTickCount() - nStartedAt < 12000 )
   {
      EnumWindows( lld_find_new_edge_window, ( LPARAM ) &oSearch );
      if( oSearch.hWindow == NULL )
         Sleep( 50 );
   }

   if( oExecute.hProcess != NULL )
      CloseHandle( oExecute.hProcess );

   if( oSearch.hWindow == NULL )
   {
      hb_retnl( -2 );
      return;
   }

   lld_configure_designer_window( oSearch.hWindow );
   lld_start_designer_close_watch( oSearch.hWindow );
   hb_retnl( 1 );
}

HB_FUNC( LLDOPENDESIGNERSUPERMIUMWINDOW )
{
   const char *cUrl = hb_parc( 1 );
   LLD_WINDOW_SNAPSHOT oBefore;
   LLD_WINDOW_SEARCH oSearch;
   SHELLEXECUTEINFOA oExecute;
   char cExecutable[ MAX_PATH ];
   char *cParameters;
   const char *cQuerySeparator;
   size_t nParametersLength;
   DWORD nStartedAt;

   if( cUrl == NULL || cUrl[ 0 ] == '\0' )
   {
      hb_retnl( -1 );
      return;
   }

   if( ! lld_find_supermium_executable( cExecutable, sizeof( cExecutable ) ) )
   {
      hb_retnl( -3 );
      return;
   }

   ZeroMemory( &oBefore, sizeof( oBefore ) );
   EnumWindows( lld_collect_supermium_windows, ( LPARAM ) &oBefore );

   cQuerySeparator = strchr( cUrl, '?' ) == NULL ? "?" : "&";
   nParametersLength = strlen( cUrl ) + 48;
   cParameters = ( char * ) malloc( nParametersLength );
   if( cParameters == NULL )
   {
      hb_retnl( -1 );
      return;
   }

   _snprintf( cParameters, nParametersLength - 1, "--kiosk \"%s%skiosk=1\"", cUrl, cQuerySeparator );
   cParameters[ nParametersLength - 1 ] = '\0';

   ZeroMemory( &oExecute, sizeof( oExecute ) );
   oExecute.cbSize       = sizeof( oExecute );
   oExecute.fMask        = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
   oExecute.lpVerb       = "open";
   oExecute.lpFile       = cExecutable;
   oExecute.lpParameters = cParameters;
   oExecute.nShow        = SW_SHOWNORMAL;

   if( ! ShellExecuteExA( &oExecute ) )
   {
      free( cParameters );
      hb_retnl( -1 );
      return;
   }

   lld_log_supermium_executable( cExecutable );
   free( cParameters );
   nStartedAt = GetTickCount();
   ZeroMemory( &oSearch, sizeof( oSearch ) );
   oSearch.pBefore = &oBefore;
   if( oExecute.hProcess != NULL )
      oSearch.nExpectedProcessId = GetProcessId( oExecute.hProcess );

   while( oSearch.hWindow == NULL && GetTickCount() - nStartedAt < 12000 )
   {
      oSearch.hFallbackWindow = NULL;
      EnumWindows( lld_find_new_supermium_window, ( LPARAM ) &oSearch );
      if( oSearch.hWindow == NULL && oSearch.hFallbackWindow != NULL )
         oSearch.hWindow = oSearch.hFallbackWindow;
      if( oSearch.hWindow == NULL )
         Sleep( 50 );
   }

   if( oExecute.hProcess != NULL )
      CloseHandle( oExecute.hProcess );

   if( oSearch.hWindow == NULL )
   {
      hb_retnl( -2 );
      return;
   }

   lld_configure_designer_window( oSearch.hWindow );
   lld_start_designer_close_watch( oSearch.hWindow );
   hb_retnl( 1 );
}

#pragma ENDDUMP
