// OneScript: ./OInt/core/Modules/OPI_GoogleDrive.os
// Lib: Google Drive
// CLI: gdrive

// MIT License

// Copyright (c) 2023 Anton Tsitavets

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// https://github.com/Bayselonarrend/OpenIntegrations

// BSLLS:LatinAndCyrillicSymbolInWord-off
// BSLLS:IncorrectLineBreak-off
// BSLLS:UsingServiceTag-off

//@skip-check module-structure-top-region
//@skip-check module-structure-method-in-regions
//@skip-check wrong-string-literal-content

// Uncomment if OneScript is executed
// #Use "../../tools"

#Region Public

#Region FileAndDirectoryManagement

// Get object information
// Gets information about a folder or file by ID
//
// Parameters:
// Token - String - Token - token
// Identifier - String - Identifier of the file or folder - object
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function GetObjectInformation(Val Token, Val Identifier) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Identifier);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL     = "https://www.googleapis.com/drive/v3/files/" + Identifier;

    Parameters = New Structure;
    Parameters.Insert("fields", "*");

    Response = OPI_Tools.Get(URL, Parameters, Headers);

    Return Response;

EndFunction

// Get list of directories
// Gets the list of drive directories
//
// Parameters:
// Token - String - Token - token
// NameContains - String - Filter by name - querry
// Detailed - Boolean - Adds a list of files to the directory fields - depth
//
// Returns:
// Map Of KeyAndValue - Array of directory mappings
Function GetDirectoriesList(Val Token, Val NameContains = "", Val Detailed = False) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(NameContains);
    OPI_TypeConversion.GetBoolean(Detailed);

    Headers        = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    ArrayOfObjects = New Array;
    Filter         = New Array;

    Filter.Add("mimeType = 'application/vnd.google-apps.folder'");

    If ValueIsFilled(NameContains) Then
        Filter.Add("name contains '" + NameContains + "'");
    EndIf;

    GetObjectsListRecursively(Headers, ArrayOfObjects, Detailed, Filter);

    If Detailed Then
        BreakDownObjectsInDetail(Token, ArrayOfObjects);
    EndIf;

    Return ArrayOfObjects;

EndFunction

// Get list of files
// Gets the list of files
//
// Parameters:
// Token - String - Token - token
// NameContains - String - Filter by name - querry
// Directory - String - Filter by parent directory ID - catalog
//
// Returns:
// Map Of KeyAndValue - Array of file mappings
Function GetFilesList(Val Token, Val NameContains = "", Val Directory = "") Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(NameContains);
    OPI_TypeConversion.GetLine(Directory);

    Headers        = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    ArrayOfObjects = New Array;
    Filter         = New Array;

    Filter.Add("mimeType != 'application/vnd.google-apps.folder'");

    If ValueIsFilled(NameContains) Then
        Filter.Add("name contains '" + NameContains + "'");
    EndIf;

    If ValueIsFilled(Directory) Then
        Filter.Add("'" + Directory + "' in parents");
    EndIf;

    GetObjectsListRecursively(Headers, ArrayOfObjects, , Filter);

    Return ArrayOfObjects;

EndFunction

// Upload file
// Uploads a file to the drive
//
// Parameters:
// Token - String - Token - token
// File - BinaryData,String - File to be uploaded - file
// Description - Map Of KeyAndValue - See GetFileDescription - props - JSON description or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function UploadFile(Val Token, Val File, Val Description) Export

    Return FileManagement(Token, File, Description);

EndFunction

// Create folder
// Creates an empty directory on the drive
//
// Parameters:
// Token - String - Token - token
// Name - String - Folder name - title
// Parent - String - Parent - catalog
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function CreateFolder(Val Token, Val Name, Val Parent = "") Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Name);
    OPI_TypeConversion.GetLine(Parent);

    Description = New Map;
    Description.Insert("MIME"       , "application/vnd.google-apps.folder");
    Description.Insert("Name"       , Name);
    Description.Insert("Description", "");
    Description.Insert("Parent"     , ?(ValueIsFilled(Parent), Parent, "root"));

    Return FileManagement(Token, , Description);

EndFunction

// Download file
// Gets file by ID
//
// Parameters:
// Token - String - Token - token
// Identifier - String - File identifier - object
// SavePath - String - File save path - out
//
// Returns:
// BinaryData,String - Binary data or file path when SavePath parameter is specified
Function DownloadFile(Val Token, Val Identifier, Val SavePath = "") Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Identifier);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL     = "https://www.googleapis.com/drive/v3/files/" + Identifier;

    Parameters = New Map;
    Parameters.Insert("alt", "media");

    Response = OPI_Tools.Get(URL, Parameters , Headers, SavePath);

    Return Response;

EndFunction

