"""File creation, update, shortcuts.

"""

"""GAM Google Drive file, permission, shared drive, and label management."""

import re

from gam.cmd.drive.core import DFA_IGNORE_DEFAULT_VISIBILITY, DFA_KEEP_REVISION_FOREVER, DFA_LOCALFILENAME, DFA_LOCALFILEPATH, DFA_LOCALMIMETYPE, DFA_OCRLANGUAGE, DFA_PARENTID, DFA_PRESERVE_FILE_TIMES, DFA_REPLACEFILENAME, DFA_SHAREDDRIVE_PARENT, DFA_STRIPNAMEPREFIX, DFA_TIMEFORMAT, DFA_TIMESTAMP, DFA_URL, DFA_USE_CONTENT_AS_INDEXABLE_TEXT, _getDriveFileAddRemoveParentInfo, _getDriveFileParentInfo, _validateUserGetFileIDs, escapeDriveFileName, getDriveFileAddRemoveParentAttribute, getDriveFileAttribute, getDriveFileEntity, getDriveFileParentAttribute, getMediaBody, initDriveFileAttributes, setPreservedFileTimes
from gam.cmd.drive.fileinfo import writeReturnIdLink
import time

from gamlib import api as API
from gamlib import settings as GC
from gamlib import gapi as GAPI
from gamlib import state as GM
from gamlib import msgs as Msg
from gam.util.svcacct import buildGAPIServiceObject
from gam.util.api_call import callGAPI, callGAPIitems, callGAPIpages
from gam.util.args import (
        getAddCSVData,
    getArgument,
    getBoolean,
    getCharacter,
    getSheetEntity,
    getSheetIdFromSheetEntity,
    getString,
    protectedSheetId,
)
from gam.util.output import ISOformatTimeStamp
from gam.util.csv_pf import CSVPrintFile
from gam.util.display import (
    entityActionFailedWarning,
    entityActionNotPerformedWarning,
    entityActionPerformed,
    entityModifierNewValueActionPerformed,
    entityModifierNewValueItemValueListActionPerformed,
    entityPerformAction,
    entityPerformActionNumItems,
    entityPerformActionSubItemModifierNumItems,
    userDriveServiceNotEnabledWarning,
)
from gam.util.entity import (
    _getEntityMimeType,
    _getTargetEntityMimeType,
    getEntityArgument,
    shlexSplitList,
)
from gam.util.errors import emptyArgumentExit, unknownArgumentExit, usageErrorExit
from gam.util.fileio import UNKNOWN, readFile
from gam.util.output import writeStdout
from gam.constants import AND_NOT_SHORTCUT, ANY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS, MY_DRIVE, MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS

from gam.var import Act, Cmd, Ent, Ind

APPLICATION_VND_GOOGLE_APPS = 'application/vnd.google-apps.'
MIMETYPE_GA_DOCUMENT = f'{APPLICATION_VND_GOOGLE_APPS}document'
MIMETYPE_GA_DRAWING = f'{APPLICATION_VND_GOOGLE_APPS}drawing'
MIMETYPE_GA_FILE = f'{APPLICATION_VND_GOOGLE_APPS}file'
MIMETYPE_GA_FOLDER = f'{APPLICATION_VND_GOOGLE_APPS}folder'
MIMETYPE_GA_FORM = f'{APPLICATION_VND_GOOGLE_APPS}form'
MIMETYPE_GA_FUSIONTABLE = f'{APPLICATION_VND_GOOGLE_APPS}fusiontable'
MIMETYPE_GA_JAM = f'{APPLICATION_VND_GOOGLE_APPS}jam'
MIMETYPE_GA_MAP = f'{APPLICATION_VND_GOOGLE_APPS}map'
MIMETYPE_GA_PRESENTATION = f'{APPLICATION_VND_GOOGLE_APPS}presentation'
MIMETYPE_GA_SCRIPT = f'{APPLICATION_VND_GOOGLE_APPS}script'
MIMETYPE_GA_SCRIPT_JSON = f'{APPLICATION_VND_GOOGLE_APPS}script+json'
MIMETYPE_GA_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}shortcut'
MIMETYPE_GA_3P_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}drive-sdk'
MIMETYPE_GA_SITE = f'{APPLICATION_VND_GOOGLE_APPS}site'
MIMETYPE_GA_SPREADSHEET = f'{APPLICATION_VND_GOOGLE_APPS}spreadsheet'
ME_IN_OWNERS = "'me' in owners"
ME_IN_OWNERS_AND = ME_IN_OWNERS + " and "
NOT_ME_IN_OWNERS = "not " + ME_IN_OWNERS
NOT_ME_IN_OWNERS_AND = NOT_ME_IN_OWNERS + " and "
WITH_ANY_FILE_NAME = "name = '{0}'"
WITH_MY_FILE_NAME = ME_IN_OWNERS_AND + WITH_ANY_FILE_NAME
WITH_OTHER_FILE_NAME = NOT_ME_IN_OWNERS_AND + WITH_ANY_FILE_NAME
ROOT = 'root'
ORPHANS = 'Orphans'
SHARED_WITHME = 'SharedWithMe'
SHARED_DRIVES = 'SharedDrives'

def processFilenameReplacements(name, replacements):
  for replacement in replacements:
    name = re.sub(replacement[0], replacement[1], name)
  return name

