package tc.oc.commons.bukkit.localization;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder;

import com.google.common.cache.LoadingCache;
import com.google.inject.assistedinject.Assisted;
import org.xml.sax.SAXException;
import tc.oc.commons.bukkit.logging.MapdevLogger;
import tc.oc.minecraft.scheduler.MainThreadExecutor;
import tc.oc.commons.core.localization.Locales;
import tc.oc.commons.core.localization.LocalizedFileManager;
import tc.oc.commons.core.util.CacheUtils;
import tc.oc.file.PathWatcher;
import tc.oc.file.PathWatcherHandle;
import tc.oc.file.PathWatcherService;
import tc.oc.parse.ParseException;
import tc.oc.parse.xml.DocumentParser;

/**
 * Monitors a localized file that can be parsed with a {@link DocumentParser<T>} (which must have a binding)
 *
 * Each localized version of the file is monitored and parsed immediately when changes are detected.
 */
public class LocalizedDocument<T> {

    public interface Factory<T> {
        LocalizedDocument<T> create(@Assisted("source") Path source, @Assisted("localized") Path localized);
    }

    private final Path sourcePath;
    private final Path localizedPath;
    private final PathWatcherService watcherService;
    private final LocalizedFileManager localizedFiles;
    private final DocumentParser<T> parser;
    private final DocumentBuilder builder;
    private final MapdevLogger mapdevLogger;
    private final MainThreadExecutor mainThread;

    private final LoadingCache<Locale, Watcher> watchers = CacheUtils.newCache(Watcher::new);

    @Inject LocalizedDocument(@Assisted("source") Path sourcePath, @Assisted("localized") Path localizedPath, DocumentParser<T> parser, PathWatcherService watcherService, LocalizedFileManager localizedFiles, DocumentBuilder builder, MapdevLogger mapdevLogger, MainThreadExecutor mainThread) {
        this.sourcePath = sourcePath;
        this.localizedPath = localizedPath;
        this.parser = parser;
        this.watcherService = watcherService;
        this.localizedFiles = localizedFiles;
        this.builder = builder;
        this.mapdevLogger = mapdevLogger;
        this.mainThread = mainThread;
    }

    public void disable() {
        watchers.asMap().values()
                .forEach(watcher -> watcher.handle.cancel());
    }

    public Optional<T> getDefault() {
        return Optional.ofNullable(watchers.getUnchecked(Locales.DEFAULT_LOCALE).document);
    }

    public Stream<T> get(Locale locale) {
        return Stream.concat(localizedFiles.match(locale),
                             Stream.of(Locales.DEFAULT_LOCALE))
                     .map(l -> watchers.getUnchecked(l).document)
                     .filter(d -> d != null);
    }

    private class Watcher implements PathWatcher {
        final Locale locale;
        final PathWatcherHandle handle;

        @Nullable T document;

        private Watcher(Locale locale) throws IOException {
            this.locale = locale;
            final Path path = Locales.isDefault(locale)
                              ? sourcePath
                              : localizedFiles.localizedPath(locale, localizedPath);
            this.handle = watcherService.watch(path, mainThread, this);
        }

        @Override
        public void fileCreated(Path path) {
            fileModified(path);
        }

        @Override
        public void fileModified(Path path) {
            try {
                document = parser.parse(builder.parse(path.toFile()));
            } catch(SAXException | IOException | ParseException e) {
                mapdevLogger.log(Level.SEVERE, "Failed to load " + path + " for locale " + locale, e);
            }
        }

        @Override
        public void fileDeleted(Path path) {
            document = null;
        }
    }

}
