import {service} from '@loopback/core';
import {indexOf, isString} from 'lodash';
import {ObjectTreeService} from '../../services';
import {ActionEntityService} from '../../services/action-entity/action-entity.service';
import {ObjectNodeService} from '../../services/object-node/object-node.service';
import {LocationService} from '../location/location.service';
import {ApplicationError} from './../../helper/application-error';
import {EntityName} from './../../models/entity-name';
import {ObjectNode} from './../../models/object-node.model';
import {ObjectTree} from './../../models/object-tree.model';
import {ObjectType} from './../../models/object-type.model';
import {
  CurrentContext,
  ExpectedValue,
} from './../../services/application.service';
import {
  ObjectNodeDefinitionService,
  OneOfTreeOption,
} from './../../services/entity-definition/object-node-definition.service';
import {InsideRestService} from './../../services/inside-rest/inside-rest.service';
import {SortMethod} from './../../services/object-tree/action-tree.service';
import {ObjectTypeService} from './../../services/object-type.service';
import {TransientNodeService} from './../../services/transient-entity/transient-node.service';
import {UriCompleteService} from './../../services/uri-complete/uri-complete.service';
import {
  CONTENT_IMAGE_PROVIDER,
  DISPLAYED_IMAGE_GALLERY_TYPE,
  IMAGE_ALBUM_TYPE,
  IMAGE_GALLERIES_TYPE,
  IMAGE_GALLERY_REFERRER_TYPE,
  IMAGE_GALLERY_SELECTOR_TYPE,
  IMAGE_GALLERY_TYPE,
  IMAGE_TYPE,
} from './content-image.const';
import {ContentImageService, Image} from './content-image.definition';
import {InterceptImageService} from './intercept-image.service';
import {TransientImageService} from './transient-image.service';

import {APPLICATION_ERRORS} from '@jacquesparis/objects-model';
import {
  GeneratedResponse,
  HtmlGeneratedResponse,
  normalizeFileName,
  RedirectGeneratedResponse,
  ZipGeneratedResponse,
} from '../../helper/generated-response';
import {LogService} from '../log/log.service';
import {ServerConfigService} from '../server-config/server-config.service';

class ImagesHolderEntry {
  public parentPath: string;
  public treeId: string;
  public size: number;
  public name: string;
  public filename: string;
  constructor(
    imageHolder: ImagesHolder,
    public includeChildren: boolean = true,
  ) {
    this.parentPath = imageHolder.path;
    this.treeId = imageHolder.tree.id;
    this.size = includeChildren ? imageHolder.totalSize : imageHolder.levelSize;
    this.name = imageHolder.tree.name;
    this.filename = normalizeFileName(this.parentPath + this.name + '.zip');
  }
}

class ImagesHolder {
  public totalSize = 0;
  public levelSize = 0;
  public level = 0;
  public path = '';
  private children: ImagesHolder[] = [];
  public tree: {id: string; name: string};
  constructor(objectTree: ObjectTree, public parent?: ImagesHolder) {
    this.tree = {
      id: objectTree.id,
      name: objectTree.treeNode.title || objectTree.treeNode.name,
    };
    if (parent) {
      this.level = parent.level + 1;
      this.path = parent.path + parent.tree.name + '/';
      parent.children.push(this);
    }
  }
  public addSize(size: number) {
    this.levelSize += size;
    this.addChildSize(size);
  }
  private addChildSize(size: number) {
    this.totalSize += size;
    if (this.parent) {
      this.parent.addChildSize(size);
    }
  }

  public getEntries(): ImagesHolderEntry[] {
    const entries = [];
    if (this.levelSize > 0) {
      entries.push(new ImagesHolderEntry(this, false));
    }
    for (const child of this.children) {
      entries.push(new ImagesHolderEntry(child));
    }
    return entries;
  }
}