def addTimestampToFilename(parameters, body):
  tdtime = arrow.now(GC.Values[GC.TIMEZONE])
  body['name'] += ' - '
  if not parameters[DFA_TIMEFORMAT]:
    body['name'] += ISOformatTimeStamp(tdtime)
  else:
    body['name'] += tdtime.strftime(parameters[DFA_TIMEFORMAT])

createReturnItemMap = {
  'returnidonly': 'id',
  'returnlinkonly': 'webViewLink',
  'returneditlinkonly': 'editLink'
  }

# gam <UserTypeEntity> create drivefile
#	[(localfile <FileName>|-)|(url <URL>)]
#	[(drivefilename|newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
#	[stripnameprefix <String>]
#	[timestamp <Boolean>]] [timeformat <DateTimeFormat>]
#	<DriveFileCreateAttribute>* [noduplicate]
#	[(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*)) |
#	 (returnidonly|returnlinkonly|returneditlinkonly|showdetails)]
def createDriveFile(users):
  csvPF = media_body = None
  addCSVData = {}
  returnIdLink = None
  noDuplicate = showDetails = False
  body = {}
  newName = None
  assignLocalName = True
  parameters = initDriveFileAttributes()
  while Cmd.ArgumentsRemaining():
    myarg = getArgument()
    if myarg in {'drivefilename', 'newfilename'}:
      newName = getString(Cmd.OB_DRIVE_FILE_NAME)
      assignLocalName = False
    elif myarg in createReturnItemMap:
      returnIdLink = createReturnItemMap[myarg]
      showDetails = False
    elif myarg == 'showdetails':
      returnIdLink = None
      showDetails = True
    elif myarg == 'noduplicate':
      noDuplicate = True
    elif myarg == 'csv':
      csvPF = CSVPrintFile()
    elif csvPF and myarg == 'todrive':
      csvPF.GetTodriveParameters()
    elif csvPF and myarg == 'addcsvdata':
      getAddCSVData(addCSVData)
    else:
      getDriveFileAttribute(myarg, body, parameters, False)
  if assignLocalName and parameters[DFA_LOCALFILENAME] and parameters[DFA_LOCALFILENAME] != '-':
    newName = parameters[DFA_LOCALFILENAME]
  if newName:
    if parameters[DFA_STRIPNAMEPREFIX] and newName.startswith(parameters[DFA_STRIPNAMEPREFIX]):
      newName = newName[len(parameters[DFA_STRIPNAMEPREFIX]):]
    if parameters[DFA_REPLACEFILENAME]:
      body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
    else:
      body['name'] = newName
  else:
    body['name'] = 'Untitled'
  if parameters[DFA_TIMESTAMP]:
    addTimestampToFilename(parameters, body)
  if parameters[DFA_LOCALFILEPATH]:
    if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
      setPreservedFileTimes(body, parameters, False)
    if body.get('mimeType') == MIMETYPE_GA_SCRIPT_JSON:
      parameters[DFA_LOCALMIMETYPE] = body['mimeType']
    media_body = getMediaBody(parameters)
  elif parameters[DFA_URL]:
    media_body = getMediaBody(parameters)
    body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
  if csvPF:
    csvPF.SetTitles(['User', 'name', 'id'])
    if showDetails:
      csvPF.AddTitles(['parentId', 'mimeType'])
    if addCSVData:
      csvPF.AddTitles(sorted(addCSVData.keys()))
  Act.Set(Act.CREATE)
  i, count, users = getEntityArgument(users)
  for user in users:
    i += 1
    user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
    if not drive:
      continue
    if not _getDriveFileParentInfo(drive, user, i, count, body, parameters):
      continue
    entityType = _getEntityMimeType(body) if 'mimeType' in body else Ent.DRIVE_FILE
    try:
      if noDuplicate:
        # Check for existing file/folder, do not duplicate
        files = callGAPIitems(drive.files(), 'list', 'files',
                              throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
                              q=f"name = '{escapeDriveFileName(body['name'])}'and '{body['parents'][0]}' in parents and trashed = false",
                              fields='files(id)', **parameters['searchargs'])
        if files:
          entityActionNotPerformedWarning([Ent.USER, user, entityType, body['name'], Ent.DRIVE_PARENT_FOLDER_ID, body['parents'][0]],
                                          f"{Msg.DUPLICATE} IDs {','.join([file['id'] for file in files])}", i, count)
          continue
      result = callGAPI(drive.files(), 'create',
                        throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
                                                                    GAPI.PERMISSION_DENIED, GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
                                                                    GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR,
                                                                    GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
                                                                    GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
                                                                    GAPI.UPLOAD_TOO_LARGE, GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
                        ocrLanguage=parameters[DFA_OCRLANGUAGE],
                        ignoreDefaultVisibility=parameters[DFA_IGNORE_DEFAULT_VISIBILITY],
                        keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
                        useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
                        media_body=media_body, body=body, fields='id,name,mimeType,parents,webViewLink', supportsAllDrives=True)
      parentId = result['parents'][0] if 'parents' in result and result['parents'] else UNKNOWN
      if returnIdLink:
        writeReturnIdLink(returnIdLink, parameters[DFA_LOCALMIMETYPE], result)
      elif not csvPF:
        kvList = [Ent.USER, user]
        if not showDetails:
          titleInfo = f'{result["name"]}({result["id"]})'
          if parameters[DFA_LOCALFILENAME]:
            kvList.extend([Ent.DRIVE_FILE, titleInfo])
          else:
            kvList.extend([_getEntityMimeType(result), titleInfo])
        else:
          if result['mimeType'] != MIMETYPE_GA_FOLDER:
            kvList.extend([Ent.DRIVE_FILE, result['name'], Ent.DRIVE_FILE_ID, result['id']])
          else:
            kvList.extend([Ent.DRIVE_FOLDER, result['name'], Ent.DRIVE_FOLDER_ID, result['id']])
          kvList.extend([Ent.DRIVE_PARENT_FOLDER_ID, parentId, Ent.MIMETYPE, result['mimeType']])
        if media_body:
          entityModifierNewValueActionPerformed(kvList, Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME] or parameters[DFA_URL], i, count)
        else:
          entityActionPerformed(kvList, i, count)
      else:
        row = {'User': user, 'name': result['name'], 'id': result['id']}
        if showDetails:
          row.update({'parentId': parentId, 'mimeType': result['mimeType']})
        if addCSVData:
          row.update(addCSVData)
        csvPF.WriteRow(row)
    except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
            GAPI.invalidQuery, GAPI.permissionDenied, GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
            GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
            GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
            GAPI.uploadTooLarge, GAPI.teamDrivesShortcutFileNotSupported) as e:
      entityActionFailedWarning([Ent.USER, user, entityType, body['name']], str(e), i, count)
    except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
      userDriveServiceNotEnabledWarning(user, str(e), i, count)
  if csvPF:
    csvPF.writeCSVfile('Files')

