#-------------------------------------------------------------------------------
# Name:        Exo2Ass Converter (JP/EN Combined version)
# Purpose:     Convert EXO files generated by AviUtl(JP/EN Combined) to Srt subtitle
#
# Author:      MaverickTse
#
# Created:     23/11/2013
# Copyright:   (c) MaverickTse 2013
# Licence:     MIT
#-------------------------------------------------------------------------------
import sys
import collections
import re
import os
import traceback
from string import Template
#----------------------------------------
# Global variables
#----------------------------------------
src_encoding= "sjis" #default EXO is in Shift-JIS. If error occurred complaining can't decode file, change this to your system codepage
#For a list of usable encodings, check:
#http://docs.python.org/2.4/lib/standard-encodings.html

out_encoding= "utf-8" #encoding for ASS file. Usually utf-8 works well.


ASS_Script_Info_Template= Template("""
[Script Info]
 ; Script generated by EXO2ASS
 ; http://mavericktse.is-a-geek.com
 Title: $title
 Original Script: <unknown>
 ScriptType: v4.00+
 Collisions: Normal
 PlayResX: $SrcResX
 PlayResY: $SrcResY
 PlayDepth: 0
 Timer: 100,0000
 WrapStyle: 1
""")
ASS_Style_Header= ("""
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
""")
ASS_Style_Template= Template("Style: $Name, $Fontname, $Fontsize, $PrimaryColour, &H00000008, $OutlineColour, $BackColour, $Bold, $Italic, 0, 0, $ScaleX, $ScaleY, 0, $Angle, $BorderStyle, $Outline, $Shadow, $Alignment, 0, 0, 0, $Encoding\n")

ASS_Events_Header=("""
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
""")
ASS_Event_Template= Template("Dialogue: $Layer, $Start, $End, $Style, $Name, $MarginL, $MarginR, $MarginV, $Effect, $Text\n")

Align_mapping={0:7, 1:8, 2:9, 3:4, 4:5, 5:6, 6:1, 7:2, 8:3, 9:9, 10:6, 11:3, 12:8, 13:5, 14:2, 15:7, 16:4, 17:1}
CenX=0
CenY=0
#---------------------------------------------------------
#Genaral users should not need to change anything below
#--------------------------------------------------------
def setScriptInfo(srcString, filename):
    splitFilename= filename.split('\\')
    file= splitFilename[len(splitFilename)-1]
    findWidth= re.compile(r"width=(\d+)", re.M)
    findHeight= re.compile(r"height=(\d+)",re.M)
    matchWidth= findWidth.search(srcString)
    matchHeight= findHeight.search(srcString)
    resX= 1280
    resY= 720
    if matchWidth:
        resX= int(matchWidth.group(1))
    if matchHeight:
        resY= int(matchHeight.group(1))
    ScriptInfoString= ASS_Script_Info_Template.substitute(title=file, SrcResX=resX, SrcResY=resY)
    global CenX
    global CenY
    CenX= resX // 2
    CenY= resY // 2
    return ScriptInfoString



def setFrameDuration(src_str):
    rg= r"rate=(\d+)"
    rg2= r"scale=(\d+)"
    p= re.compile(rg, re.M)
    p2= re.compile(rg2, re.M)
    m= p.search(src_str)
    m2= p2.search(src_str)
    if m:
        fps= m.group(1)
        fps_scale= m2.group(1)
        ifps= float(fps)/float(fps_scale)
        return 1000/ifps
    else:
        return -1


def readEXO(filename):  #filename as string
    with open(filename, encoding= src_encoding) as handle:
        src_string= handle.read()
    return src_string   # return the file's text as string

