/*
 * ICESOFT COMMERCIAL SOURCE CODE LICENSE V 1.1
 *
 * The contents of this file are subject to the ICEsoft Commercial Source
 * Code License Agreement V1.1 (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.icesoft.com/license/commercial-source-v1.1.html
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * Copyright 2009-2015 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 */
package com.icesoft.faces.facelets.component.table.editable;

import com.icesoft.faces.component.ext.HtmlDataTable;
import com.icesoft.faces.component.ext.RowSelectorEvent;
import com.icesoft.faces.facelets.component.table.BaseTableBean;
import com.icesoft.faces.facelets.component.util.Util;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;

import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import java.beans.PropertyDescriptor;
import java.util.*;

/**
 * Abstract class base for an editable table with basic CRUD events and listener
 * notification. Concrete classes extending this class should implement the
 * DataModel implementation supporting the table structure, which is suitable
 * for their own requirements.
 * 
 * @author philip.breau@icesoft.com
 */
public abstract class AbstractEditableTableBean extends BaseTableBean {

   protected Map selectedRows = new HashMap();
   protected Map selectedRowEdits = new HashMap();
   protected Set properties = new HashSet();
   protected IEditableTableEventListener eventListener;
   protected Class clazz;

   // *********** CTORS ****************************
   protected AbstractEditableTableBean() {
   } // allows extension

   /**
    * An empty table will be created.
    * 
    * @param clazz
    *           Class reference of row-level bean
    */
   public AbstractEditableTableBean(Class clazz) {
      if (clazz == null)
         throw new IllegalArgumentException(this.getClass().getSimpleName()
               + " needs to be initialized with a non-null Row class name");
      this.clazz = clazz;
      this.createDefaultDataModel();
      initFields();
   }

   /**
    * A table with the provided list will be created.
    * 
    * @param list
    *           non-empty List used to initalize the table
    * @throws IllegalArgumentException
    *            if empty List is provided
    */
   public AbstractEditableTableBean(List list) {
      if (list.size() == 0)
         throw new IllegalArgumentException(
               "If not specifying the Class in constructor arguments, "
                     + "a non-empty List must be provided.");
      this.clazz = list.get(0).getClass();
      initFields();
   }

   /**
    * A table will be created with the provide list and with the provided event
    * listener
    * 
    * @param list
    *           non-empty List used to initalize the table
    * @param eventListener
    *           IEditableTableEventListener instance to be notified of events
    */
   public AbstractEditableTableBean(List list,
         IEditableTableEventListener eventListener) {
      this(list);
      this.eventListener = eventListener;
   }

   /**
    * An empty table will be created with the provided event listener.
    * 
    * @param clazz
    *           The row-level bean class
    * @param eventListener
    *           IEditableTableEventListener instance to be notified of events
    */
   public AbstractEditableTableBean(Class clazz,
         IEditableTableEventListener eventListener) {
      this(clazz);
      this.eventListener = eventListener;
   }

   /**
    * Get the edit space map of current row edits
    * 
    * @return Map<String, Object> of current edits
    */
   public Map getSelectedRowEdits() {
      return selectedRowEdits;
   }

   /**
    * Get the selected rows
    * 
    * @return Map of selected rows
    */
   public Map getSelectedRows() {
      return selectedRows;
   }

   private void selectRow(Object row) {
      if (selectedRowEdits.get(row) == null) {
         try {
            selectedRowEdits.put(row, BeanUtils.cloneBean(row));
         } catch (Exception e) {
            e.printStackTrace();
            return;
         }
      }
      selectedRows.put(row, Boolean.TRUE);
   }

   /**
    * listener called by row-click event to select a row
    * 
    * @param evt
    *           RowSelectorEvent constructed by ICEfaces
    */
   public void rowSelectionListener(RowSelectorEvent evt) {
      try{
         data.setRowIndex(evt.getRow());
         Object row = data.getRowData();
         if (evt.isSelected()) {
            if (eventListener != null) {
               eventListener.rowSelected(row);
            }
            clearDataTableInputComponents(evt.getComponent());
            selectRow(row);            
   
         } else {
            selectedRows.put(row, Boolean.TRUE);
            FacesContext.getCurrentInstance().renderResponse();
         }
      } catch (EditableTableException e) {
         // do nothing
      }

   }

   private void initFields() {

      PropertyDescriptor[] propertyDescs = PropertyUtils
            .getPropertyDescriptors(clazz);
      Object newInstance = newInstanceOfBean();

      for (PropertyDescriptor propertyDesc : propertyDescs) {
         if (PropertyUtils.isReadable(newInstance, propertyDesc.getName())
               && PropertyUtils
                     .isWriteable(newInstance, propertyDesc.getName()))
            properties.add(propertyDesc.getName());
      }
   }

