/*

   Derby - Class org.apache.derby.impl.load.ColumnInfo

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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.

 */

package org.apache.derby.impl.load;

import org.apache.derby.iapi.util.IdUtil;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Types;
import java.util.*;
import org.apache.derby.iapi.jdbc.EngineConnection;
import org.apache.derby.shared.common.reference.SQLState;
import org.apache.derby.shared.common.error.StandardException;
import org.apache.derby.shared.common.error.PublicAPI;
/**
 *	
 * This class provides supportto  create casting/conversions required to 
 * perform import. Import VTI  gives all the data in VARCHAR type becuase data
 * in the files is in CHAR format. There is no implicit cast availabile from
 * VARCHAR to some of the types. In cases where explicit casting is allowed, 
 * columns are casted with  explict cast to the type of table column; in case of 
 * double/real explicit casting is also not allowd , scalar fuction DOUBLE is
 * used in those cases.
 *  
 */
class ColumnInfo {

	private ArrayList<String> vtiColumnNames ;
    private ArrayList<String> insertColumnNames;
    private ArrayList<String> columnTypes ;
    private ArrayList<Integer> jdbcColumnTypes;
	private int noOfColumns;
	private ArrayList columnPositions;
	private boolean createolumnNames = true;
	private int expectedNumberOfCols ; //number of Columns that are suppose
                                       // to be in the file to imported  
	private Connection conn;
	private String tableName;
	private String schemaName;
        private String[] headerColumnNames;
    private HashMap<String,String> udtClassNames;

	/**
	 * Initialize the column type and name  information
	 * @param conn  - connection to use for metadata queries
	 * @param sName - table's schema
	 * @param tName - table Name
	 * @param insertColumnList - comma seperared insert statement column list 
	 * @param  vtiColumnIndexes - Indexes in the file
	 * @param  vtiColumnPrefix - Prefix to use to generate column names to select from VTI
	 * @exception Exception on error 
	 */
	public ColumnInfo(Connection conn,
					  String sName, 
					  String tName,
					  String insertColumnList, 
					  String vtiColumnIndexes,
					  String vtiColumnPrefix,
					  String[] headerColumnNames)
		throws SQLException 
	{

		vtiColumnNames = new ArrayList<String>(1);
		insertColumnNames = new ArrayList<String>(1);
		columnTypes = new ArrayList<String>(1);
        jdbcColumnTypes = new ArrayList<Integer>(1);
        udtClassNames = new HashMap<String,String>();
		noOfColumns = 0;
		this.conn = conn;
		this.headerColumnNames=headerColumnNames;
        if (sName == null) {
            // Use the current schema if no schema is specified.
            sName = ((EngineConnection) conn).getCurrentSchemaName();
        }

		this.schemaName = sName;
		this.tableName =  tName;

		if(insertColumnList!=null)
		{
			//break the comma seperated column list and initialze column info
			//eg: C2 , C1 , C3
			StringTokenizer st = new StringTokenizer(insertColumnList , ",");
			while (st.hasMoreTokens()) 
			{
				String columnName = (st.nextToken()).trim();
				if(!initializeColumnInfo(columnName))
				{
					if(tableExists())
						throw  LoadError.invalidColumnName(columnName);
					else
					{
						String entityName = (schemaName !=null ? 
											 schemaName + "." + tableName :tableName); 
						throw LoadError.tableNotFound(entityName);
					}
				}
			}
		}else
		{
			//All columns in the table
			if(!initializeColumnInfo(null))
			{
				String entityName = (schemaName !=null ? 
									 schemaName + "." + tableName :tableName); 
				throw LoadError.tableNotFound(entityName);
			}
		}
		
				
		//break the comma seperated column indexes for import file give by the user
		//eg: "1, 3, 5, 7"
		if(vtiColumnIndexes !=null)
		{
			
			StringTokenizer st = new StringTokenizer(vtiColumnIndexes, ",");
			while (st.hasMoreTokens()) 
			{
				int cIndex;
				String columnIndex = (st.nextToken()).trim();
				if("\"".equals(columnIndex.substring(0,1))){
					
					cIndex = readHeaders((columnIndex.replace('"', ' ')).trim());
					vtiColumnNames.add(vtiColumnPrefix + cIndex);

				}
				else{
					
					vtiColumnNames.add(vtiColumnPrefix + columnIndex);
					cIndex = Integer.parseInt(columnIndex );
				}
				if(cIndex > expectedNumberOfCols )
					expectedNumberOfCols= cIndex ;
			}

		}


		//if column indexes are not specified  ; create names for all collumns requested
		if(vtiColumnNames.size() < 1)
		{
			for(int index = 1 ; index <= noOfColumns; index++)
			{
				vtiColumnNames.add(vtiColumnPrefix + index);
			}
			expectedNumberOfCols = noOfColumns ;
		}
	}

