package de.uni_hildesheim.sse.trans.in.rsf;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import de.uni_hildesheim.sse.model.cst.CSTSemanticException;
import de.uni_hildesheim.sse.model.cst.ConstraintSyntaxTree;
import de.uni_hildesheim.sse.model.varModel.Constraint;
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.Enum;
import de.uni_hildesheim.sse.model.varModel.datatypes.IDatatype;
import de.uni_hildesheim.sse.model.varModel.datatypes.IntegerType;
import de.uni_hildesheim.sse.model.varModel.datatypes.RealType;
import de.uni_hildesheim.sse.model.varModel.datatypes.StringType;
import de.uni_hildesheim.sse.trans.in.AbstractReader;
import de.uni_hildesheim.sse.trans.in.ParserException;
import de.uni_hildesheim.sse.trans.in.ParserException.ParserExceptionType;
import de.uni_hildesheim.sse.utils.logger.EASyLoggerFactory;
import de.uni_hildesheim.sse.utils.logger.EASyLoggerFactory.EASyLogger;

/**
 * This reader reads <tt>*.rsf</tt> files, as they are created by the patched version of
 * <a href="https://github.com/ckaestne/undertaker">Undertaker's Dumpconf</a> tool.
 * These models must be pure boolean already.
 * @author El-Sharkawy
 *
 */
public class RSFReader extends AbstractReader {
    private static final EASyLogger LOGGER = EASyLoggerFactory.INSTANCE.getLogger(RSFReader.class, "RSFReader");
    
    private Map<String, RSFItem> items;
    private List<RSFCondition> conditions;
    private Map<String, RSFChoice> choices;

    /**
     * Specification, whether the resulting model shall be a pure boolean model.
     * <ul>
     * <li>true: Translation to pure boolean variables</li>
     * <li>false: Translation will include Tristates and Integers</li>
     * </ul>
     */
    private boolean booleanTranslation;
    
    /**
     * Loads a <a href="https://github.com/ckaestne/undertaker">Undertaker's Dumpconf</a> rsf file.
     * @param rsfFile A RSF file, created by Undertaker's dumpconf
     * @param booleanTranslation Specification, whether the resulting model shall be a pure boolean model.
     *   <ul>
     *   <li>true: Translation to pure boolean variables</li>
     *   <li>false: Translation will include Tristates and Integers</li>
     *   </ul>
     * @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 RSFReader(File rsfFile, boolean booleanTranslation) throws FileNotFoundException {
        super(rsfFile);
        this.booleanTranslation = booleanTranslation;
        items = new LinkedHashMap<String, RSFItem>();
        conditions = new ArrayList<RSFCondition>();
        choices = new HashMap<String, RSFChoice>();
    }
    
    /**
     * Returns the specified {@link RSFItem} for the given name. Creates a new {@link RSFItem} if not already known.
     * @param name The name of the KConfig item.
     * @return The {@link RSFItem} representing the KConfig item.
     */
    private RSFItem getItem(String name) {
        RSFItem item = items.get(name);
        if (null == item) {
            item = new RSFItem(name);
            items.put(name, item);
        }
        
        return item;
    }
    
    /**
     * Returns the specified {@link RSFItem} for the given name.
     * @param name The name of the KConfig item.
     * @return The {@link RSFItem} representing the KConfig item. <code>null</code> if not known.
     */
    RSFItem getItemNoCreate(String name) {
        return items.get(name);
    }

    @Override
    public Project read() throws IOException, ParserException {
        readItems();
        Project model = getModel();
        if (booleanTranslation) {
            createPureBooleanModel(model);
        } else {
            createNotBooleanModel(model);
        }
        return model;
    }

    /**
     * Creates a pure boolean model.
     * @param model An empty {@link Project} where variables and constraints shall be added to.
     * @throws ParserException ParserException If the input file could not be parsed completely.
     */
    private void createPureBooleanModel(Project model) throws ParserException {
        // 1. Add variables
        for (RSFItem item : items.values()) {
            switch(item.getDatatype()) {
            case BOOLEAN:
                getVariable(item.getName(), BooleanType.TYPE);
                break;
            case TRISTATE:
                getVariable(item.getName(), BooleanType.TYPE);
                getVariable(item.getName() + "_MODULE", BooleanType.TYPE);
                conditions.add(new RSFModuleCondition(item.getName(), item.getName() + "_MODULE"));
                break;
            case INTEGER:
            case HEX:
                // Ignore these variables here; they will be created depending on the comparisons found in the
                //  constraints
                break;
            case STRING:
                // This variable is true if the string is not empty
                // Other variables will be added depending on the comparisons found in the constraints 
                getVariable(item.getName(), BooleanType.TYPE);
                break;
            default:
                throw new ParserException(ParserExceptionType.NOT_SUPPORTED_DATATYPE);
            }
            
            // Add default condition if no prompt
            if (!item.hasPrompt() || item.getPromptCondition() != null) {
                RSFDefaultCondition defaultCondition = item.getDefaultCondition();
                if (defaultCondition != null) {
                    conditions.add(defaultCondition);
                } else {
                    // TODO
                    LOGGER.warn("No prompt and no default for varible: " + item.getName());
                }
            }
        }
        
        // 2. Add conditions
        for (RSFCondition condition : conditions) {
            List<ConstraintSyntaxTree> trees = condition.toPureBooleanConstraintSyntaxTree(this);
            for (ConstraintSyntaxTree tree : trees) {
                Constraint constraint = new Constraint(model);
                try {
                    constraint.setConsSyntax(tree);
                    model.add(constraint);
                } catch (CSTSemanticException e) {
                    LOGGER.exception(e);
                }
            }
        }
        
        // 3. Add choice conditions
        for (RSFChoice choice : choices.values()) {
            List<ConstraintSyntaxTree> trees = choice.getBooleanConstraintSyntaxTree(this);
            for (ConstraintSyntaxTree tree : trees) {
                Constraint constraint = new Constraint(model);
                try {
                    constraint.setConsSyntax(tree);
                    model.add(constraint);
                } catch (CSTSemanticException e) {
                    LOGGER.exception(e);
                }
            }
        }
    }
    
