/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

namespace Photos {

public class TiffFileFormatDriver : PhotoFileFormatDriver {
    private static TiffFileFormatDriver instance = null;
    
    public static void init() {
        instance = new TiffFileFormatDriver();
        TiffFileFormatProperties.init();
    }
    
    public static TiffFileFormatDriver get_instance() {
        return instance;
    }
    
    public override PhotoFileFormatProperties get_properties() {
        return TiffFileFormatProperties.get_instance();
    }
    
    public override PhotoFileReader create_reader(string filepath) {
        return new TiffReader(filepath);
    }
    
    public override PhotoMetadata create_metadata() {
        return new PhotoMetadata();
    }
    
    public override bool can_write_image() {
        return true;
    }
    
    public override bool can_write_metadata() {
        return true;
    }
    
    public override PhotoFileWriter? create_writer(string filepath) {
        return new TiffWriter(filepath);
    }
    
    public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
        return new TiffMetadataWriter(filepath);
    }
    
    public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
        return new TiffSniffer(file, options);
    }
}

private class TiffFileFormatProperties : PhotoFileFormatProperties {
    private static string[] KNOWN_EXTENSIONS = {
        "tif", "tiff"
    };
    
    private static string[] KNOWN_MIME_TYPES = {
        "image/tiff"
    };
    
    private static TiffFileFormatProperties instance = null;
    
    public static void init() {
        instance = new TiffFileFormatProperties();
    }
    
    public static TiffFileFormatProperties get_instance() {
        return instance;
    }
    
    public override PhotoFileFormat get_file_format() {
        return PhotoFileFormat.TIFF;
    }
    
    public override PhotoFileFormatFlags get_flags() {
        return PhotoFileFormatFlags.NONE;
    }
    
    public override string get_default_extension() {
        return "tif";
    }

    public override string get_user_visible_name() {
        return _("TIFF");
    }

    public override string[] get_known_extensions() {
        return KNOWN_EXTENSIONS;
    }
    
    public override string get_default_mime_type() {
        return KNOWN_MIME_TYPES[0];
    }
    
    public override string[] get_mime_types() {
        return KNOWN_MIME_TYPES;
    }
}

private class TiffSniffer : GdkSniffer {
    public TiffSniffer(File file, PhotoFileSniffer.Options options) {
        base (file, options);
    }
    
    public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
        // Rely on GdkSniffer to detect corruption
        is_corrupted = false;
        
        if (!is_tiff(file))
            return null;
        
        DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
        if (detected == null)
            return null;
        
        return (detected.file_format == PhotoFileFormat.TIFF) ? detected : null;
    }
}

private class TiffReader : GdkReader {
    public TiffReader(string filepath) {
        base (filepath, PhotoFileFormat.TIFF);
    }
}

private class TiffWriter : PhotoFileWriter {
    private const string COMPRESSION_NONE = "1";
    private const string COMPRESSION_HUFFMAN = "2";
    private const string COMPRESSION_LZW = "5";
    private const string COMPRESSION_JPEG = "7";
    private const string COMPRESSION_DEFLATE = "8";
    
    public TiffWriter(string filepath) {
        base (filepath, PhotoFileFormat.TIFF);
    }
    
    public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error {
        pixbuf.save(get_filepath(), "tiff", "compression", COMPRESSION_LZW);
    }

    public override async void write_async(Gdk.Pixbuf pixbuf, Jpeg.Quality quality, int io_priority, Cancellable? cancellable) throws Error {
        var file = File.new_for_path(get_filepath());
        var os = yield file.replace_async(null, false, GLib.FileCreateFlags.NONE, io_priority, cancellable);
        yield pixbuf.save_to_stream_async(os, "tiff", cancellable, "compression", COMPRESSION_LZW, null);
    }

}

private class TiffMetadataWriter : PhotoFileMetadataWriter {
    public TiffMetadataWriter(string filepath) {
        base (filepath, PhotoFileFormat.TIFF);
    }
    
    public override void write_metadata(PhotoMetadata metadata) throws Error {
        metadata.write_to_file(get_file());
    }
}

private const uint16 FILE_MARKER_TIFF = 42;
private const uint16 FILE_MARKER_BIGTIFF = 43;

public bool is_tiff(File file, Cancellable? cancellable = null) throws Error {
    DataInputStream dins = new DataInputStream(file.read());
    
    // first two bytes: "II" (0x4949, for Intel) or "MM" (0x4D4D, for Motorola)
    DataStreamByteOrder order;
    switch (dins.read_uint16(cancellable)) {
        case 0x4949:
            order = DataStreamByteOrder.LITTLE_ENDIAN;
        break;
        
        case 0x4D4D:
            order = DataStreamByteOrder.BIG_ENDIAN;
        break;
        
        default:
            return false;
    }
    
    dins.set_byte_order(order);
    
    // second two bytes: some random number
    uint16 lue = dins.read_uint16(cancellable);
    if (lue != FILE_MARKER_TIFF && lue != FILE_MARKER_BIGTIFF) {
        return false;
    }
    
    // remaining bytes are offset of first IFD, which doesn't matter for our purposes
    return true;
}

}
