# -*- coding: utf-8 -*-
#!/usr/bin/env python
# **************************************************************************
# *
# * Authors:     Pablo Conesa (pconesa@cnb.csic.es)
# *
# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
# *
# * This program is free software; you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation; either version 3 of the License, or
# * (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program; if not, write to the Free Software
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
# * 02111-1307  USA
# *
# *  All comments concerning this program package may be sent to the
# *  e-mail address 'scipion@cnb.csic.es'
# *
# **************************************************************************
"""
Creates a scipion workflow file (json formatted) base on a template.
The template may have some ~placeholders~ that will be overwritten with values
Template may look like this, separator is "~" and within it you can define:
~title|value|type~
Template string sits at the end of the file ready for a running streaming demo.
"""
import subprocess
import sys
import os
import re
import tkinter as tk
import tkinter.font as tkFont
import traceback
import time

import pyworkflow as pw
from pyworkflow.config import VarTypes
import pyworkflow.utils as pwutils
from pyworkflow.gui import Message, dialog, askPath
from pyworkflow.project import Project
import pyworkflow.gui as pwgui
from pyworkflow.gui.project.base import ProjectBaseWindow
from pyworkflow.gui.widgets import HotButton, Button
from pyworkflow.template import TemplateList, LocalTemplate, Template
from scipion.constants import SCIPION_EP, MODE_PROJECT

# Custom labels
from scipion.utils import getExternalJsonTemplates
from pyworkflow.utils import Icon

ENTRY = 'entry'
LABEL = 'label'
CHECKBUTTON = 'Checkbutton'
YES = "Yes"
NO = "No"

FLAG_PARAM = "--"
NOGUI_FLAG = FLAG_PARAM + "nogui"
NOSCHEDULE_FLAG = FLAG_PARAM + "noschedule"


ACCEPT_BUTTON = "Accept"
LEN_LABEL_IN_CHARS = 30
LABEL_ALIGN_PATTERN = '{:>%s}' % LEN_LABEL_IN_CHARS
LEN_BUTTON_IN_CHARS = 12
BUTTON_ALIGN_PATTERN = "{:^%s}" % LEN_BUTTON_IN_CHARS

FIELD_SEP = '~'
VIEW_WIZARD = 'wizardview'

# Project name files
PROJECT_NAME = 'Project name'
DO_NOT_SCHEDULE = "Cancel schedule"
DO_NOT_SHOW_GUI = "Don't show the project"

# Project regex to validate the session id name
PROJECT_PATTERN = "^\w{2}\d{4,6}-\d+$"
PROJECT_REGEX = re.compile(PROJECT_PATTERN)


class KickoffWindow(ProjectBaseWindow):
    """ Windows to manage all projects. """

    def __init__(self, template, argsList, showScheduleOption, schedule,
                 showProjectOption, showProject, showProjectName):
        try:
            title = '%s (%s on %s)' % ('Workflow template customizer',
                                       pwutils.getLocalUserName(),
                                       pwutils.getLocalHostName())
        except Exception:
            title = Message.LABEL_PROJECTS

        ProjectBaseWindow.__init__(self, title, minsize=(800, 350))
        self.template = template
        self.argsList = argsList
        self.showScheduleOption = showScheduleOption
        self.schedule = schedule
        self.showProjectOption = showProjectOption
        self.showProject = showProject
        self.showProjectName = showProjectName

        self.viewFuncs = {VIEW_WIZARD: KickoffView}
        self.action = Message.LABEL_BUTTON_CANCEL
        self.switchView(VIEW_WIZARD)

    def getTemplate(self):
        return self.template

    def getAction(self):
        return self.action

    def switchView(self, newView, **kwargs):
        # Destroy the previous view if exists:
        if self.viewWidget:
            self.viewWidget.grid_forget()
            self.viewWidget.destroy()
        # Create the new view: Instantiates KickoffView HERE!.
        self.viewWidget = self.viewFuncs[newView](self.footer, self,
                                                  template=self.template,
                                                  argsList=self.argsList,
                                                  showScheduleOption=self.showScheduleOption,
                                                  schedule=self.schedule,
                                                  showProjectOption=self.showProjectOption,
                                                  showProject=self.showProject,
                                                  showProjectName=self.showProjectName)

        # Grid in the second row (1)
        self.viewWidget.grid(row=0, column=0, columnspan=10, sticky='news')
        self.footer.rowconfigure(0, weight=1)
        self.footer.columnconfigure(0, weight=1)
        self.view = newView

    def _onClosing(self):
        self.root.destroy()
        if self.showScheduleOption and self.showProjectOption:
            sys.exit()


