# play list functions
# this is just a part of MMDDirector

from UnityEngine import Quaternion, Vector3, Vector2, Color, Rect, GameObject, Transform
from UnityEngine import GUI, GUILayout, GUIStyle, GUIContent, GUIUtility
from mmddirector import mmddGuiSwitch, mmddGuiMain, guiPartsSeekbar, guiSelectDialog, guiPromptDialog, guiConfirmDialog, guiWaitDialog
from mmddirector import LC, redText, greenText, greyText, yellowText, guiOpenFileDialog, onMmddStop
from vngameengine import random_choice
import ConfigParser
import traceback
import random
import copy
from os import path

def onMmddStop_playlist(game, stopByTimer):
    onMmddStop(game, stopByTimer)
    if stopByTimer:
        playNextDance(game)

def mmddGuiPlaylist(game):
    if mmddGuiSwitch(game, mmddGuiPlaylist):
        mmdd = game.gdata.mmdd
        mmdd.loop = False
        mmdd.onStopFunc = onMmddStop_playlist
        game.scenedata.playlistScrollPos = Vector2.zero
        game.scenedata.playlistEditMode = False
        # try restore current playling index
        try:
            di = game.scenedata.playlist[game.scenedata.currentPlayingIndex]
            for i in range(len(mmdd.motionSequence)):
                did = mmdd.motionSequence[i]
                mc = mmdd.getMotionController(did)
                mp = mmdd.getMorphController(did)
                if i < di.dancerNumber:
                    j = i
                elif di.dancerNumber == 1:
                    j = 0
                else:
                    raise Exception("dancer number not match")
                if mc and mc.vmdFilePath != di.dancerFiles[j][0]:
                    raise Exception("motion of dancer #%d mismatch"%i)
                if not mc and len(di.dancerFiles[j][0]) > 0:
                    raise Exception("motion of dancer #%d missed"%i)
                if mp and mp.vmdFilePath != di.dancerFiles[j][1]:
                    raise Exception("morph of dancer #%d mismatch"%i)
                if not mp and len(di.dancerFiles[j][1]) > 0:
                    raise Exception("morph of dancer #%d missed"%i)
            if mmdd.cameraController and mmdd.cameraController.vmdFilePath != di.cameraFile:
                raise Exception("camera motion mismatch")
            if not mmdd.cameraController and len(di.cameraFile) > 0:
                raise Exception("camera motion missed")
            if mmdd.soundController and mmdd.soundController.wavFilePath != di.musicFile:
                raise Exception("music mismatch")
            if not mmdd.soundController and len(di.musicFile) > 0:
                raise Exception("music missed")
        except Exception, e:
            print "Warning: Fail to restore current playlist index:", e
            game.scenedata.currentPlayingIndex = None
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    cbwidth = (fullw - 4*4) / 5
    leftw = fullw - 200
    loopModeText = (LC("No Loop"), LC("Single Loop"), LC("List Loop"), LC("Shuffle"))

    def trySaveAndPrompt(filename):
        errMsg = savePlaylist(filename, sd.playlist, mmddConfig.playlistSaveRelPath)
        if errMsg:
            guiPromptDialog(game, "Failed to save playlist:\n\n" + redText(errMsg))
            return False
        else:
            mmddConfig.defPlaylist = filename
            sd.playlistModiftied = False
            return True
    def tryLoadAndPrompt(filename):
        pl, errMsg = loadPlaylist(filename)
        if pl:
            mmddConfig.defPlaylist = filename
            sd.playlist = pl
            if len(errMsg):
                guiPromptDialog(game, "Some track failed to load and ignored:\n\n" + yellowText(errMsg))
        else:
            mmddConfig.defPlaylist = ""
            sd.playlist = []
            guiPromptDialog(game, "Failed to load playlist:\n\n" + redText(errMsg))
        sd.playlistModiftied = False

    # load playlist if not exist
    if not hasattr(sd, "playlist"):
        if len(mmddConfig.defPlaylist) > 0:
            tryLoadAndPrompt(mmddConfig.defPlaylist)
        else:
            sd.playlist = []
        sd.playlistModiftied = False
    # prepare candidate if needed
    if not hasattr(sd, "dancerCandidate"):
        sd.dancerCandidate = autoPrepareCandidate(game)
        sd.backupCandidateSeq = copy.copy(sd.dancerCandidate)
        sd.backupCandidatePos = {}
        sd.backupCandidateRot = {}
        for dc in sd.dancerCandidate:
            sd.backupCandidatePos[dc.objctrl] = dc.pos
            sd.backupCandidateRot[dc.objctrl] = dc.rot
    else:
        # deleted non-exited dancer
        for dc in sd.dancerCandidate:
            if dc.objctrl.charInfo.objBody == None: 
                del sd.backupCandidatePos[dc.objctrl]
                del sd.backupCandidateRot[dc.objctrl]
                sd.backupCandidateSeq.remove(dc)
                sd.dancerCandidate.remove(dc)
    # check next-dance button status
    nextBtnState = mmddConfig.playlistLoop and sd.currentPlayingIndex != None and mmdd.isPlaying
    if nextBtnState:
        sd.playlistNextCallback = playNextDance
    else:
        sd.playlistNextCallback = None

    # seek bar
    guiPartsSeekbar(game)

    GUILayout.BeginHorizontal()
    # playlist contents (left side)
    sd.playlistScrollPos = GUILayout.BeginScrollView(sd.playlistScrollPos, GUI.skin.box, GUILayout.Width(leftw))
    for dix in range(len(sd.playlist)):
        di = sd.playlist[dix]
        if sd.playlistEditMode:
            canup = dix > 0
            candn = dix < len(sd.playlist) - 1
            GUILayout.BeginHorizontal()
            di.name = GUILayout.TextField(di.name)
            #GUILayout.FlexibleSpace()
            if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
                sd.playlist[dix-1], sd.playlist[dix] = sd.playlist[dix], sd.playlist[dix-1]
                break
            if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
                sd.playlist[dix+1], sd.playlist[dix] = sd.playlist[dix], sd.playlist[dix+1]
                break
            if GUILayout.Button("X", GUILayout.Width(25)):
                sd.playlist.pop(dix)
                break
            GUILayout.EndHorizontal()
        else:
            if di.dancerNumber > len(sd.dancerCandidate):   # not enough dancer
                ntxt = redText(str(di.dancerNumber))
            elif di.dancerNumber == len(sd.dancerCandidate):   # just the same
                ntxt = greenText(str(di.dancerNumber))
            elif di.dancerNumber == 1 and mmddConfig.extendMultiDancer: # extend multi dancer
                if mmddConfig.extendMultiLimit > 1:
                    extlimit = min(mmddConfig.extendMultiLimit, len(sd.dancerCandidate))
                else:
                    extlimit = len(sd.dancerCandidate)
                ntxt = yellowText("1->%d"%extlimit)
            else:   # part of dancers will perform
                ntxt = yellowText(str(di.dancerNumber))
            dTitle = "%s (%s)"%(di.name, ntxt)
            color = GUI.color
            if dix == sd.currentPlayingIndex:
                GUI.color = Color.green
            if GUILayout.Button(dTitle):
                changeDancerAuto(game)
                loadDanceTrack(game, di)
            GUI.color = color
    GUILayout.EndScrollView()

    # control and option area (right side)
    GUILayout.BeginVertical()
    # playlist title
    GUILayout.BeginHorizontal()
    listname = path.split(mmddConfig.defPlaylist)[1] if len(mmddConfig.defPlaylist) > 0 else "<i>Newlist</i>"
    if sd.playlistModiftied:
        GUILayout.Label(LC("Playlist: %s")%greenText(listname) + " " + redText(u"\u2605"))
    else:
        GUILayout.Label(LC("Playlist: %s")%greenText(listname))
    GUILayout.EndHorizontal()
    # edit/load/save/saveas
    GUILayout.BeginHorizontal()
    if sd.playlistEditMode:
        if GUILayout.Button(LC("OK")):
            sd.playlistEditMode = False
            sd.playlistModiftied = True
    else:
        if GUILayout.Button(LC("Add")):
            mmddGuiPlaylist_Add(game)
        if GUILayout.Button(LC("Edit")):
            sd.playlistEditMode = True
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    if len(mmddConfig.defPlaylist) > 0 and GUILayout.Button(LC("Save")):
        trySaveAndPrompt(mmddConfig.defPlaylist)
    if GUILayout.Button(LC("Save As...")):
        def onSavePlaylist(files):
            if files and len(files) > 0:
                filename = files[0]
                if not filename.lower().endswith(".txt"): filename += ".txt"
                trySaveAndPrompt(filename)
        guiOpenFileDialog(onSavePlaylist, mmddConfig.defPlaylist if len(mmddConfig.defPlaylist) else mmddConfig.vmdHomeDir, False, "TXT")
    if GUILayout.Button(LC("Load...")):
        def onLoadPlaylist(files):
            if files and len(files) > 0:
                tryLoadAndPrompt(files[0])
        guiOpenFileDialog(onLoadPlaylist, mmddConfig.defPlaylist if len(mmddConfig.defPlaylist) else mmddConfig.vmdHomeDir, True, "TXT")
    GUILayout.EndHorizontal()
    # option header
    GUILayout.BeginHorizontal() 
    GUILayout.Label("Playlist Options:")
    GUILayout.EndHorizontal()
    # loop mode
    GUILayout.BeginHorizontal()
    GUILayout.Label("Loop mode:")
    if GUILayout.Button(loopModeText[mmddConfig.playlistLoop]):
        mmddConfig.playlistLoop = (mmddConfig.playlistLoop + 1) % len(loopModeText)
    GUILayout.EndHorizontal()
    # extend dance setting
    GUILayout.BeginHorizontal() 
    mmddConfig.extendMultiDancer = GUILayout.Toggle(mmddConfig.extendMultiDancer, LC("Extend single dancer to"))
    extvaltxt_cur = str(mmddConfig.extendMultiLimit) if mmddConfig.extendMultiLimit > 1 else "all"
    extvaltxt_new = GUILayout.TextField(extvaltxt_cur, GUILayout.Width(30))
    if extvaltxt_cur != extvaltxt_new:
        try:
            mmddConfig.extendMultiLimit = int(extvaltxt_new)
            if mmddConfig.extendMultiLimit <= 1:
                mmddConfig.extendMultiLimit = 0
        except:
            pass
    GUILayout.EndHorizontal()
    # hide dancer setting
    GUILayout.BeginHorizontal() 
    mmddConfig.hideNonActiveDancer = GUILayout.Toggle(mmddConfig.hideNonActiveDancer, LC("Auto hide non-active dancer"))
    GUILayout.EndHorizontal()
    # auto switch camera
    GUILayout.BeginHorizontal()
    mmddConfig.autoSwitchCamera = GUILayout.Toggle(mmddConfig.autoSwitchCamera, LC("Auto switch camera"))
    GUILayout.EndHorizontal()
    # disable camera setting
    GUILayout.BeginHorizontal() 
    mmddConfig.disableCamera = GUILayout.Toggle(mmddConfig.disableCamera, LC("Disable camera"))
    if mmdd.cameraController:
        mmdd.cameraController.enable = not mmddConfig.disableCamera
    GUILayout.EndHorizontal()
    GUILayout.EndVertical()
    GUILayout.EndHorizontal()

    # Control buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button("Next" if nextBtnState else " ", btnstyle, GUILayout.Width(cbwidth)) and nextBtnState:
        playNextDance(game)
    if GUILayout.Button("Dancer (%d)"%len(sd.dancerCandidate), btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiPlaylist_dancer(game)
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        def saveAndExit(g):
            if len(mmddConfig.defPlaylist) > 0:
                saved = trySaveAndPrompt(mmddConfig.defPlaylist)
                if saved:
                    exitPlaylist(g)
            else:
                mmddGuiPlaylist(game)
                guiPromptDialog(game, LC("New playlist must be saved by [Save as...] function."))
        def exitPlaylist(g):
            mmdd.onStopFunc = onMmddStop
            if mmddConfig.hideNonActiveDancer:
                for d in sd.dancerCandidate:
                    d.visible = True
            resetDancerCandidatePosRot(game)
            mmddGuiMain(game)
        if sd.playlistModiftied:
            param = (LC("Playlist modified! Save it to %s?")%greenText(listname), (LC("Save and back"), saveAndExit), (LC("Back without save"), exitPlaylist), (LC("Cancel"), None))
            guiSelectDialog(game, param)
        else:
            exitPlaylist(None)
    GUILayout.EndHorizontal()

def mmddGuiPlaylist_Add(game):
    def initAddMenu():
        sd = game.scenedata
        mmdd = game.gdata.mmdd
        sd.playlistAddScrollPos = Vector2.zero
        if len(mmdd.motionSequence) > 0:
            mc0 = mmdd.motionControllers[mmdd.motionSequence[0]]
            vmdPath, vmdName = path.split(mc0.vmdFilePath)
            pPath, folderName = path.split(vmdPath)
            sd.playlistAddTempName = folderName if folderName else vmdName
            sd.playlistAddTempName = sd.playlistAddTempName.replace("[", "")
            sd.playlistAddTempName = sd.playlistAddTempName.replace("]", "")
        else:
            sd.playlistAddTempName = ""
        sd.playlistAddTempNumber = len(mmdd.motionSequence)
        sd.playlistAddTempAlign = True
        if len(mmdd.motionSequence) > 1:
            dc0 = game.scenef_get_actor(mmdd.motionSequence[0])
            for i in range(1, len(mmdd.motionSequence)):
                dcx = game.scenef_get_actor(mmdd.motionSequence[i])
                if dcx.pos != dc0.pos or dcx.rot != dc0.rot:
                    sd.playlistAddTempAlign = False
    def clearAddMenu():
        sd = game.scenedata
        del sd.playlistAddScrollPos
        del sd.playlistAddTempName
        del sd.playlistAddTempNumber
        del sd.playlistAddTempAlign
    def checkInput(anyAsset):
        # check name
        sd = game.scenedata
        if len(sd.playlistAddTempName) == 0:
            return LC("Dance must has a title!")
        if sd.playlistAddTempName.find("[") != -1 or sd.playlistAddTempName.find("]") != -1:
            return LC("Square brackets ('[' and ']') can not be used in dance title.")
        if sd.playlistAddTempName in (di.name for di in sd.playlist):
            return LC("Dance title duplicated!")
        if not anyAsset:
            return LC("No asset at all!")
        return None
    def checkIkStatus(motionCtrl):
        isAnyON = False
        for ik in motionCtrl.ikSettingMap:
            isAnyON |= bool(motionCtrl.ikSettingMap[ik].enable)
        return isAnyON

    if mmddGuiSwitch(game, mmddGuiPlaylist_Add):
        initAddMenu()
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 16
    labelStyle.wordWrap = True
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    cbwidth = (fullw - 4) / 2

    # Title
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Add current MMDD setting to playlist:"), labelStyle)
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Dance Title:"), GUILayout.Width(100))
    sd.playlistAddTempName = GUILayout.TextField(sd.playlistAddTempName)
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Dancer Number:"), GUILayout.Width(100))
    if GUILayout.Button("-", GUILayout.Width(20)) and sd.playlistAddTempNumber > 0:
        sd.playlistAddTempNumber -= 1
    GUILayout.Label(greenText(str(sd.playlistAddTempNumber)), GUILayout.Width(30))
    if GUILayout.Button("+", GUILayout.Width(20)) and sd.playlistAddTempNumber < len(mmdd.motionSequence):
        sd.playlistAddTempNumber += 1
    if sd.playlistAddTempNumber > 1:
        GUILayout.Space(20)
        sd.playlistAddTempAlign = GUILayout.Toggle(sd.playlistAddTempAlign, "Auto align all dancers")
    GUILayout.EndHorizontal()

    # contents to add
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Dance Detail:"), GUILayout.Width(100))
    GUILayout.EndHorizontal()
    sd.playlistAddScrollPos = GUILayout.BeginScrollView(sd.playlistAddScrollPos, GUI.skin.box, GUILayout.Width(fullw))
    anything = False
    ndi = DanceInfo(sd.playlistAddTempName)
    ndi.dancerNumber = sd.playlistAddTempNumber
    ndi.dancerFiles = []
    ndi.dancerBones = []
    ndi.dancerEnableIK = []
    ndi.alignDancer = sd.playlistAddTempAlign
    # dancer
    for i in range(sd.playlistAddTempNumber):
        did = mmdd.motionSequence[i]
        mc = mmdd.getMotionController(did)
        mp = mmdd.getMorphController(did)
        mctxt = LC("Dancer #%d motion : %s")%(i, redText("None") if not mc else greenText(mc.FileName))
        mptxt = LC("Dancer #%d morph  : %s")%(i, redText("None") if not mp else greenText(mp.FileName))
        GUILayout.BeginHorizontal()
        GUILayout.Label(mctxt)
        if mc and not checkIkStatus(mc):
            GUILayout.Label(redText(" *IK OFF*"))
            GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label(mptxt)
        GUILayout.EndHorizontal()
        if not mc.IsDefaultBoneDefine:
            GUILayout.BeginHorizontal()
            bdFilename = path.split(mc.boneDefineFilename)[1]
            GUILayout.Label("Dancer #%d bone define: %s"%(i, yellowText(bdFilename)))
            GUILayout.EndHorizontal()
        else:
            bdFilename = None
        ndi.dancerFiles.append(("" if not mc else mc.vmdFilePath, "" if not mp else mp.vmdFilePath))
        ndi.dancerEnableIK.append(None if not mc else checkIkStatus(mc))
        ndi.dancerBones.append(bdFilename)
        anything = True
    # camera
    if mmdd.cameraController:
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Camera: %s")%greenText(mmdd.cameraController.FileName))
        GUILayout.EndHorizontal()
        ndi.cameraFile = mmdd.cameraController.vmdFilePath
        anything = True
    # music
    if mmdd.soundController:
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Music : %s")%greenText(mmdd.soundController.FileName))
        GUILayout.EndHorizontal()
        ndi.musicFile = mmdd.soundController.wavFilePath
        anything = True
    # anything
    if not anything:
        GUILayout.BeginHorizontal()
        GUILayout.Label(yellowText(LC("Nothing in MMDD, add some assets please.")))
        GUILayout.EndHorizontal()
    GUILayout.EndScrollView()

    # Warning
    GUILayout.BeginHorizontal()
    GUILayout.Label(yellowText(LC("Warning: All adjustment will be ignored!")))
    GUILayout.EndHorizontal()

    # Control buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(LC("Add to playlist"), btnstyle, GUILayout.Width(cbwidth)):
        errMsg = checkInput(anything)
        if errMsg:
            guiPromptDialog(game, "Failed to add to playlist:\n\n" + redText(errMsg))
        else:
            sd.playlist.append(ndi)
            sd.playlistModiftied = True
            clearAddMenu()
            mmddGuiPlaylist(game)
    if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth)):
        clearAddMenu()
        mmddGuiPlaylist(game)
    GUILayout.EndHorizontal()

