/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco 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 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Alfresco. If not, see .
 */
package org.alfresco.cmis.changelog;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.alfresco.cmis.CMISBaseObjectTypeIds;
import org.alfresco.cmis.CMISCapabilityChanges;
import org.alfresco.cmis.CMISChangeEvent;
import org.alfresco.cmis.CMISChangeLog;
import org.alfresco.cmis.CMISChangeLogService;
import org.alfresco.cmis.CMISChangeType;
import org.alfresco.cmis.CMISInvalidArgumentException;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.repository.NodeRef;
/**
 * ChangeLog Service Implementation
 * 
 * @author Dmitry Velichkevich
 */
public class CMISChangeLogServiceImpl implements CMISChangeLogService
{
    private static final String PATH_DELIMITER = "/";
    private static final int DEFAULT_RETURN_SIZE = 100;
    private AuditService auditService;
    private String cmisAuditApplicationName;
    private List changesOnTypeCapability;
    /**
     * Set the AuditService.
     * 
     * @param auditService AuditService
     */
    public void setAuditService(AuditService auditService)
    {
        this.auditService = auditService;
    }
    /**
     * Set the Audit Application Name.
     * 
     * @param cmisAuditApplicationName Audit Application Name
     */
    public void setCmisAuditApplicationName(String cmisAuditApplicationName)
    {
        this.cmisAuditApplicationName = cmisAuditApplicationName;
    }
    /**
     * Set the ChangesOnTypeCapability.
     * 
     * @param changesOnTypeCapability list of CMISBaseObjectTypeIds
     */
    public void setChangesOnTypeCapability(List changesOnTypeCapability)
    {
        this.changesOnTypeCapability = changesOnTypeCapability;
    }
    /**
     * @see org.alfresco.cmis.CMISChangeLogService#getCapability()
     */
    public CMISCapabilityChanges getCapability()
    {
        return (auditService.isAuditEnabled(cmisAuditApplicationName, PATH_DELIMITER + cmisAuditApplicationName)) ? (CMISCapabilityChanges.OBJECTIDSONLY)
                : (CMISCapabilityChanges.NONE);
    }
    /**
     * @throws CMISInvalidArgumentException 
     * @see org.alfresco.cmis.CMISChangeLogService#getChangeLogEvents(java.lang.String, java.lang.Integer)
     */
    public CMISChangeLog getChangeLogEvents(String changeLogToken, Integer maxItems) throws CMISInvalidArgumentException
    {
        if (!auditService.isAuditEnabled(cmisAuditApplicationName, ("/" + cmisAuditApplicationName)))
        {
            throw new AlfrescoRuntimeException("Auditing for " + cmisAuditApplicationName + " is disabled!");
        }
        CMISChangeLogImpl result = new CMISChangeLogImpl();
        final List changeEvents = result.getChangeEvents();
        EntryIdCallback changeLogCollectingCallback = new EntryIdCallback(true)
        {
            @Override
            public boolean handleAuditEntry(Long entryId, String user, long time, Map values)
            {
                List changeLogEvents = convertValuesMapToChangeLogEvents(values, time);
                changeEvents.addAll(changeLogEvents);
                return super.handleAuditEntry(entryId, user, time, values);
            }
        };
        Long from;
        try
        {
            from = changeLogToken != null ? Long.parseLong(changeLogToken) : null;
        }
        catch (NumberFormatException e)
        {
            throw new CMISInvalidArgumentException("Invalid change log token " + changeLogToken);
        }
        AuditQueryParameters params = new AuditQueryParameters();
        params.setApplicationName(cmisAuditApplicationName);
        params.setForward(true);
        params.setFromId(from);
        // Query one past the last item, so that we know what the next ID is
        int maxAmount = ((null == maxItems) || (0 == maxItems)) ? (0) : (maxItems + 1);
        auditService.auditQuery(changeLogCollectingCallback, params, maxAmount);
        if ((0 != maxAmount) && (changeEvents.size() > maxItems))
        {
            changeEvents.remove(changeEvents.size() - 1);
            result.setNextChangeToken(changeLogCollectingCallback.getEntryId().toString());
            result.setHasMoreItems(true);
        }
        return result;
    }
    /**
     * @see org.alfresco.cmis.CMISChangeLogService#getChangesIncomplete()
     */
    public boolean getChangesIncomplete()
    {
        return true;
    }
    /**
     * @see org.alfresco.cmis.CMISChangeLogService#getLastChangeLogToken()
     */
    public String getLastChangeLogToken()
    {
        EntryIdCallback auditQueryCallback = new EntryIdCallback(false);
        AuditQueryParameters params = new AuditQueryParameters();
        params.setApplicationName(cmisAuditApplicationName);
        params.setForward(false);
        auditService.auditQuery(auditQueryCallback, params, 1);
        return auditQueryCallback.getEntryId();
    }
    /**
     * @see org.alfresco.cmis.CMISChangeLogService#getPreviousPageChangeLogToken(java.lang.String, java.lang.Integer)
     */
    public String getPreviousPageChangeLogToken(String currentPageToken, Integer maxItems)
    {
        if (currentPageToken == null)
        {
            return null;
        }
        maxItems = maxItems == null ? DEFAULT_RETURN_SIZE : maxItems;
        EntryIdCallback auditQueryCallback = new EntryIdCallback(false);
        AuditQueryParameters params = new AuditQueryParameters();
        params.setApplicationName(cmisAuditApplicationName);
        params.setForward(false);
        params.setToId(Long.parseLong(currentPageToken));
        auditService.auditQuery(auditQueryCallback, params, maxItems);
        return auditQueryCallback.getEntryId();
    }
    /**
     * @see org.alfresco.cmis.CMISChangeLogService#getLastPageChangeLogToken(java.lang.String, java.lang.Integer)
     */
    public String getLastPageChangeLogToken(String currentPageToken, Integer maxItems)
    {
        maxItems = maxItems == null ? DEFAULT_RETURN_SIZE : maxItems;
        EntryIdCallback auditQueryCallback = new PageStartEntryIdCallback(maxItems);
        AuditQueryParameters params = new AuditQueryParameters();
        params.setApplicationName(cmisAuditApplicationName);
        if (currentPageToken != null)
        {
            params.setFromId(Long.parseLong(currentPageToken));
        }
        auditService.auditQuery(auditQueryCallback, params, -1);
        return auditQueryCallback.getEntryId();
    }
    /**
     * @see org.alfresco.cmis.CMISChangeLogService#getChangesOnTypeCapability()
     */
    public List getChangesOnTypeCapability()
    {
        if (null == changesOnTypeCapability)
        {
            changesOnTypeCapability = new LinkedList();
        }
        return changesOnTypeCapability;
    }
    /**
     * Converts audit values map to list of CMISChangeEvents.
     * 
     * @param values audit values map
     * @param time audit event time
     * @return list of CMISChangeEvent
     */
    @SuppressWarnings("unchecked")
    private List convertValuesMapToChangeLogEvents(Map values, long time)
    {
        List result = new ArrayList();
        if (values != null && values.size() > 0)
        {
            for (Entry entry : values.entrySet())
            {
                if (entry.getKey() != null && entry.getValue() != null)
                {
                    String path = entry.getKey();
                    CMISChangeType changeType = getCMISChangeType(path);
                    if (changeType != null && entry.getValue() instanceof Map)
                    {
                        Map valueMap = (Map)entry.getValue();
                        result.add(new CMISChangeEventImpl(changeType, new Date(time), (NodeRef) valueMap
                                .get(CMISChangeLogDataExtractor.KEY_NODE_REF), (String) valueMap
                                .get(CMISChangeLogDataExtractor.KEY_OBJECT_ID)));
                    }
                }
            }
        }
        return result;
    }
    /**
     * Gets CMISChangeType by audit path.
     * 
     * @param auditPath audit path
     * @return CMISChangeType
     */
    private CMISChangeType getCMISChangeType(String auditPath)
    {
        CMISChangeType result = null;
        if (auditPath != null)
        {
            if (auditPath.startsWith(PATH_DELIMITER))
            {
                auditPath = auditPath.substring(PATH_DELIMITER.length());
            }
            if (auditPath.startsWith(cmisAuditApplicationName))
            {
                auditPath = auditPath.substring(cmisAuditApplicationName.length());
            }
            if (auditPath.startsWith(PATH_DELIMITER))
            {
                auditPath = auditPath.substring(PATH_DELIMITER.length());
            }
            auditPath = auditPath.toLowerCase();
            for (CMISChangeType changeType : CMISChangeType.values())
            {
                if (auditPath.startsWith(changeType.getLabel()))
                {
                    result = changeType;
                    break;
                }
            }
        }
        return result;
    }
    private class EntryIdCallback implements AuditQueryCallback
    {
        private final boolean valuesRequired;
        private Long entryId;
        public EntryIdCallback(boolean valuesRequired)
        {
            this.valuesRequired = valuesRequired;
        }
        public String getEntryId()
        {
            return entryId == null ? null : entryId.toString();
        }
        public boolean valuesRequired()
        {
            return this.valuesRequired;
        }
        public final boolean handleAuditEntry(Long entryId, String applicationName, String user, long time, Map values)
        {
            if (applicationName.equals(CMISChangeLogServiceImpl.this.cmisAuditApplicationName))
            {
                return handleAuditEntry(entryId, user, time, values);
            }
            return true;
        }
        public boolean handleAuditEntry(Long entryId, String user, long time, Map values)
        {
            this.entryId = entryId;
            return true;
        }
        public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error)
        {
            throw new AlfrescoRuntimeException(errorMsg, error);
        }
    };
    private class PageStartEntryIdCallback extends EntryIdCallback
    {
        private final int pageSize;
        private int indexWithinPage;
        public PageStartEntryIdCallback(int pageSize)
        {
            super(false);
            this.pageSize = pageSize;
            this.indexWithinPage = -1;
        }
        @Override
        public boolean handleAuditEntry(Long entryId, String user, long time, Map values)
        {
            if (++this.indexWithinPage == this.pageSize)
            {
                this.indexWithinPage = 0;
                return super.handleAuditEntry(entryId, user, time, values);
            }
            return true;
        }
    }
}