package ch.threema.domain.protocol.csp.messages.file;

import static ch.threema.common.JavaCompat.hexToByteArray;
import static ch.threema.common.JavaCompat.toHexString;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import ch.threema.domain.protocol.csp.ProtocolDefines;
import ch.threema.domain.protocol.csp.messages.BadMessageException;

public class FileData {
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({RENDERING_DEFAULT, RENDERING_MEDIA, RENDERING_STICKER})
    public @interface RenderingType {
    }

    public static final int RENDERING_DEFAULT = 0;
    public static final int RENDERING_MEDIA = 1;
    public static final int RENDERING_STICKER = 2;

    private final static String KEY_BLOB_ID = "b";
    private final static String KEY_THUMBNAIL_BLOB_ID = "t";
    private final static String KEY_ENCRYPTION_KEY = "k";
    private final static String KEY_MIME_TYPE = "m";
    private final static String KEY_THUMBNAIL_MIME_TYPE = "p";
    private final static String KEY_FILE_NAME = "n";
    private final static String KEY_FILE_SIZE = "s";
    private final static String KEY_RENDERING_TYPE_DEPRECATED = "i";
    private final static String KEY_RENDERING_TYPE = "j";
    private final static String KEY_CAPTION = "d";
    private final static String KEY_CORRELATION_ID = "c";
    private final static String KEY_META_DATA = "x";

    private byte[] fileBlobId;
    private byte[] thumbnailBlobId;
    private byte[] encryptionKey;
    private String mimeType;
    @Nullable
    private String thumbnailMimeType;
    private long fileSize;
    private String fileName;
    @RenderingType
    private int renderingType;
    private String caption;
    private String correlationId;
    @Nullable
    private Map<String, Object> metaData;

    public byte[] getFileBlobId() {
        return fileBlobId;
    }

    public FileData setFileBlobId(byte[] fileBlobId) {
        this.fileBlobId = fileBlobId;
        return this;
    }

    public byte[] getThumbnailBlobId() {
        return thumbnailBlobId;
    }

    public FileData setThumbnailBlobId(byte[] thumbnailBlobId) {
        this.thumbnailBlobId = thumbnailBlobId;
        return this;
    }

    public byte[] getEncryptionKey() {
        return encryptionKey;
    }

    public FileData setEncryptionKey(byte[] encryptionKey) {
        this.encryptionKey = encryptionKey;
        return this;
    }

    public String getMimeType() {
        return mimeType;
    }

    public FileData setMimeType(String mimeType) {
        this.mimeType = mimeType;
        return this;
    }

    @Nullable
    public String getThumbnailMimeType() {
        return thumbnailMimeType;
    }

    public FileData setThumbnailMimeType(@Nullable String thumbnailMimeType) {
        this.thumbnailMimeType = thumbnailMimeType;
        return this;
    }

    public long getFileSize() {
        return fileSize;
    }

    public FileData setFileSize(long fileSize) {
        this.fileSize = fileSize;
        return this;
    }

    public String getFileName() {
        return fileName;
    }

    public FileData setFileName(String fileName) {
        this.fileName = fileName;
        return this;
    }

    public @RenderingType int getRenderingType() {
        return this.renderingType;
    }

    public FileData setRenderingType(@RenderingType int renderingType) {
        this.renderingType = renderingType;
        return this;
    }

    public String getCaption() {
        return this.caption;
    }

    public FileData setCaption(String caption) {
        if (caption != null && !caption.isBlank()) {
            this.caption = caption;
        } else {
            this.caption = null;
        }
        return this;
    }

    public String getCorrelationId() {
        return this.correlationId;
    }

    public FileData setCorrelationId(String correlationId) {
        this.correlationId = correlationId;
        return this;
    }

    @Nullable
    public Map<String, Object> getMetaData() {
        return this.metaData;
    }

    public FileData setMetaData(@Nullable Map<String, Object> metaData) {
        this.metaData = metaData;
        return this;
    }

