/*
 * © Copyright IBM Corp. 2010
 * 
 * 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.ibm.domino.services.rest.das.view;

import static com.ibm.domino.services.rest.RestServiceConstants.ITEM_FORM;
import static com.ibm.domino.services.rest.RestServiceConstants.SORT_ORDER_DESCENDING;

import java.util.Vector;

import lotus.domino.Database;
import lotus.domino.DateTime;
import lotus.domino.Document;
import lotus.domino.NotesException;
import lotus.domino.View;
import lotus.domino.ViewColumn;
import lotus.domino.ViewEntry;
import lotus.domino.ViewEntryCollection;
import lotus.domino.ViewNavigator;

import com.ibm.commons.util.StringUtil;
import com.ibm.domino.services.Loggers;
import com.ibm.domino.services.ResponseCode;
import com.ibm.domino.services.ServiceException;
import com.ibm.xsp.model.domino.wrapped.DominoViewEntry;


/**
 * Domino View Service.
 */
public class RestViewNavigatorFactory {

    /**
     * Factory that creates a RestViewNavigator based on the view parameters.
     */
    public static RestViewNavigator createNavigator(View view, ViewParameters parameters) throws ServiceException {
        // NOI navigators
        try {
            String ftSearch = parameters.getSearch();
            if(StringUtil.isNotEmpty(ftSearch)) {
                return new FullTextNavigator(view,parameters);
            }
            
            // Key filtered navigator
            Object keys = parameters.getKeys();
            if(keys!=null) {
                return new SearchKeyNavigator(view,parameters);
            }
            
            // On the parent id
            String parentId = parameters.getParentId();
            if(parentId!=null) {
                return new ParentNavigator(view,parameters);
            }
            
            // On a category
            String cat = parameters.getCategoryFilter();
            if(cat!=null) {
                return new CategoryNavigator(view,parameters);
            }
            
            // On a startkey
            Object startKeys = parameters.getStartKeys();
            if(startKeys != null) {
                return new StartKeyNavigator(view,parameters);
            }
            
            // Regular navigator...
            return new GenericNavigator(view,parameters);
        } catch(NotesException ex) {
            throw new ServiceException(ex,""); // $NON-NLS-1$
        }
    }

