/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.model;

import static org.richfaces.application.configuration.ConfigurationServiceHelper.getBooleanConfigurationValue;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.el.ValueExpression;
import javax.faces.context.FacesContext;
import javax.faces.model.DataModelListener;

import org.ajax4jsf.model.DataVisitResult;
import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.richfaces.application.IterationComponentsConfiguration;
import org.richfaces.component.SortOrder;

/**
 * @author Konstantin Mishin
 *
 */
public class ArrangeableModel extends ExtendedDataModel<Object> implements Arrangeable {
    private ArrangeableState state;
    private List<Object> rowKeys;
    private ExtendedDataModel<?> originalModel;
    private String var;
    private String filterVar;
    private Comparator<? super String> stringComparator;

    public ArrangeableModel(ExtendedDataModel<?> originalModel, String var, String filterVar) {
        this.originalModel = originalModel;
        this.var = var;
        this.filterVar = filterVar;
    }

    public void addDataModelListener(DataModelListener listener) {
        originalModel.addDataModelListener(listener);
    }

    public void removeDataModelListener(DataModelListener listener) {
        originalModel.removeDataModelListener(listener);
    }

    public DataModelListener[] getDataModelListeners() {
        return originalModel.getDataModelListeners();
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.model.ExtendedDataModel#getRowKey()
     */
    public Object getRowKey() {
        return originalModel.getRowKey();
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.model.ExtendedDataModel#setRowKey(java.lang.Object)
     */
    public void setRowKey(Object key) {
        originalModel.setRowKey(key);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.model.ExtendedDataModel#walk(javax.faces.context.FacesContext, org.richfaces.model.DataVisitor,
     * org.richfaces.model.Range, java.lang.Object)
     */
    public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) {
        final SequenceRange seqRange = (SequenceRange) range;
        int rows = seqRange.getRows();
        int rowCount = getRowCount();
        int currentRow = seqRange.getFirstRow();
        if (rows > 0) {
            rows += currentRow;
            rows = Math.min(rows, rowCount);
        } else {
            rows = rowCount;
        }
        for (; currentRow < rows; currentRow++) {
            visitor.process(context, rowKeys.get(currentRow), argument);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#getRowCount()
     */
    public int getRowCount() {
        if (rowKeys == null) {
            return -1;
        } else {
            return rowKeys.size();
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#getRowData()
     */
    public Object getRowData() {
        return originalModel.getRowData();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#getRowIndex()
     */
    public int getRowIndex() {
        return rowKeys.indexOf(originalModel.getRowKey());
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#getWrappedData()
     */
    public Object getWrappedData() {
        return originalModel.getWrappedData();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#isRowAvailable()
     */
    public boolean isRowAvailable() {
        return originalModel.isRowAvailable();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#setRowIndex(int)
     */
    public void setRowIndex(int rowIndex) {
        Object originalKey = null;
        if (rowIndex >= 0 && rowIndex < rowKeys.size()) {
            originalKey = rowKeys.get(rowIndex);
        }
        originalModel.setRowKey(originalKey);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.model.DataModel#setWrappedData(java.lang.Object)
     */
    public void setWrappedData(Object data) {
        originalModel.setWrappedData(data);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.model.Modifiable#modify(org.richfaces.model.ModifiableState)
     */
    public void arrange(FacesContext context, ArrangeableState state) {
        initializeRowKeys(context);
        if (state != null) {
            this.state = state;
            Map<String, Object> map = context.getExternalContext().getRequestMap();
            Object value = null;
            Object filterValue = null;
            if (var != null && var.length() > 0) {
                value = map.get(var);
            }
            if (filterVar != null && filterVar.length() > 0) {
                filterValue = map.get(filterVar);
            }
            filter(context);
            sort(context);
            if (var != null && var.length() > 0) {
                map.put(var, value);
            }
            if (filterVar != null && filterVar.length() > 0) {
                map.put(filterVar, filterValue);
            }
        }
    }

    private void initializeRowKeys(FacesContext context) {
        int rowCount = originalModel.getRowCount();
        if (rowCount > 0) {
            rowKeys = new ArrayList<Object>(rowCount);
        } else {
            rowKeys = new ArrayList<Object>();
        }
        Object rowKey = originalModel.getRowKey();
        originalModel.walk(context, new DataVisitor() {
            public DataVisitResult process(FacesContext context, Object rowKey, Object argument) {
                originalModel.setRowKey(rowKey);
                if (originalModel.isRowAvailable()) {
                    rowKeys.add(rowKey);
                }
                return DataVisitResult.CONTINUE;
            }
        }, new SequenceRange(0, -1), null);
        originalModel.setRowKey(rowKey);
    }

    private void filter(FacesContext context) {
        List<FilterField> filterFields = state.getFilterFields();
        if (filterFields != null && !filterFields.isEmpty()) {
            List<Object> filteredCollection = new ArrayList<Object>();
            for (Object rowKey : rowKeys) {
                if (accept(context, rowKey)) {
                    filteredCollection.add(rowKey);
                }
            }
            rowKeys = filteredCollection;
        }
    }

    private void sort(final FacesContext context) {
        List<SortField> sortFields = state.getSortFields();
        if (sortFields != null && !sortFields.isEmpty()) {
            Collections.sort(rowKeys, new Comparator<Object>() {
                public int compare(Object rowKey1, Object rowKey2) {
                    return ArrangeableModel.this.compare(context, rowKey1, rowKey2);
                }
            });
        }
    }

    private boolean accept(FacesContext context, Object rowKey) {
        originalModel.setRowKey(rowKey);
        Object object = originalModel.getRowData();
        updateVar(context, var, object);
        for (FilterField filterField : state.getFilterFields()) {
            Filter filter = filterField.getFilter();
            if (filter != null && !filter.accept(object)) {
                return false;
            } else {
                ValueExpression filterExpression = filterField.getFilterExpression();
                if (filterExpression != null) {
                    updateVar(context, filterVar, filterField.getFilterValue());
                    if (Boolean.FALSE.equals(filterExpression.getValue(context.getELContext()))) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    private int compare(FacesContext context, Object rowKey1, Object rowKey2) {
        originalModel.setRowKey(rowKey1);
        Object object1 = originalModel.getRowData();
        originalModel.setRowKey(rowKey2);
        Object object2 = originalModel.getRowData();
        int result = 0;
        for (Iterator<SortField> iterator = state.getSortFields().iterator(); iterator.hasNext() && result == 0;) {
            SortField sortField = iterator.next();
            SortOrder sortOrder = sortField.getSortOrder();
            if (sortOrder != null && !SortOrder.unsorted.equals(sortOrder)) {
                Comparator comparator = sortField.getComparator();
                if (comparator != null) {
                    result = comparator.compare(object1, object2);
                } else {
                    ValueExpression sortBy = sortField.getSortBy();
                    if (sortBy != null) {
                        updateVar(context, var, object1);
                        Object value1 = sortBy.getValue(context.getELContext());
                        updateVar(context, var, object2);
                        Object value2 = sortBy.getValue(context.getELContext());
                        result = compareSortByValues(context, value1, value2);
                    }
                }
                if (SortOrder.descending.equals(sortOrder)) {
                    result = -result;
                }
            }
        }
        return result;
    }

    private int compareSortByValues(FacesContext context, Object value1, Object value2) {
        int result = 0;
        if (value1 instanceof String && value2 instanceof String) {
            if (stringComparator == null) {
                stringComparator = createStringComparator(context);
            }
            result = stringComparator.compare(((String) value1).trim(), ((String) value2).trim());
        } else if (value1 == null && value2 != null) {
            result = -1;
        } else if (value2 == null && value1 != null) {
            result = 1;
        } else if (value1 instanceof Comparable<?>) {
            result = ((Comparable) value1).compareTo(value2);
        }
        return result;
    }

    private Comparator<? super String> createStringComparator(FacesContext context) {
        Comparator<? super String> comparator = null;
        Locale locale = state.getLocale();
        if (locale != null
            && getBooleanConfigurationValue(context, IterationComponentsConfiguration.Items.datatableUsesViewLocale)) {
            comparator = Collator.getInstance(locale);
        } else {
            comparator = new Comparator<String>() {
                public int compare(String o1, String o2) {
                    return o1.compareToIgnoreCase(o2);
                }
            };
        }
        return comparator;
    }

    private void updateVar(FacesContext context, String var, Object value) {
        if (var != null && var.length() > 0) {
            context.getExternalContext().getRequestMap().put(var, value);
        }
    }
}