    public static FileData parse(String jsonObjectString) throws BadMessageException {
        try {
            JSONObject o = new JSONObject(jsonObjectString);

            FileData fileData = new FileData();

            try {
                fileData.fileBlobId = hexToByteArray(o.getString(KEY_BLOB_ID));
            } catch (IllegalArgumentException e) {
                throw new BadMessageException("TM038");
            }

            //optional field
            if (o.has(KEY_THUMBNAIL_BLOB_ID)) {
                try {
                    fileData.thumbnailBlobId = hexToByteArray(o.getString(KEY_THUMBNAIL_BLOB_ID));
                } catch (IllegalArgumentException e) {
                    throw new BadMessageException("TM039");
                }
            }

            try {
                fileData.encryptionKey = hexToByteArray(o.getString(KEY_ENCRYPTION_KEY));
                if (fileData.encryptionKey.length != ProtocolDefines.BLOB_KEY_LEN) {
                    throw new BadMessageException("Invalid encryption key length: " + fileData.encryptionKey.length);
                }
            } catch (IllegalArgumentException e) {
                throw new BadMessageException("TM040");
            }

            try {
                fileData.mimeType = o.getString(KEY_MIME_TYPE);
            } catch (IllegalArgumentException e) {
                throw new BadMessageException("TM041");
            }

            //optional field
            if (o.has(KEY_THUMBNAIL_MIME_TYPE)) {
                fileData.thumbnailMimeType = o.getString(KEY_THUMBNAIL_MIME_TYPE);
            }

            //optional field
            if (o.has(KEY_FILE_NAME)) {
                fileData.fileName = o.getString(KEY_FILE_NAME);
            }

            try {
                fileData.fileSize = o.getInt(KEY_FILE_SIZE);
            } catch (IllegalArgumentException e) {
                throw new BadMessageException("TM042");
            }

            if (o.has(KEY_RENDERING_TYPE)) {
                fileData.renderingType = o.getInt(KEY_RENDERING_TYPE);
                if (fileData.renderingType > RENDERING_STICKER) {
                    fileData.renderingType = RENDERING_DEFAULT;
                }
            } else {
                try {
                    fileData.renderingType = o.getInt(KEY_RENDERING_TYPE_DEPRECATED);
                } catch (IllegalArgumentException | JSONException e) {
                    fileData.renderingType = RENDERING_DEFAULT;
                }
            }

            //optional field
            if (o.has(KEY_CAPTION)) {
                try {
                    fileData.caption = o.getString(KEY_CAPTION);
                } catch (IllegalArgumentException e) {
                    //ignore, optional field
                }
            }

            if (o.has(KEY_CORRELATION_ID)) {
                try {
                    fileData.correlationId = o.getString(KEY_CORRELATION_ID);
                } catch (IllegalArgumentException e) {
                    //ignore, optional field
                }
            }

            if (o.has(KEY_META_DATA)) {
                try {
                    final JSONObject metaData = o.getJSONObject(KEY_META_DATA);

                    Iterator<String> keys = metaData.keys();
                    fileData.metaData = new HashMap<>();
                    while (keys.hasNext()) {
                        String key = keys.next();
                        fileData.metaData.put(key, metaData.get(key));
                    }
                } catch (IllegalArgumentException e) {
                    //ignore, optional field
                }
            }
            return fileData;
        } catch (JSONException e) {
            throw new BadMessageException("TM037", e);
        }
    }

    public void write(ByteArrayOutputStream bos) throws Exception {
        bos.write(this.generateString().getBytes(StandardCharsets.UTF_8));
    }

    protected String generateString() throws BadMessageException {
        JSONObject o = new JSONObject();
        try {
            o.put(KEY_BLOB_ID, toHexString(this.fileBlobId));
            o.put(KEY_THUMBNAIL_BLOB_ID, toHexString(this.thumbnailBlobId));
            o.put(KEY_ENCRYPTION_KEY, toHexString(this.encryptionKey));
            o.put(KEY_MIME_TYPE, this.mimeType);
            o.put(KEY_THUMBNAIL_MIME_TYPE, this.thumbnailMimeType);
            o.put(KEY_FILE_NAME, this.fileName);
            o.put(KEY_FILE_SIZE, this.fileSize);
            o.put(KEY_RENDERING_TYPE_DEPRECATED, this.renderingType == RENDERING_MEDIA ? RENDERING_MEDIA : RENDERING_DEFAULT);
            if (this.caption != null) {
                o.put(KEY_CAPTION, this.caption);
            }

            if (this.correlationId != null) {
                o.put(KEY_CORRELATION_ID, this.correlationId);
            }

            if (this.metaData != null) {
                JSONObject metaDataJsonObject = new JSONObject();

                for (Map.Entry<String, Object> metaValue : this.metaData.entrySet()) {
                    metaDataJsonObject.put(metaValue.getKey(), metaValue.getValue());
                }
                o.put(KEY_META_DATA, metaDataJsonObject);
            }
            o.put(KEY_RENDERING_TYPE, this.renderingType);
        } catch (Exception e) {
            throw new BadMessageException("TM037", e);
        }

        return o.toString();
    }
}
