/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.csu.sdmx.validation;

import cz.csu.sdmx.validation.model.ValidationResult;
import cz.csu.sdmx.Utils;
import cz.csu.sdmx.dao.LogicConditionsDao;
import cz.csu.sdmx.validation.model.LogicCondition;
import cz.csu.sdmx.validation.model.DataSet;
import cz.csu.sdmx.validation.model.Expression;
import cz.csu.sdmx.validation.model.Series;
import cz.csu.sdmx.validation.model.SeriesSet;
import cz.csu.sdmx.validation.model.ValidationError;
import cz.csu.sdmx.validation.model.data.DataFactory;
import cz.csu.sdmx.validation.model.data.SDMXData;
import cz.csu.sdmx.validation.model.dsds.Dsd;
import cz.csu.sdmx.validation.model.dsds.DsdFactory;
import cz.csu.sdmx.validation.xml.XmlValidator;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 *
 * @author themi
 */
//@Component
public class SDMXValidator {
    private LogicConditionsDao conditionsDao;
    
    private ValidationResult result;
    private List<Dsd> dsds;
    
    public SDMXValidator(LogicConditionsDao conditionsDao) {
        this.conditionsDao = conditionsDao;
        
        this.result = new ValidationResult();
        this.dsds = new ArrayList<>();
    }
    
    public ValidationResult getValidationResult(){
        return result;
    }

    public void addDsd(String name, InputStream is) {
        //check XML structure of DSD
        Document dsdDoc = XmlValidator.validate(name, is, result);
        //if structure has errors, add validation error 
        if(dsdDoc == null){
            ValidationError error = new ValidationError();
            error.setErrorCode("OBSTRUCTION");
            error.setText("Due to previous errors, dimensions neither conditions was validated.");
            error.setType(ValidationError.FATAL);
            result.addError(error);
            return;
        }
        
        //determine type of DSD
        Dsd dsd = DsdFactory.get(dsdDoc);
        //if type is not dererminer, add validation error
        if (dsd == null) {
            ValidationError error = new ValidationError();
            error.setErrorCode("Unsupported");
            error.setText("Unsuported DSD data format.");
            error.setFilename(name);
            error.setType(ValidationError.FATAL);
            result.addError(error);
            return;
        }
            
        dsds.add(dsd);
    }
    
    private SDMXData parseData(String dataName, InputStream dataIs){
        Document dataDoc = XmlValidator.validate(dataName, dataIs, result);
        if(dataDoc == null)
            return null;
        
        SDMXData data = DataFactory.get(dataDoc);
        if (data == null) {
            ValidationError error = new ValidationError();
            error.setErrorCode("Unsupported");
            error.setText("Unsuported SDMX data format.");
            error.setFilename(dataName);
            error.setType(ValidationError.FATAL);
            result.addError(error);
        }
        
        return data;
    }
    
    public void validate(String dataName, InputStream dataIs){
        SDMXData data = parseData(dataName, dataIs);
        //if during data instance creating is found error, add validation error
        if(result.hasErrors()){
            ValidationError error = new ValidationError();
            error.setErrorCode("OBSTRUCTION");
            error.setText("Due to previous errors, dimensions neither conditions was validated.");
            error.setType(ValidationError.FATAL);
            result.addError(error);
            return;
        }
        
        validateDataStructureAndDimensions(data);
        //if during validation is found error, add validation error
        if(result.hasErrors()){
            ValidationError error = new ValidationError();
            error.setErrorCode("OBSTRUCTION");
            error.setText("Due to previous errors, conditions was not validated.");
            error.setType(ValidationError.FATAL);
            result.addError(error);
            return;
        }
        
        //parse data values for all namespaces defined by DSD
        for(Dsd dsd : dsds){
            boolean contains = data.parseDatasets(dsd);
            //if datasets defined by DSD are found, add DSD namespace to result
            if(contains)
                result.addDsd(dsd.getURN());
        }
        
        //apply found conditions for all datasets
        for(DataSet dataSet : data.getDataSets()){
            List<LogicCondition> conditions = conditionsDao.getByDsdUrn(dataSet.getNamespace());
            if(conditions != null)
                validateConditions(dataSet, conditions, dataName);
        }
    }

