/*
 * Copyright 2000-2004 The Apache Software Foundation.
 *
 * 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.
 */
package com.marevol.utils.hibernate.faces.model;

import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.faces.FacesException;
import javax.faces.model.DataModel;
import javax.faces.model.DataModelEvent;
import javax.faces.model.DataModelListener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.type.Type;

/**
 *  HibernateDataModel is DataModel class for Hibernate ScrollableResults.
 * 
 * 
 * @author <a href="mailto:shinsuke@yahoo.co.jp">Shinsuke Sugaya</a>
 */
public class HibernateDataModel extends DataModel
{
    /**
     * Logger for this class
     */
    private static final Log log = LogFactory.getLog(HibernateDataModel.class);

    // FIELDS

    private int _currentIndex = -1;

    private int _rowCount = -1;

    private Session session;

    /**
     * The ResultSet being wrapped by this DataModel.
     */
    private ScrollableResults _scrollableResults = null;

    // CONSTRUCTORS
    public HibernateDataModel()
    {
        super();
    }

    public HibernateDataModel(Session session, String hSql, Object[] params)
    {
        super();
        this.session = session;

        if (hSql == null)
        {
            throw new IllegalArgumentException("HSQL statement is null.");
        }
        Query query = session.createQuery(hSql);

        if (params != null)
        {
            for (int i = 0; i < params.length; i++)
            {
                query.setParameter(i, params[i]);
            }
        }

        _scrollableResults = query.scroll();
        setWrappedData(_scrollableResults);

        guessRowCount(hSql, params);
    }

    private void guessRowCount(String hSql, Object[] params)
    {
        int index = hSql.toLowerCase().indexOf("from");
        if (index >= 0)
        {
            Query q = session.createQuery(createHsqlForCount(hSql));
            for (int i = 0; i < params.length; i++)
            {
                q.setParameter(i, params[i]);
            }
            Integer count = (Integer) q.uniqueResult();
            if (count != null)
            {
                setRowCount(count.intValue());
            }
            else
            {
                setRowCount(0);
            }
        }
        else
        {
            setRowCount(0);
        }
    }

    public HibernateDataModel(Session session, String hSql, Object[] params, Type[] types)
    {
        super();
        this.session = session;

        if (hSql == null)
        {
            throw new IllegalArgumentException("HSQL statement is null.");
        }
        Query query = session.createQuery(hSql);

        if ((params == null && types != null) || (params != null && types == null))
        {
            throw new IllegalArgumentException("Wrong parameters or types.");
        }
        if (params != null && types != null)
        {
            if (params.length != types.length)
            {
                throw new IllegalArgumentException("The number of parameter is not equal to the number of type.");
            }
            for (int i = 0; i < params.length; i++)
            {
                query.setParameter(i, params[i], types[i]);
            }
        }

        _scrollableResults = query.scroll();
        setWrappedData(_scrollableResults);

        guessRowCount(hSql, params, types);
    }

    private void guessRowCount(String hSql, Object[] params, Type[] types)
    {
        int index = hSql.toLowerCase().indexOf("from");
        if (index >= 0)
        {
            Query q = session.createQuery(createHsqlForCount(hSql));
            for (int i = 0; i < params.length; i++)
            {
                q.setParameter(i, params[i], types[i]);
            }
            Integer count = (Integer) q.uniqueResult();
            if (count != null)
            {
                setRowCount(count.intValue());
            }
            else
            {
                setRowCount(0);
            }
        }
        else
        {
            setRowCount(0);
        }
    }

    private String createHsqlForCount(String hSql)
    {
        String sql = "select count(*) " + hSql.substring(hSql.toLowerCase().indexOf("from"));

        if (log.isDebugEnabled())
        {
            log.debug("createHsqlForCount(" + hSql + ")="
                    + sql.replaceAll("order by(\\s+[^ ]*(\\s+(asc)|(desc))?,?)+", ""));
        }

        return sql.replaceAll("order by(\\s*[^ ]+(\\s+(asc)|(desc))?,?)+", "");
    }

    public void setRowCount(int count)
    {
        _rowCount = count;
    }

    public int getRowCount()
    {
        return _rowCount;
    }

