/* BdfServer - Copyright (c) 2011-2025 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package fr.exemole.bdfserver.tools.storage;

import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.BdfServerConstants;
import fr.exemole.bdfserver.api.storage.BdfStorageException;
import fr.exemole.bdfserver.api.storage.EditableResourceStorage;
import fr.exemole.bdfserver.api.storage.StorageContent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import net.fichotheque.EditOrigin;
import net.fichotheque.corpus.FicheMeta;
import net.fichotheque.history.HistoryUnit;
import net.fichotheque.tools.history.HistoryUnitBuilder;
import net.fichotheque.utils.EditOriginUtils;
import net.mapeadores.util.exceptions.ShouldNotOccurException;
import net.mapeadores.util.io.FileUtils;
import net.mapeadores.util.io.IOUtils;
import net.mapeadores.util.io.ResourceStorage;
import net.mapeadores.util.date.FuzzyDate;
import net.mapeadores.util.text.RelativePath;
import net.mapeadores.util.text.StringUtils;
import org.w3c.dom.Document;


/**
 *
 * @author Vincent Calame
 */
public final class StorageUtils {

    public final static String EXTENSION_RESOURCE_PREFIX = "ext-rsc/";
    public final static RelativePath EXTENSION_RESOURCE_ROOT = RelativePath.build("ext-rsc");
    public final static String APPS_PREFIX = "apps/";
    public final static RelativePath APPS_ROOT = RelativePath.build("apps");
    public final static RelativePath ICON = RelativePath.build("images/icon.png");
    public final static RelativePath ICON32 = RelativePath.build("images/icon32.png");
    public final static RelativePath ICON48 = RelativePath.build("images/icon48.png");
    public final static RelativePath LOGO = RelativePath.build("images/logo.png");
    public final static RelativePath LOGO_ODT_INI = RelativePath.build("images/logo-odt.ini");
    public final static Pattern NAME_PATTERN = Pattern.compile("^[_a-zA-Z][-_.a-zA-Z0-9]*$");
    public final static List<StorageContent> EMPTY_STORAGECONTENTLIST = Collections.emptyList();
    private final static String NULL = "NULL";


    private StorageUtils() {
    }

    public static String getExtensionResourcePath(String registrationName, String relativePath) {
        return EXTENSION_RESOURCE_PREFIX + registrationName + "/" + relativePath;
    }

    public static RelativePath parseExtensionResourcePath(String registrationName, String relativePath) throws ParseException {
        return RelativePath.parse(getExtensionResourcePath(registrationName, relativePath));
    }

    public static RelativePath buildExtensionResourcePath(String registrationName, String relativePath) {
        try {
            return parseExtensionResourcePath(registrationName, relativePath);
        } catch (ParseException pe) {
            throw new IllegalArgumentException(pe.getMessage());
        }
    }

    public static String getAppResourcePath(String appName, String relativePath) {
        return APPS_PREFIX + appName + "/" + relativePath;
    }

    public static RelativePath parseAppResourcePath(String appName, String relativePath) throws ParseException {
        return RelativePath.parse(getAppResourcePath(appName, relativePath));
    }

    public static RelativePath buildAppResourcePath(String appName, String relativePath) {
        try {
            return parseAppResourcePath(appName, relativePath);
        } catch (ParseException pe) {
            throw new IllegalArgumentException(pe.getMessage());
        }
    }

