/*
 * 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.ode.bpel.extvar.jdbc;

import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import javax.sql.DataSource;
import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.GUID;
import org.apache.ode.utils.ISO8601DateParser;
import org.apache.ode.bpel.evar.ExternalVariableModuleException;
import org.apache.ode.bpel.evar.ExternalVariableModule.Locator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Configuration for an external variable.
 * 
 * @author Maciej Szefler <mszefler at gmail dot com>
 */
class DbExternalVariable {
    private static final Log __log = LogFactory.getLog(DbExternalVariable.class);

	private static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";

	EVarId evarId;

	DataSource dataSource;

	final ArrayList<Column> _columns = new ArrayList<Column>();

	private final HashMap<String, Column> _colmap = new HashMap<String, Column>();

	final ArrayList<Column> _keycolumns = new ArrayList<Column>();

	final ArrayList<Column> _inscolumns = new ArrayList<Column>();

	final ArrayList<Column> _updcolumns = new ArrayList<Column>();

	InitType _initType = InitType.update_insert;

	public String[] _autoColNames;

	String select;

	String insert;

	String update;

	String table;

    String schema; // table schema

	/** Does the database support retrieval of generated keys? */
	boolean generatedKeys;

	DbExternalVariable(EVarId evar, DataSource ds) {
		this.evarId = evar;
		this.dataSource = ds;
	}

	Column getColumn(String key) {
		return _colmap.get(key);
	}

	void addColumn(Column c) {
		c.idx = _columns.size();
		_colmap.put(c.name, c);
		_columns.add(c);
		if (c.key) {
			_keycolumns.add(c);
			_autoColNames = new String[_keycolumns.size()];
			for (int i = 0; i < _autoColNames.length; ++i)
				_autoColNames[i] = _keycolumns.get(i).colname;
		}
		createSelect();
		createInsert();
		createUpdate();
	}

	public int numColumns() {
		return _columns.size();
	}

	/**
	 * Create a key from a locator.
	 */
	RowKey keyFromLocator(Locator locator) throws ExternalVariableModuleException {
		RowKey rc = new RowKey();
		parseXmlRow(rc, locator.reference);
		
        // Put in the static goodies such as pid/iid
		for (Column c : rc._columns) {
			switch (c.genType) {
			case iid:
			case pid:
                rc.put(c.name, c.getValue(c.name, null, null, locator.iid));
				break;
			}
		}
		
		return rc;
	}

	private void createSelect() {
		StringBuilder sb = new StringBuilder("select ");
		boolean first = true;
		for (Column c : _columns) {
			if (!first) {
				sb.append(',');
			}
			first = false;

			sb.append(c.colname);
		}
		sb.append(" from " + table);
		if (_keycolumns.size() > 0) {
			sb.append(" where ");
			first = true;

			for (Column kc : _keycolumns) {
				if (!first) {
					sb.append(" and ");
				}
				first = false;

				sb.append(kc.colname);
				sb.append(" = ?");
			}
			select = sb.toString();

		} else {
			select = null;
		}
	}

	private void createUpdate() {
		_updcolumns.clear();
		StringBuilder sb = new StringBuilder("update ");
		sb.append(table);
		sb.append(" set ");
		boolean first = true;
		for (Column c : _columns) {
			// Don't ever update keys or sequences or create time stamps
            if (c.genType == GenType.sequence || c.key || c.genType == GenType.ctimestamp)
				continue;

			if (!first)
				sb.append(", ");
			first = false;

			sb.append(c.colname);
			sb.append(" = ");
			if (c.genType == GenType.expression)
				sb.append(c.expression);
			else {
				sb.append(" ?");
				_updcolumns.add(c);
			}
		}

		if (_keycolumns.size() > 0) {
			sb.append(" where ");
			first = true;

			for (Column kc : _keycolumns) {
				if (!first) {
					sb.append(" and ");
				}
				first = false;

				sb.append(kc.colname);
				sb.append(" = ?");
			}
		}

		// If we have no key columns, we cannot do an update
		if (_keycolumns.size() == 0)
			update = null;
		else
			update = sb.toString();
	}