// Copy object
// Copies file or directory
//
// Parameters:
// Token - String - Token - token
// Identifier - String - Object identifier - object
// NewName - String - New object name - title
// NewParent - String - New parent directory - catalog
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function CopyObject(Val Token, Val Identifier, Val NewName = "", Val NewParent = "") Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(NewName);
    OPI_TypeConversion.GetLine(Identifier);
    OPI_TypeConversion.GetLine(NewParent);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL     = "https://www.googleapis.com/drive/v3/files/" + Identifier + "/copy";

    Parameters = New Structure;

    If ValueIsFilled(NewName) Then
        Parameters.Insert("name", NewName);
    EndIf;

    If ValueIsFilled(NewParent) Then

        ArrayOfParents = New Array;
        ArrayOfParents.Add(NewParent);
        Parameters.Insert("parents", ArrayOfParents);

    EndIf;

    Response = OPI_Tools.Post(URL, Parameters , Headers, True);

    Return Response;

EndFunction

// Update file
// Updates file binary data
//
// Parameters:
// Token - String - Token - token
// Identifier - String - Identifier of the object to update - object
// File - BinaryData,String - File source for update - file
// NewName - String - New file name (if necessary) - title
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function UpdateFile(Val Token, Val Identifier, Val File, Val NewName = "") Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Identifier);
    OPI_TypeConversion.GetLine(NewName);
    OPI_TypeConversion.GetBinaryData(File);

    If ValueIsFilled(NewName) Then
        Description = New Map;
        Description.Insert("Name", NewName);
    Else
        Description = "";
    EndIf;

    Return FileManagement(Token, File, Description, Identifier);

EndFunction

// Delete object
// Deletes file or directory by ID
//
// Parameters:
// Token - String - Token - token
// Identifier - String - Identifier of the object to delete - object
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function DeleteObject(Val Token, Val Identifier) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Identifier);

    Headers  = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL      = "https://www.googleapis.com/drive/v3/files/" + Identifier;
    Response = OPI_Tools.Delete(URL, , Headers);

    Return Response;

EndFunction

// Get file description !NOCLI
//
// Returns:
// Map Of KeyAndValue - File description
Function GetFileDescription() Export

    Description = New Map;
    Description.Insert("MIME"        , "image/jpeg"); // MIME-type uploading file
    Description.Insert("Name"        , "New file.jpg"); // File name with extension
    Description.Insert("Description" , "This is a new file"); // File description
    Description.Insert("Parent"      , "root"); // ID directory upload or "root" for upload in root

    Return Description;

EndFunction

#EndRegion

#Region CommentManagement

// Create comment
// Creates a comment for a file or directory
//
// Parameters:
// Token - String - Token - token
// Identifier - String - Identifier of the object that needs a comment - object
// Comment - String - Comment text - text
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function CreateComment(Val Token, Val Identifier, Val Comment) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Identifier);
    OPI_TypeConversion.GetLine(Comment);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL = "https://www.googleapis.com/drive/v3/files/" + Identifier + "/comments?fields=*";

    Parameters = New Structure;
    Parameters.Insert("content", Comment);

    Response = OPI_Tools.POST(URL, Parameters, Headers);

    Return Response;

EndFunction

// Get comment
// Gets comment by ID
//
// Parameters:
// Token - String - Token - token
// ObjectID - String - Identifier of the file or directory where the comment is located - object
// CommentID - String - Comment identifier - comment
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function GetComment(Val Token, Val ObjectID, Val CommentID) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(ObjectID);
    OPI_TypeConversion.GetLine(CommentID);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL     = "https://www.googleapis.com/drive/v3/files/" + ObjectID + "/comments/" + CommentID;

    Parameters = New Structure;
    Parameters.Insert("fields", "*");

    Response = OPI_Tools.Get(URL, Parameters, Headers);

    Return Response;

EndFunction

// Get list of comments
// Gets the list of all comments of the object
//
// Parameters:
// Token - String - Token - token
// ObjectID - String - Object identifier - object
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function GetCommentList(Val Token, Val ObjectID) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(ObjectID);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL     = "https://www.googleapis.com/drive/v3/files/" + ObjectID + "/comments";

    Parameters = New Structure;
    Parameters.Insert("fields", "*");

    Response = OPI_Tools.Get(URL, Parameters, Headers);

    Return Response;

EndFunction

// Delete comment
// Deletes comment by ID
//
// Parameters:
// Token - String - Token - token
// ObjectID - String - Identifier of the file or directory where the comment is located - object
// CommentID - String - Comment identifier - comment
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Google
Function DeleteComment(Val Token, Val ObjectID, Val CommentID) Export

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(ObjectID);
    OPI_TypeConversion.GetLine(CommentID);

    Headers = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    URL     = "https://www.googleapis.com/drive/v3/files/" + ObjectID + "/comments/" + CommentID;

    Response = OPI_Tools.Delete(URL, , Headers);

    Return Response;