	//Check for matchng patterns of column names
	private int readHeaders(String columnPattern) 
		throws SQLException
	{
	    	if(headerColumnNames!=null){
		
			for(int i=0;i<headerColumnNames.length;i++){
				if(headerColumnNames[i].equals(columnPattern)){
					return i+1;
				}
 			}
				
	       		throw PublicAPI.wrapStandardException(StandardException.newException
				(SQLState.LANG_INVALID_INPUT_COLUMN_NAME, columnPattern));
		}
		throw PublicAPI.wrapStandardException(StandardException.newException
				(SQLState.LANG_INVALID_NUMBEROF_HEADER_LINES));
	}

	private boolean initializeColumnInfo(String columnPattern)
		throws SQLException
	{
		DatabaseMetaData dmd = conn.getMetaData();
		ResultSet rs = dmd.getColumns(null, 
									  schemaName,
									  tableName,
									  columnPattern);
		boolean foundTheColumn=false;
		while (rs.next())
		{

			// 4.COLUMN_NAME String => column name
			String columnName = rs.getString(4);

			// 5.DATA_TYPE short => SQL type from java.sql.Types
			short dataType = rs.getShort(5);

			// 6.TYPE_NAME String => Data source dependent type name
			String typeName = rs.getString(6);

			
			// 7.COLUMN_SIZE int => column size. For char or date types
			// this is the maximum number of characters, for numeric or
			// decimal types this is precision.
			int columnSize = rs.getInt(7);

			// 9.DECIMAL_DIGITS int => the number of fractional digits
			int decimalDigits = rs.getInt(9);

			// 10.NUM_PREC_RADIX int => Radix (typically either 10 or 2)
			int numPrecRadix = rs.getInt(10);
			foundTheColumn = true;
			if(importExportSupportedType(dataType))
			{

				insertColumnNames.add(columnName);
				String sqlType = typeName + getTypeOption(typeName , columnSize , columnSize , decimalDigits);
				columnTypes.add(sqlType);
                jdbcColumnTypes.add((int) dataType);
				noOfColumns++;

                if ( dataType == java.sql.Types.JAVA_OBJECT )
                {
                    udtClassNames.put( "COLUMN" +  noOfColumns, getUDTClassName( dmd, typeName ) );
                }
			}else
			{
				rs.close();
				throw
					LoadError.nonSupportedTypeColumn(columnName,typeName);
			}

		}

		rs.close();
		return foundTheColumn;
	}

    // look up the class name of a UDT
    private String getUDTClassName( DatabaseMetaData dmd, String sqlTypeName )
        throws SQLException
    {
        String className = null;
        
        try {
            // special case for system defined types
            if ( sqlTypeName.charAt( 0 ) != '"' ) { return sqlTypeName; }

            String[] nameParts = IdUtil.parseMultiPartSQLIdentifier( sqlTypeName );

            String schemaName = nameParts[ 0 ];
            String unqualifiedName = nameParts[ 1 ];

            ResultSet rs = dmd.getUDTs( null, schemaName, unqualifiedName, new int[] { java.sql.Types.JAVA_OBJECT } );

            if ( rs.next() )
            {
                className = rs.getString( 4 );
            }
            rs.close();
        }
        catch (Exception e) { throw LoadError.unexpectedError( e ); }

        if ( className == null ) { className = "???"; }
        
        return className;
    }


	//return true if the given type is supported by import/export
	public  static final boolean importExportSupportedType(int type){

		return !(type == java.sql.Types.BIT ||
				 type == java.sql.Types.OTHER ||
                 type == Types.SQLXML );
	}


	private String getTypeOption(String type , int length , int precision , int scale)
	{

			if ((type.equals("CHAR") ||
				 type.equals("BLOB") ||
				 type.equals("CLOB") ||
				 type.equals("VARCHAR")) && length != 0)
			{
				 return "(" + length + ")";
			}

			if (type.equals("FLOAT")  && precision != 0)
				return  "(" + precision + ")";

			//there are three format of decimal and numeric. Plain decimal, decimal(x)
			//and decimal(x,y). x is precision and y is scale.
			if (type.equals("DECIMAL") ||
				type.equals("NUMERIC")) 
			{
				if ( precision != 0 && scale == 0)
					return "(" + precision + ")";
				else if (precision != 0 && scale != 0)
					return "(" + precision + "," + scale + ")";
				else if(precision == 0 && scale!=0)
					return "(" + scale + ")";
			}

			if ((type.equals("DECIMAL") ||
				 type.equals("NUMERIC")) && scale != 0)
				return "(" + scale + ")";

			//no special type option
			return "";
	}

