/*******************************************************************************
 * Copyright 2Fi Business Solutions Ltd.
 *
 * This code is copyrighted. Under no circumstances should any party, people,
 * or organization should redistribute any portions of this code in any form,
 * either verbatim or through electronic media, to any third parties, unless
 * under explicit written permission by 2Fi Business Solutions Ltd.
 ******************************************************************************/
package com.ffii.tbms.file.service;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import com.ffii.core.dao.JdbcDao;
import com.ffii.core.utils.FileUtils;
import com.ffii.core.utils.MapUtils;
import com.ffii.core.web.AbstractService;
import com.ffii.core.web.view.AbstractView;
import com.ffii.tbms.file.File;
import com.ffii.tbms.file.FileBlob;
import com.ffii.tbms.file.FileRef;
import com.ffii.tbms.file.dao.FileBlobDao;
import com.ffii.tbms.file.dao.FileDao;
import com.ffii.tbms.file.dao.FileRefDao;

/**
 * @author Patrick
 */
@Service
public class FileService extends AbstractService {

	@Autowired
	private JdbcDao jdbcDao;

	@Autowired
	private FileDao fileDao;

	@Autowired
	private FileBlobDao fileBlobDao;

	@Autowired
	private FileRefDao fileRefDao;

	/**
	 * Save File, FileBlob, and FileRef in one go
	 *
	 * @param filename
	 *            the filename, cannot be null
	 * @param description
	 *            optional File description
	 * @param refType
	 *            the File reference type, should not be empty
	 * @param refId
	 *            mandatory, use 0 if N/A
	 * @param refCode
	 *            optional
	 * @param bytes
	 *            the File byte array
	 *
	 * @return File ID
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public Integer saveFile(String filename, String description, String refType, int refId, String refCode, byte[] bytes) {
		File file = new File();
		file.setFilename(filename);
		file.setDescription(description);
		file.setMimetype(FileUtils.guessMimetype(filename));
		file.setFilesize(bytes.length);
		file.setSkey(RandomStringUtils.randomAlphanumeric(16));

		FileBlob fileBlob = new FileBlob();
		fileBlob.setBytes(bytes);

		FileRef fileRef = new FileRef();
		fileRef.setRefId(refId);
		fileRef.setRefType(refType);
		fileRef.setRefCode(refCode);

		// try to get width and height if mimetype is png or jpeg
		if (AbstractView.CONTENT_TYPE_PNG.equals(file.getMimetype()) || AbstractView.CONTENT_TYPE_JPEG.equals(file.getMimetype())) {
			BufferedImage image;
			try {
				image = ImageIO.read(new ByteArrayInputStream(bytes));
				if (image != null) {
					file.setImageWidth(image.getWidth());
					file.setImageHeight(image.getHeight());
				}
			} catch (IOException e) {
				// ignore
			}
		}

		// save File
		saveFile(file);

		// save FileBlob
		fileBlob.setFileId(file.getId());
		saveFileBlob(fileBlob);

		// save FileRef
		fileRef.setFileId(file.getId());
		saveFileRef(fileRef);

		return file.getId();
	}

	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public Integer saveFile(File instance) {
		return fileDao.saveOrUpdate(instance);
	}

	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public Integer saveFileBlob(FileBlob instance) {
		return fileBlobDao.saveOrUpdate(instance);
	}

	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public Integer saveFileRef(FileRef instance) {
		return fileRefDao.saveOrUpdate(instance);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public File findFileByIdAndKey(int id, String skey) {
		return fileDao.findByQuery("from com.ffii.tbms.file.File f where f.id = ? and f.skey = ?", id, skey);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public File findFileById(int id) {
		return fileDao.findByQuery("from com.ffii.tbms.file.File f where f.id = ? ", id);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public FileRef findFileRefByTypeAndId(String refType, int refId) {
		return fileRefDao.findByQuery("from com.ffii.tbms.file.FileRef where refType = ? and refId = ?", refType, refId);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public FileRef findFileRefByfileId(int fileId) {
		return fileRefDao.findByQuery("from com.ffii.tbms.file.FileRef where fileId = ?", fileId);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public FileBlob findFileBlobByFileId(int fileId) {
		return fileBlobDao.findByQuery("from com.ffii.tbms.file.FileBlob fb where fb.fileId = ?", fileId);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public boolean isFileExists(int id, String skey) {

		String sql = "SELECT"
				+ " COUNT(1)"
				+ " FROM files f"
				+ " WHERE f.deleted = 0"
				+ " AND f.id = :id"
				+ " AND f.skey = :skey";

		int count = jdbcDao.queryForInt(sql, MapUtils.toHashMap("id", id, "skey", skey));

		return (count > 0);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public List<Map<String, Object>> searchFiles(Map<String, Object> args) {

		StringBuilder sql = new StringBuilder("SELECT"
				+ " f.id,"
				+ " f.filename,"
				+ " f.filesize,"
				+ " f.mimetype, "
				+ " f.skey,"
				+ " fr.refId,"
				+ " fr.refType,"
				+ " f.created,"
				+ " f.imageWidth,"
				+ " f.imageHeight,"
				+ " u.fullname AS createdByName,"
				+ " f.description"
				+ " FROM files f"
				+ " LEFT JOIN files_ref fr ON f.id = fr.fileId"
				+ " LEFT JOIN users u ON f.createdBy = u.id"
				+ " WHERE f.deleted = 0 AND fr.deleted =0 ");

		if (args.containsKey("filename")) sql.append(" AND f.filename = :filename");

		if (args.containsKey("refType")) sql.append(" AND fr.refType = :refType");

		if (args.containsKey("refId")) sql.append(" AND fr.refId = :refId");

		if (args.containsKey("startDate")) sql.append(" AND f.created >= :startDate");

		if (args.containsKey("endDate")) sql.append(" AND f.created < :endDate");

		if (args.containsKey("fileId")) sql.append(" AND f.id = :fileId");
		

		sql.append(" ORDER BY f.created DESC");

		return jdbcDao.queryForList(sql.toString(), args);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public List<Map<String, Object>> searchFilesFromDocument(Map<String, Object> args) {

		StringBuilder sql = new StringBuilder("SELECT"
			+ " f.id,"
			+ " f.filename,"
			+ " f.filesize,"
			+ " f.mimetype,"
			+ " f.skey,"
			+ " fr.refId,"
			+ " fr.refType,"
			+ " f.created,"
			+ " u.fullname AS createdByName,"
			+ " f.description"
			+ " FROM files_ref fr"
			+ " LEFT JOIN files f ON f.id = fr.fileId"
			+ " LEFT JOIN users u ON f.createdBy = u.id"
			+ " LEFT JOIN document_ref dr ON dr.documentId = fr.refId AND fr.refType = 'document'"
			+ " WHERE 1 = 1"
			+ " AND f.deleted = 0"
			+ " AND fr.deleted = 0"
			+ " AND dr.deleted = 0");

		if (args != null) {
			if (args.containsKey("filename")) sql.append(" AND f.filename = :filename");
			if (args.containsKey("refType")) sql.append(" AND fr.refType = :refType");
			if (args.containsKey("refId")) sql.append(" AND fr.refId = :refId");
			if (args.containsKey("startDate")) sql.append(" AND f.created >= :startDate");
			if (args.containsKey("endDate")) sql.append(" AND f.created < :endDate");
			if (args.containsKey("documentRefType")) sql.append(" AND dr.refType = :documentRefType");
			if (args.containsKey("documentRefId")) sql.append(" AND dr.refId = :documentRefId");
		}

		sql.append(" GROUP BY f.id"
			+ " ORDER BY f.created DESC");

		return jdbcDao.queryForList(sql.toString(), args);
	}

	/**
	 * Delete <code>FileRef</code> by <code>fileId</code>, <code>refId</code>, <code>refType</code>, and <code>skey</code>
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public void deleteFile(Integer fileId, Integer refId, String refType, String skey) {
		Map<String, ?> args = MapUtils.toHashMap("fileId", fileId, "refId", refId, "refType", refType, "skey", skey);

		jdbcDao.executeUpdate("DELETE FROM files_ref"
				+ " WHERE fileId = :fileId"
				+ " AND refId = :refId"
				+ " AND refType = :refType"
				+ " AND EXISTS (SELECT 1 FROM files WHERE id = files_ref.fileId AND skey = :skey)",
				args);
		jdbcDao.executeUpdate("DELETE FROM files_blob WHERE id = :fileId ",args);
	}

	/**
	 * Scheduled daily job
	 *
	 * Delete all orphan (without any <code>FileRef</code>) <code>File</code>s and <code>FileBlob</code>s
	 */
	@Scheduled(cron = "0 0 0 * * ?") // everyday at 00:00:00
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public void deleteOrphanFiles() {
		jdbcDao.executeUpdate("DELETE FROM files_blob WHERE NOT EXISTS (SELECT 1 FROM files_ref WHERE deleted = 0 AND fileId = files_blob.fileId)");
		jdbcDao.executeUpdate("DELETE FROM files WHERE NOT EXISTS (SELECT 1 FROM files_ref WHERE deleted = 0 AND fileId = files.id)");
	}

	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = true)
	public List<Map<String, Object>> getCustImageList() {
		return jdbcDao.queryForList(" Select cp.* From cust_photo cp ", null);
	}

	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = true)
	public List<Map<String, Object>> getOrderImageList() {
		return jdbcDao.queryForList(" Select op.* From order_photo cp ", null);
	}



}
