/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.uberfire.backend.server;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpSession;

import org.jboss.errai.bus.server.annotations.Service;
import org.jboss.errai.bus.server.api.RpcContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.uberfire.backend.server.util.Paths;
import org.uberfire.backend.vfs.Path;
import org.uberfire.backend.vfs.PathFactory;
import org.uberfire.backend.vfs.VFSLockService;
import org.uberfire.backend.vfs.impl.LockInfo;
import org.uberfire.backend.vfs.impl.LockResult;
import org.uberfire.io.IOService;
import org.uberfire.java.nio.IOException;
import org.uberfire.java.nio.file.DirectoryStream.Filter;
import org.uberfire.java.nio.file.FileSystem;
import org.uberfire.java.nio.file.Files;
import org.uberfire.java.nio.file.NoSuchFileException;
import org.uberfire.rpc.SessionInfo;
import org.uberfire.workbench.events.ResourceDeletedEvent;
import org.uberfire.workbench.events.ResourceRenamedEvent;

/**
 * Errai RPC endpoint exposing a {@link VFSLockService}.
 */
@Service
@ApplicationScoped
public class VFSLockServiceImpl implements VFSLockService {

    public static final String LOCK_SESSION_ATTRIBUTE_NAME = "uf-locks";
    private static final Logger logger = LoggerFactory.getLogger(VFSLockServiceImpl.class);

    @Inject
    @Named("configIO")
    private IOService ioService;

    @Inject
    @Named("systemFS")
    private FileSystem fileSystem;

    @Inject
    private SessionInfo sessionInfo;

    @Override
    public LockResult acquireLock(final Path path)
            throws IllegalArgumentException, IOException, UnsupportedOperationException {

        try {
            ioService.startBatch(fileSystem);

            final String userId = sessionInfo.getIdentity().getIdentifier();
            final LockInfo lockInfo = retrieveLockInfo(path);
            final LockResult result;
            if (lockInfo.isLocked() && !lockInfo.lockedBy().equals(userId)) {
                result = LockResult.failed(lockInfo);
            } else {
                ioService.write(Paths.convert(lockInfo.getLock()),
                                userId);
                result = LockResult.acquired(path,
                                             userId);
                updateSession(result.getLockInfo());
            }
            return result;
        } finally {
            ioService.endBatch();
        }
    }

    @Override
    public LockResult releaseLock(final Path path)
            throws IllegalArgumentException, IOException {

        return releaseLock(path,
                           false);
    }

    @Override
    public LockResult forceReleaseLock(final Path path)
            throws IllegalArgumentException, IOException {

        final String userId = sessionInfo.getIdentity().getIdentifier();
        logger.info("User " + userId + " forced a lock release of: " + path.toURI());

        return releaseLock(path,
                           true);
    }

    private LockResult releaseLock(final Path path,
                                   final boolean force)
            throws IllegalArgumentException, IOException {

        try {
            ioService.startBatch(fileSystem);

            final LockInfo lockInfo = retrieveLockInfo(path);
            final LockResult result;
            if (lockInfo.isLocked()) {
                if (sessionInfo.getIdentity().getIdentifier().equals(lockInfo.lockedBy()) || force) {
                    ioService.delete(Paths.convert(lockInfo.getLock()));
                    updateSession(lockInfo,
                                  true);
                    result = LockResult.released(path);
                } else {
                    logger.error("Client requested to release a lock it doesn't hold: " + path.toURI());
                    throw new IOException("Not allowed");
                }
            } else {
                result = LockResult.failed(lockInfo);
            }
            return result;
        } finally {
            ioService.endBatch();
        }
    }

