package com.lemenzo.gallery.privacy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Streaming JPEG metadata sanitizer.
 * Keeps the compressed image scan and color-critical APP2/ICC data while removing
 * EXIF/XMP (APP1), IPTC/Photoshop (APP13), comments and other application payloads
 * that may carry identifying metadata. No image decode/re-encode is performed.
 */
public final class JpegSanitizer {
    private JpegSanitizer() {}

    public static void sanitize(InputStream rawIn, OutputStream rawOut) throws IOException {
        BufferedInputStream in = new BufferedInputStream(rawIn, 64 * 1024);
        BufferedOutputStream out = new BufferedOutputStream(rawOut, 64 * 1024);
        if (readU8(in) != 0xFF || readU8(in) != 0xD8) throw new IOException("Not a JPEG stream");
        out.write(0xFF); out.write(0xD8);

        while (true) {
            int prefix;
            do { prefix = readU8(in); } while (prefix != 0xFF);
            int marker;
            do { marker = readU8(in); } while (marker == 0xFF);

            if (marker == 0xD9) { // EOI
                out.write(0xFF); out.write(marker); out.flush(); return;
            }
            if (marker == 0xDA) { // SOS: headers after this point are entropy-coded data; copy verbatim.
                out.write(0xFF); out.write(marker);
                int length = readU16(in);
                writeU16(out, length);
                copyExactly(in, out, length - 2);
                copyRest(in, out);
                out.flush();
                return;
            }
            // Standalone markers have no length field.
            if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7)) {
                out.write(0xFF); out.write(marker); continue;
            }

            int length = readU16(in);
            if (length < 2) throw new IOException("Invalid JPEG segment length");
            int payload = length - 2;

            // Strip APP0/APP1/APP3..APP15 and COM. Preserve APP2 because ICC profiles live there.
            boolean strip = marker == 0xFE || marker == 0xE0 || marker == 0xE1
                    || (marker >= 0xE3 && marker <= 0xEF);
            if (strip) {
                skipExactly(in, payload);
            } else {
                out.write(0xFF); out.write(marker); writeU16(out, length);
                copyExactly(in, out, payload);
            }
        }
    }

    private static int readU8(InputStream in) throws IOException {
        int value = in.read(); if (value < 0) throw new EOFException(); return value;
    }
    private static int readU16(InputStream in) throws IOException { return (readU8(in) << 8) | readU8(in); }
    private static void writeU16(OutputStream out, int value) throws IOException { out.write((value >>> 8) & 0xFF); out.write(value & 0xFF); }
    private static void copyExactly(InputStream in, OutputStream out, long bytes) throws IOException {
        byte[] buffer = new byte[64 * 1024]; long remaining = bytes;
        while (remaining > 0) { int read = in.read(buffer, 0, (int)Math.min(buffer.length, remaining)); if (read < 0) throw new EOFException(); out.write(buffer, 0, read); remaining -= read; }
    }
    private static void skipExactly(InputStream in, long bytes) throws IOException {
        long remaining = bytes; byte[] buffer = new byte[8192];
        while (remaining > 0) { long skipped = in.skip(remaining); if (skipped > 0) { remaining -= skipped; continue; } int read = in.read(buffer, 0, (int)Math.min(buffer.length, remaining)); if (read < 0) throw new EOFException(); remaining -= read; }
    }
    private static void copyRest(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[64 * 1024]; int read; while ((read = in.read(buffer)) >= 0) if (read > 0) out.write(buffer, 0, read);
    }
}