# gam <UserTypeEntity> create drivefolderpath
#	[pathdelimiter <Character>]
#	((fullpath <DriveFolderPath) |
#	 (path <DriveFolderPath [<DriveFileParentAttribute>]) |
#	 (list <DriveFolderNameList>) [<DriveFileParentAttribute>]))
#	[(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) |
#	 returnidonly]
def createDriveFolderPath(users):
  csvPF = None
  addCSVData = {}
  fullPath = parentSpecified = returnIdOnly = False
  parentBody = {}
  parentParms = initDriveFileAttributes()
  driveFolderNameList = []
  pathDelimiter = '/'
  while Cmd.ArgumentsRemaining():
    myarg = getArgument()
    if myarg == 'pathdelimiter':
      pathDelimiter = getCharacter()
    elif myarg == 'fullpath':
      folderPathLocation = Cmd.Location()
      driveFolderNameList = getString(Cmd.OB_DRIVE_FOLDER_PATH).lstrip(pathDelimiter).strip(' ').split(pathDelimiter)
      if len(driveFolderNameList) > 0:
        if driveFolderNameList[0].lower() == MY_DRIVE.lower():
          parentParms[DFA_PARENTID] = ROOT
          driveFolderNameList = driveFolderNameList[1:]
        elif driveFolderNameList[0].lower() == SHARED_DRIVES.lower() and len(driveFolderNameList) > 1:
          parentParms[DFA_SHAREDDRIVE_PARENT] = driveFolderNameList[1]
          driveFolderNameList = driveFolderNameList[2:]
        else:
          usageErrorExit(Msg.FULL_PATH_MUST_START_WITH_DRIVE.format(MY_DRIVE, f'{SHARED_DRIVES}{pathDelimiter}<SharedDriveName>'))
        fullPath = True
    elif myarg == 'path':
      folderPathLocation = Cmd.Location()
      driveFolderNameList = getString(Cmd.OB_DRIVE_FOLDER_PATH).strip(' ').split(pathDelimiter)
    elif myarg == 'list':
      folderPathLocation = Cmd.Location()
      driveFolderNameList = shlexSplitList(getString(Cmd.OB_DRIVE_FOLDER_NAME_LIST), dataDelimiter=',')
    elif getDriveFileParentAttribute(myarg, parentParms):
      parentSpecified = True
    elif myarg == 'returnidonly':
      returnIdOnly = True
    elif myarg == 'csv':
      csvPF = CSVPrintFile(['User', 'name', 'id', 'status', 'pathIndex', 'pathLength'], 'sortall')
    elif csvPF and myarg == 'todrive':
      csvPF.GetTodriveParameters()
    elif csvPF and myarg == 'addcsvdata':
      getAddCSVData(addCSVData)
    else:
      unknownArgumentExit()
  if not driveFolderNameList:
    Cmd.SetLocation(folderPathLocation)
    emptyArgumentExit('fullpath|path|list')
  if fullPath and parentSpecified:
    usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('fullpath', '<DriveFileParentAttribute>'))
  for folderName in driveFolderNameList:
    if not folderName.strip():
      Cmd.SetLocation(folderPathLocation)
      usageErrorExit(Msg.ALL_FOLDER_NAMES_MUST_BE_NON_BLANK.format(driveFolderNameList))
  if csvPF:
    if addCSVData:
      csvPF.AddTitles(sorted(addCSVData.keys()))
  i, count, users = getEntityArgument(users)
  for user in users:
    i += 1
    user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
    if not drive:
      continue
    if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms,
                                   defaultToRoot=True, entityType=Ent.DRIVE_FOLDER):
      continue
    parentId = parentBody['parents'][0]
    if parentParms.get('searchargs', {}).get('corpora', ''):
      query = ANY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS
    else:
      query = MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS
    errors = False
    createOnly = False
    if not returnIdOnly and not csvPF:
      entityPerformAction([Ent.USER, user, Ent.DRIVE_FOLDER_PATH, ''], i, count)
    jcount = len(driveFolderNameList)
    Ind.Increment()
    j = 0
    for folderName in driveFolderNameList:
      j += 1
      try:
        folderFound = False
        if not createOnly:
          op = 'Find Folder'
          result = callGAPIpages(drive.files(), 'list', 'files',
                                 throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INSUFFICIENT_PERMISSIONS],
                                 retryReasons=[GAPI.UNKNOWN_ERROR],
                                 q=query.format(escapeDriveFileName(folderName), parentId),
                                 supportsAllDrives=True, includeItemsFromAllDrives=True,
                                 fields='nextPageToken,files(id,name)')
          if result:
            if len(result) > 1:
              entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], f'{op}: {len(result)} Folders with same name')
              errors = True
              break
            parentId = result[0]['id']
            parentName = result[0]['name']
            folderFound = True
            Act.Set(Act.EXISTS)
        if not folderFound:
          op = 'Create Folder'
          result = callGAPI(drive.files(), 'create',
                            throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
                                                                        GAPI.UNKNOWN_ERROR, GAPI.BAD_REQUEST,
                                                                        GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
                            body={'parents': [parentId], 'name': folderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id,name', supportsAllDrives=True)
          parentId = result['id']
          parentName = result['name']
          createOnly = True
          Act.Set(Act.CREATE)
      except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
              GAPI.unknownError, GAPI.badRequest, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
        entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], f'{op}: {str(e)}', j, jcount)
        errors = True
        break
      except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
        userDriveServiceNotEnabledWarning(user, str(e), i, count)
        errors = True
        break
      if not returnIdOnly:
        if not csvPF:
          entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER_NAME, f'{parentName}({parentId})'],
                                j, jcount)
        else:
          row = {'User': user, 'name': parentName, 'id': parentId, 'status': Act.Performed(), 'pathIndex': j, 'pathLength': jcount}
          if addCSVData:
            row.update(addCSVData)
          csvPF.WriteRow(row)
    if returnIdOnly and not errors:
      writeStdout(f'{parentId}\n')
    Ind.Decrement()
  if csvPF:
    csvPF.writeCSVfile('Folders')