EndFunction

#EndRegion

#EndRegion

#Region Private

Procedure GetObjectsListRecursively(Val Headers, ArrayOfObjects, Detailed = False, Filter = "", Page = "")

    URL        = "https://www.googleapis.com/drive/v3/files";
    Files      = "files";
    NPT        = "nextPageToken";
    Parameters = New Structure;
    Parameters.Insert("fields", "*");

    If ValueIsFilled(Page) Then
        Parameters.Insert("pageToken", Page);
    EndIf;

    If ValueIsFilled(Filter) And TypeOf(Filter) = Type("Array") Then
        FilterString = StrConcat(Filter, " and ");
        Parameters.Insert("q", FilterString);
    EndIf;

    Result = OPI_Tools.Get(URL, Parameters, Headers);

    Objects = Result[Files];
    Page    = Result[NPT];

    For Each CurrentObject In Objects Do
         ArrayOfObjects.Add(CurrentObject);
    EndDo;

    If Objects.Count() > 0 And ValueIsFilled(Page) Then
        GetObjectsListRecursively(Headers, ArrayOfObjects, Detailed, Filter, Page);
    EndIf;

EndProcedure

Procedure BreakDownObjectsInDetail(Val Token, ArrayOfObjects)

    For Each CurrentObject In ArrayOfObjects Do

        ArrayOfFiles = New Array;
        CurrentID    = CurrentObject["id"];

        Result = GetFilesList(Token, , CurrentID);

        For Each File In Result Do
            ArrayOfFiles.Add(File);
        EndDo;

        CurrentObject.Insert("files", ArrayOfFiles);

    EndDo;

EndProcedure

Procedure FormFileUploadParameters(Description)

    FormedDescription = New Map;
    OPI_Tools.RemoveEmptyCollectionFields(Description);

    FieldMapping = New Map;
    FieldMapping.Insert("MIME"        , "mimeType");
    FieldMapping.Insert("Name"        , "name");
    FieldMapping.Insert("Description" , "description");
    FieldMapping.Insert("Parent"      , "parents");
    FieldMapping.Insert("Extension"   , "fileExtension");

    For Each Element In Description Do

        If Element.Key = "Parent" Then

            CurrentValue = New Array;
            CurrentValue.Add(Element.Value);

        Else

            CurrentValue = Element.Value;

        EndIf;

        FieldName = FieldMapping.Get(Element.Key);
        FormedDescription.Insert(FieldName, CurrentValue);

    EndDo;

    Description = FormedDescription;

EndProcedure

Function FileManagement(Val Token, Val File = "", Val Description = "", Val Identifier = "")

    OPI_TypeConversion.GetLine(Token);
    OPI_TypeConversion.GetLine(Identifier);

    If ValueIsFilled(Description) Then
        OPI_TypeConversion.GetCollection(Description);
    EndIf;

    MimeType = "mimeType";

    If ValueIsFilled(Identifier) Then
        MIME = GetObjectInformation(Token, Identifier)[MimeType];
    Else
        MIME = Description["MIME"];
    EndIf;

    If Not ValueIsFilled(Description) Then
        Description = New Map;
    EndIf;

    Headers         = OPI_GoogleWorkspace.GetAuthorizationHeader(Token);
    FormFileUploadParameters(Description);
    JSONDescription = OPI_Tools.JSONString(Description);

    FileMapping = New Map;

    If ValueIsFilled(File) Then

        ChunkSize = 268435457;
        Size      = OPI_Tools.ConvertDataWithSizeRetrieval(File, ChunkSize);

        FileMapping.Insert(File, MIME);

        If Size < ChunkSize And TypeOf(File) = Type("BinaryData") Then
            Response = UploadSmallFile(JSONDescription, FileMapping, Headers, Identifier);
        Else
            Response = UploadLargeFile(Description, FileMapping, Headers, Identifier);
        EndIf;

    Else
       Response = UploadSmallFile(JSONDescription, FileMapping, Headers, Identifier);
    EndIf;

    Return Response;

EndFunction

Function UploadSmallFile(Val Description, Val FileMapping, Val Headers, Val Identifier = "")

    URL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";

    If ValueIsFilled(Identifier) Then
        URL      = StrReplace(URL, "/files", "/files/" + Identifier);
        Response = OPI_Tools.PatchMultipartRelated(URL, Description, FileMapping, Headers);
    Else
        Response = OPI_Tools.PostMultipartRelated(URL, Description, FileMapping, Headers);
    EndIf;

    Return Response;

EndFunction

