# -*-coding:utf-8 -*

import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
from tkinter import ttk
from pdfembannersrc.wmark import Wmark
import configparser
from subprocess import Popen, CalledProcessError
from pdfembannersrc import strings
from pdfembannersrc import subwindows
import shlex
import os.path
import random
import logging
import PyPDF2
logger = logging.getLogger()


class Interface(tk.Toplevel):
    """
    Main window
    """

    def __init__(self, parent, **kwargs):
        tk.Toplevel.__init__(self, parent)
        self.transient(parent)
        self.grab_set()
        self.title("PDF Embanner : watermark")
        self.geometry("800x400")
        self.protocol("WM_DELETE_WINDOW", self.close)
        self.bind("<Escape>", self.close)
        self.bind("<Control-o>", self.open)
        self.bind("<Control-f>", self.open_markfile)
        self.bind("<Control-s>", self.saveas)
        self.bind("<Control-m>", self.editmetadata)
        self.bind("<Control-a>", self.add)
        self.bind("<Control-Return>", self.do)

        self.file = None
        self.npages=0
        self.box=None
        self.markfile = None
        self.save_file = None
        self.wmarks = []
        self.output_produced=False
        self.metadata = {'/Title': '',
                         '/Author': '',
                         '/Subject': '',
                         '/Creator': 'PDF Embanner'}

        self.f = tk.Frame(self, width=768, height=576, **kwargs)
        self.f.pack(fill=tk.BOTH)

        # Création de nos widgets
        self.f.columnconfigure(1, weight=1)
        self.f.rowconfigure(8, weight=1)
        
        tk.Button(self.f, text="Open", command=self.open).grid(row=0, column=0)
        tk.Button(self.f, text="Close", command=self.close).grid(row=0, column=4, sticky=tk.E)
        self.file_label = tk.Label(self.f, text="-" if self.file is None else self.file)
        self.file_label.grid(row=0, column=1, columnspan=4, sticky=tk.W)

        ttk.Separator(self.f, orient="horizontal").grid(row=1, column=0, columnspan=6, sticky=tk.W+tk.E, padx=5, pady=5)

        tk.Label(self.f, text="Watermark with an other PDF", bg="blue", fg="white", padx=20).grid(row=2, column=0, columnspan=5, sticky=tk.W)
        tk.Button(self.f, text="File", command=self.open_markfile).grid(row=3, column=0)
        self.markfile_label = tk.Label(self.f, text="-" if self.markfile is None else self.markfile)
        self.markfile_label.grid(row=3, column=1, columnspan=2, sticky=tk.W)
        tk.Label(self.f, text="Select pages").grid(row=3, column=3, sticky=tk.W)
        self.select_page_markfile = tk.StringVar()
        tk.Entry(self.f, textvariable=self.select_page_markfile).grid(row=3, column=4)
        self.all_pages_markfile = tk.IntVar()
        self.all_pages_markfile.set(0)
        tk.Radiobutton(self.f, text="Apply on first page(s) only", variable=self.all_pages_markfile, value=0).grid(row=4, column=0, columnspan=2, sticky=tk.W)
        tk.Radiobutton(self.f, text="Apply on all pages", variable=self.all_pages_markfile, value=1).grid(row=5, column=0, columnspan=2, sticky=tk.W)
        self.markfile_bottom = tk.IntVar()
        self.markfile_bottom.set(0)
        tk.Radiobutton(self.f, text="Apply on top", variable=self.markfile_bottom, value=0).grid(row=4, column=2, columnspan=3, sticky=tk.W)
        tk.Radiobutton(self.f, text="Apply on bottom", variable=self.markfile_bottom, value=1).grid(row=5, column=2, columnspan=3, sticky=tk.W)

        ttk.Separator(self.f, orient="horizontal").grid(row=6, column=0, columnspan=6, sticky=tk.W+tk.E, padx=5, pady=5)

        tk.Label(self.f, text="Text watermark", bg="blue", fg="white", padx=20).grid(row=7, column=0, columnspan=5, sticky=tk.W)
        tk.Button(self.f, text="Add", command=self.add).grid(row=7, column=4)

        vsb = tk.Scrollbar(self.f, orient=tk.VERTICAL)
        vsb.grid(row=8, column=5, sticky=tk.N+tk.S)
        self.c = tk.Canvas(self.f,yscrollcommand=vsb.set)
        self.c.grid(row=8, column=0, columnspan=5, sticky="news")
        vsb.config(command=self.c.yview)
        self.frame_wmark = tk.Frame(self.c)
        self.c.create_window(0, 0, window=self.frame_wmark, anchor=tk.NW)
        self.frame_wmark.update_idletasks()
        self.c.config(scrollregion=self.c.bbox("all"))
        self.frame_wmark.columnconfigure(3, weight=1)

        ttk.Separator(self.f, orient="horizontal").grid(row=9, column=0, columnspan=6, sticky=tk.W+tk.E, padx=5, pady=5)

        tk.Button(self.f, text="Save as", command=self.saveas).grid(row=10, column=0)
        self.save_label = tk.Label(self.f, text="-" if self.save_file is None else self.save_file)
        self.save_label.grid(row=10, column=1, columnspan=3, sticky=tk.W)
        tk.Button(self.f, text="Metadata", command=self.editmetadata).grid(row=10, column=4, sticky=tk.E)
        tk.Button(self.f, text="Generate PDF", fg="blue", command=self.do).grid(row=11, column=3, columnspan=3, sticky=tk.E)

        self.message = tk.Label(self.f, text="Welcome !")
        self.message.grid(row=12, column=0, columnspan=6, sticky=tk.W)

    def add(self, *args):
        """
        Add watermark
        """
        wm = Wmark(None)
        i = len(self.wmarks)
        self.wmarks.append(wm)
        wm.add_widgets(self.frame_wmark, self, i)
        self.frame_wmark.update_idletasks()
        self.c.config(scrollregion=self.c.bbox("all"))
        self.message["text"] = "Added watermark"

    def do(self, *args):
        """
        Do watermarking with correct options !
        """
        if(self.file is None):
            messagebox.showwarning(title="PDF Output", message="Please open an input PDF file before !")
            return
        elif(self.save_file is None):
            messagebox.showwarning(title="PDF Output", message="Please define the 'Save As' path before !")
            return
        if(len(self.wmarks)>0):
            # Generate PDF for watermarking with Latex
            err, use_pages_generated, pdfgeneratedname = generate_pdf(self.wmarks, self.save_file, self.npages)
            if(err>0 and err<100):
                self.message["text"] = "Error : no output generated"
                if(err==1):
                    logger.error("Watermark :: Could not write in output folder")
                    messagebox.showerror(title="PDF Output", message="Could not write in output folder")
                elif(err==2):
                    logger.error("Watermark :: Could not parse compiler config file")
                    messagebox.showerror(title="PDF Output", message="Could not parse compiler config file")
                elif(err==3):
                    logger.error("Watermark :: No latex compiler found")
                    messagebox.showerror(title="PDF Output", message="No latex compiler found. Check your compiler.conf file")
                elif(err==4):
                    logger.error("Watermark :: Latex compilation error")
                    messagebox.showerror(title="PDF Output", message="Latex compilation error. You may have put an incorrect caracter in text area.")
                elif(err==5):
                    logger.error("Watermark :: IOError during compilation")
                    messagebox.showerror(title="PDF Output", message="IOError during compilation. Check rights on output folder.")
                return
            elif(err==100):
                logger.info("Watermark :: There were errors in your page ranges. Some page ranges not taken in consideration.")
                messagebox.showinfo(title="PDF Output", message="There were errors in your page ranges. Some page ranges not taken in consideration.")
            genpdf=True
        else:
            genpdf=False
        logger.debug("Genpdf is set to {}".format(genpdf))

        if(self.markfile is not None):
            try:
                self.message["text"] = "Opening {}".format(self.markfile)
                with open(self.markfile, "rb") as in_f:
                    input2 = PyPDF2.PdfFileReader(in_f)
                    markf_npages = input2.getNumPages()
                    correct, use_pages_markf = parse_range(self.select_page_markfile.get(), markf_npages)
                    if(not correct):
                        messagebox.showerror(title="Watermark file", message="Incorrect 'Select page' range. Please correct it!".format(self.markfile))
                        return
                    if(len(use_pages_markf)==0):
                        use_pages_markf = list(range(1,markf_npages+1))
            except (PyPDF2.utils.PdfReadError, EOFError, IOError, NotImplementedError):
                self.message["text"] = "Failed to open {}".format(self.markfile)
                messagebox.showerror(title="PDF Open", message="Impossible to read PDF input file ! {}".format(self.markfile))
                logger.warning("Wartermark : Error while opening {}".format(self.markfile))
                return
            markf = True
        else:
            markf=False
            use_pages_markf = []
        logger.debug("Markf is set to {} with pages {}".format(markf, use_pages_markf))

        genpdf_f = None
        markf_f = None
        try:
            if(markf):
                markf_f = open(self.markfile, 'rb')
                input_mark = PyPDF2.PdfFileReader(markf_f)
            if(genpdf):
                genpdf_f = open(pdfgeneratedname, 'rb')
                input_gen = PyPDF2.PdfFileReader(genpdf_f)
            output = PyPDF2.PdfFileWriter()
            with open(self.file,'rb') as in_f:
                input_ref = PyPDF2.PdfFileReader(in_f)
                for i in range(self.npages):
                    # Adding i-th page !
                    if(markf and (self.all_pages_markfile.get()==1 or i<len(use_pages_markf))):
                        if(self.all_pages_markfile):
                            print("add 1 on {}".format(i))
                            if(self.markfile_bottom.get()):
                                page = input_mark.getPage(use_pages_markf[i%len(use_pages_markf)]-1)
                                page.mergePage(input_ref.getPage(i))
                            else:
                                page = input_ref.getPage(i)
                                page2 = input_mark.getPage(use_pages_markf[i%len(use_pages_markf)]-1)
                                page.mergePage(page2)
                        else:
                            print("add 2 on {}".format(i))
                            if(self.markfile_bottom.get()):
                                page = input_mark.getPage(use_pages_markf[i]-1)
                                page.mergePage(input_ref.getPage(i))
                            else:
                                page = input_ref.getPage(i)
                                page2 = input_mark.getPage(use_pages_markf[i]-1)
                                page.mergePage(page2)
                    else:
                        page = input_ref.getPage(i)
                    if(genpdf and use_pages_generated[i]):
                        page3 = input_gen.getPage(i)
                        sp3 = page3.mediaBox
                        if(self.box is not None):
                            try:
                                sx = float(self.box.upperRight[0] - self.box.upperLeft[0])/float(sp3.upperRight[0] - sp3.upperLeft[0])
                                sy = float(self.box.lowerRight[1] - self.box.upperRight[1])/float(sp3.lowerRight[1] - sp3.upperRight[1])
                                page3.scale(sx,sy)
                            except:
                                logger.warn("Watermark :: Error during rescaling of pages")
                        page.mergePage(page3)
                    output.addPage(page)

                # Writing output file
                try:
                    with open(self.save_file, 'wb') as out_f:
                        output.write(out_f)
                except IOError:
                    logger.error("Watermark :: IOError :: Could not write output file {}".format(self.save_file))
                    self.message["text"] = "Could not write into {}".format(self.save_file)
                    messagebox.showerror(title="PDF Open", message="Impossible to write in output file ! {}".format(self.save_file))
        except IOError:
            logger.error("Watermark :: IOError :: Could not open one of the input file")
            self.message["text"] = "Failed to open input file(s)".format(self.markfile)
            messagebox.showerror(title="PDF Open", message="Impossible to read PDF input file(s) !")
        except PyPDF2.utils.PdfReadError:
            logger.error("Watermark :: Invalid PDF")
            self.message["text"] = "Invalid input PDF".format(self.markfile)
            messagebox.showerror(title="PDF Open", message="Invalid PDF as input file!")
        finally:
            if(genpdf_f is not None):
                genpdf_f.close()
                # Removing genpdf file
                try:
                    os.remove(pdfgeneratedname)
                except:
                    logger.warn("Watermark :: Error while cleaning generated PDF")
            if(markf_f is not None):
                markf_f.close()
        self.message["text"] = "Done"

    def close(self, *args):
        if(self.output_produced or messagebox.askyesno("Quit", "Are you sure you want to quit ?")):
            logger.debug("Watermark: Quit")
            self.destroy()

    def open(self, *args):
        self.message["text"] = "Open..."
        ftypes = [('PDF files (Portable Document Format)', '*.pdf'), ('All files', '*')]
        fl = filedialog.askopenfilename(filetypes = ftypes)
        if fl != '':
            try:
                self.message["text"] = "Opening {}".format(fl)
                with open(fl, "rb") as in_f:
                    input1 = PyPDF2.PdfFileReader(in_f)
                    self.npages = input1.getNumPages()
                    self.box = input1.getPage(0).mediaBox
                    self.file = fl
                    self.file_label["text"] = fl
                    self.message["text"] = "Opened {}".format(fl)
            except (PyPDF2.utils.PdfReadError, EOFError, IOError, NotImplementedError):
                self.message["text"] = "Failed to open {}".format(fl)
                messagebox.showwarning(title="PDF Open", message="Impossible to read PDF input file ! {}".format(fl))
                logger.warning("Watermark: Error while opening {}".format(fl))
        else:
            self.message["text"] = "Nothing to open"

    def open_markfile(self, *args):
        self.message["text"] = "File..."
        ftypes = [('PDF files (Portable Document Format)', '*.pdf'), ('All files', '*')]
        fl = filedialog.askopenfilename(filetypes = ftypes)
        if fl != '':
            self.markfile = fl
            self.markfile_label["text"] = fl
            self.message["text"] = "Watermark file set to {}".format(fl)
        else:
            self.message["text"] = "Nothing to open"

    def saveas(self, *args):
        self.message["text"] = "Save as..."
        fsas = filedialog.asksaveasfilename()
        if fsas != '':
            self.save_file = fsas
            self.save_label["text"] = fsas
            self.message["text"] = "Save as set to {}".format(fsas)
        else:
            self.message["text"] = "Aborted save as"

    def delete(self, ident):
        i=0
        for i in range(len(self.wmarks)):
            if(ident==self.wmarks[i].id):
                break
        self.wmarks[i].destroy_widgets()
        del self.wmarks[i]
        for j in range(i,len(self.wmarks)):
            self.wmarks[j].unset_i_widgets()
            self.wmarks[j].set_i_widgets(j)
        self.message["text"] = "Deleted."

    def editmetadata(self, *args):
        self.message["text"] = "Edit Metadata..."
        interface3 = subwindows.Metadata(self)
        logger.debug("Watermark : Output metadata set")
        interface3.mainloop()
        interface3.destroy()
        logger.debug("Watermark : End output metadata set with value {}".format(self.metadata))
        self.message["text"] = "Edited Metadata"