    /**
     * Get the column type names.
     */
    public String getColumnTypeNames()
        throws Exception
    {
        // we use the object serializer logic
        return ExportAbstract.stringifyObject( columnTypes );
    }

    /**
     * Get the class names of udt columns as a string.
     */
    public String getUDTClassNames()
        throws Exception
    {
        // we use the object serializer logic
        return ExportAbstract.stringifyObject( udtClassNames );
    }

	/*
	 * Returns a  string of columns with proper casting/conversion
	 * to be used to select from import VTI.
	 */
	public String getColumnNamesWithCasts()
	{
		StringBuffer sb = new StringBuffer();
		boolean first = true;
		int noOfVtiCols =  vtiColumnNames.size();
		for(int index = 0 ; index < noOfColumns && index < noOfVtiCols; index++)
		{
			if(!first)
				sb.append(", ");
			else
				first = false;
			String type = (String) columnTypes.get(index);
			String columnName = (String) vtiColumnNames.get(index);
		   
			if(type.startsWith("SMALLINT") ||
			   type.startsWith("INTEGER") ||
			   type.startsWith("DECIMAL") ||
			   type.startsWith("BIGINT") ||
			   type.startsWith("NUMERIC"))							  
			{
				//these types require explicit casting
				sb.append(" cast" + "(" + columnName + " AS " + type + ") "); 

			}else
			{
				//if it is DOUBLE use scalar DOUBLE function no explicit casting allowed
				if(type.startsWith("DOUBLE"))
				{
					sb.append(" DOUBLE" + "(" + columnName + ") ");

				}else
				{
					//REAL: use DOUBLE function to convert from string and the cast to REAL
					if(type.startsWith("REAL"))
					{
						sb.append("cast" + "(" + 
								  " DOUBLE" + "(" + columnName + ") " + 
								  " AS " +  "REAL" + ") ");
					}else
					{
						//all other types does  not need any special casting
						sb.append(" " + columnName +  " "); 
					}
				}

			}
		}

		//there is no column info available
		if(first)
			return " * ";
		else
			return sb.toString();
	}

	/* returns comma seperated column Names delimited by quotes for the insert 
     * statement
	 * eg: "C1", "C2" , "C3" , "C4" 
	 */
	public String getInsertColumnNames()
	{
		StringBuffer sb = new StringBuffer();
		boolean first = true;
		for(int index = 0 ; index < noOfColumns; index++)
		{
			if(!first)
				sb.append(", ");
			else
				first = false;
            // Column names can be SQL reserved words, or they can contain
            // spaces and special characters, so it is necessary delimit them
            // for insert to work correctly.
            String name = (String) insertColumnNames.get(index);
            sb.append(IdUtil.normalToDelimited(name));
		}
	
		//there is no column info available
		if(first)
			return null;
		else
			return sb.toString();
	}

	/*
	  Returns number of columns expected to be in  the file from the user input paramters.
	 */
	public int getExpectedNumberOfColumnsInFile()
	{
		return expectedNumberOfCols;
	}

	//Return true if the given table exists in the database
	private boolean tableExists() throws SQLException
	{
		DatabaseMetaData dmd = conn.getMetaData();
		ResultSet rs = dmd.getTables(null, schemaName, tableName, null);
		boolean foundTable = false;
		if(rs.next())
		{
			//found the entry
			foundTable = true;
		}
		
		rs.close();
		return foundTable;
	}


    /*
     * Returns the the expected vti data column types in a String format. 
     * Format : (COLUMN NAME : TYPE [, COLUMN NAME : TYPE]*)
     * eg: COLUMN1:1 (java.sql.Types.CHAR) , COLUMN2: -1(LONGVARCHAR) , 
     * COLUMN3 : 2004 (BLOB)
     */
	public String getExpectedVtiColumnTypesAsString() {

        StringBuffer vtiColumnTypes = new StringBuffer();
        // expected types of data in the import file, based on 
        // the how columns in the data file are  mapped to 
        // the table  columns. 
        boolean first = true;
        for (int i =0 ; i < noOfColumns && i < vtiColumnNames.size(); i++) {
            if (first) 
                first = false;
            else
                vtiColumnTypes.append(",");

            vtiColumnTypes.append(vtiColumnNames.get(i) + ":" + 
                                  jdbcColumnTypes.get(i));
        }   

		if(first) {
            // there is no information about column types.
			return null;
        }
		else
			return vtiColumnTypes.toString();
	}


