"""
Show how to use libRocket in Panda3D.
"""
import sys
from panda3d.core import loadPrcFile, loadPrcFileData, Point3,Vec4, Mat4, LoaderOptions  # @UnusedImport
from panda3d.core import DirectionalLight, AmbientLight, PointLight
from panda3d.core import Texture, PNMImage
from panda3d.core import PandaSystem
import random
from direct.interval.LerpInterval import LerpHprInterval, LerpPosInterval, LerpFunc
from direct.showbase.ShowBase import ShowBase

# workaround: https://www.panda3d.org/forums/viewtopic.php?t=10062&p=99697#p99054
#from panda3d import rocket
import _rocketcore as rocket

from panda3d.rocket import RocketRegion, RocketInputHandler

loadPrcFileData("", "model-path $MAIN_DIR/assets")

import console

global globalClock

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        self.win.setClearColor(Vec4(0.2, 0.2, 0.2, 1))

        self.disableMouse()

        self.render.setShaderAuto()

        dlight = DirectionalLight('dlight')
        alight = AmbientLight('alight')
        dlnp = self.render.attachNewNode(dlight)
        alnp = self.render.attachNewNode(alight)
        dlight.setColor((0.8, 0.8, 0.5, 1))
        alight.setColor((0.2, 0.2, 0.2, 1))
        dlnp.setHpr(0, -60, 0)
        self.render.setLight(dlnp)
        self.render.setLight(alnp)

        # Put lighting on the main scene
        plight = PointLight('plight')
        plnp = self.render.attachNewNode(plight)
        plnp.setPos(0, 0, 10)
        self.render.setLight(plnp)
        self.render.setLight(alnp)

        self.loadRocketFonts()

        self.loadingTask = None

        #self.startModelLoadingAsync()
        self.startModelLoading()

        self.inputHandler = RocketInputHandler()
        self.mouseWatcher.attachNewNode(self.inputHandler)

        self.openLoadingDialog()

    def loadRocketFonts(self):
        """ Load fonts referenced from e.g. 'font-family' RCSS directives.

        Note: the name of the font as used in 'font-family'
        is not always the same as the filename;
        open the font in your OS to see its display name.
        """
        rocket.LoadFontFace("modenine.ttf")


    def startModelLoading(self):
        self.monitorNP = None
        self.keyboardNP = None
        self.loadingError = False

        self.taskMgr.doMethodLater(1, self.loadModels, 'loadModels')

    def loadModels(self, task):
        self.monitorNP = self.loader.loadModel("monitor")
        self.keyboardNP = self.loader.loadModel("takeyga_kb")

    def startModelLoadingAsync(self):
        """
        NOTE: this seems to invoke a few bugs (crashes, sporadic model
        reading errors, etc) so is disabled for now...
        """
        self.monitorNP = None
        self.keyboardNP = None
        self.loadingError = False

        # force the "loading" to take some time after the first run...
        options = LoaderOptions()
        options.setFlags(options.getFlags() | LoaderOptions.LFNoCache)

        def gotMonitorModel(model):
            if not model:
                self.loadingError = True
            self.monitorNP = model

        self.loader.loadModel("monitor", loaderOptions=options, callback=gotMonitorModel)

        def gotKeyboardModel(model):
            if not model:
                self.loadingError = True
            self.keyboardNP = model

        self.loader.loadModel("takeyga_kb", loaderOptions=options, callback=gotKeyboardModel)

    def openLoadingDialog(self):
        self.userConfirmed = False

        self.windowRocketRegion = RocketRegion.make('pandaRocket', self.win)
        self.windowRocketRegion.setActive(1)

        self.windowRocketRegion.setInputHandler(self.inputHandler)

        self.windowContext = self.windowRocketRegion.getContext()

        self.loadingDocument = self.windowContext.LoadDocument("loading.rml")
        if not self.loadingDocument:
            raise AssertionError("did not find loading.rml")

        self.loadingDots = 0
        el = self.loadingDocument.GetElementById('loadingLabel')
        self.loadingText = el.first_child
        self.stopLoadingTime = globalClock.getFrameTime() + 3
        self.loadingTask = self.taskMgr.add(self.cycleLoading, 'doc changer')


        # note: you may encounter errors like 'KeyError: 'document'"
        # when invoking events using methods from your own scripts with this
        # obvious code:
        #
        # self.loadingDocument.AddEventListener('aboutToClose',
        #                                       self.onLoadingDialogDismissed, True)
        #
        # A workaround is to define callback methods in standalone Python
        # files with event, self, and document defined to None.
        #
        # see https://www.panda3d.org/forums/viewtopic.php?f=4&t=16412
        #

        # Or, use this indirection technique to work around the problem,
        # by publishing the app into the context, then accessing it through
        # the document's context...

        self.windowContext.app = self
        self.loadingDocument.AddEventListener('aboutToClose',
                                              'document.context.app.handleAboutToClose()', True)

        self.loadingDocument.Show()

    def handleAboutToClose(self):
        self.userConfirmed = True
        if self.monitorNP and self.keyboardNP:
            self.onLoadingDialogDismissed()

    def attachCustomRocketEvent(self, document, rocketEventName, pandaHandler, once=False):
        # handle custom event

        # note: you may encounter errors like 'KeyError: 'document'"
        # when invoking events using methods from your own scripts with this
        # obvious code:
        #
        # self.loadingDocument.AddEventListener('aboutToClose',
        #                                       self.onLoadingDialogDismissed, True)
        #
        # see https://www.panda3d.org/forums/viewtopic.php?f=4&t=16412


        # this technique converts Rocket events to Panda3D events

        pandaEvent = 'panda.' + rocketEventName

        document.AddEventListener(
            rocketEventName,
            "messenger.send('" + pandaEvent + "', [event])")

        if once:
            self.acceptOnce(pandaEvent, pandaHandler)
        else:
            self.accept(pandaEvent, pandaHandler)


    def cycleLoading(self, task):
        """
        Update the "loading" text in the initial window until
        the user presses Space, Enter, or Escape or clicks (see loading.rxml)
        or sufficient time has elapsed (self.stopLoadingTime).
        """
        text = self.loadingText

        now = globalClock.getFrameTime()
        if self.monitorNP and self.keyboardNP:
            text.text = "Ready"
            if now > self.stopLoadingTime or self.userConfirmed:
                self.onLoadingDialogDismissed()
                return task.done
        elif self.loadingError:
            text.text = "Assets not found"
        else:
            count = 5
            intv = int(now * 4) % count  # @UndefinedVariable
            text.text = "Loading" + ("." * (1+intv)) + (" " * (2 - intv))

        return task.cont

    def onLoadingDialogDismissed(self):
        """ Once a models are loaded, stop 'loading' and proceed to 'start' """
        if self.loadingDocument:
            if self.loadingTask:
                self.taskMgr.remove(self.loadingTask)
            self.loadingTask = None

            self.showStarting()

    def fadeOut(self, element, time):
        """ Example updating RCSS attributes from code
        by modifying the 'color' RCSS attribute to slowly
        change from solid to transparent.

        element: the Rocket element whose style to modify
        time: time in seconds for fadeout
        """

        # get the current color from RCSS effective style
        color = element.style.color
        # convert to RGBA form
        prefix = color[:color.rindex(',')+1].replace('rgb(', 'rgba(')

        def updateAlpha(t):
            # another way of setting style on a specific element
            attr = 'color: ' + prefix + str(int(t)) +');'
            element.SetAttribute('style', attr)

        alphaInterval = LerpFunc(updateAlpha,
                             duration=time,
                             fromData=255,
                             toData=0,
                             blendType='easeIn')

        return alphaInterval

    def showStarting(self):
        """ Models are loaded, so update the dialog,
        fade out, then transition to the console. """
        self.loadingText.text = 'Starting...'

        alphaInterval = self.fadeOut(self.loadingText, 0.5)
        alphaInterval.setDoneEvent('fadeOutFinished')

        def fadeOutFinished():
            if self.loadingDocument:
                self.loadingDocument.Close()
                self.loadingDocument = None
                self.createConsole()

        self.accept('fadeOutFinished', fadeOutFinished)

        alphaInterval.start()

    def createConsole(self):
        """ Create the in-world console, which displays
        a RocketRegion in a GraphicsBuffer, which appears
        in a Texture on the monitor model. """

        self.monitorNP.reparentTo(self.render)
        self.monitorNP.setScale(1.5)

        self.keyboardNP.reparentTo(self.render)
        self.keyboardNP.setHpr(-90, 0, 15)
        self.keyboardNP.setScale(20)

        self.placeItems()

        self.setupRocketConsole()

        # re-enable mouse
        mat=Mat4(self.camera.getMat())
        mat.invertInPlace()
        self.mouseInterfaceNode.setMat(mat)
        self.enableMouse()

    def placeItems(self):
        self.camera.setPos(0, -20, 0)
        self.camera.setHpr(0, 0, 0)
        self.monitorNP.setPos(0, 0, 1)
        self.keyboardNP.setPos(0, -5, -2.5)


    def setupRocketConsole(self):
        """
        Place a new rocket window onto a texture
        bound to the front of the monitor.
        """
        self.win.setClearColor(Vec4(0.5, 0.5, 0.8, 1))

        faceplate = self.monitorNP.find("**/Faceplate")
        assert faceplate

        mybuffer = self.win.makeTextureBuffer("Console Buffer", 1024, 512)
        tex = mybuffer.getTexture()
        tex.setMagfilter(Texture.FTLinear)
        tex.setMinfilter(Texture.FTLinear)

        faceplate.setTexture(tex, 1)

        self.rocketConsole = RocketRegion.make('console', mybuffer)
        self.rocketConsole.setInputHandler(self.inputHandler)

        self.consoleContext = self.rocketConsole.getContext()
        self.console = console.Console(self, self.consoleContext, 40, 13, self.handleCommand)

        self.console.addLine("Panda DOS")
        self.console.addLine("type 'help'")
        self.console.addLine("")

        self.console.allowEditing(True)

    def handleCommand(self, command):
        if command is None:
            # hack for Ctrl-Break
            self.spewInProgress = False
            self.console.addLine("*** break ***")
            self.console.allowEditing(True)
            return

        command = command.strip()
        if not command:
            return

        tokens = [x.strip() for x in command.split(' ')]
        command = tokens[0].lower()

        if command == 'help':
            self.console.addLines([
                "Sorry, this is utter fakery.",
                "You won't get much more",
                "out of this simulation unless",
                "you program it yourself. :)"
            ])
        elif command == 'dir':
            self.console.addLines([
                "Directory of C:\\:",
                "HELP     COM    72 05-06-2015 14:07",
                "DIR      COM   121 05-06-2015 14:11",
                "SPEW     COM   666 05-06-2015 15:02",
                "   2 Files(s)  859 Bytes.",
                "   0 Dirs(s)  7333 Bytes free.",
                ""])
        elif command == 'cls':
            self.console.cls()
        elif command == 'echo':
            self.console.addLine(' '.join(tokens[1:]))
        elif command == 'ver':
            self.console.addLine('Panda DOS v0.01 in Panda3D ' + PandaSystem.getVersionString())
        elif command == 'spew':
            self.startSpew()
        elif command == 'exit':
            self.console.setPrompt("System is shutting down NOW!")
            self.terminateMonitor()
        else:
            self.console.addLine("command not found")

    def startSpew(self):
        self.console.allowEditing(False)
        self.console.addLine("LINE NOISE 1.0")
        self.console.addLine("")

        self.spewInProgress = True

        # note: spewage always occurs in 'doMethodLater';
        # time.sleep() would be pointless since the whole
        # UI would be frozen during the wait.
        self.queueSpew(2)

    def queueSpew(self, delay=0.1):
        self.taskMgr.doMethodLater(delay, self.spew, 'spew')

    def spew(self, task):
        # generate random spewage, just like on TV!
        if not self.spewInProgress:
            return

        def randchr():
            return chr(int(random.random() < 0.25 and 32 or random.randint(32, 127)))

        line = ''.join([randchr() for _ in range(40) ])

        self.console.addLine(line)
        self.queueSpew()

    def terminateMonitor(self):
        alphaInterval = self.fadeOut(self.console.getTextContainer(), 2)

        alphaInterval.setDoneEvent('fadeOutFinished')

        def fadeOutFinished():
            sys.exit(0)

        self.accept('fadeOutFinished', fadeOutFinished)

        alphaInterval.start()

app = MyApp()
app.run()
