/*
 * Copyright 2015-2016 Todd Kulesza <[email protected]>.
 *
 * This file is part of Archivo.
 *
 * Archivo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Archivo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Archivo.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.straylightlabs.archivo.model;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;

/**
 * Store a record of each successful archive task, so we can show the user which
 * recordings they've already archived.
 */
public class ArchiveHistory {
    private final Path location;
    private final Map<String, ArchiveHistoryItem> items;

    private final static String ATT_ID = "id";
    private final static String ATT_DATE = "date";
    private final static String ATT_PATH = "path";

    private final static Logger logger = LoggerFactory.getLogger(ArchiveHistory.class);
    private final static DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();

    public static ArchiveHistory loadFrom(Path location) {
        ArchiveHistory ah = new ArchiveHistory(location);
        if (ah.exists()) {
            ah.load();
        }
        return ah;
    }

    private ArchiveHistory(Path location) {
        this.location = location;
        this.items = new HashMap<>();
    }

    private boolean exists() {
        return Files.isRegularFile(location);
    }

    private void load() {
        logger.info("Loading archive history from {}", location);
        try (InputStream historyReader = Files.newInputStream(location)) {
            DocumentBuilder builder = builderFactory.newDocumentBuilder();
            Document doc = builder.parse(historyReader);
            NodeList itemList = doc.getElementsByTagName("Item");
            for (int i = 0; i < itemList.getLength(); i++) {
                Node item = itemList.item(i);
                NamedNodeMap attributes = item.getAttributes();
                String id = attributes.getNamedItem(ATT_ID).getTextContent();
                LocalDate date = LocalDate.parse(attributes.getNamedItem(ATT_DATE).getTextContent());
                Path location = Paths.get(attributes.getNamedItem(ATT_PATH).getTextContent());
                ArchiveHistoryItem historyItem = new ArchiveHistoryItem(id, date, location);
                items.put(id, historyItem);
            }
        } catch (ParserConfigurationException | SAXException | IOException e) {
            logger.error("Error loading archive history: ", e);
        }
    }

    public void save() {
        logger.info("Saving archive history to {}", location);
        try (BufferedWriter historyWriter = Files.newBufferedWriter(location)) {
            DocumentBuilder builder = builderFactory.newDocumentBuilder();
            Document doc = builder.newDocument();
            Element root = doc.createElement("HistoryItems");
            doc.appendChild(root);
            items.entrySet().forEach(entry -> {
                ArchiveHistoryItem item = entry.getValue();
                Element element = doc.createElement("Item");
                element.setAttribute(ATT_ID, item.getRecordingId());
                element.setAttribute(ATT_DATE, item.getDateArchived().toString());
                element.setAttribute(ATT_PATH, item.getLocation().toString());
                root.appendChild(element);
            });
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            DOMSource source = new DOMSource(doc);
            StreamResult historyFile = new StreamResult(historyWriter);
            transformer.transform(source, historyFile);
        } catch (ParserConfigurationException | TransformerException | IOException e) {
            logger.error("Error saving archive history: ", e);
        }
    }

    public boolean contains(Recording recording) {
        verifyRecordingIsValid(recording);

        return items.containsKey(recording.getRecordingId());
    }

    private void verifyRecordingIsValid(Recording recording) {
        if (recording == null) {
            throw new IllegalArgumentException("Recording can't be null");
        }
    }

    public ArchiveHistoryItem get(Recording recording) {
        verifyRecordingIsValid(recording);

        ArchiveHistoryItem item = items.get(recording.getRecordingId());
        if (item == null) {
            throw new IllegalArgumentException(
                    String.format("No history exists for recording '%s'", recording.getRecordingId())
            );
        }

        return item;
    }

    public void add(Recording recording) {
        verifyRecordingIsValid(recording);

        ArchiveHistoryItem historyItem = new ArchiveHistoryItem(
                recording.getRecordingId(), LocalDate.now(), recording.getDestination()
        );
        items.put(historyItem.getRecordingId(), historyItem);
    }

    public static class ArchiveHistoryItem {
        private final LocalDate dateArchived;
        private final Path location;
        private final String recordingId;

        public ArchiveHistoryItem(String recordingId, LocalDate dateArchived, Path location) {
            this.recordingId = recordingId;
            this.dateArchived = dateArchived;
            this.location = location;
        }

        public String getRecordingId() {
            return recordingId;
        }

        public Path getLocation() {
            return location;
        }

        public LocalDate getDateArchived() {
            return dateArchived;
        }
    }
}