package ctbrec.recorder;

import static ctbrec.Recording.State.*;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.sites.Site;

public class RecordingManager {
    private static final Logger LOG = LoggerFactory.getLogger(RecordingManager.class);

    private Config config;
    private Moshi moshi;
    private JsonAdapter<Recording> adapter;
    private List<Recording> recordings = new ArrayList<>();
    private ReentrantLock recordingsLock = new ReentrantLock();

    public RecordingManager(Config config, List<Site> sites) throws IOException {
        this.config = config;
        moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter(sites))
                .add(Instant.class, new InstantJsonAdapter())
                .build();
        adapter = moshi.adapter(Recording.class).indent("  ");

        loadRecordings();
    }

    public void add(Recording rec) throws IOException {
        saveRecording(rec);
        recordingsLock.lock();
        try {
            recordings.add(rec);
        } finally {
            recordingsLock.unlock();
        }
    }

    public void saveRecording(Recording rec) throws IOException {
        String json = adapter.toJson(rec);
        File recordingsMetaDir = getDir();
        String filename = rec.toString() + ".json";
        File recordingMetaData = new File(recordingsMetaDir, filename);
        rec.setMetaDataFile(recordingMetaData.getAbsolutePath());
        Files.createDirectories(recordingsMetaDir.toPath());
        Files.write(recordingMetaData.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
    }

    private void loadRecordings() throws IOException {
        File recordingsMetaDir = getDir();
        File[] metaFiles = recordingsMetaDir.listFiles((file, name) -> name.endsWith(".json"));
        if (metaFiles != null) {
            for (File file : metaFiles) {
                String json = new String(Files.readAllBytes(file.toPath()), UTF_8);
                try {
                    Recording recording = adapter.fromJson(json);
                    if (recording.getStatus() == RECORDING || recording.getStatus() == GENERATING_PLAYLIST || recording.getStatus() == POST_PROCESSING) {
                        recording.setStatus(WAITING);
                    }
                    if (recordingExists(recording)) {
                        recordings.add(recording);
                    } else {
                        LOG.info("Recording {} does not exist anymore -> deleting meta data", recording);
                        Files.deleteIfExists(file.toPath());
                    }
                } catch(Exception e) {
                    LOG.error("Couldn't load recording {}", file, e);
                }
            }
        }
    }

    private boolean recordingExists(Recording recording) {
        File rec = new File(config.getSettings().recordingsDir, recording.getPath());
        return rec.exists();
    }

    private File getDir() {
        File configDir = config.getConfigDir();
        File recordingsMetaDir = new File(configDir, "recordings");
        return recordingsMetaDir;
    }

    /**
     * Deletes a recording from disk and removes it from ctbrec
     * @param recording
     * @throws IOException
     */
    public void delete(Recording recording) throws IOException {
        if (recording.isPinned()) {
            throw new RecordingPinnedException(recording);
        }

        recordingsLock.lock();
        try {
            int idx = recordings.indexOf(recording);
            recording = recordings.get(idx);

            recording.setStatus(State.DELETING);
            File recordingsDir = new File(config.getSettings().recordingsDir);
            File path = new File(recordingsDir, recording.getPath());
            LOG.debug("Deleting {}", path);

            // delete the video files
            if (path.isFile()) {
                Files.delete(path.toPath());
                deleteEmptyParents(path.getParentFile());
            } else {
                deleteDirectory(path);
                deleteEmptyParents(path);
            }

            // delete the meta data
            Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());

            // remove from data structure
            recordings.remove(recording);
            recording.setStatus(State.DELETED);
        } finally {
            recordingsLock.unlock();
        }
    }

    /**
     * Removes the recording from ctbrec, but does not touch the actual file
     * @param recording
     * @throws IOException
     */
    public void remove(Recording recording) throws IOException {
        recordingsLock.lock();
        try {
            int idx = recordings.indexOf(recording);
            recording = recordings.get(idx);
            File recordingsDir = new File(config.getSettings().recordingsDir);
            File path = new File(recordingsDir, recording.getPath());
            deleteEmptyParents(path.getParentFile());
            // delete the meta data
            Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
            // remove from data structure
            recordings.remove(recording);
        } finally {
            recordingsLock.unlock();
        }
    }

    public List<Recording> getAll() {
        recordingsLock.lock();
        try {
            Instant start = Instant.now();
            for (Recording recording : recordings) {
                recording.refresh();
            }
            Instant finish = Instant.now();
            long timeElapsed = Duration.between(start, finish).toMillis();
            LOG.trace("Recordings list refreshed in {} ms", timeElapsed);
            return new ArrayList<>(recordings);
        } finally {
            recordingsLock.unlock();
        }
    }

    public static void deleteEmptyParents(File parent) throws IOException {
        File recDir = new File(Config.getInstance().getSettings().recordingsDir);
        while (parent != null && parent.list() != null && parent.list().length == 0) {
            if (parent.equals(recDir)) {
                return;
            }
            LOG.debug("Deleting empty directory {}", parent.getAbsolutePath());
            Files.delete(parent.toPath());
            parent = parent.getParentFile();
        }
    }

    private void deleteDirectory(File directory) throws IOException {
        if (!directory.exists()) {
            return;
        }

        File[] files = directory.listFiles();
        boolean deletedAllFiles = true;
        for (File file : files) {
            try {
                LOG.trace("Deleting {}", file.getAbsolutePath());
                Files.delete(file.toPath());
            } catch (Exception e) {
                deletedAllFiles = false;
                LOG.debug("Couldn't delete {}", file, e);
            }
        }

        if (!deletedAllFiles) {
            throw new IOException("Couldn't delete all files in " + directory);
        }
    }

    public void pin(Recording recording) throws IOException {
        recordingsLock.lock();
        try {
            recording.setPinned(true);
            saveRecording(recording);
        } finally {
            recordingsLock.unlock();
        }
    }

    public void unpin(Recording recording) throws IOException {
        recordingsLock.lock();
        try {
            recording.setPinned(false);
            saveRecording(recording);
        } finally {
            recordingsLock.unlock();
        }
    }
}
