/*
 * Copyright 2014 MovingBlocks
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.terasology.engine.paths;

import com.google.common.collect.ImmutableList;
import com.sun.jna.platform.win32.KnownFolders;
import com.sun.jna.platform.win32.Shell32Util;

import org.lwjgl.LWJGLUtil;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import javax.swing.JFileChooser;

/**
 * Manager class that keeps track of the game's various paths and save directories.
 */
public final class PathManager {
    public static final String TERASOLOGY_FOLDER_NAME = "Terasology";
    public static final Path LINUX_HOME_SUBPATH = Paths.get(".local", "share", "terasology");

    private static final String SAVED_GAMES_DIR = "saves";
    private static final String LOG_DIR = "logs";
    private static final String SHADER_LOG_DIR = "shaders";
    private static final String MOD_DIR = "modules";
    private static final String SCREENSHOT_DIR = "screenshots";
    private static final String NATIVES_DIR = "natives";

    private static PathManager instance;
    private Path installPath;
    private Path homePath;
    private Path savesPath;
    private Path logPath;
    private Path shaderLogPath;
    private Path currentWorldPath;

    private ImmutableList<Path> modPaths = ImmutableList.of();
    private Path screenshotPath;
    private Path nativesPath;

    private PathManager() {
        // By default, the path should be the code location (where terasology.jar is)
        try {
            URL urlToSource = PathManager.class.getProtectionDomain().getCodeSource().getLocation();

            Path codeLocation = Paths.get(urlToSource.toURI());
            System.out.println("codeLocation: " + codeLocation);
            if (Files.isRegularFile(codeLocation)) {
                installPath = findNativesHome(codeLocation.getParent(), 5);
                if (installPath == null) {
                    System.out.println("Failed to find the natives dir - unable to launch!");
                    throw new RuntimeException("Failed to find natives from .jar launch");
                }
            }
        } catch (URISyntaxException e) {
            // Can't use logger, because logger not set up when PathManager is used.
            System.out.println("Failed to convert code location to uri");
        }
        // We might be running from an IDE which can cause the installPath to be null. Try current working directory.
        if (installPath == null) {
            installPath = Paths.get("").toAbsolutePath();
            System.out.println("installPath was null, running from IDE or headless server? Setting to: " + installPath);
            installPath = findNativesHome(installPath, 5);
            if (installPath == null) {
                System.out.println("Failed to find the natives dir - unable to launch!");
                throw new RuntimeException("Failed to find natives from likely IDE launch");
            }
        }
        homePath = installPath;
    }

    /**
     * Searches for a parent directory containing the natives directory
     *
     * @param startPath path to start from
     * @param maxDepth  max directory levels to search
     * @return the adjusted path containing the natives directory or null if not found
     */
    private Path findNativesHome(Path startPath, int maxDepth) {
        int levelsToSearch = maxDepth;
        Path checkedPath = startPath;
        while (levelsToSearch > 0) {
            File dirToTest = new File(checkedPath.toFile(), NATIVES_DIR);
            if (dirToTest.exists()) {
                System.out.println("Found the natives dir: " + dirToTest);
                return checkedPath;
            }

            checkedPath = checkedPath.getParent();
            if (checkedPath.equals(startPath.getRoot())) {
                System.out.println("Uh oh, reached the root path, giving up");
                return null;
            }
            levelsToSearch--;
        }

        System.out.println("Failed to find the natives dir within " + maxDepth + " levels of " + startPath);
        return null;
    }

    /**
     *
     * @return An instance of the path manager for this execution.
     */
    public static PathManager getInstance() {
        if (instance == null) {
            instance = new PathManager();
        }
        return instance;
    }

    /**
     * Uses the given path as the home instead of the default home path.
     * @param rootPath Path to use as the home path.
     * @throws IOException Thrown when required directories cannot be accessed.
     */
    public void useOverrideHomePath(Path rootPath) throws IOException {
        this.homePath = rootPath;
        updateDirs();
    }