def getTimeStringFromFrame(frameNum, frameLen):
    fullMs= int(frameNum* frameLen)
    ms= fullMs % 1000
    hr= int(fullMs // 3600000)
    min= int((fullMs-hr*3600000)//60000)
    sec= int((fullMs-ms)//1000-hr*3600-min*60)
    format_str= "{:0>2}" #leading zero making up 2 digits
    hr_str= "{:0>1}".format(hr)
    min_str= format_str.format(min)
    sec_str= format_str.format(sec)
    ms_str= "{:0>2}".format(ms//10) #leading zeros making up 3 digits for milliseconds
    return hr_str+':'+min_str+':'+sec_str+'.'+ms_str


def hex2String(hexstring):
    txt= bytes.fromhex(hexstring).decode("utf-16")    #convert the hex string to actual text
    txt= txt.rstrip("\x00")
    if ("\x00" in txt):                       #Find interrupting NULL byte
                firstnull= txt.find("\x00")           #get NULL's index
                txt=txt[0:firstnull]
    txt= txt.replace("\r","") #remove excess CR mark
    txt= txt.replace("\n","\\N") #add linebreak cmd

    return txt

def srtWriter(assfilename, out_str):
    with open(assfilename, mode='w', encoding=out_encoding) as saveHandle:
        saveHandle.write(out_str)

def srtBuilder(infile):
    dtbl_StartFrame= dict() #table of Starting frames
    dtbl_EndFrame= dict() #table of Ending frames
    dtbl_Content= dict() #table of text
    dtbl_Actor= dict() #table of Actor/layer
    dtbl_FontSize= dict() #table of font size
    dtbl_Bold= dict() #table of bold flags; 0= off, -1= on
    dtbl_Italic= dict() #table of italic flags; 0= off, -1= on
    dtbl_Outline= dict() #Outline width: 2 or 4
    dtbl_Shadow= dict() #Shadow size: 2 or 4
    dtbl_Align= dict() #Numpad alignment number
    dtbl_Alpha= dict() # alpha value
    dtbl_PrimaryColor= dict() #font color
    dtbl_OutlineColor= dict() #BBGGRR for outline or shadow color
    dtbl_Font= dict() #font name
    dtbl_Scale=dict() #scaleX/Y
    dtbl_X= dict() #AnchorX
    dtbl_Y= dict() #AnchorY
    dtbl_isVert= dict() #is Vertical?
    dtbl_hasBorder= dict() #has border or shadow?



    out_ass=""
    out_event_block=ASS_Events_Header

    offsetPattern=r"offset(\d+)"
    offsetrg= re.compile(offsetPattern, re.I)
    offsetmatch= offsetrg.search(infile)
    offsetFrame= 0
    if (offsetmatch):
        offsetFrame= int(offsetmatch.group(1))
        print("Found offset", offsetFrame)

    p= (r"start=(?P<dStart>\d+?)\n"
    r"end=(?P<dEnd>\d+?)\n"
    r"layer=(?P<dLayer>\d+?)\n"
    r"(?:group=\d+?\n)*?"
    r"overlay=\d+?\n"
    r"camera=\d+?\n"
    r"\[\d+?\.0\]\n"
    r"_name=(?:テキスト|Text)\n"
    r"(?:Size|サイズ)=(?P<dSize>\d+?)\n"
    r".+?"
    r"^B=(?P<dBold>\d)\n"
    r"^I=(?P<dItalic>\d)\n"
    r"type=(?P<dBorder>\d)\n"
    r".+?"
    r"align=(?P<dAlign>\d+?)\n"
    r"(?:.+?\n){3}"
    r"color=(?P<dColor>[0-9a-f]+?)\n"
    r"color2=(?P<dColor2>[0-9a-f]+?)\n"
    r"font=(?P<dFont>.+?)\n"
    r"text=(?P<dText>[0-9a-f]+?)\n"
    r".+?"
    r"(^X=(?P<dX>[-.\d]+?))\n"
    r"(^Y=(?P<dY>[-.\d]+?))\n"
    r"Z=(?:[-.\d]+?)\n"
    r"(?:Zoom%|拡大率)=(?P<dScale>[-.\d]+?)\n"
    r"(?:Clearness|透明度)=(?P<dAlpha>[-.\d]+?)\n")

    rg= re.compile(p, re.M|re.IGNORECASE|re.DOTALL)
    src= readEXO(infile)
    fLen= setFrameDuration(src)
    if (fLen==-1):
        print("Warning: missing frame rate info in file- {0}".format(infile))
        print("Assuming 30fps")
        fLen= 100/3

    out_ass+=setScriptInfo(src,infile)
    out_ass+=ASS_Style_Header
    m= rg.finditer(src)
    if m: #finditer will not throw error anyway
        print("regex matched")
        #0: st; 1: ed; 2: txt
        i=1 #dicts' key field
        trackActor=0 #monitor the change in actor
        for amatch in m:
            stTime= int(amatch.group("dStart"))+offsetFrame

            edTime= int(amatch.group("dEnd"))+offsetFrame
##            print("From: ", stTime, " To: ", edTime) #debug use

            content= hex2String(amatch.group("dText"))
            dtbl_StartFrame[i]= stTime                    #Add to Start Frame Tabel
            dtbl_EndFrame[i]= edTime                      #Add to End Frame Table
            dtbl_Content[i]= content                      #Add to text content Table
            currentActor=int(amatch.group("dLayer"))
            dtbl_Actor[i]= "Actor"+ str(currentActor)
            dtbl_Font[i]= amatch.group("dFont")
            dtbl_FontSize[i]= amatch.group("dSize")
            alpha= (int(float(amatch.group("dAlpha")))*255) //100
            dtbl_Alpha[i]= "{0:0{1}X}".format(alpha,2)

            dtbl_PrimaryColor[i]= amatch.group("dColor")[-2:]+amatch.group("dColor")[2:4]+amatch.group("dColor")[:2]
            dtbl_OutlineColor[i]= amatch.group("dColor2")[-2:]+amatch.group("dColor2")[2:4]+amatch.group("dColor2")[:2] #also use as shadow color
            dtbl_Bold[i]= int(amatch.group("dBold"))*-1
            dtbl_Italic[i]= int(amatch.group("dItalic"))*-1
##            print("X=", amatch.group("dX"))
##            print("Y=", amatch.group("dY"))
            dtbl_X[i]=int(float(amatch.group("dX")))+CenX
            dtbl_Y[i]=int(float(amatch.group("dY")))+CenY
            dtbl_Scale[i]=float(amatch.group("dScale"))
            #process alignment
            exoAlign= int(amatch.group("dAlign"))
            assAlign= Align_mapping[exoAlign]
            dtbl_Align[i]= assAlign
            if(exoAlign>8):
                dtbl_isVert[i]= True
                dtbl_Font[i]= "@"+dtbl_Font[i]
            else:
                dtbl_isVert[i]= False

            #Process border
            exoBorder= int(amatch.group("dBorder"))
            if(exoBorder==0):
                dtbl_hasBorder[i]=False
                dtbl_Outline[i]=1
                dtbl_Shadow[i]=1
            elif (exoBorder==1): #Shadowed
                dtbl_hasBorder[i]=True
                dtbl_Outline[i]=0
                dtbl_Shadow[i]=4
            elif (exoBorder==2): #Light Shadow
                dtbl_hasBorder[i]=True
                dtbl_Outline[i]=0
                dtbl_Shadow[i]=2
            elif(exoBorder==3): #Outlined
                dtbl_hasBorder[i]=True
                dtbl_Outline[i]=4
                dtbl_Shadow[i]=0
            else: #assume thin outline
                dtbl_hasBorder[i]=True
                dtbl_Outline[i]=2
                dtbl_Shadow[i]=0


            if(currentActor!=trackActor): #on changing layer/actor, add a new style
                #Add a new style
                style_cmd= ASS_Style_Template.substitute(Name=dtbl_Actor[i], Fontname=dtbl_Font[i], Fontsize=dtbl_FontSize[i], PrimaryColour="&H"+dtbl_Alpha[i]+dtbl_PrimaryColor[i], OutlineColour="&H"+dtbl_Alpha[i]+dtbl_OutlineColor[i], BackColour="&H"+dtbl_Alpha[i]+dtbl_OutlineColor[i], Bold=dtbl_Bold[i], Italic=dtbl_Italic[i], ScaleX=dtbl_Scale[i], ScaleY=dtbl_Scale[i], Angle=dtbl_isVert[i]*-90, BorderStyle=int(dtbl_hasBorder[i]), Outline=dtbl_Outline[i], Shadow=dtbl_Shadow[i], Alignment=dtbl_Align[i], Encoding=1)
                out_ass+= style_cmd
                trackActor= currentActor
            i+=1 #increment key's value

        ordered_SFrame= collections.OrderedDict(sorted(dtbl_StartFrame.items(), key=lambda t:int(t[1]))) #sort by starting frame no.
        print("Length of ordered Dict=", len(ordered_SFrame))
        i=1 #ASS dialogue num.
        out_srt=""
        for pair in ordered_SFrame.items():
            key= pair[0]
            stStamp= getTimeStringFromFrame(pair[1], fLen)
            edStamp= getTimeStringFromFrame(dtbl_EndFrame[key], fLen)
            txt= dtbl_Content[key]
            #print(txt) #debug
            out_event_block+= ASS_Event_Template.substitute(Layer=1, Start=stStamp, End=edStamp, Style=dtbl_Actor[key], Name=dtbl_Actor[key], MarginL=0, MarginR=0, MarginV=0, Effect="", Text=txt)
            i+=1

        srtWriter(infile[:-4]+"-NoPOS.ass",out_ass+out_event_block) #default to original's file path, just rename the extension to .ass
        print("Processed {0} text objects from {1}".format(i-1, infile))
    else:
        print("No Text object found in {0}".format(infile))




def main():
##    srtBuilder("ENFormattedSample.exo")

    try:
        if len(sys.argv) > 1:
            filelist= sys.argv[1:]
            for afile in filelist:
                srtBuilder(afile)
                print(afile)
        else:
            print("No file to process")

    except:
        traceback.print_exc()



if __name__ == '__main__':
    main()
    dummy= input("Press any key")