# gam <UserTypeEntity> create drivefileshortcut <DriveFileEntity> [shortcutname <String>]
#	[<DriveFileParentAttribute>|convertparents]
#	[(csv [todrive <ToDriveAttribute>*]) | returnidonly]
def createDriveFileShortcut(users):
  csvPF = baseShortcutName = None
  convertParents = newParentsSpecified = returnIdOnly = False
  fileIdEntity = getDriveFileEntity()
  parentBody = {}
  parentParms = initDriveFileAttributes()
  while Cmd.ArgumentsRemaining():
    myarg = getArgument()
    if myarg == 'shortcutname':
      baseShortcutName = getString(Cmd.OB_DRIVE_FILE_NAME)
    elif myarg == 'returnidonly':
      returnIdOnly = True
    elif myarg == 'csv':
      csvPF = CSVPrintFile(['User', 'name', 'id', 'targetName', 'targetId'], 'sortall')
    elif csvPF and myarg == 'todrive':
      csvPF.GetTodriveParameters()
    elif getDriveFileParentAttribute(myarg, parentParms):
      if convertParents:
        usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'convertparents'))
      newParentsSpecified = True
    elif myarg == 'convertparents':
      if newParentsSpecified:
        usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, '<DriveFileParentAttribute>'))
      convertParents = True
    else:
      unknownArgumentExit()
  if fileIdEntity['query']:
    fileIdEntity['query'] = fileIdEntity['query']+AND_NOT_SHORTCUT
  elif fileIdEntity['shareddrivefilequery']:
    fileIdEntity['shareddrivefilequery'] = fileIdEntity['shareddrivefilequery']+AND_NOT_SHORTCUT
  i, count, users = getEntityArgument(users)
  for user in users:
    i += 1
    user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
    if not returnIdOnly and not csvPF:
      entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_SHORTCUT,
                                                 Act.MODIFIER_FOR, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
    if jcount == 0:
      continue
    if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
      continue
    if newParentsSpecified:
      newParents = parentBody['parents']
      numNewParents = len(newParents)
    elif not convertParents:
      try:
        rootFolderId = callGAPI(drive.files(), 'get',
                                throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
                                fileId=ROOT, fields='id')['id']
      except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
        userDriveServiceNotEnabledWarning(user, str(e), i, count)
        continue
      newParents = [rootFolderId]
      numNewParents = 1
    Ind.Increment()
    j = 0
    for fileId in fileIdEntity['list']:
      j += 1
      Act.Set(Act.CREATE)
      try:
        target = callGAPI(drive.files(), 'get',
                          throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
                          fileId=fileId, fields='mimeType,name,parents', supportsAllDrives=True)
      except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.invalid, GAPI.badRequest,
              GAPI.fileNotFound, GAPI.unknownError, GAPI.teamDrivesSharingRestrictionNotAllowed) as e:
        entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], str(e), j, jcount)
        continue
      except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
        userDriveServiceNotEnabledWarning(user, str(e), i, count)
        break
      targetName = target['name']
      if baseShortcutName:
        shortcutName = baseShortcutName.replace('#filename#', targetName)
      else:
        shortcutName = targetName
      targetEntityType = _getEntityMimeType(target)
      if convertParents:
        newParents = target.get('parents', [])[:-1]
        numNewParents = len(newParents)
        if numNewParents <= 1:
          entityActionNotPerformedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, None],
                                          Msg.NO_PARENTS_TO_CONVERT_TO_SHORTCUTS, j, jcount)
          continue
      removeParents = []
      body = {'name': shortcutName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': None, 'shortcutDetails': {'targetId': fileId}}
      if not returnIdOnly and not csvPF:
        entityPerformActionNumItems([Ent.USER, user, targetEntityType, targetName], numNewParents, Ent.DRIVE_FILE_SHORTCUT, j, jcount)
      try:
        existingShortcuts = callGAPI(drive.files(), 'list',
                                     throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
                                     retryReasons=[GAPI.UNKNOWN_ERROR],
                                     supportsAllDrives=True, includeItemsFromAllDrives=True,
                                     q=f"shortcutDetails.targetId = '{fileId}' and trashed = False", fields='files(id,name,parents)')['files']
      except (GAPI.invalidQuery, GAPI.invalid, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
        existingShortcuts = []
      Ind.Increment()
      k = 0
      for parentId in newParents:
        k += 1
        duplicateShortcut = False
        for shortcut in existingShortcuts:
          if parentId in shortcut['parents'] and shortcutName == shortcut['name']:
            entityActionNotPerformedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, f'{shortcut["name"]}({shortcut["id"]})'],
                                            Msg.DUPLICATE, k, numNewParents)
            duplicateShortcut = True
            break
        if duplicateShortcut:
          continue
        body['parents'] = [parentId]
        try:
          result = callGAPI(drive.files(), 'create',
                            throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
                                                                        GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
                                                                        GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
                                                                        GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
                                                                        GAPI.SHORTCUT_TARGET_INVALID],
                            body=body, fields='id,name', supportsAllDrives=True)
          removeParents.append(parentId)
          if returnIdOnly:
            writeStdout(f'{result["id"]}\n')
          elif not csvPF:
            entityActionPerformed([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, f'{result["name"]}({result["id"]})'],
                                  k, numNewParents)
          else:
            csvPF.WriteRow({'User': user, 'name': result['name'], 'id': result['id'], 'targetName': targetName, 'targetId': fileId})
        except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
                GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
                GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
          entityActionFailedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), k, numNewParents)
      Ind.Decrement()
      if convertParents and removeParents:
        if not returnIdOnly and not csvPF:
          lcount = len(removeParents)
          Act.Set(Act.DELETE)
          entityPerformActionNumItems([Ent.USER, user, targetEntityType, targetName], lcount, Ent.DRIVE_PARENT_FOLDER_REFERENCE, j, jcount)
        try:
          callGAPI(drive.files(), 'update',
                   throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
                   fileId=fileId,
                   removeParents=','.join(removeParents), body={}, fields='id', supportsAllDrives=True)
          if not returnIdOnly and not csvPF:
            Ind.Increment()
            for l, parent in enumerate(removeParents):
              entityActionPerformed([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_PARENT_FOLDER_REFERENCE, parent], l+1, lcount)
            Ind.Decrement()
        except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.invalid, GAPI.badRequest,
                GAPI.fileNotFound, GAPI.unknownError, GAPI.teamDrivesSharingRestrictionNotAllowed) as e:
          entityActionFailedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_PARENT_FOLDER_REFERENCE, str(l)], str(e), j, jcount)
    Ind.Decrement()
  if csvPF:
    csvPF.writeCSVfile('Shortcuts')