###############################################################################################

def parse_range(srange, nmax):
    """
    Parse a text as a range of pages
    separated by ; and -
    Recognise 'e' for even pages, 'o' for odd pages and 'end'

    srange : string to be parsed
    nmax : page maximum number

    NB : Pages are numbered from 1 to nmax
    """
    correct=True
    ret = []
    for e in srange.split(';'):
        e = e.split('-')
        if(len(e)==1):
            if(e[0].lower()=='e'): # Even/pair
                ret+=list(range(1,nmax,2))
            elif(e[0].lower()=='o'): # Odd/impair
                ret+=list(range(0,nmax,2))
            elif(e[0].lower()=='end'): # Odd/impair
                ret.append(nmax)
            elif(e[0].isdigit() and int(e[0])>0 and int(e[0])<nmax):
                ret+=[int(e[0])]
            else:
                correct=False
        elif(len(e)==2):
            if(e[0].isdigit() and e[1].isdigit()):
                i = int(e[0])
                j = int(e[1])
                if(i>0 and i<j and j<nmax):
                    ret+=list(range(i,j+1))
                elif(i>0 and i<j):
                    ret+=list(range(i,nmax))
                    correct=False
                else:
                    correct=False
            if(e[0].isdigit() and e[1].lower()=='end'):
                i = int(e[0])
                if(i>0 and i<nmax):
                    ret+=list(range(i,nmax))
                else:
                    correct=False
            else:
                correct=False
        else:
            correct=False
    return correct, ret