    public static boolean containsDefaultResource(BdfServer bdfServer, RelativePath relativePath) {
        for (ResourceStorage resourceStorage : bdfServer.getResourceStorages()) {
            if (!resourceStorage.getName().equals(BdfServerConstants.VAR_STORAGE)) {
                boolean here = resourceStorage.containsResource(relativePath);
                if (here) {
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean containsVarResource(BdfServer bdfServer, RelativePath relativePath) {
        ResourceStorage resourceStorage = bdfServer.getVarResourceStorage();
        if (resourceStorage != null) {
            return resourceStorage.containsResource(relativePath);
        } else {
            return false;
        }
    }

    public static boolean removeResource(BdfServer bdfServer, RelativePath relativePath, EditOrigin editOrigin) {
        EditableResourceStorage resourceStorage = bdfServer.getVarResourceStorage();
        if (resourceStorage != null) {
            return resourceStorage.removeResource(relativePath, editOrigin);
        } else {
            return false;
        }
    }

    public static void saveResource(BdfServer bdfServer, RelativePath relativePath, InputStream inputStream, EditOrigin editOrigin) throws IOException {
        EditableResourceStorage resourceStorage = bdfServer.getVarResourceStorage();
        if (resourceStorage != null) {
            resourceStorage.saveResource(relativePath, inputStream, editOrigin);
        }
    }

    public static void createDirectory(BdfServer bdfServer, RelativePath relativePath) {
        EditableResourceStorage resourceStorage = bdfServer.getVarResourceStorage();
        if (resourceStorage != null) {
            resourceStorage.createFolder(relativePath);
        }
    }

    public static boolean isValidResourceName(String name) {
        Matcher matcher = NAME_PATTERN.matcher(name);
        if (matcher.matches()) {
            int idx = name.indexOf('.');
            if ((idx > 0) && (idx < (name.length() - 1))) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    public static boolean isValidDirectoryName(String name) {
        Matcher matcher = NAME_PATTERN.matcher(name);
        return matcher.matches();
    }

    public static boolean isValidResourceRelativePath(RelativePath relativePath) {
        String[] array = relativePath.toArray();
        int length = array.length;
        if (length < 2) {
            return false;
        }
        for (int i = 0; i < (length - 1); i++) {
            String name = array[i];
            if (!isValidDirectoryName(name)) {
                return false;
            }
        }
        return isValidResourceName(array[length - 1]);
    }

    public static void writePasswordArray(Writer writer, String[] passwordArray) throws IOException {
        int length = passwordArray.length / 2;
        for (int i = 0; i < length; i++) {
            writer.write(passwordArray[i * 2]);
            writer.write(":");
            writer.write(passwordArray[i * 2 + 1]);
            writer.write("\n");
        }
    }

    public static void writeExtensionList(Writer writer, List<String> extensionList) throws IOException {
        for (String registrationName : extensionList) {
            writer.append(registrationName);
            writer.write("\n");
        }
    }

    public static void writeChrono(Writer writer, FicheMeta ficheMeta) throws IOException {
        FuzzyDate creationDate = ficheMeta.getCreationDate();
        if (creationDate != null) {
            writer.write(creationDate.toString());
            FuzzyDate modificationDate = ficheMeta.getModificationDate();
            if (!modificationDate.equals(creationDate)) {
                writer.write("\n");
                writer.write(modificationDate.toString());
            }
        }
    }

    public static HistoryUnit getHistoryUnit(File mainFile, File backupFile) {
        HistoryUnitBuilder builder = new HistoryUnitBuilder();
        if (mainFile.exists()) {
            populateLastRevisions(backupFile, builder);
        } else {
            builder.setDeleted(true);
        }
        File archivesDir = getArchivesDir(backupFile);
        if ((archivesDir.exists())) {
            File versionsLogFile = new File(archivesDir, "versions.log");
            if (versionsLogFile.exists()) {
                LogBlock logBlock = null;
                List<LogBlock> logBlockList = new ArrayList<LogBlock>();
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(versionsLogFile), "UTF-8"))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        if (line.isEmpty()) {
                            if (logBlock != null) {
                                logBlockList.add(logBlock);
                                logBlock = null;
                            }
                        } else if (logBlock == null) {
                            logBlock = new LogBlock(line);
                        } else {
                            logBlock.addLine(line);
                        }
                    }
                    if (logBlock != null) {
                        logBlockList.add(logBlock);
                    }
                } catch (IOException ioe) {
                }
                int size = logBlockList.size();
                for (int i = (size - 1); i >= 0; i--) {
                    logBlockList.get(i).addRevision(builder);
                }
            }
        }
        return builder.toHistoryUnit();
    }

    public static void archive(File mainFile, File backupFile, EditOrigin editOrigin) throws IOException {
        if (backupFile == null) {
            return;
        }
        SessionInfo sessionInfo = initSessionInfo(backupFile, editOrigin);
        if (mainFile.exists()) {
            byte[] mainArray;
            if (FileUtils.contentEquals(mainFile, backupFile)) {
                mainArray = null;
            } else {
                mainArray = FileUtils.readFileToByteArray(mainFile);
                try (FileOutputStream os = new FileOutputStream(backupFile)) {
                    IOUtils.write(mainArray, os);
                }
            }
            if (sessionInfo.isSameSession()) {
                sessionInfo.appendEditOrigin();
            } else {
                if ((!sessionInfo.isFirstSession()) && (mainArray != null)) {
                    ArchivesInfo archivesInfo = new ArchivesInfo(getArchivesDir(backupFile));
                    archivesInfo.archive(mainArray, sessionInfo, false);
                }
                sessionInfo.saveNewSession();
            }
        } else {
            if (backupFile.exists()) {
                backupFile.delete();
            }
            sessionInfo.saveNewSession();
        }
    }

    public static void archiveBeforeDelete(File mainFile, File backupFile, EditOrigin editOrigin) throws IOException {
        if (backupFile == null) {
            return;
        }
        SessionInfo sessionInfo = initSessionInfo(backupFile, editOrigin);
        if (mainFile.exists()) {
            ArchivesInfo archivesInfo = new ArchivesInfo(getArchivesDir(backupFile));
            archivesInfo.archive(FileUtils.readFileToByteArray(mainFile), sessionInfo, true);
        }
        if (backupFile.exists()) {
            backupFile.delete();
        }
        sessionInfo.deleteSessionFile();
    }


    public static Document readDocument(File mainFile, File backupFile, String revisionName) {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docbuilder;
        try {
            docbuilder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException pce) {
            throw new BdfStorageException(pce);
        }
        try (InputStream is = getInputStream(mainFile, backupFile, revisionName)) {
            return docbuilder.parse(is);
        } catch (Exception e) {
            return null;
        }
    }

    public static void populate(List<StorageContent> storageContentList, File dir, String pathRoot) {
        String path = pathRoot + dir.getName() + "/";
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {
                populate(storageContentList, f, path);
            } else {
                String filePath = path + f.getName();
                storageContentList.add(toStorageContent(filePath, f));
            }
        }
    }


    public static List<StorageContent> wrap(StorageContent[] array) {
        return new StorageContentList(array);
    }

    public static StorageContent toStorageContent(String path, String text) {
        return new StringStorageContent(path, text);
    }

    public static StorageContent toStorageContent(String path, byte[] array) {
        return new ByteStorageContent(path, array);
    }

    public static StorageContent toStorageContent(String path, File file) {
        return new FileStorageContent(path, file);
    }

    private static InputStream getInputStream(File mainFile, File backupFile, String revisionName) throws IOException, MissingRevisionException {
        if (revisionName.equals(HistoryUnit.CURRENT_REVISION)) {
            if (mainFile.exists()) {
                return new FileInputStream(mainFile);
            } else {
                throw new MissingRevisionException();
            }
        } else if (revisionName.equals(HistoryUnit.PENULTIMATE_REVISION)) {
            if (backupFile.exists()) {
                return new FileInputStream(backupFile);
            } else {
                throw new MissingRevisionException();
            }
        } else {
            if (!StringUtils.isTechnicalName(revisionName, true)) {
                throw new MissingRevisionException();
            }
            File archivesDir = getArchivesDir(backupFile);
            File zipFile = new File(archivesDir, revisionName + ".zip");
            ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFile));
            ZipEntry zipEntry;
            while ((zipEntry = zipInputStream.getNextEntry()) != null) {
                if (zipEntry.getName().equals(revisionName)) {
                    return zipInputStream;
                }
            }
            throw new MissingRevisionException();
        }
    }

    private static File getArchivesDir(File backupFile) {
        return new File(backupFile.getAbsolutePath() + ".archives");
    }

    private static void populateLastRevisions(File backupFile, HistoryUnitBuilder historyUnitBuilder) {
        List<String> lignes = readSessionLines(getSessionFile(backupFile));
        if (lignes == null) {
            return;
        }
        int size = lignes.size();
        EditOrigin current = stringToEditOrigin(lignes.get(size - 1), HistoryUnit.CURRENT_REVISION);
        if (current != null) {
            historyUnitBuilder.addRevision(HistoryUnit.CURRENT_REVISION, current);
        }
        if (size > 2) {
            List<EditOrigin> penultimateList = new ArrayList<EditOrigin>();
            for (int i = (size - 2); i >= 1; i--) {
                EditOrigin penultimate = stringToEditOrigin(lignes.get(i), HistoryUnit.PENULTIMATE_REVISION);
                if (penultimate != null) {
                    penultimateList.add(penultimate);
                }
            }
            if (!penultimateList.isEmpty()) {
                historyUnitBuilder.addRevision(HistoryUnit.PENULTIMATE_REVISION, penultimateList);
            }
        }
    }

    private static List<String> readSessionLines(File sessionFile) {
        if ((sessionFile.exists()) && (sessionFile.isDirectory())) {
            sessionFile.delete();
            return null;
        }
        if (!sessionFile.exists()) {
            return null;
        }
        List<String> currentLines = new ArrayList<String>();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(sessionFile), "UTF-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                currentLines.add(line);
            }
        } catch (IOException ioe) {
        }
        if (currentLines.isEmpty()) {
            return null;
        } else {
            return currentLines;
        }
    }

    private static File getSessionFile(File backupFile) {
        return new File(backupFile.getAbsolutePath() + ".session");
    }

    private static SessionInfo initSessionInfo(File backupFile, EditOrigin editOrigin) {
        File dir = backupFile.getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return new SessionInfo(getSessionFile(backupFile), editOrigin);
    }


    private static String editOriginToString(EditOrigin editOrigin) {
        StringBuilder buf = new StringBuilder();
        String redacteurGlobalId = editOrigin.getRedacteurGlobalId();
        if (redacteurGlobalId != null) {
            buf.append(redacteurGlobalId);
        } else {
            buf.append(NULL);
        }
        buf.append('|');
        buf.append(editOrigin.getISOTime());
        buf.append('|');
        buf.append(editOrigin.getSource());
        return buf.toString();
    }

    private static EditOrigin stringToEditOrigin(String editOriginString, String sessionId) {
        int idx1 = editOriginString.indexOf('|');
        if (idx1 < 0) {
            return null;
        }
        int idx2 = editOriginString.indexOf('|', idx1 + 1);
        if (idx2 < 0) {
            return null;
        }
        String redacteurGlobalId = editOriginString.substring(0, idx1);
        if (redacteurGlobalId.equals(NULL)) {
            redacteurGlobalId = null;
        }
        String isoTime = editOriginString.substring(idx1 + 1, idx2);
        String source = editOriginString.substring(idx2 + 1);
        return EditOriginUtils.newEditOrigin(source, sessionId, redacteurGlobalId, isoTime);
    }


    private static class LogBlock {

        private final String name;
        private final List<String> editOriginStringList = new ArrayList<String>();

        private LogBlock(String name) {
            this.name = name;
        }

        private void addLine(String line) {
            editOriginStringList.add(line);
        }

        private void addRevision(HistoryUnitBuilder historyUnitBuilder) {
            int size = editOriginStringList.size();
            if (size == 0) {
                return;
            }
            List<EditOrigin> editOriginList = new ArrayList<EditOrigin>();
            for (int i = (size - 1); i >= 0; i--) {
                EditOrigin editOrigin = stringToEditOrigin(editOriginStringList.get(i), name);
                if (editOrigin != null) {
                    editOriginList.add(editOrigin);
                }
            }
            if (!editOriginList.isEmpty()) {
                historyUnitBuilder.addRevision(name, editOriginList);
            }
        }


    }


    private static class MissingRevisionException extends Exception {

        private MissingRevisionException() {

        }

    }


    private static class StorageContentList extends AbstractList<StorageContent> implements RandomAccess {

        private final StorageContent[] array;

        private StorageContentList(StorageContent[] array) {
            this.array = array;
        }

        @Override
        public int size() {
            return array.length;
        }

        @Override
        public StorageContent get(int index) {
            return array[index];
        }

    }


    private static class ByteStorageContent implements StorageContent {

        private final String path;
        private final byte[] array;

        private ByteStorageContent(String path, byte[] array) {
            this.path = path;
            this.array = array;
        }

        @Override
        public String getPath() {
            return path;
        }

        @Override
        public InputStream getInputStream() {
            return new ByteArrayInputStream(array);
        }

    }


    private static class StringStorageContent implements StorageContent {

        private final String path;
        private final String text;

        private StringStorageContent(String path, String text) {
            this.path = path;
            this.text = text;
        }

        @Override
        public String getPath() {
            return path;
        }

        @Override
        public InputStream getInputStream() {
            try {
                byte[] array = text.getBytes("UTF-8");
                return new ByteArrayInputStream(array);
            } catch (IOException ioe) {
                throw new ShouldNotOccurException(ioe);
            }
        }

    }


    private static class FileStorageContent implements StorageContent {

        private final String path;
        private final File file;

        private FileStorageContent(String path, File file) {
            this.path = path;
            this.file = file;
        }

        @Override
        public String getPath() {
            return path;
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return new FileInputStream(file);
        }

    }


    private static class SessionInfo {

        private final File sessionFile;
        private final List<String> lignes;
        private final String currentSessionId;
        private final String lastEditOriginString;
        private final String newEditOriginString;
        private final String newSessionId;


        private SessionInfo(File sessionFile, EditOrigin newEditOrigin) {
            this.sessionFile = sessionFile;
            this.lignes = readSessionLines(sessionFile);
            this.newEditOriginString = editOriginToString(newEditOrigin);
            this.newSessionId = getNewSessionId(newEditOrigin);
            this.currentSessionId = getCurrentSessionId();
            this.lastEditOriginString = getLastEditOriginString();
        }

        private String getNewSessionId(EditOrigin newEditOrigin) {
            String sessionId = newEditOrigin.getSessionId();
            if (sessionId == null) {
                return NULL;
            } else {
                return sessionId;
            }
        }

        private String getCurrentSessionId() {
            if (lignes == null) {
                return null;
            } else {
                return lignes.get(0);
            }
        }

        private String getLastEditOriginString() {
            if (lignes != null) {
                int size = lignes.size();
                if (size > 1) {
                    return lignes.get(size - 1);
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }

        private boolean isFirstSession() {
            return (currentSessionId == null);
        }

        private boolean isSameSession() {
            if (currentSessionId == null) {
                return false;
            } else if (currentSessionId.equals(NULL)) {
                if (lastEditOriginString == null) {
                    return false;
                }
                if (newSessionId.equals(NULL)) {
                    return (lastEditOriginString.equals(newEditOriginString));
                } else {
                    return false;
                }
            }
            return currentSessionId.equals(newSessionId);
        }

        private void saveNewSession() throws IOException {
            try (BufferedWriter writer = getWriter(false)) {
                writer.write(newSessionId);
                writer.newLine();
                writer.write(newEditOriginString);
            }
        }

        private void appendEditOrigin() throws IOException {
            if ((lastEditOriginString != null) && (lastEditOriginString.equals(newEditOriginString))) {
                return;
            }
            try (BufferedWriter writer = getWriter(true)) {
                writer.newLine();
                writer.write(newEditOriginString);
            }
        }

        private BufferedWriter getWriter(boolean append) throws IOException {
            return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(sessionFile, append), "UTF-8"));
        }

        private void writePreviousEditOriginList(BufferedWriter writer) throws IOException {
            if (lignes != null) {
                int size = lignes.size();
                for (int i = 1; i < size; i++) {
                    writer.write(lignes.get(i));
                    writer.newLine();
                }
            }
        }

        private void writeNewEditOrigin(BufferedWriter writer) throws IOException {
            if ((lastEditOriginString != null) && (lastEditOriginString.equals(newEditOriginString))) {
                return;
            }
            writer.write(newEditOriginString);
            writer.newLine();
        }

        private void deleteSessionFile() {
            if (sessionFile.exists()) {
                sessionFile.delete();
            }
        }

    }


    private static class ArchivesInfo {

        private final File archivesDir;

        private ArchivesInfo(File archivesDir) {
            if ((archivesDir.exists()) && (!archivesDir.isDirectory())) {
                archivesDir.delete();
            }
            archivesDir.mkdir();

            this.archivesDir = archivesDir;
        }

        private int getNewVersionNumber() {
            File[] currentFiles = archivesDir.listFiles();
            int versionNumber = 1;
            int length = currentFiles.length;
            for (int i = 0; i < length; i++) {
                File f = currentFiles[i];
                String name = f.getName();
                if (name.startsWith("v_") && (name.endsWith(".zip"))) {
                    try {
                        int vn = Integer.parseInt(name.substring(2, name.length() - 4));
                        versionNumber = Math.max(versionNumber, vn + 1);
                    } catch (NumberFormatException nfe) {
                    }
                }
            }
            return versionNumber;
        }

        private void archive(byte[] array, SessionInfo sessionInfo, boolean logCurrentSession) throws IOException {
            String vName = zip(array);
            log(vName, sessionInfo, logCurrentSession);
        }

        private String zip(byte[] array) throws IOException {
            String vName = "v_" + getNewVersionNumber();
            File zipFile = new File(archivesDir, vName + ".zip");
            try (ZipOutputStream os = new ZipOutputStream(new FileOutputStream(zipFile))) {
                os.putNextEntry(new ZipEntry(vName));
                os.write(array, 0, array.length);
            }
            return vName;
        }

        private void log(String vName, SessionInfo sessionInfo, boolean logCurrentSession) throws IOException {
            File versionsLogFile = new File(archivesDir, "versions.log");
            boolean append = false;
            if (versionsLogFile.exists()) {
                if (versionsLogFile.isDirectory()) {
                    versionsLogFile.delete();
                } else {
                    append = true;
                }
            }
            try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(versionsLogFile, append), "UTF-8"))) {
                if (append) {
                    writer.newLine();
                }
                writer.write(vName);
                writer.newLine();
                sessionInfo.writePreviousEditOriginList(writer);
                if (logCurrentSession) {
                    sessionInfo.writeNewEditOrigin(writer);
                }
            }
        }

    }


}