SHORTCUT_CODE_VALID = 0
SHORTCUT_CODE_SHORTCUT_NOT_FOUND = 1
SHORTCUT_CODE_NOT_A_SHORTCUT = 2
SHORTCUT_CODE_TARGET_NOT_FOUND = 3
SHORTCUT_CODE_MIMETYPE_MISMATCH = 4

# gam <UserTypeEntity> check drivefileshortcut <DriveFileEntity>
#	[csv [todrive <ToDriveAttribute>*]]
def checkDriveFileShortcut(users):
  csvPF = None
  fileIdEntity = getDriveFileEntity()
  while Cmd.ArgumentsRemaining():
    myarg = getArgument()
    if myarg == 'csv':
      csvPF = CSVPrintFile(['User', 'name', 'id', 'owner', 'parentId', 'shortcutDetails.targetId', 'shortcutDetails.targetMimeType',
                            'targetName', 'targetId', 'targetMimeType', 'code'], 'sortall')
    elif csvPF and myarg == 'todrive':
      csvPF.GetTodriveParameters()
    else:
      unknownArgumentExit()
  scfields = 'id,name,mimeType,owners(emailAddress),parents,shortcutDetails'
  trfields = 'id,name,mimeType'
  i, count, users = getEntityArgument(users)
  for user in users:
    i += 1
    user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
    if not csvPF:
      entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DRIVE_FILE_SHORTCUT, i, count)
    if jcount == 0:
      continue
    Ind.Increment()
    j = 0
    for fileId in fileIdEntity['list']:
      j += 1
      row = {'User': user, 'id': fileId}
      try:
        scresult = callGAPI(drive.files(), 'get',
                            throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
                            fileId=fileId, fields=scfields, supportsAllDrives=True)
        row['name'] = scresult['name']
        if scresult['mimeType'] != MIMETYPE_GA_SHORTCUT:
          if not csvPF:
            entityActionFailedWarning([Ent.USER, user, _getEntityMimeType(scresult), fileId],
                                      Msg.INVALID_MIMETYPE.format(scresult['mimeType'], MIMETYPE_GA_SHORTCUT), j, jcount)
          else:
            row['code'] = SHORTCUT_CODE_NOT_A_SHORTCUT
            csvPF.WriteRow(row)
          continue
        if 'owners' in scresult:
          row['owner'] = scresult['owners'][0]['emailAddress']
        row['parentId'] = scresult['parents'][0]
        row[f'shortcutDetails{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}targetId'] = scresult['shortcutDetails']['targetId']
        row[f'shortcutDetails{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}targetMimeType'] = scresult['shortcutDetails']['targetMimeType']
        trfileId = scresult['shortcutDetails']['targetId']
        try:
          trresult = callGAPI(drive.files(), 'get',
                              throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
                              fileId=trfileId, fields=trfields, supportsAllDrives=True)
          row['targetName'] = trresult['name']
          row['targetId'] = trresult['id']
          row['targetMimeType'] = trresult['mimeType']
          entityList = [Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, f"{scresult['name']}({fileId})",
                        _getEntityMimeType(trresult), f"{trresult['name']}({trfileId})"]
          if scresult['shortcutDetails']['targetMimeType'] == trresult['mimeType']:
            if not csvPF:
              entityActionPerformed(entityList, j, jcount)
            else:
              row['code'] = SHORTCUT_CODE_VALID
          else:
            if not csvPF:
              entityActionFailedWarning(entityList, Msg.MIMETYPE_MISMATCH.format(scresult['shortcutDetails']['targetMimeType'], trresult['mimeType']), j, jcount)
            else:
              row['code'] = SHORTCUT_CODE_MIMETYPE_MISMATCH
        except GAPI.fileNotFound:
          if not csvPF:
            entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, f"{scresult['name']}({fileId})",
                                       _getTargetEntityMimeType(scresult), trfileId], Msg.NOT_FOUND, j, jcount)
          else:
            row['code'] = SHORTCUT_CODE_TARGET_NOT_FOUND
      except GAPI.fileNotFound:
        if not csvPF:
          entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, fileId], Msg.NOT_FOUND, j, jcount)
        else:
          row['code'] = SHORTCUT_CODE_SHORTCUT_NOT_FOUND
      except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
        userDriveServiceNotEnabledWarning(user, str(e), i, count)
        break
      if csvPF:
        csvPF.WriteRow(row)
    Ind.Decrement()
  if csvPF:
    csvPF.writeCSVfile('Check Shortcuts')