class KickoffView(tk.Frame):
    def __init__(self, parent, window, template:Template=None, argsList=[],
                 showScheduleOption=True, schedule=True, showProjectOption=True,
                 showProject=True, showProjectName=True, **kwargs):

        tk.Frame.__init__(self, parent, bg='white', **kwargs)
        self.window = window
        self.root = window.root
        self.vars = {}
        self.checkvars = []
        self.template = template
        self.argsList = argsList
        self.showScheduleOption = showScheduleOption
        self.schedule = schedule
        self.showProjectOption = showProjectOption
        self.showProject = showProject
        self.showProjectName = showProjectName

        bigSize = pw.Config.SCIPION_FONT_SIZE + 2
        smallSize = pw.Config.SCIPION_FONT_SIZE - 2
        fontName = pw.Config.SCIPION_FONT_NAME

        self.bigFont = tkFont.Font(size=bigSize, family=fontName)
        self.bigFontBold = tkFont.Font(size=bigSize, family=fontName,
                                       weight='bold')

        self.projDateFont = tkFont.Font(size=smallSize, family=fontName)
        self.projDelFont = tkFont.Font(size=smallSize, family=fontName,
                                       weight='bold')
        # Body section
        bodyFrame = tk.Frame(self, bg='white')
        bodyFrame.columnconfigure(0, minsize=120)
        bodyFrame.columnconfigure(1, minsize=120, weight=1)
        bodyFrame.grid(row=0, column=0, sticky='news')
        self._fillContent(bodyFrame)

        # Add the create project button
        btnFrame = tk.Frame(self, bg='white')
        btn = HotButton(btnFrame, text=ACCEPT_BUTTON,
                        font=self.bigFontBold,
                        command=self._onReadDataFromTemplateForm)
        btn.grid(row=0, column=1, sticky='ne', padx=10, pady=10)

        # Add the Import project button
        btn = Button(btnFrame, Message.LABEL_BUTTON_CANCEL,
                     font=self.bigFontBold,
                     command=self._closeCallback)
        btn.grid(row=0, column=0, sticky='ne', pady=10)

        btnFrame.columnconfigure(0, weight=1)
        btnFrame.grid(row=1, column=0, sticky='news')

        self.columnconfigure(0, weight=1)

    def _closeCallback(self):
        self.root.destroy()
        if self.showScheduleOption and self.showProjectOption:
            sys.exit()

    def _fillContent(self, frame):
        # Add project name
        if self.template.projectName is None:
            self.template.genProjectName()

        self._addPair(PROJECT_NAME, PROJECT_NAME, 1, frame,
                      value=self.template.projectName,
                      varType=VarTypes.STRING.value,
                      visible=self.showProjectName)

        self._addPair(DO_NOT_SCHEDULE, DO_NOT_SCHEDULE, 2, frame,
                      varType=VarTypes.BOOLEAN.value, value=not self.schedule,
                      visible=self.showScheduleOption)

        self._addPair(DO_NOT_SHOW_GUI, DO_NOT_SHOW_GUI, 3, frame,
                      varType=VarTypes.BOOLEAN.value, value=not self.showProject,
                      pady=(5, 30), visible=self.showProjectOption)

        # Add template params
        self._addTemplateFieldsToForm(frame)

    def _addPair(self, text, title, r, lf, varType=None,
                 value=None, pady=2, visible=True):

        if visible:
            label = tk.Label(lf, text=text, bg='white', font=self.bigFont)
            label.grid(row=r, column=0, padx=(10, 5), pady=pady, sticky='nes')

        var = tk.StringVar()

        if value is not None:
            var.set(value)

        self.vars[title] = var
        if visible:
            if varType is None:
                widget = tk.Label(lf, font=self.bigFont, textvariable=var)

            elif varType == VarTypes.BOOLEAN.value:
                var.set(YES if value else NO)
                widget = tk.Checkbutton(lf, text="", font=self.bigFont, variable=var,
                                        onvalue=YES, offvalue=NO, bg=pw.Config.SCIPION_BG_COLOR)
            else:
                widget = tk.Entry(lf, width=30, font=self.bigFont,
                                  textvariable=var)

            widget.grid(row=r, column=1, sticky='news', padx=(5, 10), pady=pady)

            if varType in (VarTypes.FOLDER.value, VarTypes.PATH.value):
                def searchPath(onlyFolders):
                    result= askPath(path=var.get(), onlyFolders=onlyFolders, master=self.window)
                    var.set(result)

                btn = tk.Label(lf, text="", font=self.bigFont, image=self.window.getImage(Icon.FOLDER), bg=pw.Config.SCIPION_BG_COLOR)
                btn.bind('<Button-1>', lambda e: searchPath(varType == VarTypes.FOLDER.value))

                btn.grid(row=r,column=2)
    def _addTemplateFieldsToForm(self, labelFrame):
        row = 5
        for field in self.template.params.values():
            alias = field.getAlias()
            text = field.getTitle() if alias is None else "%s (%s)" % (field.getTitle(), alias)

            self._addPair(text, field.getTitle(), row, labelFrame,
                          varType=field.getType(), value=field.getValue())
            row += 1

    def _getVar(self, varKey):
        return self.vars[varKey]

    def _getValue(self, varKey):
        return self.vars[varKey].get()

    def _setValue(self, varKey, value):
        return self.vars[varKey].set(value)

    # noinspection PyUnusedLocal
    def _onReadDataFromTemplateForm(self, e=None):
        errors = []

        # Check the entered data
        for field in self.template.params.values():
            newValue = self._getValue(field.getTitle())
            field.setValue(newValue)

            validationMsg = field.validate()

            if validationMsg:
                errors.append("%s value does not validate: %s. Value: %s, Type: %s."
                              % (field.getTitle(), validationMsg, field.getValue(),
                                 field.getType()))

        # Do more checks only if there are no previous errors
        if errors:
            errors.insert(0, "*Errors*:")
            self.window.showError("\n  - ".join(errors))
        else:
            self.template.projectName = self._getValue(PROJECT_NAME)

            # Set parent with the data
            self.window.template = self.template
            if self._getValue(DO_NOT_SCHEDULE) == YES:
                self.argsList.append(NOSCHEDULE_FLAG)
            if self._getValue(DO_NOT_SHOW_GUI) == YES:
                self.argsList.append(NOGUI_FLAG)

            self.window.action = ACCEPT_BUTTON
            self.window.root.quit()
            self.window.root.withdraw()
            return