def mmddGuiPlaylist_dancer(game):
    if mmddGuiSwitch(game, mmddGuiPlaylist_dancer):
        game.scenedata.playlistDancerScrollPos = Vector2.zero
        game.scenedata.playlistTempDancerCandidate = copy.copy(game.scenedata.backupCandidateSeq)
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 16
    labelStyle.wordWrap = True
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    cbwidth = (fullw - 4*3) / 4

    def goBack():
        del sd.playlistDancerScrollPos
        mmddGuiPlaylist(game)
    
    # data
    allChars = game.scene_get_all_females()
    allChars += game.scene_get_all_males()

    # Title
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Select dancers for your playlist:"), labelStyle)
    GUILayout.EndHorizontal()
    sd.playlistDancerScrollPos = GUILayout.BeginScrollView(sd.playlistDancerScrollPos, GUI.skin.box, GUILayout.Width(fullw))
    # selected
    for dix in range(len(sd.playlistTempDancerCandidate)):
        dc = sd.playlistTempDancerCandidate[dix]
        canup = dix > 0
        candn = dix < len(sd.playlistTempDancerCandidate) - 1
        GUILayout.BeginHorizontal()
        sel = GUILayout.Toggle(True, "Dancer #%d: "%dix + greenText(dc.text_name))
        if not sel:
            sd.playlistTempDancerCandidate.pop(dix)
            break
        if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
            sd.playlistTempDancerCandidate[dix-1], sd.playlistTempDancerCandidate[dix] = sd.playlistTempDancerCandidate[dix], sd.playlistTempDancerCandidate[dix-1]
            break
        if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
            sd.playlistTempDancerCandidate[dix+1], sd.playlistTempDancerCandidate[dix] = sd.playlistTempDancerCandidate[dix], sd.playlistTempDancerCandidate[dix+1]
            break
        GUILayout.EndHorizontal()
        for char in allChars:
            if char.objctrl == dc.objctrl:
                allChars.remove(char)
                break
    # non-selected
    for char in allChars:
        GUILayout.BeginHorizontal()
        sel = GUILayout.Toggle(False, char.text_name)
        if sel:
            sd.playlistTempDancerCandidate.append(char.as_actor)
            break
        GUILayout.EndHorizontal()
    GUILayout.EndScrollView()
    # option
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Change dancer on each dance: "))
    if GUILayout.Button(greenText(LC("No change")) if mmddConfig.dancersLoop == 0 else LC("No change ")):
        mmddConfig.dancersLoop = 0
    if GUILayout.Button(greenText(LC("Sequency")) if mmddConfig.dancersLoop == 1 else LC("Sequency")):
        mmddConfig.dancersLoop = 1
    if GUILayout.Button(greenText(LC("Randomly")) if mmddConfig.dancersLoop == 2 else LC("Randomly ")):
        mmddConfig.dancersLoop = 2
    GUILayout.Space(5)
    mmddConfig.remainDancerPosY = GUILayout.Toggle(mmddConfig.remainDancerPosY, LC("Remain Y offset"))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # Control buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(LC("OK"), btnstyle, GUILayout.Width(cbwidth)):
        resetDancerCandidatePosRot(game)
        sd.dancerCandidate = sd.playlistTempDancerCandidate
        sd.backupCandidateSeq = copy.copy(sd.dancerCandidate)
        sd.backupCandidatePos = {}
        sd.backupCandidateRot = {}
        for dc in sd.dancerCandidate:
            sd.backupCandidatePos[dc.objctrl] = dc.pos
            sd.backupCandidateRot[dc.objctrl] = dc.rot
        goBack()
    if GUILayout.Button(LC("All"), btnstyle, GUILayout.Width(cbwidth)):
        for char in allChars:
            sd.playlistTempDancerCandidate.append(char.as_actor)
    if GUILayout.Button(LC("Auto"), btnstyle, GUILayout.Width(cbwidth)):
        sd.playlistTempDancerCandidate = autoPrepareCandidate(game)
    if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth)):
        goBack()
    GUILayout.EndHorizontal()

