// image.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const path = require('path');
const fs = require('fs');

const mongoose = require('mongoose');
const SiteImage = mongoose.model('Image');

const sharp = require('sharp');

const { SiteService, SiteAsync } = require('../../lib/site-lib');

class ImageService extends SiteService {

  constructor(dtp) {
    super(dtp, module.exports);
    this.populateImage = [
      {
        path: 'owner',
        select: '_id username username_lc displayName picture'
      },
    ];
  }

  async start() {
    await super.start();
    await fs.promises.mkdir(process.env.DTP_IMAGE_WORK_PATH, { recursive: true });
  }

  async create(owner, imageDefinition, file) {
    const NOW = new Date();
    const { minio: minioService } = this.dtp.services;
    try {
      this.log.debug('processing uploaded image', { imageDefinition, file });

      const sharpImage = sharp(file.path);
      const metadata = await sharpImage.metadata();

      // create an Image model instance, but leave it here in application memory.
      // we don't persist it to the db until MinIO accepts the binary data.
      const image = new SiteImage();
      image.created = NOW;
      image.owner = owner._id;
      image.type = file.mimetype;
      image.size = file.size;
      image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
      image.metadata = this.makeImageMetadata(metadata);

      const imageId = image._id.toString();
      const ownerId = owner._id.toString();
      const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`;
      image.file.key = fileKey;

      // upload the image file to MinIO
      const response = await minioService.uploadFile({
        bucket: image.file.bucket,
        key: image.file.key,
        filePath: file.path,
        metadata: {
          'Content-Type': file.mimetype,
          'Content-Length': file.size,
        },
      });

      // store the eTag from MinIO in the Image model
      image.file.etag = response.etag;

      // save the Image model to the db
      await image.save();

      this.log.info('processed uploaded image', { ownerId, imageId, fileKey });
      return image.toObject();
    } catch (error) {
      this.log.error('failed to process image', { error });
      throw error;
    } finally {
      this.log.info('removing uploaded image from local file system', { file: file.path });
      await fs.promises.rm(file.path);
    }
  }

  async getImageById(imageId) {
    const image = await SiteImage
      .findById(imageId)
      .populate(this.populateImage);
    return image;
  }

  async getRecentImagesForOwner(owner) {
    const images = await SiteImage
      .find({ owner: owner._id })
      .sort({ created: -1 })
      .limit(10)
      .populate(this.populateImage)
      .lean();
    return images;
  }

  async deleteImage(image) {
    const { minio: minioService } = this.dtp.services;

    this.log.debug('removing image from storage', { bucket: image.file.bucket, key: image.file.key });
    await minioService.removeObject(image.file.bucket, image.file.key);

    this.log.debug('removing image from MongoDB', { _id: image._id });
    await SiteImage.deleteOne({ _id: image._id });
  }

  async processImageFile(owner, file, outputs, options) {
    this.log.debug('processing image file', { owner, file, outputs });
    const sharpImage = sharp(file.path);
    return this.processImage(owner, sharpImage, outputs, options);
  }

  async processImage(owner, sharpImage, outputs, options) {
    const NOW = new Date();
    const service = this;
    const { minio: minioService } = this.dtp.services;

    options = Object.assign({
      removeWorkFiles: true,
    }, options);

    const imageWorkPath = process.env.DTP_IMAGE_WORK_PATH || '/tmp';
    const metadata = await sharpImage.metadata();

    async function processOutputImage(output) {
      const outputMetadata = service.makeImageMetadata(metadata);
      outputMetadata.width = output.width;
      outputMetadata.height = output.height;

      service.log.debug('processing image', { output, outputMetadata });

      const image = new SiteImage();
      image.created = NOW;
      image.owner = owner._id;
      image.type = `image/${output.format}`;
      image.metadata = outputMetadata;

      try {
        let chain = sharpImage
          .clone()
          .resize({
            width: output.width,
            height: output.height,
            options: output.resizeOptions,
          })
          ;
        chain = chain[output.format](output.formatParameters);

        output.filePath = path.join(imageWorkPath, `${image._id}.${output.width}x${output.height}.${output.format}`);
        output.mimetype = `image/${output.format}`;
        await chain.toFile(output.filePath);
        output.stat = await fs.promises.stat(output.filePath);
      } catch (error) {
        service.log.error('failed to process output image', { output, error });
        throw error;
      }

      try {
        const imageId = image._id.toString();
        const ownerId = owner._id.toString();
        const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/images/${imageId.slice(0, 3)}/${imageId}.${output.format}`;

        image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
        image.file.key = fileKey;
        image.size = output.stat.size;

        // upload the image file to MinIO
        const response = await minioService.uploadFile({
          bucket: image.file.bucket,
          key: image.file.key,
          filePath: output.filePath,
          metadata: {
            'Content-Type': output.mimetype,
            'Content-Length': output.stat.size,
          },
        });

        // store the eTag from MinIO in the Image model
        image.file.etag = response.etag;

        // save the Image model to the db
        await image.save();

        service.log.info('processed uploaded image', { ownerId, imageId, fileKey });

        if (options.removeWorkFiles) {
          service.log.debug('removing work file', { path: output.filePath });
          await fs.promises.unlink(output.filePath);
          delete output.filePath;
        }

        output.image = {
          _id: image._id,
          bucket: image.file.bucket,
          key: image.file.key,
        };
      } catch (error) {
        service.log.error('failed to persist output image', { output, error });
        if (options.removeWorkFiles) {
          service.log.debug('removing work file', { path: output.filePath });
          await SiteAsync.each(outputs, async (output) => {
            await fs.promises.unlink(output.filePath);
            delete output.filePath;
          }, 4);
        }
        throw error;
      }
    }

