package it.unibz.inf.data_pumper.columns;

/*
 * #%L
 * dataPumper
 * %%
 * Copyright (C) 2014 Free University of Bozen-Bolzano
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.util.LinkedList;
import java.util.List;

import it.unibz.inf.data_pumper.columns.exceptions.BoundariesUnsetException;
import it.unibz.inf.data_pumper.columns.exceptions.ValueUnsetException;
import it.unibz.inf.data_pumper.columns.intervals.Interval;
import it.unibz.inf.data_pumper.columns_cluster.ColumnsCluster;
import it.unibz.inf.data_pumper.columns_cluster.ColumnsClusterImpl;
import it.unibz.inf.data_pumper.connection.DBMSConnection;
import it.unibz.inf.data_pumper.tables.MySqlDatatypes;
import it.unibz.inf.data_pumper.tables.Schema;
import it.unibz.inf.vig_mappings_analyzer.core.utils.QualifiedName;

public abstract class ColumnPumper<T> extends Column implements ColumnPumperInterface<T>{

    private boolean fixed;
    private float duplicateRatio;
    private float nullRatio;
    private long numRowsToInsert;
    protected long numFreshsToInsert;
    protected long numDupsToInsert;
    protected long numNullsToInsert;

    private boolean duplicateRatioSet;
    private boolean nullRatioSet;
    private boolean numRowsToInsertSet;

    protected boolean numDupsNullRowsSet;

    protected CyclicGroupGenerator generator;

    protected ColumnsCluster<T> cluster;

    private long generatedCounter = 0;

    // For general purposes
    public boolean visited;
    private double scaleFactor;
    private boolean notNull;

    public ColumnPumper(String name, MySqlDatatypes type, int index, Schema schema){ // index: index of the column
	super(name, type, index, schema);
	this.duplicateRatio = 0;
	this.nullRatio = 0;
	this.numRowsToInsert = 0;
	this.numFreshsToInsert = 0;
	this.numDupsToInsert = 0;

	duplicateRatioSet = false;
	nullRatioSet = false;
	numRowsToInsertSet = false;
	this.numDupsNullRowsSet = false;
	visited = false;
	this.fixed = false;
	this.notNull = false;
    }

    //	@Override
    /**
     * Get the closure under referredBy and refersTo
     * @return
     */
    public ColumnsCluster<T> getCluster() {
	return new ColumnsClusterImpl<T>(this);
    }

    @Override
    public void setDuplicatesRatio(float ratio) {
	this.duplicateRatio = ratio;
	this.duplicateRatioSet = true;
    }

    @Override
    public float getDuplicateRatio() {
	if( ! duplicateRatioSet ) throw new ValueUnsetException();
	float result = this.duplicateRatio;
	return result;
    }

    @Override
    public float getNullRatio() {
	if( ! nullRatioSet ) throw new ValueUnsetException();
	return this.nullRatio;
    }

    @Override
    public void setNullRatio(float ratio) {
	this.nullRatioSet = true;
	this.nullRatio = ratio;
    }

    @Override
    public void setNumRowsToInsert(int num) {
	this.numRowsToInsertSet = true;
	this.numRowsToInsert = num;
    }

    @Override
    public long getNumRowsToInsert() {
	if( ! numRowsToInsertSet ) throw new ValueUnsetException();		
	return this.numRowsToInsert;
    }

    protected void initNumDupsNullsFreshs() {

	this.numDupsToInsert = Math.round(this.getNumRowsToInsert() * this.getDuplicateRatio());
	this.numNullsToInsert = Math.round(this.getNumRowsToInsert() * this.getNullRatio());

	if( this.isFixed() ) {
	    this.setDuplicatesRatio(1);
	    this.numFreshsToInsert = this.getNumRowsToInsert() - this.numDupsToInsert - this.numNullsToInsert;
	    this.numFreshsToInsert = Math.round(this.numFreshsToInsert / this.scaleFactor);
	    this.numDupsToInsert = this.getNumRowsToInsert() - Math.round(this.numFreshsToInsert + this.numNullsToInsert);
	}
	else{
	    this.numFreshsToInsert = this.getNumRowsToInsert() - this.numDupsToInsert - this.numNullsToInsert;
	    if( this.numFreshsToInsert == 0 ){
		logger.info("Zero freshs to insert. Setting to at least one.");
		this.numDupsToInsert = 0; this.numNullsToInsert = 0;
		this.numFreshsToInsert = this.getNumRowsToInsert();
	    }
	}

	//	    checkIfTooManyDups();

	this.numDupsNullRowsSet = true;
	this.generator = new CyclicGroupGenerator(numFreshsToInsert);
    }


    //	private void checkIfTooManyDups(){
    //	    	    
    //	    // If the duplicates ratio has been alterated to 1
    //	    if( this.dupsCorrectionFactorApplied() ){
    //	    
    //		// Check
    //		if( this.numDupsToInsert + this.numNullsToInsert >= this.getNumRowsToInsert() ){
    //		    this.numDupsToInsert = this.getNumRowsToInsert() - this.numNullsToInsert - 1;
    //		    
    //		    assert this.numDupsToInsert > 0 : "Assertion Failed: No duplicate can be subtracted for column " + this.toString(); 
    //		} 
    //	    }
    //	}

    @Override
    public long getNumFreshsToInsert() {
	if( !this.numDupsNullRowsSet ) throw new ValueUnsetException();
	return numFreshsToInsert;
    }

    @Override
    public void incrementNumFreshs() {
	++this.numFreshsToInsert;
	Interval<T> interval = this.getIntervals().get(0);
	interval.setNFreshsToInsert(interval.getNFreshsToInsert() + 1);
	interval.updateMaxEncodingAndValue(interval.getMaxEncoding() + 1);
    }

    @Override
    public void decrementNumFreshs() {
	--this.numFreshsToInsert;
	Interval<T> interval = this.getIntervals().get(0);
	interval.setNFreshsToInsert(interval.getNFreshsToInsert() - 1);
	interval.updateMaxEncodingAndValue(interval.getMaxEncoding() - 1);
    }

    public abstract void createValues(Schema schema, DBMSConnection db);
    public abstract void createNValues(Schema schema, DBMSConnection db, long n);


    @Override
    public void generateValues(Schema schema, DBMSConnection db)
	    throws BoundariesUnsetException, ValueUnsetException{

	createValues(schema, db);

	this.generatedCounter = this.numRowsToInsert;
    }

    @Override
    public boolean generateNValues(Schema schema, DBMSConnection db, long n)
	    throws BoundariesUnsetException, ValueUnsetException{

	long nOld = n;

	n = this.numRowsToInsert - this.generatedCounter > n ? n : this.numRowsToInsert - this.generatedCounter;

	createNValues(schema, db, n);

	this.generatedCounter += n;

	return n != nOld; // true: All the values have been generated
    }

    /**
     * 
     * @return The number of values generated so far for <b>this</b> column.
     */
    protected long getGeneratedCounter(){
	return this.generatedCounter;
    }

    public List<ColumnPumper<T>> getRefersToClosure() {

	List<ColumnPumper<T>> result = new LinkedList<>();
	for( QualifiedName qN : this.referencesTo() ){
	    @SuppressWarnings("unchecked")
	    ColumnPumper<T> cP = (ColumnPumper<T>)DBMSConnection.getInstance().getSchema(qN.getTableName()).getColumn(qN.getColName());

	    if( !result.contains(cP) ) result.add(cP);

	    for( ColumnPumper<T> inClosure : cP.getRefersToClosure() ){
		if( !result.contains(inClosure) ){
		    result.add(inClosure);
		}
	    }
	}
	return result;
    }

    public List<ColumnPumper<T>> getReferredByClosure() {

	List<ColumnPumper<T>> result = new LinkedList<>();
	for( QualifiedName qN : this.referencedBy() ){
	    @SuppressWarnings("unchecked")
	    ColumnPumper<T> cP = (ColumnPumper<T>)DBMSConnection.getInstance().getSchema(qN.getTableName()).getColumn(qN.getColName());

	    if( !result.contains(cP) ) result.add(cP);

	    for( ColumnPumper<T> inClosure : cP.getReferredByClosure() ){
		if( !result.contains(inClosure) ){
		    result.add(inClosure);
		}
	    }
	}
	return result;
    }

    public void setScaleFactor(double percentage) {
	this.scaleFactor = percentage;
    }

    public boolean isFixed(){
	return this.fixed;
    }

    public void setFixed(){
	this.fixed = true;
    }

    public void setNotNull() {
	this.notNull = true;
    }
    
    public boolean isNotNull(){
	return this.notNull;
    }
};