export class ActionImageService {
  constructor(
    @service(ActionEntityService)
    protected actionEntityService: ActionEntityService,
    @service(ObjectNodeService)
    private objectNodeService: ObjectNodeService,
    @service(ObjectTypeService)
    private objectTypeService: ObjectTypeService,
    @service(InsideRestService) private insideRestService: InsideRestService,
    @service(UriCompleteService) private uriCompleteService: UriCompleteService,
    @service(ObjectNodeDefinitionService)
    private objectNodeDefinitionService: ObjectNodeDefinitionService,
    @service(TransientImageService)
    private transientImageService: TransientImageService,
    @service(InterceptImageService)
    private interceptImageService: InterceptImageService,
    @service(TransientNodeService)
    private transientNodeService: TransientNodeService,
    @service(LocationService) private locationService: LocationService,
    @service(ObjectTreeService) private objectTreeService: ObjectTreeService,
    @service(ContentImageService)
    private contentImageService: ContentImageService,
    @service(LogService) private logService: LogService,
    @service(ServerConfigService)
    private serverConfigService: ServerConfigService,
  ) {
    this.actionEntityService.registerNewViewFunction<ObjectTree>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Load gallery in zip file',
      EntityName.objectTree,
      'zip',
      DISPLAYED_IMAGE_GALLERY_TYPE.name,
      this.downloadGallery.bind(this),
      'read',
    );