# gam <UserTypeEntity> update drivefile <DriveFileEntity> [copy] [returnidonly|returnlinkonly]
#	[(localfile <FileName>|-)|(url <URL>)]
#	[retainname | (newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
#	[stripnameprefix <String>]
#	[timestamp <Boolean>]] [timeformat <DateTimeFormat>]
#	<DriveFileUpdateAttribute>*
#	[(gsheet|csvsheet <SheetEntity> [clearfilter])|(addsheet <String>)]
#	[charset <String>] [columndelimiter <Character>]
def updateDriveFile(users):
  fileIdEntity = getDriveFileEntity()
  body = {}
  newName = None
  assignLocalName = True
  parameters = initDriveFileAttributes()
  media_body = None
  addSheetBody = addSheetEntity = None
  updateSheetEntity = None
  clearFilter = False
  preserveModifiedTime = False
  encoding = GC.Values[GC.CHARSET]
  columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
  returnIdLink = None
  operation = 'update'
  while Cmd.ArgumentsRemaining():
    myarg = getArgument()
    if myarg == 'copy':
      operation = 'copy'
      Act.Set(Act.COPY)
    elif myarg == 'returnidonly':
      returnIdLink = 'id'
    elif myarg == 'returnlinkonly':
      returnIdLink = 'webViewLink'
    elif myarg == 'retainname':
      assignLocalName = False
    elif myarg == 'newfilename':
      newName = getString(Cmd.OB_DRIVE_FILE_NAME)
      assignLocalName = False
    elif getDriveFileAddRemoveParentAttribute(myarg, parameters):
      pass
    elif myarg == 'addsheet':
      if updateSheetEntity:
        usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'csvsheet'))
      sheetTitle = getString(Cmd.OB_STRING)
      addSheetEntity = {'sheetType': Ent.SHEET, 'sheetValue': sheetTitle, 'sheetId': None, 'sheetTitle': sheetTitle}
      addSheetBody = {'requests': [{'addSheet': {'properties': {'title': sheetTitle, 'sheetType': 'GRID'}}}]}
    elif myarg in {'gsheet', 'csvsheet'}:
      if addSheetEntity:
        usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'addsheet'))
      updateSheetEntity = getSheetEntity(False)
    elif myarg == 'clearfilter':
      clearFilter = getBoolean()
    elif myarg == 'charset':
      encoding = getString(Cmd.OB_CHAR_SET)
    elif myarg == 'columndelimiter':
      columnDelimiter = getCharacter()
    else:
      getDriveFileAttribute(myarg, body, parameters, True)
  if assignLocalName and parameters[DFA_LOCALFILENAME] and parameters[DFA_LOCALFILENAME] != '-':
    newName = parameters[DFA_LOCALFILENAME]
  if newName:
    if parameters[DFA_STRIPNAMEPREFIX] and newName.startswith(parameters[DFA_STRIPNAMEPREFIX]):
      newName = newName[len(parameters[DFA_STRIPNAMEPREFIX]):]
  if operation == 'update' and parameters[DFA_LOCALFILEPATH]:
    if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
      setPreservedFileTimes(body, parameters, True)
    if not addSheetEntity and not updateSheetEntity:
      media_body = getMediaBody(parameters)
  elif operation == 'update' and parameters[DFA_URL]:
    addSheetEntity =  updateSheetEntity = None
    media_body = getMediaBody(parameters)
    body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
  elif operation == 'update' and parameters[DFA_PRESERVE_FILE_TIMES]:
    preserveModifiedTime = True
  i, count, users = getEntityArgument(users)
  for user in users:
    i += 1
    user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
                                                  entityType=Ent.DRIVE_FILE_OR_FOLDER if returnIdLink is None else None)
    if jcount == 0:
      continue
    if not _getDriveFileParentInfo(drive, user, i, count, body, parameters, defaultToRoot=False):
      continue
    newParents = body.pop('parents', [])
    if operation == 'update':
      status, addParentsBase, removeParentsBase = _getDriveFileAddRemoveParentInfo(user, i, count, parameters, drive)
      if not status:
        continue
      Ind.Increment()
      j = 0
      for fileId in fileIdEntity['list']:
        j += 1
        try:
          addParents = addParentsBase[:]
          removeParents = removeParentsBase[:]
          if newParents or (not newName and parameters[DFA_REPLACEFILENAME]) or preserveModifiedTime:
            result = callGAPI(drive.files(), 'get',
                              throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
                              fileId=fileId, fields='name,parents,modifiedTime', supportsAllDrives=True)
            if newParents:
              addParents.extend(newParents)
              removeParents.extend(result.get('parents', []))
            if not newName and parameters[DFA_REPLACEFILENAME]:
              body['name'] = processFilenameReplacements(result['name'], parameters[DFA_REPLACEFILENAME])
            if preserveModifiedTime:
              body['modifiedTime'] = result['modifiedTime']
          if newName:
            if parameters[DFA_REPLACEFILENAME]:
              body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
            else:
              body['name'] = newName
            if parameters[DFA_TIMESTAMP]:
              addTimestampToFilename(parameters, body)
          if addSheetEntity or updateSheetEntity:
            entityValueList = [Ent.USER, user, Ent.DRIVE_FILE_ID, fileId]
            try:
              result = callGAPI(drive.files(), 'get',
                                throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
                                fileId=fileId, fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
              if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
                entityActionNotPerformedWarning(entityValueList, f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}', j, jcount)
                continue
              if not result['capabilities']['canEdit']:
                entityActionNotPerformedWarning(entityValueList, Msg.NOT_WRITABLE, j, jcount)
                continue
              _, sheet = buildGAPIServiceObject(API.SHEETS, user)
              if sheet is None:
                continue
              if addSheetEntity:
                addresult = callGAPI(sheet.spreadsheets(), 'batchUpdate',
                                     throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
                                     spreadsheetId=fileId, body=addSheetBody)
                sheetEntity = addSheetEntity.copy()
                sheetEntity['sheetId'] = addresult['replies'][0]['addSheet']['properties']['sheetId']
                entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetTitle']])
                sheetEntity['sheetType'] = Ent.SHEET_ID # Temporarily set addsheet type to ID
              else:
                sheetEntity = updateSheetEntity.copy()
                entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetValue']])
              spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
                                     throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
                                     spreadsheetId=fileId,
                                     fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
              sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
              if sheetId is None:
                entityActionNotPerformedWarning(entityValueList, Msg.NOT_FOUND, j, jcount)
                continue
              if protectedSheetId(spreadsheet, sheetId):
                entityActionNotPerformedWarning(entityValueList, Msg.NOT_WRITABLE, j, jcount)
                continue
              if addSheetEntity: # Restore addsheet type
                sheetEntity['sheetType'] = Ent.SHEET
              result = callGAPI(drive.files(), 'update',
                                throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
                                                                              GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
                                                                              GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT,
                                                                              GAPI.TEAMDRIVES_PARENT_LIMIT,
                                                                              GAPI.TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED,
                                                                              GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED],
                                fileId=fileId,
                                ocrLanguage=parameters[DFA_OCRLANGUAGE],
                                keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
                                useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
                                addParents=','.join(addParents), removeParents=','.join(removeParents),
                                body=body, fields='id,name,mimeType,webViewLink', supportsAllDrives=True)