# save playlist file (.txt)
def savePlaylist(filename, playlist, useRelativePath):
    try:
        if useRelativePath:
            baseDir = path.split(filename)[0]
        else:
            baseDir = None
        content = "; MMDDirector playlist\n\n"
        for di in playlist:
            content += di.export(baseDir) + "\n"
        # save file
        import codecs
        cfile = codecs.open(filename, "w", "utf-8")
        cfile.write(content)
        cfile.close()
        return None
    except Exception, e:
        traceback.print_exc()
        return str(e)

# load playlist file (.txt)
def loadPlaylist(filename):
    if not path.isfile(filename):
        return None, "File '%s' not found"%filename

    baseDir = path.split(filename)[0]
    danceList = []
    errMsg = ""
    try:
        config = ConfigParser.SafeConfigParser()
        config.read(filename)

        for danceName in config.sections():
            # load dance
            danceTitle = danceName.decode("utf8")
            di = DanceInfo(danceTitle)
            try:
                di.load(config.items(danceName))
            except Exception, e:
                traceback.print_exc()
                errMsg += "Fail to parse setting of [%s]: %s\n"%(danceTitle, str(e))
                continue
            # check and add to list
            err = di.checkError(baseDir)
            if err:
                errMsg += "Fail when check setting of [%s]: %s\n"%(danceTitle, err)
            else:
                danceList.append(di)
        # done
        return danceList, errMsg

    except Exception, e:
        traceback.print_exc()
        return None, str(e)