    private void validateConditions(DataSet dataSet, List<LogicCondition> conditions, String dataName) {
        List<Series> seriesList = dataSet.getSeries();
        
        for(LogicCondition condition : conditions){
            //parse dimension parameters from condition string
            condition.parseParams();
            List<Expression> expressions = new ArrayList<>();
            
            //find matching series with parsed params
            List<SeriesSet> matchingSeries = findMatchingSeries(condition, seriesList);
            
            for(SeriesSet set : matchingSeries){
                //if found series with needed dimensions defined in params
                if(set.foundMatches()){
                    //try to parse expressions 
                    result.addErrors(set.parseExpressions());
                    //add all parsed expressions to list
                    expressions.addAll(set.getExpressions());                    
                }else{
                    //if not all matching series found, create error
                    result.addError(set.createNoMatchError(dataName, condition.getId()));
                }
            }
            
            //evaluate all expressions
            for(Expression expression : expressions)
                if(ExpressionEvaluator.evaluate(expression))
                    result.addError(expression.createNotValidError(dataName, condition.getId()));
        }
    }

    private List<SeriesSet> findMatchingSeries(LogicCondition condition, List<Series> seriesList) {
        List<SeriesSet> matchingSeries = new ArrayList<>();
        
        //get common dimensions that all matching series have to have
        List<String> commonDimensions = condition.getCommonDimensions();
        
        for(Series series : seriesList){
            //create unique string used for matching series 
            // [S1]
            String valuesString = createSeriesValString(commonDimensions, series); 
            
            boolean foundMatch = false;
            for(int j = 0; j < matchingSeries.size() && !foundMatch; j++){
                //if already exists series set with string [S1], add series to series set
                SeriesSet set = matchingSeries.get(j);
                if(set.matches(valuesString)){
                    set.addSeries(series);
                    foundMatch = true;
                }
            }
            
            //if not found matching series set, create a new one 
            if(!foundMatch){
                SeriesSet seriesSet = new SeriesSet(valuesString, condition);
                seriesSet.addSeries(series);
                matchingSeries.add(seriesSet);
            }
        }
        
        return matchingSeries;
    }
    
    private String createSeriesValString(List<String> commonDimensions, Series series){
        //join values of common dimensions to single string
        StringBuilder builder = new StringBuilder();
        for(String dimension : commonDimensions){
            String val = series.getAttrValue(dimension);
            if(val != null)
                builder.append(val).append(", ");
        }
        return builder.toString();
    }

    private void validateDataStructureAndDimensions(SDMXData data) {
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Source dataSource = new StreamSource(Utils.docToIS(data.getDocument()));
        try {
            Schema schema = schemaFactory.newSchema();
            Validator validator = schema.newValidator();
            validator.setResourceResolver(new SDMXResourceResolver(dsds));
            validator.setErrorHandler(new SimpleErrorHandler(data.getFilename(),result));
            validator.validate(dataSource);
        } catch (IOException | SAXException ex){
            //unknown exception thrown
            ValidationError error = new ValidationError();
            error.setErrorCode("Exception");
            error.setFilename(data.getFilename());
            error.setType(ValidationError.FATAL);
            error.setText(ex.getMessage() + "Fatal exception while validating structure.");
            result.addError(error);
        } catch(SDMXResourceResolver.NoResourceFoundException ex){
            //exception thrown when not found resource by resource resolver
            ValidationError error = new ValidationError();
            error.setErrorCode("ResourceNotFound");
            error.setFilename(data.getFilename());
            error.setType(ValidationError.FATAL);
            error.setText(ex.getMessage());
            result.addError(error);
        }
    }
}
