package org.infinispan.globalstate.impl;

import static org.infinispan.globalstate.ScopedPersistentState.GLOBAL_SCOPE;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.infinispan.Version;
import org.infinispan.commons.util.Util;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.annotations.Start;
import org.infinispan.factories.annotations.Stop;
import org.infinispan.globalstate.GlobalStateManager;
import org.infinispan.globalstate.GlobalStateProvider;
import org.infinispan.globalstate.ScopedPersistentState;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.util.TimeService;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * GlobalStateManagerImpl. This global component manages persistent state across restarts. The
 * information is stored in a Properties file. On a graceful shutdown it persists the following
 * information:
 *
 * version = full version (e.g. major.minor.micro.qualifier) timestamp = timestamp using ISO-8601
 *
 * as well as any additional information contributed by registered {@link GlobalStateProvider}s
 *
 * @author Tristan Tarrant
 * @since 8.1
 */
public class GlobalStateManagerImpl implements GlobalStateManager {
   public static final String VERSION = "@version";
   public static final String TIMESTAMP = "@timestamp";
   public static final String VERSION_MAJOR = "version-major";
   private static Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass());
   private GlobalConfiguration globalConfiguration;
   private List<GlobalStateProvider> stateProviders = new ArrayList<>();
   private TimeService timeService;

   @Inject
   public void inject(GlobalConfiguration globalConfiguration, TimeService timeService,
         EmbeddedCacheManager cacheManager) {
      this.globalConfiguration = globalConfiguration;
      this.timeService = timeService;
   }

   @Start(priority = 1) // Must start before everything else
   public void start() {
      File stateFile = getStateFile(GLOBAL_SCOPE);
      Optional<ScopedPersistentState> globalState = readScopedState(GLOBAL_SCOPE);
      if (globalState.isPresent()) {
         ScopedPersistentState state = globalState.get();
         // We proceed only if we can write to the file
         if (!stateFile.canWrite()) {
            throw log.nonWritableStateFile(stateFile);
         }
         // Validate the state before proceeding
         log.globalStateLoad(state.getProperty(VERSION), state.getProperty(TIMESTAMP));

         stateProviders.forEach(provider -> provider.prepareForRestore(state));
      } else {
         // Clean slate. Try to create an empty state file before proceeding
         try {
            stateFile.getParentFile().mkdirs();
            stateFile.createNewFile();
         } catch (IOException e) {
            throw log.nonWritableStateFile(stateFile);
         }
      }
   }

   @Stop(priority = 1)
   public void stop() {
      writeGlobalState();
   }

   @Override
   public void writeGlobalState() {
      ScopedPersistentState state = new ScopedPersistentStateImpl(GLOBAL_SCOPE);
      state.setProperty(VERSION, Version.getVersion());
      state.setProperty(VERSION_MAJOR, Version.getMajor());
      state.setProperty(TIMESTAMP, timeService.instant().toString());
      // ask any state providers to contribute to the global state
      stateProviders.forEach(provider -> provider.prepareForPersist(state));
      writeScopedState(state);
      log.globalStateWrite(state.getProperty(VERSION), state.getProperty(TIMESTAMP));
   }

   @Override
   public void writeScopedState(ScopedPersistentState state) {
      File stateFile = getStateFile(state.getScope());
      try (PrintWriter w = new PrintWriter(stateFile)) {
         state.forEach((key, value) -> {
            w.printf("%s=%s%n", Util.unicodeEscapeString(key), Util.unicodeEscapeString(value));
         });
      } catch (IOException e) {
         throw log.failedWritingGlobalState(e, stateFile);
      }
   }

   @Override
   public Optional<ScopedPersistentState> readScopedState(String scope) {
      File stateFile = getStateFile(scope);
      if (!stateFile.exists())
         return Optional.empty();
      try (BufferedReader r = new BufferedReader(new FileReader(stateFile))) {
         ScopedPersistentState state = new ScopedPersistentStateImpl(scope);
         for (String line = r.readLine(); line != null; line = r.readLine()) {
            if (!line.startsWith("#")) { // Skip comment lines
               int eq = line.indexOf('=');
               while (eq > 0 && line.charAt(eq-1) == '\\') {
                  eq = line.indexOf('=', eq + 1);
               }
               if (eq > 0) {
                  state.setProperty(Util.unicodeUnescapeString(line.substring(0, eq).trim()),
                        Util.unicodeUnescapeString(line.substring(eq + 1).trim()));
               }
            }
         }
         return Optional.of(state);
      } catch (IOException e) {
         throw log.failedReadingPersistentState(e, stateFile);
      }
   }

   private File getStateFile(String scope) {
      return new File(globalConfiguration.globalState().persistentLocation(), scope + ".state");
   }

   @Override
   public void registerStateProvider(GlobalStateProvider provider) {
      this.stateProviders.add(provider);
   }
}
