"use strict";

const express = require("express"),
  Promise = require("bluebird");

let photoDB;

require("../db/photos").then(function(db) {
  photoDB = db;
});

const router = express.Router();

router.put("/faces/add/:id", (req, res) => {
  if (!req.user.maintainer) {
    return res.status(401).send("Unauthorized to modify photos.");
  }

  const id = parseInt(req.params.id);
  if (id != req.params.id) {
    return res.status(400).send("Invalid identity id.");
  }
  
  if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
    return res.status(400).send("No faces supplied.");
  }

  return photoDB.sequelize.query("UPDATE faces SET identityId=:identityId,identityDistance=0 " +
    "WHERE id IN (:faceIds)", {
    replacements: {
      identityId: id,
      faceIds: req.body.faces
    }
  }).then(() => {
    const identity = {
      id: id,
      faces: req.body.faces
    };
    return res.status(200).json([identity]);
  }).catch((error) => {
    console.error(error);
    return res.status(500).send("Error processing request.");
  });
});

router.post("/", (req, res) => {
  if (!req.user.maintainer) {
    return res.status(401).send("Unauthorized to modify photos.");
  }
  const identity = {
    lastName: req.body.lastName || "",
    firstName: req.body.firstName || "",
    middleName: req.body.middleName || ""
  };
  identity.name = req.body.name || (identity.firstName + " " + identity.lastName);

  let fields = [];
  for (let key in identity) {
    fields.push(key);
  }

  if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
    return res.status(400).send("No faces supplied.");
  }

  return photoDB.sequelize.query("INSERT INTO identities " +
    "(" + fields.join(",") + ") " +
    "VALUES(:" + fields.join(",:") + ")", {
    replacements: identity,
  }).spread(function(results, metadata) {
    identity.id = metadata.lastID;
    return photoDB.sequelize.query("UPDATE faces SET identityId=:identityId,identityDistance=0 " +
      "WHERE id IN (:faceIds)", {
      replacements: {
        identityId: identity.id,
        faceIds: req.body.faces
      }
    }).then(() => {
      identity.faces = req.body.faces;
      return res.status(200).json([identity]);
    });
  }).catch((error) => {
    console.error(error);
    return res.status(500).send("Error processing request.");
  });
});

function bufferToFloat32Array(buffer) {
  return new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float32Array.BYTES_PER_ELEMENT);
}

function euclideanDistance(a, b) {
  let A = bufferToFloat32Array(a);
  let B = bufferToFloat32Array(b);
  let sum = 0;
  for (let i = 0; i < 128; i++) {
    let delta = A[i] - B[i];
    sum += delta * delta;
  }
  return Math.sqrt(sum);
}

router.get("/:id?", (req, res) => {
  let id;

  if (req.params.id) {
    id = parseInt(req.params.id);
    if (id != req.params.id) {
      return res.status(400).send({ message: "Usage /[id]"});
    }
  }

  const filter = id ? "WHERE identities.id=:id " : "";

  return photoDB.sequelize.query("SELECT " +
    "identities.*," +
    "GROUP_CONCAT(faces.id) AS relatedFaceIds," +
    "GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," +
    "GROUP_CONCAT(faces.identityDistance) AS relatedIdentityDistances " +
    "FROM identities " + 
    "INNER JOIN faces ON identities.id=faces.identityId " +
    filter +
    "GROUP BY identities.id", {
    replacements: {
      id: id
    },
    type: photoDB.Sequelize.QueryTypes.SELECT,
    raw: true
  }).then((identities) => {
    identities.forEach((identity) => {
      const relatedFaces = identity.relatedFaceIds.split(","),
        relatedFacePhotos = identity.relatedFacePhotoIds.split(","),
        relatedIdentityDistances = identity.relatedIdentityDistances.split(",");
      if (relatedFaces.length != relatedFacePhotos.length) {
        console.warn("Face ID to Photo ID mapping doesn't match!");
      }
      delete identity.relatedFaceIds;
      delete identity.relatedFacePhotoIds;
      identity.relatedFaces = relatedFaces.map((faceId, index) => {
        return {
          faceId: faceId,
          photoId: relatedFacePhotos[index],
          distance: parseFloat(relatedIdentityDistances[index] !== undefined ? relatedIdentityDistances[index] : -1)
        };
      });
    });

    if (!req.query.withScore) {
      console.log("No score request.");
      return identities;
    }
    console.log("Looking up score against: " + req.query.withScore);

    return Promise.map(identities, (identity) => {
      return photoDB.sequelize.query("SELECT * FROM facedescriptors WHERE faceId IN (:id,:faceIds)", {
        replacements: {
          id: parseInt(req.query.withScore),
          faceIds: identity.relatedFaces.map(face => face.faceId)
        },
        type: photoDB.Sequelize.QueryTypes.SELECT,
        raw: true
      }).then((descriptors) => {
        let target;
        for (let i = 0; i < descriptors.length; i++) {
          if (descriptors[i].faceId == req.query.withScore) {
            target = descriptors[i].descriptors;
            break;
          }
        }
        if (!target) {
          console.warn("Could not find descriptor for requested face: " + req.query.withScore);
          return;
        }

        /* For each face's descriptor returned for this identity, compute the distance between the
         * requested photo and that face descriptor */
        descriptors.forEach((descriptor) => {
          for (let i = 0; i < identity.relatedFaces.length; i++) {
            if (identity.relatedFaces[i].faceId == descriptor.faceId) {
              identity.relatedFaces[i].distance = euclideanDistance(target, descriptor.descriptors);
              identity.relatedFaces[i].descriptors = descriptor.descriptors;
              return;
            }
          }
        });
      });
    }, {
      concurrency: 5
    }).then(() => {
      return identities;
    });
  }).then((identities) => {
    return res.status(200).json(identities);
  }).catch((error) => {
    console.error(error);
    return res.status(500).send("Error processing request.");
  });
});

module.exports = router;
