package it.unibz.inf.data_pumper.columns.intervals;

import it.unibz.inf.data_pumper.columns.ColumnPumper;
import it.unibz.inf.data_pumper.core.main.exceptions.DebugException;
import it.unibz.inf.data_pumper.tables.MySqlDatatypes;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public abstract class Interval<T> {
    
    private static final String NULL = "\\N";
    
    protected T max;
    protected T min;
    
    protected long minEncoding;
    protected long maxEncoding;
    
    protected List<T> domain;
    private int domainIndex;
    private final MySqlDatatypes type;
    
    private String key;
    protected long nFreshsToInsert;
    
    // ColumnPumpers in the interval
    List<ColumnPumper<T>> intervalColumns;
    
    
    public Interval(String key, MySqlDatatypes type, long nValues, List<ColumnPumper<T>> intervalColumns){
        this.domain = null;
        this.domainIndex = 0;
        this.key = key;
        this.type = type;
        this.nFreshsToInsert = nValues;
        this.intervalColumns = intervalColumns;
    }
    
    public Interval(String key, MySqlDatatypes type, long minEncoding, long maxEncoding){
	this.domain = null;
	this.domainIndex = 0;
	this.key = key;
	this.type = type;
	this.nFreshsToInsert = maxEncoding - minEncoding;
	this.maxEncoding = maxEncoding;
	this.minEncoding = minEncoding;
	this.intervalColumns = new LinkedList<>();
    }
    
    public void setNFreshsToInsert(long nFreshsToInsert){
        this.nFreshsToInsert = nFreshsToInsert;
    }
    
    public long getNFreshsToInsert(){
        return this.nFreshsToInsert;
    }
    
    public String getKey(){
        return this.key;
    }
       
    /** This method has to be called whenever information held for the column can be released **/
    public void reset(){
        if( domain != null ) domain.clear();
        domainIndex = 0;
        System.gc();
    }
    
    public String getNextValue(){
        String result = domain.get(domainIndex++).toString();
        return result;
    }
        
//    public void setMaxValue(T max){
//        this.max = max;
//    }
    
    public T getMaxValue(){
        return max;
    }
    
//    public void setMinValue(T min){
//        this.min = min;
//    }
    
    public T getMinValue(){
        return min;
    }
    
    public void setDomain(List<T> newDomain){
        if( domain == null ){
            domain = newDomain;
        }
    }
    
    public String getNthInDomain(int n){
        String result = domain.get(n) == null ? NULL : domain.get(n).toString() ;
        if( result == null ){
            return NULL;
        }
        if( this.type.equals(MySqlDatatypes.BIGINT) ){
            String value1 = result.substring(0, result.indexOf("."));
            result = value1;
        }
        return result;
    }
    
    public MySqlDatatypes getType(){
        return this.type;
    }

    // Public updatable interface
    
    /**
     * 
     * CONTRACT: Each overrider should call <b>this</b> implementation
     * @param newMin
     */
    public void updateMinEncodingAndValue(long newMin){
	assert newMin >= 0 : "Negative encoding" +newMin+" is not allowed for "+this.toString();
    }
    public abstract void updateMaxEncodingAndValue(long newMax);
    
    public abstract long getMinEncoding();
    public abstract long getMaxEncoding();

    /**
     * It adapts the boundaries of <b>this</b> interval
     * @param toInsert
     * @return
     */
    public boolean adaptBounds(Interval<T> toInsert) {
        
        boolean killMe = false;
        
        long splitterMaxEncoding = toInsert.maxEncoding;
        long splitterMinEncoding = toInsert.minEncoding;
        
        // Assert
        if( splitterMaxEncoding <= splitterMinEncoding ){
            throw new DebugException("Assertion failed: splitterMaxEncoding <= splitterMinEncoding");
        }
        // Assert
        if( this.getMinEncoding() < splitterMinEncoding ){
            throw new DebugException("The splitter minimum is not equal to the minimum of the to-be-splitted interval");
        }
        
        if( this.getMaxEncoding() > splitterMaxEncoding ){
            // Shrink
            this.updateMinEncodingAndValue(splitterMaxEncoding);
        
            this.setNFreshsToInsert( this.getMaxEncoding() - this.getMinEncoding() );
        }
        else{
//            if(! (this.getMaxEncoding() == splitterMaxEncoding) ){
//        	throw new DebugException("Assertion failed: this.getMaxEncoding() == splitterMaxEncoding");
//            }
            // This interval HAS TO DIE!
            killMe/*please*/ = true;
            /*++evilLaugh*/
        }
        return killMe;
    }

    /**
     * 
     * @return The set of columns for which this interval is generating values
     */
    public Collection<? extends ColumnPumper<T>> getInvolvedColumnPumpers() {
        return Collections.unmodifiableCollection(this.intervalColumns);
    } 
    
    /**
     * Each interval represents the values in an intersection of one or more columns. This
     * method adds adds a column to the intersection of the columns. This method modifies the 
     * key of <b>this</b> interval with <b>this.getKey() + cP.getQualifiedName()</b>. 
     * 
     * @param cP
     */
    public void addInvolvedColumnPumper(ColumnPumper<T> cP){
        if( !this.intervalColumns.contains(cP) ){
            this.intervalColumns.add(cP);
            this.key = this.key + "---" + cP.getQualifiedName().toString();
        }
    }

    public abstract Interval<T> getCopyInstance();

    /**
     * It removes all the stable references to <b>this</b> interval.
     */
    public void suicide() {
        // For each involved column pumper, remove me from their lists of intervals
        for( ColumnPumper<T> cP : this.intervalColumns ){
            cP.removeIntervalOfKey(this.getKey());
        }   
    }
    
    public int sizeIntersection(){
        return this.getInvolvedColumnPumpers().size();
    }
    
    public Set<String> getKeysSet(){
        
        Set<String> result = new HashSet<String>();
        
        String[] splits = this.getKey().split("---");
        for( String s : splits ){
            result.add(s);
        }
        
        return result;
    } 
    
    
    @Override
    public String toString(){
        StringBuilder builder = new StringBuilder();
        builder.append(this.key + "\n");
        builder.append("Min Encoding = " + this.minEncoding + "\n");
        builder.append("Max encoding = " + this.maxEncoding + "\n");
        builder.append("nFreshsToInsert = " + this.nFreshsToInsert + "\n");
        return builder.toString();
    }

    public void intersect(Interval<?> that) {
	
	// Assert: same type
	assert this.getType().equals(that.getType()) : "Type mismatch between intervals";
	
	if( that.minEncoding > this.minEncoding ){
	    this.updateMinEncodingAndValue(that.minEncoding);
	}
	if( that.maxEncoding < this.maxEncoding ){
	    this.updateMaxEncodingAndValue(that.maxEncoding);
	}
    }
    
    /**
     * 
     * @return Is the interval empty?
     */
    public boolean isEmpty() {
	return this.minEncoding == this.maxEncoding;
    }
    
    public abstract void synchronizeMinMaxNFreshs();
};