    public static RestViewNavigator createNavigatorForDesign(View view, ViewParameters parameters) throws ServiceException {
        // NOI navigators
        try {
            return new NOINavigatorForDesign(view,parameters);
        } catch(NotesException ex) {
            throw new ServiceException(ex,""); // $NON-NLS-1$
        }
    }
    
    
    /**
     * Abstract Backend classes navigator.
     */
    public static abstract class NOINavigator extends RestViewNavigator {
        View view;
        Vector<ViewColumn> columns;
        ViewEntry entry;
        Vector<Object> columnValues;
        protected NOINavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
            this.view = view;
            this.columns = (Vector<ViewColumn>)view.getColumns();
        }
        protected void resortView() {
            // resort the view if sort criteria are specified
            String sortColumn = getParameters().getSortColumn();
            if (StringUtil.isNotEmpty(sortColumn)) {
                try {
                    String sortOrder = getParameters().getSortOrder();
                    boolean ascending = !StringUtil.equals(sortOrder, SORT_ORDER_DESCENDING);
                    view.resortView(sortColumn, ascending);
                } catch(NotesException ex) {
                    // Swallow the error if sort doesn't work
                }
            }
        }
        // Global methods
        @Override
        public int getTopLevelEntryCount() throws ServiceException {
            try {
                return view.getEntryCount();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        
        // Set the current entry
        public boolean initEntry(ViewEntry e) throws NotesException {
            if(entry!=null) {
                entry.recycle();
            }
            columnValues = null;
            entry = e;
            if(e!=null) {
                e.setPreferJavaDates(true);
                return true;
            }
            return false;
        }
        // System columns
        @Override
        public String getUniversalId() throws ServiceException {
            try {
                return entry.getUniversalID();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public String getNoteId() throws ServiceException {
            try {
                return entry.getNoteID();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public String getPosition() throws ServiceException {
            try {
                return entry.getPosition('.');
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public boolean getRead() throws ServiceException {
            try {
                return entry.getRead();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public int getSiblings() throws ServiceException {
            try {
                return entry.getSiblingCount();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public int getDescendants() throws ServiceException {
            try {
                return entry.getDescendantCount();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public int getChildren() throws ServiceException {
            try {
                return entry.getChildCount();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public int getIndent() throws ServiceException {
            try {
                return entry.getIndentLevel();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public boolean isDocument() throws ServiceException {
            try {
                return entry.isDocument();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public boolean isCategory() throws ServiceException {
            try {
                return entry.isCategory();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public boolean isResponse() throws ServiceException {
            try{
                Object op = entry.getParent();
                if(null != op && op instanceof ViewEntry){
                    ViewEntry parent = (ViewEntry)op;
                    return parent.isDocument();
                }
                return false;
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        // Column values
        @Override
        public int getColumnCount() throws ServiceException {
            return columns.size();
        }
        @Override
        public String getColumnName(int index) throws ServiceException {
            try {
                ViewColumn c = columns.get(index);
                return c.getItemName();
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public Object getColumnValue(int index) throws ServiceException {
            try {
                if(index>=0) {
                    if(columnValues==null) {
                        columnValues = entry.getColumnValues();
                    }
                    ViewColumn c = columns.get(index);
                    int idx = c.getColumnValuesIndex();
                    if(idx!=DominoViewEntry.VC_NOT_PRESENT) {
                        //Looks like the vector is smaller when the entry is a category                     
                        if(idx<columnValues.size()) {
                            return columnValues.get(idx);
                        }
                    }
                }
                return null; // No values...
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public Object getColumnValue(String name) throws ServiceException {
            return getColumnValue(findColumnPosition(name));
        }
        
        // Added by Tim Tripcony
        // Hoping to add support for Form Formulas later on
        @Override
        public String getForm() throws ServiceException {
            String result = "";
            if (isDocument()) {
                try {
                    Document document = entry.getDocument();
                    if (document != null) {
                        result = document.getItemValueString(ITEM_FORM);
                    }
                } catch (NotesException ex) {
                    throw new ServiceException(ex,""); // $NON-NLS-1$
                }
            }
            return result;
        }
        
        public int findColumnPosition(String name) throws ServiceException {
            try {
                int count = columns.size();
    
                // Look for a programmatic name first
                for(int i=0; i<count; i++) {
                    ViewColumn c = columns.get(i);
                    String s = c.getItemName();
                    if(StringUtil.equalsIgnoreCase(s, name)) {
                        return i;
                    }
                }
    
                // Then default to the title
                for(int i=0; i<count; i++) {
                    ViewColumn c = columns.get(i);
                    String s = c.getTitle();
                    if(StringUtil.equalsIgnoreCase(s, name)) {
                        return i;
                    }
                }
                
                // Nothing found!
                return Integer.MIN_VALUE;
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public String getItemName(String columnName) throws ServiceException {
            try {
                int pos = findColumnPosition(columnName);
                if(pos>=0) {
                    ViewColumn col = columns.get(pos);
                    if(!col.isFormula()) {
                        // Hope that the item name is correct here...
                        return col.getItemName();
                    }
                }
                return null;
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        
        @Override
        public ViewEntry getViewEntry() {
        	return entry;
        }
    }
    public static class NOINavigatorForDesign extends NOINavigator {
        protected NOINavigatorForDesign(View view, ViewParameters parameters) throws NotesException {
            super(view, parameters);
        }
        // This method is valid for this navigator
        //public abstract String getItemName(String columnName) throws ServiceException;

        @Override
        public int getTopLevelEntryCount() throws ServiceException {throw new IllegalStateException();}
        @Override
        public boolean first(int start, int count) throws ServiceException {throw new IllegalStateException();}
        @Override
        public boolean next() throws ServiceException {throw new IllegalStateException();}
        @Override
        public String getUniversalId() throws ServiceException {throw new IllegalStateException();}
        @Override
        public String getNoteId() throws ServiceException {throw new IllegalStateException();}
        @Override
        public String getPosition() throws ServiceException {throw new IllegalStateException();}
        @Override
        public boolean getRead() throws ServiceException {throw new IllegalStateException();}
        @Override
        public int getSiblings() throws ServiceException {throw new IllegalStateException();}
        @Override
        public int getDescendants() throws ServiceException {throw new IllegalStateException();}
        @Override
        public int getChildren() throws ServiceException {throw new IllegalStateException();}
        @Override
        public int getIndent() throws ServiceException {throw new IllegalStateException();}
        @Override
        public int getColumnCount() throws ServiceException {throw new IllegalStateException();}
        @Override
        public String getColumnName(int index) throws ServiceException {throw new IllegalStateException();}
        @Override
        public Object getColumnValue(int index) throws ServiceException {throw new IllegalStateException();}
        @Override
        public Object getColumnValue(String name) throws ServiceException {throw new IllegalStateException();}
        @Override
        public boolean isDocument() throws ServiceException {throw new IllegalStateException();}
    }

    
    /**
     * Abstract Backend classes navigator based on a ViewEntryCollection.
     */
    public static abstract class ViewCollectionNavigator extends NOINavigator {
        ViewEntryCollection collection;
        protected ViewCollectionNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
        }
        @Override
        public void recycle() {
            try {
                if(collection != null) {
                    collection.recycle();
                    collection = null;
                }
            } catch(NotesException ex) {
            	if( Loggers.SERVICES_LOGGER.isTraceDebugEnabled() ){
            		Loggers.SERVICES_LOGGER.info("Exception thrown on recycle.", ex);   // $NON-NLS-1$
            	}
            }
            super.recycle();
        }
        boolean bFirst = false;
        @Override
        public boolean first(int start, int count) throws ServiceException {
            if (bFirst)
                return bFirst;
            try {
                collection = createCollection();
                if(!initEntry(collection.getFirstEntry())) {
                    return false;
                }
                if(start>0) {
                    for(int i=0; i<start; i++) {
                        if(!initEntry(collection.getNextEntry())) {
                            return false;
                        }
                    }
                }
                bFirst = true;
                return true;
            } catch(NotesException ex) {
                throw new ServiceException(ex, ResponseCode.BAD_REQUEST, ex.getLocalizedMessage());
            }
        }
        @Override
        public boolean next() throws ServiceException {
            try {
                if(!initEntry(collection.getNextEntry())) {
                    return false;
                }
                return true;
            } catch(NotesException ex) {
                throw new ServiceException(ex, ResponseCode.BAD_REQUEST, ex.getLocalizedMessage());
            }
        }
        @Override
        public int getTopLevelEntryCount() throws ServiceException {
            if (collection != null) {
                try {
                    return collection.getCount();
                } catch(NotesException ex) {
                    throw new ServiceException(ex, ResponseCode.BAD_REQUEST, ex.getLocalizedMessage());
                }
            } else {
                return super.getTopLevelEntryCount();
            }
        }
        protected abstract ViewEntryCollection createCollection() throws NotesException, ServiceException;
    }
    
    /**
     * Backend classes navigator filtering on a FT search.
     */
    public static class FullTextNavigator extends ViewCollectionNavigator {
        public FullTextNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
        }
        @Override
        protected ViewEntryCollection createCollection() throws NotesException, ServiceException {
            
            Database db = view.getParent();
            if (!db.isFTIndexed()) {
                throw new ServiceException(null, "Database is not full text indexed."); // $NON-NLS-1$
            }
            String search = getParameters().getSearch();
            int maxDocs = getParameters().getSearchMaxDocs();
            String sortColumn = getParameters().getSortColumn();
            // if 'sortColumn' is specified then use FTSearchSorted
            // FTSearchSorted call will fail if the DB is not full text indexed
            if (StringUtil.isNotEmpty(sortColumn)) {
                try {
                    String sortOrder = getParameters().getSortOrder();
                    boolean ascending = !StringUtil.equals(sortOrder, SORT_ORDER_DESCENDING);
                    if (maxDocs < 0)
                    	maxDocs = 0;
                    boolean exact = getParameters().isKeysExactMatch();
                    boolean variants = false, fuzzy = false;
                    view.FTSearchSorted(search, maxDocs, sortColumn, ascending, exact, variants, fuzzy);
                    
                } catch(NotesException ex) {
	                throw new ServiceException(ex, ResponseCode.BAD_REQUEST, ex.getLocalizedMessage());
                }
                return view.getAllEntries();                
            }            
            if(maxDocs<=0) {
                view.FTSearch(search);
            } else {
                view.FTSearch(search,maxDocs);
            }
            return view.getAllEntries();
        }
    }

    /**
     * Backend classes navigator filtering on a search key.
     */
    public static class SearchKeyNavigator extends ViewCollectionNavigator {
        protected SearchKeyNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
            resortView();
        }
        @Override
        protected ViewEntryCollection createCollection() throws NotesException {
            boolean exact = getParameters().isKeysExactMatch();
            Object keys = getParameters().getKeys();
            if(keys instanceof Vector<?>) {
                Vector v = (Vector)keys;
                for (int i = 0; i < v.size(); i++) {
                    if (v.get(i) instanceof java.util.Date) {
                        DateTime notesDate = view.getParent().getParent().createDateTime((java.util.Date)v.get(i));
                        v.remove(i);
                        v.add(i, notesDate);
                    }
                }
                return view.getAllEntriesByKey((Vector<?>)keys,exact);
            } else if (keys instanceof java.util.Date) {
                DateTime notesDate = view.getParent().getParent().createDateTime((java.util.Date)keys);
                return view.getAllEntriesByKey(notesDate,exact);
            } else {
                return view.getAllEntriesByKey(keys,exact);
            }
        }
    }

    /**
     * Abstract Backend classes navigator based on the ViewNavigator.
     */
    public static abstract class ViewNavigatorNavigator extends NOINavigator {
        ViewNavigator navigator;
        protected ViewNavigatorNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
        }
        @Override
        public void recycle() {
            if(navigator!=null) {
                try {
                    navigator.recycle();
                    navigator = null;
                } catch(NotesException ex) {}
            }
            super.recycle();
        }
        boolean bFirst = false;
        @Override
        public boolean first(int start, int count) throws ServiceException {
            if (bFirst)
                return bFirst;
            try {
                navigator = createNavigator();
                int maxLevels = getParameters().getExpandLevel();
                if(maxLevels!=Integer.MAX_VALUE) {
                    navigator.setMaxLevel(maxLevels);
                }
                if(!navigator.gotoFirst()) {
                    return false;
                }
                navigator.setBufferMaxEntries(count);
                if(start>0) {
                    int skipped = navigator.skip(start);
                    if(skipped<start) {
                        return false;
                    }
                }
                bFirst = initEntry(navigator.getCurrent());
                return bFirst;
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }
        @Override
        public boolean next() throws ServiceException {
            try {
                return initEntry(navigator.getNext());
            } catch(NotesException ex) {
                throw new ServiceException(ex,""); // $NON-NLS-1$
            }
        }       
        @Override
        public int getTopLevelEntryCount() throws ServiceException {
            if (view!=null) {
                try {                   
                    if(view.isCategorized()) {
                        return navigator.getCount();
                    }
                    else {
                        return view.getEntryCount();    
                    }
                } catch(NotesException ex) {
                    throw new ServiceException(ex,""); // $NON-NLS-1$
                }
            } else {
                return super.getTopLevelEntryCount();
            }
        }
        protected abstract ViewNavigator createNavigator() throws NotesException;
    }

    /**
     * Backend classes generic navigator.
     */
    public static class GenericNavigator extends ViewNavigatorNavigator {
        public GenericNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
            resortView();
        }
        @Override
        protected ViewNavigator createNavigator() throws NotesException {
            return view.createViewNav();
        }
    }

    /**
     * Backend classes navigator starting from a particular key.
     */
    public static class StartKeyNavigator extends ViewNavigatorNavigator {
        public StartKeyNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
            resortView();
        }
        @Override
        protected ViewNavigator createNavigator() throws NotesException {
            Object startKeys = getParameters().getStartKeys();
            ViewEntry ve = view.getEntryByKey(startKeys);
            if(ve!=null) {
                return view.createViewNavFrom(ve);
            }
            return view.createViewNav();
        }
        @Override
        public int getTopLevelEntryCount() throws ServiceException {
            if (navigator!=null) {              
                return navigator.getCount();
            } else {
                return super.getTopLevelEntryCount();
            }
        }
    }

    /**
     * Backend classes navigator filtering on a category.
     */
    public static class CategoryNavigator extends ViewNavigatorNavigator {
        public CategoryNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
        }
        @Override
        protected ViewNavigator createNavigator() throws NotesException {
            String cat = getParameters().getCategoryFilter();
            return view.createViewNavFromCategory(cat);
        }
        @Override
        public int getTopLevelEntryCount() throws ServiceException {
            if (navigator!=null) {              
                return navigator.getCount();
            } else {
                return super.getTopLevelEntryCount();
            }
        }
    }
    
    /**
     * Backend classes navigator filtering on a parent id.
     */
    public static class ParentNavigator extends ViewNavigatorNavigator {
        public ParentNavigator(View view, ViewParameters parameters) throws NotesException {
            super(view,parameters);
        }
        @Override
        protected ViewNavigator createNavigator() throws NotesException {
            String parentId = getParameters().getParentId();
            Document doc = null;
            
            if (parentId.length() == 32)                
                doc = view.getParent().getDocumentByUNID(parentId);
            else
                doc = view.getParent().getDocumentByID(parentId);
            
            return view.createViewNavFromDescendants(doc);
        }
        @Override
        public int getTopLevelEntryCount() throws ServiceException {
            if (navigator!=null) {              
                return navigator.getCount();
            } else {
                return super.getTopLevelEntryCount();
            }
        }
    }   
}