    /**
     * Creates a not pure boolean model, which will also include Tristate variables, Integers and so on.
     * @param model An empty {@link Project} where variables and constraints shall be added to.
     * @throws ParserException ParserException If the input file could not be parsed completely.
     */
    private void createNotBooleanModel(Project model) throws ParserException {
        // 1. Create Tristate type
        Enum tristate = new Enum("Tristate", model, "Undefined", "Defined", "Module");
        model.add(tristate);
        
        // 2. Add all Variables with correct datatype
        for (RSFItem item : items.values()) {
            IDatatype type = null;
            switch(item.getDatatype()) {
            case BOOLEAN:
                type = BooleanType.TYPE;
                break;
            case TRISTATE:
                type = tristate;
                break;
            case INTEGER:
                type = IntegerType.TYPE;
                break;
            case HEX:
                // TODO handle as enum
                type = RealType.TYPE;
                break;
            case STRING:
                type = StringType.TYPE;
                break;
            default:
                throw new ParserException(ParserExceptionType.NOT_SUPPORTED_DATATYPE);
            }
            getVariable(item.getName(), type);
        }
        
        // 3. Add conditions
        for (RSFCondition condition : conditions) {
            try {
                Constraint constraint = new Constraint(model);
                ConstraintSyntaxTree tree = condition.toNotBooleanConstraintSyntaxTree(this, tristate);
                if (tree != null) {
                    constraint.setConsSyntax(tree);
                    model.add(constraint);
                }
            } catch (CSTSemanticException e) {
                LOGGER.exception(e);
            }
        }
        
        // 4. TODO: Add choice conditions
    }

    /**
     * First step of the translation, reads the items of the RSF file.
     * @throws IOException If an I/O error occurs
     * @throws ParserException If the input file contains a field, which is not supported by the Reader.
     */
    private void readItems() throws IOException, ParserException {
        BufferedReader reader = getReader();
        String line;
        while ((line = reader.readLine()) != null) {
            // Skip lines starting with a #
            if (line.charAt(0) != '#') {
                String[] columns = line.split("\t");
                String fieldType = columns[0];
                String itemName = columns[1];
                //System.out.println(itemName);
                RSFItem item = getItem(itemName);
                switch (fieldType) {
                case "Item":
                    // Datatype in [2]
                    item.setDatatype(columns[2]);
                    break;
                case "Prompt":
                    // Condition in [2]
                    item.setPromptCondition(normalizeString(columns[2]));
                    break;
                case "Default":
                    // Default value in [2], condition in [3]
                    item.setDefaultCondition(
                        new RSFDefaultCondition(item, normalizeString(columns[2]), normalizeString(columns[3])));
                    break;
                case "HasPrompts":
                    // 0 or 1 in [2]
                    item.setHasPrompt(columns[2].equals("1"));
                    break;
                case "ItemSelects":
                    // other variable in [2], condition in [3]
                    conditions.add(new RSFItemSelectsCondition(itemName, normalizeString(columns[2]),
                        normalizeString(columns[3])));
                    break;
                case "Depends":
                    // condition in [2]
                    conditions.add(new RSFDependsCondition(itemName, columns[2]));
                    break;
                case "Choice":
                    // importance (?) in [2], datatype in [3]
                    item.setDatatype(columns[3]);
                    if (choices.get(itemName) == null) {
                        choices.put(itemName, new RSFChoice(itemName));
                    } else {
                        LOGGER.debug("Double decleratation of choice");
                        // TODO
                    }
                    break;
                case "ChoiceItem":
                    // related choice in [2]
                    RSFChoice choice = choices.get(columns[2]);
                    if (choice != null) {
                        choice.addChoiceItem(itemName);
                    } else {
                        LOGGER.debug("Missing choice declaration for ChoiceItem");
                        // TODO
                    }
                    break;
                case "Range":
                    // Range in [2], condition in [3]
                    break;
                default:
                    throw new ParserException(ParserExceptionType.NOT_SUPPORTED_FIELD);
                }
            }
        }
    }
    
    /**
     * Normalizes a given (constraint) string. It will:
     * <ul>
     * <li>Remove Surrounding quotes</li>
     * </ul>
     * @param element A constraint or other element which shall be normalized for later parsing operations.
     * @return The normalized <tt>element</tt>.
     */
    private static String normalizeString(String element) {
        // Removes surrounding quotes (if present)
        return element.replaceAll("^\"|\"$", "");
    }
}