Function UploadLargeFile(Val Description, Val FileMapping, Val Headers, Val Identifier = "")

    For Each File In FileMapping Do
        Binary = File.Key;
        Break;
    EndDo;

    URL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable";

    If ValueIsFilled(Identifier) Then
        URL      = StrReplace(URL, "/files", "/files/" + Identifier);
        Response = OPI_Tools.Patch(URL, Description, Headers, True, True);
    Else
        Response = OPI_Tools.Post(URL, Description, Headers, True, True);
    EndIf;

    UploadURL = Response.Headers["Location"];

    If Not ValueIsFilled(UploadURL) Then
        OPI_Tools.ProcessResponse(Response);
        Return Response;
    EndIf;

    UploadResponse = UploadFileInParts(Binary, UploadURL);
    Response       = ?(ValueIsFilled(UploadResponse), UploadResponse, Response);

    OPI_Tools.ProcessResponse(Response);
    Return Response;

EndFunction

Function UploadFileInParts(Val Binary, Val UploadURL)

    Response        = "";
    ChunkSize       = 268435456;
    BytesRead       = 0;
    CurrentPosition = 0;
    TotalSize       = Binary.Size();
    StrTotalSize    = OPI_Tools.NumberToString(TotalSize);
    DataReader      = New DataReader(Binary);
    SourceStream    = DataReader.SourceStream();

    WHile BytesRead < TotalSize Do

        BytesRead    = SourceStream.CurrentPosition();
        Result       = DataReader.Read(ChunkSize);
        CurrentData  = Result.GetBinaryData();
        CurrentSize  = CurrentData.Size();
        NextPosition = CurrentPosition + CurrentSize - 1;

        If Not ValueIsFilled(CurrentData) Then
            Break;
        EndIf;

        StreamHeader = "bytes "
            + OPI_Tools.NumberToString(CurrentPosition)
            + "-"
            + OPI_Tools.NumberToString(NextPosition)
            + "/"
            + StrTotalSize;

        AdditionalHeaders = New Map;
        AdditionalHeaders.Insert("Content-Length", OPI_Tools.NumberToString(CurrentSize));
        AdditionalHeaders.Insert("Content-Range" , StreamHeader);
        AdditionalHeaders.Insert("Content-Type"  , "application/octet-stream");

        Response = OPI_Tools.Put(UploadURL, CurrentData, AdditionalHeaders, False, True);

        CheckResult = CheckPartUpload(Response, StrTotalSize, AdditionalHeaders, UploadURL, CurrentPosition);

        If ValueIsFilled(CheckResult) Then
            Return CheckResult;
        EndIf;

        // !OInt KB = 1024;
        // !OInt MB = KB * KB;
        // !OInt Message(OPI_Tools.ProgressInfo(CurrentPosition, TotalSize, "MB", MB));

        // !OInt RunGarbageCollection();
        // !OInt FreeObject(CurrentData);

    EndDo;

    Return Response;

EndFunction

Function CheckPartUpload(Response, StrTotalSize, AdditionalHeaders, UploadURL, CurrentPosition)

    StartOfErrorCodes   = 400;
    EndOfFailureCodes   = 600;
    StartOfSuccessCodes = 200;
    EndOfSuccessCodes   = 300;
    Redirection         = 308;

    If Response.StatusCode >= StartOfErrorCodes And Response.StatusCode < EndOfFailureCodes Then

        StreamHeader = "bytes */" + StrTotalSize;
        AdditionalHeaders.Insert("Content-Range" , StreamHeader);

        CheckResponse = OPI_Tools.Put(UploadURL, "", AdditionalHeaders, False, True);

        If CheckResponse.StatusCode >= StartOfSuccessCodes And CheckResponse.StatusCode < EndOfSuccessCodes Then

            OPI_Tools.ProcessResponse(CheckResponse);
            Return CheckResponse;

        ElsIf CheckResponse.StatusCode = Redirection Then

            UploadedData = Response.Headers["Range"];

        Else

            OPI_Tools.ProcessResponse(Response);
            Return Response;

        EndIf;

    Else
        UploadedData = Response.Headers["Range"];
    EndIf;

    If Not ValueIsFilled(UploadedData) Then
        OPI_Tools.ProcessResponse(Response);
        Return Response;
    EndIf;

    UploadedData = StrReplace(UploadedData, "bytes=", "");
    ArrayOfInformation = StrSplit(UploadedData, "-", False);
    PartsRequired      = 2;

    If Not ArrayOfInformation.Count() = PartsRequired Then
        OPI_Tools.ProcessResponse(Response);
        Return Response;
    EndIf;

    CurrentPosition = Number(ArrayOfInformation[1]) + 1;

    Return "";

EndFunction

#EndRegion