    /**
     * Uses a platform-specific default home path for this execution.
     * @throws IOException Thrown when required directories cannot be accessed.
     */
    public void useDefaultHomePath() throws IOException {
        switch (LWJGLUtil.getPlatform()) {
            case LWJGLUtil.PLATFORM_LINUX:
                homePath = Paths.get(System.getProperty("user.home")).resolve(LINUX_HOME_SUBPATH);
                break;
            case LWJGLUtil.PLATFORM_MACOSX:
                homePath = Paths.get(System.getProperty("user.home"), "Library", "Application Support", TERASOLOGY_FOLDER_NAME);
                break;
            case LWJGLUtil.PLATFORM_WINDOWS:
                String savedGamesPath = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_SavedGames);
                if (savedGamesPath == null) {
                    savedGamesPath = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_Documents);
                }
                Path rawPath;
                if (savedGamesPath != null) {
                    rawPath = Paths.get(savedGamesPath);
                } else {
                    rawPath = new JFileChooser().getFileSystemView().getDefaultDirectory().toPath();
                }
                homePath = rawPath.resolve(TERASOLOGY_FOLDER_NAME);
                break;
            default:
                homePath = Paths.get(System.getProperty("user.home")).resolve(LINUX_HOME_SUBPATH);
                break;
        }
        updateDirs();
    }

    /**
     *
     * @return This execution's home path.
     */
    public Path getHomePath() {
        return homePath;
    }

    /**
     *
     * @return The path of the running installation.
     */
    public Path getInstallPath() {
        return installPath;
    }

    /**
     *
     * @return Path in which world saves are saved.
     */
    public Path getSavesPath() {
        return savesPath;
    }

    /**
     *
     * @return Path in which this execution's logs are saved.
     */
    public Path getLogPath() {
        return logPath;
    }

    /**
     *
     * @return Path in which this execution's shader logs are saved.
     */
    public Path getShaderLogPath() {
        return shaderLogPath;
    }

    /**
     *
     * @return List of paths to all of the modules.
     */
    public List<Path> getModulePaths() {
        return modPaths;
    }

    /**
     *
     * @return Path in which this execution's screen-shots are saved.
     */
    public Path getScreenshotPath() {
        return screenshotPath;
    }

    /**
     *
     * @return Path in which the game's native libraries are saved.
     */
    public Path getNativesPath() {
        return nativesPath;
    }

    /**
     * Updates all of the path manager's file/directory references to match the path settings. Creates directories if they don't already exist.
     * @throws IOException Thrown when required directories cannot be accessed.
     */
    private void updateDirs() throws IOException {
        Files.createDirectories(homePath);
        savesPath = homePath.resolve(SAVED_GAMES_DIR);
        Files.createDirectories(savesPath);
        logPath = homePath.resolve(LOG_DIR);
        Files.createDirectories(logPath);
        shaderLogPath = logPath.resolve(SHADER_LOG_DIR);
        Files.createDirectories(shaderLogPath);
        Path homeModPath = homePath.resolve(MOD_DIR);
        Files.createDirectories(homeModPath);
        Path installModPath = installPath.resolve(MOD_DIR);
        Files.createDirectories(installModPath);
        if (Files.isSameFile(homeModPath, installModPath)) {
            modPaths = ImmutableList.of(homeModPath);
        } else {
            modPaths = ImmutableList.of(installModPath, homeModPath);
        }
        screenshotPath = homePath.resolve(SCREENSHOT_DIR);
        Files.createDirectories(screenshotPath);
        nativesPath = installPath.resolve(NATIVES_DIR);
        if (currentWorldPath == null) {
            currentWorldPath = homePath;
        }
    }

    public Path getHomeModPath() {
        return modPaths.get(0);
    }

    public Path getSavePath(String title) {
        return savesPath.resolve(title.replaceAll("[^A-Za-z0-9-_ ]", ""));
    }
}