def generate_pdf(wmarks, save_file, npages):
    """
    Generate a PDF with Latex with appropriate marks on it

    npages : total number of pages
    wmarks : list of Wmark objects to be placed on pages
    save_file : name of the program output file, used to 
                choose a name for pdf output

    Return errnum, pdf_output_name
    errnum:
      1 : Could not write in output folder
      2 : Could not parse config file
      3 : No compiler available
      4 : Error during compilation
      5 : IOError
      100 : Warning : Error in Wmark range
    """
    err=0
    use_pages_generated=[]

    # Test file to write
    texoutname = save_file + ".tex"
    if(os.path.exists(texoutname)):
        texoutname = save_file + "_" + str(random.randint(0,100000)) + ".tex"
        if(os.path.exists(texoutname)):
            return 1, None, None
    f_tex = None
    reussi, markbypage = wmbypage(wmarks, npages)
    if(not reussi):
        err = 100
    # Writting Tex file
    try:
        f_tex = open(texoutname, 'w')
        f_tex.write(strings.tex_entete)
        for i in range(npages):
            f_tex.write(genpagetex(markbypage[i]))
            if(len(markbypage[i])==0):
                use_pages_generated.append(False)
            else:
                use_pages_generated.append(True)
        f_tex.write(strings.tex_end)
        # Compiling Tex file
    except IOError:
        return 1, None, None
    finally:
        if(f_tex is not None):
            f_tex.close()
    erreurs, pdfgeneratedname  = compiletex(texoutname, deletetex=True)
    if(erreurs>0):
        return erreurs+1, None, None
    return err, use_pages_generated, pdfgeneratedname