	private void createInsert() {
		_inscolumns.clear();
		StringBuilder sb = new StringBuilder("insert into ");
		sb.append(table);
		sb.append(" ( ");
		boolean first = true;
		for (Column c : _columns) {
			if (c.genType == GenType.sequence)
				continue;

			if (!first)
				sb.append(',');

			first = false;
			sb.append(c.colname);
		}
		sb.append(" ) ");

		sb.append(" values ( ");

		first = true;
		for (Column c : _columns) {
			if (c.genType == GenType.sequence)
				continue;
			if (!first)
				sb.append(',');
			first = false;

			if (c.genType == GenType.expression)
				sb.append(c.expression);
			else {
				sb.append(" ? ");
				_inscolumns.add(c);
			}
		}
		sb.append(" ) ");

		insert = sb.toString();

	}

    <T extends RowSubset> Element renderXmlRow(Locator locator, QName varType, T value) throws ExternalVariableModuleException {
		Document doc = DOMUtils.newDocument();
        Element el = doc.createElementNS(varType.getNamespaceURI(), varType.getLocalPart());
		doc.appendChild(el);
        if (value != null) {
            for (Column c : value._columns) {
                Object data = value.get(c.idx);
                addElement(el, varType, c, data);
            }
        } else {
            // initialize variable with default/generated values
            RowKey keys = keyFromLocator(locator);
            for (Column c : _columns) {
                Object data = c.getValue(c.name, keys, new RowVal(), locator.iid);
                addElement(el, varType, c, data);
            }
        }
		return el;
	}

    private void addElement(Element parent, QName varType, Column c, Object data) {
        Document doc = parent.getOwnerDocument();
        Element cel = doc.createElementNS(varType.getNamespaceURI(), c.name);
        String strdat = c.toText(data);
        if (strdat != null) {
            cel.appendChild(doc.createTextNode(strdat));
        } else if (c.nullok || c.isGenerated()) { 
            cel.setAttributeNS(XSI_NS, "xsi:nil", "true");
        }
        parent.appendChild(cel);
    }

	<T extends RowSubset> T parseXmlRow(T ret, Node rowel)
			throws ExternalVariableModuleException {
		if (rowel == null)
			return ret;
		
		NodeList nl = rowel.getChildNodes();
        if (__log.isDebugEnabled()) __log.debug("parseXmlRow: element="+rowel.getLocalName());
		for (int i = 0; i < nl.getLength(); ++i) {
			Node n = nl.item(i);
			if (n.getNodeType() != Node.ELEMENT_NODE)
				continue;
			String key = n.getLocalName();
			String val = n.getTextContent();
            if (__log.isDebugEnabled()) __log.debug("Extvar key: "+key+" value: "+val);

			Column column = ret.getColumn(key);
            if (column == null) {
                if (__log.isDebugEnabled()) __log.debug("No matching column for key '"+key+"'");
				continue;
            }

			String nil = ((Element) n).getAttributeNS(XSI_NS, "nil");
            if (nil != null && "true".equalsIgnoreCase(nil) && (val == null || val.trim().length() == 0)) {
                if (__log.isDebugEnabled()) __log.debug("Extvar key: "+key+" is null (xsi:nil)");
				ret.put(key, null);
            } else {
				ret.put(key, column.fromText(val));
            }
		}
		return ret;
	}

	class Column {

		int idx;

		/** name of the column */
        final String name;

		/** database name of the column (in case we need to override */
        final String colname;

		/** Is this a key column? */
        final boolean key;

		/** Type of value generator to use for creating values for this column. */
        final GenType genType;

		/** The (SQL) expression used to populate the column. */
        final String expression;

		/** The SQL data type of this column, one of java.sql.Types */
		int dataType;

		/** Indicates NULL values are OK */
		boolean nullok;

        Column(String name, String colname, boolean key, GenType genType, String expression) {
			this.name = name;
			this.colname = colname == null ? name : colname;
			this.key = key;
			this.genType = genType;
			this.expression = expression;
		}

        public Object getValue(String name, RowKey keys, RowVal values, Long iid) {
			switch (genType) {
			case ctimestamp:
			case utimestamp:
				return isTimeStamp() ? new Timestamp(new Date().getTime())
						: new Date();
			case uuid:
				return new GUID().toString();
			case pid:
				return evarId.pid.toString();
			case iid:
				return iid;
			case none:
			default:
                if (key && keys.get(name) != null)
                    return keys.get(name);
                else
                    return values.get(name);
			}
		}