# play next dance
def playNextDance(game):
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    # check ready
    if not hasattr(sd, "playlist") or not hasattr(sd, "currentPlayingIndex") or not hasattr(sd, "dancerCandidate"):
        return
    # check loop mode and do loop
    if mmddConfig.playlistLoop and sd.currentPlayingIndex != None:
        # dance loop
        if mmddConfig.playlistLoop == 1 :
            nextidx = sd.currentPlayingIndex
        elif mmddConfig.playlistLoop == 2:
            nextidx = (sd.currentPlayingIndex + 1)%len(sd.playlist)
        elif mmddConfig.playlistLoop == 3:
            nc = range(len(sd.playlist))
            nc.remove(sd.currentPlayingIndex)
            nextidx = random_choice(nc)
        else:
            raise Exception("Unexpected loop mode")
        # dancer loop
        changeDancerAuto(game)
        # load dance
        di = sd.playlist[nextidx]
        loadDanceTrack(game, di, True)

# get dancer candidate
def autoPrepareCandidate(game):
    def verifyCandidate(actor, ignoreAnime):
        if not actor.visible:
            print "%s fail to be candidate because of visible = False"%actor.text_name
            return False
        if not ignoreAnime and actor.get_animate()[:3] != (0, 0, 0):
            print "%s fail to be candidate because of anime not set to t-pose"%actor.text_name
            return False
        if actor.get_kinematic() != 0:
            print "%s fail to be candidate because of kinematice status not 0"%actor.text_name
            return False
        return True

    mmdd = game.gdata.mmdd
    allChars = game.scene_get_all_females()
    allChars += game.scene_get_all_males()
    extActors = game.scenef_get_all_actors()

    # sort and merge
    candidates = []
    for aid in sorted(extActors.keys()):
        if aid in mmdd.motionSequence or verifyCandidate(extActors[aid], True):
            candidates.append(extActors[aid])
        for char in allChars:
            if char.objctrl == extActors[aid].objctrl:
                allChars.remove(char)
                break
    for char in allChars:
        actor = char.as_actor
        if verifyCandidate(actor, False):
            candidates.append(actor)

    # done
    return candidates