### File size check??
              sbody = {'requests': []}
              if clearFilter and updateSheetEntity:
                sbody['requests'].append({'clearBasicFilter': {'sheetId': sheetId}})
              sbody['requests'].append({'updateCells': {'range': {'sheetId': sheetId}, 'fields': '*'}})
              sbody['requests'].append({'pasteData': {'coordinate': {'sheetId': sheetId, 'rowIndex': '0', 'columnIndex': '0'},
                                                      'data': readFile(parameters[DFA_LOCALFILEPATH], encoding=encoding),
                                                      'type': 'PASTE_NORMAL', 'delimiter': columnDelimiter}})
              callGAPI(sheet.spreadsheets(), 'batchUpdate',
                       throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
                       spreadsheetId=fileId, body=sbody)
              if returnIdLink:
                writeStdout(f'{result[returnIdLink]}\n')
              else:
                entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']],
                                                      Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME], j, jcount)
            except GAPI.fileNotFound as e:
              entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_ID, fileId], str(e), j, jcount)
            except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
                    GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
                    GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
              entityActionFailedWarning(entityValueList, str(e), j, jcount)
            except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
              userDriveServiceNotEnabledWarning(user, str(e), i, count)
              break
          else:
            result = callGAPI(drive.files(), 'update',
                              throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
                                                                            GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
                                                                            GAPI.FILE_NEVER_WRITABLE, GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT,
                                                                            GAPI.SHARE_OUT_NOT_PERMITTED, GAPI.SHARE_OUT_NOT_PERMITTED_TO_USER,
                                                                            GAPI.TEAMDRIVES_PARENT_LIMIT, GAPI.TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED,
                                                                            GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
                                                                            GAPI.CROSS_DOMAIN_MOVE_RESTRICTION, GAPI.UPLOAD_TOO_LARGE,
                                                                            GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED,
                                                                            GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
                                                                            GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED],
                              fileId=fileId,
                              ocrLanguage=parameters[DFA_OCRLANGUAGE],
                              keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
                              useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
                              addParents=','.join(addParents), removeParents=','.join(removeParents),
                              media_body=media_body, body=body, fields='id,name,mimeType,webViewLink',
                              supportsAllDrives=True)
            if result:
              if returnIdLink:
                writeStdout(f'{result[returnIdLink]}\n')
              elif media_body:
                entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
                                                      Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME] or parameters[DFA_URL], j, jcount)
              else:
                entityActionPerformed([Ent.USER, user, _getEntityMimeType(result), result['name']], j, jcount)
            else:
              if returnIdLink:
                writeStdout(f'{fileId}\n')
              else:
                entityActionPerformed([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], j, jcount)
        except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
                GAPI.unknownError, GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
                GAPI.fileNeverWritable, GAPI.cannotModifyViewersCanCopyContent,
                GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
                GAPI.teamDrivesParentLimit, GAPI.teamDrivesFolderMoveInNotSupported,
                GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.crossDomainMoveRestriction,
                GAPI.uploadTooLarge, GAPI.teamDrivesShortcutFileNotSupported,
                GAPI.fileOwnerNotMemberOfWriterDomain, GAPI.fileWriterTeamDriveMoveInDisabled) as e:
          entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
        except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
          userDriveServiceNotEnabledWarning(user, str(e), i, count)
          break
      Ind.Decrement()
    else:
      if newName:
        if parameters[DFA_REPLACEFILENAME]:
          body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
        else:
          body['name'] = newName
        if parameters[DFA_TIMESTAMP]:
          addTimestampToFilename(parameters, body)
      Ind.Increment()
      j = 0
      for fileId in fileIdEntity['list']:
        j += 1
        try:
          result = callGAPI(drive.files(), 'copy',
                            throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT],
                            fileId=fileId,
                            ignoreDefaultVisibility=parameters[DFA_IGNORE_DEFAULT_VISIBILITY],
                            keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
                            body=body, fields='id,name,webViewLink', supportsAllDrives=True)
          if returnIdLink:
            writeReturnIdLink(returnIdLink, parameters[DFA_LOCALMIMETYPE], result)
          else:
            entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, fileId],
                                                               Act.MODIFIER_TO, result['name'], [Ent.DRIVE_FILE_ID, result['id']], j, jcount)
        except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
                GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
                GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
                GAPI.cannotModifyViewersCanCopyContent) as e:
          entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, fileId], str(e), j, jcount)
        except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,) as e:
          entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, fileId], str(e), j, jcount)
          break
        except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
          userDriveServiceNotEnabledWarning(user, str(e), i, count)
          break
      Ind.Decrement()

