package de.uni_hildesheim.sse.trans.out;

import java.io.Writer;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import de.uni_hildesheim.sse.model.validation.IvmlIdentifierCheck;
import de.uni_hildesheim.sse.model.validation.IvmlValidationVisitor;
import de.uni_hildesheim.sse.model.varModel.AbstractVariable;
import de.uni_hildesheim.sse.model.varModel.Comment;
import de.uni_hildesheim.sse.model.varModel.Constraint;
import de.uni_hildesheim.sse.model.varModel.IvmlKeyWords;
import de.uni_hildesheim.sse.model.varModel.ModelElement;
import de.uni_hildesheim.sse.model.varModel.Project;
import de.uni_hildesheim.sse.model.varModel.datatypes.CustomDatatype;
import de.uni_hildesheim.sse.model.varModel.filter.ConstraintFinder;
import de.uni_hildesheim.sse.model.varModel.filter.DatatypeFinder;
import de.uni_hildesheim.sse.model.varModel.filter.DeclarationFinder;
import de.uni_hildesheim.sse.model.varModel.filter.DeclarationFinder.VisibilityType;
import de.uni_hildesheim.sse.model.varModel.filter.FilterType;
import de.uni_hildesheim.sse.trans.Main;
import de.uni_hildesheim.sse.utils.logger.EASyLoggerFactory;
import de.uni_hildesheim.sse.utils.logger.EASyLoggerFactory.EASyLogger;
import de.uni_hildesheim.sse.utils.messages.Message;
import de.uni_hildesheim.sse.utils.modelManagement.Version;
import de.uni_hildesheim.sse.utils.modelManagement.VersionFormatException;

/**
 * Writes/Saves a read Project in IVML syntax.
 * @author El-Sharkawy
 *
 */
public class IVMLWriter implements IModelWriter {
    
    private static final EASyLogger LOGGER = EASyLoggerFactory.INSTANCE.getLogger(IVMLWriter.class, Main.ID);
    private static final DateFormat DF = new SimpleDateFormat("dd.MM.yyyy");
    
    private de.uni_hildesheim.sse.persistency.IVMLWriter writer;
    private Project project;
    
    /**
     * Writes the given Project.
     * @param writer Writer which should be used for writing the output.
     * @param project The project which shall be saved.
     * @see #write(String)
     */
    public IVMLWriter(Writer writer, Project project) {
        this.writer = new de.uni_hildesheim.sse.persistency.IVMLWriter(writer);
        this.project = project;
    }

    /**
     * Writes the given project via the given writer.
     * It will first write all {@link de.uni_hildesheim.sse.model.varModel.DecisionVariableDeclaration}s
     * in alphabetical order, than all constraints are written.
     * @param comment An optional comment (e.g. information about the translated model), can be <tt>null</tt>.
     * @param version Optionally the version of the translated model. Can be <tt>null</tt>. If not <tt>null</tt>,
     *     it must be in {@link de.uni_hildesheim.sse.utils.modelManagement.Version#Version(String)} syntax.
     */
    public void write(String comment, String version) {
        // Store complete content of project (imports are not used in this project).
        DatatypeFinder typeFinder = new DatatypeFinder(project, FilterType.ALL, null);
        DeclarationFinder declfinder = new DeclarationFinder(project, FilterType.ALL, null);
        ConstraintFinder constFinder = new ConstraintFinder(project);
        
        // Clear old content
        project.clear();
        
        // 1. add comment & version
        if (null != comment) {
            if (!comment.startsWith("//") || comment.startsWith("    //")) {
                comment = "    // " + comment;
            }
            Date currentTime = new Date();
            comment += "    // Created on " + DF.format(currentTime) + IvmlKeyWords.LINEFEED;
            Comment cmt = new Comment(comment, project);
            project.add(cmt);
        }
        if (null != version) {
            try {
                Version ver = new Version(version);
                project.setVersion(ver);
            } catch (VersionFormatException e) {
                LOGGER.warn(e.getMessage());
            }
        }
        
        // 2. Re-add Custom Datatypes
        List<CustomDatatype> customTypes = typeFinder.getFoundDatatypes();
        for (int i = 0, n = customTypes.size(); i < n; i++) {
            project.add(customTypes.get(i));
        }
        
        // 3. re-add Declarations
        List<AbstractVariable> declarations = declfinder.getVariableDeclarations(VisibilityType.ALL);
        handleDeclarations(declarations);
        
        // 4. re-add all constraints
        List<Constraint> constraints = constFinder.getConstraints();
        for (Constraint constraint : constraints) {
            project.add(constraint);
        }
        
        // 5. check re-written model
        IvmlValidationVisitor validator = new IvmlValidationVisitor();
        project.accept(validator);
        if (validator.getErrorCount() == 0) {
            // 5. write model
            project.accept(writer);
        } else {
            StringBuffer errorMsg = new StringBuffer();
            for (int i = 0, n = validator.getMessageCount(); i < n; i++) {
                Message msg = validator.getMessage(i);
                errorMsg.append(msg.getStatus() + ": " + msg.getDescription());
            }
            LOGGER.error("Errors occurred during writing the model:\n" + errorMsg.toString());
        }
    }

    /**
     * Ensures that Declarations have an valid identifier and are sorted alphabetically.
     * @param declarations The declarations of this project.
     */
    private void handleDeclarations(List<AbstractVariable> declarations) {
        for (AbstractVariable decl : declarations) {
            String name = decl.getName();
            if (!IvmlIdentifierCheck.isValidIdentifier(name)) {
                // Replace whitespaces
                name = name.replaceAll("\\s", "_");
                // Replace "punctuation" signs, like !, #, <, ...
                name = name.replaceAll("\\p{Punct}", "_");
                // If name starts with a number, add _ at the beginning
                if (Character.isDigit(name.charAt(0))) {
                    name = "_" + name;
                }
                
                // Use reflection to overwrite name
                try {
                    Field field = ModelElement.class.getDeclaredField("name");
                    field.setAccessible(true);
                    field.set(decl, name);
                } catch (ReflectiveOperationException e) {
                    LOGGER.exception(e);
                } catch (SecurityException e) {
                    LOGGER.exception(e);
                }

            }
        }
        Collections.sort(declarations, new DeclarationsComparator());
        for (AbstractVariable decl : declarations) {
            project.add(decl);
        }
    }
}
