package org.forkalsrud.album.exif;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.forkalsrud.album.db.DirectoryProps;
import org.forkalsrud.album.video.MovieMetadataGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.jpeg.JpegDirectory;

public class DirectoryMetadataGenerator {

    private static Logger log = LoggerFactory.getLogger(DirectoryMetadataGenerator.class);
    final static String CACHE_FILE = "cache.properties";
    final static String OVERRIDE_FILE = "album.properties";

    MovieMetadataGenerator movieMetadataGenerator;
    
    public DirectoryMetadataGenerator(MovieMetadataGenerator generator) {
        this.movieMetadataGenerator = generator;
    }

    
    public DirectoryProps generate(File dir) throws IOException, InterruptedException {

        long start = System.currentTimeMillis();
        DirectoryProps props = new DirectoryProps();
        generateFileEntries(dir, props);
        long end = System.currentTimeMillis();
        props.setTimestamp(end);
        log.info("Time to generate properties for " + dir.getPath() + ": " + (end - start)/1000d + " s");
        return props;
    }


    private void generateFileEntries(File dir, Properties props) throws IOException, InterruptedException {

        File[] files = dir.listFiles();
        if (files == null) {
            return;
        }
        for (File f : files) {

            if (f.length() == 0) {
                continue;
            }

            String name = f.getName();
            if (f.isDirectory()) {
                if ("CVS".equals(name)) {
                    continue;
                }
                if (f.isHidden() || !f.canRead()) {
                    continue;
                }
                generateDirectoryProperties(props, f);
                continue;
            }
            if (f.isHidden() || !f.canRead()) {
                continue;
            }
            if (CACHE_FILE.equals(name) || OVERRIDE_FILE.equals(name)) {
                // cache.properties and album.properties
                continue;
            }
            if (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".JPG") || name.endsWith(".tif") || name.endsWith(".tiff") || name.endsWith(".TIF")) {
                Map<String, String> p = generateThumbnailProperties(f);
                if (p != null) {
                    addPropsForFile(props, f, p);
                }
            } else if (name.endsWith(".mov") || name.endsWith(".MOV") || name.endsWith(".mp4") || name.endsWith(".m4v") || name.endsWith(".avi")) {
                Map<String, String> p = movieMetadataGenerator.generateVideoProperties(f);
                addPropsForFile(props, f, p);
            } else if (name.endsWith(".pdf")) {
                Map<String, String> p = generatePdfProperties(f);
                addPropsForFile(props, f, p);
            }
        }
    }


    /**
     * Adds properties by prefixing each with "file." plus the file name
     * @param props all props
     * @param f file (to get the name)
     * @param p the props to add
     */
    private void addPropsForFile(Properties props, File f, Map<String, String> p) {
        String base = "file." + f.getName() + ".";
        for (Map.Entry<String, String> e : p.entrySet()) {
            props.setProperty(base + e.getKey(), e.getValue());
        }
    }


    private void generateDirectoryProperties(Properties props, File f) {

        String name = f.getName();
        props.setProperty("dir." + name, "present");
    }

    /**
     * 
     * @param f the jpeg file
     * @return a map with the following keys: orientation dimensions captureDate comment etag
     * @throws IOException
     */
    private Map<String, String> generateThumbnailProperties(File f) throws IOException {
        
        HashMap<String, String> props = new HashMap<String, String>();
        String name = f.getName();
        boolean hasDate = false;
        boolean hasOrientation = false;
        boolean hasDim = false;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");

        Metadata metadata;
        try {
            metadata = ImageMetadataReader.readMetadata(f);
        } catch (ImageProcessingException e) {
            // not a JPEG file
            return null;
        } catch (RuntimeException e) {
            log.error("Unexpected error processing file " + f.getAbsolutePath(), e);
            return null;
        }
        try {
            Directory exifDirectory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
            if (exifDirectory != null
                    && exifDirectory.containsTag(ExifSubIFDDirectory.TAG_IMAGE_WIDTH)
                    && exifDirectory.containsTag(ExifSubIFDDirectory.TAG_IMAGE_HEIGHT)) {
                int width = exifDirectory.getInt(ExifSubIFDDirectory.TAG_IMAGE_WIDTH);
                int height = exifDirectory.getInt(ExifSubIFDDirectory.TAG_IMAGE_HEIGHT);
                props.put("dimensions", new Dimension(width, height).toString());
                hasDim = true;
            }

            Directory exifSubDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
            if (exifSubDirectory != null && exifSubDirectory.containsTag(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH) &&
            		exifSubDirectory.containsTag(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT)) {
                int width = exifSubDirectory.getInt(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH);
                int height = exifSubDirectory.getInt(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT);
                props.put("dimensions", new Dimension(width, height).toString());
                hasDim = true;
            }
            if (exifSubDirectory != null && exifSubDirectory.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) {
                Date captureDate = getExifDate(exifSubDirectory, ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
                if (captureDate != null) {
                    props.put("captureDate", sdf.format(captureDate));
                    hasDate = true;
                }
            }
            if (exifSubDirectory != null && exifSubDirectory.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
                String comment = exifSubDirectory.getString(ExifSubIFDDirectory.TAG_USER_COMMENT);
                props.put("comment", comment);
            }

            JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
            if (jpegDirectory != null
                    && jpegDirectory.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)
                    && jpegDirectory.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
                int width = jpegDirectory.getImageWidth();
                int height = jpegDirectory.getImageHeight();
                props.put("dimensions", new Dimension(width, height).toString());
                hasDim = true;
            }
        } catch (Exception e) {
            throw new RuntimeException("problem reading file " + f.getPath(), e);
        }
        props.put("etag", Integer.toHexString(name.hashCode() + Long.valueOf(f.lastModified()).hashCode()));
        if (!hasDate) {
            props.put("captureDate", sdf.format(new Date(f.lastModified())));
        }
        if (!hasOrientation) {
            props.put("orientation", "1");
        }
        if (!hasDim) {
            Dimension dim = decodeImageForDimensions(f);
            if (dim != null) {
                props.put("dimensions", dim.toString());
                hasDim = true;
            }
        }
        return props;
    }


    private Date getExifDate(Directory exifDirectory, int tagName) {

        String dateStr = exifDirectory.getString(tagName);
        if ("    :  :     :  :  ".equals(dateStr)) {
            return null;
        }
        return exifDirectory.getDate(tagName);
    }

    private Dimension decodeImageForDimensions(File file) throws IOException {

        ImageReader reader = getImageReader(file);
        if (reader == null) {
            return null;
        }
        ImageInputStream iis = ImageIO.createImageInputStream(file);
        reader.setInput(iis, true);
        ImageReadParam param = reader.getDefaultReadParam();
        BufferedImage img = reader.read(0, param);
        iis.close();
        return new Dimension(img.getWidth(), img.getHeight());
    }

    private ImageReader getImageReader(File file) {
        String name = file.getName();
        if (name.indexOf('.') < 0) {
            return null;
        }
        String suffix = name.substring(name.lastIndexOf('.') + 1);
        Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix(suffix);
        if (!readers.hasNext()) {
            return null;
        }
        ImageReader reader = readers.next();
        return reader;
    }


    private Map<String, String> generatePdfProperties(File file) throws IOException {

        HashMap<String, String> props = new HashMap<>();

        String name = file.getName();
        props.put("etag", Integer.toHexString(name.hashCode() + Long.valueOf(file.lastModified()).hashCode()));

        try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(file))) {

            PDPage page = document.getPage(0);
            PDRectangle rect = page.getMediaBox();


            int width = Math.round(rect.getWidth());
            int height = Math.round(rect.getHeight());
            props.put("dimensions", new Dimension(width, height).toString());
            props.put("orientation", "1");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
            Calendar cal = document.getDocumentInformation().getCreationDate();
            Date date = cal != null ? cal.getTime() : new Date(file.lastModified());
            props.put("captureDate", sdf.format(date));

        }
        return props;
    }


}
