/*
 * Copyright (C) 2015 Jan Pokorsky
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */
package cz.cas.lib.proarc.common.workflow.profile;

import cz.cas.lib.proarc.common.i18n.BundleValue;
import cz.cas.lib.proarc.common.i18n.BundleValueMap;
import cz.cas.lib.proarc.common.object.ValueMap;
import cz.cas.lib.proarc.common.workflow.profile.ValueMapDefinition.ValueMapItemDefinition;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.util.ValidationEventCollector;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.apache.commons.io.FileUtils;
import org.xml.sax.SAXException;

/**
 * Reads actual configuration of workflow profiles.
 *
 * @author Jan Pokorsky
 */
public class WorkflowProfiles {

    private static final Logger LOG = Logger.getLogger(WorkflowProfiles.class.getName());
    private static WorkflowProfiles INSTANCE;

    private final File file;
    private long lastModified;
    private WorkflowDefinition profiles;

    public static WorkflowProfiles getInstance() {
        return INSTANCE;
    }

    public static void setInstance(WorkflowProfiles wp) {
        INSTANCE = wp;
    }

    /**
     * Creates a new workflow definition file if not exists.
     * @param target
     * @throws IOException error
     */
    public static void copyDefaultFile(File target) throws IOException {
        if (target.exists()) {
            return ;
        }
        FileUtils.copyURLToFile(WorkflowProfiles.class.getResource("workflow.xml"), target);
    }

    public WorkflowProfiles(File file) {
        if (file == null) {
            throw new NullPointerException();
        }
        this.file = file;
    }

    /**
     * Gets actual profiles or {@code null} if in case of an error.
     */
    public synchronized WorkflowDefinition getProfiles() {
        try {
            read();
            return profiles;
        } catch (JAXBException ex) {
            LOG.log(Level.SEVERE, file.toString(), ex);
            return null;
        }
    }

    public JobDefinition getProfile(WorkflowDefinition workflow, String name) {
        for (JobDefinition job : workflow.getJobs()) {
            if (job.getName().equals(name)) {
                return job;
            }
        }
        return null;
    }

    public TaskDefinition getTaskProfile(WorkflowDefinition workflow, String taskName) {
        for (TaskDefinition task : workflow.getTasks()) {
            if (task.getName().equals(taskName)) {
                return task;
            }
        }
        return null;
    }

    /**
     * Finds job's step.
     * @return the step or {@code null}
     */
    public static StepDefinition findStep(JobDefinition job, String stepName) {
        for (StepDefinition step : job.getSteps()) {
            if (stepName.equals(step.getTask().getName())) {
                return step;
            }
        }
        return null;
    }

    public ParamDefinition getParamProfile(TaskDefinition task, String paramName) {
        for (ParamDefinition paramDef : task.getParams()) {
            if (paramDef.getName().equals(paramName)) {
                return paramDef;
            }
        }
        return null;
    }

    public MaterialDefinition getMaterialProfile(WorkflowDefinition workflow, String materialName) {
        for (MaterialDefinition md : workflow.getMaterials()) {
            if (md.getName().equals(materialName)) {
                return md;
            }
        }
        return null;
    }

    public List<ValueMap> getValueMap(ValueMap.Context ctx) {
        WorkflowDefinition wd = getProfiles();
        if (wd == null) {
            return Collections.emptyList();
        }
        ArrayList<ValueMap> result = new ArrayList<ValueMap>();
        for (ValueMapDefinition vmd : wd.getValueMaps()) {
            if (vmd.getSource() == ValueMapSource.INTERNAL) {
                String id = vmd.getId();
                List<ValueMapItemDefinition> items = vmd.getItems();
                ArrayList<BundleValue> bitems = new ArrayList<BundleValue>();
                for (ValueMapItemDefinition vmitem : items) {
                    String key = vmitem.getKey() != null ? vmitem.getKey() : vmitem.getValue();
                    bitems.add(new BundleValue(key, vmitem.getValue()));
                }

                BundleValueMap bundleValueMap = new BundleValueMap();
                bundleValueMap.setMapId(id);
                bundleValueMap.setValues(bitems);
                result.add(bundleValueMap);
            }
        }
        result.add(getAllTaskValueMap(wd, ctx));
        return result;
    }

    private ValueMap<WorkflowItemView> getAllTaskValueMap(WorkflowDefinition wd, ValueMap.Context ctx) {
        String lang = ctx.getLocale().getLanguage();
        ArrayList<WorkflowItemView> taskList = new ArrayList<WorkflowItemView>(wd.getTasks().size());
        for (TaskDefinition task : wd.getTasks()) {
            taskList.add(new WorkflowItemView(task, lang));
        }
        Collections.sort(taskList, new WorkflowItemComparator(ctx.getLocale()));
        return new ValueMap<WorkflowItemView>(
                WorkflowProfileConsts.WORKFLOWITEMVIEW_TASKS_VALUEMAP, taskList);
    }

