/**
 *
 * Copyright 2017 Paul Schaub
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.omemo;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jivesoftware.smack.util.CloseableUtil;
import org.jivesoftware.smack.util.stringencoder.BareJidEncoder;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;

import org.jxmpp.jid.BareJid;

/**
 * Like a rocket!
 *
 * @author Paul Schaub
 */
public abstract class FileBasedOmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
        extends OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {

    private final FileHierarchy hierarchy;
    private static final Logger LOGGER = Logger.getLogger(FileBasedOmemoStore.class.getName());
    private static BareJidEncoder bareJidEncoder = new BareJidEncoder.UrlSafeEncoder();

    public FileBasedOmemoStore(File basePath) {
        super();
        if (basePath == null) {
            throw new IllegalStateException("No FileBasedOmemoStoreDefaultPath set in OmemoConfiguration.");
        }
        this.hierarchy = new FileHierarchy(basePath);
    }

    @Override
    public T_IdKeyPair loadOmemoIdentityKeyPair(OmemoDevice userDevice)
            throws CorruptedOmemoKeyException {
        File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(userDevice);
        return keyUtil().identityKeyPairFromBytes(readBytes(identityKeyPairPath));
    }

    @Override
    public void storeOmemoIdentityKeyPair(OmemoDevice userDevice, T_IdKeyPair identityKeyPair) {
        File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(userDevice);
        writeBytes(identityKeyPairPath, keyUtil().identityKeyPairToBytes(identityKeyPair));
    }

    @Override
    public void removeOmemoIdentityKeyPair(OmemoDevice userDevice) {
        File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(userDevice);
        if (!identityKeyPairPath.delete()) {
            LOGGER.log(Level.WARNING, "Could not delete OMEMO IdentityKeyPair " + identityKeyPairPath.getAbsolutePath());
        }
    }

    @Override
    public T_IdKey loadOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice)
            throws CorruptedOmemoKeyException {
        File identityKeyPath = hierarchy.getContactsIdentityKeyPath(userDevice, contactsDevice);
        byte[] bytes = readBytes(identityKeyPath);
        return bytes != null ? keyUtil().identityKeyFromBytes(bytes) : null;
    }

    @Override
    public void storeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice, T_IdKey t_idKey) {
        File identityKeyPath = hierarchy.getContactsIdentityKeyPath(userDevice, contactsDevice);
        writeBytes(identityKeyPath, keyUtil().identityKeyToBytes(t_idKey));
    }

    @Override
    public void removeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File identityKeyPath = hierarchy.getContactsIdentityKeyPath(userDevice, contactsDevice);
        if (!identityKeyPath.delete()) {
            LOGGER.log(Level.WARNING, "Could not delete OMEMO identityKey " + identityKeyPath.getAbsolutePath());
        }
    }

    @Override
    public SortedSet<Integer> localDeviceIdsOf(BareJid localUser) {
        SortedSet<Integer> deviceIds = new TreeSet<>();
        File userDir = hierarchy.getUserDirectory(localUser);
        File[] list = userDir.listFiles();
        for (File d : (list != null ? list : new File[] {})) {
            if (d.isDirectory()) {
                try {
                    deviceIds.add(Integer.parseInt(d.getName()));
                } catch (NumberFormatException e) {
                    // ignore
                }
            }
        }
        return deviceIds;
    }

    @Override
    public void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) {
        File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice);
        writeLong(lastMessageReceived, date.getTime());
    }

    @Override
    public Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice);
        Long date = readLong(lastMessageReceived);
        return date != null ? new Date(date) : null;
    }

    @Override
    public void setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) {
        File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice);
        writeLong(lastDeviceIdPublished, date.getTime());
    }

    @Override
    public Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice);
        Long date = readLong(lastDeviceIdPublished);
        return date != null ? new Date(date) : null;
    }

    @Override
    public void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) {
        File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice);
        writeLong(lastSignedPreKeyRenewal, date.getTime());
    }

    @Override
    public Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) {
        File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice);
        Long date = readLong(lastSignedPreKeyRenewal);
        return date != null ? new Date(date) : null;
    }

    @Override
    public T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) {
        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
        byte[] bytes = readBytes(preKeyPath);

        if (bytes != null) {
            try {
                return keyUtil().preKeyFromBytes(bytes);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e);
            }
        }

        return null;
    }

    @Override
    public void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey t_preKey) {
        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
        writeBytes(preKeyPath, keyUtil().preKeyToBytes(t_preKey));
    }

    @Override
    public void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId) {
        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
        if (!preKeyPath.delete()) {
            LOGGER.log(Level.WARNING, "Deleting OMEMO preKey " + preKeyPath.getAbsolutePath() + " failed.");
        }
    }

    @Override
    public TreeMap<Integer, T_PreKey> loadOmemoPreKeys(OmemoDevice userDevice) {
        File preKeyDirectory = hierarchy.getPreKeysDirectory(userDevice);
        TreeMap<Integer, T_PreKey> preKeys = new TreeMap<>();

        if (preKeyDirectory == null) {
            return preKeys;
        }

        File[] keys = preKeyDirectory.listFiles();

        for (File f : keys != null ? keys : new File[0]) {
            byte[] bytes = readBytes(f);
            if (bytes != null) {
                try {
                    T_PreKey p = keyUtil().preKeyFromBytes(bytes);
                    preKeys.put(Integer.parseInt(f.getName()), p);
                } catch (IOException e) {
                    LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e);
                }
            }
        }

        return preKeys;
    }

    @Override
    public T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) {
        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
        byte[] bytes = readBytes(signedPreKeyPath);
        if (bytes != null) {
            try {
                return keyUtil().signedPreKeyFromBytes(bytes);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Could not deserialize signed preKey from bytes.", e);
            }
        }
        return null;
    }

    @Override
    public TreeMap<Integer, T_SigPreKey> loadOmemoSignedPreKeys(OmemoDevice userDevice) {
        File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(userDevice);
        TreeMap<Integer, T_SigPreKey> signedPreKeys = new TreeMap<>();

        if (signedPreKeysDirectory == null) {
            return signedPreKeys;
        }

        File[] keys = signedPreKeysDirectory.listFiles();

        for (File f : keys != null ? keys : new File[0]) {
            byte[] bytes = readBytes(f);
            if (bytes != null) {
                try {
                    T_SigPreKey p = keyUtil().signedPreKeyFromBytes(bytes);
                    signedPreKeys.put(Integer.parseInt(f.getName()), p);
                } catch (IOException e) {
                    LOGGER.log(Level.WARNING, "Could not deserialize signed preKey.", e);
                }
            }
        }

        return signedPreKeys;
    }

    @Override
    public void storeOmemoSignedPreKey(OmemoDevice userDevice,
                                       int signedPreKeyId,
                                       T_SigPreKey signedPreKey) {
        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
        writeBytes(signedPreKeyPath, keyUtil().signedPreKeyToBytes(signedPreKey));
    }

    @Override
    public void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) {
        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
        if (!signedPreKeyPath.delete()) {
            LOGGER.log(Level.WARNING, "Deleting signed OMEMO preKey " + signedPreKeyPath.getAbsolutePath() + " failed.");
        }
    }

    @Override
    public T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
        byte[] bytes = readBytes(sessionPath);
        if (bytes != null) {
            try {
                return keyUtil().rawSessionFromBytes(bytes);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e);
            }
        }
        return null;
    }

    @Override
    public HashMap<Integer, T_Sess> loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) {
        File contactsDirectory = hierarchy.getContactsDir(userDevice, contact);
        HashMap<Integer, T_Sess> sessions = new HashMap<>();
        String[] devices = contactsDirectory.list();

        for (String deviceId : devices != null ? devices : new String[0]) {
            int id;
            try {
                id = Integer.parseInt(deviceId);
            } catch (NumberFormatException e) {
                continue;
            }
            OmemoDevice device = new OmemoDevice(contact, id);
            File session = hierarchy.getContactsSessionPath(userDevice, device);

            byte[] bytes = readBytes(session);

            if (bytes != null) {
                try {
                    T_Sess s = keyUtil().rawSessionFromBytes(bytes);
                    sessions.put(id, s);
                } catch (IOException e) {
                    LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e);
                }
            }

        }
        return sessions;
    }

    @Override
    public void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice, T_Sess session) {
        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
        writeBytes(sessionPath, keyUtil().rawSessionToBytes(session));
    }

    @Override
    public void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
        if (!sessionPath.delete()) {
            LOGGER.log(Level.WARNING, "Deleting raw OMEMO session " + sessionPath.getAbsolutePath() + " failed.");
        }
    }

    @Override
    public void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) {
        File contactsDirectory = hierarchy.getContactsDir(userDevice, contact);
        String[] devices = contactsDirectory.list();

        for (String deviceId : devices != null ? devices : new String[0]) {
            int id = Integer.parseInt(deviceId);
            OmemoDevice device = new OmemoDevice(contact, id);
            File session = hierarchy.getContactsSessionPath(userDevice, device);
            if (!session.delete()) {
                LOGGER.log(Level.WARNING, "Deleting raw OMEMO session " + session.getAbsolutePath() + "failed.");
            }
        }
    }

    @Override
    public boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File session = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
        return session.exists();
    }

    @Override
    public void storeOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice, int counter) {
        File messageCounterFile = hierarchy.getDevicesMessageCounterPath(userDevice, contactsDevice);
        writeIntegers(messageCounterFile, Collections.singleton(counter));
    }

    @Override
    public int loadOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice) {
        File messageCounterFile = hierarchy.getDevicesMessageCounterPath(userDevice, contactsDevice);
        Set<Integer> integers = readIntegers(messageCounterFile);

        if (integers == null || integers.isEmpty()) {
            return 0;
        }

        return integers.iterator().next();
    }

    @Override
    public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) {
        OmemoCachedDeviceList cachedDeviceList = new OmemoCachedDeviceList();

        if (contact == null) {
            throw new IllegalArgumentException("Contact can not be null.");
        }

        // active
        File activeDevicesPath = hierarchy.getContactsActiveDevicesPath(userDevice, contact);
        Set<Integer> active = readIntegers(activeDevicesPath);
        if (active != null) {
            cachedDeviceList.getActiveDevices().addAll(active);
        }

        // inactive
        File inactiveDevicesPath = hierarchy.getContactsInactiveDevicesPath(userDevice, contact);
        Set<Integer> inactive = readIntegers(inactiveDevicesPath);
        if (inactive != null) {
            cachedDeviceList.getInactiveDevices().addAll(inactive);
        }

        return cachedDeviceList;
    }

    @Override
    public void storeCachedDeviceList(OmemoDevice userDevice,
                                      BareJid contact,
                                      OmemoCachedDeviceList contactsDeviceList) {
        if (contact == null) {
            return;
        }

        File activeDevices = hierarchy.getContactsActiveDevicesPath(userDevice, contact);
        writeIntegers(activeDevices, contactsDeviceList.getActiveDevices());

        File inactiveDevices = hierarchy.getContactsInactiveDevicesPath(userDevice, contact);
        writeIntegers(inactiveDevices, contactsDeviceList.getInactiveDevices());
    }

    @Override
    public void purgeOwnDeviceKeys(OmemoDevice userDevice) {
        File deviceDirectory = hierarchy.getUserDeviceDirectory(userDevice);
        deleteDirectory(deviceDirectory);
    }

    private static void writeLong(File target, long i) {
        if (target == null) {
            LOGGER.log(Level.WARNING, "Could not write long to null-path.");
            return;
        }

        try {
            FileHierarchy.createFile(target);
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not create file.", e);
            return;
        }

        DataOutputStream out = null;
        try {
            out = new DataOutputStream(new FileOutputStream(target));
            out.writeLong(i);

        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not write longs to file.", e);
        } finally {
            CloseableUtil.maybeClose(out, LOGGER);
        }
    }

    private static Long readLong(File target) {
        if (target == null) {
            LOGGER.log(Level.WARNING, "Could not read long from null-path.");
            return null;
        }

        Long l;
        DataInputStream in = null;

        try {
            in = new DataInputStream(new FileInputStream(target));
            l = in.readLong();
        } catch (FileNotFoundException e) {
            l = null;
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not read long from file.", e);
            return null;
        } finally {
            CloseableUtil.maybeClose(in, LOGGER);
        }

        return l;
    }

    private static void writeBytes(File target, byte[] bytes) {
        if (target == null) {
            LOGGER.log(Level.WARNING, "Could not write bytes to null-path.");
            return;
        }

        // Create file
        try {
            FileHierarchy.createFile(target);
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not create file.", e);
            return;
        }

        DataOutputStream out = null;

        try {
            out = new DataOutputStream(new FileOutputStream(target));
            out.write(bytes);

        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not write bytes to file.", e);
        } finally {
            CloseableUtil.maybeClose(out, LOGGER);
        }
    }

    private static byte[] readBytes(File target) {
        if (target == null) {
            LOGGER.log(Level.WARNING, "Could not read bytes from null-path.");
            return null;
        }

        byte[] b = null;
        DataInputStream in = null;

        try {
            in = new DataInputStream(new FileInputStream(target));
            b = new byte[(int) target.length()];
            in.read(b);
        } catch (FileNotFoundException e) {
            b = null;
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not read bytes from file.", e);
        } finally {
            CloseableUtil.maybeClose(in, LOGGER);
        }

        return b;
    }

    private static void writeIntegers(File target, Set<Integer> integers) {
        if (target == null) {
            LOGGER.log(Level.WARNING, "Could not write integers to null-path.");
            return;
        }

        DataOutputStream out = null;

        try {
            out = new DataOutputStream(new FileOutputStream(target));
            for (int i : integers) {
                out.writeInt(i);
            }

        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not write integers to file.", e);
        } finally {
            CloseableUtil.maybeClose(out, LOGGER);
        }
    }

    private static Set<Integer> readIntegers(File target) {
        if (target == null) {
            LOGGER.log(Level.WARNING, "Could not read integers from null-path.");
            return null;
        }

        HashSet<Integer> integers = new HashSet<>();
        DataInputStream in = null;

        try {
            in = new DataInputStream(new FileInputStream(target));

            try {
                while (true) {
                    integers.add(in.readInt());
                }
            } catch (EOFException e) {
                // Reached end of the list.
            }

        } catch (FileNotFoundException e) {
            integers = null;
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Could not read integers.", e);
        } finally {
            CloseableUtil.maybeClose(in, LOGGER);
        }

        return integers;
    }

    /**
     * One day... *sheds a tear*
     * TODO Use methods below once Smack's minimum Android API level is 19 or higher
     */
    /*
    private static void writeLong(File target, long i)
            throws IOException
    {
        if (target == null) {
            throw new IOException("Could not write long to null-path.");
        }

        FileHierarchy.createFile(target);

        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
            out.writeLong(i);
        }
    }

    private static Long readLong(File target)
            throws IOException
    {
        if (target == null) {
            throw new IOException("Could not read long from null-path.");
        }

        if (!target.exists() || !target.isFile()) {
            return null;
        }

        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
            return in.readLong();
        }
    }

    private static void writeBytes(File target, byte[] bytes)
            throws IOException
    {
        if (target == null) {
            throw new IOException("Could not write bytes to null-path.");
        }

        // Create file
        FileHierarchy.createFile(target);

        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
            out.write(bytes);
        }
    }

    private static byte[] readBytes(File target)
            throws IOException
    {
        if (target == null) {
            throw new IOException("Could not read bytes from null-path.");
        }

        if (!target.exists() || !target.isFile()) {
            return null;
        }

        byte[] b = new byte[(int) target.length()];
        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
            in.read(b);
        }

        return b;
    }

    private static void writeIntegers(File target, Set<Integer> integers)
            throws IOException
    {
        if (target == null) {
            throw new IOException("Could not write integers to null-path.");
        }

        FileHierarchy.createFile(target);

        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
            for (int i : integers) {
                out.writeInt(i);
            }
        }
    }

    private static Set<Integer> readIntegers(File target)
            throws IOException
    {
        if (target == null) {
            throw new IOException("Could not write integers to null-path.");
        }

        if (!target.exists() || !target.isFile()) {
            return null;
        }

        HashSet<Integer> integers = new HashSet<>();

        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
            while (true) {
                try {
                    integers.add(in.readInt());
                } catch (EOFException e) {
                    break;
                }
            }
        }

        return integers;
    }
     */

    /**
     * Delete a directory with all subdirectories.
     * @param root
     */
    public static void deleteDirectory(File root) {
        File[] currList;
        Stack<File> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            if (stack.lastElement().isDirectory()) {
                currList = stack.lastElement().listFiles();
                if (currList != null && currList.length > 0) {
                    for (File curr : currList) {
                        stack.push(curr);
                    }
                } else {
                    stack.pop().delete();
                }
            } else {
                stack.pop().delete();
            }
        }
    }

    /**
     * This class represents the directory structure of the FileBasedOmemoStore.
     * The directory looks as follows:
     *
     *  OMEMO_Store/
     *      'romeo@montague.lit'/                           //Our bareJid
     *          ...
     *      'juliet@capulet.lit'/                           //Our other bareJid
     *          '13371234'/                                 //deviceId
     *              identityKeyPair                         //Our identityKeyPair
     *              lastSignedPreKeyRenewal                 //Date of when the signedPreKey was last renewed.
     *              preKeys/                                //Our preKeys
     *                  '1'
     *                  '2'
     *                  ...
     *              signedPreKeys/                          //Our signedPreKeys
     *                  '1'
     *                  '2'
     *                  ...
     *              contacts/
     *                  'romeo@capulet.lit'/                //Juliets contact Romeo
     *                      activeDevice                    //List of Romeos active devices
     *                      inactiveDevices                 //List of his inactive devices
     *                      'deviceId'/                     //Romeos deviceId
     *                          identityKey                 //Romeos identityKey
     *                          session                     //Our session with romeo
     *                          trust                       //Records about the trust in romeos device
     *                          (lastReceivedMessageDate)   //Only, for our own other devices:
     *                                                          //date of the last received message
     *
     */
    public static class FileHierarchy {

        static final String STORE = "OMEMO_Store";
        static final String CONTACTS = "contacts";
        static final String IDENTITY_KEY = "identityKey";
        static final String IDENTITY_KEY_PAIR = "identityKeyPair";
        static final String PRE_KEYS = "preKeys";
        static final String LAST_MESSAGE_RECEVIED_DATE = "lastMessageReceivedDate";
        static final String LAST_DEVICEID_PUBLICATION_DATE = "lastDeviceIdPublicationDate";
        static final String SIGNED_PRE_KEYS = "signedPreKeys";
        static final String LAST_SIGNED_PRE_KEY_RENEWAL = "lastSignedPreKeyRenewal";
        static final String SESSION = "session";
        static final String DEVICE_LIST_ACTIVE = "activeDevices";
        static final String DEVICE_LIST_INAVTIVE = "inactiveDevices";
        static final String MESSAGE_COUNTER = "messageCounter";

        File basePath;

        FileHierarchy(File basePath) {
            this.basePath = basePath;
            basePath.mkdirs();
        }

        File getStoreDirectory() {
            return createDirectory(basePath, STORE);
        }

        File getUserDirectory(OmemoDevice userDevice) {
            return getUserDirectory(userDevice.getJid());
        }

        File getUserDirectory(BareJid bareJid) {
            return createDirectory(getStoreDirectory(), bareJidEncoder.encode(bareJid));
        }

        File getUserDeviceDirectory(OmemoDevice userDevice) {
            return createDirectory(getUserDirectory(userDevice.getJid()),
                    Integer.toString(userDevice.getDeviceId()));
        }

        File getContactsDir(OmemoDevice userDevice) {
            return createDirectory(getUserDeviceDirectory(userDevice), CONTACTS);
        }

        File getContactsDir(OmemoDevice userDevice, BareJid contact) {
            return createDirectory(getContactsDir(userDevice), bareJidEncoder.encode(contact));
        }

        File getContactsDir(OmemoDevice userDevice, OmemoDevice contactsDevice) {
            return createDirectory(getContactsDir(userDevice, contactsDevice.getJid()),
                    Integer.toString(contactsDevice.getDeviceId()));
        }

        File getIdentityKeyPairPath(OmemoDevice userDevice) {
            return new File(getUserDeviceDirectory(userDevice), IDENTITY_KEY_PAIR);
        }

        File getPreKeysDirectory(OmemoDevice userDevice) {
            return createDirectory(getUserDeviceDirectory(userDevice), PRE_KEYS);
        }

        File getPreKeyPath(OmemoDevice userDevice, int preKeyId) {
            return new File(getPreKeysDirectory(userDevice), Integer.toString(preKeyId));
        }

        File getLastMessageReceivedDatePath(OmemoDevice userDevice, OmemoDevice device) {
            return new File(getContactsDir(userDevice, device), LAST_MESSAGE_RECEVIED_DATE);
        }

        File getLastDeviceIdPublicationDatePath(OmemoDevice userDevice, OmemoDevice device) {
            return new File(getContactsDir(userDevice, device), LAST_DEVICEID_PUBLICATION_DATE);
        }

        File getSignedPreKeysDirectory(OmemoDevice userDevice) {
            return createDirectory(getUserDeviceDirectory(userDevice), SIGNED_PRE_KEYS);
        }

        File getLastSignedPreKeyRenewal(OmemoDevice userDevice) {
            return new File(getUserDeviceDirectory(userDevice), LAST_SIGNED_PRE_KEY_RENEWAL);
        }

        File getContactsIdentityKeyPath(OmemoDevice userDevice, OmemoDevice contactsDevice) {
            return new File(getContactsDir(userDevice, contactsDevice), IDENTITY_KEY);

        }

        File getContactsSessionPath(OmemoDevice userDevice, OmemoDevice contactsDevice) {
            return new File(getContactsDir(userDevice, contactsDevice), SESSION);
        }

        File getContactsActiveDevicesPath(OmemoDevice userDevice, BareJid contact) {
            return new File(getContactsDir(userDevice, contact), DEVICE_LIST_ACTIVE);
        }

        File getContactsInactiveDevicesPath(OmemoDevice userDevice, BareJid contact) {
            return new File(getContactsDir(userDevice, contact), DEVICE_LIST_INAVTIVE);
        }

        File getDevicesMessageCounterPath(OmemoDevice userDevice, OmemoDevice otherDevice) {
            return new File(getContactsDir(userDevice, otherDevice), MESSAGE_COUNTER);
        }

        private static File createFile(File f) throws IOException {
            File p = f.getParentFile();
            createDirectory(p);
            f.createNewFile();
            return f;

        }

        private static File createDirectory(File dir, String subdir) {
            File f = new File(dir, subdir);
            return createDirectory(f);
        }

        private static File createDirectory(File f) {
            if (f.exists() && f.isDirectory()) {
                return f;
            }

            f.mkdirs();
            return f;
        }
    }

    /**
     * Convert {@link BareJid BareJids} to Strings using the legacy {@link BareJid#toString()} method instead of the
     * proper, url safe {@link BareJid#asUrlEncodedString()} method.
     * While it is highly advised to use the new format, you can use this method to stay backwards compatible to data
     * sets created by the old implementation.
     */
    @SuppressWarnings("deprecation")
    public static void useLegacyBareJidEncoding() {
        bareJidEncoder = new BareJidEncoder.LegacyEncoder();
    }
}
