/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.curate;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.services.factory.DSpaceServicesFactory;

/**
 * FileTaskQueue provides a TaskQueue implementation based on flat files
 * for the queues and semaphores.
 *
 * @author richardrodgers
 */
public class FileTaskQueue implements TaskQueue {
    private static final Logger log = LogManager.getLogger(TaskQueue.class);

    // base directory for curation task queues
    protected String tqDir;

    // ticket for queue readers
    protected long readTicket = -1L;

    // list of queues owned by reader
    protected List<Integer> readList = new ArrayList<>();

    public FileTaskQueue() {
        tqDir = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("curate.taskqueue.dir");
    }

    @Override
    public String[] queueNames() {
        return new File(tqDir).list();
    }

    @Override
    public synchronized void enqueue(String queueName, TaskQueueEntry entry)
        throws IOException {
        Set entrySet = new HashSet<TaskQueueEntry>();
        entrySet.add(entry);
        enqueue(queueName, entrySet);
    }

    @Override
    public synchronized void enqueue(String queueName, Set<TaskQueueEntry> entrySet)
        throws IOException {
        // don't block or fail - iterate until an unlocked queue found/created
        int queueIdx = 0;
        File qDir = ensureQueue(queueName);
        while (true) {
            File lock = new File(qDir, "lock" + Integer.toString(queueIdx));

            // Check for lock, and create one if it doesn't exist.
            // If the lock file already exists, this will return false
            if (lock.createNewFile()) {
                // append set contents to queue
                BufferedWriter writer = null;
                try {
                    File queue = new File(qDir, "queue" + Integer.toString(queueIdx));
                    writer = new BufferedWriter(new FileWriter(queue, StandardCharsets.UTF_8, true));
                    Iterator<TaskQueueEntry> iter = entrySet.iterator();
                    while (iter.hasNext()) {
                        writer.write(iter.next().toString());
                        writer.newLine();
                    }
                } finally {
                    if (writer != null) {
                        writer.close();
                    }
                }
                // remove lock
                if (!lock.delete()) {
                    log.error("Unable to remove lock: " + lock.getName());
                }
                break;
            }
            queueIdx++;
        }
    }

    @Override
    public synchronized Set<TaskQueueEntry> dequeue(String queueName, long ticket)
        throws IOException {
        Set<TaskQueueEntry> entrySet = new HashSet<>();
        if (readTicket == -1L) {
            // hold the ticket & copy all Ids available, locking queues
            // stop when no more queues or one found locked
            File qDir = ensureQueue(queueName);
            readTicket = ticket;
            int queueIdx = 0;
            while (true) {
                File queue = new File(qDir, "queue" + Integer.toString(queueIdx));
                File lock = new File(qDir, "lock" + Integer.toString(queueIdx));

                // If the queue file exists, atomically check for a lock file and create one if it doesn't exist
                // If the lock file exists already, then this simply returns false
                if (queue.exists() && lock.createNewFile()) {
                    // read contents from file
                    BufferedReader reader = null;
                    try {
                        reader = new BufferedReader(new FileReader(queue, StandardCharsets.UTF_8));
                        String entryStr;
                        while ((entryStr = reader.readLine()) != null) {
                            entryStr = entryStr.trim();
                            if (entryStr.length() > 0) {
                                entrySet.add(new TaskQueueEntry(entryStr));
                            }
                        }
                    } finally {
                        if (reader != null) {
                            reader.close();
                        }
                    }
                    readList.add(queueIdx);
                } else {
                    break;
                }
                queueIdx++;
            }
        }
        return entrySet;
    }

    @Override
    public synchronized void release(String queueName, long ticket, boolean remove) {
        if (ticket == readTicket) {
            readTicket = -1L;
            File qDir = ensureQueue(queueName);
            // remove locks & queues (if flag true)
            for (Integer queueIdx : readList) {
                File lock = new File(qDir, "lock" + Integer.toString(queueIdx));
                if (remove) {
                    File queue = new File(qDir, "queue" + Integer.toString(queueIdx));
                    if (!queue.delete()) {
                        log.error("Unable to delete queue file: " + queue.getName());
                    }
                }

                if (!lock.delete()) {
                    log.error("Unable to delete lock file: " + lock.getName());
                }
            }
            readList.clear();
        }
    }

    protected File ensureQueue(String queueName) {
        // create directory structures as needed
        File baseDir = new File(tqDir, queueName);
        if (!baseDir.exists() && !baseDir.mkdirs()) {
            throw new IllegalStateException("Unable to create directories");
        }

        return baseDir;
    }
}