def getTemplates(templateName=None):
    """ Get a template or templates either from arguments
        or from the templates directory.
        If more than one template is found or passed, a dialog is raised
        to choose one.
    """
    tempList = TemplateList()
    tempId = None
    if templateName:
        if os.path.isfile(templateName) and os.path.exists(templateName):
            t = LocalTemplate("custom_template", templateName)
            tempList.addTemplate(t)
        else:
            tempId = templateName

    # Try to find all templates from the template folder and the plugins
    if len(tempList.templates) == 0:
        tempList.addScipionTemplates(tempId)
        if not (tempId is not None and len(tempList.templates) == 1):
            tempList.addPluginTemplates(tempId)

    if not len(tempList.templates):

        msg=("No valid file found (*.json.template).\n"
                        "Please, add (at least one) at %s "
                        "or pass it as argument(s).\n"
                        "\n -> Usage: scipion template [PATH.json.template]\n"
                        "\n see 'scipion help'\n" % getExternalJsonTemplates())

        sys.exit(msg)

    return tempList.sortListByPluginName().templates


def chooseTemplate(templates, parentWindow=None):
    chosenTemplate = None
    if len(templates) == 1:
        chosenTemplate = templates[0]
    else:
        provider = pwgui.tree.ListTreeProviderTemplate(templates)
        dlg = dialog.ListDialog(parentWindow, "Workflow templates", provider,
                                "Select one of the templates.",
                                selectOnDoubleClick=True)

        if dlg.result == dialog.RESULT_YES:
            chosenTemplate = dlg.values[0]

    if chosenTemplate is not None:
        print("Template to use: %s" % chosenTemplate)
        # Replace environment variables
        chosenTemplate.replaceEnvVariables()

    return chosenTemplate