    /** Get the actual data of this row
     *  wrapped into a map.
     *  The specification is very strict about what has to be
     *  returned from here, so check the spec before
     *  modifying anything here.
     */
    public Object getRowData()
    {
        if (_scrollableResults == null)
        {
            if (log.isDebugEnabled())
            {
                log.debug("getRowData() - _query is null.");
            }
            return null;
        }
        else if (!isRowAvailable())
        {
            throw new IllegalArgumentException(
                    "the requested row is not available in the ScrollableResults - you have scrolled beyond the end.");
        }

        try
        {
            return new WrapScrollableResultsMap(String.CASE_INSENSITIVE_ORDER);
        }
        catch (HibernateException e)
        {
            log.error("getRowData()", e);

            throw new FacesException(e);
        }

    }

    public int getRowIndex()
    {
        return _currentIndex;
    }

    public Object getWrappedData()
    {
        return _scrollableResults;
    }

    public boolean isRowAvailable()
    {
        if (_scrollableResults == null)
        {
            if (log.isDebugEnabled())
            {
                log.debug("isRowAvailable() - _query is null.");
            }
            return false;
        }
        else if (_currentIndex < 0)
        {
            if (log.isDebugEnabled())
            {
                log.debug("isRowAvailable() - _currentIndex < 0");
            }
            return false;
        }

        try
        {
            return _scrollableResults.setRowNumber(_currentIndex);
        }
        catch (HibernateException e)
        {
            log.error("isRowAvailable()", e);

            throw new FacesException(e);
        }
    }

    public void setRowIndex(int rowIndex)
    {
        if (rowIndex < -1)
        {
            throw new IllegalArgumentException("you cannot set the rowIndex to anything less than 0");
        }

        int old = _currentIndex;
        _currentIndex = rowIndex;

        //if no underlying data has been set, the listeners
        //need not be notified
        if (_scrollableResults == null)
        {
            if (log.isDebugEnabled())
            {
                log.debug("setRowIndex(int) - _query is null.");
            }
            return;
        }

        //Notify all listeners of the upated row
        DataModelListener[] listeners = getDataModelListeners();

        if ((old != _currentIndex) && (listeners != null))
        {
            Object rowData = null;

            if (isRowAvailable())
            {
                rowData = getRowData();
            }

            DataModelEvent event = new DataModelEvent(this, _currentIndex, rowData);

            int n = listeners.length;

            for (int i = 0; i < n; i++)
            {
                if (listeners[i] != null)
                {
                    listeners[i].rowSelected(event);
                }
            }
        }

    }

    public void setWrappedData(Object data)
    {
        if (data == null)
        {
            _scrollableResults = null;
            setRowIndex(-1);
        }
        else
        {
            _scrollableResults = (ScrollableResults) data;
            _currentIndex = -1;
            setRowIndex(0);
        }
    }

    /* A map wrapping the result set and calling
     * the corresponding operations on the result set,
     * first setting the correct row index.
     */
    private class WrapScrollableResultsMap extends TreeMap
    {
        /**
         * 
         */
        private static final long serialVersionUID = 2774501371528418953L;

        /**
         * Logger for this class
         */
        private final Log log = LogFactory.getLog(WrapScrollableResultsMap.class);

        private int _currentIndex;

        //       private ScrollableResults _scrollableResults;

        public WrapScrollableResultsMap(Comparator comparator)
        {
            super(comparator);

            _currentIndex = HibernateDataModel.this._currentIndex;

            if (_scrollableResults.setRowNumber(_currentIndex))
            {
                Object[] objects = _scrollableResults.get();

                for (int i = 0; i < objects.length; i++)
                {
                    super.put(Integer.toString(i), Integer.toString(i));
                }
            }
            else
            {
                if (log.isDebugEnabled())
                {
                    log.debug("WrapScrollableResultsMap(Comparator) -  : The current row is not available.");
                }
            }
        }

        public void clear()
        {
            throw new UnsupportedOperationException("It is not allowed to remove from this map");
        }

        public boolean containsValue(Object value)
        {
            Set keys = keySet();
            for (Iterator iterator = keys.iterator(); iterator.hasNext();)
            {
                Object object = get(iterator.next());
                if (object == null)
                {
                    return value == null;
                }
                if (object.equals(value))
                {
                    return true;
                }

            }

            return false;
        }

        public Set entrySet()
        {
            return new WrapScrollableResultsEntries(this);
        }

        public Object get(Object key)
        {
            if (!containsKey(key))
            {
                if (log.isDebugEnabled())
                {
                    log.debug("get(Object) - get(" + key + ") is null.");
                }
                return null;
            }
            return basicGet(key);
        }