        boolean supportsEmptyValue() {
            return (dataType == Types.VARCHAR || dataType == Types.LONGVARCHAR || dataType == Types.CLOB); 
        }

		/**
		 * Return <code>true</code> if column is a date-like type.
		 */
		boolean isDate() {
			return dataType == Types.DATE;
		}

		boolean isTimeStamp() {
			return dataType == Types.TIMESTAMP;
		}

		boolean isTime() {
			return dataType == Types.TIME;
		}

		/**
		 * Is this column best represented as an integer?
		 */
		boolean isInteger() {
			switch (dataType) {
			case Types.BIGINT:
			case Types.INTEGER:
			case Types.SMALLINT:
			case Types.TINYINT:
				return true;
			default:
				return false;
			}
		}

		/**
		 * Is this column best represented as a real number?
		 */
		boolean isReal() {
			switch (dataType) {
			case Types.DECIMAL:
			case Types.REAL:
			case Types.NUMERIC:
				return true;
			default:
				return false;
			}

		}

		boolean isBoolean() {
			switch (dataType) {
			case Types.BIT:
				return true;
			default:
				return false;
			}
		}

		String toText(Object val) {
			if (val == null)
				return null;

            Date date = null;
            if (val instanceof java.util.Date) {
                // also applies to java.sql.Time, java.sql.Timestamp
                date = (Date) val;
				return ISO8601DateParser.format((Date) val);
            }
            return val.toString();
		}

		Object fromText(String val) throws ExternalVariableModuleException {
			try {
                if (val == null)
                    return null;
                
                if (!supportsEmptyValue() && val.trim().length() == 0) {
                    return null;
                }
                
				// TODO: use xsd:date and xsd:time conversions
				if (isDate())
                    return new java.sql.Date(ISO8601DateParser.parse(val).getTime());
				else if (isTime())
                    return new java.sql.Time(ISO8601DateParser.parse(val).getTime());
				else if (isTimeStamp())
                    return new java.sql.Timestamp(ISO8601DateParser.parse(val).getTime());
                else if (isInteger()) {
                    String v = val.trim().toLowerCase();
                    if (v.equals("true"))
                        return 1;
                    if (v.equals("false"))
                        return 0;
					return Long.valueOf(val);
                } else if (isReal())
					return Double.valueOf(val);
                else if (isBoolean()) {
                    String v = val.trim();
                    if (v.equals("1"))
                        return true;
                    if (v.equals("0"))
                        return false;
					return Boolean.valueOf(val);
                }

				return val;
			} catch (Exception ex) {
				throw new ExternalVariableModuleException(
						"Unable to convert value \"" + val + "\" for column \""
								+ name + "\" !", ex);
			}
		}

        public boolean isGenerated() {
            return (genType != null && !genType.equals(GenType.none));
        }
        
        public boolean isDatabaseGenerated() {
            return isGenerated() && (genType.equals(GenType.sequence) || genType.equals(GenType.expression));
        }
        
        public String toString() {
            return "Column {idx="+idx
                +",name="+name
                +",colname="+colname
                +",key="+key
                +",genType="+genType
                +")";
        }
	}

	/**
	 * Key used to identify a row.
	 */
	class RowKey extends RowSubset {
		private static final long serialVersionUID = 1L;

		/**
		 * Create empty row key.
		 */
		RowKey() {
			super(_keycolumns);
		}

		/**
		 * Write the key to a locator.
		 */
        void write(QName varType, Locator locator) throws ExternalVariableModuleException {
            locator.reference = renderXmlRow(locator, varType, this);
		}

		public Set<String> getMissing() {
			HashSet<String> missing = new HashSet<String>();
			for (Column c : _keycolumns) {
                if (get(c.idx) == null)
					missing.add(c.name);
			}
			return missing;
		}
	}

	/**
	 * Row values.
	 */
	class RowVal extends RowSubset {
		private static final long serialVersionUID = 1L;

		RowVal() {
			super(DbExternalVariable.this._columns);
		}
	}

}
