"use strict";

process.env.TZ = "Etc/GMT";

require('@tensorflow/tfjs-node');

let photoDB = null;

const config = require("config"),
  Promise = require("bluebird"),
  { exists, mkdir, unlink } = require("./lib/util"),
  faceapi = require("face-api.js"),
  fs = require("fs"),
  canvas = require("canvas");

const { createCanvas, Canvas, Image, ImageData } = canvas;

faceapi.env.monkeyPatch({ Canvas, Image, ImageData });

const maxConcurrency = require("os").cpus().length;

require("./console-line.js"); /* Monkey-patch console.log with line numbers */

const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/",
  faceData = picturesPath + "face-data/";

process.stdout.write("Loading DB.");
require("./db/photos").then(function(db) {
  process.stdout.write("done\n");
  photoDB = db;
}).then(() => {
  console.log("DB connected.");
  process.stdout.write("Loading models.");
  return faceapi.nets.ssdMobilenetv1.loadFromDisk('./models');
}).then(() => {
  process.stdout.write(".");
  return faceapi.nets.faceLandmark68Net.loadFromDisk('./models');
}).then(() => {
  process.stdout.write(".");
  return faceapi.nets.faceRecognitionNet.loadFromDisk('./models');
}).then(async () => {
  process.stdout.write(".done\n");

  if (process.argv[0].match(/node/)) {
    process.argv.shift(); /* node */
  }
  process.argv.shift(); /* script name */

  return Promise.resolve().then(() => {
    if (process.argv.length != 0) {
      return process.argv;
    }

    /* If no parameters provided, scan all faces to create image crops */
    return photoDB.sequelize.query("SELECT id FROM faces ORDER BY id ASC", {
      type: photoDB.sequelize.QueryTypes.SELECT,
      raw: true
    }).then((results) => {
      return results.map(result => result.id);
    });
  });
}).then((args) => {
  const faces = [];

  console.log(`Scanning ${args.length} faces.`);
  return Promise.map(args, (arg) => {
    const file = arg,
      id = parseInt(arg);

    let loader;

    if (id == file) {
      /* This is a face id */
      console.log(`Looking up face-id ${id}...`);
      loader = photoDB.sequelize.query(
        "SELECT albums.path,photos.filename,photos.width,photos.height,faces.* " +
        "FROM faces,photos,albums " +
        "WHERE photos.id=faces.photoId " +
        "AND albums.id=photos.albumId " +
        "AND faces.id=:id", {
          replacements: {
            id: id
          },
          type: photoDB.sequelize.QueryTypes.SELECT,
          raw: true
      }).then((results) => {
        if (results.length != 1) {
          console.error(`...error. No face-id found: ${id}.\n`);
          process.exit(-1);
        }
        const photo = results[0];
        console.log(`...loading ${photo.filename}`);

        const file = photo.path + photo.filename;
        return canvas.loadImage(picturesPath + file).then(async (image) => {
          const detectors = [ {
            detection: {
              _box: {
                _x: photo.left * photo.width,
                _y: photo.top * photo.height,
                _width: (photo.right - photo.left) * photo.width,
                _height: (photo.bottom - photo.top) * photo.height,
              }
            },
            descriptor: JSON.parse(fs.readFileSync(faceData + (id % 100) + "/" + id + "-data.json"))
          } ];
          return [ file, image, detectors ];
        });
      });
    } else {
      /* This is a file */
      console.log(`Loading ${file}...`);
      id = undefined;
      loader = canvas.loadImage(picturesPath + file).then(async (image) => {
        const detectors = await faceapi.detectAllFaces(image, 
            new faceapi.SsdMobilenetv1Options({
              minConfidence: 0.8
            })
          ).withFaceLandmarks().withFaceDescriptors();
        detectors.forEach((detector) => {
          const data = [];
          /* Confert from sparse object to dense array */
          for (let i = 0; i < 128; i++) {
            data.push(detector.descriptor[i]);
          }
          detector.descriptor = data;
        });
        return [ file, image, detectors ];
      });
    }

    return loader.spread((filepath, image, detectors) => {
      process.stdout.write(`${detectors.length} faces.\n`);
        
      return Promise.map(detectors, (face, index) => {
        faces.push({
          filepath: filepath,
          index: index,
          descriptor: face.descriptor
        })

        /* If this is a face-id, output the -original.png 
         * meta-data file */
        if (!id) {
          return;
        }

        const path = "face-data/" + (id % 100),
          target = `${path}/${id}-original.png`,
          box = face.detection._box,
          aspect = box._width / box._height,
          dx = (aspect > 1.0) ? 200 : (200 * aspect),
          dy = (aspect < 1.0) ? 200 : (200 / aspect);
          
        return exists(target).then((doesExist) => {
          if (doesExist) {
            console.log(`...${target} already exists.`);
            return;
          }
          const canvas = createCanvas(200, 200),
            ctx = canvas.getContext('2d');

          ctx.fillStyle = "rgba(0, 0, 0, 0)";
          ctx.fillRect(0, 0, 200, 200);
          ctx.drawImage(image, box._x, box._y, box._width, box._height, 
            Math.floor((200 - dx) * 0.5),
            Math.floor((200 - dy) * 0.5), dx, dy);

          console.log(`...writing to ${target}.`);

          return mkdir(path).then(() => {
            fs.writeFileSync(picturesPath + target, canvas.toBuffer("image/png", {
              quality: 0.95,
              chromaSubsampling: false
            }));
          });
        });
      });
    });
  }, {
    concurrency: maxConcurrency
  }).then(() => {
    console.log("Face detection scanning completed.");
    if (0) faces.forEach((a, i) => {
      faces.forEach((b, j) => {
        if (i == j) {
          return;
        }
        const distance = faceapi.euclideanDistance(a.descriptor, b.descriptor);
        if (distance < 0.4) {
          console.log(`${a.filepath}.${a.index} is similar to ${b.filepath}.${b.index}: ${distance}`);
        }
      })
    });
  });
}).catch((error) => {
  console.error(error);
  process.exit(-1);
});