   /**
    * Save the current row, moving property value changes from the edit-space to
    * the bean instance, and call the IEditableTableEventListener.rowUpdated()
    * listener
    * 
    * @param evt
    *           JSF ActionEvent
    */
   public void saveCurrentEdit(ActionEvent evt) {
      Object oldBean = getActionEventRow(evt);
      Object newBean = selectedRowEdits.get(oldBean);
      try {
         if (eventListener != null)
            eventListener.rowUpdated(oldBean, newBean);
         deselectRow(oldBean);
         clearDataTableInputComponents(evt.getComponent());
         try {
            BeanUtils.copyProperties(oldBean, newBean);
         } catch (Exception e) {
            e.printStackTrace();
         }

      } catch (EditableTableException e) {
         // do nothing
      }

   }

   /**
    * Cancel editing the currently selected row and discard all changes
    * 
    * @param evt
    *           JSF ActionEvent
    */
   public void cancelCurrentEdit(ActionEvent evt) {
      Object oldBean = getActionEventRow(evt);
      Object newBean = selectedRowEdits.get(oldBean);
      try {
         if (eventListener != null)
            eventListener.rowEditCanceled(oldBean, newBean);
         deselectRow(oldBean);
         Util.clearAllUIInputs(evt.getComponent().getParent().getParent()
               .getParent());
         FacesContext.getCurrentInstance().renderResponse();
      } catch (EditableTableException e) {
         // do nothing
      }
   }

   private void deselectRow(Object row) {
      selectedRows.remove(row);
      selectedRowEdits.remove(row);
   }

   /**
    * Create a new instance of the row-level bean, add it to the table at the
    * current row index, and call the IEditableTableEventListener.rowCreated() listener.
    *
    * @param evt JSF ActionEvent
    */
   public void createNewRow(ActionEvent evt) {
       Object row = evt.getComponent().getAttributes().get("row");
       String location = FacesContext.getCurrentInstance().getExternalContext()
               .getRequestParameterMap().get("location");
       createNewRow(row, location, evt);
   }

    public void createNewRow(Object row, String location, ActionEvent evt) {
       int index = 0;
       if (row != null) {
           index = ((List) getData().getWrappedData()).indexOf(row);
           if ("below".equals(location))
               index++;
       } else if( evt != null ){
           UIData dataTable = findParentDataTable(evt.getComponent());
           if (dataTable != null) {
               index = dataTable.getFirst();
               if (index < 0) {
                   index = 0;
               }
           }
       }
       createNewRowAtIndex(index);
    }
    
    protected void createNewRowAtIndex(int index){
        try {
            Object newRow = eventListener != null ? eventListener.createNewRow() : newInstanceOfBean();
            addRow(index, newRow);
            selectRow(newRow);
        } catch (EditableTableException e) {
            //do nothing
        }
    }
    
    
   
    private UIData findParentDataTable(UIComponent uic) {
        UIComponent parent = uic.getParent();
        while (parent != null) {
            if (parent instanceof UIData) {
                break;
            }
            parent = parent.getParent();
        }
        return (UIData) parent;
    }

   private Object newInstanceOfBean() {
      try {
         return clazz.newInstance();
      } catch (IllegalAccessException iae) {
         throw new IllegalArgumentException(
               "Bean must have public no-args constructor", iae);
      } catch (InstantiationException ie) {
         throw new IllegalArgumentException(
               "Bean must have public no-args constructor", ie);
      }
   }

   /**
    * Delete the currently selected row, and call the
    * IEditableTableEventListener.rowDeleted() listener.
    * 
    * @param evt
    *           JSF ActionEvent
    */
   public void deleteRow(ActionEvent evt) {
      Object row = getActionEventRow(evt);
      try {
         if (eventListener != null)
            eventListener.rowDeleted(row);
         removeRow(row);
         deselectRow(row);
         clearDataTableInputComponents(evt.getComponent());
         FacesContext.getCurrentInstance().renderResponse();
      } catch (EditableTableException e) {
         // do nothing
      }
   }

   private void clearDataTableInputComponents(UIComponent uic) {
      UIComponent comp = uic;
      while (!(comp instanceof HtmlDataTable) && comp.getParent() != null) {
         comp = comp.getParent();
      }
      if (comp instanceof HtmlDataTable) {
         Util.clearAllUIInputs(comp);
      }
   }

   /**
    * Concrete classes that extend this abstract class should implement this
    * method and create default DataModel instance. This method will be called
    * when constructors that do not specify an initial list are called.
    */
   protected abstract void createDefaultDataModel();

   private Object getActionEventRow(ActionEvent evt) {
      return evt.getComponent().getAttributes().get("row");
   }

   public abstract List getListData();

   public abstract void addRow(int index, Object row);

   public abstract void removeRow(Object row);

}