def resolveTemplate(template, argsList, showScheduleOption=True, schedule=True,
                    showProjectOption=True, showProject=True, showProjectName=True):
    """ Resolve a template assigning CML params to the template.
    if not enough, a window will pop pup to ask for missing ones only"""

    if not assignAllParams(argsList, template):
        wizWindow = KickoffWindow(template=template,
                                  argsList=argsList,
                                  showScheduleOption=showScheduleOption,
                                  schedule=schedule,
                                  showProjectOption=showProjectOption,
                                  showProject=showProject,
                                  showProjectName=showProjectName)
        wizWindow.show()
        return wizWindow.action == ACCEPT_BUTTON
    else:
        # All parameters have been assigned and template is fully populated
        # Add schedule and showProject flags back: removed at flag2value
        if not schedule:
            argsList.append(NOSCHEDULE_FLAG)

        if not showProject:
            argsList.append(NOGUI_FLAG)

        return True


def assignAllParams(argsList, template):
    """
    Assign CML params to the template, if missing params after assignment
    return False
    """
    paramsSetted = 0
    template.parseContent()
    if argsList:

        for attr in argsList:
            # skipp --params
            if attr.startswith(FLAG_PARAM):
                continue

            aliasAttr, valAttr = attr.split('=')
            try:
                if aliasAttr=="project_name":
                    template.projectName=valAttr
                else:
                    paramsSetted += template.setParamValue(aliasAttr, valAttr)
            except Exception as e:
                print(pwutils.redStr(e))
                sys.exit(os.EX_DATAERR)

    return len(template.params) == paramsSetted


def createTemplateFile(template):
    try:
        workflow = template.createTemplateFile()
    except Exception as e:
        workflow = None
        errorStr = "Couldn't create the template.\n" + str(e)
        print(errorStr)
        traceback.print_exc()
    return workflow


def launchTemplate(argsList, template:Template):
    """ Launches a resolved template"""
    workflow = createTemplateFile(template)
    if workflow is not None:
        # Create the project
        if not template.projectName:
            template.genProjectName()
        createProjectFromWorkflow(workflow, template.projectName, argsList, template.description)


def importTemplate(template, window):
    """
    Import a resolved template
    """
    workflow = createTemplateFile(template)
    if workflow is not None:
        try:
            window.getViewWidget().info('Importing workflow %s' % workflow)
            window.project.loadProtocols(workflow)
            window.getViewWidget().updateRunsGraph(True)
            window.getViewWidget().cleanInfo()
        except Exception as ex:
            window.showError(str(ex), exception=ex)


