package com.beemdevelopment.aegis.db;

import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotListException;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.HexException;

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

import java.io.UnsupportedEncodingException;

public class DatabaseFile {
    public static final byte VERSION = 1;

    private Object _content;
    private Header _header;

    public DatabaseFile() {

    }

    private DatabaseFile(Object content, Header header) {
        _content = content;
        _header = header;
    }

    public Header getHeader() {
        return _header;
    }

    public boolean isEncrypted() {
        return !_header.isEmpty();
    }

    public JSONObject toJson() {
        try {
            JSONObject obj = new JSONObject();
            obj.put("version", VERSION);
            obj.put("header", _header.toJson());
            obj.put("db", _content);
            return obj;
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] toBytes() {
        JSONObject obj = toJson();

        try {
            String string = obj.toString(4);
            return string.getBytes("UTF-8");
        } catch (JSONException | UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public static DatabaseFile fromJson(JSONObject obj) throws DatabaseFileException {
        try {
            if (obj.getInt("version") > VERSION) {
                throw new DatabaseFileException("unsupported version");
            }

            Header header = Header.fromJson(obj.getJSONObject("header"));
            if (!header.isEmpty()) {
                return new DatabaseFile(obj.getString("db"), header);
            }

            return new DatabaseFile(obj.getJSONObject("db"), header);
        } catch (JSONException e) {
            throw new DatabaseFileException(e);
        }
    }

    public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
        try {
            JSONObject obj = new JSONObject(new String(data, "UTF-8"));
            return DatabaseFile.fromJson(obj);
        } catch (UnsupportedEncodingException | JSONException e) {
            throw new DatabaseFileException(e);
        }
    }

    public JSONObject getContent() {
        return (JSONObject) _content;
    }

    public JSONObject getContent(DatabaseFileCredentials creds) throws DatabaseFileException {
        try {
            byte[] bytes = Base64.decode((String) _content);
            CryptResult result = creds.decrypt(bytes, _header.getParams());
            return new JSONObject(new String(result.getData(), "UTF-8"));
        } catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) {
            throw new DatabaseFileException(e);
        }
    }

    public void setContent(JSONObject obj) {
        _content = obj;
        _header = new Header(null, null);
    }

    public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
        try {
            String string = obj.toString(4);
            byte[] dbBytes = string.getBytes("UTF-8");

            CryptResult result = creds.encrypt(dbBytes);
            _content = Base64.encode(result.getData());
            _header = new Header(creds.getSlots(), result.getParams());
        } catch (MasterKeyException | UnsupportedEncodingException | JSONException e) {
            throw new DatabaseFileException(e);
        }
    }

    public static class Header {
        private SlotList _slots;
        private CryptParameters _params;

        public Header(SlotList slots, CryptParameters params) {
            _slots = slots;
            _params = params;
        }

        public static Header fromJson(JSONObject obj) throws DatabaseFileException {
            if (obj.isNull("slots") && obj.isNull("params")) {
                return new Header(null, null);
            }

            try {
                SlotList slots = SlotList.fromJson(obj.getJSONArray("slots"));
                CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
                return new Header(slots, params);
            } catch (SlotListException | JSONException | HexException e) {
                throw new DatabaseFileException(e);
            }
        }

        public JSONObject toJson() {
            try {
                JSONObject obj = new JSONObject();
                obj.put("slots", _slots != null ? _slots.toJson() : JSONObject.NULL);
                obj.put("params", _params != null ? _params.toJson() : JSONObject.NULL);
                return obj;
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }

        public SlotList getSlots() {
            return _slots;
        }

        public CryptParameters getParams() {
            return _params;
        }

        public boolean isEmpty() {
            return _slots == null && _params == null;
        }
    }
}