    this.actionEntityService.registerNewViewFunction<ObjectTree>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Load all galleries in zip file',
      EntityName.objectTree,
      'zipAll',
      IMAGE_GALLERIES_TYPE.name,
      this.downloadGalleries.bind(this),
      'read',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Load and create several Image entities in an Image Album',
      EntityName.objectNode,
      'load',
      IMAGE_ALBUM_TYPE.name,
      this.loadImageGalleryNode.bind(this),
      'create',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Load and create several Image entities in a referenced Image Gallery',
      EntityName.objectNode,
      'load',
      IMAGE_GALLERY_REFERRER_TYPE.name,
      this.loadImageGalleryReferrerNode.bind(this),
      'create',
    );
    this.actionEntityService.registerNewViewFunction<ObjectNode>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Download referred images in zip file',
      EntityName.objectNode,
      'zip',
      IMAGE_GALLERY_REFERRER_TYPE.name,
      this.downloadReferredImages.bind(this),
      'read',
    );
    this.actionEntityService.registerNewViewFunction<ObjectTree>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Download all content referred images in zip file',
      EntityName.objectTree,
      'zipAll',
      IMAGE_GALLERY_REFERRER_TYPE.name,
      this.downloadContentImages.bind(this),
      'read',
    );
    this.actionEntityService.registerNewViewFunction<ObjectTree>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Download all content referred images in separated zip files',
      EntityName.objectTree,
      'zipAllFiles',
      IMAGE_GALLERY_REFERRER_TYPE.name,
      this.downloadContentImagesFiles.bind(this),
      'read',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Create new Image Gallery for a gallery referrer',
      EntityName.objectNode,
      'createGallery',
      IMAGE_GALLERY_REFERRER_TYPE.name,
      this.createGalleryImageGalleryReferrerNode.bind(this),
      'create',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Configure an Image Gallery for a gallery referrer',
      EntityName.objectNode,
      'configureGallery',
      IMAGE_GALLERY_REFERRER_TYPE.name,
      this.configureGalleryImageGalleryReferrerNode.bind(this),
      'update',
    );
    this.actionEntityService.registerNewMethodFunction<ObjectNode>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Reset image position and/or date',
      EntityName.objectNode,
      'resetInfo',
      IMAGE_TYPE.name,
      this.resetInfoImageNode.bind(this),
      'update',
    );
    this.actionEntityService.registerNewMethodFunction<ObjectNode>(
      CONTENT_IMAGE_PROVIDER,
      ActionImageService.name,
      'Reset images order',
      EntityName.objectNode,
      'resetOrder',
      IMAGE_ALBUM_TYPE.name,
      this.resetOrderImageGalleryNode.bind(this),
      'update',
    );

    this.locationService.registerLocationProvider({
      locatableItems: [
        {
          objectType: IMAGE_GALLERY_REFERRER_TYPE.name,
          entityType: EntityName.objectNode,
          treeItemsConditionField: 'selectedImages',
          treeItemsField: 'images',
          locationField: 'imagePosition',
        },
        {
          objectType: IMAGE_GALLERY_TYPE.name,
          entityType: EntityName.objectTree,
          treeItemsField: 'images',
          locationField: 'imagePosition',
        },
      ],
    });
    this.serverConfigService.registerNewConfigurationKey('pctZipMaxSize', {
      type: 'number',
      title: 'Percentage max image zip file size (default 70)',
    });
  }

  public async resetOrderImageGalleryNode(
    entity: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<ObjectTree> {
    const galleryTree: ObjectTree = await this.insideRestService.read<ObjectTree>(
      this.uriCompleteService.getUri(
        EntityName.objectTree,
        entity.id as string,
        ctx,
      ),
      ctx,
    );
    await new SortMethod(this.objectNodeService).runMethod(
      galleryTree,
      galleryTree.children
        .sort((a, b) => {
          if (!a.treeNode.imageDate && !b.treeNode.imageDate) {
            return (a.treeNode.index as number) - (b.treeNode.index as number);
          }
          if (!a.treeNode.imageDate || !b.treeNode.imageDate) {
            return !a.treeNode.imageDate ? 1 : -1;
          }
          return Date.parse(a.treeNode.imageDate) <
            Date.parse(b.treeNode.imageDate)
            ? -1
            : 1;
        })
        .map((tree) => tree.treeNode.id as string),
      ctx,
    );

    return this.actionEntityService.getEntity<ObjectTree>(
      EntityName.objectTree,
      entity.id as string,
      CurrentContext.get(ctx, {}),
    );
  }

  public async resetInfoImageNode(
    entity: ObjectNode,
    args: {position?: boolean; date?: boolean},
    ctx: CurrentContext,
  ): Promise<ObjectNode> {
    if (args.position || args.date) {
      const update: Partial<ObjectNode> = {};
      if (entity.contentImageId && !entity.contentImage) {
        await this.transientNodeService.completeReturnedEntity(entity, ctx);
      }
      const exif: {
        latitude?: number;
        longitude?: number;
        CreateDate?: Date;
        DateTimeOriginal?: Date;
        ModifyDate?: Date;
      } = (await this.transientImageService.getImageExif(
        entity as {
          contentImage?: {base64?: string};
        },
      )) as {
        latitude?: number;
        longitude?: number;
        CreateDate?: Date;
        DateTimeOriginal?: Date;
        ModifyDate?: Date;
      };

      if (args.position) {
        const imagePosition = this.transientImageService.getPositionFromExif(
          exif,
        );
        if (imagePosition) {
          update.imagePosition = imagePosition;
        }
      }
      if (args.date) {
        const imageDate = this.transientImageService.getImageDateFromExif(exif);
        if (imageDate) {
          update.imageDate = imageDate;
        }
      }
      if (0 < Object.keys(update).length) {
        await this.objectNodeService.modifyById(
          entity.id as string,
          update,
          ctx,
        );
      }
    }
    return this.actionEntityService.getEntity<ObjectNode>(
      EntityName.objectNode,
      entity.id as string,
      CurrentContext.get(ctx, {}),
    );
  }

  protected async createImage(
    image: Image,
    parent: ObjectNode,
    ctx: CurrentContext,
    parentType?: ObjectType,
    title?: string,
  ): Promise<ObjectNode> {
    await this.interceptImageService.checkAndReduceImageSize(image);

    const childCtx: CurrentContext = CurrentContext.get(ctx, {
      nodeContext: {
        parent: new ExpectedValue(parent),
        parentType: new ExpectedValue(parentType),
      },
    });
    const nameParts = image.name.split('.');
    if (1 < nameParts.length) {
      nameParts.pop();
    }

    const exif: {
      latitude?: number;
      longitude?: number;
      CreateDate?: Date;
      DateTimeOriginal?: Date;
      ModifyDate?: Date;
    } = (await this.transientImageService.getImageExif({
      contentImage: image,
    })) as {
      latitude?: number;
      longitude?: number;
      CreateDate?: Date;
      DateTimeOriginal?: Date;
      ModifyDate?: Date;
    };
    const position = this.transientImageService.getPositionFromExif(exif);

    const date = this.transientImageService.getImageDateFromExif(exif);

    return this.objectNodeService.add(
      {
        name: title ? title : nameParts.join('.'),
        imagePosition: position,
        imageDate: date,
        parentNodeId: parent.id,
        objectTypeId: IMAGE_TYPE.name,
        contentImage: image,
      },
      childCtx,
      false,
      true,
      true,
    );
  }

  private async downloadImages(
    name: string,
    images: ObjectTree[],
    ctx: CurrentContext,
    response?: ZipGeneratedResponse,
    path = '',
  ): Promise<GeneratedResponse> {
    if (!response) {
      response = new ZipGeneratedResponse(name);
    }
    for (const image of images) {
      const content = await this.contentImageService.getContent({}, undefined, {
        contentId: image.original.contentImageId,
      });
      await this.serverConfigService.checkMemoryUsage();
      response.addBase64(path + content.name, content.base64);

      if (await this.logService.isDebug('DEBUG')) {
        if (!ctx.downloadImagesCtx) {
          ctx.downloadImagesCtx = {
            totalLength: 0,
            nbFile: 0,
          };
        }
        ctx.downloadImagesCtx.totalLength += content.base64.length;
        ctx.downloadImagesCtx.nbFile += 1;
        await this.logService.addLog(
          'DEBUG',
          ActionImageService.name,
          'downloadImages',
          `${path + content.name}, length ${content.base64.length}, added`,
          ctx,
          ctx.downloadImagesCtx,
        );
      }
    }

    return response;
  }

  public async downloadGalleries(
    galleries: ObjectTree,
    args: Object,
    ctx: CurrentContext,
  ): Promise<GeneratedResponse> {
    const response: ZipGeneratedResponse = new ZipGeneratedResponse(
      galleries.treeNode.title || galleries.treeNode.name,
    );
    for (const gallery of galleries.children) {
      await this.downloadGallery(
        gallery,
        args,
        ctx,
        response,
        (gallery.treeNode.title || gallery.treeNode.name) + '/',
      );
    }

    return response;
  }

  public async downloadGallery(
    tree: ObjectTree,
    args: Object,
    ctx: CurrentContext,
    response?: ZipGeneratedResponse,
    path = '',
  ): Promise<GeneratedResponse> {
    const images: ObjectTree[] = (
      await this.insideRestService.read(
        this.uriCompleteService.getUri(EntityName.objectTree, tree.id, ctx),
        ctx,
      )
    ).images;

    return this.downloadImages(
      tree.treeNode.title || tree.treeNode.name,
      images,
      ctx,
      response,
      path,
    );
  }

  public async downloadReferredImages(
    node: ObjectNode,
    args: {0?: string; 1?: string; 2?: string},
    ctx: CurrentContext,
    response?: ZipGeneratedResponse,
    path = '',
  ): Promise<GeneratedResponse> {
    if (!response) {
      if (args[0] && isString(args[0]) && args[0].length > 1) {
        path = args[0];
      }
      response = new ZipGeneratedResponse(path + (node.title || node.name));

      if (path) {
        path += node.title || node.name;
      }
    }
    const images: ObjectTree[] = (
      await this.insideRestService.read(
        this.uriCompleteService.getUri(
          EntityName.objectNode,
          node.id as string,
          ctx,
        ),
        ctx,
      )
    ).images;
    return this.downloadImages(
      node.title || node.name,
      images,
      ctx,
      response,
      path,
    );
  }

  public async downloadContentImagesFiles(
    tree: ObjectTree,
    args: {0?: string; 1?: string; 2?: string},
    ctx: CurrentContext,
  ) {
    const maxZipFileSize = parseInt(args[0] as string);
    const path: string = args[1] || '';

    const imagesHolder: ImagesHolder = await this.buildImagesTree(tree, ctx);
    const entries: ImagesHolderEntry[] = imagesHolder.getEntries();

    let downloadZips = '';
    for (const entry of entries) {
      downloadZips += `
      <div class="card col-12 col-sm-6 col-md-4 col-lg-3 col-xl-3 col-xxl-2" id="entry${
        entry.treeId
      }">
        <div class="card-body">
          <h5 class="card-title">${entry.name}</h5>
          <h6 class="card-subtitle mb-2 text-muted">${entry.parentPath}</h6>
          <p class="card-text${
            maxZipFileSize < entry.size ? ' text-danger' : ''
          }">Images of ${entry.name} (${
        entry.includeChildren ? '' : 'not '
      }included sub contents)</p>
          <a target="_blank" onclick="window.setTimeout(()=>{document.getElementById('entry${
            entry.treeId
          }').remove()})"  href="${this.uriCompleteService.getUri(
        entry.includeChildren ? EntityName.objectTree : EntityName.objectNode,
        entry.treeId,
        ctx,
      )}/view/${entry.includeChildren ? 'zipAll' : 'zip'}/${encodeURIComponent(
        path + entry.parentPath,
      )}" class="card-link">Download ${entry.filename}</a>
      <a  onclick="window.setTimeout(()=>{document.getElementById('entry${
        entry.treeId
      }').remove()})" href="#" class="card-link">Cancel</a>

      </div>
      </div>
      `;
    }

    const htmlError = new HtmlGeneratedResponse(`
    <!DOCTYPE html>
    <html>
      <head>
      <style>
      .card:not(:first-of-type)
        a {display:none;
    }

.card:not(:first-of-type){
  opacity:0.5
}
      </style>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css" />

        <link rel="stylesheet" href="/fontawesome/css/fontawesome.css" />
        <link rel="stylesheet" href="/fontawesome/css/regular.css" />
        <link rel="stylesheet" href="/fontawesome/css/solid.css" />

        <script src="/jquery/jquery.slim.min.js"></script>
        <script src="/bootstrap/js/bootstrap.bundle.min.js"></script>
      </head>
      <body>
        <h2 class="display-2">The Zip exceeds max size</h2>

        <div class="alert alert-warning" role="alert">
        The cumulated file size exceed ${maxZipFileSize} bytes.
        </div>
        <div class="row">
          ${downloadZips}
        </div>
      </body>
    </html>
`);
    return htmlError;
  }

  private async buildImagesTree(
    tree: ObjectTree,
    ctx: CurrentContext,
  ): Promise<ImagesHolder> {
    const imagesTree = new ImagesHolder(tree);
    if (tree.treeNode.imageGalleryObjectTreeId) {
      imagesTree.addSize(1);
    }

    for (const child of tree.children) {
      new ImagesHolder(child, imagesTree);
    }
    return imagesTree;
  }

  public async downloadContentImages(
    tree: ObjectTree,
    args: {0?: string; 1?: string; 2?: string},
    ctx: CurrentContext,
    response?: ZipGeneratedResponse,
    path = '',
  ): Promise<GeneratedResponse> {
    let recursiveCall = true;
    let initialPath = '';
    /**
     * Failed after :
     *
     * Las Bardenas Reales/Mercredi 26 octobre/Tudela/20221026_181109.jpg,
     * length 2529800,
     * added (totalLength = 525190748, nbFile = 186)
     * |  memoryUsage: (rss = 531013632, heapTotal = 202461184, heapUsed = 153659208, external = 412463114)"     */
    if (!response) {
      recursiveCall = false;
      if (args[0] && isString(args[0]) && args[0].length > 1) {
        path = args[0];
        initialPath = path;
      }
      response = new ZipGeneratedResponse(
        path + (tree.treeNode.title || tree.treeNode.name),
      );
      if (path) {
        path += tree.treeNode.title || tree.treeNode.name;
      }
    }
    try {
      if (tree.treeNode.imageGalleryObjectTreeId) {
        await this.downloadReferredImages(
          tree.treeNode,
          args,
          ctx,
          response,
          path,
        );
      }
      for (const child of tree.children) {
        const title = child.treeNode.paragraphTitle || child.treeNode.pageTitle;
        await this.downloadContentImages(
          child,
          args,
          ctx,
          response,
          path + (title ? title + '/' : ''),
        );
      }
    } catch (error) {
      if (
        !recursiveCall &&
        error instanceof ApplicationError &&
        error.errorCode === APPLICATION_ERRORS.OUT_OF_MEMORY_ERROR.errorCode
      ) {
        const maxZipFileSize = Math.ceil(
          (response.addedBase64Size *
            (await this.serverConfigService.get('pctZipMaxSize', async () => {
              return 70;
            }))) /
            100,
        );
        const baseUri = `${this.uriCompleteService.getUri(
          EntityName.objectTree,
          tree.id,
          ctx,
        )}/view/zipAllFiles/${maxZipFileSize}/${encodeURIComponent(
          initialPath,
        )}`;
        return new RedirectGeneratedResponse(baseUri);
      }
      throw error;
    }

    return response;
  }

  public async loadImageGalleryNode(
    gallery: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<ObjectTree> {
    const images: {images: Image[]; title?: string} = args as {
      images: Image[];
      title?: string;
    };
    for (const image of images.images) {
      await this.createImage(
        image,
        ctx.nodeContext.node.value,
        ctx,
        ctx.nodeContext.objectType.value,
        images.title,
      );
    }
    return this.actionEntityService.getEntity<ObjectTree>(
      EntityName.objectTree,
      gallery.id as string,
      CurrentContext.get(ctx, {}),
    );
  }

  public async configureGalleryImageGalleryReferrerNode(
    objectNode: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<ObjectNode> {
    const imageGalleryObjectTreeId = (args as {
      imageGalleryObjectTreeId: string;
    }).imageGalleryObjectTreeId;

    if (
      undefined === imageGalleryObjectTreeId ||
      '' === imageGalleryObjectTreeId
    ) {
      throw ApplicationError.missingParameter(imageGalleryObjectTreeId);
    }

    await this.objectNodeService.modifyById(
      objectNode.id as string,
      {imageGalleryObjectTreeId: imageGalleryObjectTreeId},
      CurrentContext.get(ctx),
    );

    return this.actionEntityService.getEntity<ObjectNode>(
      EntityName.objectNode,
      objectNode.id as string,
      CurrentContext.get(ctx, {}),
    );
  }

  public async createGalleryImageGalleryReferrerNode(
    objectNode: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<[ObjectTree, ObjectNode] | undefined> {
    const creationArguments: {
      name?: string;
      parentId?: string;
      childTypeId?: string;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } = args as any;
    let name: string | undefined = creationArguments.name;
    if (!name || '' === name) {
      name = objectNode.title || objectNode.name;
    }
    let parentId: string | undefined = creationArguments.parentId;
    if (!parentId || '' === parentId) {
      const galleries: ObjectNode[] = await this.transientImageService.getImageAlbumsParents(
        objectNode,
      );
      if (!galleries || 0 === galleries.length) {
        throw ApplicationError.missing({object: IMAGE_GALLERIES_TYPE.name});
      }
      parentId = galleries[0].id as string;
    }

    let childTypeId: string | undefined = creationArguments.childTypeId;

    const refererType = await this.objectTypeService.searchById(
      objectNode.objectTypeId,
    );
    if (!refererType.definition.properties.imageGalleryObjectTreeId.oneOfTree) {
      throw ApplicationError.forbidden();
    }

    const oneOfTreeOptions: OneOfTreeOption[] = refererType.definition.properties.imageGalleryObjectTreeId.oneOfTree.filter(
      (referencedTree: OneOfTreeOption) => !referencedTree.namespaceType,
    );
    const referredTypes: string[] = oneOfTreeOptions.map(
      (oneOfTree: {treeType: string}) => oneOfTree.treeType,
    );

    let galleryTypes: string[] = [];
    for (const referredType of referredTypes) {
      galleryTypes.push(
        ...(await this.objectTypeService.getImplementingTypes(referredType)),
      );
    }
    if (!childTypeId || '' === childTypeId) {
      const parentGalleryTree: ObjectTree = (await this.insideRestService.read(
        this.uriCompleteService.getUri(EntityName.objectTree, parentId, ctx),
        ctx,
      )) as ObjectTree;
      if (!parentGalleryTree.entityCtx?.actions?.creations) {
        throw ApplicationError.missing({
          object: IMAGE_GALLERIES_TYPE.name,
          parentId: parentId,
          childType: IMAGE_ALBUM_TYPE.name,
        });
      }
      const childTypeIds = Object.keys(
        parentGalleryTree.entityCtx.actions.creations,
      );
      galleryTypes = galleryTypes.filter(
        (type) => -1 < childTypeIds.indexOf(type),
      );
      if (0 === childTypeIds.length) {
        throw ApplicationError.missing({
          object: IMAGE_GALLERIES_TYPE.name,
          parentId: parentId,
          childType: IMAGE_ALBUM_TYPE.name,
        });
      }
      childTypeId = galleryTypes[0];
    }
    if (-1 === galleryTypes.indexOf(childTypeId)) {
      throw ApplicationError.unauthorizedValue({childTypeId: childTypeId});
    }
    for (let index = oneOfTreeOptions.length - 1; index--; index >= 0) {
      if (
        -1 ===
        indexOf(
          await this.objectTypeService.getImplementingTypes(
            oneOfTreeOptions[index].treeType,
          ),
          childTypeId,
        )
      ) {
        oneOfTreeOptions.splice(index, 1);
      }
    }
    if (0 === oneOfTreeOptions.length) {
      throw ApplicationError.unauthorizedValue({childTypeId: childTypeId});
    }
    const oneOfTreeOption: OneOfTreeOption = oneOfTreeOptions[0];

    const newGallery = await this.objectNodeService.add(
      {parentNodeId: parentId, objectTypeId: childTypeId, name: name},
      CurrentContext.get(ctx),
      false,
      true,
      true,
    );
    const owner = await this.objectNodeService.getNode(
      newGallery.parentOwnerId,
      CurrentContext.get(ctx),
    );
    const namespace = await this.objectNodeService.getNode(
      newGallery.parentNamespaceId,
      CurrentContext.get(ctx),
    );
    const imageGalleryObjectTreeId = this.objectNodeDefinitionService.getTreeIdFromTreeOption(
      oneOfTreeOption,
      owner,
      namespace,
      newGallery,
    );
    await this.objectNodeService.modifyById(
      objectNode.id as string,
      {imageGalleryObjectTreeId: imageGalleryObjectTreeId},
      CurrentContext.get(ctx),
    );

    return [
      await this.actionEntityService.getEntity<ObjectTree>(
        EntityName.objectTree,
        parentId,
        CurrentContext.get(ctx, {}),
      ),
      await this.actionEntityService.getEntity<ObjectNode>(
        EntityName.objectNode,
        objectNode.id as string,
        CurrentContext.get(ctx, {}),
      ),
    ];
  }

  public async loadImageGalleryReferrerNode(
    objectNode: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<[ObjectTree, ObjectNode] | undefined> {
    if (objectNode.imageGalleryObjectTreeId) {
      const imageGalleryParts = objectNode.imageGalleryObjectTreeId.split('/');
      const imageGalleryNode = await this.objectNodeService.searchTree(
        imageGalleryParts[1],
        imageGalleryParts[2],
        imageGalleryParts[3],
        imageGalleryParts[4],
        imageGalleryParts[5],
        imageGalleryParts[6],
      );

      if (imageGalleryNode) {
        let selectedImages: string[] | undefined = undefined;
        let initialSelectedImagesCount = 0;
        const implementedTypes = await this.objectTypeService.getImplementedTypes(
          objectNode.objectTypeId,
        );
        if (-1 < indexOf(implementedTypes, IMAGE_GALLERY_SELECTOR_TYPE.name)) {
          selectedImages = objectNode.selectedImages
            ? objectNode.selectedImages
            : [];
          initialSelectedImagesCount = (selectedImages as string[]).length;
        }

        const images: {images: Image[]; title?: string} = args as {
          images: Image[];
          title?: string;
        };
        for (const image of images.images) {
          const imageObject = await this.createImage(
            image,
            imageGalleryNode,
            ctx,
            undefined,
            images.title,
          );
          if (imageObject && selectedImages) {
            selectedImages.push(imageObject.name);
          }
        }

        if (
          selectedImages &&
          selectedImages.length > initialSelectedImagesCount
        ) {
          await this.objectNodeService.modifyById(
            objectNode.id as string,
            {selectedImages},
            ctx,
          );
        }

        return [
          await this.actionEntityService.getEntity<ObjectTree>(
            EntityName.objectTree,
            imageGalleryNode.id as string,
            CurrentContext.get(ctx, {}),
          ),
          await this.actionEntityService.getEntity<ObjectNode>(
            EntityName.objectNode,
            objectNode.id as string,
            ctx,
          ),
        ];
      }
    }
  }
}
