package prefs;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.egit.github.core.RepositoryId;
import util.FileHelper;
import util.HTLog;
import util.JsonHelper;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Represents persistent user configuration.
 *
 * The Preferences class is the facade of all configuration and preferences information. All preferences and config
 * settings can be set in this class.
 *
 * Use either Preferences.create() or Preferences.load() to obtain a Preferences instance.
 *
 * Overrides PMD's recommendation that this class should be final.
 * It cannot be as we need to mock it.
 * 
 */
public class Preferences { // NOPMD

    private static final Logger logger = LogManager.getLogger(Preferences.class.getName());

    public static final String DIRECTORY = "settings";

    public static final String SESSION_CONFIG_FILENAME = "global.json";
    public static final String USER_CONFIG_FILENAME = "user.json";

    private static final String DEFAULT_FILE_CONTENTS = "{}";

    private final String configDirectory;
    private final String sessionConfigFileName;
    private final String userConfigFileName;

    private final SessionConfig sessionConfig;
    private final UserConfig userConfig;

    /**
     * Initialises a Preferences instance that contains the configurations in the config files specified.
     * @param configDirectory The directory that the config files are held in.
     * @param sessionConfigFileName The name of the session config file to load.
     * @param userConfigFileName The name of the user config file to load.
     * @return The initialised Preferences instance.
     */
    public static Preferences load(String configDirectory, String sessionConfigFileName, String userConfigFileName) {
        return new Preferences(configDirectory, sessionConfigFileName, userConfigFileName, false);
    }

    /**
     * Initialises a Preferences instance that creates new config files.
     * @param configDirectory The directory that the config files are held in.
     * @param sessionConfigFileName The name of the session config file to create.
     * @param userConfigFileName The name of the user config file to create.
     * @return The initialised Preferences instance.
     */
    public static Preferences create(String configDirectory, String sessionConfigFileName, String userConfigFileName) {
        return new Preferences(configDirectory, sessionConfigFileName, userConfigFileName, true);
    }

    /**
     * @param configDirectory The directory that the config files are held in.
     * @param sessionConfigFileName The name of the session config file
     * @param userConfigFileName The name of the user config file
     * @param isIgnoreExisting True if existing config file is to be ignored.
     */
    private Preferences(String configDirectory,
                        String sessionConfigFileName,
                        String userConfigFileName,
                        boolean isIgnoreExisting) {
        this.configDirectory = configDirectory;
        this.sessionConfigFileName = sessionConfigFileName;
        this.userConfigFileName = userConfigFileName;

        if (isIgnoreExisting) {
            this.sessionConfig = createConfig(DEFAULT_FILE_CONTENTS, SessionConfig.class);
            this.userConfig = createConfig(DEFAULT_FILE_CONTENTS, UserConfig.class);
        } else {
            this.sessionConfig = loadConfig(configDirectory, sessionConfigFileName, SessionConfig.class);
            this.userConfig = loadConfig(configDirectory, userConfigFileName, UserConfig.class);
        }
    }

    /**
     * Creates a new Config instance
     * @param contents The JSON representation of an contents of the Config object
     * @param configClass The class of the Config instance
     * @return The created Config instance
     */
    private <T> T createConfig(String contents, Class<T> configClass) {
        return JsonHelper.fromJsonString(contents, configClass);
    }

    /**
     * Loads a Config instance from a file on disk
     * @param configDirectory The directory that the config file is held in.
     * @param configFileName The filename of the config file on disk
     * @param configClass The class of the config to load
     * @return The loaded Config instance
     */
    private <T> T loadConfig(String configDirectory, String configFileName, Class<T> configClass) {
        String fileContents = DEFAULT_FILE_CONTENTS;
        if (FileHelper.isFileExists(configDirectory, configFileName)) {
            try {
                fileContents = FileHelper.getFileContents(configDirectory, configFileName);
            } catch (IOException e) {
                // if we can't read the file, just ignore the existing files
                HTLog.error(logger, e);
                logger.error(e.toString());
            }
        }

        return createConfig(fileContents, configClass);
    }

    /**
     * Saves the session and user configs to file
     */
    private void save() {
        try {
            saveConfig(sessionConfig, configDirectory, sessionConfigFileName, SessionConfig.class);

        } catch (IOException e) {
            HTLog.error(logger, e);
            logger.error("Could not save session config");
        }

        try {
            saveConfig(userConfig, configDirectory, userConfigFileName, UserConfig.class);

        } catch (IOException e) {
            HTLog.error(logger, e);
            logger.error("Could not save user config");
        }
    }