        private Object basicGet(Object key)
        {
            _scrollableResults.setRowNumber(_currentIndex);
            Integer i = new Integer((String) getUnderlyingKey(key));
            if (i != null)
            {
                return _scrollableResults.get(i.intValue());
            }

            if (log.isDebugEnabled())
            {
                log.debug("basicGet(Object) - Cannot find the target column : i=" + i);
            }
            return null;
        }

        public Set keySet()
        {
            return new WrapScrollableResultsKeys(this);
        }

        public Object put(Object key, Object value)
        {
            throw new UnsupportedOperationException("It is not allowed to update entries from this set.");
        }

        public void putAll(Map map)
        {
            for (Iterator i = map.entrySet().iterator(); i.hasNext();)
            {
                Map.Entry entry = (Map.Entry) i.next();
                put(entry.getKey(), entry.getValue());
            }

        }

        public Object remove(Object key)
        {
            throw new UnsupportedOperationException("It is not allowed to remove entries from this set.");
        }

        public Collection values()
        {

            return new WrapScrollableResultsValues(this);
        }

        Object getUnderlyingKey(Object key)
        {

            return super.get(key);
        }

        Iterator getUnderlyingKeys()
        {
            return super.keySet().iterator();
        }

    }

    private static class WrapScrollableResultsEntries extends AbstractSet
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsEntries.class);

        private WrapScrollableResultsMap _wrapMap;

        public WrapScrollableResultsEntries(WrapScrollableResultsMap wrapMap)
        {
            _wrapMap = wrapMap;
        }

        public boolean add(Object o)
        {
            throw new UnsupportedOperationException("it is not allowed to add to this set");
        }

        public boolean addAll(Collection c)
        {
            throw new UnsupportedOperationException("it is not allowed to add to this set");
        }

        public void clear()
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this set");
        }

        public boolean contains(Object o)
        {

            if (o == null)
                throw new NullPointerException();
            if (!(o instanceof Map.Entry))
                return false;

            Map.Entry e = (Map.Entry) o;
            Object key = e.getKey();

            if (!_wrapMap.containsKey(key))
            {
                return false;
            }

            Object value = e.getValue();
            Object cmpValue = _wrapMap.get(key);

            return value == null ? cmpValue == null : value.equals(cmpValue);
        }

        public boolean isEmpty()
        {
            return _wrapMap.isEmpty();
        }

        public Iterator iterator()
        {
            return new WrapScrollableResultsEntriesIterator(_wrapMap);
        }

        public boolean remove(Object o)
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this set");
        }

        public boolean removeAll(Collection c)
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this set");
        }

        public boolean retainAll(Collection c)
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this set");
        }

        public int size()
        {
            return _wrapMap.size();
        }
    }

    private static class WrapScrollableResultsEntriesIterator implements Iterator
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsEntriesIterator.class);

        private WrapScrollableResultsMap _wrapMap = null;

        private Iterator _keyIterator = null;

        public WrapScrollableResultsEntriesIterator(WrapScrollableResultsMap wrapMap)
        {
            _wrapMap = wrapMap;
            _keyIterator = _wrapMap.keySet().iterator();
        }

        public boolean hasNext()
        {
            return _keyIterator.hasNext();
        }

        public Object next()
        {
            return new WrapScrollableResultsEntry(_wrapMap, _keyIterator.next());
        }

        public void remove()
        {
            throw new UnsupportedOperationException("It is not allowed to remove from this iterator");
        }

    }

    private static class WrapScrollableResultsEntry implements Map.Entry
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsEntry.class);

        private WrapScrollableResultsMap _wrapMap;

        private Object _entryKey;

        public WrapScrollableResultsEntry(WrapScrollableResultsMap wrapMap, Object entryKey)
        {
            _wrapMap = wrapMap;
            _entryKey = entryKey;
        }

        public boolean equals(Object o)
        {
            if (o == null)
                return false;

            if (!(o instanceof Map.Entry))
                return false;

            Map.Entry cmpEntry = (Map.Entry) o;

            if (_entryKey == null ? cmpEntry.getKey() != null : !_entryKey.equals(cmpEntry.getKey()))
                return false;

            Object value = _wrapMap.get(_entryKey);
            Object cmpValue = cmpEntry.getValue();

            return value == null ? cmpValue != null : value.equals(cmpValue);
        }

        public Object getKey()
        {
            return _entryKey;
        }

        public Object getValue()
        {
            return _wrapMap.get(_entryKey);
        }

        public int hashCode()
        {
            int result;
            result = (_entryKey != null ? _entryKey.hashCode() : 0);
            result = 29 * result + (_wrapMap.get(_entryKey) != null ? _wrapMap.get(_entryKey).hashCode() : 0);

            return result;
        }

        public Object setValue(Object value)
        {
            Object oldValue = _wrapMap.get(_entryKey);
            _wrapMap.put(_entryKey, value);

            return oldValue;
        }
    }

    private static class WrapScrollableResultsKeys extends AbstractSet
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsKeys.class);

        private WrapScrollableResultsMap _wrapMap;

        public WrapScrollableResultsKeys(WrapScrollableResultsMap wrapMap)
        {
            _wrapMap = wrapMap;
        }

        public boolean add(Object o)
        {
            throw new UnsupportedOperationException("It is not allowed to add to this set");
        }

        public boolean addAll(Collection c)
        {
            throw new UnsupportedOperationException("It is not allowed to add to this set");
        }

        public void clear()
        {
            throw new UnsupportedOperationException("It is not allowed to remove from this set");
        }

        public boolean contains(Object obj)
        {
            return _wrapMap.containsKey(obj);
        }

        public boolean isEmpty()
        {
            return _wrapMap.isEmpty();
        }

        public Iterator iterator()
        {

            return new WrapScrollableResultsKeysIterator(_wrapMap);
        }

        public boolean remove(Object o)
        {
            throw new UnsupportedOperationException("It is not allowed to remove from this set");
        }

        public boolean removeAll(Collection c)
        {
            throw new UnsupportedOperationException("It is not allowed to remove from this set");
        }

        public boolean retainAll(Collection c)
        {
            throw new UnsupportedOperationException("It is not allowed to remove from this set");
        }

        public int size()
        {
            return _wrapMap.size();
        }
    }

    private static class WrapScrollableResultsKeysIterator implements Iterator
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsKeysIterator.class);

        private Iterator _keyIterator = null;

        public WrapScrollableResultsKeysIterator(WrapScrollableResultsMap map)
        {
            _keyIterator = map.getUnderlyingKeys();
        }

        public boolean hasNext()
        {
            return _keyIterator.hasNext();
        }

        public Object next()
        {
            return _keyIterator.next();
        }

        public void remove()
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this iterator");
        }

    }

    private static class WrapScrollableResultsValues extends AbstractCollection
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsValues.class);

        private WrapScrollableResultsMap _wrapMap;

        public WrapScrollableResultsValues(WrapScrollableResultsMap wrapMap)
        {
            _wrapMap = wrapMap;
        }

        public boolean add(Object o)
        {
            throw new UnsupportedOperationException("it is not allowed to add to this collection");
        }

        public boolean addAll(Collection c)
        {
            throw new UnsupportedOperationException("it is not allowed to add to this collection");
        }

        public void clear()
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this collection");
        }

        public boolean contains(Object value)
        {
            return _wrapMap.containsValue(value);
        }

        public Iterator iterator()
        {
            return new WrapScrollableResultsValuesIterator(_wrapMap);
        }

        public boolean remove(Object o)
        {
            throw new UnsupportedOperationException();
        }

        public boolean removeAll(Collection c)
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this collection");
        }

        public boolean retainAll(Collection c)
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this collection");
        }

        public int size()
        {
            return _wrapMap.size();
        }

    }

    private static class WrapScrollableResultsValuesIterator implements Iterator
    {
        /**
         * Logger for this class
         */
        private static final Log log = LogFactory.getLog(WrapScrollableResultsValuesIterator.class);

        private WrapScrollableResultsMap _wrapMap;

        private Iterator _keyIterator;

        public WrapScrollableResultsValuesIterator(WrapScrollableResultsMap wrapMap)
        {
            _wrapMap = wrapMap;
            _keyIterator = _wrapMap.keySet().iterator();
        }

        public boolean hasNext()
        {
            return _keyIterator.hasNext();
        }

        public Object next()
        {
            return _wrapMap.get(_keyIterator.next());
        }

        public void remove()
        {
            throw new UnsupportedOperationException("it is not allowed to remove from this map");
        }

    }

}