    @Override
    public LockInfo retrieveLockInfo(Path path)
            throws IllegalArgumentException, IOException {

        final Path vfsLock = PathFactory.newLock(path);
        final org.uberfire.java.nio.file.Path realLock = Paths.convert(vfsLock);

        if (ioService.exists(realLock)) {
            try {
                final String lockedBy = ioService.readAllString(realLock);
                return new LockInfo(true,
                                    lockedBy,
                                    path,
                                    vfsLock);
            } catch (NoSuchFileException nsfe) {
                // We want to avoid starting a batch (to ensure cluster-wide consistent reads) here since 
                // this method is invoked very frequently. Therefore it's possible that the lock file
                // was deleted after the check to exists but before readAllString was invoked. There's
                // no need for special exception handling as it simply means that file is no longer locked.
            }
        }
        return new LockInfo(false,
                            null,
                            path,
                            vfsLock);
    }

    @Override
    public List<LockInfo> retrieveLockInfos(Path path,
                                            boolean excludeOwnedLocks)
            throws IllegalArgumentException, IOException {

        if (!Files.isDirectory(Paths.convert(path))) {
            return Collections.emptyList();
        }

        final Path lockPath = PathFactory.newLockPath(path);

        final List<Path> locks = new ArrayList<Path>();
        retrieveLocks(ioService.get(URI.create(lockPath.toURI())),
                      locks);

        final List<LockInfo> lockInfos = new LinkedList<LockInfo>();
        for (Path lock : locks) {
            final LockInfo lockInfo = retrieveLockInfo(PathFactory.fromLock(lock));

            if (!excludeOwnedLocks || !sessionInfo.getIdentity().getIdentifier().equals(lockInfo.lockedBy())) {
                if (Files.exists(Paths.convert(lockInfo.getFile()))) {
                    lockInfos.add(lockInfo);
                }
            }
        }

        return lockInfos;
    }

    private void retrieveLocks(final org.uberfire.java.nio.file.Path path,
                               final List<Path> accu) {

        if (!Files.exists(path)) {
            return;
        }

        Filter<org.uberfire.java.nio.file.Path> filter = new Filter<org.uberfire.java.nio.file.Path>() {

            @Override
            public boolean accept(final org.uberfire.java.nio.file.Path entry) throws org.uberfire.java.nio.IOException {
                if (Paths.convert(entry).toURI().endsWith(PathFactory.LOCK_FILE_EXTENSION)) {
                    accu.add(Paths.convert(entry));
                } else if (Files.isDirectory(entry)) {
                    retrieveLocks(ioService.get(entry.toUri()),
                                  accu);
                }
                return true;
            }
        };

        Iterator<org.uberfire.java.nio.file.Path> it = ioService.newDirectoryStream(path,
                                                                                    filter).iterator();
        while (it.hasNext()) {
            it.next();
        }
    }

    /**
     * Updates the user's session to track all currently held locks so we can
     * release locks on session expiry.
     * @param lockInfo the lock to update
     * @param remove true to remove the lock, false to add it
     */
    private void updateSession(final LockInfo lockInfo,
                               boolean remove) {
        final HttpSession session = RpcContext.getHttpSession();
        if (session != null) {
            @SuppressWarnings("unchecked")
            Set<LockInfo> locks = (Set<LockInfo>) session.getAttribute(LOCK_SESSION_ATTRIBUTE_NAME);

            if (remove) {
                if (locks != null) {
                    locks.remove(lockInfo);
                }
            } else {
                if (locks == null) {
                    locks = new HashSet<LockInfo>();
                }

                locks.add(lockInfo);
                session.setAttribute(LOCK_SESSION_ATTRIBUTE_NAME,
                                     locks);
            }
        }
    }

    private void updateSession(final LockInfo lockInfo) {
        updateSession(lockInfo,
                      false);
    }

    @SuppressWarnings("unused")
    private void onResourceDeleted(@Observes ResourceDeletedEvent res) {
        maybeDeleteLock(res.getPath());
    }

    @SuppressWarnings("unused")
    private void onResourceRenamed(@Observes ResourceRenamedEvent res) {
        maybeDeleteLock(res.getPath());
    }

    private void maybeDeleteLock(final Path path) {
        try {
            ioService.startBatch(fileSystem);

            final LockInfo lockInfo = retrieveLockInfo(path);
            if (lockInfo.isLocked()) {
                ioService.delete(Paths.convert(lockInfo.getLock()));
            }
        } finally {
            ioService.endBatch();
        }
    }
}