def wmbypage(wmarks, npages):
    """
    Return a list indexed by page number (from 0 to npages-1)
    in which element i is a list of Wmark to put on the i+1 page.
    """
    ret = []
    correct = True
    for i in range(npages):
        ret.append([])
    for m in wmarks:
        correct1, onlyp = parse_range(m.onlypages, npages)
        correct2, notp = parse_range(m.notpages, npages)
        correct = correct and correct1 and correct2
        if((1 not in notp and m.onlyfirstpage) or (1 in onlyp) or (len(onlyp)==0 and not m.onlyfirstpage and not m.onlylastpage and 1 not in notp)):
            ret[0].append(m)
        for i in range(1,npages-1):
            if((i+1 not in notp and len(onlyp)==0 and not m.onlyfirstpage and not m.onlylastpage) or (i+1 in onlyp)):
                ret[i].append(m)
        if((npages not in notp and m.onlylastpage) or (npages in onlyp) or (len(onlyp)==0 and not m.onlyfirstpage and not m.onlylastpage and npages not in notp)):
            ret[-1].append(m)
    return correct, ret

def genpagetex(wmarks):
    """
    Return a string to be inserted in tex file
    to generate one page with marks given in
    wmarks (list of Wmark)
    """
    headrule = False
    footrule = False
    lhead =[]
    chead =[]
    rhead =[]
    lfoot =[]
    cfoot =[]
    rfoot =[]
    custom = []
    for m in wmarks:
        if(m.position==strings.topleft):
            lhead.append(m.format_tex())
        elif(m.position==strings.topcenter):
            chead.append(m.format_tex())
        elif(m.position==strings.topright):
            rhead.append(m.format_tex())
        elif(m.position==strings.botleft):
            lfoot.append(m.format_tex())
        elif(m.position==strings.botcenter):
            cfoot.append(m.format_tex())
        elif(m.position==strings.botright):
            rfoot.append(m.format_tex())
        elif(m.position==strings.custom):
            custom.append([m.userposition, m.format_tex()])
        else:
            rhead.append(m.format_tex())
        if(m.lined and m.position in [strings.top]):
            headrule = True
        elif(m.lined and m.position in [strings.bot]):
            footrule = True
    s = "\n\\newpage"
    s += "\n\\lhead{" + ' '.join(lhead) + "}"
    s += "\n\\chead{" + ' '.join(chead) + "}"
    s += "\n\\rhead{" + ' '.join(rhead) + "}"
    s += "\n\\lfoot{" + ' '.join(lfoot) + "}"
    s += "\n\\cfoot{" + ' '.join(cfoot) + "}"
    s += "\n\\rfoot{" + ' '.join(rfoot) + "}"
    if(headrule):
        s += "\n\\renewcommand{\\headrulewidth}{0.4pt}"
    else:
        s += "\n\\renewcommand{\\headrulewidth}{0pt}"
    if(footrule):
        s += "\n\\renewcommand{\\footrulewidth}{0.4pt}"
    else:
        s += "\n\\renewcommand{\\footrulewidth}{0pt}"
    for e in custom:
        s += "\n\\begin{textblock}{1}"+"({},{})".format(e[0][0]*21, e[0][1]*29.7)
        s += "\n" + e[1]
        s += "\n\\end{textblock}"
    s += "\n\\null\n"
    return s

