package de.uni_hildesheim.sse.trans.in;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.io.FilenameUtils;

import de.uni_hildesheim.sse.model.cst.CSTSemanticException;
import de.uni_hildesheim.sse.model.cst.ConstraintSyntaxTree;
import de.uni_hildesheim.sse.model.cst.OCLFeatureCall;
import de.uni_hildesheim.sse.model.cst.Parenthesis;
import de.uni_hildesheim.sse.model.cst.VariablePool;
import de.uni_hildesheim.sse.model.validation.IvmlIdentifierCheck;
import de.uni_hildesheim.sse.model.varModel.Constraint;
import de.uni_hildesheim.sse.model.varModel.DecisionVariableDeclaration;
import de.uni_hildesheim.sse.model.varModel.Project;
import de.uni_hildesheim.sse.model.varModel.datatypes.BooleanType;
import de.uni_hildesheim.sse.model.varModel.datatypes.OclKeyWords;
import de.uni_hildesheim.sse.trans.convert.OptimizationParameter;
import de.uni_hildesheim.sse.utils.logger.EASyLoggerFactory;
import de.uni_hildesheim.sse.utils.logger.EASyLoggerFactory.EASyLogger;

/**
 * This reader reads <tt>*.model</tt> files, as they are specified by
 * <a href="https://github.com/ckaestne/kconfigreader">KConfigReader</a>.
 * These models must be pure boolean already.
 * @author El-Sharkawy
 *
 */
public class ModelReader implements IModelReader {
    private static final EASyLogger LOGGER = EASyLoggerFactory.INSTANCE.getLogger(ModelReader.class, "ModelReader");
    
    private BufferedReader reader;
    private Project model;
    private Map<String, DecisionVariableDeclaration> variables;
    private Set<String> parsedConstraints;
    private VariablePool varPool;
    private OptimizationParameter optimizations;
    
    /**
     * Loads a <a href="https://github.com/ckaestne/kconfigreader">KConfigReader</a> model file.
     * @param modelFile The model file to load.
     * @param optimizations Specification which optimizations shall be applied
     *     (some can already applied during parsing the model files)
     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file,
     * or for some other reason cannot be opened for reading.
     */
    public ModelReader(File modelFile, OptimizationParameter optimizations) throws FileNotFoundException {
        reader = new BufferedReader(new FileReader(modelFile));
        parsedConstraints = new HashSet<String>();
        this.optimizations = optimizations;
        
        String name = FilenameUtils.removeExtension(modelFile.getName());
        while (!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;
            }
        }
        
        model = new Project(name);
        variables = new HashMap<String, DecisionVariableDeclaration>();
        
        varPool = new VariablePool();
    }
    
    /**
     * Returns a {@link DecisionVariableDeclaration} for the specified name. If such a variables was not created yet,
     * a new variable will be created and added to the {@link #model}.
     * @param name The name of the desired variable.
     * @return A boolean {@link DecisionVariableDeclaration} with the specified name.
     */
    private DecisionVariableDeclaration getVariable(String name) {
        DecisionVariableDeclaration variable = variables.get(name);
        if (null == variable) {
            variable = new DecisionVariableDeclaration(name, BooleanType.TYPE, model);
            model.add(variable);
            variables.put(name, variable);
        }
        
        return variable;
    }
    
    /**
     * Reads a single line into a {@link ConstraintSyntaxTree}.
     * 
     * @param line the line to read
     * @param layer the recursion depth
     * @return a {@link ConstraintSyntaxTree} representing the line
     */
    ConstraintSyntaxTree parseLine(String line, int layer) {
        ConstraintSyntaxTree result = null;
        
        if (line.startsWith("def(") && line.indexOf(')') == line.lastIndexOf(')')) {
            String variableName = line.substring("def(".length(), line.length() - 1);
            result = varPool.obtainVariable(getVariable(variableName));
        } else if (line.startsWith("!def(") && line.indexOf(')') == line.lastIndexOf(')')) {
            String variableName = line.substring("!def(".length(), line.length() - 1);
            result = new OCLFeatureCall(varPool.obtainVariable(getVariable(variableName)), OclKeyWords.NOT);
        } else {
            
            int highestOperandPos = line.length();
            int highestOperandLevel = Integer.MAX_VALUE;
            String highestOperand = "";
            int currentLevel = 0;
            for (int i = 0; i < line.length(); i++) {
                char c = line.charAt(i);
                switch (c) {
                case '(':
                    currentLevel++;
                    break;
                case ')':
                    currentLevel--;
                    break;
                    
                case '&':
                    if (highestOperandLevel > currentLevel) {
                        highestOperand = OclKeyWords.AND;
                        highestOperandPos = i;
                        highestOperandLevel = currentLevel;
                    }
                    break;
                
                case '|':
                    if (highestOperandLevel > currentLevel) {
                        highestOperand = OclKeyWords.OR;
                        highestOperandPos = i;
                        highestOperandLevel = currentLevel;
                    }
                    break;
                
                default:
                    break;
                }
            }
            
            StringBuffer left = new StringBuffer();
            StringBuffer right = new StringBuffer();
            
            split(line, highestOperandPos, highestOperandLevel, left, right);
            
            ConstraintSyntaxTree leftSide = parseLine(left.toString(), layer + 1);
            ConstraintSyntaxTree rightSide = parseLine(right.toString(), layer + 1);
            result = new OCLFeatureCall(leftSide, highestOperand, rightSide);
            
            if (layer > 0) {
                result = new Parenthesis(result);
            }
        }
        
        return result;
    }
    
    /**
     * Splits the given line at the given operand position into a left and right part.
     * @param line The whole line
     * @param operandPos The position of the operand in the line
     * @param operandLevel The logic level of the operand (by counting brackets)
     * @param left A {@link StringBuffer} where the left side of the operand will be written to
     * @param right A {@link StringBuffer} where the right side of the operand will be written to
     */
    private void split(String line, int operandPos, int operandLevel, StringBuffer left, StringBuffer right) {
        left.append(line.substring(0, operandPos));
        right.append(line.substring(operandPos + 1, line.length()));
        int currentLevel = 0;
        for (int i = 0; i < left.length(); i++) {
            char c = left.charAt(i);
            if ((c == '(' || c == ')') && currentLevel < operandLevel) {
                left.replace(i, i + 1, "");
            }
            if (c == '(') {
                currentLevel++;
            }
            if (c == ')') {
                currentLevel--;
            }
        }
        currentLevel = 0;
        for (int i = right.length() - 1; i >= 0; i--) {
            char c = right.charAt(i);
            if ((c == '(' || c == ')') && currentLevel < operandLevel) {
                right.replace(i, i + 1, "");
            }
            if (c == ')') {
                currentLevel++;
            }
            if (c == '(') {
                currentLevel--;
            }
        }
    }
    
    /**
     * Parses the given file and converts it to a {@link Project}.
     * @return The parsed file as a {@link Project}.
     * @throws IOException If an I/O error occurs
     */
    public Project read() throws IOException {
        String line;
        while ((line = reader.readLine()) != null) {
            // Skip lines starting with a #
            if (line.charAt(0) != '#'
                && (!parsedConstraints.contains(line) || !optimizations.removeDuplicatedConstraints())) {
                
                // Avoid parsing the same constraint twice
                parsedConstraints.add(line);
                ConstraintSyntaxTree cst = parseLine(line, 0);
                Constraint constraint = new Constraint(model);
                try {
                    constraint.setConsSyntax(cst);
                    model.add(constraint);
                } catch (CSTSemanticException e) {
                    // TODO
                    LOGGER.exception(e);
                }
            }
        }
        
        return model;
    }
}