    /**
     * Gets a list of sorted job's task names. The order of tasks is driven by
     * the position of their step declaration in XML and by their blockers.
     */
    public List<String> getSortedTaskNames(JobDefinition job) {
        List<String> sortedTasks = new ArrayList<String>(job.getSteps().size());
        // tasks sorted by step declaration; taskName->blockers
        Map<String, Set<String>> taskDeps = new LinkedHashMap<String, Set<String>>();
        for (StepDefinition step : job.getSteps()) {
            String stepTaskName = step.getTask().getName();
            Set<String> blockers = taskDeps.get(stepTaskName);
            if (blockers == null) {
                blockers = new HashSet<String>();
                taskDeps.put(stepTaskName, blockers);
            } else {
                throw new IllegalStateException("A duplicate step declaration: "
                        + job.getName() + ", " + stepTaskName);
            }
            for (BlockerDefinition blocker : step.getBlockers()) {
                if (stepTaskName.equals(blocker.getTask().getName())) {
                    // short cycle, ignore
                    continue;
                }
                blockers.add(blocker.getTask().getName());
            }
        }
        while (!taskDeps.isEmpty()) {
            List<String> shakedOffDeps = shakeOffUnblockedTasks(taskDeps);
            if (shakedOffDeps.isEmpty() && !taskDeps.isEmpty()) {
                throw new IllegalStateException("There must be a cycle: "
                        + job.getName() + ", " + taskDeps);
            }
            sortedTasks.addAll(shakedOffDeps);
        }
        return sortedTasks;
    }

    /**
     * Removes unblocked tasks from the map and returns their ordered names.
     */
    private List<String> shakeOffUnblockedTasks(Map<String, Set<String>> taskDeps) {
        List<String> sortedTasks = new ArrayList<String>();
        for (Entry<String, Set<String>> taskEntry : taskDeps.entrySet()) {
            String taskName = taskEntry.getKey();
            Set<String> blockers = taskEntry.getValue();
            for (Iterator<String> it = blockers.iterator(); it.hasNext();) {
                if (!taskDeps.containsKey(it.next())) {
                    it.remove();
                }
            }
            if (blockers.isEmpty()) {
                sortedTasks.add(taskName);
            }
        }
        // remove resolved tasks
        for (String sortedTask : sortedTasks) {
            taskDeps.remove(sortedTask);
        }
        return sortedTasks;
    }

    private synchronized void setProfiles(WorkflowDefinition profiles, long time) {
        if (time != lastModified) {
            this.profiles = profiles;
            this.lastModified = time;
        }
    }

    private void read() throws JAXBException {
        long currentTime = file.lastModified();
        if (currentTime == lastModified) {
            return ;
        }
        Unmarshaller unmarshaller = getUnmarshaller();
        ValidationEventCollector errors = (ValidationEventCollector) unmarshaller.getEventHandler();
        WorkflowDefinition fetchedWf = null;
        try {
            WorkflowDefinition wf = (WorkflowDefinition) unmarshaller.unmarshal(file);
            if (!errors.hasEvents()) {
                readCaches(wf);
                fetchedWf = wf;
            }
        } catch (UnmarshalException ex) {
            if (!errors.hasEvents()) {
                throw ex;
            }
        } finally {
            setProfiles(fetchedWf, currentTime);
        }
        if (errors.hasEvents()) {
            StringBuilder err = new StringBuilder();
            for (ValidationEvent event : errors.getEvents()) {
                err.append(event).append('\n');
            }
            throw new JAXBException(err.toString());
        }
    }

    private void readCaches(WorkflowDefinition wf) {
        if (wf == null) {
            return ;
        }
        for (JobDefinition job : wf.getJobs()) {
            job.setTaskNamesSortedByBlockers(Collections.unmodifiableList(getSortedTaskNames(job)));
        }
    }

    private Unmarshaller getUnmarshaller() throws JAXBException {
        JAXBContext jctx = JAXBContext.newInstance(WorkflowDefinition.class);
        Unmarshaller unmarshaller = jctx.createUnmarshaller();
        SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        URL schemaUrl = WorkflowDefinition.class.getResource("workflow.xsd");
        Schema schema = null;
        try {
            schema = sf.newSchema(new StreamSource(schemaUrl.toExternalForm()));
        } catch (SAXException ex) {
            throw new JAXBException("Missing schema workflow.xsd!", ex);
        }
        unmarshaller.setSchema(schema);
        ValidationEventCollector errors = new ValidationEventCollector() {

            @Override
            public boolean handleEvent(ValidationEvent event) {
                super.handleEvent(event);
                return true;
            }

        };
        unmarshaller.setEventHandler(errors);
        return unmarshaller;
    }

}