/*
 *  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 jpa.tools.swing;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SingularAttribute;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;

/**
 * A data model for a tabular view of a list of persistent entities.
 * The data supplied by this model can be filtered to display field values of
 * basic type or single-valued or multi-valued relationships.
 * <br>
 * The meta-information about the attributes of the entity are supplied by
 * newly defined {@link Metamodel meta-model API} of JPA 2.0 specification.
 *
 * @author Pinaki Poddar
 *
 */
public class EntityDataModel<T> extends AbstractTableModel implements TableModel, Iterable<T> {
    private static final long serialVersionUID = 1L;
    /**
     * Constant designates to include non-relation fields.
     */
    public static int BASIC_ATTR       = 1 << 0;
    /**
     * Constant designates to include single-valued relation fields.
     */
    public static int ASSOCIATION_ATTR = 1 << 1;
    /**
     * Constant designates to include multi-valued relation fields.
     */
    public static int PLURAL_ATTR      = 1 << 2;
    /**
     * Constant designates to include all fields.
     */
    public static int ALL_ATTR         = BASIC_ATTR | ASSOCIATION_ATTR | PLURAL_ATTR;
    /**
     * Constant designates to show a row count field at the first column.
     */
    public static int ROW_COUNT        = 1 << 3;

    private List<String>   columnNames;
    private List<Class<?>> columnClasses;
    private List<Method>   methods;
    private List<T>        data;
    private List<Attribute<? super T,?>> attributes;
    private static Object[] EMPTY_ARGS = null;
    private static Class<?>[] EMPTY_CLASSES = null;

    private boolean showsRowCount;
    private boolean showsBasicAttr;
    private boolean showsSingularAttr;
    private boolean showsPluralAttr;

    /**
     * Attributes of the entity are reordered with basic attributes, followed by singular
     * association followed by the many-valued attributes.
     */
    public EntityDataModel(Class<T> cls, List<T> data, Metamodel meta, int styleBits) {
        super();
        this.data = data;
        EntityType<T> entityType = meta.entity(cls);

        columnNames   = new ArrayList<>();
        columnClasses = new ArrayList<>();
        attributes    = new ArrayList<>();
        methods       = new ArrayList<>();

        showsRowCount     = (styleBits & ROW_COUNT) != 0;
        showsBasicAttr    = (styleBits & BASIC_ATTR) != 0;
        showsSingularAttr = (styleBits & ASSOCIATION_ATTR) != 0;
        showsPluralAttr   = (styleBits & PLURAL_ATTR) != 0;

        Set<SingularAttribute<? super T, ?>> sAttrs = entityType.getSingularAttributes();
        if (showsBasicAttr) {
            for (SingularAttribute<? super T, ?> attr : sAttrs) {
                if (!attr.isAssociation()) {
                    attributes.add(attr);
                }
            }
        }
        if (showsSingularAttr) {
            for (SingularAttribute<? super T, ?> attr : sAttrs) {
                if (attr.isAssociation()) {
                    attributes.add(attr);
                }
            }
        }
        if (showsPluralAttr) {
            Set<PluralAttribute<? super T, ?, ?>> pAttrs = entityType.getPluralAttributes();
            for (PluralAttribute<? super T, ?, ?> attr : pAttrs) {
                attributes.add(attr);
            }
        }
        Collections.sort(attributes, new MetamodelHelper.AttributeComparator());
        for (Attribute<? super T, ?> attr : attributes) {
            populate(cls, attr);
        }
        if (showsRowCount) {
            columnNames.add(0,"Row");
            columnClasses.add(0, Long.class);
            attributes.add(0, null);
            methods.add(0, null);
        }
    }

    private void populate(Class<T> cls, Attribute<?,?> attr) {
        columnNames.add(attr.getName());
        columnClasses.add(wrap(attr.getJavaType()));
        methods.add(getMethod(cls, attr.getName()));
    }

    /**
     * Gets the attribute at a given column index.
     * Can be null for 0-th index if row count is being shown.
     */
    public Attribute<?,?> getAttribute(int columnIndex) {
        return attributes.get(columnIndex);
    }

    /**
     * Gets the entity represented in the given row.
     */
    public T getRow(int row) {
        return data.get(row);
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return columnClasses.get(columnIndex);
    }

    @Override
    public int getColumnCount() {
        return columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return columnNames.get(columnIndex);
    }

    @Override
    public int getRowCount() {
        return data.size();
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Method method = methods.get(columnIndex);
        if (method == null) {
            return columnIndex == 0 && showsRowCount ? rowIndex+1 :  "?";
        }
        Object row = data.get(rowIndex);
        Object val = getValueByReflection(rowIndex, row, columnIndex, method);
        return val;
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return false;
    }

    @Override
    public void setValueAt(Object value, int rowIndex, int columnIndex) {
        // should never be called
    }

    /**
     * Gets the value by reflection.
     */
    Object getValueByReflection(int rowIndex, Object o, int columnIndex, Method m) {
        if (o == null) {
            System.err.println("Row " + rowIndex + " is null");
            return null;
        }
        if (m == null) {
            System.err.println("Column " + columnIndex + ":" + getColumnName(columnIndex) + " method is null");
            return null;
        }
        try {
            return m.invoke(o, EMPTY_ARGS);
        } catch (Exception e) {
            System.err.println("Can not extract value at [" + rowIndex + "," + columnIndex + "] due to " + e);
            e.printStackTrace();
        }
        return null;
    }

    Class<?> wrap(Class<?> c) {
        if (c == null || c.isInterface() || c.isArray())
            return Object.class;
        if (c.isPrimitive()) {
            if (c == boolean.class) return Boolean.class;
            if (c == byte.class) return Byte.class;
            if (c == char.class) return Character.class;
            if (c == double.class) return Double.class;
            if (c == float.class) return Float.class;
            if (c == int.class) return Integer.class;
            if (c == long.class) return Long.class;
            if (c == short.class) return Short.class;
        }
        return c;
    }

    private Method getMethod(Class<T> type, String p) {
        try {
            String getter = "get" + Character.toUpperCase(p.charAt(0))+p.substring(1);
            return type.getMethod(getter, EMPTY_CLASSES);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

    public void updateData(List<T> newData) {
        data = newData;
        fireTableDataChanged();
    }

    @Override
    public Iterator<T> iterator() {
        return data.iterator();
    }

}