    /**
     * Saves a Config to file
     * @param config The config object to save.
     * @param configDirectory The directory that the config file is held in.
     * @param configFileName The file name of the config file on disk
     * @param configClass The class of the config object
     */
    private <T> void saveConfig(T config, String configDirectory, String configFileName, Class configClass)
            throws IOException {
        String jsonString = JsonHelper.toJsonString(config, configClass);
        FileHelper.writeFileContents(configDirectory, configFileName, jsonString);
    }

    public String getLastLoginPassword() {
        return sessionConfig.getLastLoginPassword();
    }

    public String getLastLoginUsername() {
        return sessionConfig.getLastLoginUsername();
    }

    public void setLastLoginCredentials(String lastLoginUsername, String lastLoginPassword) {
        sessionConfig.setLastLoginCredentials(lastLoginUsername, lastLoginPassword);
        save();
    }

    public List<PanelInfo> getPanelInfo() {
        return sessionConfig.getPanelInfo();
    }

    public void setPanelInfo(List<PanelInfo> panelInfo) {
        sessionConfig.setPanelInfo(panelInfo);
        save();
    }

    public void addBoard(String boardName, List<PanelInfo> panelsInBoard) {
        assert boardName != null && panelsInBoard != null;
        sessionConfig.addBoard(boardName, panelsInBoard);
        save();
    }

    public Map<String, List<PanelInfo>> getAllBoards() {
        return sessionConfig.getAllBoards();
    }

    public List<String> getAllBoardNames() {
        return new ArrayList<>(getAllBoards().keySet());
    }

    public void removeBoard(String boardName) {
        sessionConfig.removeBoard(boardName);
        save();
    }

    public void setLastOpenBoard(String boardName) {
        sessionConfig.setLastOpenBoard(boardName);
        save();
    }

    public Optional<String> getLastOpenBoard() {
        return sessionConfig.getLastOpenBoard();
    }

    public void setLastOpenBoardPanelInfos(List<PanelInfo> panelInfos) {
        sessionConfig.setLastOpenBoardPanelInfos(panelInfos);
    }

    public Optional<List<PanelInfo>> getLastOpenBoardPanelInfos() {
        return sessionConfig.getLastOpenBoardPanelInfos();
    }

    /**
     * Finds the next board in the list of boards. Circles through the boards one at a time.
     * If {@code lastOpenBoard} exists, it will look for the board next to it.
     * Otherwise it will just return the first board on the list.
     *
     * @return name of the next board
     */
    public Optional<String> getNextBoardName() {
        List<String> boardNames = getAllBoardNames();

        if (boardNames.isEmpty()) {
            return Optional.empty();
        }

        int currentIndex = getLastOpenBoard().isPresent() ? boardNames.indexOf(getLastOpenBoard().get()) : -1;
        int nextIndex = (currentIndex + 1) % boardNames.size();

        return Optional.of(boardNames.get(nextIndex));
    }

    public void clearLastOpenBoard() {
        sessionConfig.clearLastOpenBoard();
        save();
    }

    public void clearLastOpenBoardPanelInfos() {
        sessionConfig.clearLastOpenBoardPanelInfos();
        save();
    }

    public List<PanelInfo> getBoardPanels(String boardName) {
        return sessionConfig.getBoardPanels(boardName);
    }

    public void clearAllBoards() {
        sessionConfig.clearAllBoards();
        save();
    }

    public void setLastViewedRepository(String repositoryName) {
        sessionConfig.setLastViewedRepository(repositoryName);
        save();
    }

    public Optional<RepositoryId> getLastViewedRepository() {
        if (sessionConfig.getLastViewedRepository().isEmpty()) {
            return Optional.empty();
        }
        RepositoryId repositoryId = RepositoryId.createFromId(sessionConfig.getLastViewedRepository());
        if (repositoryId == null) {
            return Optional.empty();
        }
        return Optional.of(repositoryId);
    }

    /**
     * Clears marked read at of an issue at the specified repo
     * @param repoId The repo that this issue resides in
     * @param issue The issue to clear
     */
    public void clearMarkedReadAt(String repoId, int issue) {
        sessionConfig.clearMarkedReadAt(repoId, issue);
        save();
    }

    /**
     * Sets the marked read at of an issue in a repo, to a certain time
     * @param repoId The repo of the issue
     * @param issue The issue to set
     * @param timeReadAt The time it was marked read
     */
    public void setMarkedReadAt(String repoId, int issue, LocalDateTime timeReadAt) {
        sessionConfig.setMarkedReadAt(repoId, issue, timeReadAt);
        save();
    }

    /**
     * Retrieves the marked read at of a specified issue in a repo
     * @param repoId The repo of the issue
     * @param issue The issue to retrieve the marked read at
     * @return An Optional of the marked read at
     */
    public Optional<LocalDateTime> getMarkedReadAt(String repoId, int issue) {
        return sessionConfig.getMarkedReadAt(repoId, issue);
    }
}