"""

RenderPipeline

Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

"""

import os
import atexit

from panda3d.core import Filename, VirtualFileSystem, get_model_path
from panda3d.core import VirtualFileMountRamdisk
from direct.stdpy.file import join, isdir, isfile

from rpcore.rpobject import RPObject


class MountManager(RPObject):

    """ This classes mounts the required directories for the pipeline to run.
    This is important if the pipeline is in a subdirectory for example. The
    mount manager also handles the lock, storing the current PID into a file
    named instance.pid and ensuring that there is only 1 instance of the
    pipeline running at one time. """

    def __init__(self, pipeline):
        """ Creates a new mount manager """
        RPObject.__init__(self)
        self._pipeline = pipeline
        self._base_path = self._find_basepath()
        self._lock_file = "instance.pid"
        self._model_paths = []
        self._write_path = None
        self._mounted = False
        self._do_cleanup = True
        self._config_dir = None

        self.debug("Auto-Detected base path to", self._base_path)
        atexit.register(self._on_exit_cleanup)

    @property
    def write_path(self):
        """ Returns the write path previously set with set_write_path, or None
        if no write path has been set yet. """
        return self._write_path

    @write_path.setter
    def write_path(self, pth):
        """ Set a writable directory for generated files. This can be a string
        path name or a multifile with openReadWrite(). If no pathname is set
        then the root directory is used.

        This feature is usually only used for debugging, the pipeline will dump
        all generated shaders and other temporary files to that directory.
        If you don't need this, you can use set_virtual_write_path(), which
        will create the temporary path in the VirtualFileSystem, thus not
        writing any files to disk. """
        if pth is None:
            self._write_path = None
            self._lock_file = "instance.pid"
        else:
            self._write_path = Filename.from_os_specific(pth).get_fullpath()
            self._lock_file = join(self._write_path, "instance.pid")

    @property
    def base_path(self):
        """ Returns the base path of the pipeline. This returns the path previously
        set with set_base_path, or the auto detected base path if no path was
        set yet """
        return self._base_path

    @base_path.setter
    def base_path(self, pth):
        """ Sets the path where the base shaders and models on are contained. This
        is usually the root of the rendering pipeline folder """
        self.debug("Set base path to '" + pth + "'")
        self._base_path = Filename.from_os_specific(pth).get_fullpath()

    @property
    def config_dir(self):
        """ Returns the config directory previously set with set_config_dir, or
        None if no directory was set yet """

    @config_dir.setter
    def config_dir(self, pth):
        """ Sets the path to the config directory. Usually this is the config/
        directory located in the pipeline root directory. However, if you want
        to load your own configuration files, you can specify a custom config
        directory here. Your configuration directory should contain the
        pipeline.yaml, plugins.yaml, daytime.yaml and configuration.prc.

        It is highly recommended you use the pipeline provided config files, modify
        them to your needs, and as soon as you think they are in a final version,
        copy them over. Please also notice that you should keep your config files
        up-to-date, e.g. when new configuration variables are added.

        Also, specifying a custom configuration_dir disables the functionality
        of the PluginConfigurator and DayTime editor, since they operate on the
        pipelines default config files.

        Set the directory to None to use the default directory. """
        self._config_dir = Filename.from_os_specific(pth).get_fullpath()

    @property
    def do_cleanup(self):
        """ Returns whether the mount manager will attempt to cleanup the
        generated files after the application stopped running """
        return self._do_cleanup

    @do_cleanup.setter
    def do_cleanup(self, cleanup):
        """ Sets whether to cleanup the tempfolder after the application stopped.
        This is mostly useful for debugging, to analyze the generated tempfiles
        even after the pipeline stopped running """
        self._do_cleanup = cleanup

    def get_lock(self):
        """ Checks if we are the only instance running. If there is no instance
        running, write the current PID to the instance.pid file. If the
        instance file exists, checks if the specified process still runs. This
        way only 1 instance of the pipeline can be run at one time. """

        # Check if there is a lockfile at all
        if isfile(self._lock_file):
            # Read process id from lockfile
            try:
                with open(self._lock_file, "r") as handle:
                    pid = int(handle.readline())
            except IOError as msg:
                self.error("Failed to read lockfile:", msg)
                return False

            # Check if the process is still running
            if self._is_pid_running(pid):
                self.error("Found running instance")
                return False

            # Process is not running anymore, we can write the lockfile
            self._write_lock()
            return True

        else:
            # When there is no lockfile, just create it and continue
            self._write_lock()
            return True

    def _find_basepath(self):
        """ Attempts to find the pipeline base path by looking at the location
        of this file """
        pth = os.path.abspath(join(os.path.dirname(os.path.realpath(__file__)), ".."))
        return Filename.from_os_specific(pth).get_fullpath()

    def _is_pid_running(self, pid):
        """ Checks if a pid is still running """

        # Code snippet from:
        # http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid

        if os.name == 'posix':
            import errno
            if pid < 0:
                return False
            try:
                os.kill(pid, 0)
            except OSError as err:
                return err.errno == errno.EPERM
            else:
                return True
        else:
            import ctypes
            kernel32 = ctypes.windll.kernel32
            process = kernel32.OpenProcess(0x100000, 0, pid)
            if process != 0:
                kernel32.CloseHandle(process)
                return True
            else:
                return False

    def _write_lock(self):
        """ Internal method to write the current process id to the instance.pid
        lockfile. This is used to ensure no second instance of the pipeline is
        running. """

        with open(self._lock_file, "w") as handle:
            handle.write(str(os.getpid()))

    def _try_remove(self, fname):
        """ Tries to remove the specified filename, returns either True or False
        depending if we had success or not """
        try:
            os.remove(fname)
            return True
        except (IOError, OSError):
            pass
        return False

    def _on_exit_cleanup(self):
        """ Gets called when the application exists """

        if self._do_cleanup:
            self.debug("Cleaning up ..")

            if self._write_path is not None:

                # Try removing the lockfile
                self._try_remove(self._lock_file)

                # Check for further tempfiles in the write path
                # We explicitely use os.listdir here instead of pandas listdir,
                # to work with actual paths
                for fname in os.listdir(self._write_path):
                    pth = join(self._write_path, fname)

                    # Tempfiles from the pipeline start with "$$" to distinguish
                    # them from user created files
                    if isfile(pth) and fname.startswith("$$"):
                        self._try_remove(pth)

                # Delete the write path if no files are left
                if len(os.listdir(self._write_path)) < 1:
                    try:
                        os.removedirs(self._write_path)
                    except IOError:
                        pass

    @property
    def is_mounted(self):
        """ Returns whether the MountManager was already mounted by calling
        mount() """
        return self._mounted

    def mount(self):
        """ Inits the VFS Mounts. This creates the following virtual directory
        structure, from which all files can be located:

        /$$rp/  (Mounted from the render pipeline base directory)
           + rpcore/
           + shader/
           + ...

        /$rpconfig/ (Mounted from config/, may be set by user)
           + pipeline.yaml
           + ...

        /$$rptemp/ (Either ramdisk or user specified)
            + day_time_config
            + shader_auto_config
            + ...

        /$$rpshader/ (Link to /$$rp/rpcore/shader)

         """
        self.debug("Setting up virtual filesystem")
        self._mounted = True

        def convert_path(pth):
            return Filename.from_os_specific(pth).get_fullpath()
        vfs = VirtualFileSystem.get_global_ptr()

        # Mount config dir as $$rpconf
        if self._config_dir is None:
            config_dir = convert_path(join(self._base_path, "config/"))
            self.debug("Mounting auto-detected config dir:", config_dir)
            vfs.mount(config_dir, "/$$rpconfig", 0)
        else:
            self.debug("Mounting custom config dir:", self._config_dir)
            vfs.mount(convert_path(self._config_dir), "/$$rpconfig", 0)

        # Mount directory structure
        vfs.mount(convert_path(self._base_path), "/$$rp", 0)
        vfs.mount(convert_path(join(self._base_path, "rpcore/shader")), "/$$rp/shader", 0)
        vfs.mount(convert_path(join(self._base_path, "effects")), "effects", 0)

        # Mount the pipeline temp path:
        # If no write path is specified, use a virtual ramdisk
        if self._write_path is None:
            self.debug("Mounting ramdisk as /$$rptemp")
            vfs.mount(VirtualFileMountRamdisk(), "/$$rptemp", 0)
        else:
            # In case an actual write path is specified:
            # Ensure the pipeline write path exists, and if not, create it
            if not isdir(self._write_path):
                self.debug("Creating temporary path, since it does not exist yet")
                try:
                    os.makedirs(self._write_path)
                except IOError as msg:
                    self.fatal("Failed to create temporary path:", msg)
            self.debug("Mounting", self._write_path, "as /$$rptemp")
            vfs.mount(convert_path(self._write_path), '/$$rptemp', 0)

        get_model_path().prepend_directory("/$$rp")
        get_model_path().prepend_directory("/$$rp/shader")
        get_model_path().prepend_directory("/$$rptemp")

    def unmount(self):
        """ Unmounts the VFS """
        raise NotImplementedError("TODO")
