/* 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.
 */

public class JfifFileFormatDriver : PhotoFileFormatDriver {
    private static JfifFileFormatDriver instance = null;

    public static void init() {
        instance = new JfifFileFormatDriver();
        JfifFileFormatProperties.init();
    }
    
    public static JfifFileFormatDriver get_instance() {
        return instance;
    }
    
    public override PhotoFileFormatProperties get_properties() {
        return JfifFileFormatProperties.get_instance();
    }
    
    public override PhotoFileReader create_reader(string filepath) {
        return new JfifReader(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 JfifWriter(filepath);
    }
    
    public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
        return new JfifMetadataWriter(filepath);
    }
    
    public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
        return new JfifSniffer(file, options);
    }
}

public class JfifFileFormatProperties : PhotoFileFormatProperties {
    private static string[] KNOWN_EXTENSIONS = {
        "jpg", "jpeg", "jpe", "thm", "mpo"
    };

    private static string[] KNOWN_MIME_TYPES = {
        "image/jpeg"
    };
        
    private static JfifFileFormatProperties instance = null;

    public static void init() {
        instance = new JfifFileFormatProperties();
    }
    
    public static JfifFileFormatProperties get_instance() {
        return instance;
    }
    
    public override PhotoFileFormat get_file_format() {
        return PhotoFileFormat.JFIF;
    }
    
    public override PhotoFileFormatFlags get_flags() {
        return PhotoFileFormatFlags.NONE;
    }
    
    public override string get_default_extension() {
        return "jpg";
    }

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

    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;
    }
}

public class JfifSniffer : GdkSniffer {
    public JfifSniffer(File file, PhotoFileSniffer.Options options) {
        base (file, options);
    }
    
    public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
        is_corrupted = false;
        if (!calc_md5) {
            return fast_sniff (out is_corrupted);
        } else {
            if (!Jpeg.is_jpeg(file)) {
                return null;
            }

            // Rely on GdkSniffer to detect corruption

            DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
            if (detected == null)
                return null;

            return (detected.file_format == PhotoFileFormat.JFIF) ? detected : null;
        }
    }

    private DetectedPhotoInformation? fast_sniff(out bool is_corrupted) throws Error {
        is_corrupted = false;
        var detected = new DetectedPhotoInformation();

        detected.metadata = new PhotoMetadata();
        try {
            detected.metadata.read_from_file(file);
        } catch (Error err) {
            // no metadata detected
            detected.metadata = null;
        }

        var fins = file.read(null);
        var dins = new DataInputStream(fins);
        dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
        var seekable = (Seekable) dins;

        var marker = Jpeg.Marker.INVALID;
        var length = Jpeg.read_marker_2(dins, out marker);

        if (marker != Jpeg.Marker.SOI) {
            return null;
        }

        length = Jpeg.read_marker_2(dins, out marker);
        while (!marker.is_sof() && length > 0) {
            seekable.seek(length, SeekType.CUR, null);
            length = Jpeg.read_marker_2(dins, out marker);
        }

        if (marker.is_sof()) {
            if (length < 6) {
                is_corrupted = true;
                return null;
            }

            // Skip precision
            dins.read_byte();

            // Next two 16 bytes are image dimensions
            uint16 height = dins.read_uint16();
            uint16 width = dins.read_uint16();

            detected.image_dim = Dimensions(width, height);
            detected.colorspace = Gdk.Colorspace.RGB;
            detected.channels = 3;
            detected.bits_per_channel = 8;
            detected.format_name = "jpeg";
            detected.file_format = PhotoFileFormat.from_pixbuf_name(detected.format_name);
        } else {
            is_corrupted = true;
        }

        return detected;
    }
}

public class JfifReader : GdkReader {
    public JfifReader(string filepath) {
        base (filepath, PhotoFileFormat.JFIF);
    }
}

public class JfifWriter : PhotoFileWriter {
    public JfifWriter(string filepath) {
        base (filepath, PhotoFileFormat.JFIF);
    }
    
    public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error {
        if (pixbuf.has_alpha) {
            apply_alpha_channel(pixbuf).save(get_filepath(), "jpeg", "quality", quality.get_pct_text());
        } else {
            pixbuf.save(get_filepath(), "jpeg", "quality", quality.get_pct_text());
        }
    }

    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);
        var output = pixbuf;
        if (pixbuf.has_alpha) {
            output = apply_alpha_channel(pixbuf);
        }        
        
        yield output.save_to_stream_async(os, "jpeg", cancellable, "quality", quality.get_pct_text(), cancellable);
    }

}

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

namespace Jpeg {
    public const uint8 MARKER_PREFIX = 0xFF;
    
    public enum Marker {
        // Could also be 0xFF according to spec
        INVALID = 0x00,
        
        SOI = 0xD8,
        EOI = 0xD9,
        
        APP0 = 0xE0,
        APP1 = 0xE1;
        
        public uint8 get_byte() {
            return (uint8) this;
        }

        public bool is_sof() {
            // FFCn is SOF unless n is a multiple of 4 > 0 (FFC4, FFC8, FFCC)
            if ((this >> 4) != 0xC) {
                return false;
            }

            var variant = this & 0x0F;
            return variant == 0 || variant % 4 != 0;
        }
    }
    
    public enum Quality {
        LOW = 50,
        MEDIUM = 75,
        HIGH = 90,
        MAXIMUM = 100;
        
        public int get_pct() {
            return (int) this;
        }
        
        public string get_pct_text() {
            return "%d".printf((int) this);
        }
        
        public static Quality[] get_all() {
            return { LOW, MEDIUM, HIGH, MAXIMUM };
        }
        
        public string? to_string() {
            switch (this) {
                case LOW:
                    return _("Low (%d%%)").printf((int) this);
                
                case MEDIUM:
                    return _("Medium (%d%%)").printf((int) this);
                
                case HIGH:
                    return _("High (%d%%)").printf((int) this);
                    
                case MAXIMUM:
                    return _("Maximum (%d%%)").printf((int) this);
            }
            
            warn_if_reached();
            
            return null;
        }
    }
    
    public bool is_jpeg(File file) throws Error {
        var fins = file.read(null);
        return is_jpeg_stream(fins);
    }

    public bool is_jpeg_stream(InputStream ins) throws Error {
        Marker marker;
        int segment_length = read_marker(ins, out marker);
        
        // for now, merely checking for SOI
        return (marker == Marker.SOI) && (segment_length == 0);
    }

    public bool is_jpeg_bytes(Bytes bytes) throws Error {
        var mins = new MemoryInputStream.from_bytes(bytes);

        return is_jpeg_stream(mins);
    }

    private int32 read_marker_2(DataInputStream dins, out Jpeg.Marker marker) throws Error {
        marker = Jpeg.Marker.INVALID;

        if (dins.read_byte() != Jpeg.MARKER_PREFIX)
            return -1;
        
        marker = (Jpeg.Marker) dins.read_byte();
        if ((marker == Jpeg.Marker.SOI) || (marker == Jpeg.Marker.EOI)) {
            // no length
            return 0;
        }
        
        uint16 length = dins.read_uint16();
        var seekable = dins as Seekable;
        if (length < 2 && dins != null) {
            debug("Invalid length %Xh at ofs %" + int64.FORMAT + "Xh", length,
                    seekable.tell() - 2);
            
            return -1;
        }
        
        // account for two length bytes already read
        return length - 2;
    }

    private int read_marker(InputStream fins, out Jpeg.Marker marker) throws Error {
        DataInputStream dins = new DataInputStream(fins);
        dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);

        return read_marker_2(dins, out marker);
    }
}