# reset dancer canidate postition and rotation
def resetDancerCandidatePosRot(game):
    sd = game.scenedata
    for dc in sd.dancerCandidate:
        dc.move(sd.backupCandidatePos[dc.objctrl], sd.backupCandidateRot[dc.objctrl])

# change dancer
def changeDancerAuto(game):
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    if mmddConfig.dancersLoop and len(sd.dancerCandidate) > 1:
        if mmddConfig.dancersLoop == 1:
            dc = sd.dancerCandidate.pop(0)
            sd.dancerCandidate.append(dc)
        elif mmddConfig.dancersLoop == 2:
            rng = random.WichmannHill()
            rng.shuffle(sd.dancerCandidate)
        else:
            mmddConfig.dancersLoop = 0

# select who is the dancer
def selectDancer(game, number, copyDance):
    dcs = game.scenedata.dancerCandidate
    mmddConfig = game.gdata.mmdd_config
    if number >= len(dcs):
        # just match or not enough candidate, return all candidate
        dancers = tuple(dcs)
    else:
        # more candidate than needed
        if number == 1:
            if copyDance:
                if mmddConfig.extendMultiLimit < 1 or len(dcs) <= mmddConfig.extendMultiLimit:
                    # let's all dance
                    dancers = tuple(dcs)
                else:
                    # limited dancers
                    dancers = tuple(dcs[:mmddConfig.extendMultiLimit])
            else:
                # just first candidate
                dancers = (dcs[0],)
        else:
            # needed only, hide unneeded ones
            dancers = tuple(dcs[:number])
    return dancers