    /*
     * Get the expected vti data column types. This information was 
     * earlier passed as a string to the vti. This routine extracts the 
     * information from the string.
     * @param columnTypesStr  import data column type information , 
     *                        encoded as string. 
     * @param noOfColumns     number of columns in the import file.
     * 
     * @see getExpectedVtiColumnTypesAsString()
     */
    public static int[] getExpectedVtiColumnTypes(String columnTypesStr, 
                                                  int noOfColumns) 
    {
        // extract the table column types. Break the comma seperated 
        // column types into java.sql.Types int values from the columnTypes 
        // string that got passed to the import VTI.

        //eg: COLUMN1:1 (java.sql.Types.CHAR) , COLUMN2: -1(LONGVARCHAR) , 
        //COLUMN3 : 2004 (BLOB)

        int[] vtiColumnTypes = new int[noOfColumns];

        // expected column type information is only available 
        // for the columns that are being imported from the file.
        // columns type information is not required when 
        // a column in the data file is not one of the 
        // imported column, just assume they are of VARCHAR type. 
        
        for (int i = 0 ; i < noOfColumns ; i++)
            vtiColumnTypes[i] = java.sql.Types.VARCHAR;

        StringTokenizer st = new StringTokenizer(columnTypesStr , ",");

        while (st.hasMoreTokens()) 
        {
            String colTypeInfo = (st.nextToken()).trim();
            int colTypeOffset = colTypeInfo.indexOf(":");

            // column names format is "COLUMN" + columnNumner
            int colIndex = Integer.parseInt(colTypeInfo.substring(6, colTypeOffset));
            int colType = Integer.parseInt(colTypeInfo.substring(colTypeOffset+1));

            // column numbers start with 1. Check if user by mistake has 
            // specified a column number that is large than than the 
            // number of columns exist in the file, if that is the case
            // don't assign the type.
            if (colIndex <=  noOfColumns) 
                vtiColumnTypes[colIndex-1] = colType;
            
        }
        
        return vtiColumnTypes;
    }


    /*
     * Get the expected vti column type names. This information was 
     * passed earlier as a string to the vti. This routine extracts the 
     * information from the string.
     * @param columnTypeNamesString  import data column type information, encoded as string. 
     * @param noOfColumns     number of columns in the import file.
     * 
     * @see getColumnTypeNames()
     */
    public static String[] getExpectedColumnTypeNames
        ( String columnTypeNamesString, int noOfColumns )
        throws Exception
    {
        ArrayList list = (ArrayList)
                ImportAbstract.destringifyObject( columnTypeNamesString );

        String[] retval = new String[ list.size() ];

        for (int i = 0; i < retval.length; i++) {
            retval[i] = (String) list.get(i);
        }

        return retval;
    }

    /*
     * Get the expected classes bound to UDT columns. This information was 
     * passed earlier as a string to the vti. This routine extracts the 
     * information from the string.
     * @param stringVersion The result of calling toString() on the original HashMap<String><String>.
     * @return a HashMap<String><Class> mapping column names to their udt classes
     * 
     * @see initializeColumnInfo()
     */
    public static HashMap getExpectedUDTClasses( String stringVersion )
        throws Exception
    {
        // deserialize the original HashMap<String><String>
        HashMap stringMap = deserializeHashMap( stringVersion );

        if ( stringMap == null ) { return null; }
        
        HashMap<String,Class<?>> retval = new HashMap<String,Class<?>>();
        Iterator entries = stringMap.entrySet().iterator();

        while ( entries.hasNext() )
        {
            Map.Entry entry = (Map.Entry)entries.next();
            String columnName = (String) entry.getKey();
            String className = (String) entry.getValue();

            Class<?> classValue = Class.forName( className );

            retval.put( columnName, classValue );
        }

        return retval;
    }
    
    /*
     * Deserialize a HashMap produced by ExportAbstract.stringifyObject()
     */
    public static HashMap deserializeHashMap( String stringVersion )
        throws Exception
    {
        if ( stringVersion == null ) { return null; }

        HashMap retval = (HashMap) ImportAbstract.destringifyObject( stringVersion );

        return retval;
    }
    
}