STAT_FOLDER_TOTAL = 0
STAT_FOLDER_COPIED_MOVED = 1
STAT_FOLDER_SHORTCUT_CREATED = 2
STAT_FOLDER_SHORTCUT_EXISTS = 3
STAT_FOLDER_MERGED = 4
STAT_FOLDER_DUPLICATE = 5
STAT_FOLDER_FAILED = 6
STAT_FOLDER_NOT_WRITABLE = 7
STAT_FOLDER_PERMISSIONS_FAILED = 8
STAT_FILE_TOTAL = 9
STAT_FILE_COPIED_MOVED = 10
STAT_FILE_SHORTCUT_CREATED = 11
STAT_FILE_SHORTCUT_EXISTS = 12
STAT_FILE_DUPLICATE = 13
STAT_FILE_FAILED = 14
STAT_FILE_NOT_COPYABLE_MOVABLE = 15
STAT_FILE_IN_SKIPIDS = 16
STAT_FILE_PERMISSIONS_FAILED = 17
STAT_FILE_PROTECTEDRANGES_FAILED = 18
STAT_USER_NOT_ORGANIZER = 19
STAT_LENGTH = 20

FOLDER_SUBTOTAL_STATS = [STAT_FOLDER_COPIED_MOVED, STAT_FOLDER_SHORTCUT_CREATED, STAT_FOLDER_SHORTCUT_EXISTS,
                         STAT_FOLDER_DUPLICATE, STAT_FOLDER_MERGED, STAT_FOLDER_FAILED, STAT_FOLDER_NOT_WRITABLE]
FILE_SUBTOTAL_STATS = [STAT_FILE_COPIED_MOVED, STAT_FILE_SHORTCUT_CREATED, STAT_FILE_SHORTCUT_EXISTS,
                       STAT_FILE_DUPLICATE, STAT_FILE_FAILED, STAT_FILE_NOT_COPYABLE_MOVABLE, STAT_FILE_IN_SKIPIDS]

def _initStatistics():
  return [0] * STAT_LENGTH

def _incrStatistic(statistics, stat):
  statistics[stat] += 1
  if stat in FOLDER_SUBTOTAL_STATS:
    statistics[STAT_FOLDER_TOTAL] += 1
  elif stat in FILE_SUBTOTAL_STATS:
    statistics[STAT_FILE_TOTAL] += 1