# load a dance track
def loadDanceTrack(game, danceInfo, startAfterLoad = None, dancers = None):
    from vmdlib import VmdLib
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    def enableAndCheck(param):
        mcs = param
        for mc in mcs:
            if not mc.Enable:
                mc.setEnable(True)
            else:
                mcs.remove(mc)
        if len(mcs) > 0:
            game.updFuncParam = mcs
            game.updFunc = enableAndCheck

    try:
        # select dancer if not set
        copyDance = danceInfo.dancerNumber == 1 and mmddConfig.extendMultiDancer
        if not dancers:
            dancers = selectDancer(game, danceInfo.dancerNumber, copyDance)

        # hide/show dancer
        for actor in sd.dancerCandidate:
            if mmddConfig.hideNonActiveDancer:
                actor.visible = actor in dancers
            else:
                actor.visible = True    

        # clean mmdd
        if startAfterLoad == None:
            startAfterLoad = mmdd.isPlaying
        mmdd.cleanup(True)

        # load vmd files
        mcToActive = []
        if not hasattr(sd, "vmdResultBuffer"):
            sd.vmdResultBuffer = {}
        for i in range(danceInfo.dancerNumber):
            if i >= len(dancers):
                break
            motionFile = danceInfo.dancerFiles[i][0]
            if len(motionFile) > 0:
                if not sd.vmdResultBuffer.has_key(motionFile):
                    vmdRes = VmdLib.loadVmd(motionFile)
                    if len(vmdRes) == 1:
                        raise Exception("Fail to load motion file '%s': %s"%(motionFile, vmdRes[0]))
                    sd.vmdResultBuffer[motionFile] = vmdRes
                # import motion
                mc = mmdd.createMotionController(dancers[i], danceInfo.dancerBones[i])
                mc.importBoneKeyframes(motionFile, sd.vmdResultBuffer[motionFile][1])
                mmdd.postProcessForMotionImport(dancers[i])
                if not danceInfo.getIkForDancer(i):     # disable IK if asked
                    for iks in mc.ikSettingMap.values():
                        iks.enable = False
                    mc.buildWorkBoneIndex()
                mcToActive.append(mc)
                # copy move
                if copyDance:
                    for j in range(1, len(dancers)):
                        mc = mmdd.createMotionController(dancers[j], danceInfo.dancerBones[i]) # with same bone define
                        mc.importBoneKeyframes(motionFile, sd.vmdResultBuffer[motionFile][1])
                        mmdd.postProcessForMotionImport(dancers[j])
                        if not danceInfo.getIkForDancer(i):     # disable IK for copies too
                            for iks in mc.ikSettingMap.values():
                                iks.enable = False
                            mc.buildWorkBoneIndex()
                        mcToActive.append(mc)
                
            morphFile = danceInfo.dancerFiles[i][1]
            if len(morphFile) > 0:
                if not sd.vmdResultBuffer.has_key(morphFile):
                    vmdRes = VmdLib.loadVmd(morphFile)
                    if len(vmdRes) == 1:
                        raise Exception("Fail to load morph file '%s': %s"%(morphFile, vmdRes[0]))
                    sd.vmdResultBuffer[morphFile] = vmdRes
                # import morph
                mp = mmdd.createMorphController(dancers[i])
                mp.importMorphKeyframes(morphFile, sd.vmdResultBuffer[morphFile][2])
                # copy move
                if copyDance:
                    for j in range(1, len(dancers)):
                        mp = mmdd.createMorphController(dancers[j])
                        mp.importMorphKeyframes(morphFile, sd.vmdResultBuffer[morphFile][2])
        # enable mcs
        game.updFuncParam = mcToActive
        game.updFunc = enableAndCheck

        # load camera
        if len(danceInfo.cameraFile) > 0:
            if not sd.vmdResultBuffer.has_key(danceInfo.cameraFile):
                vmdRes = VmdLib.loadVmd(danceInfo.cameraFile)
                if len(vmdRes) == 1:
                    raise Exception("Fail to load camera file '%s': %s"%(danceInfo.cameraFile, vmdRes[0]))
                sd.vmdResultBuffer[danceInfo.cameraFile] = vmdRes
            # import camera
            mmdd.loadCameraKeyframes(danceInfo.cameraFile, sd.vmdResultBuffer[danceInfo.cameraFile][3])
            # setup auto camera switch
            mmdd.cameraController.switchMinFramePeriod = mmddConfig.switchMinFramePeriod
            mmdd.cameraController.switchMaxFramePeriod = mmddConfig.switchMaxFramePeriod
            mmdd.cameraController.switchProbability = mmddConfig.switchProbability
            mmdd.cameraController.enableMultiTarget = mmddConfig.autoSwitchCamera and copyDance and len(dancers) > 0

        # load music
        if len(danceInfo.musicFile) > 0:
            err = mmdd.loadWavFile(danceInfo.musicFile)
            if err:
                raise Exception("Fail to load wav file: " + err)

        # auto align dancer or set position/rotation
        if len(dancers) > 0 and danceInfo.dancerNumber > 1 and danceInfo.alignDancer:
            fstPos = sd.backupCandidatePos[sd.backupCandidateSeq[0].objctrl]
            fstRot = sd.backupCandidateRot[sd.backupCandidateSeq[0].objctrl]
            for i in range(len(dancers)):
                if mmddConfig.remainDancerPosY:
                    offY = sd.backupCandidatePos[dancers[i].objctrl].y
                    fstPos = Vector3(fstPos.x, offY, fstPos.z)
                dancers[i].move(fstPos, fstRot)
            for i in range(len(dancers), len(sd.dancerCandidate)):
                orgPos = sd.backupCandidatePos[sd.backupCandidateSeq[i].objctrl]
                orgRot = sd.backupCandidateRot[sd.backupCandidateSeq[i].objctrl]
                if mmddConfig.remainDancerPosY:
                    offY = sd.backupCandidatePos[sd.dancerCandidate[i].objctrl].y
                    orgPos = Vector3(orgPos.x, offY, orgPos.z)
                sd.dancerCandidate[i].move(orgPos, orgRot)
        else:
            for i in range(len(sd.dancerCandidate)):
                orgPos = sd.backupCandidatePos[sd.backupCandidateSeq[i].objctrl]
                orgRot = sd.backupCandidateRot[sd.backupCandidateSeq[i].objctrl]
                if mmddConfig.remainDancerPosY:
                    offY = sd.backupCandidatePos[sd.dancerCandidate[i].objctrl].y
                    orgPos = Vector3(orgPos.x, offY, orgPos.z)
                sd.dancerCandidate[i].move(orgPos, orgRot)

        # auto align camera
        if mmdd.cameraController and len(dancers) > 0:
            mmdd.cameraController.offPos = dancers[0].pos
            mmdd.cameraController.offRot = dancers[0].rot
            mmdd.cameraController.enable = not mmddConfig.disableCamera
            mc = mmdd.getMotionController(dancers[0])
            if mc:
                mmdd.cameraController.posScale *= mc.charScale * 1.05
                mmdd.cameraController.dstScale *= mc.charScale * 1.05
        
        # done
        sd.currentPlayingIndex = sd.playlist.index(danceInfo)
        mmdd.refreshTotalFrame()
        if startAfterLoad:
            mmdd.gotoStartFrame()
            mmdd.start()
        else:
            mmdd.update()
        return True
    except:
        traceback.print_exc()
        return False
    