def compiletex(texoutname, deletetex=False, deletetemp=True):
    """
    Compile the tex file texoutname

    Rerurns :
    error number, pdf_filename

    error number : 
      0 : No error
      1 : Could not parse config file
      2 : No compiler available
      3 : Error during compilation
      4 : IOError
    """
    err = 0
    pdfgeneratedname = texoutname[:-4] + ".pdf"
    curdir = os.getcwd()
    os.chdir(os.path.dirname(texoutname))
    texbasename = os.path.basename(texoutname)
    try:
        config = configparser.ConfigParser()
        config.read('pdfembannersrc/compiler.conf')
    except configparser.Error:
        logger.error("Watermark :: configparser failed to parse compiler.conf")
        return 1, None   
    
    for compiler in config.sections():
        if 'cmd' not in config[compiler]:
            logger.error("Watermark :: cmd entry not found for compiler {}".format(compiler))
            return 1, None
        cmd1 = config[compiler]['cmd']
        if 'args' in config[compiler]:
            cmd1 += ' ' + config[compiler]['args']
        cmd1+=' '+texbasename
        cmd = shlex.split(cmd1)
        try:
            logger.debug("Try {}".format(cmd1))
            p = Popen(cmd)
            p.wait()
            p = Popen(cmd)
            p.wait()
            if(p.returncode>0):
                logger.error("Watermark :: Latex Error")
                return 3, None
            logger.info("Watermark :: Compiled successfully with {}".format(cmd1))
            break # On a trouve le bon compilo
        except OSError:
            logger.warn("Watermark :: Failed to compile with {}. Compiler not found.".format(cmd1))
            continue
        except ValueError:
            logger.warn("Watermark :: Failed to compile with {}. Value Error.".format(cmd1))
            continue
        except CalledProcessError:
            return 3, None
        except IOError:
            logger.error("Watermark :: IOError during compilation")
            return 4, None
    else:
        logger.error("Watermark :: No latex compilater found after trying all!")
        return 2, None

    # Delete temp files
    if(deletetemp):
        if('clean_ext' in config[compiler]):
            for ext in config[compiler]['clean_ext'].split():
                todel = texoutname[:-4]+'.'+ext
                logger.debug("Trying to del {}".format(todel))
                if(os.path.exists(todel)):
                    try:
                        os.remove(todel)
                    except (OSError, IOError):
                        logger.error("Watermark :: Error while deleting {}".format(todel))

    # Delete tex file
    if(deletetex):
        try:
            os.remove(texoutname)
        except (OSError, IOError):
            logger.error("Error while deleting {}".format(texoutname))

    os.chdir(curdir)

    return err, pdfgeneratedname