    await SiteAsync.each(outputs, processOutputImage, 4);
  }

  async getSiteIconInfo() {
    const siteDomain = this.dtp.config.site.domainKey;

    const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
    const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);

    let icon;

    try {

      await fs.promises.access(siteIconDir);
      const iconMetadata = await sharp(path.join(siteIconDir, 'icon-512x512.png')).metadata();
      icon = {
        metadata: iconMetadata,
        path: `/img/icon/${siteDomain}/icon-512x512.png`,
      };

    } catch (error) {

      icon = null;
    } 
      
      return icon;
  }
  
  async getPostImageInfo() {

    const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');

    let icon;

    try {

      await fs.promises.access(siteImagesDir);
      const iconMetadata = await sharp(path.join(siteImagesDir, 'default-poster.jpg')).metadata();
      icon = {
        metadata: iconMetadata,
        path: `/img/default-poster.jpg`,
      };

    } catch (error) {

      icon = null;
    } 
      
      return icon;
  }
  
  async updatePostImage(imageDefinition, file) {
    
    this.log.debug('updating site icon', { imageDefinition, file });
    try {
      
      
      
      const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
            
      const sourceIconFilePath = file.path;
      
           
      await sharp(sourceIconFilePath).resize({
        fit: sharp.fit.inside,
        width: 540,
        height: 960,
      }).jpeg()
      .toFile(path.join(siteImagesDir, `default-poster.jpg`));

      return path.join(siteImagesDir, 'default-poster.jpg');
    } catch (error) {
      this.log.error('failed to update site icon', { error });
      throw error;
    } finally {
      this.log.info('removing uploaded image from local file system', { file: file.path });
      await fs.promises.rm(file.path);
    }
    
  }

  async updateSiteIcon(imageDefinition, file) {
    
    this.log.debug('updating site icon', { imageDefinition, file });
    try {
      
      
      const siteDomain = this.dtp.config.site.domainKey;
      
      const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
      
      const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
      
      const sourceIconFilePath = file.path;
      
      const sizes = [16, 32, 36, 48, 57, 60, 70, 72, 76, 96, 114, 120, 144, 150, 152, 180, 192, 256, 310, 384, 512];
      
      await fs.promises.mkdir(siteIconDir, { force: true, recursive: true });
      
      for (var size of sizes) {
        await sharp(sourceIconFilePath).resize({
          fit: sharp.fit.inside,
          width: size,
          height: size,
        }).png()
        .toFile(path.join(siteIconDir, `icon-${size}x${size}.png`));
      }
            
      await fs.promises.cp(sourceIconFilePath, path.join(siteImagesDir, 'social-cards', `${siteDomain}.png`));
      return path.join(siteIconDir, 'icon-512x512.png');
    } catch (error) {
      this.log.error('failed to update site icon', { error });
      throw error;
    } finally {
      this.log.info('removing uploaded image from local file system', { file: file.path });
      await fs.promises.rm(file.path);
    }
    
  }

  makeImageMetadata(metadata) {
    return {
      format: metadata.format,
      size: metadata.size,
      width: metadata.width,
      height: metadata.height,
      space: metadata.space,
      channels: metadata.channels,
      depth: metadata.depth,
      density: metadata.density,
      hasAlpha: metadata.hasAlpha,
      orientation: metadata.orientation,
    };
  }
}

module.exports = {
  slug: 'image',
  name: 'image',
  create: (dtp) => { return new ImageService(dtp); },
};