class DanceInfo():
    def __init__(self, name):
        self.name = name
        self.dancerNumber = None
        self.dancerFiles = None
        self.dancerBones = None
        self.dancerEnableIK = []
        self.cameraFile = ""
        self.musicFile = ""
        self.alignDancer = True

    def checkError(self, baseDir):
        def checkAdjPath(orgpath):
            if len(orgpath) == 0:
                return True, orgpath
            elif path.isfile(orgpath):
                return True, path.abspath(orgpath)
            elif path.isfile(path.join(baseDir, orgpath)):
                return True, path.join(baseDir, orgpath)
            else:
                return False, orgpath
        #
        if self.dancerNumber == None:
            return "No dancer setted!"
        if self.dancerFiles == None or self.dancerNumber != len(self.dancerFiles):
            return "Actor files missed!"
        for i in range(self.dancerNumber):
            if len(self.dancerFiles[i][0]) == 0 and len(self.dancerFiles[i][1]) == 0:
                return "No motion file nor morph file setted for dancer%d!"%i
            isok, self.dancerFiles[i][0] = checkAdjPath(self.dancerFiles[i][0])
            if not isok:
                return "Motion file for dancer%d not found!"%i
            isok, self.dancerFiles[i][1] = checkAdjPath(self.dancerFiles[i][1])
            if not isok:
                return "Morph file for dancer%d not found!"%i
        isok, self.cameraFile = checkAdjPath(self.cameraFile)
        if not isok:
            return "Camera file not found!"
        isok, self.musicFile = checkAdjPath(self.musicFile)
        if not isok:
            return "Music file not found!"
        # no error
        return None

    def getIkForDancer(self, dancerIndex):
        if len(self.dancerEnableIK) == 0:
            return True     # default true if no setting
        elif dancerIndex >= len(self.dancerEnableIK):
            return self.dancerEnableIK[-1]  # last setting
        else:
            return self.dancerEnableIK[dancerIndex]  # right setting

    def load(self, items):
        baseFolder = ""
        for k, v in items:
            sv = v.decode("utf8")
            if k == "dancernumber":
                self.dancerNumber = int(v)
                self.dancerFiles = []
                self.dancerBones = []
                for i in range(self.dancerNumber):
                    self.dancerFiles.append(["", ""])
                    self.dancerBones.append(None)
            elif k.startswith("dancer") and k.endswith("motion"):
                no = int(k[6:-6])
                self.dancerFiles[no][0] = sv
            elif k.startswith("dancer") and k.endswith("morph"):
                no = int(k[6:-5])
                self.dancerFiles[no][1] = sv
            elif k.startswith("dancer") and k.endswith("bone"):
                no = int(k[6:-4])
                self.dancerBones[no] = sv
            elif k.startswith("dancer"):
                no = int(k[6:])
                self.dancerFiles[no][0] = sv
                self.dancerFiles[no][1] = sv
            elif k == "camera":
                self.cameraFile = sv
            elif k == "music":
                self.musicFile = sv
            elif k == "aligndancer":
                self.alignDancer = bool(eval(v))
            elif k == "enableik":
                self.dancerEnableIK = eval("[" + v + "]")
            elif k == "basefolder":
                baseFolder = sv
            else:
                print "Playlist [%s] encount Unknown key: %s"%(self.name, k)
        # add base folder if exist
        if len(baseFolder) > 0:
            for vf in self.dancerFiles:
                if len(vf[0]):
                    vf[0] = path.join(baseFolder, vf[0])
                if len(vf[1]):
                    vf[1] = path.join(baseFolder, vf[1])
            if len(self.cameraFile):
                self.cameraFile = path.join(baseFolder, self.cameraFile)
            if len(self.musicFile):
                self.musicFile = path.join(baseFolder, self.musicFile)

    def export(self, baseDir):
        def adjPath(orgPath, baseDir):
            if len(orgPath) == 0:
                return orgPath
            if baseDir:
                try:
                    return path.relpath(orgPath, baseDir)
                except:
                    return orgPath
            else:
                return orgPath
        # collect files
        allFiles = []
        dFiles = []
        for (mcf, mpf) in self.dancerFiles:
            mcf = adjPath(mcf, baseDir)
            mpf = adjPath(mpf, baseDir)
            dFiles.append([mcf, mpf])
            if mcf: 
                allFiles.append(mcf.split("\\"))
            if mpf: 
                allFiles.append(mpf.split("\\"))
        cFile = adjPath(self.cameraFile, baseDir)
        if cFile:
            allFiles.append(cFile.split("\\"))
        mFile = adjPath(self.musicFile, baseDir)
        if mFile:
            allFiles.append(mFile.split("\\"))
        # get basefolder
        baseFolderArr = []
        if len(allFiles) >= 2:
            bi = 0
            while True:
                checkFlag = True
                for n in range(len(allFiles) - 1):
                    if bi >= len(allFiles[n]) or bi >= len(allFiles[n+1]) or allFiles[n][bi] != allFiles[n+1][bi]:
                        checkFlag = False
                        break
                if checkFlag:
                    baseFolderArr.append(allFiles[0][bi])
                    bi += 1
                else:
                    break
        if len(baseFolderArr):
            baseFolder = "\\".join(baseFolderArr)
            baseFolderOffset = len(baseFolder) + 1
        else:
            baseFolder = ""
            baseFolderOffset = 0
        # build content
        content = "[" + self.name + "]\n"
        if baseFolderOffset:
            content += "basefolder=" + baseFolder + "\n"
        content += "DancerNumber=" + str(self.dancerNumber) + "\n"
        for i in range(self.dancerNumber):
            if dFiles[i][0] == dFiles[i][1]:
                content += "Dancer%d="%i + dFiles[i][0][baseFolderOffset:] + "\n"
            else:
                content += "Dancer%dMotion="%i + dFiles[i][0][baseFolderOffset:] + "\n"
                content += "Dancer%dMorph="%i + dFiles[i][1][baseFolderOffset:] + "\n"
            if self.dancerBones[i]:
                content += "Dancer%dBone="%i + self.dancerBones[i] + "\n"
        content += "Camera=" + cFile[baseFolderOffset:] + "\n"
        content += "Music=" + mFile[baseFolderOffset:] + "\n"
        if False in self.dancerEnableIK:
            content += "EnableIK=" + (",".join([str(b) for b in self.dancerEnableIK])) + "\n"
        if not self.alignDancer:
            content += "AlignDancer=False\n"
        return content