def createProjectFromWorkflow(workflow, projectName, argsList, comment):
    scipion = SCIPION_EP
    scriptsPath = pw.join('project', 'scripts')

    # Clean the project name as pyworkflow will do
    projectName = Project.cleanProjectName(projectName)

    # Create the project
    print("Creating project %s and waiting 2 secs before continuing." % projectName)
    createProjectScript = os.path.join(scriptsPath, 'create.py')

    # Escape ' in bash --> '"'"' (gluing)
    comment = comment.replace("'", "'\"'\"'")
    result = os.system("python -m %s  python %s %s %s - '%s'" % (scipion, createProjectScript, projectName, workflow, comment))

    #error creating the project... exit
    if result != 0:
        return

    # Wait 2 seconds to avoid activity
    time.sleep(2)

    if scheduleProject(argsList):

        # Schedule the project
        scheduleProjectScript = os.path.join(scriptsPath, 'schedule.py')
        print("Scheduling project %s and waiting 5 secs to settle things up." % projectName)
        subprocess.Popen(["python", "-m", scipion, "python", scheduleProjectScript, projectName])
        # Wait 5 seconds to avoid activity
        time.sleep(5)

    if launchGUI(argsList):

        print("Showing project %s" % projectName)
        # Launch scipion
        subprocess.Popen(["python", "-m", scipion, MODE_PROJECT, projectName])


def flag2Value(argsList, flag):
    # Remove the flag from sys.argsv
    value = getFlagArg(argsList, flag)

    if value:
        argsList.remove(flag)

    return value


def launchGUI(argsList):
    """Checks if project GUI has to be launched. Only if --noGUI param is
       found in the argument List it will return False"""
    return not getFlagArg(argsList, NOGUI_FLAG)


def scheduleProject(argsList):
    return not getFlagArg(argsList, NOSCHEDULE_FLAG)


def getFlagArg(argsList, flag):
    """Checks if a flag exists (True) or not (False)"""
    for arg in argsList:
        if flag == arg.lower():
            return True

    # Flag not found
    return False


def main():
    """ Resolves command line arguments for scipion template"""

    # Remove "template"
    argsList = sys.argv[1:]


    if len(argsList)>0 and argsList[0] == "-h":

        print("""
    Arguments: [TEMPLATE] [--noschedule] [--nogui] [project_name=NAME][cmdId1=VALUE1,...]
                    
    Shows all the templates available:
           1.- *.json.template files found in the config folder
           2.- templates provided by plugins.
           3.- templates at workflowhub
    If TEMPLATE (a path to a template or a template name) is provided, 
           then that template is used.
           
    If the template is dynamic 
    (see https://scipion-em.github.io/docs/release-3.0.0/docs/facilities/facilities-workflows.html#id2)
    then it will pop up a window to ask for the dynamic fields.
    
    It will create a project based on the template
    
    It will schedule the project unless --noschedule is passed or cancelled in the GUI
    
    It will show the project unless --nogui is passed or cancelled in the GUI
    
    Dynamic templates may have defined "cmdId" (a parameter name to refer to it from the command line)
    If so, dynamic template parameters are filled with the corresponding arguments. If all parameters are populated
    no window is shown and project is created straight away.""")
        return

    # Now, there 2 cases:
    # 1.- it comes with a template name (full path) or id (name)
    # 2.- is empty
    templateName = argsList[0] if argsList else None

    if templateName is not None:
        if "=" in templateName or FLAG_PARAM in templateName:
            templateName=None
        else:
            # Remove the name from the args, in case it is passed
            argsList = argsList[1:]

    templates = getTemplates(templateName)

    chosenTemplate = chooseTemplate(templates)

    if chosenTemplate is not None and resolveTemplate(chosenTemplate, argsList,
                                                      schedule=not flag2Value(argsList, NOSCHEDULE_FLAG),
                                                      showProject=not flag2Value(argsList, NOGUI_FLAG)):
        launchTemplate(argsList, chosenTemplate)


if __name__ == "__main__":
    main()
